2026-05-14 09:05:08 +08:00
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
|
|
|
|
|
|
|
export type WorkbenchTrend = 'up' | 'down' | 'flat';
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
export type WorkbenchTodoCategory = 'task' | 'ticket' | 'personal' | 'approval';
|
2026-05-14 09:05:08 +08:00
|
|
|
|
|
2026-05-25 14:30:44 +08:00
|
|
|
|
export type WorkbenchTodoMainTab = 'all' | WorkbenchTodoCategory;
|
|
|
|
|
|
|
|
|
|
|
|
export type WorkbenchTodoDeadlineFilter = 'overdue' | 'today' | 'week' | null;
|
|
|
|
|
|
|
|
|
|
|
|
export type WorkbenchTodoPriority = 'high' | 'mid' | 'low';
|
2026-05-14 09:05:08 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-25 14:30:44 +08:00
|
|
|
|
/** 创建时间,ISO 字符串。列表默认按这个升序 */
|
|
|
|
|
|
createdTime: string;
|
2026-05-14 09:05:08 +08:00
|
|
|
|
/** 截止时间,ISO 字符串 */
|
|
|
|
|
|
deadline: string | null;
|
|
|
|
|
|
/** 来源(提交人/项目名/工单号) */
|
|
|
|
|
|
source: string;
|
2026-05-25 14:30:44 +08:00
|
|
|
|
/** 优先级,用于前端排序与高亮 */
|
|
|
|
|
|
priority?: WorkbenchTodoPriority;
|
2026-05-14 09:05:08 +08:00
|
|
|
|
/** 是否逾期 */
|
|
|
|
|
|
overdue?: 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 }
|
|
|
|
|
|
> = {
|
|
|
|
|
|
task: { label: '任务', tone: 'emerald', icon: 'mdi:checkbox-marked-circle-outline' },
|
|
|
|
|
|
ticket: { label: '工单', tone: 'amber', icon: 'mdi:ticket-confirmation-outline' },
|
2026-05-25 14:30:44 +08:00
|
|
|
|
personal: { label: '个人事项', tone: 'violet', icon: 'mdi:notebook-edit-outline' },
|
2026-05-28 08:20:01 +08:00
|
|
|
|
approval: { label: '待审批', tone: 'sky', icon: 'mdi:checkbox-multiple-marked-outline' }
|
2026-05-25 14:30:44 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const todoPriorityWeight: Record<WorkbenchTodoPriority, number> = {
|
|
|
|
|
|
high: 0,
|
|
|
|
|
|
mid: 1,
|
|
|
|
|
|
low: 2
|
2026-05-14 09:05:08 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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 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) => {
|
2026-05-25 14:30:44 +08:00
|
|
|
|
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;
|
2026-05-14 09:05:08 +08:00
|
|
|
|
})
|
|
|
|
|
|
.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;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 14:30:44 +08:00
|
|
|
|
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();
|
|
|
|
|
|
});
|
2026-05-14 09:05:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 09:05:08 +08:00
|
|
|
|
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 '夜深了';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
// === 团队工时分布(C12 · teamWorklog) ===
|
2026-05-21 21:42:23 +08:00
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
export type WorkbenchTeamWorklogItemKind = 'project' | 'personal' | 'other';
|
2026-05-21 21:42:23 +08:00
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
export interface WorkbenchTeamWorklogItem {
|
|
|
|
|
|
/** 项目 id 或 'personal' / 'other' */
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
hours: number;
|
|
|
|
|
|
kind: WorkbenchTeamWorklogItemKind;
|
2026-05-21 21:42:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
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<string, { key: string; label: string; kind: WorkbenchTeamWorklogItemKind }>();
|
|
|
|
|
|
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;
|
2026-05-21 21:42:23 +08:00
|
|
|
|
})
|
2026-05-28 08:20:01 +08:00
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
|
}
|
2026-05-21 21:42:23 +08:00
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
return {
|
|
|
|
|
|
weekStart: source.weekStart,
|
|
|
|
|
|
weekLabel,
|
|
|
|
|
|
members: memberView,
|
|
|
|
|
|
categories,
|
|
|
|
|
|
seriesMatrix,
|
|
|
|
|
|
totalHours,
|
|
|
|
|
|
averageHours,
|
|
|
|
|
|
expectedTotalHours,
|
|
|
|
|
|
fillRate,
|
|
|
|
|
|
lowCount,
|
|
|
|
|
|
highCount,
|
|
|
|
|
|
lowest,
|
|
|
|
|
|
highest
|
|
|
|
|
|
};
|
2026-05-21 21:42:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
// === 团队负载(C13 · teamLoad) ===
|
|
|
|
|
|
|
|
|
|
|
|
export type WorkbenchTeamLoadLevel = 'high' | 'mid' | 'normal';
|
2026-05-21 21:42:23 +08:00
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
export type WorkbenchTeamLoadItemKind = 'project' | 'personal';
|
2026-05-21 21:42:23 +08:00
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
export interface WorkbenchTeamLoadItemSource {
|
|
|
|
|
|
/** projectId 或 'personal' */
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
kind: WorkbenchTeamLoadItemKind;
|
|
|
|
|
|
/** 该项目/个人事项下,该成员进行中的任务/事项数(任务按"负责人/协办人"口径,单任务多人各算 1 条) */
|
|
|
|
|
|
count: number;
|
2026-05-21 21:42:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
export interface WorkbenchTeamLoadMemberSource {
|
2026-05-21 21:42:23 +08:00
|
|
|
|
memberId: string;
|
|
|
|
|
|
memberName: string;
|
2026-05-28 08:20:01 +08:00
|
|
|
|
/** 进行中按项目 + 个人事项的拆分(合计即"进行中总数") */
|
|
|
|
|
|
items: WorkbenchTeamLoadItemSource[];
|
|
|
|
|
|
/** 今天 ≤ 计划结束 ≤ 今天+3 天 且未完成 */
|
|
|
|
|
|
dueSoon: number;
|
|
|
|
|
|
/** 计划结束 < 今天 且未完成 */
|
2026-05-21 21:42:23 +08:00
|
|
|
|
overdue: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
export interface WorkbenchTeamLoadSource {
|
|
|
|
|
|
/** 数据快照所属周(与 C12 对齐口径) */
|
|
|
|
|
|
weekStart: string;
|
|
|
|
|
|
members: WorkbenchTeamLoadMemberSource[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorkbenchTeamLoadSegment extends WorkbenchTeamLoadItemSource {
|
|
|
|
|
|
/** 段宽占"柱子内部"的百分比(段总和 = 100%) */
|
|
|
|
|
|
widthPercent: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface WorkbenchTeamLoadMember extends Omit<WorkbenchTeamLoadMemberSource, 'items'> {
|
|
|
|
|
|
/** 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<WorkbenchTeamLoadLevel, number> = { 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);
|
2026-05-21 21:42:23 +08:00
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
return {
|
|
|
|
|
|
weekStart: source.weekStart,
|
|
|
|
|
|
members: enriched,
|
|
|
|
|
|
scaleMax,
|
|
|
|
|
|
highCount,
|
|
|
|
|
|
midCount,
|
|
|
|
|
|
urgentTotal
|
|
|
|
|
|
};
|
2026-05-21 21:42:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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))) }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
export interface WorkbenchMyExecutionItemSource {
|
2026-05-21 21:42:23 +08:00
|
|
|
|
id: string;
|
2026-05-28 08:20:01 +08:00
|
|
|
|
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;
|
|
|
|
|
|
});
|
2026-05-21 21:42:23 +08:00
|
|
|
|
}
|