feat(projects): 工作台接口切换为真实数据
This commit is contained in:
@@ -131,12 +131,14 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
|||||||
* @param callback callback function
|
* @param callback callback function
|
||||||
*/
|
*/
|
||||||
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
||||||
if (!isRendered()) return;
|
|
||||||
|
|
||||||
const updatedOpts = callback(chartOptions, optionsFactory);
|
const updatedOpts = callback(chartOptions, optionsFactory);
|
||||||
|
|
||||||
Object.assign(chartOptions, updatedOpts);
|
Object.assign(chartOptions, updatedOpts);
|
||||||
|
|
||||||
|
// 图表未初始化(容器尺寸未就绪)时只缓存最新 options,待 render() 初始化时一并应用;
|
||||||
|
// 否则数据先于初始化到达会被静默丢弃,首屏永远停留在空数据
|
||||||
|
if (!isRendered()) return;
|
||||||
|
|
||||||
if (isRendered()) {
|
if (isRendered()) {
|
||||||
chart?.clear();
|
chart?.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
import { normalizeNullableStringId, normalizeStringId } from './shared';
|
import { normalizeNullableStringId, normalizeStringId } from './shared';
|
||||||
|
|
||||||
type ProjectStatusCode = Api.Project.ProjectStatusCode;
|
type ProjectStatusCode = Api.Project.ProjectStatusCode;
|
||||||
@@ -76,6 +77,60 @@ export type MyOwnedProjectResponse = Omit<Api.Project.MyOwnedProjectItem, 'id' |
|
|||||||
members?: MyOwnedProjectMemberResponse[] | null;
|
members?: MyOwnedProjectMemberResponse[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MyTaskResponse = Omit<
|
||||||
|
Api.Project.MyTaskItem,
|
||||||
|
| 'id'
|
||||||
|
| 'projectId'
|
||||||
|
| 'executionId'
|
||||||
|
| 'priority'
|
||||||
|
| 'plannedEndDate'
|
||||||
|
| 'progressRate'
|
||||||
|
| 'createTime'
|
||||||
|
| 'parentTaskId'
|
||||||
|
| 'availableActions'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
projectId: StringIdResponse;
|
||||||
|
executionId?: StringIdResponse | null;
|
||||||
|
priority?: string | number | null;
|
||||||
|
plannedEndDate?: ProjectLocalDateValue;
|
||||||
|
progressRate?: number | string | null;
|
||||||
|
createTime?: string | number | null;
|
||||||
|
parentTaskId?: StringIdResponse | null;
|
||||||
|
availableActions?: LifecycleActionResponse<Api.Project.ProjectTaskActionCode>[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadDistributionItemResponse = Omit<Api.Project.TeamLoadDistributionItem, 'projectId'> & {
|
||||||
|
projectId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadMemberResponse = Omit<Api.Project.TeamLoadMember, 'userId' | 'items'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
items?: TeamLoadDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadResponse = {
|
||||||
|
members?: TeamLoadMemberResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorklogDistributionItemResponse = Omit<Api.Project.WorklogDistributionItem, 'projectId'> & {
|
||||||
|
projectId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyWorklogWeekResponse = Omit<Api.Project.MyWorklogWeekResult, 'dailyHours' | 'distribution'> & {
|
||||||
|
dailyHours?: number[] | null;
|
||||||
|
distribution?: WorklogDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamWorklogWeekMemberResponse = Omit<Api.Project.TeamWorklogWeekMember, 'userId' | 'items'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
items?: WorklogDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamWorklogWeekResponse = Omit<Api.Project.TeamWorklogWeekResult, 'members'> & {
|
||||||
|
members?: TeamWorklogWeekMemberResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ExecutionAssigneeResponse = Omit<Api.Project.ExecutionAssignee, 'id' | 'executionId' | 'userId'> & {
|
export type ExecutionAssigneeResponse = Omit<Api.Project.ExecutionAssignee, 'id' | 'executionId' | 'userId'> & {
|
||||||
id: StringIdResponse;
|
id: StringIdResponse;
|
||||||
executionId: StringIdResponse;
|
executionId: StringIdResponse;
|
||||||
@@ -263,6 +318,28 @@ export function normalizeProjectLocalDate(value: ProjectLocalDateValue | undefin
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后端 LocalDateTime 统一序列化为毫秒时间戳(也可能是数字字符串/格式化字符串),
|
||||||
|
* 归一为 'YYYY-MM-DD HH:mm:ss' 供展示与 dayjs 解析。
|
||||||
|
*/
|
||||||
|
export function normalizeProjectDateTime(value: string | number | null | undefined): string {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: dayjs.Dayjs;
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
parsed = dayjs(value);
|
||||||
|
} else if (/^\d+$/.test(value)) {
|
||||||
|
// 字符串形态的毫秒时间戳:dayjs 无法直接解析,先转数值(时间值非 ID,安全整数范围内)
|
||||||
|
parsed = dayjs(Number(value));
|
||||||
|
} else {
|
||||||
|
parsed = dayjs(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm:ss') : '';
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeLifecycleActions<ActionCode extends string>(
|
export function normalizeLifecycleActions<ActionCode extends string>(
|
||||||
actions: LifecycleActionResponse<ActionCode>[] | null | undefined
|
actions: LifecycleActionResponse<ActionCode>[] | null | undefined
|
||||||
): Api.Project.LifecycleAction<ActionCode>[] {
|
): Api.Project.LifecycleAction<ActionCode>[] {
|
||||||
@@ -296,6 +373,15 @@ function normalizePriority(value: string | number | null | undefined): string {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProgressRate(value: number | string | null | undefined) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeric = typeof value === 'number' ? value : Number(value ?? 0);
|
||||||
|
return Number.isFinite(numeric) ? numeric : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution {
|
export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution {
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
@@ -366,6 +452,75 @@ export function normalizeMyOwnedProject(response: MyOwnedProjectResponse): Api.P
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeMyTask(response: MyTaskResponse): Api.Project.MyTaskItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
projectId: normalizeStringId(response.projectId),
|
||||||
|
executionId: normalizeNullableStringId(response.executionId),
|
||||||
|
executionName: response.executionName ?? null,
|
||||||
|
statusName: response.statusName ?? null,
|
||||||
|
priority: normalizePriority(response.priority),
|
||||||
|
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
|
||||||
|
progressRate: normalizeProgressRate(response.progressRate) ?? 0,
|
||||||
|
createTime: normalizeProjectDateTime(response.createTime),
|
||||||
|
parentTaskId: normalizeNullableStringId(response.parentTaskId),
|
||||||
|
terminal: Boolean(response.terminal),
|
||||||
|
allowEdit: Boolean(response.allowEdit),
|
||||||
|
availableActions: normalizeLifecycleActions(response.availableActions)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorklogDistributionItem(
|
||||||
|
response: WorklogDistributionItemResponse | TeamLoadDistributionItemResponse
|
||||||
|
): { projectId: string | null; projectName: string | null; kind: 'project' | 'personal' | 'other' } {
|
||||||
|
return {
|
||||||
|
projectId: normalizeNullableStringId(response.projectId),
|
||||||
|
projectName: response.projectName ?? null,
|
||||||
|
kind: response.kind
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTeamLoad(response: TeamLoadResponse): Api.Project.TeamLoadResult {
|
||||||
|
return {
|
||||||
|
members: (response.members ?? []).map(member => ({
|
||||||
|
userId: normalizeStringId(member.userId),
|
||||||
|
userNickname: member.userNickname ?? '',
|
||||||
|
items: (member.items ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
count: typeof item.count === 'number' ? item.count : 0
|
||||||
|
})),
|
||||||
|
dueSoonCount: typeof member.dueSoonCount === 'number' ? member.dueSoonCount : 0,
|
||||||
|
overdueCount: typeof member.overdueCount === 'number' ? member.overdueCount : 0
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyWorklogWeek(response: MyWorklogWeekResponse): Api.Project.MyWorklogWeekResult {
|
||||||
|
return {
|
||||||
|
weekStart: response.weekStart ?? '',
|
||||||
|
dailyHours: response.dailyHours ?? [0, 0, 0, 0, 0],
|
||||||
|
distribution: (response.distribution ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
hours: typeof item.hours === 'number' ? item.hours : 0
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTeamWorklogWeek(response: TeamWorklogWeekResponse): Api.Project.TeamWorklogWeekResult {
|
||||||
|
return {
|
||||||
|
weekStart: response.weekStart ?? '',
|
||||||
|
members: (response.members ?? []).map(member => ({
|
||||||
|
userId: normalizeStringId(member.userId),
|
||||||
|
userNickname: member.userNickname ?? '',
|
||||||
|
items: (member.items ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
hours: typeof item.hours === 'number' ? item.hours : 0
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee {
|
export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee {
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
type MyExecutionResponse,
|
type MyExecutionResponse,
|
||||||
type MyOwnedProjectResponse,
|
type MyOwnedProjectResponse,
|
||||||
type MyParticipatedProjectResponse,
|
type MyParticipatedProjectResponse,
|
||||||
|
type MyTaskResponse,
|
||||||
|
type MyWorklogWeekResponse,
|
||||||
type ProjectExecutionResponse,
|
type ProjectExecutionResponse,
|
||||||
type ProjectLocalDateValue,
|
type ProjectLocalDateValue,
|
||||||
type ProjectMemberResponse,
|
type ProjectMemberResponse,
|
||||||
@@ -20,19 +22,25 @@ import {
|
|||||||
type TaskAssigneeFromApiResponse,
|
type TaskAssigneeFromApiResponse,
|
||||||
type TaskAssigneeLogResponse,
|
type TaskAssigneeLogResponse,
|
||||||
type TaskWorklogResponse,
|
type TaskWorklogResponse,
|
||||||
|
type TeamLoadResponse,
|
||||||
|
type TeamWorklogWeekResponse,
|
||||||
getProjectLifecycleActions,
|
getProjectLifecycleActions,
|
||||||
normalizeExecutionAssignee,
|
normalizeExecutionAssignee,
|
||||||
normalizeExecutionAssigneeLog,
|
normalizeExecutionAssigneeLog,
|
||||||
normalizeMyExecution,
|
normalizeMyExecution,
|
||||||
normalizeMyOwnedProject,
|
normalizeMyOwnedProject,
|
||||||
normalizeMyParticipatedProject,
|
normalizeMyParticipatedProject,
|
||||||
|
normalizeMyTask,
|
||||||
|
normalizeMyWorklogWeek,
|
||||||
normalizeProjectExecution,
|
normalizeProjectExecution,
|
||||||
normalizeProjectLocalDate,
|
normalizeProjectLocalDate,
|
||||||
normalizeProjectMember,
|
normalizeProjectMember,
|
||||||
normalizeProjectTask,
|
normalizeProjectTask,
|
||||||
normalizeTaskAssignee,
|
normalizeTaskAssignee,
|
||||||
normalizeTaskAssigneeLog,
|
normalizeTaskAssigneeLog,
|
||||||
normalizeTaskWorklog
|
normalizeTaskWorklog,
|
||||||
|
normalizeTeamLoad,
|
||||||
|
normalizeTeamWorklogWeek
|
||||||
} from './project-shared';
|
} from './project-shared';
|
||||||
|
|
||||||
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
|
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
|
||||||
@@ -440,6 +448,57 @@ export async function fetchGetMyOwnedProjectPage(params?: Api.Project.MyProjectS
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我的任务」(跨项目聚合,负责人/在岗协办人口径,只返回非终态;隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyTaskPage(params?: Api.Project.MyTaskSearchParams) {
|
||||||
|
type MyTaskPageResponse = Api.Project.PageResult<MyTaskResponse>;
|
||||||
|
const result = await request<MyTaskPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/tasks/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyTaskPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyTask)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「团队负载」(团队 = 当前用户 + 管理链路直接下级,members[0] 恒为当前用户) */
|
||||||
|
export async function fetchGetMyTeamLoad() {
|
||||||
|
const result = await request<TeamLoadResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/team-load`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamLoadResponse>, normalizeTeamLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我的工时周聚合」(weekStart 传任意日期,后端归一到所在周周一;逐日工时为均摊推算值) */
|
||||||
|
export async function fetchGetMyWorklogWeek(params: Api.Project.WorklogWeekParams) {
|
||||||
|
const result = await request<MyWorklogWeekResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/worklog-week`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyWorklogWeekResponse>, normalizeMyWorklogWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「团队工时周聚合」(成员集合与团队负载同口径;周标准工时后端不返回,前端落常量) */
|
||||||
|
export async function fetchGetTeamWorklogWeek(params: Api.Project.WorklogWeekParams) {
|
||||||
|
const result = await request<TeamWorklogWeekResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/team-worklog-week`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamWorklogWeekResponse>, normalizeTeamWorklogWeek);
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取项目执行状态看板 */
|
/** 获取项目执行状态看板 */
|
||||||
export function fetchGetProjectExecutionStatusBoard(
|
export function fetchGetProjectExecutionStatusBoard(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|||||||
106
src/typings/api/project.d.ts
vendored
106
src/typings/api/project.d.ts
vendored
@@ -405,6 +405,112 @@ declare namespace Api {
|
|||||||
members: MyOwnedProjectMember[];
|
members: MyOwnedProjectMember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 工作台「我的任务」(跨项目)查询入参 */
|
||||||
|
type MyTaskSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
/** 身份过滤:owner 我负责 / collaborator 我协办;缺省 = 两者并集 */
|
||||||
|
involveType: 'owner' | 'collaborator';
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 工作台「我的任务」单项(跨项目;当前用户为负责人或在岗协办人,接口只返回非终态任务) */
|
||||||
|
interface MyTaskItem {
|
||||||
|
/** 任务 ID(雪花 ID,字符串) */
|
||||||
|
id: string;
|
||||||
|
taskTitle: string;
|
||||||
|
/** 所属项目 */
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
/** 所属执行,未挂执行为 null */
|
||||||
|
executionId: string | null;
|
||||||
|
executionName: string | null;
|
||||||
|
/** 任务状态:pending / active / paused(非终态) */
|
||||||
|
statusCode: ProjectTaskStatusCode;
|
||||||
|
statusName: string | null;
|
||||||
|
/** 优先级字典 value(rdms_req_priority,"0"~"3",数字越小越高) */
|
||||||
|
priority: string;
|
||||||
|
/** 计划结束日期(YYYY-MM-DD),可空 */
|
||||||
|
plannedEndDate: string | null;
|
||||||
|
/** 任务进度(0-100);后端定稿直接返回,无进度明确返 0 */
|
||||||
|
progressRate: number;
|
||||||
|
/** 创建时间(YYYY-MM-DD HH:mm:ss;后端返毫秒时间戳,适配层归一) */
|
||||||
|
createTime: string;
|
||||||
|
/** 我的角色:owner 负责人 / collaborator 协办人;双重身份只返 owner */
|
||||||
|
myRole: 'owner' | 'collaborator';
|
||||||
|
/** 父任务 ID(字符串),一级任务为 null */
|
||||||
|
parentTaskId: string | null;
|
||||||
|
/** 是否终态;本接口只返非终态任务,正常恒为 false */
|
||||||
|
terminal: boolean;
|
||||||
|
/** 当前状态是否允许编辑任务 */
|
||||||
|
allowEdit: boolean;
|
||||||
|
/** 当前登录用户可执行的生命周期动作(与任务详情同口径;auto_start 不返回),无动作为 [] */
|
||||||
|
availableActions: LifecycleAction<ProjectTaskActionCode>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队负载」分布子项(kind != project 时 projectId / projectName 为 null) */
|
||||||
|
interface TeamLoadDistributionItem {
|
||||||
|
projectId: string | null;
|
||||||
|
projectName: string | null;
|
||||||
|
/** project 项目任务 / personal 个人事项 / other 无法归类的残留 */
|
||||||
|
kind: 'project' | 'personal' | 'other';
|
||||||
|
/** 未完成任务数(含待开始/已暂停) */
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队负载」成员(members[0] 恒为当前用户) */
|
||||||
|
interface TeamLoadMember {
|
||||||
|
/** 用户 ID(字符串) */
|
||||||
|
userId: string;
|
||||||
|
userNickname: string;
|
||||||
|
/** 未完成任务按归属分布,无任务为 [] */
|
||||||
|
items: TeamLoadDistributionItem[];
|
||||||
|
/** 临期:今天 ≤ 计划结束 ≤ 今天+3 天,且未完成(与逾期互斥) */
|
||||||
|
dueSoonCount: number;
|
||||||
|
/** 逾期:计划结束 < 今天,且未完成 */
|
||||||
|
overdueCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队负载」响应(GET /project/project/me/team-load,团队 = 自己 + 管理链路直接下级) */
|
||||||
|
interface TeamLoadResult {
|
||||||
|
members: TeamLoadMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台工时分布子项(kind != project 时 projectId / projectName 为 null;hours=0 的行后端不输出) */
|
||||||
|
interface WorklogDistributionItem {
|
||||||
|
projectId: string | null;
|
||||||
|
projectName: string | null;
|
||||||
|
kind: 'project' | 'personal' | 'other';
|
||||||
|
hours: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「我的工时周聚合」响应(GET /project/project/me/worklog-week) */
|
||||||
|
interface MyWorklogWeekResult {
|
||||||
|
/** 归一后的周一日期 YYYY-MM-DD */
|
||||||
|
weekStart: string;
|
||||||
|
/** 周一~周五逐日工时(固定 5 元素;均摊推算值,周末份额归周五) */
|
||||||
|
dailyHours: number[];
|
||||||
|
/** 本周工时按归属分布,hours 降序 */
|
||||||
|
distribution: WorklogDistributionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队工时周聚合」成员(members[0] 恒为当前用户;该周未填报成员 items 为 []) */
|
||||||
|
interface TeamWorklogWeekMember {
|
||||||
|
userId: string;
|
||||||
|
userNickname: string;
|
||||||
|
items: WorklogDistributionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队工时周聚合」响应(GET /project/project/me/team-worklog-week;周标准工时后端不返回,前端落常量 35) */
|
||||||
|
interface TeamWorklogWeekResult {
|
||||||
|
weekStart: string;
|
||||||
|
members: TeamWorklogWeekMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台工时周聚合查询入参(weekStart 传任意日期,后端归一到所在周周一) */
|
||||||
|
interface WorklogWeekParams {
|
||||||
|
weekStart: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 创建执行入参(含 ownerId + assigneeUserIds) */
|
/** 创建执行入参(含 ownerId + assigneeUserIds) */
|
||||||
interface CreateProjectExecutionParams {
|
interface CreateProjectExecutionParams {
|
||||||
executionName: string;
|
executionName: string;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
export type WorkbenchTrend = 'up' | 'down' | 'flat';
|
|
||||||
|
|
||||||
export type WorkbenchTodoCategory = 'task' | 'ticket' | 'personal' | 'approval';
|
export type WorkbenchTodoCategory = 'task' | 'ticket' | 'personal' | 'approval';
|
||||||
|
|
||||||
export type WorkbenchTodoMainTab = 'all' | WorkbenchTodoCategory;
|
export type WorkbenchTodoMainTab = 'all' | WorkbenchTodoCategory;
|
||||||
@@ -10,40 +8,6 @@ export type WorkbenchTodoDeadlineFilter = 'overdue' | 'today' | 'week' | null;
|
|||||||
|
|
||||||
export type WorkbenchTodoPriority = 'high' | 'mid' | 'low';
|
export type WorkbenchTodoPriority = 'high' | 'mid' | 'low';
|
||||||
|
|
||||||
export interface WorkbenchKpiSource {
|
|
||||||
/** 待办 */
|
|
||||||
todo: {
|
|
||||||
count: number;
|
|
||||||
diffFromYesterday: number;
|
|
||||||
};
|
|
||||||
/** 我负责的任务 */
|
|
||||||
task: {
|
|
||||||
count: number;
|
|
||||||
diffFromYesterday: number;
|
|
||||||
};
|
|
||||||
/** 我参与的项目 */
|
|
||||||
project: {
|
|
||||||
count: number;
|
|
||||||
activeCount: number;
|
|
||||||
};
|
|
||||||
/** 我负责的需求 */
|
|
||||||
requirement: {
|
|
||||||
count: number;
|
|
||||||
pendingReview: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkbenchKpiCard {
|
|
||||||
key: 'todo' | 'task' | 'project' | 'requirement';
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
trend: WorkbenchTrend;
|
|
||||||
trendText: string;
|
|
||||||
hint: string;
|
|
||||||
icon: string;
|
|
||||||
tone: 'sky' | 'emerald' | 'amber' | 'rose';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkbenchTodoItemSource {
|
export interface WorkbenchTodoItemSource {
|
||||||
id: string;
|
id: string;
|
||||||
category: WorkbenchTodoCategory;
|
category: WorkbenchTodoCategory;
|
||||||
@@ -54,12 +18,18 @@ export interface WorkbenchTodoItemSource {
|
|||||||
deadline: string | null;
|
deadline: string | null;
|
||||||
/** 来源(提交人/项目名/工单号) */
|
/** 来源(提交人/项目名/工单号) */
|
||||||
source: string;
|
source: string;
|
||||||
|
/** 进度百分比,仅任务和个人事项使用 */
|
||||||
|
progressRate?: number | null;
|
||||||
/** 优先级,用于前端排序与高亮 */
|
/** 优先级,用于前端排序与高亮 */
|
||||||
priority?: WorkbenchTodoPriority;
|
priority?: WorkbenchTodoPriority;
|
||||||
|
/** 优先级原始标签(字典 label,如 P0~P3),有值才渲染角标;不做高/中/低翻译 */
|
||||||
|
priorityLabel?: string | null;
|
||||||
/** 是否逾期 */
|
/** 是否逾期 */
|
||||||
overdue?: boolean;
|
overdue?: boolean;
|
||||||
/** 跳转路由 key(可选,未配则不跳转) */
|
/** 跳转路由 key(可选,未配则不跳转) */
|
||||||
routeKey?: string;
|
routeKey?: string;
|
||||||
|
/** 任务条目携带:所属项目 ID,点击带对象上下文跳进该项目的任务导航 */
|
||||||
|
projectId?: string;
|
||||||
/** 审批业务类型 */
|
/** 审批业务类型 */
|
||||||
approvalBizType?: 'overtime_application' | string;
|
approvalBizType?: 'overtime_application' | string;
|
||||||
/** 审批业务 ID */
|
/** 审批业务 ID */
|
||||||
@@ -74,26 +44,6 @@ export interface WorkbenchTodoItem extends Omit<WorkbenchTodoItemSource, 'deadli
|
|||||||
categoryTone: 'sky' | 'emerald' | 'amber' | 'rose' | 'violet';
|
categoryTone: 'sky' | 'emerald' | 'amber' | 'rose' | 'violet';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchActivityItemSource {
|
|
||||||
id: string;
|
|
||||||
actor: string;
|
|
||||||
action: string;
|
|
||||||
/** 目标对象(需求/任务/工单/项目名等) */
|
|
||||||
target: string;
|
|
||||||
/** 目标对象的种类,用于颜色 */
|
|
||||||
targetKind: 'requirement' | 'task' | 'ticket' | 'project' | 'product';
|
|
||||||
/** 时间,ISO */
|
|
||||||
time: string;
|
|
||||||
/** 是否 @ 了当前用户 */
|
|
||||||
mentioned?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkbenchActivityItem extends Omit<WorkbenchActivityItemSource, 'time'> {
|
|
||||||
timeLabel: string;
|
|
||||||
relativeLabel: string;
|
|
||||||
tone: 'sky' | 'emerald' | 'amber' | 'rose' | 'violet';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 「我参与的项目」展示项(由 Api.Project.MyParticipatedProjectItem 衍生) */
|
/** 「我参与的项目」展示项(由 Api.Project.MyParticipatedProjectItem 衍生) */
|
||||||
export interface WorkbenchParticipatedProjectView {
|
export interface WorkbenchParticipatedProjectView {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -123,14 +73,6 @@ const todoPriorityWeight: Record<WorkbenchTodoPriority, number> = {
|
|||||||
low: 2
|
low: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
const activityToneMap: Record<WorkbenchActivityItemSource['targetKind'], WorkbenchActivityItem['tone']> = {
|
|
||||||
requirement: 'sky',
|
|
||||||
task: 'emerald',
|
|
||||||
ticket: 'amber',
|
|
||||||
project: 'violet',
|
|
||||||
product: 'rose'
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 列表只含进行中项目;按已知状态编码上色,未知回退 sky */
|
/** 列表只含进行中项目;按已知状态编码上色,未知回退 sky */
|
||||||
function resolveParticipatedProjectTone(statusCode: string): 'sky' | 'emerald' | 'amber' {
|
function resolveParticipatedProjectTone(statusCode: string): 'sky' | 'emerald' | 'amber' {
|
||||||
if (statusCode === 'active') return 'emerald';
|
if (statusCode === 'active') return 'emerald';
|
||||||
@@ -143,25 +85,6 @@ function clampPercent(value: number) {
|
|||||||
return Math.min(100, Math.max(0, Math.round(value)));
|
return Math.min(100, Math.max(0, Math.round(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRelative(value: string) {
|
|
||||||
const target = dayjs(value);
|
|
||||||
if (!target.isValid()) return '--';
|
|
||||||
|
|
||||||
const now = dayjs();
|
|
||||||
const diffMinutes = now.diff(target, 'minute');
|
|
||||||
|
|
||||||
if (diffMinutes < 1) return '刚刚';
|
|
||||||
if (diffMinutes < 60) return `${diffMinutes} 分钟前`;
|
|
||||||
|
|
||||||
const diffHours = now.diff(target, 'hour');
|
|
||||||
if (diffHours < 24) return `${diffHours} 小时前`;
|
|
||||||
|
|
||||||
const diffDays = now.diff(target, 'day');
|
|
||||||
if (diffDays < 7) return `${diffDays} 天前`;
|
|
||||||
|
|
||||||
return target.format('MM-DD HH:mm');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDeadline(value: string | null) {
|
function formatDeadline(value: string | null) {
|
||||||
if (!value) return '不限';
|
if (!value) return '不限';
|
||||||
const target = dayjs(value);
|
const target = dayjs(value);
|
||||||
@@ -187,60 +110,6 @@ function getRemainingDays(value: string | null) {
|
|||||||
return target.startOf('day').diff(dayjs().startOf('day'), 'day');
|
return target.startOf('day').diff(dayjs().startOf('day'), 'day');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWorkbenchKpiCards(source: WorkbenchKpiSource): WorkbenchKpiCard[] {
|
|
||||||
function diffTrend(diff: number): { trend: WorkbenchTrend; text: string } {
|
|
||||||
if (diff > 0) return { trend: 'up', text: `较昨日 +${diff}` };
|
|
||||||
if (diff < 0) return { trend: 'down', text: `较昨日 ${diff}` };
|
|
||||||
return { trend: 'flat', text: '与昨日持平' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const todoTrend = diffTrend(source.todo.diffFromYesterday);
|
|
||||||
const taskTrend = diffTrend(source.task.diffFromYesterday);
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
key: 'todo',
|
|
||||||
label: '我的待办',
|
|
||||||
value: source.todo.count,
|
|
||||||
trend: todoTrend.trend,
|
|
||||||
trendText: todoTrend.text,
|
|
||||||
hint: '需要我处理的评审、任务、工单与 @ 提醒合计',
|
|
||||||
icon: 'mdi:clipboard-text-clock-outline',
|
|
||||||
tone: 'sky'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'task',
|
|
||||||
label: '我负责的任务',
|
|
||||||
value: source.task.count,
|
|
||||||
trend: taskTrend.trend,
|
|
||||||
trendText: taskTrend.text,
|
|
||||||
hint: '当前由我负责的进行中任务',
|
|
||||||
icon: 'mdi:checkbox-marked-circle-outline',
|
|
||||||
tone: 'emerald'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'project',
|
|
||||||
label: '我参与的项目',
|
|
||||||
value: source.project.count,
|
|
||||||
trend: 'flat',
|
|
||||||
trendText: `进行中 ${source.project.activeCount} 个`,
|
|
||||||
hint: '我作为成员参与的项目总数',
|
|
||||||
icon: 'mdi:briefcase-outline',
|
|
||||||
tone: 'amber'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'requirement',
|
|
||||||
label: '我负责的需求',
|
|
||||||
value: source.requirement.count,
|
|
||||||
trend: 'flat',
|
|
||||||
trendText: `待评审 ${source.requirement.pendingReview} 项`,
|
|
||||||
hint: '由我担任产品经理或负责人的需求',
|
|
||||||
icon: 'mdi:file-document-multiple-outline',
|
|
||||||
tone: 'rose'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildWorkbenchTodoItems(source: readonly WorkbenchTodoItemSource[]): WorkbenchTodoItem[] {
|
export function buildWorkbenchTodoItems(source: readonly WorkbenchTodoItemSource[]): WorkbenchTodoItem[] {
|
||||||
return [...source]
|
return [...source]
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
@@ -301,18 +170,6 @@ export function sortWorkbenchTodoItemsByPriority(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWorkbenchActivityItems(source: readonly WorkbenchActivityItemSource[]): WorkbenchActivityItem[] {
|
|
||||||
return [...source]
|
|
||||||
.filter(item => dayjs(item.time).isValid())
|
|
||||||
.sort((left, right) => dayjs(right.time).valueOf() - dayjs(left.time).valueOf())
|
|
||||||
.map(item => ({
|
|
||||||
...item,
|
|
||||||
timeLabel: dayjs(item.time).format('YYYY-MM-DD HH:mm'),
|
|
||||||
relativeLabel: formatRelative(item.time),
|
|
||||||
tone: activityToneMap[item.targetKind]
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildWorkbenchParticipatedProjects(
|
export function buildWorkbenchParticipatedProjects(
|
||||||
source: readonly Api.Project.MyParticipatedProjectItem[]
|
source: readonly Api.Project.MyParticipatedProjectItem[]
|
||||||
): WorkbenchParticipatedProjectView[] {
|
): WorkbenchParticipatedProjectView[] {
|
||||||
@@ -389,37 +246,16 @@ export interface WorkbenchWorklogDistributionItem {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单周工时数据源(按天填 / 按周填两类共存) */
|
/** 周标准工时:系统无配置项,后端不返回,产品确认前端落常量 35(不是 40) */
|
||||||
export interface WorkbenchWeekWorklogSource {
|
export const WORKBENCH_WEEK_TARGET_HOURS = 35;
|
||||||
/** ISO 周首日(周一),YYYY-MM-DD */
|
|
||||||
weekStart: string;
|
|
||||||
/** 整周一笔填的工时总和(将按工作日 5 等分均摊到周一~周五) */
|
|
||||||
weeklyFilledHours: number;
|
|
||||||
/** 周一~周五按天填的工时,长度恒为 5 */
|
|
||||||
dailyHours: [number, number, number, number, number];
|
|
||||||
/** 工时分布;前端不再做截断,超出 5 行靠容器滚动 */
|
|
||||||
distribution: WorkbenchWorklogDistributionItem[];
|
|
||||||
/** 周目标小时数(默认 40) */
|
|
||||||
target: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 双周工时 mock 容器:本周 + 上周 */
|
/** 单周工时视图(builder 衍生;逐日工时为后端按填报日期段均摊到工作日的推算值) */
|
||||||
export interface WorkbenchMyWeekWorklogSource {
|
|
||||||
current: WorkbenchWeekWorklogSource;
|
|
||||||
previous: WorkbenchWeekWorklogSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 单周工时视图(builder 衍生) */
|
|
||||||
export interface WorkbenchWeekWorklogView {
|
export interface WorkbenchWeekWorklogView {
|
||||||
weekStart: string;
|
weekStart: string;
|
||||||
weekLabel: string;
|
weekLabel: string;
|
||||||
/** 周一~周五按天填部分(5 长度) */
|
/** 周一~周五逐日工时(5 长度,均摊推算值,周末份额归周五) */
|
||||||
dailyByDay: number[];
|
dailyHours: number[];
|
||||||
/** 周一~周五按周均分部分(5 长度,每项 = weeklyFilledHours / 5) */
|
/** 本周累计 */
|
||||||
dailyByWeekAvg: number[];
|
|
||||||
/** 周一~周五合计(5 长度,dailyByDay + dailyByWeekAvg) */
|
|
||||||
dailyTotal: number[];
|
|
||||||
/** 累计(含按周) */
|
|
||||||
totalHours: number;
|
totalHours: number;
|
||||||
target: number;
|
target: number;
|
||||||
/** 累计 - 目标;正=领先、负=落后 */
|
/** 累计 - 目标;正=领先、负=落后 */
|
||||||
@@ -435,29 +271,37 @@ function roundHours(value: number) {
|
|||||||
return Math.round(value * 10) / 10;
|
return Math.round(value * 10) / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWorkbenchWeekWorklogView(source: WorkbenchWeekWorklogSource): WorkbenchWeekWorklogView {
|
/** 契约分布项 → 展示行项(key/label 归一,kind != project 时用固定字面量) */
|
||||||
|
function toWorklogDistributionItem(item: Api.Project.WorklogDistributionItem): WorkbenchWorklogDistributionItem {
|
||||||
|
const isProject = item.kind === 'project' && Boolean(item.projectId);
|
||||||
|
return {
|
||||||
|
key: isProject ? (item.projectId as string) : item.kind,
|
||||||
|
label: item.projectName ?? (item.kind === 'personal' ? '个人事项' : '其他'),
|
||||||
|
hours: roundHours(item.hours),
|
||||||
|
kind: item.kind,
|
||||||
|
projectId: isProject ? (item.projectId as string) : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWorkbenchWeekWorklogView(source: Api.Project.MyWorklogWeekResult): WorkbenchWeekWorklogView {
|
||||||
const start = dayjs(source.weekStart);
|
const start = dayjs(source.weekStart);
|
||||||
const weekLabel = start.isValid() ? `${start.isoWeekYear()}年第${start.isoWeek()}周` : source.weekStart;
|
const weekLabel = start.isValid() ? `${start.isoWeekYear()}年第${start.isoWeek()}周` : source.weekStart;
|
||||||
|
|
||||||
const weekAvg = roundHours(source.weeklyFilledHours / WEEKDAY_COUNT);
|
const dailyHours = Array.from({ length: WEEKDAY_COUNT }, (_, i) => roundHours(source.dailyHours[i] ?? 0));
|
||||||
const dailyByWeekAvg = Array.from({ length: WEEKDAY_COUNT }, () => weekAvg);
|
const totalHours = roundHours(dailyHours.reduce((s, h) => s + h, 0));
|
||||||
const dailyByDay = source.dailyHours.map(roundHours);
|
const target = WORKBENCH_WEEK_TARGET_HOURS;
|
||||||
const dailyTotal = dailyByDay.map((h, i) => roundHours(h + dailyByWeekAvg[i]));
|
const delta = roundHours(totalHours - target);
|
||||||
const totalHours = roundHours(dailyTotal.reduce((s, h) => s + h, 0));
|
const completionRate = target > 0 ? clampPercent((totalHours / target) * 100) : 0;
|
||||||
const delta = roundHours(totalHours - source.target);
|
|
||||||
const completionRate = source.target > 0 ? clampPercent((totalHours / source.target) * 100) : 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
weekStart: source.weekStart,
|
weekStart: source.weekStart,
|
||||||
weekLabel,
|
weekLabel,
|
||||||
dailyByDay,
|
dailyHours,
|
||||||
dailyByWeekAvg,
|
|
||||||
dailyTotal,
|
|
||||||
totalHours,
|
totalHours,
|
||||||
target: source.target,
|
target,
|
||||||
delta,
|
delta,
|
||||||
completionRate,
|
completionRate,
|
||||||
distribution: source.distribution
|
distribution: source.distribution.map(toWorklogDistributionItem)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,31 +314,10 @@ export function getGreeting(hour: number = dayjs().hour()) {
|
|||||||
return '夜深了';
|
return '夜深了';
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 团队工时分布(C12 · teamWorklog) ===
|
// === 团队工时分布(D16 团队 tab,原 C12 teamWorklog) ===
|
||||||
|
|
||||||
export type WorkbenchTeamWorklogItemKind = 'project' | 'personal' | 'other';
|
export type WorkbenchTeamWorklogItemKind = 'project' | 'personal' | 'other';
|
||||||
|
|
||||||
export interface WorkbenchTeamWorklogItem {
|
|
||||||
/** 项目 id 或 'personal' / 'other' */
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
hours: number;
|
|
||||||
kind: WorkbenchTeamWorklogItemKind;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkbenchTeamWorklogMemberSource {
|
|
||||||
memberId: string;
|
|
||||||
memberName: string;
|
|
||||||
items: WorkbenchTeamWorklogItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkbenchTeamWorklogSource {
|
|
||||||
weekStart: string;
|
|
||||||
/** 单人周应填工时(用于填报率口径) */
|
|
||||||
expectedHoursPerMember: number;
|
|
||||||
members: WorkbenchTeamWorklogMemberSource[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 团队工时分布视图(builder 衍生) */
|
/** 团队工时分布视图(builder 衍生) */
|
||||||
export interface WorkbenchTeamWorklogView {
|
export interface WorkbenchTeamWorklogView {
|
||||||
weekStart: string;
|
weekStart: string;
|
||||||
@@ -512,27 +335,36 @@ export interface WorkbenchTeamWorklogView {
|
|||||||
totalHours: number;
|
totalHours: number;
|
||||||
/** 团队人均工时 */
|
/** 团队人均工时 */
|
||||||
averageHours: number;
|
averageHours: number;
|
||||||
/** 应填工时合计(人数 × expectedHoursPerMember) */
|
/** 应填工时合计(人数 × WORKBENCH_WEEK_TARGET_HOURS) */
|
||||||
expectedTotalHours: number;
|
expectedTotalHours: number;
|
||||||
/** 填报率 0-100 整数(= 总工时 / 应填工时) */
|
/** 填报率 0-100 整数(= 总工时 / 应填工时) */
|
||||||
fillRate: number;
|
fillRate: number;
|
||||||
/** 偏低人数:< 团队均值 × 0.8 */
|
/** 偏低人数:< 团队均值 × 0.8 */
|
||||||
lowCount: number;
|
lowCount: number;
|
||||||
/** 加班人数:> 45h(应填 × 1.125) */
|
/** 加班人数:> 周标准 × 1.125 */
|
||||||
highCount: number;
|
highCount: number;
|
||||||
|
/** 加班判定阈值(小时),KPI 文案展示用 */
|
||||||
|
overtimeThreshold: number;
|
||||||
/** 工时最低成员(用于底部小结) */
|
/** 工时最低成员(用于底部小结) */
|
||||||
lowest: { memberName: string; hours: number } | null;
|
lowest: { memberName: string; hours: number } | null;
|
||||||
/** 工时最高成员(用于底部小结) */
|
/** 工时最高成员(用于底部小结) */
|
||||||
highest: { memberName: string; hours: number } | null;
|
highest: { memberName: string; hours: number } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource): WorkbenchTeamWorklogView {
|
export function buildWorkbenchTeamWorklogView(source: Api.Project.TeamWorklogWeekResult): WorkbenchTeamWorklogView {
|
||||||
const start = dayjs(source.weekStart);
|
const start = dayjs(source.weekStart);
|
||||||
const weekLabel = start.isValid() ? `${start.isoWeekYear()}年第${start.isoWeek()}周` : source.weekStart;
|
const weekLabel = start.isValid() ? `${start.isoWeekYear()}年第${start.isoWeek()}周` : source.weekStart;
|
||||||
|
|
||||||
|
// 契约:members[0] 恒为当前用户,展示加「(我)」后缀
|
||||||
|
const members = source.members.map((member, index) => ({
|
||||||
|
memberId: member.userId,
|
||||||
|
memberName: index === 0 ? `${member.userNickname}(我)` : member.userNickname,
|
||||||
|
items: member.items.map(toWorklogDistributionItem)
|
||||||
|
}));
|
||||||
|
|
||||||
// 列保序去重:按成员遍历顺序首次出现即入列;项目优先、个人/其他按出现顺序追加
|
// 列保序去重:按成员遍历顺序首次出现即入列;项目优先、个人/其他按出现顺序追加
|
||||||
const categoryMap = new Map<string, { key: string; label: string; kind: WorkbenchTeamWorklogItemKind }>();
|
const categoryMap = new Map<string, { key: string; label: string; kind: WorkbenchTeamWorklogItemKind }>();
|
||||||
for (const m of source.members) {
|
for (const m of members) {
|
||||||
for (const it of m.items) {
|
for (const it of m.items) {
|
||||||
if (!categoryMap.has(it.key)) {
|
if (!categoryMap.has(it.key)) {
|
||||||
categoryMap.set(it.key, { key: it.key, label: it.label, kind: it.kind });
|
categoryMap.set(it.key, { key: it.key, label: it.label, kind: it.kind });
|
||||||
@@ -541,7 +373,7 @@ export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource
|
|||||||
}
|
}
|
||||||
const categories = Array.from(categoryMap.values());
|
const categories = Array.from(categoryMap.values());
|
||||||
|
|
||||||
const memberView = source.members.map(m => ({
|
const memberView = members.map(m => ({
|
||||||
memberId: m.memberId,
|
memberId: m.memberId,
|
||||||
memberName: m.memberName,
|
memberName: m.memberName,
|
||||||
totalHours: roundHours(m.items.reduce((s, it) => s + it.hours, 0))
|
totalHours: roundHours(m.items.reduce((s, it) => s + it.hours, 0))
|
||||||
@@ -549,7 +381,7 @@ export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource
|
|||||||
|
|
||||||
const seriesMatrix = categories.map(cat => ({
|
const seriesMatrix = categories.map(cat => ({
|
||||||
...cat,
|
...cat,
|
||||||
data: source.members.map(m => {
|
data: members.map(m => {
|
||||||
const hit = m.items.find(it => it.key === cat.key);
|
const hit = m.items.find(it => it.key === cat.key);
|
||||||
return hit ? roundHours(hit.hours) : 0;
|
return hit ? roundHours(hit.hours) : 0;
|
||||||
})
|
})
|
||||||
@@ -558,11 +390,11 @@ export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource
|
|||||||
const totalHours = roundHours(memberView.reduce((s, m) => s + m.totalHours, 0));
|
const totalHours = roundHours(memberView.reduce((s, m) => s + m.totalHours, 0));
|
||||||
const memberCount = memberView.length;
|
const memberCount = memberView.length;
|
||||||
const averageHours = memberCount > 0 ? roundHours(totalHours / memberCount) : 0;
|
const averageHours = memberCount > 0 ? roundHours(totalHours / memberCount) : 0;
|
||||||
const expectedTotalHours = memberCount * source.expectedHoursPerMember;
|
const expectedTotalHours = memberCount * WORKBENCH_WEEK_TARGET_HOURS;
|
||||||
const fillRate = expectedTotalHours > 0 ? clampPercent((totalHours / expectedTotalHours) * 100) : 0;
|
const fillRate = expectedTotalHours > 0 ? clampPercent((totalHours / expectedTotalHours) * 100) : 0;
|
||||||
|
|
||||||
const lowThreshold = averageHours * 0.8;
|
const lowThreshold = averageHours * 0.8;
|
||||||
const highThreshold = source.expectedHoursPerMember * 1.125;
|
const highThreshold = WORKBENCH_WEEK_TARGET_HOURS * 1.125;
|
||||||
const lowCount = memberView.filter(m => m.totalHours < lowThreshold).length;
|
const lowCount = memberView.filter(m => m.totalHours < lowThreshold).length;
|
||||||
const highCount = memberView.filter(m => m.totalHours > highThreshold).length;
|
const highCount = memberView.filter(m => m.totalHours > highThreshold).length;
|
||||||
|
|
||||||
@@ -585,6 +417,7 @@ export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource
|
|||||||
fillRate,
|
fillRate,
|
||||||
lowCount,
|
lowCount,
|
||||||
highCount,
|
highCount,
|
||||||
|
overtimeThreshold: roundHours(highThreshold),
|
||||||
lowest,
|
lowest,
|
||||||
highest
|
highest
|
||||||
};
|
};
|
||||||
@@ -594,31 +427,29 @@ export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource
|
|||||||
|
|
||||||
export type WorkbenchTeamLoadLevel = 'high' | 'mid' | 'normal';
|
export type WorkbenchTeamLoadLevel = 'high' | 'mid' | 'normal';
|
||||||
|
|
||||||
export type WorkbenchTeamLoadItemKind = 'project' | 'personal';
|
export type WorkbenchTeamLoadItemKind = 'project' | 'personal' | 'other';
|
||||||
|
|
||||||
export interface WorkbenchTeamLoadItemSource {
|
export interface WorkbenchTeamLoadItemSource {
|
||||||
/** projectId 或 'personal' */
|
/** projectId 或 'personal' / 'other' */
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
kind: WorkbenchTeamLoadItemKind;
|
kind: WorkbenchTeamLoadItemKind;
|
||||||
/** 该项目/个人事项下,该成员进行中的任务/事项数(任务按"负责人/协办人"口径,单任务多人各算 1 条) */
|
/** 该项目/个人事项下,该成员未完成的任务/事项数(含待开始/已暂停;任务按"负责人/协办人"口径,单任务多人各算 1 条) */
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchTeamLoadMemberSource {
|
export interface WorkbenchTeamLoadMemberSource {
|
||||||
memberId: string;
|
memberId: string;
|
||||||
memberName: string;
|
memberName: string;
|
||||||
/** 进行中按项目 + 个人事项的拆分(合计即"进行中总数") */
|
/** 未完成数按项目 + 个人事项的拆分(合计即"未完成总数") */
|
||||||
items: WorkbenchTeamLoadItemSource[];
|
items: WorkbenchTeamLoadItemSource[];
|
||||||
/** 今天 ≤ 计划结束 ≤ 今天+3 天 且未完成 */
|
/** 今天 ≤ 计划结束 ≤ 今天+3 天 且未完成(与逾期互斥) */
|
||||||
dueSoon: number;
|
dueSoon: number;
|
||||||
/** 计划结束 < 今天 且未完成 */
|
/** 计划结束 < 今天 且未完成 */
|
||||||
overdue: number;
|
overdue: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchTeamLoadSource {
|
export interface WorkbenchTeamLoadSource {
|
||||||
/** 数据快照所属周(与 C12 对齐口径) */
|
|
||||||
weekStart: string;
|
|
||||||
members: WorkbenchTeamLoadMemberSource[];
|
members: WorkbenchTeamLoadMemberSource[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,7 +459,7 @@ export interface WorkbenchTeamLoadSegment extends WorkbenchTeamLoadItemSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchTeamLoadMember extends Omit<WorkbenchTeamLoadMemberSource, 'items'> {
|
export interface WorkbenchTeamLoadMember extends Omit<WorkbenchTeamLoadMemberSource, 'items'> {
|
||||||
/** items 合计 */
|
/** items 合计(未完成总数) */
|
||||||
inProgress: number;
|
inProgress: number;
|
||||||
/** dueSoon + overdue */
|
/** dueSoon + overdue */
|
||||||
urgent: number;
|
urgent: number;
|
||||||
@@ -642,7 +473,6 @@ export interface WorkbenchTeamLoadMember extends Omit<WorkbenchTeamLoadMemberSou
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchTeamLoadView {
|
export interface WorkbenchTeamLoadView {
|
||||||
weekStart: string;
|
|
||||||
/** 已按 level → inProgress desc 排序,高负载置顶 */
|
/** 已按 level → inProgress desc 排序,高负载置顶 */
|
||||||
members: WorkbenchTeamLoadMember[];
|
members: WorkbenchTeamLoadMember[];
|
||||||
/** 柱子量程上限(固定值,避免某个成员极端值把全员压扁) */
|
/** 柱子量程上限(固定值,避免某个成员极端值把全员压扁) */
|
||||||
@@ -709,7 +539,6 @@ export function buildWorkbenchTeamLoadView(source: WorkbenchTeamLoadSource): Wor
|
|||||||
const urgentTotal = enriched.reduce((s, m) => s + m.urgent, 0);
|
const urgentTotal = enriched.reduce((s, m) => s + m.urgent, 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
weekStart: source.weekStart,
|
|
||||||
members: enriched,
|
members: enriched,
|
||||||
scaleMax,
|
scaleMax,
|
||||||
highCount,
|
highCount,
|
||||||
@@ -741,19 +570,6 @@ export function buildWorkbenchProjectHealthCards(
|
|||||||
return source.map(s => ({ ...s, healthLabel: labelMap[s.health] }));
|
return source.map(s => ({ ...s, healthLabel: labelMap[s.health] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchProgressBarSource {
|
|
||||||
projectId: string;
|
|
||||||
projectName: string;
|
|
||||||
/** 完成率 0-100 */
|
|
||||||
weekCompletionRate: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WorkbenchProgressBar = WorkbenchProgressBarSource;
|
|
||||||
|
|
||||||
export function buildWorkbenchProgressBars(source: readonly WorkbenchProgressBarSource[]): WorkbenchProgressBar[] {
|
|
||||||
return source.map(s => ({ ...s, weekCompletionRate: Math.min(100, Math.max(0, Math.round(s.weekCompletionRate))) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 前端兜底过滤:剔除已完成 / 已取消 / 进度满 100 的执行(默认不在工作台呈现)。
|
* 前端兜底过滤:剔除已完成 / 已取消 / 进度满 100 的执行(默认不在工作台呈现)。
|
||||||
* 后端接口已按此口径过滤,此处为双保险;泛型保证接口返回类型可复用。
|
* 后端接口已按此口径过滤,此处为双保险;泛型保证接口返回类型可复用。
|
||||||
|
|||||||
@@ -1,423 +1,7 @@
|
|||||||
import dayjs from 'dayjs';
|
import type { WorkbenchProjectHealthCardSource } from './homepage';
|
||||||
import type {
|
|
||||||
WorkbenchActivityItemSource,
|
|
||||||
WorkbenchKpiSource,
|
|
||||||
WorkbenchMyWeekWorklogSource,
|
|
||||||
WorkbenchProgressBarSource,
|
|
||||||
WorkbenchProjectHealthCardSource,
|
|
||||||
WorkbenchTeamLoadSource,
|
|
||||||
WorkbenchTeamWorklogSource,
|
|
||||||
WorkbenchTodoItemSource
|
|
||||||
} from './homepage';
|
|
||||||
|
|
||||||
const now = dayjs();
|
|
||||||
const iso = (date: dayjs.Dayjs) => date.format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
|
|
||||||
export const workbenchKpiMock = {
|
|
||||||
todo: { count: 8, diffFromYesterday: 1 },
|
|
||||||
task: { count: 14, diffFromYesterday: -1 },
|
|
||||||
project: { count: 3, activeCount: 2 },
|
|
||||||
requirement: { count: 6, pendingReview: 2 }
|
|
||||||
} satisfies WorkbenchKpiSource;
|
|
||||||
|
|
||||||
export const workbenchTodoMock = [
|
|
||||||
{
|
|
||||||
id: 'todo-1',
|
|
||||||
category: 'task',
|
|
||||||
title: '支付回调接口联调遗留问题处理',
|
|
||||||
createdTime: iso(now.subtract(7, 'day').hour(9).minute(20)),
|
|
||||||
deadline: iso(now.subtract(1, 'day').hour(17).minute(30)),
|
|
||||||
source: '收银台 V3 · 后端联调',
|
|
||||||
priority: 'high',
|
|
||||||
overdue: true,
|
|
||||||
routeKey: 'project_list'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-2',
|
|
||||||
category: 'ticket',
|
|
||||||
title: '工单 #1018 用户登录异常',
|
|
||||||
createdTime: iso(now.subtract(6, 'day').hour(11).minute(0)),
|
|
||||||
deadline: iso(now.subtract(2, 'day').hour(10).minute(0)),
|
|
||||||
source: '客户支持 · 紧急',
|
|
||||||
priority: 'mid',
|
|
||||||
overdue: true,
|
|
||||||
routeKey: 'ticket'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-3',
|
|
||||||
category: 'approval',
|
|
||||||
title: '李四 · 第 21 周周报待审批',
|
|
||||||
createdTime: iso(now.subtract(2, 'day').hour(18).minute(20)),
|
|
||||||
deadline: null,
|
|
||||||
source: '周报 · 产品组',
|
|
||||||
priority: 'mid',
|
|
||||||
routeKey: 'workbench'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-4',
|
|
||||||
category: 'personal',
|
|
||||||
title: '提交昨日工时与本周计划',
|
|
||||||
createdTime: iso(now.subtract(4, 'day').hour(8).minute(30)),
|
|
||||||
deadline: iso(now.hour(18).minute(0)),
|
|
||||||
source: '个人事项 · 工时',
|
|
||||||
priority: 'mid',
|
|
||||||
routeKey: 'personal-center_my-item'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-5',
|
|
||||||
category: 'task',
|
|
||||||
title: '会员等级提示文案最终校对',
|
|
||||||
createdTime: iso(now.subtract(3, 'day').hour(10).minute(10)),
|
|
||||||
deadline: iso(now.hour(20).minute(0)),
|
|
||||||
source: '会员中心 · 文案',
|
|
||||||
priority: 'high',
|
|
||||||
routeKey: 'project_list'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-6',
|
|
||||||
category: 'ticket',
|
|
||||||
title: '工单 #1024 客户反馈待处理',
|
|
||||||
createdTime: iso(now.subtract(3, 'day').hour(15).minute(0)),
|
|
||||||
deadline: iso(now.add(2, 'day').hour(17).minute(0)),
|
|
||||||
source: '王五 · 客户反馈',
|
|
||||||
priority: 'mid',
|
|
||||||
routeKey: 'ticket'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-7',
|
|
||||||
category: 'personal',
|
|
||||||
title: '复盘上周交付遗留并归档',
|
|
||||||
createdTime: iso(now.subtract(2, 'day').hour(9).minute(0)),
|
|
||||||
deadline: iso(now.add(4, 'day').hour(18).minute(0)),
|
|
||||||
source: '个人事项 · 复盘',
|
|
||||||
priority: 'low',
|
|
||||||
routeKey: 'personal-center_my-item'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-8',
|
|
||||||
category: 'task',
|
|
||||||
title: '订单中心结算文档评审',
|
|
||||||
createdTime: iso(now.subtract(2, 'day').hour(13).minute(20)),
|
|
||||||
deadline: iso(now.add(5, 'day').hour(15).minute(0)),
|
|
||||||
source: '订单中心 · 文档',
|
|
||||||
priority: 'mid',
|
|
||||||
routeKey: 'project_list'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-9',
|
|
||||||
category: 'approval',
|
|
||||||
title: '张三 · 5 月加班 12h 申请待审批',
|
|
||||||
createdTime: iso(now.subtract(1, 'day').hour(17).minute(0)),
|
|
||||||
deadline: null,
|
|
||||||
source: '加班申请 · 研发组',
|
|
||||||
priority: 'mid',
|
|
||||||
routeKey: 'workbench'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-10',
|
|
||||||
category: 'personal',
|
|
||||||
title: '安排下周外出培训行程',
|
|
||||||
createdTime: iso(now.subtract(1, 'day').hour(19).minute(40)),
|
|
||||||
deadline: iso(now.add(10, 'day').hour(12).minute(0)),
|
|
||||||
source: '个人事项 · 行程',
|
|
||||||
priority: 'low',
|
|
||||||
routeKey: 'personal-center_my-item'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-11',
|
|
||||||
category: 'task',
|
|
||||||
title: '会员中心首页骨架屏改造',
|
|
||||||
createdTime: iso(now.subtract(20, 'hour')),
|
|
||||||
deadline: iso(now.add(12, 'day').hour(18).minute(0)),
|
|
||||||
source: '会员中心 · 前端',
|
|
||||||
priority: 'low',
|
|
||||||
routeKey: 'project_list'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'todo-12',
|
|
||||||
category: 'ticket',
|
|
||||||
title: '工单 #1031 提示信息文案优化',
|
|
||||||
createdTime: iso(now.subtract(8, 'hour')),
|
|
||||||
deadline: iso(now.add(14, 'day').hour(17).minute(0)),
|
|
||||||
source: '客户支持 · 普通',
|
|
||||||
priority: 'low',
|
|
||||||
routeKey: 'ticket'
|
|
||||||
}
|
|
||||||
] satisfies WorkbenchTodoItemSource[];
|
|
||||||
|
|
||||||
export const workbenchActivityMock = [
|
|
||||||
{
|
|
||||||
id: 'act-1',
|
|
||||||
actor: '张三',
|
|
||||||
action: '提交了需求评审申请',
|
|
||||||
target: '订单导出 V2',
|
|
||||||
targetKind: 'requirement',
|
|
||||||
time: iso(now.subtract(10, 'minute'))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'act-2',
|
|
||||||
actor: '李四',
|
|
||||||
action: '完成了任务',
|
|
||||||
target: '支付回调接口联调',
|
|
||||||
targetKind: 'task',
|
|
||||||
time: iso(now.subtract(2, 'hour'))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'act-3',
|
|
||||||
actor: '李四',
|
|
||||||
action: '在需求中 @ 了你',
|
|
||||||
target: '会员等级',
|
|
||||||
targetKind: 'requirement',
|
|
||||||
time: iso(now.subtract(1, 'day').hour(17).minute(23)),
|
|
||||||
mentioned: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'act-4',
|
|
||||||
actor: '王五',
|
|
||||||
action: '提交了工单',
|
|
||||||
target: '#1024 · 客户反馈',
|
|
||||||
targetKind: 'ticket',
|
|
||||||
time: iso(now.subtract(1, 'day').hour(15).minute(8))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'act-5',
|
|
||||||
actor: '赵六',
|
|
||||||
action: '把项目状态调整为',
|
|
||||||
target: '试运行(订单中心)',
|
|
||||||
targetKind: 'project',
|
|
||||||
time: iso(now.subtract(2, 'day').hour(10).minute(0))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'act-6',
|
|
||||||
actor: '系统',
|
|
||||||
action: '提醒:你有 1 项任务即将逾期',
|
|
||||||
target: 'API 返回结构调整评审',
|
|
||||||
targetKind: 'requirement',
|
|
||||||
time: iso(now.subtract(3, 'day').hour(9).minute(30))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'act-7',
|
|
||||||
actor: '钱七',
|
|
||||||
action: '更新了产品资料',
|
|
||||||
target: '收银台 V3 · 定位说明',
|
|
||||||
targetKind: 'product',
|
|
||||||
time: iso(now.subtract(4, 'day').hour(16).minute(45))
|
|
||||||
}
|
|
||||||
] satisfies WorkbenchActivityItemSource[];
|
|
||||||
|
|
||||||
const currentWeekStart = now.startOf('isoWeek').format('YYYY-MM-DD');
|
|
||||||
const previousWeekStart = now.subtract(1, 'week').startOf('isoWeek').format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
export const workbenchMyWeekWorklogMock = {
|
|
||||||
current: {
|
|
||||||
weekStart: currentWeekStart,
|
|
||||||
weeklyFilledHours: 5,
|
|
||||||
dailyHours: [4, 7, 6, 8, 7.5],
|
|
||||||
target: 40,
|
|
||||||
distribution: [
|
|
||||||
{ key: 'prj-mall-v2', label: '商城 V2 升级', hours: 12.5, kind: 'project', projectId: 'prj-mall-v2' },
|
|
||||||
{ key: 'prj-risk', label: '风控引擎接入', hours: 8, kind: 'project', projectId: 'prj-risk' },
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', hours: 6, kind: 'project', projectId: 'prj-cashier' },
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 5, kind: 'project', projectId: 'prj-order' },
|
|
||||||
{ key: 'prj-member', label: '会员中心', hours: 3, kind: 'project', projectId: 'prj-member' },
|
|
||||||
{ key: 'prj-marketing', label: '营销中台', hours: 2, kind: 'project', projectId: 'prj-marketing' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 4, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 2, kind: 'other' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
previous: {
|
|
||||||
weekStart: previousWeekStart,
|
|
||||||
weeklyFilledHours: 0,
|
|
||||||
dailyHours: [8, 8, 7, 8, 7],
|
|
||||||
target: 40,
|
|
||||||
distribution: [
|
|
||||||
{ key: 'prj-mall-v2', label: '商城 V2 升级', hours: 15, kind: 'project', projectId: 'prj-mall-v2' },
|
|
||||||
{ key: 'prj-risk', label: '风控引擎接入', hours: 9, kind: 'project', projectId: 'prj-risk' },
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', hours: 7, kind: 'project', projectId: 'prj-cashier' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 5, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 2, kind: 'other' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
} satisfies WorkbenchMyWeekWorklogSource;
|
|
||||||
|
|
||||||
// 团队工时口径约定:「团队 = 当前用户所在团队,含自己」。
|
|
||||||
// 后端 /workbench/team-worklog 接口返回的 members 必须包含当前用户自己——
|
|
||||||
// 这样没有下级的人(普通员工)切到「团队工时」tab 也至少有自己这一条数据,
|
|
||||||
// 不会出现空白态。约定 members[0] = 当前用户,UI 用「(我)」后缀标识。
|
|
||||||
export const workbenchTeamWorklogMock = {
|
|
||||||
current: {
|
|
||||||
weekStart: currentWeekStart,
|
|
||||||
expectedHoursPerMember: 40,
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
memberId: 'u-1',
|
|
||||||
memberName: '张三(我)',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', hours: 22, kind: 'project' },
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 10, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 4, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 2, kind: 'other' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-2',
|
|
||||||
memberName: '李四',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', hours: 18, kind: 'project' },
|
|
||||||
{ key: 'prj-member', label: '会员中心', hours: 20, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 3, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 1, kind: 'other' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-3',
|
|
||||||
memberName: '王五',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-member', label: '会员中心', hours: 14, kind: 'project' },
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 12, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 2, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 2, kind: 'other' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-4',
|
|
||||||
memberName: '赵六',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', hours: 24, kind: 'project' },
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 18, kind: 'project' },
|
|
||||||
{ key: 'prj-member', label: '会员中心', hours: 4, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 2, kind: 'personal' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-5',
|
|
||||||
memberName: '钱七',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 14, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 6, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 5, kind: 'other' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
previous: {
|
|
||||||
weekStart: previousWeekStart,
|
|
||||||
expectedHoursPerMember: 40,
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
memberId: 'u-1',
|
|
||||||
memberName: '张三(我)',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', hours: 26, kind: 'project' },
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 8, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 3, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 3, kind: 'other' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-2',
|
|
||||||
memberName: '李四',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', hours: 15, kind: 'project' },
|
|
||||||
{ key: 'prj-member', label: '会员中心', hours: 22, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 4, kind: 'personal' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-3',
|
|
||||||
memberName: '王五',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-member', label: '会员中心', hours: 16, kind: 'project' },
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 10, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 5, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 3, kind: 'other' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-4',
|
|
||||||
memberName: '赵六',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', hours: 20, kind: 'project' },
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 16, kind: 'project' },
|
|
||||||
{ key: 'prj-member', label: '会员中心', hours: 6, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 3, kind: 'personal' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-5',
|
|
||||||
memberName: '钱七',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-order', label: '订单中心', hours: 16, kind: 'project' },
|
|
||||||
{ key: 'personal', label: '个人事项', hours: 4, kind: 'personal' },
|
|
||||||
{ key: 'other', label: '其他', hours: 4, kind: 'other' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
} satisfies { current: WorkbenchTeamWorklogSource; previous: WorkbenchTeamWorklogSource };
|
|
||||||
|
|
||||||
// 项目 key 与 workbenchTeamWorklogMock 对齐,保证跨 widget 项目色一致
|
|
||||||
export const workbenchTeamLoadMock = {
|
|
||||||
weekStart: now.startOf('isoWeek').format('YYYY-MM-DD'),
|
|
||||||
members: [
|
|
||||||
// 高负载:进行中 7 或 临期+逾期 ≥ 2
|
|
||||||
{
|
|
||||||
memberId: 'u-1',
|
|
||||||
memberName: '张三',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', kind: 'project', count: 4 },
|
|
||||||
{ key: 'prj-order', label: '订单中心', kind: 'project', count: 2 },
|
|
||||||
{ key: 'personal', label: '个人事项', kind: 'personal', count: 1 }
|
|
||||||
],
|
|
||||||
dueSoon: 2,
|
|
||||||
overdue: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-4',
|
|
||||||
memberName: '赵六',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', kind: 'project', count: 2 },
|
|
||||||
{ key: 'prj-order', label: '订单中心', kind: 'project', count: 2 },
|
|
||||||
{ key: 'prj-member', label: '会员中心', kind: 'project', count: 1 }
|
|
||||||
],
|
|
||||||
dueSoon: 1,
|
|
||||||
overdue: 2
|
|
||||||
},
|
|
||||||
// 中负载:进行中 ≥ 4 或 临期+逾期 ≥ 1
|
|
||||||
{
|
|
||||||
memberId: 'u-2',
|
|
||||||
memberName: '李四',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-cashier', label: '收银台 V3', kind: 'project', count: 2 },
|
|
||||||
{ key: 'prj-member', label: '会员中心', kind: 'project', count: 2 }
|
|
||||||
],
|
|
||||||
dueSoon: 1,
|
|
||||||
overdue: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 'u-3',
|
|
||||||
memberName: '王五',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-member', label: '会员中心', kind: 'project', count: 2 },
|
|
||||||
{ key: 'prj-order', label: '订单中心', kind: 'project', count: 1 }
|
|
||||||
],
|
|
||||||
dueSoon: 1,
|
|
||||||
overdue: 0
|
|
||||||
},
|
|
||||||
// 正常
|
|
||||||
{
|
|
||||||
memberId: 'u-5',
|
|
||||||
memberName: '钱七',
|
|
||||||
items: [
|
|
||||||
{ key: 'prj-order', label: '订单中心', kind: 'project', count: 1 },
|
|
||||||
{ key: 'personal', label: '个人事项', kind: 'personal', count: 1 }
|
|
||||||
],
|
|
||||||
dueSoon: 0,
|
|
||||||
overdue: 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} satisfies WorkbenchTeamLoadSource;
|
|
||||||
|
|
||||||
|
// projectHealth 为「演示态保留」widget(RDMS 业务暂无风险登记 / 计划基线流程,不接真实数据),
|
||||||
|
// 本 mock 是其唯一数据源;其余 widget 已全部接通真实接口,对应 mock 已清除。
|
||||||
export const workbenchProjectHealthMock = [
|
export const workbenchProjectHealthMock = [
|
||||||
{
|
{
|
||||||
projectId: 'prj-1',
|
projectId: 'prj-1',
|
||||||
@@ -447,9 +31,3 @@ export const workbenchProjectHealthMock = [
|
|||||||
backlogRequirements: 6
|
backlogRequirements: 6
|
||||||
}
|
}
|
||||||
] satisfies WorkbenchProjectHealthCardSource[];
|
] satisfies WorkbenchProjectHealthCardSource[];
|
||||||
|
|
||||||
export const workbenchProgressChartMock = [
|
|
||||||
{ projectId: 'prj-1', projectName: '收银台 V3', weekCompletionRate: 78 },
|
|
||||||
{ projectId: 'prj-2', projectName: '会员中心', weekCompletionRate: 62 },
|
|
||||||
{ projectId: 'prj-3', projectName: '订单中心', weekCompletionRate: 45 }
|
|
||||||
] satisfies WorkbenchProgressBarSource[];
|
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, onActivated, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||||
|
import { fetchGetMyWorklogWeek, fetchGetTeamWorklogWeek } from '@/service/api';
|
||||||
import { type ECOption, useEcharts } from '@/hooks/common/echarts';
|
import { type ECOption, useEcharts } from '@/hooks/common/echarts';
|
||||||
import { getWorkbenchItemColor } from '../composables/use-workbench-colors';
|
import { getWorkbenchItemColor } from '../composables/use-workbench-colors';
|
||||||
import {
|
import {
|
||||||
type WorkbenchTeamWorklogView,
|
|
||||||
type WorkbenchWeekWorklogView,
|
|
||||||
type WorkbenchWorklogDistributionItem,
|
type WorkbenchWorklogDistributionItem,
|
||||||
buildWorkbenchTeamWorklogView,
|
buildWorkbenchTeamWorklogView,
|
||||||
buildWorkbenchWeekWorklogView
|
buildWorkbenchWeekWorklogView
|
||||||
} from '../homepage';
|
} from '../homepage';
|
||||||
import { workbenchMyWeekWorklogMock, workbenchTeamWorklogMock } from '../mock';
|
|
||||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
|
||||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'WorkbenchMyWeekWorklog' });
|
defineOptions({ name: 'WorkbenchMyWeekWorklog' });
|
||||||
@@ -26,7 +23,8 @@ defineEmits<{ (e: 'hide'): void }>();
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { loading, refresh } = useWorkbenchRefresh();
|
const myWeekData = ref<Api.Project.MyWorklogWeekResult | null>(null);
|
||||||
|
const teamWeekData = ref<Api.Project.TeamWorklogWeekResult | null>(null);
|
||||||
|
|
||||||
// EP type='week' 默认 firstDayOfWeek=7,从日历点选时返回当周"周日"。
|
// EP type='week' 默认 firstDayOfWeek=7,从日历点选时返回当周"周日"。
|
||||||
// 我们按 ISO 周(周一-周日)存储;遇到周日 +1 天再 startOf('isoWeek'),避免回退到上一周。
|
// 我们按 ISO 周(周一-周日)存储;遇到周日 +1 天再 startOf('isoWeek'),避免回退到上一周。
|
||||||
@@ -50,25 +48,68 @@ const selectedWeekStart = computed(() => {
|
|||||||
return aligned ? aligned.format('YYYY-MM-DD') : '';
|
return aligned ? aligned.format('YYYY-MM-DD') : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 周切换必须重拉,不能被"并发守卫"拦掉,故不走 useWorkbenchRefresh:
|
||||||
|
// 自管 loading + 请求序号,旧响应(慢请求落后于新一次切周)直接丢弃
|
||||||
|
const loading = ref(false);
|
||||||
|
let requestSeq = 0;
|
||||||
|
|
||||||
|
async function loadWorklogWeek() {
|
||||||
|
const weekStart = selectedWeekStart.value;
|
||||||
|
if (!weekStart) return;
|
||||||
|
|
||||||
|
requestSeq += 1;
|
||||||
|
const seq = requestSeq;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const [myResult, teamResult] = await Promise.all([
|
||||||
|
fetchGetMyWorklogWeek({ weekStart }),
|
||||||
|
fetchGetTeamWorklogWeek({ weekStart })
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (seq !== requestSeq) return;
|
||||||
|
myWeekData.value = myResult.error || !myResult.data ? null : myResult.data;
|
||||||
|
teamWeekData.value = teamResult.error || !teamResult.data ? null : teamResult.data;
|
||||||
|
} finally {
|
||||||
|
if (seq === requestSeq) {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
loadWorklogWeek();
|
||||||
|
}
|
||||||
|
|
||||||
type TabKey = 'my' | 'team';
|
type TabKey = 'my' | 'team';
|
||||||
const activeTab = ref<TabKey>('my');
|
const activeTab = ref<TabKey>('my');
|
||||||
|
|
||||||
// ============ 我的工时 ============
|
// 周切换(含初始)拉取两 tab 数据;竞态由 loadWorklogWeek 内请求序号兜底
|
||||||
|
watch(selectedWeekStart, loadWorklogWeek, { immediate: true });
|
||||||
|
|
||||||
const myView = computed<WorkbenchWeekWorklogView | null>(() => {
|
// 工作台路由 keepAlive:切回时组件不重挂载,immediate watch 不再触发。
|
||||||
if (!selectedWeekStart.value) return null;
|
// 每次激活归位到当前周并重拉;首次激活与挂载同拍(上面 immediate 已拉过),跳过避免双发
|
||||||
if (selectedWeekStart.value === workbenchMyWeekWorklogMock.current.weekStart) {
|
let activatedOnce = false;
|
||||||
return buildWorkbenchWeekWorklogView(workbenchMyWeekWorklogMock.current);
|
onActivated(() => {
|
||||||
|
if (!activatedOnce) {
|
||||||
|
activatedOnce = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (selectedWeekStart.value === workbenchMyWeekWorklogMock.previous.weekStart) {
|
const currentWeekDate = dayjs().startOf('isoWeek');
|
||||||
return buildWorkbenchWeekWorklogView(workbenchMyWeekWorklogMock.previous);
|
if (selectedWeekStart.value === currentWeekDate.format('YYYY-MM-DD')) {
|
||||||
|
// 周未变时 watch 不会触发,手动重拉取最新填报
|
||||||
|
loadWorklogWeek();
|
||||||
|
} else {
|
||||||
|
selectedWeekDate.value = currentWeekDate.toDate();
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCurrentWeek = computed(() => selectedWeekStart.value === workbenchMyWeekWorklogMock.current.weekStart);
|
// ============ 我的工时 ============
|
||||||
|
|
||||||
const totalLabel = computed(() => (isCurrentWeek.value ? '累计' : '上周累计'));
|
const myView = computed(() => (myWeekData.value ? buildWorkbenchWeekWorklogView(myWeekData.value) : null));
|
||||||
|
|
||||||
|
const isCurrentWeek = computed(() => selectedWeekStart.value === dayjs().startOf('isoWeek').format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
const totalLabel = computed(() => (isCurrentWeek.value ? '累计' : '该周累计'));
|
||||||
|
|
||||||
const deltaInfo = computed(() => {
|
const deltaInfo = computed(() => {
|
||||||
if (!myView.value) return null;
|
if (!myView.value) return null;
|
||||||
@@ -80,9 +121,7 @@ const deltaInfo = computed(() => {
|
|||||||
return { text: `达成 ${completionRate}%`, tone: completionRate >= 100 ? ('success' as const) : ('muted' as const) };
|
return { text: `达成 ${completionRate}%`, tone: completionRate >= 100 ? ('success' as const) : ('muted' as const) };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 每日柱图的"按天/按周"分色(与项目色无关,保留本地常量)
|
|
||||||
const DAY_BAR_COLOR = '#409EFF';
|
const DAY_BAR_COLOR = '#409EFF';
|
||||||
const WEEK_BAR_COLOR = '#A0CFFF';
|
|
||||||
|
|
||||||
interface DistributionRow extends WorkbenchWorklogDistributionItem {
|
interface DistributionRow extends WorkbenchWorklogDistributionItem {
|
||||||
color: string;
|
color: string;
|
||||||
@@ -155,10 +194,8 @@ function buildMyBarOption(): ECOption {
|
|||||||
formatter: (rawParams: any) => {
|
formatter: (rawParams: any) => {
|
||||||
const params = Array.isArray(rawParams) ? rawParams : [rawParams];
|
const params = Array.isArray(rawParams) ? rawParams : [rawParams];
|
||||||
const dayName = params[0]?.axisValue ?? '';
|
const dayName = params[0]?.axisValue ?? '';
|
||||||
const dayPart = params.find((p: any) => p.seriesName === '按天填')?.value ?? 0;
|
const total = Number(params[0]?.value ?? 0);
|
||||||
const weekPart = params.find((p: any) => p.seriesName === '按周均分')?.value ?? 0;
|
return `周${dayName}:${total}h`;
|
||||||
const total = Number(dayPart) + Number(weekPart);
|
|
||||||
return `周${dayName}:${total}h<br/>按天填 ${dayPart}h<br/>按周均分 ${weekPart}h`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: { left: 28, right: 8, top: 16, bottom: 24, containLabel: false },
|
grid: { left: 28, right: 8, top: 16, bottom: 24, containLabel: false },
|
||||||
@@ -176,20 +213,11 @@ function buildMyBarOption(): ECOption {
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '按天填',
|
name: '每日工时',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
stack: 'total',
|
|
||||||
barWidth: 18,
|
barWidth: 18,
|
||||||
data: v?.dailyByDay ?? [],
|
data: v?.dailyHours ?? [],
|
||||||
itemStyle: { color: DAY_BAR_COLOR, borderRadius: [0, 0, 2, 2] }
|
itemStyle: { color: DAY_BAR_COLOR, borderRadius: [2, 2, 0, 0] }
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '按周均分',
|
|
||||||
type: 'bar',
|
|
||||||
stack: 'total',
|
|
||||||
barWidth: 18,
|
|
||||||
data: v?.dailyByWeekAvg ?? [],
|
|
||||||
itemStyle: { color: WEEK_BAR_COLOR, borderRadius: [2, 2, 0, 0] }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -208,16 +236,7 @@ const { domRef: myBarRef, updateOptions: updateMyBar } = useEcharts(buildMyBarOp
|
|||||||
|
|
||||||
// ============ 团队工时 ============
|
// ============ 团队工时 ============
|
||||||
|
|
||||||
const teamView = computed<WorkbenchTeamWorklogView | null>(() => {
|
const teamView = computed(() => (teamWeekData.value ? buildWorkbenchTeamWorklogView(teamWeekData.value) : null));
|
||||||
if (!selectedWeekStart.value) return null;
|
|
||||||
if (selectedWeekStart.value === workbenchTeamWorklogMock.current.weekStart) {
|
|
||||||
return buildWorkbenchTeamWorklogView(workbenchTeamWorklogMock.current);
|
|
||||||
}
|
|
||||||
if (selectedWeekStart.value === workbenchTeamWorklogMock.previous.weekStart) {
|
|
||||||
return buildWorkbenchTeamWorklogView(workbenchTeamWorklogMock.previous);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const teamSeriesWithColor = computed(() =>
|
const teamSeriesWithColor = computed(() =>
|
||||||
(teamView.value?.seriesMatrix ?? []).map(s => ({ ...s, color: getWorkbenchItemColor(s.key, s.kind) }))
|
(teamView.value?.seriesMatrix ?? []).map(s => ({ ...s, color: getWorkbenchItemColor(s.key, s.kind) }))
|
||||||
@@ -339,6 +358,9 @@ watch(activeTab, async tab => {
|
|||||||
<div class="ww-section-title">
|
<div class="ww-section-title">
|
||||||
<SvgIcon icon="mdi:calendar-week" class="ww-section-icon" />
|
<SvgIcon icon="mdi:calendar-week" class="ww-section-icon" />
|
||||||
<span>每日工时</span>
|
<span>每日工时</span>
|
||||||
|
<ElTooltip content="系统按填报日期段均摊到工作日的推算值(周末份额计入周五),非逐日实填" placement="top">
|
||||||
|
<SvgIcon icon="mdi:information-outline" class="ww-section-info" />
|
||||||
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -351,16 +373,6 @@ watch(activeTab, async tab => {
|
|||||||
|
|
||||||
<div class="ww-block">
|
<div class="ww-block">
|
||||||
<div ref="myBarRef" class="ww-bar" />
|
<div ref="myBarRef" class="ww-bar" />
|
||||||
<div class="ww-bar-legend">
|
|
||||||
<span class="ww-bar-legend__item">
|
|
||||||
<span class="ww-bar-legend__swatch" :style="{ background: DAY_BAR_COLOR }" />
|
|
||||||
按天填
|
|
||||||
</span>
|
|
||||||
<span class="ww-bar-legend__item">
|
|
||||||
<span class="ww-bar-legend__swatch" :style="{ background: WEEK_BAR_COLOR }" />
|
|
||||||
按周均分
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -410,7 +422,7 @@ watch(activeTab, async tab => {
|
|||||||
{{ teamView.highCount }}
|
{{ teamView.highCount }}
|
||||||
<span class="tw-kpi__unit">人</span>
|
<span class="tw-kpi__unit">人</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="tw-kpi__sub">超 45h</span>
|
<span class="tw-kpi__sub">超 {{ teamView.overtimeThreshold }}h</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -523,6 +535,12 @@ watch(activeTab, async tab => {
|
|||||||
color: var(--el-text-color-placeholder);
|
color: var(--el-text-color-placeholder);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.ww-section-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
.ww-pie-wrap {
|
.ww-pie-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -540,26 +558,6 @@ watch(activeTab, async tab => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
.ww-bar-legend {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.ww-bar-legend__item {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.ww-bar-legend__swatch {
|
|
||||||
display: inline-block;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ww-footer {
|
.ww-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { fetchGetMyTeamLoad } from '@/service/api';
|
||||||
import { getWorkbenchItemColor } from '../composables/use-workbench-colors';
|
import { getWorkbenchItemColor } from '../composables/use-workbench-colors';
|
||||||
import { type WorkbenchTeamLoadLevel, buildWorkbenchTeamLoadView } from '../homepage';
|
import {
|
||||||
import { workbenchTeamLoadMock } from '../mock';
|
type WorkbenchTeamLoadLevel,
|
||||||
|
type WorkbenchTeamLoadMemberSource,
|
||||||
|
buildWorkbenchTeamLoadView
|
||||||
|
} from '../homepage';
|
||||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||||
|
|
||||||
@@ -14,21 +18,40 @@ interface Props {
|
|||||||
withDefaults(defineProps<Props>(), { editing: false });
|
withDefaults(defineProps<Props>(), { editing: false });
|
||||||
defineEmits<{ (e: 'hide'): void }>();
|
defineEmits<{ (e: 'hide'): void }>();
|
||||||
|
|
||||||
const { loading, refresh } = useWorkbenchRefresh();
|
const teamLoadMembers = ref<Api.Project.TeamLoadMember[]>([]);
|
||||||
|
|
||||||
const view = computed(() => buildWorkbenchTeamLoadView(workbenchTeamLoadMock));
|
async function loadTeamLoad() {
|
||||||
|
const { error, data } = await fetchGetMyTeamLoad();
|
||||||
|
teamLoadMembers.value = error || !data ? [] : data.members;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { loading, refresh } = useWorkbenchRefresh(loadTeamLoad);
|
||||||
|
|
||||||
|
// 契约:members[0] 恒为当前用户,展示加「(我)」后缀(builder 内部会按负载等级重排,标记跟名字走)
|
||||||
|
function toTeamLoadSource(member: Api.Project.TeamLoadMember, index: number): WorkbenchTeamLoadMemberSource {
|
||||||
|
return {
|
||||||
|
memberId: member.userId,
|
||||||
|
memberName: index === 0 ? `${member.userNickname}(我)` : member.userNickname,
|
||||||
|
items: member.items.map(item => ({
|
||||||
|
key: item.kind === 'project' && item.projectId ? item.projectId : item.kind,
|
||||||
|
label: item.projectName ?? (item.kind === 'personal' ? '个人事项' : '其他'),
|
||||||
|
kind: item.kind,
|
||||||
|
count: item.count
|
||||||
|
})),
|
||||||
|
dueSoon: member.dueSoonCount,
|
||||||
|
overdue: member.overdueCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = computed(() => buildWorkbenchTeamLoadView({ members: teamLoadMembers.value.map(toTeamLoadSource) }));
|
||||||
|
|
||||||
|
onMounted(loadTeamLoad);
|
||||||
|
|
||||||
const LEVEL_LABEL: Record<WorkbenchTeamLoadLevel, string> = {
|
const LEVEL_LABEL: Record<WorkbenchTeamLoadLevel, string> = {
|
||||||
high: '高负载',
|
high: '高负载',
|
||||||
mid: '中负载',
|
mid: '中负载',
|
||||||
normal: '正常'
|
normal: '正常'
|
||||||
};
|
};
|
||||||
|
|
||||||
function urgentTooltip(dueSoon: number, overdue: number) {
|
|
||||||
if (dueSoon > 0 && overdue > 0) return `临期 ${dueSoon} · 逾期 ${overdue}`;
|
|
||||||
if (overdue > 0) return `逾期 ${overdue}`;
|
|
||||||
return `临期 ${dueSoon}`;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -64,12 +87,12 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="tl-list">
|
<ul v-if="view.members.length" class="tl-list">
|
||||||
<li v-for="m in view.members" :key="m.memberId" class="tl-row">
|
<li v-for="m in view.members" :key="m.memberId" class="tl-row">
|
||||||
<span class="tl-row__dot" :class="`is-${m.level}`" :title="LEVEL_LABEL[m.level]" />
|
<span class="tl-row__dot" :class="`is-${m.level}`" :title="LEVEL_LABEL[m.level]" />
|
||||||
<span class="tl-row__name">{{ m.memberName }}</span>
|
<span class="tl-row__name">{{ m.memberName }}</span>
|
||||||
<div class="tl-row__bar-wrap">
|
<div class="tl-row__bar-wrap">
|
||||||
<div class="tl-row__bar" :style="{ width: `${m.barWidthPercent}%` }">
|
<div class="tl-row__bar">
|
||||||
<ElTooltip
|
<ElTooltip
|
||||||
v-for="seg in m.segments"
|
v-for="seg in m.segments"
|
||||||
:key="seg.key"
|
:key="seg.key"
|
||||||
@@ -85,27 +108,29 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
|||||||
/>
|
/>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="m.overflowExtra > 0" class="tl-row__overflow">+{{ m.overflowExtra }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="tl-row__metrics">
|
<span class="tl-row__metrics">
|
||||||
<span class="tl-row__metric" :class="`is-${m.level}`">
|
<span class="tl-row__metric">
|
||||||
<b>{{ m.inProgress }}</b>
|
<b>{{ m.inProgress }}</b>
|
||||||
进行
|
未完成
|
||||||
</span>
|
</span>
|
||||||
<span v-if="m.urgent > 0" class="tl-row__metric is-urgent">
|
<span v-if="m.dueSoon > 0" class="tl-row__metric is-due-soon">
|
||||||
<ElTooltip :content="urgentTooltip(m.dueSoon, m.overdue)" placement="top">
|
<b>{{ m.dueSoon }}</b>
|
||||||
<span>
|
临期
|
||||||
<b>{{ m.urgent }}</b>
|
</span>
|
||||||
临期
|
<span v-if="m.overdue > 0" class="tl-row__metric is-overdue">
|
||||||
<SvgIcon v-if="m.overdue > 0" icon="mdi:alert" class="tl-row__warn-icon" />
|
<b>{{ m.overdue }}</b>
|
||||||
</span>
|
逾期
|
||||||
</ElTooltip>
|
<SvgIcon icon="mdi:alert" class="tl-row__warn-icon" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ElEmpty v-else description="暂无团队负载数据" :image-size="72" class="tl-empty" />
|
||||||
|
|
||||||
<div class="tl-hint">高 = 进行中 ≥ 6 或 临期+逾期 ≥ 2 · 中 = 进行中 ≥ 4 或 临期+逾期 ≥ 1</div>
|
<div class="tl-hint">
|
||||||
|
口径 = 未完成(含待开始/已暂停)· 高 = 未完成 ≥ 6 或 临期+逾期 ≥ 2 · 中 = 未完成 ≥ 4 或 临期+逾期 ≥ 1
|
||||||
|
</div>
|
||||||
</WorkbenchModuleCard>
|
</WorkbenchModuleCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -168,7 +193,8 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
|||||||
}
|
}
|
||||||
.tl-row {
|
.tl-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 10px 64px 1fr auto;
|
/* 指标列固定宽:每行右侧文字宽度不同(有无临期/逾期)会让 1fr 柱子列长短不一,固定后所有柱子等长对齐 */
|
||||||
|
grid-template-columns: 10px 64px 1fr 200px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 7px 0;
|
padding: 7px 0;
|
||||||
@@ -206,14 +232,15 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
/* 柱子恒撑满整行:长度不再编码数量(数量看右侧数字),段宽表示该成员内部构成占比;无数据时为空灰槽 */
|
||||||
.tl-row__bar {
|
.tl-row__bar {
|
||||||
|
width: 100%;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: var(--el-fill-color);
|
background: var(--el-fill-color);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
}
|
||||||
.tl-row__seg {
|
.tl-row__seg {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -226,49 +253,36 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
|||||||
.tl-row__seg + .tl-row__seg {
|
.tl-row__seg + .tl-row__seg {
|
||||||
box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.75);
|
box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.75);
|
||||||
}
|
}
|
||||||
.tl-row__overflow {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--el-color-danger);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tl-row__metrics {
|
.tl-row__metrics {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
/* 数字统一黑色,负载等级只靠行首圆点表达;仅临期(橙)/ 逾期(红)带警示色 */
|
||||||
|
.tl-row__metric {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.tl-row__metric b {
|
.tl-row__metric b {
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
.tl-row__metric.is-high b {
|
/* 临期用纯橙(el-color-warning 偏黄,与"逾期红"区分度不够) */
|
||||||
color: var(--el-color-danger);
|
.tl-row__metric.is-due-soon,
|
||||||
|
.tl-row__metric.is-due-soon b {
|
||||||
|
color: rgb(234 88 12);
|
||||||
}
|
}
|
||||||
.tl-row__metric.is-mid b {
|
.tl-row__metric.is-overdue,
|
||||||
color: var(--el-color-warning);
|
.tl-row__metric.is-overdue b {
|
||||||
}
|
|
||||||
.tl-row__metric.is-normal b {
|
|
||||||
color: var(--el-color-success);
|
|
||||||
}
|
|
||||||
.tl-row__metric.is-urgent {
|
|
||||||
color: var(--el-color-danger);
|
|
||||||
}
|
|
||||||
.tl-row__metric.is-urgent b {
|
|
||||||
color: var(--el-color-danger);
|
color: var(--el-color-danger);
|
||||||
}
|
}
|
||||||
.tl-row__warn-icon {
|
.tl-row__warn-icon {
|
||||||
vertical-align: -2px;
|
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-color-danger);
|
color: var(--el-color-danger);
|
||||||
@@ -280,4 +294,11 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
|||||||
color: var(--el-text-color-placeholder);
|
color: var(--el-text-color-placeholder);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-empty {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, markRaw, onMounted, ref, watch } from 'vue';
|
import { type Component, computed, markRaw, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import type { RouteKey } from '@elegant-router/types';
|
import type { RouteKey } from '@elegant-router/types';
|
||||||
|
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||||
|
import { RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
|
||||||
import {
|
import {
|
||||||
fetchApproveOvertimeApplication,
|
fetchApproveOvertimeApplication,
|
||||||
|
fetchChangePersonalItemStatus,
|
||||||
|
fetchChangeProjectTaskStatus,
|
||||||
fetchGetMonthlyReportApprovalPage,
|
fetchGetMonthlyReportApprovalPage,
|
||||||
|
fetchGetMyTaskPage,
|
||||||
fetchGetOvertimeApplicationApprovalPage,
|
fetchGetOvertimeApplicationApprovalPage,
|
||||||
|
fetchGetPersonalItemDetail,
|
||||||
|
fetchGetPersonalItemPage,
|
||||||
fetchGetProjectReportApprovalPage,
|
fetchGetProjectReportApprovalPage,
|
||||||
|
fetchGetProjectTask,
|
||||||
fetchGetWeeklyReportApprovalPage,
|
fetchGetWeeklyReportApprovalPage,
|
||||||
fetchRejectOvertimeApplication
|
fetchRejectOvertimeApplication
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
|
import { useDict } from '@/hooks/business/dict';
|
||||||
|
import type { WorklogChangedPayload } from '@/views/project/project/execution/shared';
|
||||||
|
import TaskStatusActionDialog from '@/views/project/project/execution/modules/status-action-dialog.vue';
|
||||||
|
import TaskWorklogDialog from '@/views/project/project/execution/modules/task-worklog-dialog.vue';
|
||||||
|
import PersonalItemDetailDialog from '@/views/personal-center/my-item/modules/personal-item-detail-dialog.vue';
|
||||||
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
||||||
|
import PersonalItemStatusActionDialog from '@/views/personal-center/my-item/modules/personal-item-status-action-dialog.vue';
|
||||||
import OvertimeApplicationActionDialog from '@/views/personal-center/overtime-application/modules/overtime-application-action-dialog.vue';
|
import OvertimeApplicationActionDialog from '@/views/personal-center/overtime-application/modules/overtime-application-action-dialog.vue';
|
||||||
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
|
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
|
||||||
import WorkReportPrototypePageDialog from '@/views/personal-center/work-report/shared/components/prototype-page-dialog.vue';
|
import WorkReportPrototypePageDialog from '@/views/personal-center/work-report/shared/components/prototype-page-dialog.vue';
|
||||||
@@ -30,10 +46,14 @@ import {
|
|||||||
isWorkbenchTodoOverdue,
|
isWorkbenchTodoOverdue,
|
||||||
sortWorkbenchTodoItemsByPriority
|
sortWorkbenchTodoItemsByPriority
|
||||||
} from '../homepage';
|
} from '../homepage';
|
||||||
import { workbenchTodoMock } from '../mock';
|
|
||||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||||
|
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||||
|
import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
|
||||||
|
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
|
import IconMdiPauseCircleOutline from '~icons/mdi/pause-circle-outline';
|
||||||
|
import IconMdiPlayCircleOutline from '~icons/mdi/play-circle-outline';
|
||||||
|
|
||||||
type SortKey = 'created' | 'priority' | 'deadline';
|
type SortKey = 'created' | 'priority' | 'deadline';
|
||||||
type OvertimeApprovalActionType = 'approve' | 'reject';
|
type OvertimeApprovalActionType = 'approve' | 'reject';
|
||||||
@@ -51,9 +71,20 @@ defineEmits<{
|
|||||||
(e: 'hide'): void;
|
(e: 'hide'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const { routerPushByKey } = useRouterPush();
|
const { routerPushByKey } = useRouterPush();
|
||||||
|
|
||||||
const { loading, refresh } = useWorkbenchRefresh();
|
const authStore = useAuthStore();
|
||||||
|
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||||
|
|
||||||
|
const { loading, refresh } = useWorkbenchRefresh(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadMyTaskItems(),
|
||||||
|
loadPersonalTodoItems(),
|
||||||
|
loadOvertimeApprovalItems(),
|
||||||
|
loadWorkReportApprovalItems()
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
const PAGE_SIZE = 5;
|
const PAGE_SIZE = 5;
|
||||||
|
|
||||||
@@ -95,20 +126,44 @@ const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
|
|||||||
{ key: 'overtime_application', label: '加班申请' }
|
{ key: 'overtime_application', label: '加班申请' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const allItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
|
const myTaskItems = ref<WorkbenchTodoItem[]>([]);
|
||||||
|
// 保留任务原始行,供操作图标按 availableActions 渲染并取 projectId / executionId 调状态变更接口
|
||||||
|
const myTaskRows = ref<Api.Project.MyTaskItem[]>([]);
|
||||||
|
const personalTodoItems = ref<WorkbenchTodoItem[]>([]);
|
||||||
|
// 保留个人事项原始行,供操作图标(详情/填报/完成)按 availableActions 渲染并回传给弹层
|
||||||
|
const personalItemRows = ref<Api.PersonalItem.PersonalItem[]>([]);
|
||||||
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||||
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
||||||
const workReportApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
const workReportApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||||
const weeklyApprovalRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
const weeklyApprovalRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||||||
const monthlyApprovalRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
const monthlyApprovalRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||||
const projectApprovalRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
const projectApprovalRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
||||||
const mergedItems = computed(() => {
|
// 工单 tab 等工单业务上线,当前为空态
|
||||||
const mockItems = allItems.value.filter(item => item.category !== 'approval');
|
const mergedItems = computed(() => [
|
||||||
|
...myTaskItems.value,
|
||||||
|
...personalTodoItems.value,
|
||||||
|
...overtimeApprovalItems.value,
|
||||||
|
...workReportApprovalItems.value
|
||||||
|
]);
|
||||||
|
|
||||||
return [...mockItems, ...overtimeApprovalItems.value, ...workReportApprovalItems.value];
|
// 个人事项操作弹层:详情用 view 模式、新增用 add 模式,共用同一实例
|
||||||
});
|
const personalOperateVisible = ref(false);
|
||||||
|
const personalOperateType = ref<'add' | 'view'>('add');
|
||||||
const addDialogVisible = ref(false);
|
const personalOperateRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||||
|
// 个人事项填报:复用详情弹层的 worklog tab
|
||||||
|
const personalDetailVisible = ref(false);
|
||||||
|
const personalDetailRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||||
|
// 个人事项完成:复用状态动作弹层收集原因 + 二次确认
|
||||||
|
const personalStatusVisible = ref(false);
|
||||||
|
const personalStatusRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||||
|
const personalStatusAction = ref<Api.PersonalItem.PersonalItemLifecycleAction | null>(null);
|
||||||
|
// 任务生命周期动作:复用任务工作区的状态动作弹层收集原因
|
||||||
|
const taskStatusVisible = ref(false);
|
||||||
|
const taskStatusRow = ref<Api.Project.MyTaskItem | null>(null);
|
||||||
|
const taskStatusAction = ref<Api.Project.LifecycleAction<Api.Project.ProjectTaskActionCode> | null>(null);
|
||||||
|
// 任务填报工时:复用任务工作区的工时弹层;弹层需要完整任务详情,点击时按需拉取
|
||||||
|
const taskWorklogVisible = ref(false);
|
||||||
|
const taskWorklogTask = ref<Api.Project.ProjectTask | null>(null);
|
||||||
const overtimeDetailVisible = ref(false);
|
const overtimeDetailVisible = ref(false);
|
||||||
const overtimeActionVisible = ref(false);
|
const overtimeActionVisible = ref(false);
|
||||||
const overtimeActionSubmitting = ref(false);
|
const overtimeActionSubmitting = ref(false);
|
||||||
@@ -122,13 +177,228 @@ const OVERTIME_APPROVAL_ACTION_ICONS = {
|
|||||||
detail: markRaw(IconMdiEyeOutline)
|
detail: markRaw(IconMdiEyeOutline)
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleOpenAdd() {
|
const PERSONAL_ACTION_ICONS = {
|
||||||
addDialogVisible.value = true;
|
detail: markRaw(IconMdiEyeOutline),
|
||||||
|
worklog: markRaw(IconMdiClipboardEditOutline),
|
||||||
|
complete: markRaw(IconMdiCheckCircleOutline)
|
||||||
|
};
|
||||||
|
|
||||||
|
// auto_start 是系统自动动作,接口不返回;无图标的未知动作不渲染按钮
|
||||||
|
const TASK_ACTION_ICONS: Partial<Record<Api.Project.ProjectTaskActionCode, Component>> = {
|
||||||
|
pause: markRaw(IconMdiPauseCircleOutline),
|
||||||
|
resume: markRaw(IconMdiPlayCircleOutline),
|
||||||
|
complete: markRaw(IconMdiCheckCircleOutline),
|
||||||
|
cancel: markRaw(IconMdiCloseCircleOutline)
|
||||||
|
};
|
||||||
|
|
||||||
|
function getTaskActionButtonType(code: Api.Project.ProjectTaskActionCode) {
|
||||||
|
if (code === 'complete') return 'success' as const;
|
||||||
|
if (code === 'cancel') return 'danger' as const;
|
||||||
|
return 'primary' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddSubmitted() {
|
function getTodoProgress(item: WorkbenchTodoItem) {
|
||||||
|
if (typeof item.progressRate !== 'number' || !Number.isFinite(item.progressRate)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(100, Math.max(0, Math.round(item.progressRate)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowTodoProgress(item: WorkbenchTodoItem) {
|
||||||
|
return (item.category === 'task' || item.category === 'personal') && typeof item.progressRate === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenAdd() {
|
||||||
|
personalOperateType.value = 'add';
|
||||||
|
personalOperateRow.value = null;
|
||||||
|
personalOperateVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddSubmitted() {
|
||||||
activeTab.value = 'personal';
|
activeTab.value = 'personal';
|
||||||
activeDeadlineFilter.value = null;
|
activeDeadlineFilter.value = null;
|
||||||
|
await loadPersonalTodoItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPersonalItemRow(item: WorkbenchTodoItem) {
|
||||||
|
return personalItemRows.value.find(row => `personal-${row.id}` === item.id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersonalCompleteAction(row: Api.PersonalItem.PersonalItem) {
|
||||||
|
return row.availableActions?.find(action => action.actionCode === 'complete') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅当个人事项当前可完成(availableActions 含 complete)时才渲染完成图标
|
||||||
|
function canCompletePersonalItem(item: WorkbenchTodoItem) {
|
||||||
|
const row = findPersonalItemRow(item);
|
||||||
|
return Boolean(row && getPersonalCompleteAction(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情:复用个人事项操作弹层的 view 只读模式
|
||||||
|
function openPersonalDetail(item: WorkbenchTodoItem) {
|
||||||
|
const row = findPersonalItemRow(item);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
personalOperateType.value = 'view';
|
||||||
|
personalOperateRow.value = row;
|
||||||
|
personalOperateVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填报:拉最新详情后打开详情弹层的 worklog tab
|
||||||
|
async function openPersonalWorklog(item: WorkbenchTodoItem) {
|
||||||
|
const row = findPersonalItemRow(item);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
const { error, data } = await fetchGetPersonalItemDetail(row.id);
|
||||||
|
personalDetailRow.value = error || !data ? row : data;
|
||||||
|
personalDetailVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成:走状态动作弹层(二次确认 + 按 needReason 收集原因)
|
||||||
|
function openPersonalComplete(item: WorkbenchTodoItem) {
|
||||||
|
const row = findPersonalItemRow(item);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
const completeAction = getPersonalCompleteAction(row);
|
||||||
|
if (!completeAction) return;
|
||||||
|
|
||||||
|
personalStatusRow.value = row;
|
||||||
|
personalStatusAction.value = completeAction;
|
||||||
|
personalStatusVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePersonalStatusSubmit(reason: string | null) {
|
||||||
|
if (!personalStatusRow.value || !personalStatusAction.value) return;
|
||||||
|
|
||||||
|
const { error } = await fetchChangePersonalItemStatus(personalStatusRow.value.id, {
|
||||||
|
actionCode: personalStatusAction.value.actionCode,
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) return;
|
||||||
|
|
||||||
|
personalStatusVisible.value = false;
|
||||||
|
window.$message?.success(`${personalStatusAction.value.actionName}成功`);
|
||||||
|
await loadPersonalTodoItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePersonalWorklogChanged() {
|
||||||
|
await loadPersonalTodoItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMyTaskRow(item: WorkbenchTodoItem) {
|
||||||
|
return myTaskRows.value.find(row => `task-${row.id}` === item.id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态变更接口挂在执行路径下,未挂执行的任务不渲染动作按钮
|
||||||
|
function getTaskActions(item: WorkbenchTodoItem) {
|
||||||
|
const row = findMyTaskRow(item);
|
||||||
|
if (!row?.executionId) return [];
|
||||||
|
return row.availableActions.filter(action => Boolean(TASK_ACTION_ICONS[action.actionCode]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填报工时同样依赖执行路径(工时接口挂在 project/execution/task 下)
|
||||||
|
function canReportTaskWorklog(item: WorkbenchTodoItem) {
|
||||||
|
return Boolean(findMyTaskRow(item)?.executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填报:工时弹层需要完整任务详情(负责人/状态/日期),按需拉一次详情再打开
|
||||||
|
async function openTaskWorklog(item: WorkbenchTodoItem) {
|
||||||
|
const row = findMyTaskRow(item);
|
||||||
|
if (!row?.executionId) return;
|
||||||
|
|
||||||
|
const { error, data } = await fetchGetProjectTask(row.projectId, row.executionId, row.id);
|
||||||
|
if (error || !data) return;
|
||||||
|
|
||||||
|
taskWorklogTask.value = data;
|
||||||
|
taskWorklogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填报会联动任务进度/状态(如 auto_start),变更后刷新任务列表
|
||||||
|
async function handleTaskWorklogChanged(payload: WorklogChangedPayload) {
|
||||||
|
await loadMyTaskItems();
|
||||||
|
|
||||||
|
// 与任务工作区联动一致:进度填到 100 且我是任务负责人时提示完成(仅单任务,不做级联)
|
||||||
|
if (payload.mode === 'delete' || payload.progressRate !== 100) return;
|
||||||
|
|
||||||
|
const task = taskWorklogTask.value;
|
||||||
|
if (!task || task.id !== payload.taskId || task.ownerId !== currentUserId.value) return;
|
||||||
|
|
||||||
|
// 以刷新后的行为准:后端按进度/负责人口径返回了 complete 才提示
|
||||||
|
const row = myTaskRows.value.find(item => item.id === payload.taskId);
|
||||||
|
const completeAction = row?.availableActions.find(action => action.actionCode === 'complete');
|
||||||
|
if (!row || !completeAction) return;
|
||||||
|
|
||||||
|
taskStatusRow.value = row;
|
||||||
|
taskStatusAction.value = completeAction;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.$messageBox?.confirm('任务进度已达 100%,是否完成当前任务?', '完成确认', {
|
||||||
|
confirmButtonText: '完成任务',
|
||||||
|
cancelButtonText: '仅保留工时',
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await submitTaskStatusChange(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskStatusActionTitle = computed(() =>
|
||||||
|
taskStatusAction.value ? `任务状态变更:${taskStatusAction.value.actionName}` : '任务状态变更'
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleTaskAction(
|
||||||
|
item: WorkbenchTodoItem,
|
||||||
|
action: Api.Project.LifecycleAction<Api.Project.ProjectTaskActionCode>
|
||||||
|
) {
|
||||||
|
const row = findMyTaskRow(item);
|
||||||
|
if (!row?.executionId) return;
|
||||||
|
|
||||||
|
taskStatusRow.value = row;
|
||||||
|
taskStatusAction.value = action;
|
||||||
|
|
||||||
|
// 完成动作:二次确认后直接提交(与任务工作区同口径;有未完成子任务时后端会拒绝,按业务错误提示)
|
||||||
|
if (action.actionCode === 'complete') {
|
||||||
|
try {
|
||||||
|
await window.$messageBox?.confirm(`确定要完成任务“${row.taskTitle}”吗?`, '完成确认', {
|
||||||
|
confirmButtonText: '完成任务',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await submitTaskStatusChange(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他非必填原因的动作(暂停/恢复)直接提交,不弹原因弹层
|
||||||
|
if (!action.needReason) {
|
||||||
|
await submitTaskStatusChange(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskStatusVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTaskStatusChange(reason: string | null) {
|
||||||
|
const row = taskStatusRow.value;
|
||||||
|
const action = taskStatusAction.value;
|
||||||
|
if (!row?.executionId || !action) return;
|
||||||
|
|
||||||
|
const { error } = await fetchChangeProjectTaskStatus(row.projectId, row.executionId, {
|
||||||
|
taskId: row.id,
|
||||||
|
data: { actionCode: action.actionCode, reason }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) return;
|
||||||
|
|
||||||
|
taskStatusVisible.value = false;
|
||||||
|
window.$message?.success(`${action.actionName}成功`);
|
||||||
|
await loadMyTaskItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabCounts = computed(() => {
|
const tabCounts = computed(() => {
|
||||||
@@ -253,6 +523,15 @@ function handleClickItem(item: WorkbenchTodoItem) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 任务条目:带项目对象上下文跳进该项目的任务导航(执行池页)
|
||||||
|
if (item.category === 'task' && item.projectId) {
|
||||||
|
router.push({
|
||||||
|
path: '/project/project/execution',
|
||||||
|
query: { [OBJECT_CONTEXT_QUERY_KEY]: item.projectId }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.routeKey) return;
|
if (!item.routeKey) return;
|
||||||
routerPushByKey(item.routeKey as RouteKey);
|
routerPushByKey(item.routeKey as RouteKey);
|
||||||
}
|
}
|
||||||
@@ -336,6 +615,71 @@ async function handleWorkReportSubmitted() {
|
|||||||
await loadWorkReportApprovalItems();
|
await loadWorkReportApprovalItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优先级角标用字典 label 原样回显(rdms_req_priority:P0~P3),不翻译成高/中/低
|
||||||
|
const { getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
||||||
|
|
||||||
|
// 任务优先级字典 value("0" P0 ~ "3" P3,数字越小越高)映射待办优先级(仅用于排序与高亮权重)
|
||||||
|
function mapTaskTodoPriority(priority: string) {
|
||||||
|
if (priority === '0') return 'high' as const;
|
||||||
|
if (priority === '1') return 'mid' as const;
|
||||||
|
return 'low' as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 我的任务:跨项目单接口聚合(负责/协办并集、只返回非终态,过滤排序、进度与可执行动作均由后端完成)
|
||||||
|
async function loadMyTaskItems() {
|
||||||
|
const { error, data } = await fetchGetMyTaskPage({ pageNo: 1, pageSize: -1 });
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
myTaskRows.value = [];
|
||||||
|
myTaskItems.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
myTaskRows.value = data.list;
|
||||||
|
myTaskItems.value = buildWorkbenchTodoItems(
|
||||||
|
data.list.map(task => ({
|
||||||
|
id: `task-${task.id}`,
|
||||||
|
category: 'task' as const,
|
||||||
|
title: task.taskTitle,
|
||||||
|
createdTime: task.createTime,
|
||||||
|
deadline: task.plannedEndDate,
|
||||||
|
source: task.projectName,
|
||||||
|
progressRate: task.progressRate,
|
||||||
|
priority: mapTaskTodoPriority(task.priority),
|
||||||
|
priorityLabel: getPriorityLabel(task.priority) || task.priority,
|
||||||
|
projectId: task.projectId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待办口径:未到终态的个人事项(pending / active / paused),terminal 态(completed / cancelled)不进待办
|
||||||
|
const PERSONAL_TODO_STATUSES: Api.PersonalItem.PersonalItemStatusCode[] = ['pending', 'active', 'paused'];
|
||||||
|
|
||||||
|
async function loadPersonalTodoItems() {
|
||||||
|
const { error, data } = await fetchGetPersonalItemPage({ pageNo: 1, pageSize: 100 });
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
personalItemRows.value = [];
|
||||||
|
personalTodoItems.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = data.list.filter(item => PERSONAL_TODO_STATUSES.includes(item.statusCode));
|
||||||
|
personalItemRows.value = rows;
|
||||||
|
personalTodoItems.value = buildWorkbenchTodoItems(
|
||||||
|
rows.map(item => ({
|
||||||
|
id: `personal-${item.id}`,
|
||||||
|
category: 'personal',
|
||||||
|
title: item.taskTitle,
|
||||||
|
createdTime: item.createTime,
|
||||||
|
deadline: item.plannedEndDate,
|
||||||
|
source: '个人事项',
|
||||||
|
progressRate: item.progressRate,
|
||||||
|
routeKey: 'personal-center_my-item'
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadOvertimeApprovalItems() {
|
async function loadOvertimeApprovalItems() {
|
||||||
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
|
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
@@ -443,7 +787,12 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadOvertimeApprovalItems(), loadWorkReportApprovalItems()]);
|
await Promise.all([
|
||||||
|
loadMyTaskItems(),
|
||||||
|
loadPersonalTodoItems(),
|
||||||
|
loadOvertimeApprovalItems(),
|
||||||
|
loadWorkReportApprovalItems()
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -534,25 +883,51 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div class="workbench-todo__content">
|
<div class="workbench-todo__content">
|
||||||
<div v-if="pagedItems.length" class="workbench-todo__list">
|
<div v-if="pagedItems.length" class="workbench-todo__list">
|
||||||
<article
|
<article v-for="item in pagedItems" :key="item.id" class="workbench-todo__item">
|
||||||
v-for="item in pagedItems"
|
|
||||||
:key="item.id"
|
|
||||||
class="workbench-todo__item"
|
|
||||||
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey || item.approvalBizType) }"
|
|
||||||
@click="handleClickItem(item)"
|
|
||||||
>
|
|
||||||
<div class="workbench-todo__leading">
|
<div class="workbench-todo__leading">
|
||||||
<span class="workbench-todo__category" :class="`workbench-todo__category--${item.categoryTone}`">
|
<span class="workbench-todo__category" :class="`workbench-todo__category--${item.categoryTone}`">
|
||||||
{{ item.categoryLabel }}
|
{{ item.categoryLabel }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="item.priority === 'high'" class="workbench-todo__priority">高</span>
|
<span
|
||||||
|
v-if="item.priorityLabel"
|
||||||
|
class="workbench-todo__priority"
|
||||||
|
:class="{ 'workbench-todo__priority--high': item.priority === 'high' }"
|
||||||
|
>
|
||||||
|
{{ item.priorityLabel }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="workbench-todo__body">
|
<div class="workbench-todo__body">
|
||||||
<h4 class="workbench-todo__item-title">{{ item.title }}</h4>
|
<h4 class="workbench-todo__item-title">
|
||||||
|
<span
|
||||||
|
class="workbench-todo__item-title-text"
|
||||||
|
:class="{
|
||||||
|
'workbench-todo__item-title-text--clickable': Boolean(
|
||||||
|
item.routeKey || item.approvalBizType || item.projectId
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
@click="handleClickItem(item)"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
<div class="workbench-todo__meta">
|
<div class="workbench-todo__meta">
|
||||||
<span class="workbench-todo__source">{{ item.source }}</span>
|
<span class="workbench-todo__source">{{ item.source }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowTodoProgress(item)"
|
||||||
|
class="workbench-todo__progress"
|
||||||
|
:aria-label="`进度 ${getTodoProgress(item)}%`"
|
||||||
|
>
|
||||||
|
<span class="workbench-todo__progress-label">进度:</span>
|
||||||
|
<ElProgress
|
||||||
|
class="workbench-todo__progress-bar"
|
||||||
|
:percentage="getTodoProgress(item)"
|
||||||
|
:stroke-width="6"
|
||||||
|
:show-text="false"
|
||||||
|
/>
|
||||||
|
<span class="workbench-todo__progress-text">{{ getTodoProgress(item) }}%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="workbench-todo__trailing">
|
<div class="workbench-todo__trailing">
|
||||||
@@ -574,6 +949,44 @@ onMounted(async () => {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="item.category === 'task' && canReportTaskWorklog(item)"
|
||||||
|
class="workbench-todo__actions"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<ElTooltip content="填报工时">
|
||||||
|
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openTaskWorklog(item)">
|
||||||
|
<component :is="PERSONAL_ACTION_ICONS.worklog" class="text-15px" />
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip v-for="action in getTaskActions(item)" :key="action.actionCode" :content="action.actionName">
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
:type="getTaskActionButtonType(action.actionCode)"
|
||||||
|
class="workbench-todo__action-btn"
|
||||||
|
@click="handleTaskAction(item, action)"
|
||||||
|
>
|
||||||
|
<component :is="TASK_ACTION_ICONS[action.actionCode]" class="text-15px" />
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.category === 'personal'" class="workbench-todo__actions" @click.stop>
|
||||||
|
<ElTooltip content="详情">
|
||||||
|
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPersonalDetail(item)">
|
||||||
|
<component :is="PERSONAL_ACTION_ICONS.detail" class="text-15px" />
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip content="填报工时">
|
||||||
|
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPersonalWorklog(item)">
|
||||||
|
<component :is="PERSONAL_ACTION_ICONS.worklog" class="text-15px" />
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
<ElTooltip v-if="canCompletePersonalItem(item)" content="完成">
|
||||||
|
<ElButton link type="success" class="workbench-todo__action-btn" @click="openPersonalComplete(item)">
|
||||||
|
<component :is="PERSONAL_ACTION_ICONS.complete" class="text-15px" />
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
</div>
|
||||||
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
|
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
|
||||||
{{ item.deadlineLabel }}
|
{{ item.deadlineLabel }}
|
||||||
</span>
|
</span>
|
||||||
@@ -597,13 +1010,43 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<!-- append-to-body:脱离 grid item 的 transform 容器,弹窗才能正常全屏居中 -->
|
<!-- append-to-body:脱离 grid item 的 transform 容器,弹窗才能正常全屏居中 -->
|
||||||
<PersonalItemOperateDialog
|
<PersonalItemOperateDialog
|
||||||
v-model:visible="addDialogVisible"
|
v-model:visible="personalOperateVisible"
|
||||||
operate-type="add"
|
:operate-type="personalOperateType"
|
||||||
:row-data="null"
|
:row-data="personalOperateRow"
|
||||||
append-to-body
|
append-to-body
|
||||||
@submitted="handleAddSubmitted"
|
@submitted="handleAddSubmitted"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PersonalItemDetailDialog
|
||||||
|
v-model:visible="personalDetailVisible"
|
||||||
|
:row-data="personalDetailRow"
|
||||||
|
default-tab="worklog"
|
||||||
|
append-to-body
|
||||||
|
@changed="handlePersonalWorklogChanged"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonalItemStatusActionDialog
|
||||||
|
v-model:visible="personalStatusVisible"
|
||||||
|
:action="personalStatusAction"
|
||||||
|
append-to-body
|
||||||
|
@submit="handlePersonalStatusSubmit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TaskStatusActionDialog
|
||||||
|
v-model:visible="taskStatusVisible"
|
||||||
|
:title="taskStatusActionTitle"
|
||||||
|
:action="taskStatusAction"
|
||||||
|
append-to-body
|
||||||
|
@submit="submitTaskStatusChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TaskWorklogDialog
|
||||||
|
v-model:visible="taskWorklogVisible"
|
||||||
|
:task="taskWorklogTask"
|
||||||
|
append-to-body
|
||||||
|
@changed="handleTaskWorklogChanged"
|
||||||
|
/>
|
||||||
|
|
||||||
<OvertimeApplicationDetailDialog
|
<OvertimeApplicationDetailDialog
|
||||||
v-model:visible="overtimeDetailVisible"
|
v-model:visible="overtimeDetailVisible"
|
||||||
:row-data="currentOvertimeApplication"
|
:row-data="currentOvertimeApplication"
|
||||||
@@ -826,7 +1269,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.workbench-todo__item {
|
.workbench-todo__item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
grid-template-columns: 72px minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
@@ -838,19 +1281,12 @@ onMounted(async () => {
|
|||||||
background-color 160ms ease;
|
background-color 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-todo__item--clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-todo__item--clickable:hover {
|
|
||||||
border-color: rgb(14 116 144 / 60%);
|
|
||||||
background-color: rgb(240 253 250 / 84%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-todo__leading {
|
.workbench-todo__leading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
width: 72px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-todo__category {
|
.workbench-todo__category {
|
||||||
@@ -891,20 +1327,26 @@ onMounted(async () => {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 20px;
|
min-width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
padding: 0 4px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: rgb(254 226 226 / 96%);
|
background-color: rgb(241 245 249 / 96%);
|
||||||
color: rgb(220 38 38 / 96%);
|
color: rgb(71 85 105 / 96%);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-todo__priority--high {
|
||||||
|
background-color: rgb(254 226 226 / 96%);
|
||||||
|
color: rgb(220 38 38 / 96%);
|
||||||
|
}
|
||||||
|
|
||||||
.workbench-todo__body {
|
.workbench-todo__body {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-todo__item-title {
|
.workbench-todo__item-title {
|
||||||
@@ -918,10 +1360,21 @@ onMounted(async () => {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-todo__item-title-text--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-todo__item-title-text--clickable:hover {
|
||||||
|
color: rgb(14 116 144 / 96%);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.workbench-todo__meta {
|
.workbench-todo__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-todo__source {
|
.workbench-todo__source {
|
||||||
@@ -933,6 +1386,41 @@ onMounted(async () => {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-todo__progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: min(100%, 260px);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-todo__progress-label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: rgb(100 116 139 / 92%);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-todo__progress-bar {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-todo__progress-text {
|
||||||
|
flex: 0 0 34px;
|
||||||
|
color: rgb(71 85 105 / 94%);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-todo__progress :deep(.el-progress-bar__outer) {
|
||||||
|
background-color: rgb(226 232 240 / 96%);
|
||||||
|
}
|
||||||
|
|
||||||
.workbench-todo__trailing {
|
.workbench-todo__trailing {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -992,7 +1480,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
@media (width <= 600px) {
|
@media (width <= 600px) {
|
||||||
.workbench-todo__item {
|
.workbench-todo__item {
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: 72px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-todo__trailing {
|
.workbench-todo__trailing {
|
||||||
|
|||||||
Reference in New Issue
Block a user