import dayjs from 'dayjs'; export type WorkbenchTrend = 'up' | 'down' | 'flat'; export type WorkbenchTodoCategory = 'task' | 'ticket' | 'personal' | 'approval'; export type WorkbenchTodoMainTab = 'all' | WorkbenchTodoCategory; export type WorkbenchTodoDeadlineFilter = 'overdue' | 'today' | 'week' | null; export type WorkbenchTodoPriority = 'high' | 'mid' | 'low'; export type WorkbenchProjectStatus = 'active' | 'preview' | 'paused'; 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; title: string; /** 创建时间,ISO 字符串。列表默认按这个升序 */ createdTime: string; /** 截止时间,ISO 字符串 */ deadline: string | null; /** 来源(提交人/项目名/工单号) */ source: string; /** 优先级,用于前端排序与高亮 */ priority?: WorkbenchTodoPriority; /** 是否逾期 */ overdue?: boolean; /** 跳转路由 key(可选,未配则不跳转) */ routeKey?: string; } export interface WorkbenchTodoItem extends Omit { deadlineLabel: string; /** 相对截止天数:负数表示已逾期 */ remainingDays: number | null; categoryLabel: string; 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 { timeLabel: string; relativeLabel: string; tone: 'sky' | 'emerald' | 'amber' | 'rose' | 'violet'; } export interface WorkbenchProjectItemSource { id: string; name: string; code: string; status: WorkbenchProjectStatus; /** 我的角色 */ myRole: string; /** 进度百分比 0-100 */ progress: number; /** 我负责的任务数 */ myTaskCount: number; /** 我负责的待处理任务数 */ myPendingTaskCount: number; /** 最近活动时间,ISO */ lastActiveTime: string; } export interface WorkbenchProjectItem extends Omit { statusLabel: string; statusTone: 'sky' | 'emerald' | 'amber'; progress: number; lastActiveLabel: string; } const todoCategoryMeta: Record< WorkbenchTodoCategory, { label: string; tone: WorkbenchTodoItem['categoryTone']; icon: string } > = { task: { label: '任务', tone: 'emerald', icon: 'mdi:checkbox-marked-circle-outline' }, ticket: { label: '工单', tone: 'amber', icon: 'mdi:ticket-confirmation-outline' }, personal: { label: '个人事项', tone: 'violet', icon: 'mdi:notebook-edit-outline' }, approval: { label: '待审批', tone: 'sky', icon: 'mdi:checkbox-multiple-marked-outline' } }; const todoPriorityWeight: Record = { high: 0, mid: 1, low: 2 }; const activityToneMap: Record = { requirement: 'sky', task: 'emerald', ticket: 'amber', project: 'violet', product: 'rose' }; const projectStatusMeta: Record = { active: { label: '进行中', tone: 'emerald' }, preview: { label: '试运行', tone: 'sky' }, paused: { label: '已暂停', tone: 'amber' } }; function clampPercent(value: number) { if (!Number.isFinite(value)) return 0; 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); if (!target.isValid()) return '不限'; const now = dayjs().startOf('day'); const start = target.startOf('day'); const days = start.diff(now, 'day'); if (days === 0) return `今日 ${target.format('HH:mm')}`; if (days === 1) return `明日 ${target.format('HH:mm')}`; if (days === -1) return `昨日 ${target.format('HH:mm')}`; if (days > 0 && days <= 7) return `${days} 天后 ${target.format('MM-DD')}`; if (days < 0) return `逾期 ${Math.abs(days)} 天`; return target.format('YYYY-MM-DD'); } function getRemainingDays(value: string | null) { if (!value) return null; const target = dayjs(value); if (!target.isValid()) return 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) => { const leftCreated = dayjs(left.createdTime).valueOf(); const rightCreated = dayjs(right.createdTime).valueOf(); if (leftCreated !== rightCreated) return leftCreated - rightCreated; const leftDeadline = left.deadline ? dayjs(left.deadline).valueOf() : Number.POSITIVE_INFINITY; const rightDeadline = right.deadline ? dayjs(right.deadline).valueOf() : Number.POSITIVE_INFINITY; return leftDeadline - rightDeadline; }) .map(item => { const meta = todoCategoryMeta[item.category]; return { ...item, deadlineLabel: formatDeadline(item.deadline), remainingDays: getRemainingDays(item.deadline), categoryLabel: meta.label, categoryTone: meta.tone } satisfies WorkbenchTodoItem; }); } export function filterWorkbenchTodoItemsByCategory(items: readonly WorkbenchTodoItem[], tab: WorkbenchTodoMainTab) { if (tab === 'all') return [...items]; return items.filter(item => item.category === tab); } export function filterWorkbenchTodoItemsByDeadline( items: readonly WorkbenchTodoItem[], filter: WorkbenchTodoDeadlineFilter ) { if (!filter) return [...items]; if (filter === 'overdue') return items.filter(item => isOverdue(item)); if (filter === 'today') return items.filter(item => item.remainingDays === 0); // filter === 'week':含今日,含逾期一起暴露给用户处理 return items.filter(item => item.remainingDays !== null && item.remainingDays <= 7); } export function isWorkbenchTodoOverdue(item: WorkbenchTodoItem) { return isOverdue(item); } function isOverdue(item: WorkbenchTodoItem) { if (item.overdue) return true; return item.remainingDays !== null && item.remainingDays < 0; } export function sortWorkbenchTodoItemsByPriority( items: readonly WorkbenchTodoItem[], direction: 'asc' | 'desc' = 'desc' ) { const factor = direction === 'desc' ? 1 : -1; return [...items].sort((left, right) => { const leftWeight = todoPriorityWeight[left.priority ?? 'low']; const rightWeight = todoPriorityWeight[right.priority ?? 'low']; if (leftWeight !== rightWeight) return (leftWeight - rightWeight) * factor; return dayjs(left.createdTime).valueOf() - dayjs(right.createdTime).valueOf(); }); } 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 buildWorkbenchProjectItems(source: readonly WorkbenchProjectItemSource[]): WorkbenchProjectItem[] { return source.map(item => { const meta = projectStatusMeta[item.status]; return { ...item, statusLabel: meta.label, statusTone: meta.tone, progress: clampPercent(item.progress), lastActiveLabel: formatRelative(item.lastActiveTime) } satisfies WorkbenchProjectItem; }); } export interface WorkbenchOwnedProjectMilestone { id: string; title: string; timeLabel: string; tone: 'amber' | 'slate'; } export interface WorkbenchOwnedProjectMember { name: string; /** 负载 0-100(百分比) */ load: number; level: 'ok' | 'warn' | 'over'; } export interface WorkbenchOwnedProjectItemSource { id: string; name: string; code: string; /** 进度 0-100 */ progress: number; executionCount: number; taskCount: number; memberCount: number; overdueCount: number; /** 距离计划结束剩余天数(负数表示已逾期) */ remainingDays: number; /** 我在该项目中的角色 */ myRole: string; milestones: WorkbenchOwnedProjectMilestone[]; members: WorkbenchOwnedProjectMember[]; } export interface WorkbenchOwnedProjectItem extends WorkbenchOwnedProjectItemSource { progress: number; } export function buildWorkbenchOwnedProjectItems( source: readonly WorkbenchOwnedProjectItemSource[] ): WorkbenchOwnedProjectItem[] { return source.map(item => ({ ...item, progress: clampPercent(item.progress) })); } /** 工时分布行项:项目 / 个人事项 / 其他(杂项) */ export interface WorkbenchWorklogDistributionItem { /** 唯一 key;project 用 projectId,personal/other 用固定字面量 */ key: string; label: string; hours: number; kind: 'project' | 'personal' | 'other'; /** kind === 'project' 时携带,用于跳转项目对象上下文 */ 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; } /** 双周工时 mock 容器:本周 + 上周 */ export interface WorkbenchMyWeekWorklogSource { current: WorkbenchWeekWorklogSource; previous: WorkbenchWeekWorklogSource; } /** 单周工时视图(builder 衍生) */ export interface WorkbenchWeekWorklogView { weekStart: string; weekLabel: string; /** 周一~周五按天填部分(5 长度) */ dailyByDay: number[]; /** 周一~周五按周均分部分(5 长度,每项 = weeklyFilledHours / 5) */ dailyByWeekAvg: number[]; /** 周一~周五合计(5 长度,dailyByDay + dailyByWeekAvg) */ dailyTotal: number[]; /** 累计(含按周) */ totalHours: number; target: number; /** 累计 - 目标;正=领先、负=落后 */ delta: number; /** 达成率,0-100 整数 */ completionRate: number; distribution: WorkbenchWorklogDistributionItem[]; } const WEEKDAY_COUNT = 5; function roundHours(value: number) { return Math.round(value * 10) / 10; } export function buildWorkbenchWeekWorklogView(source: WorkbenchWeekWorklogSource): 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; return { weekStart: source.weekStart, weekLabel, dailyByDay, dailyByWeekAvg, dailyTotal, totalHours, target: source.target, delta, completionRate, distribution: source.distribution }; } export function getGreeting(hour: number = dayjs().hour()) { if (hour < 6) return '凌晨好'; if (hour < 11) return '早上好'; if (hour < 13) return '中午好'; if (hour < 18) return '下午好'; if (hour < 22) return '晚上好'; return '夜深了'; } // === 团队工时分布(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; weekLabel: string; members: Array<{ memberId: string; memberName: string; totalHours: number; }>; /** 项目/个人/其他 列(保序,去重) */ categories: Array<{ key: string; label: string; kind: WorkbenchTeamWorklogItemKind }>; /** 每个 category 一个 series,data[i] = 第 i 个成员该 category 的工时(缺则 0) */ seriesMatrix: Array<{ key: string; label: string; kind: WorkbenchTeamWorklogItemKind; data: number[] }>; /** 团队总工时 */ totalHours: number; /** 团队人均工时 */ averageHours: number; /** 应填工时合计(人数 × expectedHoursPerMember) */ expectedTotalHours: number; /** 填报率 0-100 整数(= 总工时 / 应填工时) */ fillRate: number; /** 偏低人数:< 团队均值 × 0.8 */ lowCount: number; /** 加班人数:> 45h(应填 × 1.125) */ highCount: number; /** 工时最低成员(用于底部小结) */ lowest: { memberName: string; hours: number } | null; /** 工时最高成员(用于底部小结) */ highest: { memberName: string; hours: number } | null; } export function buildWorkbenchTeamWorklogView(source: WorkbenchTeamWorklogSource): WorkbenchTeamWorklogView { const start = dayjs(source.weekStart); const weekLabel = start.isValid() ? `${start.isoWeekYear()}年第${start.isoWeek()}周` : source.weekStart; // 列保序去重:按成员遍历顺序首次出现即入列;项目优先、个人/其他按出现顺序追加 const categoryMap = new Map(); for (const m of source.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 }); } } } const categories = Array.from(categoryMap.values()); const memberView = source.members.map(m => ({ memberId: m.memberId, memberName: m.memberName, totalHours: roundHours(m.items.reduce((s, it) => s + it.hours, 0)) })); const seriesMatrix = categories.map(cat => ({ ...cat, data: source.members.map(m => { const hit = m.items.find(it => it.key === cat.key); return hit ? roundHours(hit.hours) : 0; }) })); 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 fillRate = expectedTotalHours > 0 ? clampPercent((totalHours / expectedTotalHours) * 100) : 0; const lowThreshold = averageHours * 0.8; const highThreshold = source.expectedHoursPerMember * 1.125; const lowCount = memberView.filter(m => m.totalHours < lowThreshold).length; const highCount = memberView.filter(m => m.totalHours > highThreshold).length; let lowest: { memberName: string; hours: number } | null = null; let highest: { memberName: string; hours: number } | null = null; for (const m of memberView) { if (!lowest || m.totalHours < lowest.hours) lowest = { memberName: m.memberName, hours: m.totalHours }; if (!highest || m.totalHours > highest.hours) highest = { memberName: m.memberName, hours: m.totalHours }; } return { weekStart: source.weekStart, weekLabel, members: memberView, categories, seriesMatrix, totalHours, averageHours, expectedTotalHours, fillRate, lowCount, highCount, lowest, highest }; } // === 团队负载(C13 · teamLoad) === export type WorkbenchTeamLoadLevel = 'high' | 'mid' | 'normal'; export type WorkbenchTeamLoadItemKind = 'project' | 'personal'; export interface WorkbenchTeamLoadItemSource { /** projectId 或 'personal' */ key: string; label: string; kind: WorkbenchTeamLoadItemKind; /** 该项目/个人事项下,该成员进行中的任务/事项数(任务按"负责人/协办人"口径,单任务多人各算 1 条) */ count: number; } export interface WorkbenchTeamLoadMemberSource { memberId: string; memberName: string; /** 进行中按项目 + 个人事项的拆分(合计即"进行中总数") */ items: WorkbenchTeamLoadItemSource[]; /** 今天 ≤ 计划结束 ≤ 今天+3 天 且未完成 */ dueSoon: number; /** 计划结束 < 今天 且未完成 */ overdue: number; } export interface WorkbenchTeamLoadSource { /** 数据快照所属周(与 C12 对齐口径) */ weekStart: string; members: WorkbenchTeamLoadMemberSource[]; } export interface WorkbenchTeamLoadSegment extends WorkbenchTeamLoadItemSource { /** 段宽占"柱子内部"的百分比(段总和 = 100%) */ widthPercent: number; } export interface WorkbenchTeamLoadMember extends Omit { /** items 合计 */ inProgress: number; /** dueSoon + overdue */ urgent: number; level: WorkbenchTeamLoadLevel; /** 项目段(去掉 count = 0) */ segments: WorkbenchTeamLoadSegment[]; /** 柱子总长占容器百分比(0-100),溢出时 = 100 */ barWidthPercent: number; /** 溢出数量:inProgress - scaleMax(不溢出 = 0),柱子末端显示 "+N" */ overflowExtra: number; } export interface WorkbenchTeamLoadView { weekStart: string; /** 已按 level → inProgress desc 排序,高负载置顶 */ members: WorkbenchTeamLoadMember[]; /** 柱子量程上限(固定值,避免某个成员极端值把全员压扁) */ scaleMax: number; highCount: number; midCount: number; /** 临期+逾期总条数(团队聚合) */ urgentTotal: number; } const TEAM_LOAD_INPROGRESS_HIGH = 6; const TEAM_LOAD_INPROGRESS_MID = 4; const TEAM_LOAD_URGENT_HIGH = 2; const TEAM_LOAD_URGENT_MID = 1; /** 柱子量程上限:固定 10,确保高负载阈值 6 在 60% 位置可辨;超出走 "+N" 文字 */ const TEAM_LOAD_SCALE_MAX = 10; function resolveTeamLoadLevel(inProgress: number, urgent: number): WorkbenchTeamLoadLevel { if (inProgress >= TEAM_LOAD_INPROGRESS_HIGH || urgent >= TEAM_LOAD_URGENT_HIGH) return 'high'; if (inProgress >= TEAM_LOAD_INPROGRESS_MID || urgent >= TEAM_LOAD_URGENT_MID) return 'mid'; return 'normal'; } const LEVEL_RANK: Record = { high: 0, mid: 1, normal: 2 }; export function buildWorkbenchTeamLoadView(source: WorkbenchTeamLoadSource): WorkbenchTeamLoadView { const scaleMax = TEAM_LOAD_SCALE_MAX; const enriched: WorkbenchTeamLoadMember[] = source.members.map(m => { const inProgress = m.items.reduce((s, it) => s + it.count, 0); const urgent = m.dueSoon + m.overdue; const barWidthPercent = scaleMax > 0 ? Math.min(100, (inProgress / scaleMax) * 100) : 0; const overflowExtra = Math.max(0, inProgress - scaleMax); // 段宽永远相对柱子内部按 count/inProgress 算(段总和 = 100%) // 柱子总长靠 barWidthPercent 控制;不溢出时柱子未顶满,溢出时柱子顶满 + 末端 +N const segments: WorkbenchTeamLoadSegment[] = m.items .filter(it => it.count > 0) .map(it => ({ ...it, widthPercent: inProgress > 0 ? (it.count / inProgress) * 100 : 0 })); return { memberId: m.memberId, memberName: m.memberName, dueSoon: m.dueSoon, overdue: m.overdue, inProgress, urgent, level: resolveTeamLoadLevel(inProgress, urgent), segments, barWidthPercent, overflowExtra }; }); enriched.sort((a, b) => { if (LEVEL_RANK[a.level] !== LEVEL_RANK[b.level]) return LEVEL_RANK[a.level] - LEVEL_RANK[b.level]; if (a.inProgress !== b.inProgress) return b.inProgress - a.inProgress; return b.urgent - a.urgent; }); const highCount = enriched.filter(m => m.level === 'high').length; const midCount = enriched.filter(m => m.level === 'mid').length; const urgentTotal = enriched.reduce((s, m) => s + m.urgent, 0); return { weekStart: source.weekStart, members: enriched, scaleMax, highCount, midCount, urgentTotal }; } export type WorkbenchHealthLevel = 'green' | 'yellow' | 'red'; export interface WorkbenchProjectHealthCardSource { projectId: string; projectName: string; code: string; health: WorkbenchHealthLevel; riskCount: number; overdueTasks: number; backlogRequirements: number; } export interface WorkbenchProjectHealthCard extends WorkbenchProjectHealthCardSource { healthLabel: string; } export function buildWorkbenchProjectHealthCards( source: readonly WorkbenchProjectHealthCardSource[] ): WorkbenchProjectHealthCard[] { const labelMap: Record = { green: '健康', yellow: '关注', red: '风险' }; 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))) })); } export interface WorkbenchMyExecutionItemSource { id: string; executionName: string; /** 关联项目 */ projectId: string; projectName: string; /** 执行状态编码(projectExecution 域:pending / active / paused / completed / cancelled) */ statusCode: string; /** 状态名(后端字典返回) */ statusName: string; /** 优先级编码(取 RDMS_REQ_PRIORITY_DICT_CODE 字典) */ priority: string; /** 计划起止 */ plannedStartDate: string | null; plannedEndDate: string | null; /** 实际起止 */ actualStartDate: string | null; actualEndDate: string | null; /** 进度(0-100 整数) */ progressRate: number; /** 关联项目需求 ID(可选) */ projectRequirementId?: string; /** 关联项目需求名称(可选) */ projectRequirementName?: string; } export type WorkbenchMyExecutionItem = WorkbenchMyExecutionItemSource; /** 过滤掉已完成 / 已取消 / 进度满 100 的执行(默认不在工作台呈现) */ export function buildWorkbenchMyExecutionItems( source: readonly WorkbenchMyExecutionItemSource[] ): WorkbenchMyExecutionItem[] { return source.filter(item => { if (item.statusCode === 'completed' || item.statusCode === 'cancelled') return false; if (item.progressRate >= 100) return false; return true; }); }