feat(版本): 完善研发计划与预警已读

关键改动:

- 增加版本表单、发布校验和调研方向进度规则

- 扩展小宝预警已读状态、风险签名和今日证据

- 补充组长权限、加班查看范围、活动记录与相关测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-06-30 16:48:58 +08:00
parent 04520bd8c5
commit 3d3d56697a
41 changed files with 1701 additions and 121 deletions

View File

@@ -6,11 +6,18 @@ import { CalendarClock, Loader2, ShieldCheck, Sparkles, TriangleAlert } from 'lu
import { RouteGuard, useHasPermission } from '@/components/auth/Guard';
import { XiaobaoWarningCard } from '@/components/xiaobao-warning/XiaobaoWarningCard';
import { useXiaobaoWarningRisks } from '@/hooks/useXiaobaoWarningRisks';
import { useAuthStore } from '@/stores/useAuthStore';
import { useXiaobaoWarningReadStore } from '@/stores/useXiaobaoWarningReadStore';
import type { XiaobaoRiskLevel, XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
import { buildRiskInsightSignature, findPreviousRiskSnapshot, requestRiskInsight, shouldRequestRiskInsightWithCacheGate } from '@/lib/xiaobao-risk-ai';
import { buildRiskSignature, findLatestDailySnapshot, shouldSaveRiskSnapshot } from '@/lib/xiaobao-risk-trend';
import { attachXiaobaoRiskSuggestion, buildXiaobaoRiskInsightPendingKey } from '@/lib/xiaobao-risk-suggestion';
import { filterXiaobaoRiskWarnings, formatRemainingWork, sanitizeRiskInsight } from '@/lib/xiaobao-warning-view';
import {
filterXiaobaoRiskWarnings,
formatRemainingWork,
isXiaobaoWarningUpdated,
sanitizeRiskInsight,
} from '@/lib/xiaobao-warning-view';
import { formatDateTime } from '@/lib/format';
const RISK_LEVEL_LABEL: Record<XiaobaoRiskLevel, string> = {
@@ -42,6 +49,7 @@ export default function XiaobaoWarningPage() {
function XiaobaoWarningContent() {
const router = useRouter();
const canManage = useHasPermission('xiaobao.warning:manage');
const user = useAuthStore((s) => s.user);
const {
risks,
snapshots,
@@ -54,12 +62,17 @@ function XiaobaoWarningContent() {
finishInsightUpdate,
today,
} = useXiaobaoWarningRisks({ loadRiskCache: true });
const { readStates, readStateLoaded, fetchReadStates, markRiskRead } = useXiaobaoWarningReadStore();
const [selectedRiskId, setSelectedRiskId] = useState<string | null>(null);
const [selectedProductId, setSelectedProductId] = useState('');
const [selectedProjectId, setSelectedProjectId] = useState('');
const savedSnapshotKeysRef = useRef(new Set<string>());
const requestedInsightKeysRef = useRef(new Set<string>());
useEffect(() => {
if (user?.id) fetchReadStates();
}, [fetchReadStates, user?.id]);
useEffect(() => {
risks.forEach((risk) => {
const snapshot = { ...risk.currentSnapshot, createdAt: new Date().toISOString() };
@@ -139,6 +152,15 @@ function XiaobaoWarningContent() {
projectId: selectedProjectId || undefined,
}), [risksWithInsight, selectedProductId, selectedProjectId]);
const updatedRiskIds = useMemo(() => {
if (!user?.id || !readStateLoaded) return new Set<string>();
return new Set(
filteredRisks
.filter((risk) => isXiaobaoWarningUpdated(risk, readStates, user.id))
.map((risk) => risk.versionId),
);
}, [filteredRisks, readStateLoaded, readStates, user?.id]);
useEffect(() => {
if (filteredRisks.length === 0) {
setSelectedRiskId(null);
@@ -156,6 +178,12 @@ function XiaobaoWarningContent() {
? Math.round(filteredRisks.reduce((sum, risk) => sum + risk.confidence, 0) / filteredRisks.length)
: 0;
const selectRisk = (risk: XiaobaoVersionRisk) => {
setSelectedRiskId(risk.versionId);
if (!user?.id) return;
markRiskRead(user.id, risk).catch(() => {});
};
return (
<div className="flex h-full flex-col bg-[var(--bg)]">
<header className="flex h-14 shrink-0 items-center justify-between border-b border-[var(--line)] bg-[var(--bg-card)] px-5">
@@ -224,7 +252,8 @@ function XiaobaoWarningContent() {
key={risk.versionId}
active={risk.versionId === selectedRiskId}
risk={risk}
onClick={() => setSelectedRiskId(risk.versionId)}
updated={updatedRiskIds.has(risk.versionId)}
onClick={() => selectRisk(risk)}
/>
))}
</div>