2026-05-14 09:05:08 +08:00
|
|
|
|
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<WorkbenchTodoItemSource, 'deadline'> {
|
|
|
|
|
|
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<WorkbenchActivityItemSource, 'time'> {
|
|
|
|
|
|
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<WorkbenchProjectItemSource, 'lastActiveTime'> {
|
|
|
|
|
|
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<WorkbenchActivityItemSource['targetKind'], WorkbenchActivityItem['tone']> = {
|
|
|
|
|
|
requirement: 'sky',
|
|
|
|
|
|
task: 'emerald',
|
|
|
|
|
|
ticket: 'amber',
|
|
|
|
|
|
project: 'violet',
|
|
|
|
|
|
product: 'rose'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const projectStatusMeta: Record<WorkbenchProjectStatus, { label: string; tone: WorkbenchProjectItem['statusTone'] }> = {
|
|
|
|
|
|
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()]}`;
|
|
|
|
|
|
}
|
2026-05-21 21:42:23 +08:00
|
|
|
|
|
|
|
|
|
|
export type WorkbenchMyTaskBucket = 'today' | 'week' | 'overdue' | 'all';
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorkbenchMyTaskItemSource {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
statusCode: string;
|
|
|
|
|
|
statusLabel: string;
|
|
|
|
|
|
executionName: string;
|
|
|
|
|
|
projectName: string;
|
|
|
|
|
|
priority: 'high' | 'mid' | 'low';
|
|
|
|
|
|
deadline: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorkbenchMyTaskItem extends Omit<WorkbenchMyTaskItemSource, 'deadline'> {
|
|
|
|
|
|
deadlineLabel: string;
|
|
|
|
|
|
remainingDays: number | null;
|
|
|
|
|
|
overdue: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildWorkbenchMyTaskItems(source: readonly WorkbenchMyTaskItemSource[]): WorkbenchMyTaskItem[] {
|
|
|
|
|
|
return [...source]
|
|
|
|
|
|
.sort((a, b) => {
|
|
|
|
|
|
const av = a.deadline ? dayjs(a.deadline).valueOf() : Number.POSITIVE_INFINITY;
|
|
|
|
|
|
const bv = b.deadline ? dayjs(b.deadline).valueOf() : Number.POSITIVE_INFINITY;
|
|
|
|
|
|
return av - bv;
|
|
|
|
|
|
})
|
|
|
|
|
|
.map(item => {
|
|
|
|
|
|
const remaining = getRemainingDays(item.deadline);
|
|
|
|
|
|
return {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
deadlineLabel: formatDeadline(item.deadline),
|
|
|
|
|
|
remainingDays: remaining,
|
|
|
|
|
|
overdue: remaining !== null && remaining < 0
|
|
|
|
|
|
} satisfies WorkbenchMyTaskItem;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function filterWorkbenchMyTaskItems(items: readonly WorkbenchMyTaskItem[], bucket: WorkbenchMyTaskBucket) {
|
|
|
|
|
|
if (bucket === 'all') return [...items];
|
|
|
|
|
|
if (bucket === 'overdue') return items.filter(i => i.overdue);
|
|
|
|
|
|
if (bucket === 'today') return items.filter(i => i.remainingDays === 0);
|
|
|
|
|
|
return items.filter(i => i.remainingDays !== null && i.remainingDays >= 0 && i.remainingDays <= 7);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorkbenchMyRequirementGroupSource {
|
|
|
|
|
|
statusCode: string;
|
|
|
|
|
|
statusLabel: string;
|
|
|
|
|
|
count: number;
|
|
|
|
|
|
tone: 'sky' | 'amber' | 'emerald' | 'rose';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export type WorkbenchMyRequirementGroup = WorkbenchMyRequirementGroupSource;
|
|
|
|
|
|
|
|
|
|
|
|
export function buildWorkbenchMyRequirementGroups(
|
|
|
|
|
|
source: readonly WorkbenchMyRequirementGroupSource[]
|
|
|
|
|
|
): WorkbenchMyRequirementGroup[] {
|
|
|
|
|
|
return [...source];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorkbenchTeamTodoRowSource {
|
|
|
|
|
|
projectId: string;
|
|
|
|
|
|
projectName: string;
|
|
|
|
|
|
memberId: string;
|
|
|
|
|
|
memberName: string;
|
|
|
|
|
|
inProgress: number;
|
|
|
|
|
|
overdue: number;
|
|
|
|
|
|
weekDone: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export type WorkbenchTeamTodoRow = WorkbenchTeamTodoRowSource;
|
|
|
|
|
|
|
|
|
|
|
|
export function buildWorkbenchTeamTodoRows(source: readonly WorkbenchTeamTodoRowSource[]): WorkbenchTeamTodoRow[] {
|
|
|
|
|
|
return [...source].sort((a, b) => b.overdue - a.overdue);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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<WorkbenchHealthLevel, string> = { 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 type WorkbenchFavoriteKind = 'product' | 'project' | 'requirement' | 'task';
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorkbenchFavoriteItemSource {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
kind: WorkbenchFavoriteKind;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
source: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorkbenchFavoriteItem extends WorkbenchFavoriteItemSource {
|
|
|
|
|
|
kindLabel: string;
|
|
|
|
|
|
kindTone: 'sky' | 'emerald' | 'amber' | 'rose';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildWorkbenchFavoriteItems(source: readonly WorkbenchFavoriteItemSource[]): WorkbenchFavoriteItem[] {
|
|
|
|
|
|
const meta: Record<WorkbenchFavoriteKind, { label: string; tone: 'sky' | 'emerald' | 'amber' | 'rose' }> = {
|
|
|
|
|
|
product: { label: '产品', tone: 'sky' },
|
|
|
|
|
|
project: { label: '项目', tone: 'emerald' },
|
|
|
|
|
|
requirement: { label: '需求', tone: 'amber' },
|
|
|
|
|
|
task: { label: '任务', tone: 'rose' }
|
|
|
|
|
|
};
|
|
|
|
|
|
return source.map(s => ({ ...s, kindLabel: meta[s.kind].label, kindTone: meta[s.kind].tone }));
|
|
|
|
|
|
}
|