关键改动: - 增加版本表单、发布校验和调研方向进度规则 - 扩展小宝预警已读状态、风险签名和今日证据 - 补充组长权限、加班查看范围、活动记录与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
'use client';
|
|
|
|
import { ChevronRight, TriangleAlert } from 'lucide-react';
|
|
import type { XiaobaoRiskLevel, XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
|
|
|
|
const RISK_LEVEL_LABEL: Record<XiaobaoRiskLevel, string> = {
|
|
on_track: '按期',
|
|
attention: '关注',
|
|
at_risk: '有风险',
|
|
likely_delayed: '大概率延期',
|
|
blocked: '阻塞',
|
|
};
|
|
|
|
const RISK_LEVEL_STYLE: Record<XiaobaoRiskLevel, string> = {
|
|
on_track: 'border-emerald-200 bg-emerald-50 text-emerald-700',
|
|
attention: 'border-amber-200 bg-amber-50 text-amber-700',
|
|
at_risk: 'border-orange-200 bg-orange-50 text-orange-700',
|
|
likely_delayed: 'border-red-200 bg-red-50 text-red-700',
|
|
blocked: 'border-zinc-900 bg-zinc-900 text-white',
|
|
};
|
|
|
|
export function XiaobaoWarningCard({
|
|
risk,
|
|
active = false,
|
|
updated = false,
|
|
onClick,
|
|
}: {
|
|
risk: XiaobaoVersionRisk;
|
|
active?: boolean;
|
|
updated?: boolean;
|
|
onClick: () => void;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
className={`w-full rounded-lg border p-3 text-left transition-colors ${
|
|
active
|
|
? 'border-[var(--accent)] bg-[var(--accent-soft)]'
|
|
: 'border-[var(--line)] bg-[var(--bg)] hover:border-[var(--accent)] hover:bg-[var(--bg-subtle)]'
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--bg-subtle)] text-[var(--accent)]">
|
|
<TriangleAlert className="h-4 w-4" strokeWidth={1.8} />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-start gap-2">
|
|
<div className="min-w-0 flex-1">
|
|
<h3 className="truncate text-[13px] font-semibold text-[var(--ink)]">{risk.versionName}</h3>
|
|
<p className="mt-0.5 truncate text-[11px] text-[var(--ink-muted)]">
|
|
{risk.productName ?? '-'} / {risk.projectName ?? '-'}
|
|
</p>
|
|
</div>
|
|
<div className="flex shrink-0 flex-wrap justify-end gap-1.5">
|
|
{updated && (
|
|
<span className="rounded-full border border-blue-200 bg-blue-50 px-2 py-0.5 text-[10px] font-semibold text-blue-700">
|
|
有更新
|
|
</span>
|
|
)}
|
|
<span className={`rounded-full border px-2 py-0.5 text-[11px] font-medium ${RISK_LEVEL_STYLE[risk.riskLevel]}`}>
|
|
{RISK_LEVEL_LABEL[risk.riskLevel]}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
|
<Metric label="风险分" value={String(risk.riskScore)} tone={risk.riskScore >= 75 ? 'danger' : risk.riskScore >= 55 ? 'warn' : 'ok'} />
|
|
<Metric label="置信" value={`${risk.confidence}%`} tone={risk.confidence < 50 ? 'danger' : risk.confidence < 75 ? 'warn' : 'ok'} />
|
|
</div>
|
|
</div>
|
|
<ChevronRight className="mt-2 h-4 w-4 shrink-0 text-[var(--ink-muted)]" />
|
|
</div>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function Metric({ label, value, tone }: { label: string; value: string; tone: 'ok' | 'warn' | 'danger' }) {
|
|
const toneClass = tone === 'danger' ? 'text-red-600' : tone === 'warn' ? 'text-amber-600' : 'text-emerald-600';
|
|
return (
|
|
<div className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-2 py-2">
|
|
<p className="text-[10px] text-[var(--ink-muted)]">{label}</p>
|
|
<p className={`mt-0.5 text-[13px] font-semibold tabular-nums ${toneClass}`}>{value}</p>
|
|
</div>
|
|
);
|
|
}
|