- 移除历史工作日分隔符兼容逻辑,仅使用显式分隔标记切分 - 扩展工时数据显示范围从5个工作日到7天完整周,并添加周末展示控制 - 在团队工时统计中新增加班工时字段并调整图表展示方式 - 移除团队工时分布中的分类矩阵,简化数据结构 - 为API访问日志和错误日志搜索组件添加用户选择功能 - 调整日志搜索界面布局,将用户编号输入改为用户选择下拉框 - 移除用户类型筛选条件以简化搜索界面 - 更新工时图表配置以支持周末数据显示和新的数据结构
576 lines
20 KiB
TypeScript
576 lines
20 KiB
TypeScript
import dayjs from 'dayjs';
|
||
|
||
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 interface WorkbenchTodoItemSource {
|
||
id: string;
|
||
category: WorkbenchTodoCategory;
|
||
/** 左侧分类 tag 文案;不传时按 category 默认映射 */
|
||
categoryLabel?: string;
|
||
/** 左侧分类 tag 色调;不传时按 category 默认映射 */
|
||
categoryTone?: WorkbenchTodoItem['categoryTone'];
|
||
title: string;
|
||
/** 创建时间,ISO 字符串。列表默认按这个升序 */
|
||
createdTime: string;
|
||
/** 截止时间,ISO 字符串 */
|
||
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 */
|
||
approvalBizId?: string;
|
||
}
|
||
|
||
export interface WorkbenchTodoItem extends Omit<WorkbenchTodoItemSource, 'deadline'> {
|
||
deadlineLabel: string;
|
||
/** 相对截止天数:负数表示已逾期 */
|
||
remainingDays: number | null;
|
||
categoryLabel: string;
|
||
categoryTone: 'sky' | 'emerald' | 'amber' | 'rose' | 'violet';
|
||
}
|
||
|
||
/** 「我参与的项目」展示项(由 Api.Project.MyParticipatedProjectItem 衍生) */
|
||
export interface WorkbenchParticipatedProjectView {
|
||
id: string;
|
||
name: string;
|
||
code: string | null;
|
||
statusName: string | null;
|
||
statusTone: 'sky' | 'emerald' | 'amber';
|
||
myRole: string | null;
|
||
progress: number;
|
||
myTaskCount: number;
|
||
myPendingTaskCount: number;
|
||
}
|
||
|
||
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<WorkbenchTodoPriority, number> = {
|
||
high: 0,
|
||
mid: 1,
|
||
low: 2
|
||
};
|
||
|
||
/** 列表只含进行中项目;按已知状态编码上色,未知回退 sky */
|
||
function resolveParticipatedProjectTone(statusCode: string): 'sky' | 'emerald' | 'amber' {
|
||
if (statusCode === 'active') return 'emerald';
|
||
if (statusCode === 'paused') return 'amber';
|
||
return 'sky';
|
||
}
|
||
|
||
function clampPercent(value: number) {
|
||
if (!Number.isFinite(value)) return 0;
|
||
return Math.min(100, Math.max(0, Math.round(value)));
|
||
}
|
||
|
||
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 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: item.categoryLabel || meta.label,
|
||
categoryTone: item.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 buildWorkbenchParticipatedProjects(
|
||
source: readonly Api.Project.MyParticipatedProjectItem[]
|
||
): WorkbenchParticipatedProjectView[] {
|
||
return source.map(item => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
code: item.code,
|
||
statusName: item.statusName,
|
||
statusTone: resolveParticipatedProjectTone(item.statusCode),
|
||
myRole: item.myRole,
|
||
progress: clampPercent(item.progress),
|
||
myTaskCount: item.myTaskCount,
|
||
myPendingTaskCount: item.myPendingTaskCount
|
||
}));
|
||
}
|
||
|
||
/** 「我负责的项目」成员负载展示项 */
|
||
export interface WorkbenchOwnedProjectMemberView {
|
||
userId: string;
|
||
userName: string | null;
|
||
/** 该成员在本项目下进行中任务数 */
|
||
activeTaskCount: number;
|
||
}
|
||
|
||
/** 「我负责的项目」展示项(由 Api.Project.MyOwnedProjectItem 衍生) */
|
||
export interface WorkbenchOwnedProjectView {
|
||
id: string;
|
||
name: string;
|
||
code: string | null;
|
||
progress: number;
|
||
myRole: string | null;
|
||
executionCount: number;
|
||
taskCount: number;
|
||
overdueCount: number;
|
||
memberCount: number;
|
||
/** 计划结束日期 YYYY-MM-DD,可空 */
|
||
plannedEndDate: string | null;
|
||
/** 距计划结束剩余天数(负=已逾期);plannedEndDate 为空时 null */
|
||
remainingDays: number | null;
|
||
members: WorkbenchOwnedProjectMemberView[];
|
||
}
|
||
|
||
export function buildWorkbenchOwnedProjects(
|
||
source: readonly Api.Project.MyOwnedProjectItem[]
|
||
): WorkbenchOwnedProjectView[] {
|
||
return source.map(item => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
code: item.code,
|
||
progress: clampPercent(item.progress),
|
||
myRole: item.myRole,
|
||
executionCount: item.executionCount,
|
||
taskCount: item.taskCount,
|
||
overdueCount: item.overdueCount,
|
||
memberCount: item.memberCount,
|
||
plannedEndDate: item.plannedEndDate,
|
||
remainingDays: getRemainingDays(item.plannedEndDate),
|
||
members: item.members.map(member => ({
|
||
userId: member.userId,
|
||
userName: member.userName,
|
||
activeTaskCount: member.activeTaskCount
|
||
}))
|
||
}));
|
||
}
|
||
|
||
/** 工时分布行项:项目 / 我的事项 / 其他(杂项) */
|
||
export interface WorkbenchWorklogDistributionItem {
|
||
/** 唯一 key;project 用 projectId,personal/other 用固定字面量 */
|
||
key: string;
|
||
label: string;
|
||
hours: number;
|
||
kind: 'project' | 'personal' | 'other';
|
||
/** kind === 'project' 时携带,用于跳转项目对象上下文 */
|
||
projectId?: string;
|
||
}
|
||
|
||
/** 周标准工时:系统无配置项,后端不返回,产品确认前端落常量 35(不是 40) */
|
||
export const WORKBENCH_WEEK_TARGET_HOURS = 35;
|
||
|
||
/** 单周工时视图(builder 衍生;接口返回周一~周日逐日工时,按周填报时仅均摊到工作日) */
|
||
export interface WorkbenchWeekWorklogView {
|
||
weekStart: string;
|
||
weekLabel: string;
|
||
/** 周一~周日逐日工时(7 长度;周末单天工时保留在周末当天) */
|
||
dailyHours: number[];
|
||
/** 是否展示周末两列;仅当周六或周日任一天 > 0 时展示 */
|
||
showWeekend: boolean;
|
||
/** 图表/逐日展示实际使用的横轴标签 */
|
||
visibleDayLabels: string[];
|
||
/** 图表/逐日展示实际使用的逐日工时数据 */
|
||
visibleDailyHours: number[];
|
||
/** 本周累计 */
|
||
totalHours: number;
|
||
target: number;
|
||
/** 累计 - 目标;正=领先、负=落后 */
|
||
delta: number;
|
||
/** 达成率,0-100 整数 */
|
||
completionRate: number;
|
||
distribution: WorkbenchWorklogDistributionItem[];
|
||
}
|
||
|
||
const WORKDAY_COUNT = 5;
|
||
const FULL_WEEKDAY_COUNT = 7;
|
||
const WORKDAY_LABELS = ['一', '二', '三', '四', '五'] as const;
|
||
const FULL_WEEKDAY_LABELS = ['一', '二', '三', '四', '五', '六', '日'] as const;
|
||
|
||
function roundHours(value: number) {
|
||
return Math.round(value * 10) / 10;
|
||
}
|
||
|
||
/** 契约分布项 → 展示行项(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 dailyHours = Array.from({ length: FULL_WEEKDAY_COUNT }, (_, i) => roundHours(source.dailyHours[i] ?? 0));
|
||
const sat = Number(dailyHours[5] ?? 0);
|
||
const sun = Number(dailyHours[6] ?? 0);
|
||
const showWeekend = sat > 0 || sun > 0;
|
||
const visibleDayLabels = showWeekend ? [...FULL_WEEKDAY_LABELS] : [...WORKDAY_LABELS];
|
||
const visibleDailyHours = showWeekend ? [...dailyHours] : dailyHours.slice(0, WORKDAY_COUNT);
|
||
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,
|
||
dailyHours,
|
||
showWeekend,
|
||
visibleDayLabels,
|
||
visibleDailyHours,
|
||
totalHours,
|
||
target,
|
||
delta,
|
||
completionRate,
|
||
distribution: source.distribution.map(toWorklogDistributionItem)
|
||
};
|
||
}
|
||
|
||
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 '夜深了';
|
||
}
|
||
|
||
// === 团队工时分布(D16 团队 tab,原 C12 teamWorklog) ===
|
||
|
||
/** 团队工时分布视图(builder 衍生) */
|
||
export interface WorkbenchTeamWorklogView {
|
||
weekStart: string;
|
||
weekLabel: string;
|
||
members: Array<{
|
||
memberId: string;
|
||
memberName: string;
|
||
totalHours: number;
|
||
overtimeHours: number;
|
||
}>;
|
||
/** 团队总工时 */
|
||
totalHours: number;
|
||
/** 团队人均工时 */
|
||
averageHours: number;
|
||
/** 应填工时合计(人数 × WORKBENCH_WEEK_TARGET_HOURS) */
|
||
expectedTotalHours: number;
|
||
/** 填报率 0-100 整数(= 总工时 / 应填工时) */
|
||
fillRate: number;
|
||
/** 偏低人数:< 团队均值 × 0.8 */
|
||
lowCount: number;
|
||
/** 工时最低成员(用于底部小结) */
|
||
lowest: { memberName: string; hours: number } | null;
|
||
/** 工时最高成员(用于底部小结) */
|
||
highest: { memberName: string; hours: number } | null;
|
||
}
|
||
|
||
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) => {
|
||
const items = member.items.map(toWorklogDistributionItem);
|
||
const totalHours = roundHours(items.reduce((sum, item) => sum + item.hours, 0));
|
||
const overtimeHours = roundHours(member.overtimeHours ?? 0);
|
||
|
||
return {
|
||
memberId: member.userId,
|
||
memberName: index === 0 ? `${member.userNickname}(我)` : member.userNickname,
|
||
totalHours,
|
||
overtimeHours
|
||
};
|
||
});
|
||
|
||
const totalHours = roundHours(members.reduce((sum, member) => sum + member.totalHours, 0));
|
||
const memberCount = members.length;
|
||
const averageHours = memberCount > 0 ? roundHours(totalHours / memberCount) : 0;
|
||
const expectedTotalHours = memberCount * WORKBENCH_WEEK_TARGET_HOURS;
|
||
const fillRate = expectedTotalHours > 0 ? clampPercent((totalHours / expectedTotalHours) * 100) : 0;
|
||
|
||
const lowThreshold = averageHours * 0.8;
|
||
const lowCount = members.filter(member => member.totalHours < lowThreshold).length;
|
||
|
||
let lowest: { memberName: string; hours: number } | null = null;
|
||
let highest: { memberName: string; hours: number } | null = null;
|
||
for (const member of members) {
|
||
if (!lowest || member.totalHours < lowest.hours)
|
||
lowest = { memberName: member.memberName, hours: member.totalHours };
|
||
if (!highest || member.totalHours > highest.hours)
|
||
highest = { memberName: member.memberName, hours: member.totalHours };
|
||
}
|
||
|
||
return {
|
||
weekStart: source.weekStart,
|
||
weekLabel,
|
||
members,
|
||
totalHours,
|
||
averageHours,
|
||
expectedTotalHours,
|
||
fillRate,
|
||
lowCount,
|
||
lowest,
|
||
highest
|
||
};
|
||
}
|
||
|
||
// === 团队负载(C13 · teamLoad) ===
|
||
|
||
export type WorkbenchTeamLoadLevel = 'high' | 'mid' | 'normal';
|
||
|
||
export type WorkbenchTeamLoadItemKind = 'project' | 'personal' | 'other';
|
||
|
||
export interface WorkbenchTeamLoadItemSource {
|
||
/** projectId 或 'personal' / 'other' */
|
||
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 {
|
||
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 {
|
||
/** 已按 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);
|
||
|
||
return {
|
||
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<WorkbenchHealthLevel, string> = { green: '健康', yellow: '关注', red: '风险' };
|
||
return source.map(s => ({ ...s, healthLabel: labelMap[s.health] }));
|
||
}
|
||
|
||
/**
|
||
* 前端兜底过滤:剔除已完成 / 已取消 / 进度满 100 的执行(默认不在工作台呈现)。
|
||
* 后端接口已按此口径过滤,此处为双保险;泛型保证接口返回类型可复用。
|
||
*/
|
||
export function buildWorkbenchMyExecutionItems<T extends { statusCode: string; progressRate: number }>(
|
||
source: readonly T[]
|
||
): T[] {
|
||
return source.filter(item => {
|
||
if (item.statusCode === 'completed' || item.statusCode === 'cancelled') return false;
|
||
if (item.progressRate >= 100) return false;
|
||
return true;
|
||
});
|
||
}
|