Files
wk-frontend/src/store/account.ts
zhilv a1911573d1 feat: UI optimizations - button feedback, layout fixes, cache clearing, work/exam records
- Add active: state feedback to all buttons across the app
- Fix cache clearing to update Zustand store (not just localStorage)
- Remove checkboxes from settings cache section, compact layout
- Settings page: single outer scroll instead of dual-column scroll
- CourseWorkspace: elastic log panel height, work/exam record counts
- Integrate WorkList/ExamList types and display in UI
- Delete unused CourseList.tsx component
- Fix wk.ts: strict equality, remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 17:36:02 +08:00

337 lines
10 KiB
TypeScript

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<string, RecordItem[]>;
export type WorkExamCacheMap = Record<string, WorkListItem[] | ExamListItem[]>;
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<PersistPreferences>;
};
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<string, string[]>;
runningStudyMap: Record<string, boolean>;
studyHeartbeatMap: Record<string, number>;
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<AccountState>()(
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<AccountState> = {
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;
},
},
),
);