关键改动: - 增加版本表单、发布校验和调研方向进度规则 - 扩展小宝预警已读状态、风险签名和今日证据 - 补充组长权限、加班查看范围、活动记录与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
'use client';
|
|
|
|
import { create } from 'zustand';
|
|
import type { XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
|
|
import {
|
|
markXiaobaoWarningRead,
|
|
type XiaobaoWarningReadState,
|
|
} from '@/lib/xiaobao-warning-view';
|
|
import { loadServerData, saveServerData } from '@/lib/server-data';
|
|
|
|
interface XiaobaoWarningReadStoreState {
|
|
readStates: XiaobaoWarningReadState[];
|
|
readStateLoaded: boolean;
|
|
error?: string;
|
|
fetchReadStates: () => Promise<void>;
|
|
markRiskRead: (userId: string, risk: XiaobaoVersionRisk, readAt?: string) => Promise<void>;
|
|
}
|
|
|
|
let readStateSaveQueue: Promise<void> = Promise.resolve();
|
|
|
|
async function loadReadStates(): Promise<XiaobaoWarningReadState[] | null> {
|
|
try {
|
|
const rows = await loadServerData<XiaobaoWarningReadState[]>('xiaobao-warning-views');
|
|
return normalizeReadStates(rows);
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
function normalizeReadStates(rows: unknown): XiaobaoWarningReadState[] {
|
|
if (!Array.isArray(rows)) return [];
|
|
return rows.filter(isReadState);
|
|
}
|
|
|
|
function isReadState(row: unknown): row is XiaobaoWarningReadState {
|
|
if (!row || typeof row !== 'object') return false;
|
|
const item = row as Partial<XiaobaoWarningReadState>;
|
|
return (
|
|
typeof item.userId === 'string' &&
|
|
typeof item.versionId === 'string' &&
|
|
typeof item.signature === 'string' &&
|
|
typeof item.readAt === 'string'
|
|
);
|
|
}
|
|
|
|
function mergeReadStates(
|
|
remoteRows: XiaobaoWarningReadState[] = [],
|
|
localRows: XiaobaoWarningReadState[] = [],
|
|
): XiaobaoWarningReadState[] {
|
|
const byKey = new Map<string, XiaobaoWarningReadState>();
|
|
for (const row of [...remoteRows, ...localRows]) {
|
|
const key = `${row.userId}::${row.versionId}`;
|
|
const existing = byKey.get(key);
|
|
if (!existing || row.readAt.localeCompare(existing.readAt) >= 0) {
|
|
byKey.set(key, row);
|
|
}
|
|
}
|
|
return Array.from(byKey.values()).sort((a, b) => b.readAt.localeCompare(a.readAt));
|
|
}
|
|
|
|
export const useXiaobaoWarningReadStore = create<XiaobaoWarningReadStoreState>((set, get) => ({
|
|
readStates: [],
|
|
readStateLoaded: false,
|
|
error: undefined,
|
|
|
|
fetchReadStates: async () => {
|
|
const rows = await loadReadStates();
|
|
set({
|
|
readStates: rows ? mergeReadStates(rows, get().readStates) : get().readStates,
|
|
readStateLoaded: rows !== null,
|
|
error: rows === null ? '小宝预警查看状态加载失败' : undefined,
|
|
});
|
|
},
|
|
|
|
markRiskRead: async (userId, risk, readAt = new Date().toISOString()) => {
|
|
if (!userId) return;
|
|
const optimistic = markXiaobaoWarningRead(get().readStates, userId, risk, readAt);
|
|
set({ readStates: optimistic, error: undefined });
|
|
|
|
const task = readStateSaveQueue.then(async () => {
|
|
const remote = await loadServerData<XiaobaoWarningReadState[]>('xiaobao-warning-views');
|
|
const merged = mergeReadStates(normalizeReadStates(remote), get().readStates);
|
|
set({ readStates: merged, readStateLoaded: true, error: undefined });
|
|
await saveServerData('xiaobao-warning-views', merged);
|
|
});
|
|
readStateSaveQueue = task.catch(() => undefined);
|
|
|
|
try {
|
|
await task;
|
|
} catch (error) {
|
|
set({ error: '小宝预警查看状态保存失败' });
|
|
throw error;
|
|
}
|
|
},
|
|
}));
|