Files
Script Generator 9a0b16a8f1 feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择
- 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出
- 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置
- 角色管理:卡片列表、系统角色保护、CRUD
- 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页
- 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 18:16:18 +08:00

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',
};