- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
import type { RoleProgress } from './derive';
|
|
|
|
export type RiskLevel = 'low' | 'medium' | 'high';
|
|
export type DelayStatus = 'normal' | 'warning' | 'delayed';
|
|
|
|
const DELAY_WARNING_DAYS = 3;
|
|
|
|
export function calcOverallProgress(progress?: RoleProgress[]): number {
|
|
if (!progress || progress.length === 0) return 0;
|
|
const sum = progress.reduce((s, p) => s + p.percent, 0);
|
|
return Math.round(sum / progress.length);
|
|
}
|
|
|
|
export function calcDelayStatus(
|
|
status: string,
|
|
expectedReleaseDate?: string | null,
|
|
): DelayStatus {
|
|
if (status === 'released' || status === 'closed') return 'normal';
|
|
if (!expectedReleaseDate) return 'normal';
|
|
|
|
const now = new Date();
|
|
now.setHours(0, 0, 0, 0);
|
|
const deadline = new Date(expectedReleaseDate);
|
|
deadline.setHours(0, 0, 0, 0);
|
|
|
|
const diffDays = (deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
|
|
|
|
if (diffDays < 0) return 'delayed';
|
|
if (diffDays <= DELAY_WARNING_DAYS) return 'warning';
|
|
return 'normal';
|
|
}
|
|
|
|
export function calcRiskLevel(
|
|
status: string,
|
|
startDate?: string | null,
|
|
expectedReleaseDate?: string | null,
|
|
progress?: RoleProgress[],
|
|
): RiskLevel {
|
|
if (status === 'released' || status === 'closed') return 'low';
|
|
if (status === 'paused') return 'medium';
|
|
|
|
const delayStatus = calcDelayStatus(status, expectedReleaseDate);
|
|
if (delayStatus === 'delayed') return 'high';
|
|
|
|
if (!startDate || !expectedReleaseDate) return 'low';
|
|
|
|
const now = new Date();
|
|
now.setHours(0, 0, 0, 0);
|
|
const start = new Date(startDate);
|
|
start.setHours(0, 0, 0, 0);
|
|
const end = new Date(expectedReleaseDate);
|
|
end.setHours(0, 0, 0, 0);
|
|
|
|
const totalDuration = end.getTime() - start.getTime();
|
|
if (totalDuration <= 0) return 'low';
|
|
|
|
const elapsed = now.getTime() - start.getTime();
|
|
const timeProgress = Math.min(100, Math.max(0, (elapsed / totalDuration) * 100));
|
|
const actualProgress = calcOverallProgress(progress);
|
|
const gap = timeProgress - actualProgress;
|
|
|
|
if (gap >= 40) return 'high';
|
|
if (gap >= 20 || delayStatus === 'warning') return 'medium';
|
|
return 'low';
|
|
}
|
|
|
|
export const RISK_LABEL: Record<RiskLevel, string> = {
|
|
low: '低',
|
|
medium: '中',
|
|
high: '高',
|
|
};
|
|
|
|
export const RISK_COLOR: Record<RiskLevel, string> = {
|
|
low: 'text-emerald-600',
|
|
medium: 'text-amber-600',
|
|
high: 'text-red-600',
|
|
};
|
|
|
|
export const RISK_DOT: Record<RiskLevel, string> = {
|
|
low: 'bg-emerald-500',
|
|
medium: 'bg-amber-500',
|
|
high: 'bg-red-500',
|
|
};
|
|
|
|
export const DELAY_LABEL: Record<DelayStatus, string> = {
|
|
normal: '正常',
|
|
warning: '即将延期',
|
|
delayed: '已延期',
|
|
};
|
|
|
|
export const DELAY_COLOR: Record<DelayStatus, string> = {
|
|
normal: 'bg-emerald-50 text-emerald-700',
|
|
warning: 'bg-amber-50 text-amber-700',
|
|
delayed: 'bg-red-50 text-red-700',
|
|
};
|