Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读 - 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移 - 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import { create } from 'zustand';
|
|
import type { XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
|
|
import {
|
|
markXiaobaoWarningRead,
|
|
type XiaobaoWarningReadState,
|
|
} from '@/lib/xiaobao-warning-view';
|
|
|
|
interface XiaobaoWarningReadStoreState {
|
|
readStates: XiaobaoWarningReadState[];
|
|
readStateLoaded: boolean;
|
|
error?: string;
|
|
fetchReadStates: () => Promise<void>;
|
|
markRiskRead: (userId: string, risk: XiaobaoVersionRisk, readAt?: string) => Promise<void>;
|
|
}
|
|
|
|
const READ_STATE_CACHE_MS = 30_000;
|
|
const READ_STATE_STORAGE_KEY = 'ftb_xiaobao_warning_views';
|
|
let lastReadStatesFetchAt = 0;
|
|
|
|
function loadReadStates(): XiaobaoWarningReadState[] {
|
|
try {
|
|
if (typeof window === 'undefined') return [];
|
|
return normalizeReadStates(JSON.parse(window.localStorage.getItem(READ_STATE_STORAGE_KEY) || '[]'));
|
|
} catch {}
|
|
return [];
|
|
}
|
|
|
|
function saveReadStates(rows: XiaobaoWarningReadState[]) {
|
|
try {
|
|
if (typeof window === 'undefined') return;
|
|
window.localStorage.setItem(READ_STATE_STORAGE_KEY, JSON.stringify(rows));
|
|
} catch {}
|
|
}
|
|
|
|
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 () => {
|
|
if (get().readStateLoaded && Date.now() - lastReadStatesFetchAt < READ_STATE_CACHE_MS) return;
|
|
const rows = loadReadStates();
|
|
if (get().readStateLoaded && Date.now() - lastReadStatesFetchAt < READ_STATE_CACHE_MS) return;
|
|
lastReadStatesFetchAt = Date.now();
|
|
set({
|
|
readStates: mergeReadStates(rows, get().readStates),
|
|
readStateLoaded: true,
|
|
error: 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 });
|
|
saveReadStates(optimistic);
|
|
},
|
|
}));
|