feat(版本): 记录计划需求进展活动
关键改动: - 记录产品/UI 计划需求覆盖进展,并写入工作活动与实体日志 - 日报和小宝预警补充需求进展活动证据 - 调整版本列表与需求覆盖面板展示,并补充测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -17,7 +17,9 @@ import { VersionStatus, VERSION_STATUS_LABEL, VERSION_STATUS_DOT, VERSION_STATUS
|
||||
import { STAGES } from '@/lib/stage';
|
||||
import { ROLE_LABEL } from '@/lib/stage';
|
||||
import { buildVersionProgressMap } from '@/lib/version-progress';
|
||||
import { calcHealthScore, getHealthLevel, calcRiskTags, HEALTH_LEVEL_COLOR, HEALTH_LEVEL_DOT, getTagStyle } from '@/lib/health';
|
||||
import { useXiaobaoWarningRisks } from '@/hooks/useXiaobaoWarningRisks';
|
||||
import type { XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
|
||||
import { getRiskScoreTone } from '@/lib/xiaobao-warning-view';
|
||||
import { Pagination, usePagination } from '@/components/Pagination';
|
||||
|
||||
type Priority = 'P0' | 'P1' | 'P2' | 'P3' | 'P4';
|
||||
@@ -38,6 +40,18 @@ const PRIORITY_ORDER: Record<Priority, number> = {
|
||||
P4: 4,
|
||||
};
|
||||
|
||||
const RISK_SCORE_TEXT_CLASS = {
|
||||
danger: 'text-red-600',
|
||||
warn: 'text-orange-600',
|
||||
ok: 'text-emerald-600',
|
||||
} as const;
|
||||
|
||||
const RISK_SCORE_DOT_CLASS = {
|
||||
danger: 'bg-red-500',
|
||||
warn: 'bg-orange-500',
|
||||
ok: 'bg-emerald-500',
|
||||
} as const;
|
||||
|
||||
function parseVersionNumber(name: string): number[] {
|
||||
const match = name.match(/V([\d.]+)$/i);
|
||||
if (!match) return [0];
|
||||
@@ -131,6 +145,11 @@ function VersionsPageContent() {
|
||||
}, [roles, user?.roleId]);
|
||||
|
||||
const allVersionsRaw = useMemo(() => flattenVersions(overview), [overview]);
|
||||
const { risks: xiaobaoRisks } = useXiaobaoWarningRisks();
|
||||
const xiaobaoRiskMap = useMemo(
|
||||
() => new Map(xiaobaoRisks.map((risk) => [risk.versionId, risk])),
|
||||
[xiaobaoRisks],
|
||||
);
|
||||
// 只显示当前用户参与的版本(members为空时所有人可见;超管可见全部)
|
||||
const allVersions = useMemo(() => allVersionsRaw.filter((v) => {
|
||||
if (isSuperAdmin) return true;
|
||||
@@ -263,8 +282,7 @@ function VersionsPageContent() {
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">版本号</th>
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">优先级</th>
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">整体进度</th>
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">健康度</th>
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">风险标签</th>
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">风险分</th>
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">截止日期</th>
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">负责人</th>
|
||||
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]">操作</th>
|
||||
@@ -276,6 +294,7 @@ function VersionsPageContent() {
|
||||
key={v.id}
|
||||
version={v}
|
||||
overallProgress={versionProgressMap[v.id] ?? 0}
|
||||
xiaobaoRisk={xiaobaoRiskMap.get(v.id)}
|
||||
openMenuId={openMenuId}
|
||||
setOpenMenuId={setOpenMenuId}
|
||||
onNavigate={() => router.push(`/versions/${v.id}`)}
|
||||
@@ -299,17 +318,16 @@ function VersionsPageContent() {
|
||||
interface VersionRowProps {
|
||||
version: VersionWithContext;
|
||||
overallProgress: number;
|
||||
xiaobaoRisk?: XiaobaoVersionRisk;
|
||||
openMenuId: string | null;
|
||||
setOpenMenuId: (id: string | null) => void;
|
||||
onNavigate: () => void;
|
||||
onAction: (version: VersionWithContext, action: 'pause' | 'resume' | 'close' | 'delete') => void;
|
||||
}
|
||||
|
||||
function VersionRow({ version, overallProgress, openMenuId, setOpenMenuId, onNavigate, onAction }: VersionRowProps) {
|
||||
function VersionRow({ version, overallProgress, xiaobaoRisk, openMenuId, setOpenMenuId, onNavigate, onAction }: VersionRowProps) {
|
||||
const priority = (version.priority ?? 'P2') as Priority;
|
||||
const healthScore = calcHealthScore(version.status, version.startDate, version.expectedReleaseDate, version.progress);
|
||||
const healthLevel = getHealthLevel(healthScore);
|
||||
const riskTags = calcRiskTags(version.status, version.startDate, version.expectedReleaseDate, version.progress, version.currentStage, version.members);
|
||||
const riskScoreTone = xiaobaoRisk ? getRiskScoreTone(xiaobaoRisk.riskScore) : null;
|
||||
const isMenuOpen = openMenuId === version.id;
|
||||
|
||||
// Members: show first 2 with role label, then +N
|
||||
@@ -347,29 +365,15 @@ function VersionRow({ version, overallProgress, openMenuId, setOpenMenuId, onNav
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* 健康度 */}
|
||||
{/* 风险分 */}
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1.5 text-[12px] font-semibold tabular-nums ${HEALTH_LEVEL_COLOR[healthLevel]}`}>
|
||||
<span className={`h-2 w-2 rounded-full ${HEALTH_LEVEL_DOT[healthLevel]}`} />
|
||||
{healthScore}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* 风险标签 */}
|
||||
<td className="px-4 py-3">
|
||||
{riskTags.length === 0 ? (
|
||||
<span className="text-[11px] text-[var(--ink-muted)]">-</span>
|
||||
{xiaobaoRisk && riskScoreTone ? (
|
||||
<span className={`inline-flex items-center gap-1.5 text-[12px] font-semibold tabular-nums ${RISK_SCORE_TEXT_CLASS[riskScoreTone]}`}>
|
||||
<span className={`h-2 w-2 rounded-full ${RISK_SCORE_DOT_CLASS[riskScoreTone]}`} />
|
||||
{xiaobaoRisk.riskScore}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{riskTags.slice(0, 3).map((tag) => (
|
||||
<span key={tag.key} className={`inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium ${getTagStyle(tag.severity)}`}>
|
||||
{tag.label}
|
||||
</span>
|
||||
))}
|
||||
{riskTags.length > 3 && (
|
||||
<span className="text-[10px] text-[var(--ink-muted)]">+{riskTags.length - 3}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] text-[var(--ink-muted)]">-</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user