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