import { createStore } from "zustand/vanilla"; import { persist, createJSONStorage } from "zustand/middleware"; import type { CourseKind, RecordItem, RecordType, WorkListItem, ExamListItem } from "~/service/wk"; import type { CourseType } from "~/types/Course"; import type { userInfoType } from "~/types/Userinfo"; const withLogTimestamp = (message: string) => { if (/^\[\d{2}:\d{2}:\d{2}\]\s/.test(message)) { return message; } const now = new Date(); const timestamp = now.toLocaleTimeString("zh-CN", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); return `[${timestamp}] ${message}`; }; const MAX_STUDY_LOGS_PER_ACCOUNT = 1000; export type AccountAuth = { password: string; token: string; }; export type AccountItem = { id: string; username: string; host: string; status: CourseKind; sessionId: string; auth: AccountAuth; user: userInfoType; courses: CourseType[]; }; export type RecordCacheMap = Record; export type WorkExamCacheMap = Record; type PersistPreferences = { persistAccounts: boolean; persistRecords: boolean; persistLogs: boolean; }; const settingsStorageKey = "settings-storage"; const defaultPersistPreferences: PersistPreferences = { persistAccounts: true, persistRecords: true, persistLogs: true, }; const resolvePersistPreferences = (): PersistPreferences => { try { const rawValue = localStorage.getItem(settingsStorageKey); if (!rawValue) { return defaultPersistPreferences; } const parsedValue = JSON.parse(rawValue) as { state?: Partial; }; const persistedState = parsedValue?.state; return { persistAccounts: persistedState?.persistAccounts ?? defaultPersistPreferences.persistAccounts, persistRecords: persistedState?.persistRecords ?? defaultPersistPreferences.persistRecords, persistLogs: persistedState?.persistLogs ?? defaultPersistPreferences.persistLogs, }; } catch { return defaultPersistPreferences; } }; type AccountState = { accounts: AccountItem[]; selectedAccountId: string; expandedAccountId: string; selectedCourseId: number | null; courseKind: CourseKind; recordType: RecordType; records: RecordItem[]; recordCacheMap: RecordCacheMap; workList: WorkListItem[]; examList: ExamListItem[]; workExamCacheMap: WorkExamCacheMap; studyLogsMap: Record; runningStudyMap: Record; studyHeartbeatMap: Record; setSelectedAccountId: (accountId: string) => void; setExpandedAccountId: (accountId: string) => void; setSelectedCourseId: (courseId: number | null) => void; setCourseKind: (courseKind: CourseKind) => void; setRecordType: (recordType: RecordType) => void; setRecords: (records: RecordItem[]) => void; setRecordCache: (cacheKey: string, records: RecordItem[]) => void; setWorkList: (list: WorkListItem[]) => void; setExamList: (list: ExamListItem[]) => void; setWorkExamCache: (cacheKey: string, data: WorkListItem[] | ExamListItem[]) => void; setAccountRunningStudy: (accountId: string, value: boolean) => void; touchStudyHeartbeat: (accountId: string, timestamp?: number) => void; clearStudyHeartbeat: (accountId: string) => void; clearAllStudyHeartbeat: () => void; appendStudyLog: (accountId: string, message: string) => void; clearStudyLogs: (accountId: string) => void; clearAllStudyLogs: () => void; upsertAccount: (account: AccountItem) => void; setAccountCourses: (accountId: string, courses: CourseType[]) => void; removeAccount: (accountId: string) => void; clearAllData: () => void; clearRecordsData: () => void; clearAccountsData: () => void; }; export const accountStore = createStore()( persist( (set) => ({ accounts: [], selectedAccountId: "", expandedAccountId: "", selectedCourseId: null, courseKind: "run", recordType: "", records: [], recordCacheMap: {}, workList: [], examList: [], workExamCacheMap: {}, studyLogsMap: {}, runningStudyMap: {}, studyHeartbeatMap: {}, setSelectedAccountId: (accountId) => set({ selectedAccountId: accountId }), setExpandedAccountId: (accountId) => set({ expandedAccountId: accountId }), setSelectedCourseId: (courseId) => set({ selectedCourseId: courseId }), setCourseKind: (courseKind) => set({ courseKind }), setRecordType: (recordType) => set({ recordType }), setRecords: (records) => set({ records }), setRecordCache: (cacheKey, records) => set((state) => ({ recordCacheMap: { ...state.recordCacheMap, [cacheKey]: records, }, })), setWorkList: (list) => set({ workList: list }), setExamList: (list) => set({ examList: list }), setWorkExamCache: (cacheKey, data) => set((state) => ({ workExamCacheMap: { ...state.workExamCacheMap, [cacheKey]: data, }, })), setAccountRunningStudy: (accountId, value) => set((state) => ({ runningStudyMap: { ...state.runningStudyMap, [accountId]: value, }, })), touchStudyHeartbeat: (accountId, timestamp) => set((state) => ({ studyHeartbeatMap: { ...state.studyHeartbeatMap, [accountId]: timestamp ?? Date.now(), }, })), clearStudyHeartbeat: (accountId) => set((state) => ({ studyHeartbeatMap: Object.fromEntries( Object.entries(state.studyHeartbeatMap).filter( ([key]) => key !== accountId, ), ), })), clearAllStudyHeartbeat: () => set({ studyHeartbeatMap: {}, }), appendStudyLog: (accountId, message) => set((state) => { const nextLogs = [ ...(state.studyLogsMap[accountId] ?? []), withLogTimestamp(message), ].slice(-MAX_STUDY_LOGS_PER_ACCOUNT); return { studyLogsMap: { ...state.studyLogsMap, [accountId]: nextLogs, }, }; }), clearStudyLogs: (accountId) => set((state) => ({ studyLogsMap: { ...state.studyLogsMap, [accountId]: [], }, })), clearAllStudyLogs: () => set({ studyLogsMap: {}, }), clearAllData: () => set({ accounts: [], selectedAccountId: "", expandedAccountId: "", selectedCourseId: null, courseKind: "run" as CourseKind, recordType: "" as RecordType, records: [], recordCacheMap: {}, workList: [], examList: [], workExamCacheMap: {}, studyLogsMap: {}, runningStudyMap: {}, studyHeartbeatMap: {}, }), clearRecordsData: () => set({ records: [], recordCacheMap: {}, workList: [], examList: [], workExamCacheMap: {}, selectedCourseId: null, }), clearAccountsData: () => set({ accounts: [], selectedAccountId: "", expandedAccountId: "", selectedCourseId: null, }), upsertAccount: (account) => set((state) => ({ accounts: [ account, ...state.accounts.filter((item) => item.id !== account.id), ], selectedAccountId: account.id, expandedAccountId: account.id, })), setAccountCourses: (accountId, courses) => set((state) => ({ accounts: state.accounts.map((item) => item.id === accountId ? { ...item, courses } : item, ), })), removeAccount: (accountId) => set((state) => { const nextAccounts = state.accounts.filter( (item) => item.id !== accountId, ); return { accounts: nextAccounts, selectedAccountId: state.selectedAccountId === accountId ? "" : state.selectedAccountId, expandedAccountId: state.expandedAccountId === accountId ? "" : state.expandedAccountId, selectedCourseId: state.selectedAccountId === accountId ? null : state.selectedCourseId, records: state.selectedAccountId === accountId ? [] : state.records, recordCacheMap: Object.fromEntries( Object.entries(state.recordCacheMap).filter( ([key]) => !key.startsWith(`${accountId}::`), ), ), studyLogsMap: Object.fromEntries( Object.entries(state.studyLogsMap).filter( ([key]) => key !== accountId, ), ), runningStudyMap: Object.fromEntries( Object.entries(state.runningStudyMap).filter( ([key]) => key !== accountId, ), ), studyHeartbeatMap: Object.fromEntries( Object.entries(state.studyHeartbeatMap).filter( ([key]) => key !== accountId, ), ), }; }), }), { name: "account-storage", storage: createJSONStorage(() => localStorage), partialize: (state) => { const preferences = resolvePersistPreferences(); const persistedState: Partial = { courseKind: state.courseKind, recordType: state.recordType, }; if (preferences.persistAccounts) { persistedState.accounts = state.accounts; persistedState.selectedAccountId = state.selectedAccountId; persistedState.expandedAccountId = state.expandedAccountId; persistedState.selectedCourseId = state.selectedCourseId; } if (preferences.persistRecords) { persistedState.records = state.records; persistedState.recordCacheMap = state.recordCacheMap; } if (preferences.persistLogs) { persistedState.studyLogsMap = state.studyLogsMap; } return persistedState; }, }, ), );