feat(版本): 完善研发计划与预警已读
关键改动: - 增加版本表单、发布校验和调研方向进度规则 - 扩展小宝预警已读状态、风险签名和今日证据 - 补充组长权限、加班查看范围、活动记录与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user