'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Layers, ShieldCheck, TriangleAlert } from 'lucide-react'; import { RouteGuard, useHasPermission } from '@/components/auth/Guard'; import { XiaobaoWarningCard } from '@/components/xiaobao-warning/XiaobaoWarningCard'; import { XiaobaoWarningDrawer } from '@/components/xiaobao-warning/XiaobaoWarningDrawer'; import { useAuthStore } from '@/stores/useAuthStore'; import { useBugStore } from '@/stores/useBugStore'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useProductStore } from '@/stores/useProductStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useTaskWorklogStore } from '@/stores/useTaskWorklogStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useWorkActivityStore } from '@/stores/useWorkActivityStore'; import { useXiaobaoRiskStore } from '@/stores/useXiaobaoRiskStore'; import { flattenVersions } from '@/lib/derive'; import { calcXiaobaoVersionRisk, type XiaobaoVersionRisk } from '@/lib/xiaobao-risk'; import { buildRiskInsightSignature, findPreviousRiskSnapshot, getReusableInsight, requestRiskInsight, shouldRequestRiskInsightWithCacheGate } from '@/lib/xiaobao-risk-ai'; import { buildVersionDailyEvidence, buildXiaobaoWorkItems } from '@/lib/xiaobao-risk-evidence'; import { buildRiskSignature, findLatestDailySnapshot, shouldSaveRiskSnapshot } from '@/lib/xiaobao-risk-trend'; import { filterXiaobaoWarningVersions, sanitizeRiskInsight } from '@/lib/xiaobao-warning-view'; type RiskFilter = 'all' | 'attention' | 'high'; export default function XiaobaoWarningPage() { return ( ); } function XiaobaoWarningContent() { const router = useRouter(); const canManage = useHasPermission('xiaobao.warning:manage'); const user = useAuthStore((s) => s.user); const { overview, fetchOverview } = useProductStore(); const { plans, fetchPlans } = useVersionPlanStore(); const { requirements, fetchRequirements } = useRequirementStore(); const { tasks: devTasks, fetchTasks } = useDevTaskStore(); const { testCases, fetchTestCases } = useTestCaseStore(); const { bugs, fetchBugs } = useBugStore(); const { activities, fetchActivities } = useWorkActivityStore(); const { worklogs, fetchWorklogs } = useTaskWorklogStore(); const { snapshots, insights, riskDataLoaded, fetchRiskData, saveSnapshot, saveInsight } = useXiaobaoRiskStore(); const [selectedRiskId, setSelectedRiskId] = useState(null); const [filter, setFilter] = useState('all'); const [calculationNow] = useState(() => new Date()); const savedSnapshotKeysRef = useRef(new Set()); const requestedInsightKeysRef = useRef(new Set()); const today = useMemo(() => new Date().toISOString().slice(0, 10), []); useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { fetchPlans(); }, [fetchPlans]); useEffect(() => { fetchRequirements(); }, [fetchRequirements]); useEffect(() => { fetchTasks(); }, [fetchTasks]); useEffect(() => { fetchTestCases(); }, [fetchTestCases]); useEffect(() => { fetchBugs(); }, [fetchBugs]); useEffect(() => { fetchActivities(); }, [fetchActivities]); useEffect(() => { fetchWorklogs(); }, [fetchWorklogs]); useEffect(() => { fetchRiskData(); }, [fetchRiskData]); const allVersions = useMemo(() => flattenVersions(overview), [overview]); const visibleVersions = useMemo( () => filterXiaobaoWarningVersions(allVersions, { canManage, userName: user?.name }), [allVersions, canManage, user?.name], ); const requirementVersionMap = useMemo(() => { const map = new Map(); requirements.forEach((requirement) => { if (requirement.versionId) map.set(requirement.id, requirement.versionId); }); return map; }, [requirements]); const workItems = useMemo(() => buildXiaobaoWorkItems({ plans, devTasks, testCases, bugs, versions: allVersions.map((version) => ({ id: version.id, name: version.name, productName: version.productName, projectName: version.projectName, })), requirementVersionMap, }), [allVersions, bugs, devTasks, plans, requirementVersionMap, testCases]); const risks = useMemo(() => visibleVersions.map((version) => { const versionRequirements = requirements.filter((requirement) => requirement.versionId === version.id); const requirementIds = new Set(versionRequirements.map((requirement) => requirement.id)); const dailyEvidence = buildVersionDailyEvidence({ versionId: version.id, workItems, activities, worklogs, }); return calcXiaobaoVersionRisk({ version, devTasks: devTasks.filter((task) => requirementIds.has(task.requirementId)), testCases: testCases.filter((testCase) => testCase.versionId === version.id), bugs: bugs.filter((bug) => bug.versionId === version.id), dailyEvidence, recentActivityCount: dailyEvidence.recentActivityCount, lastActivityAt: dailyEvidence.lastActivityAt, snapshots: snapshots.filter((snapshot) => snapshot.versionId === version.id && snapshot.date < today), now: calculationNow, }); }).sort((a, b) => b.riskScore - a.riskScore), [ activities, bugs, devTasks, requirements, testCases, visibleVersions, workItems, worklogs, snapshots, today, calculationNow, ]); useEffect(() => { risks.forEach((risk) => { const snapshot = { ...risk.currentSnapshot, createdAt: new Date().toISOString() }; const previousToday = findLatestDailySnapshot(snapshots, snapshot.versionId, snapshot.date); if (!shouldSaveRiskSnapshot(snapshot, previousToday)) return; const key = `${snapshot.versionId}:${snapshot.date}:${buildRiskSignature(snapshot)}`; if (savedSnapshotKeysRef.current.has(key)) return; savedSnapshotKeysRef.current.add(key); saveSnapshot(snapshot).catch(() => { savedSnapshotKeysRef.current.delete(key); }); }); }, [risks, saveSnapshot, snapshots]); useEffect(() => { risks.forEach((risk) => { const previous = findPreviousRiskSnapshot(snapshots, risk.versionId, today); if (!shouldRequestRiskInsightWithCacheGate(riskDataLoaded, insights, risk, previous)) return; const signature = buildRiskInsightSignature(risk); const key = `${risk.versionId}:${signature}`; if (requestedInsightKeysRef.current.has(key)) return; requestedInsightKeysRef.current.add(key); requestRiskInsight(risk).then((response) => { if (!response.ok) return; saveInsight({ versionId: risk.versionId, riskSignature: signature, insight: sanitizeRiskInsight(response.result), generatedAt: new Date().toISOString(), providerInfo: { model: response.meta.model }, }).catch(() => {}); }).catch(() => {}); }); }, [insights, riskDataLoaded, risks, saveInsight, snapshots, today]); const risksWithInsight = useMemo(() => risks.map((risk) => { const cached = getReusableInsight(insights, risk); return cached ? { ...risk, aiInsight: sanitizeRiskInsight(cached.insight) } : risk; }), [insights, risks]); const filteredRisks = useMemo(() => risksWithInsight.filter((risk) => { if (filter === 'attention') return risk.riskLevel !== 'on_track'; if (filter === 'high') return ['at_risk', 'likely_delayed', 'blocked'].includes(risk.riskLevel); return true; }), [filter, risksWithInsight]); const selectedRisk = selectedRiskId ? risksWithInsight.find((risk) => risk.versionId === selectedRiskId) ?? null : null; const highRiskCount = risks.filter((risk) => ['at_risk', 'likely_delayed', 'blocked'].includes(risk.riskLevel)).length; const attentionCount = risks.filter((risk) => risk.riskLevel !== 'on_track').length; const avgConfidence = risks.length > 0 ? Math.round(risks.reduce((sum, risk) => sum + risk.confidence, 0) / risks.length) : 0; return (

小宝预警

{canManage ? '管理视角:全部未结束版本' : '个人视角:我参与的未结束版本'}

{canManage && ( 管理权限 )} {filteredRisks.length} / {risks.length} 个版本
setFilter('all')}>全部 setFilter('attention')}>需关注 setFilter('high')}>高风险
截止日期按版本期望发版日期计算
{filteredRisks.length === 0 ? (

暂无小宝预警

当前筛选范围内没有需要展示的未结束版本

) : (
{filteredRisks.map((risk) => ( setSelectedRiskId(risk.versionId)} /> ))}
)}
{selectedRisk && ( setSelectedRiskId(null)} onNavigate={() => router.push(`/versions/${selectedRisk.versionId}`)} /> )}
); } function SummaryTile({ label, value }: { label: string; value: string }) { return (

{label}

{value}

); } function FilterButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) { return ( ); }