import dayjs from 'dayjs'; export type WorkbenchTrend = 'up' | 'down' | 'flat'; export type WorkbenchTodoCategory = 'review' | 'task' | 'ticket' | 'mention'; export type WorkbenchTodoTimeBucket = 'all' | 'today' | 'week' | 'overdue'; export type WorkbenchProjectStatus = 'active' | 'preview' | 'paused'; export interface WorkbenchBannerSummarySource { /** 今日待办数 */ todoCount: number; /** 即将到期数(今日/明日内截止) */ upcomingCount: number; /** 本周已完成 */ weekDone: number; /** 本周总量 */ weekTotal: number; /** 本周进行中 */ weekInProgress: number; /** 本周逾期 */ weekOverdue: number; } export interface WorkbenchBannerSummary { todoCount: number; upcomingCount: number; weekDone: number; weekTotal: number; weekInProgress: number; weekOverdue: number; /** 完成率(0-100 整数) */ weekCompletionRate: number; } 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 字符串 */ deadline: string | null; /** 来源(提交人/项目名/工单号) */ source: string; /** 是否逾期 */ overdue?: boolean; /** 是否高优先级 */ highPriority?: 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 } > = { review: { label: '需求评审', tone: 'sky', icon: 'mdi:file-search-outline' }, task: { label: '任务', tone: 'emerald', icon: 'mdi:checkbox-marked-circle-outline' }, ticket: { label: '工单', tone: 'amber', icon: 'mdi:ticket-confirmation-outline' }, mention: { label: '@ 提及', tone: 'violet', icon: 'mdi:at' } }; 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 buildWorkbenchBannerSummary(source: WorkbenchBannerSummarySource): WorkbenchBannerSummary { const total = Math.max(0, source.weekTotal); const done = Math.max(0, source.weekDone); const rate = total === 0 ? 0 : clampPercent((done / total) * 100); return { todoCount: Math.max(0, source.todoCount), upcomingCount: Math.max(0, source.upcomingCount), weekDone: done, weekTotal: total, weekInProgress: Math.max(0, source.weekInProgress), weekOverdue: Math.max(0, source.weekOverdue), weekCompletionRate: rate }; } 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 leftValue = left.deadline ? dayjs(left.deadline).valueOf() : Number.POSITIVE_INFINITY; const rightValue = right.deadline ? dayjs(right.deadline).valueOf() : Number.POSITIVE_INFINITY; return leftValue - rightValue; }) .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 filterWorkbenchTodoItems(items: readonly WorkbenchTodoItem[], bucket: WorkbenchTodoTimeBucket) { if (bucket === 'all') return [...items]; if (bucket === 'overdue') return items.filter(item => item.remainingDays !== null && item.remainingDays < 0); if (bucket === 'today') return items.filter(item => item.remainingDays === 0); // bucket === 'week' return items.filter(item => item.remainingDays !== null && item.remainingDays >= 0 && item.remainingDays <= 7); } 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 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 '夜深了'; } export function getTodayLabel() { const today = dayjs(); const weekdayMap = ['日', '一', '二', '三', '四', '五', '六']; return `今天 ${today.format('YYYY-MM-DD')} 星期${weekdayMap[today.day()]}`; }