From 6896a86130409e2a932754d263d245b44926a5d8 Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Fri, 12 Jun 2026 19:49:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(projects):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=88=87=E6=8D=A2=E4=B8=BA=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/echarts.ts | 6 +- src/service/api/project-shared.ts | 155 +++++ src/service/api/project.ts | 61 +- src/typings/api/project.d.ts | 106 ++++ src/views/workbench/homepage.ts | 300 ++------- src/views/workbench/mock.ts | 428 +------------ .../modules/workbench-my-week-worklog.vue | 148 +++-- .../workbench/modules/workbench-team-load.vue | 127 ++-- .../modules/workbench-todo-panel.vue | 570 ++++++++++++++++-- 9 files changed, 1062 insertions(+), 839 deletions(-) diff --git a/src/hooks/common/echarts.ts b/src/hooks/common/echarts.ts index 7330683..fe5332a 100644 --- a/src/hooks/common/echarts.ts +++ b/src/hooks/common/echarts.ts @@ -131,12 +131,14 @@ export function useEcharts(optionsFactory: () => T, hooks: C * @param callback callback function */ async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) { - if (!isRendered()) return; - const updatedOpts = callback(chartOptions, optionsFactory); Object.assign(chartOptions, updatedOpts); + // 图表未初始化(容器尺寸未就绪)时只缓存最新 options,待 render() 初始化时一并应用; + // 否则数据先于初始化到达会被静默丢弃,首屏永远停留在空数据 + if (!isRendered()) return; + if (isRendered()) { chart?.clear(); } diff --git a/src/service/api/project-shared.ts b/src/service/api/project-shared.ts index 4a71784..b36cc28 100644 --- a/src/service/api/project-shared.ts +++ b/src/service/api/project-shared.ts @@ -1,3 +1,4 @@ +import dayjs from 'dayjs'; import { normalizeNullableStringId, normalizeStringId } from './shared'; type ProjectStatusCode = Api.Project.ProjectStatusCode; @@ -76,6 +77,60 @@ export type MyOwnedProjectResponse = Omit & { + 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[] | null; +}; + +export type TeamLoadDistributionItemResponse = Omit & { + projectId?: StringIdResponse | null; +}; + +export type TeamLoadMemberResponse = Omit & { + userId: StringIdResponse; + items?: TeamLoadDistributionItemResponse[] | null; +}; + +export type TeamLoadResponse = { + members?: TeamLoadMemberResponse[] | null; +}; + +export type WorklogDistributionItemResponse = Omit & { + projectId?: StringIdResponse | null; +}; + +export type MyWorklogWeekResponse = Omit & { + dailyHours?: number[] | null; + distribution?: WorklogDistributionItemResponse[] | null; +}; + +export type TeamWorklogWeekMemberResponse = Omit & { + userId: StringIdResponse; + items?: WorklogDistributionItemResponse[] | null; +}; + +export type TeamWorklogWeekResponse = Omit & { + members?: TeamWorklogWeekMemberResponse[] | null; +}; + export type ExecutionAssigneeResponse = Omit & { id: StringIdResponse; executionId: StringIdResponse; @@ -263,6 +318,28 @@ export function normalizeProjectLocalDate(value: ProjectLocalDateValue | undefin 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( actions: LifecycleActionResponse[] | null | undefined ): Api.Project.LifecycleAction[] { @@ -296,6 +373,15 @@ function normalizePriority(value: string | number | null | undefined): string { 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 { return { ...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 { return { ...response, diff --git a/src/service/api/project.ts b/src/service/api/project.ts index 051e098..7893462 100644 --- a/src/service/api/project.ts +++ b/src/service/api/project.ts @@ -13,6 +13,8 @@ import { type MyExecutionResponse, type MyOwnedProjectResponse, type MyParticipatedProjectResponse, + type MyTaskResponse, + type MyWorklogWeekResponse, type ProjectExecutionResponse, type ProjectLocalDateValue, type ProjectMemberResponse, @@ -20,19 +22,25 @@ import { type TaskAssigneeFromApiResponse, type TaskAssigneeLogResponse, type TaskWorklogResponse, + type TeamLoadResponse, + type TeamWorklogWeekResponse, getProjectLifecycleActions, normalizeExecutionAssignee, normalizeExecutionAssigneeLog, normalizeMyExecution, normalizeMyOwnedProject, normalizeMyParticipatedProject, + normalizeMyTask, + normalizeMyWorklogWeek, normalizeProjectExecution, normalizeProjectLocalDate, normalizeProjectMember, normalizeProjectTask, normalizeTaskAssignee, normalizeTaskAssigneeLog, - normalizeTaskWorklog + normalizeTaskWorklog, + normalizeTeamLoad, + normalizeTeamWorklogWeek } from './project-shared'; 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; + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/me/tasks/page`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeMyTask) + })); +} + +/** 获取工作台「团队负载」(团队 = 当前用户 + 管理链路直接下级,members[0] 恒为当前用户) */ +export async function fetchGetMyTeamLoad() { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/me/team-load`, + method: 'get' + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeTeamLoad); +} + +/** 获取工作台「我的工时周聚合」(weekStart 传任意日期,后端归一到所在周周一;逐日工时为均摊推算值) */ +export async function fetchGetMyWorklogWeek(params: Api.Project.WorklogWeekParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/me/worklog-week`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeMyWorklogWeek); +} + +/** 获取工作台「团队工时周聚合」(成员集合与团队负载同口径;周标准工时后端不返回,前端落常量) */ +export async function fetchGetTeamWorklogWeek(params: Api.Project.WorklogWeekParams) { + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/me/team-worklog-week`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, normalizeTeamWorklogWeek); +} + /** 获取项目执行状态看板 */ export function fetchGetProjectExecutionStatusBoard( projectId: string, diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts index 708a238..3a7f2a7 100644 --- a/src/typings/api/project.d.ts +++ b/src/typings/api/project.d.ts @@ -405,6 +405,112 @@ declare namespace Api { members: MyOwnedProjectMember[]; } + /** 工作台「我的任务」(跨项目)查询入参 */ + type MyTaskSearchParams = CommonType.RecordNullable< + Pick & { + /** 身份过滤: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[]; + } + + /** 工作台「团队负载」分布子项(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) */ interface CreateProjectExecutionParams { executionName: string; diff --git a/src/views/workbench/homepage.ts b/src/views/workbench/homepage.ts index 2000df2..dfbcef4 100644 --- a/src/views/workbench/homepage.ts +++ b/src/views/workbench/homepage.ts @@ -1,7 +1,5 @@ import dayjs from 'dayjs'; -export type WorkbenchTrend = 'up' | 'down' | 'flat'; - export type WorkbenchTodoCategory = 'task' | 'ticket' | 'personal' | 'approval'; export type WorkbenchTodoMainTab = 'all' | WorkbenchTodoCategory; @@ -10,40 +8,6 @@ export type WorkbenchTodoDeadlineFilter = 'overdue' | 'today' | 'week' | null; 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 { id: string; category: WorkbenchTodoCategory; @@ -54,12 +18,18 @@ export interface WorkbenchTodoItemSource { deadline: string | null; /** 来源(提交人/项目名/工单号) */ source: string; + /** 进度百分比,仅任务和个人事项使用 */ + progressRate?: number | null; /** 优先级,用于前端排序与高亮 */ priority?: WorkbenchTodoPriority; + /** 优先级原始标签(字典 label,如 P0~P3),有值才渲染角标;不做高/中/低翻译 */ + priorityLabel?: string | null; /** 是否逾期 */ overdue?: boolean; /** 跳转路由 key(可选,未配则不跳转) */ routeKey?: string; + /** 任务条目携带:所属项目 ID,点击带对象上下文跳进该项目的任务导航 */ + projectId?: string; /** 审批业务类型 */ approvalBizType?: 'overtime_application' | string; /** 审批业务 ID */ @@ -74,26 +44,6 @@ export interface WorkbenchTodoItem extends Omit { - timeLabel: string; - relativeLabel: string; - tone: 'sky' | 'emerald' | 'amber' | 'rose' | 'violet'; -} - /** 「我参与的项目」展示项(由 Api.Project.MyParticipatedProjectItem 衍生) */ export interface WorkbenchParticipatedProjectView { id: string; @@ -123,14 +73,6 @@ const todoPriorityWeight: Record = { low: 2 }; -const activityToneMap: Record = { - requirement: 'sky', - task: 'emerald', - ticket: 'amber', - project: 'violet', - product: 'rose' -}; - /** 列表只含进行中项目;按已知状态编码上色,未知回退 sky */ function resolveParticipatedProjectTone(statusCode: string): 'sky' | 'emerald' | 'amber' { if (statusCode === 'active') return 'emerald'; @@ -143,25 +85,6 @@ function clampPercent(value: number) { 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) { if (!value) return '不限'; const target = dayjs(value); @@ -187,60 +110,6 @@ function getRemainingDays(value: string | null) { 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[] { return [...source] .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( source: readonly Api.Project.MyParticipatedProjectItem[] ): WorkbenchParticipatedProjectView[] { @@ -389,37 +246,16 @@ export interface WorkbenchWorklogDistributionItem { projectId?: string; } -/** 单周工时数据源(按天填 / 按周填两类共存) */ -export interface WorkbenchWeekWorklogSource { - /** ISO 周首日(周一),YYYY-MM-DD */ - weekStart: string; - /** 整周一笔填的工时总和(将按工作日 5 等分均摊到周一~周五) */ - weeklyFilledHours: number; - /** 周一~周五按天填的工时,长度恒为 5 */ - dailyHours: [number, number, number, number, number]; - /** 工时分布;前端不再做截断,超出 5 行靠容器滚动 */ - distribution: WorkbenchWorklogDistributionItem[]; - /** 周目标小时数(默认 40) */ - target: number; -} +/** 周标准工时:系统无配置项,后端不返回,产品确认前端落常量 35(不是 40) */ +export const WORKBENCH_WEEK_TARGET_HOURS = 35; -/** 双周工时 mock 容器:本周 + 上周 */ -export interface WorkbenchMyWeekWorklogSource { - current: WorkbenchWeekWorklogSource; - previous: WorkbenchWeekWorklogSource; -} - -/** 单周工时视图(builder 衍生) */ +/** 单周工时视图(builder 衍生;逐日工时为后端按填报日期段均摊到工作日的推算值) */ export interface WorkbenchWeekWorklogView { weekStart: string; weekLabel: string; - /** 周一~周五按天填部分(5 长度) */ - dailyByDay: number[]; - /** 周一~周五按周均分部分(5 长度,每项 = weeklyFilledHours / 5) */ - dailyByWeekAvg: number[]; - /** 周一~周五合计(5 长度,dailyByDay + dailyByWeekAvg) */ - dailyTotal: number[]; - /** 累计(含按周) */ + /** 周一~周五逐日工时(5 长度,均摊推算值,周末份额归周五) */ + dailyHours: number[]; + /** 本周累计 */ totalHours: number; target: number; /** 累计 - 目标;正=领先、负=落后 */ @@ -435,29 +271,37 @@ function roundHours(value: number) { 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 weekLabel = start.isValid() ? `${start.isoWeekYear()}年第${start.isoWeek()}周` : source.weekStart; - const weekAvg = roundHours(source.weeklyFilledHours / WEEKDAY_COUNT); - const dailyByWeekAvg = Array.from({ length: WEEKDAY_COUNT }, () => weekAvg); - const dailyByDay = source.dailyHours.map(roundHours); - const dailyTotal = dailyByDay.map((h, i) => roundHours(h + dailyByWeekAvg[i])); - const totalHours = roundHours(dailyTotal.reduce((s, h) => s + h, 0)); - const delta = roundHours(totalHours - source.target); - const completionRate = source.target > 0 ? clampPercent((totalHours / source.target) * 100) : 0; + const dailyHours = Array.from({ length: WEEKDAY_COUNT }, (_, i) => roundHours(source.dailyHours[i] ?? 0)); + const totalHours = roundHours(dailyHours.reduce((s, h) => s + h, 0)); + const target = WORKBENCH_WEEK_TARGET_HOURS; + const delta = roundHours(totalHours - target); + const completionRate = target > 0 ? clampPercent((totalHours / target) * 100) : 0; return { weekStart: source.weekStart, weekLabel, - dailyByDay, - dailyByWeekAvg, - dailyTotal, + dailyHours, totalHours, - target: source.target, + target, delta, completionRate, - distribution: source.distribution + distribution: source.distribution.map(toWorklogDistributionItem) }; } @@ -470,31 +314,10 @@ export function getGreeting(hour: number = dayjs().hour()) { return '夜深了'; } -// === 团队工时分布(C12 · teamWorklog) === +// === 团队工时分布(D16 团队 tab,原 C12 teamWorklog) === 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 衍生) */ export interface WorkbenchTeamWorklogView { weekStart: string; @@ -512,27 +335,36 @@ export interface WorkbenchTeamWorklogView { totalHours: number; /** 团队人均工时 */ averageHours: number; - /** 应填工时合计(人数 × expectedHoursPerMember) */ + /** 应填工时合计(人数 × WORKBENCH_WEEK_TARGET_HOURS) */ expectedTotalHours: number; /** 填报率 0-100 整数(= 总工时 / 应填工时) */ fillRate: number; /** 偏低人数:< 团队均值 × 0.8 */ lowCount: number; - /** 加班人数:> 45h(应填 × 1.125) */ + /** 加班人数:> 周标准 × 1.125 */ highCount: number; + /** 加班判定阈值(小时),KPI 文案展示用 */ + overtimeThreshold: number; /** 工时最低成员(用于底部小结) */ lowest: { 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 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(); - for (const m of source.members) { + for (const m of members) { for (const it of m.items) { if (!categoryMap.has(it.key)) { 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 memberView = source.members.map(m => ({ + const memberView = members.map(m => ({ memberId: m.memberId, memberName: m.memberName, 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 => ({ ...cat, - data: source.members.map(m => { + data: members.map(m => { const hit = m.items.find(it => it.key === cat.key); 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 memberCount = memberView.length; 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 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 highCount = memberView.filter(m => m.totalHours > highThreshold).length; @@ -585,6 +417,7 @@ export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource fillRate, lowCount, highCount, + overtimeThreshold: roundHours(highThreshold), lowest, highest }; @@ -594,31 +427,29 @@ export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource export type WorkbenchTeamLoadLevel = 'high' | 'mid' | 'normal'; -export type WorkbenchTeamLoadItemKind = 'project' | 'personal'; +export type WorkbenchTeamLoadItemKind = 'project' | 'personal' | 'other'; export interface WorkbenchTeamLoadItemSource { - /** projectId 或 'personal' */ + /** projectId 或 'personal' / 'other' */ key: string; label: string; kind: WorkbenchTeamLoadItemKind; - /** 该项目/个人事项下,该成员进行中的任务/事项数(任务按"负责人/协办人"口径,单任务多人各算 1 条) */ + /** 该项目/个人事项下,该成员未完成的任务/事项数(含待开始/已暂停;任务按"负责人/协办人"口径,单任务多人各算 1 条) */ count: number; } export interface WorkbenchTeamLoadMemberSource { memberId: string; memberName: string; - /** 进行中按项目 + 个人事项的拆分(合计即"进行中总数") */ + /** 未完成数按项目 + 个人事项的拆分(合计即"未完成总数") */ items: WorkbenchTeamLoadItemSource[]; - /** 今天 ≤ 计划结束 ≤ 今天+3 天 且未完成 */ + /** 今天 ≤ 计划结束 ≤ 今天+3 天 且未完成(与逾期互斥) */ dueSoon: number; /** 计划结束 < 今天 且未完成 */ overdue: number; } export interface WorkbenchTeamLoadSource { - /** 数据快照所属周(与 C12 对齐口径) */ - weekStart: string; members: WorkbenchTeamLoadMemberSource[]; } @@ -628,7 +459,7 @@ export interface WorkbenchTeamLoadSegment extends WorkbenchTeamLoadItemSource { } export interface WorkbenchTeamLoadMember extends Omit { - /** items 合计 */ + /** items 合计(未完成总数) */ inProgress: number; /** dueSoon + overdue */ urgent: number; @@ -642,7 +473,6 @@ export interface WorkbenchTeamLoadMember extends Omit s + m.urgent, 0); return { - weekStart: source.weekStart, members: enriched, scaleMax, highCount, @@ -741,19 +570,6 @@ export function buildWorkbenchProjectHealthCards( 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 的执行(默认不在工作台呈现)。 * 后端接口已按此口径过滤,此处为双保险;泛型保证接口返回类型可复用。 diff --git a/src/views/workbench/mock.ts b/src/views/workbench/mock.ts index fa1d346..66d0716 100644 --- a/src/views/workbench/mock.ts +++ b/src/views/workbench/mock.ts @@ -1,423 +1,7 @@ -import dayjs from 'dayjs'; -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; +import type { WorkbenchProjectHealthCardSource } from './homepage'; +// projectHealth 为「演示态保留」widget(RDMS 业务暂无风险登记 / 计划基线流程,不接真实数据), +// 本 mock 是其唯一数据源;其余 widget 已全部接通真实接口,对应 mock 已清除。 export const workbenchProjectHealthMock = [ { projectId: 'prj-1', @@ -447,9 +31,3 @@ export const workbenchProjectHealthMock = [ backlogRequirements: 6 } ] 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[]; diff --git a/src/views/workbench/modules/workbench-my-week-worklog.vue b/src/views/workbench/modules/workbench-my-week-worklog.vue index ca167d4..0177434 100644 --- a/src/views/workbench/modules/workbench-my-week-worklog.vue +++ b/src/views/workbench/modules/workbench-my-week-worklog.vue @@ -1,19 +1,16 @@ @@ -168,7 +193,8 @@ function urgentTooltip(dueSoon: number, overdue: number) { } .tl-row { display: grid; - grid-template-columns: 10px 64px 1fr auto; + /* 指标列固定宽:每行右侧文字宽度不同(有无临期/逾期)会让 1fr 柱子列长短不一,固定后所有柱子等长对齐 */ + grid-template-columns: 10px 64px 1fr 200px; align-items: center; gap: 10px; padding: 7px 0; @@ -206,14 +232,15 @@ function urgentTooltip(dueSoon: number, overdue: number) { gap: 6px; min-width: 0; } +/* 柱子恒撑满整行:长度不再编码数量(数量看右侧数字),段宽表示该成员内部构成占比;无数据时为空灰槽 */ .tl-row__bar { + width: 100%; height: 8px; border-radius: 4px; background: var(--el-fill-color); overflow: hidden; display: flex; min-width: 0; - transition: width 0.3s ease; } .tl-row__seg { height: 100%; @@ -226,49 +253,36 @@ function urgentTooltip(dueSoon: number, overdue: number) { .tl-row__seg + .tl-row__seg { 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 { display: inline-flex; align-items: center; + justify-content: flex-end; gap: 10px; font-size: 12px; color: var(--el-text-color-secondary); white-space: nowrap; } +/* 数字统一黑色,负载等级只靠行首圆点表达;仅临期(橙)/ 逾期(红)带警示色 */ +.tl-row__metric { + display: inline-flex; + align-items: center; + white-space: nowrap; +} .tl-row__metric b { color: var(--el-text-color-primary); font-weight: 600; margin-right: 2px; } -.tl-row__metric.is-high b { - color: var(--el-color-danger); +/* 临期用纯橙(el-color-warning 偏黄,与"逾期红"区分度不够) */ +.tl-row__metric.is-due-soon, +.tl-row__metric.is-due-soon b { + color: rgb(234 88 12); } -.tl-row__metric.is-mid b { - color: var(--el-color-warning); -} -.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 { +.tl-row__metric.is-overdue, +.tl-row__metric.is-overdue b { color: var(--el-color-danger); } .tl-row__warn-icon { - vertical-align: -2px; margin-left: 2px; font-size: 12px; color: var(--el-color-danger); @@ -280,4 +294,11 @@ function urgentTooltip(dueSoon: number, overdue: number) { color: var(--el-text-color-placeholder); line-height: 1.5; } + +.tl-empty { + flex: 1; + min-height: 0; + margin: auto; + padding: 8px 0; +} diff --git a/src/views/workbench/modules/workbench-todo-panel.vue b/src/views/workbench/modules/workbench-todo-panel.vue index 713d0e4..f90e8ff 100644 --- a/src/views/workbench/modules/workbench-todo-panel.vue +++ b/src/views/workbench/modules/workbench-todo-panel.vue @@ -1,16 +1,32 @@ @@ -534,25 +883,51 @@ onMounted(async () => {
-
+
{{ item.categoryLabel }} - + + {{ item.priorityLabel }} +
-

{{ item.title }}

+

+ + {{ item.title }} + +

{{ item.source }}
+
+ 进度: + + {{ getTodoProgress(item) }}% +
@@ -574,6 +949,44 @@ onMounted(async () => {
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
{{ item.deadlineLabel }} @@ -597,13 +1010,43 @@ onMounted(async () => { + + + + + + + + { .workbench-todo__item { display: grid; - grid-template-columns: auto minmax(0, 1fr) auto; + grid-template-columns: 72px minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 14px 16px; @@ -838,19 +1281,12 @@ onMounted(async () => { 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 { display: flex; align-items: center; gap: 8px; + width: 72px; + min-width: 0; } .workbench-todo__category { @@ -891,20 +1327,26 @@ onMounted(async () => { display: inline-flex; align-items: center; justify-content: center; - width: 20px; + min-width: 20px; height: 20px; + padding: 0 4px; border-radius: 6px; - background-color: rgb(254 226 226 / 96%); - color: rgb(220 38 38 / 96%); + background-color: rgb(241 245 249 / 96%); + color: rgb(71 85 105 / 96%); font-size: 11px; font-weight: 800; } +.workbench-todo__priority--high { + background-color: rgb(254 226 226 / 96%); + color: rgb(220 38 38 / 96%); +} + .workbench-todo__body { min-width: 0; display: flex; flex-direction: column; - gap: 4px; + gap: 5px; } .workbench-todo__item-title { @@ -918,10 +1360,21 @@ onMounted(async () => { 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 { display: flex; align-items: center; gap: 8px; + flex-wrap: wrap; } .workbench-todo__source { @@ -933,6 +1386,41 @@ onMounted(async () => { 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 { display: flex; align-items: center; @@ -992,7 +1480,7 @@ onMounted(async () => { @media (width <= 600px) { .workbench-todo__item { - grid-template-columns: auto minmax(0, 1fr); + grid-template-columns: 72px minmax(0, 1fr); } .workbench-todo__trailing {