Files
wk-frontend/src/store/settings.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

186 lines
5.5 KiB
TypeScript

import { createStore } from "zustand/vanilla";
import { createJSONStorage, persist } from "zustand/middleware";
import { accountStore } from "~/store/account";
export type HostOption = {
label: string;
host: string;
source: "local" | "remote";
};
type CacheSection = "accounts" | "records" | "logs";
type DensityMode = "comfortable" | "compact";
type SettingsState = {
persistAccounts: boolean;
persistRecords: boolean;
persistLogs: boolean;
debugEnabled: boolean;
autoScrollLogs: boolean;
showLogTimestamps: boolean;
densityMode: DensityMode;
logFontSize: number;
sidebarWidth: number;
localHosts: HostOption[];
remoteHosts: HostOption[];
setPersistSection: (section: CacheSection, value: boolean) => void;
clearPersistedSection: (section: CacheSection) => void;
clearAllPersistedData: () => void;
setDebugEnabled: (value: boolean) => void;
setAutoScrollLogs: (value: boolean) => void;
setShowLogTimestamps: (value: boolean) => void;
setDensityMode: (value: DensityMode) => void;
setLogFontSize: (value: number) => void;
setSidebarWidth: (value: number) => void;
addLocalHost: (host: Omit<HostOption, "source">) => void;
removeLocalHost: (host: string) => void;
setRemoteHosts: (hosts: Omit<HostOption, "source">[]) => void;
};
const accountStorageKey = "account-storage";
const settingsStorageKey = "settings-storage";
type PersistedStorage = {
state?: Record<string, unknown>;
version?: number;
};
const patchAccountStorage = (
patcher: (state: Record<string, unknown>) => Record<string, unknown>,
) => {
const rawValue = localStorage.getItem(accountStorageKey);
if (!rawValue) {
return;
}
try {
const parsedValue = JSON.parse(rawValue) as PersistedStorage;
const currentState =
parsedValue.state && typeof parsedValue.state === "object"
? parsedValue.state
: {};
const nextState = patcher(currentState);
localStorage.setItem(
accountStorageKey,
JSON.stringify({
...parsedValue,
state: nextState,
}),
);
} catch {
localStorage.removeItem(accountStorageKey);
}
};
const uniqueHosts = (hosts: HostOption[]) => {
const map = new Map<string, HostOption>();
for (const item of hosts) {
if (!map.has(item.host)) {
map.set(item.host, item);
}
}
return Array.from(map.values());
};
export const getMergedHosts = (
localHosts: HostOption[],
remoteHosts: HostOption[],
) => {
return uniqueHosts([...localHosts, ...remoteHosts]);
};
export const settingsStore = createStore<SettingsState>()(
persist(
(set, get) => ({
persistAccounts: true,
persistRecords: true,
persistLogs: true,
debugEnabled: false,
autoScrollLogs: true,
showLogTimestamps: false,
densityMode: "comfortable",
logFontSize: 12,
sidebarWidth: 320,
localHosts: [
{ label: "默认站点", host: "cqcst.leykeji.com", source: "local" },
],
remoteHosts: [],
setPersistSection: (section, value) => {
if (section === "accounts") set({ persistAccounts: value });
if (section === "records") set({ persistRecords: value });
if (section === "logs") set({ persistLogs: value });
if (!value) {
queueMicrotask(() => get().clearPersistedSection(section));
}
},
clearPersistedSection: (section) => {
const store = accountStore.getState();
if (section === "accounts") {
store.clearAccountsData();
localStorage.removeItem(accountStorageKey);
}
if (section === "records") {
store.clearRecordsData();
patchAccountStorage((state) => {
const { records, recordCacheMap, workList, examList, workExamCacheMap, ...rest } = state;
void records;
void recordCacheMap;
void workList;
void examList;
void workExamCacheMap;
return rest;
});
}
if (section === "logs") {
store.clearAllStudyLogs();
patchAccountStorage((state) => {
const { studyLogsMap, ...rest } = state;
void studyLogsMap;
return rest;
});
}
},
clearAllPersistedData: () => {
accountStore.getState().clearAllData();
localStorage.removeItem(accountStorageKey);
localStorage.removeItem(settingsStorageKey);
window.location.reload();
},
setDebugEnabled: (value) => set({ debugEnabled: value }),
setAutoScrollLogs: (value) => set({ autoScrollLogs: value }),
setShowLogTimestamps: (value) => set({ showLogTimestamps: value }),
setDensityMode: (value) => set({ densityMode: value }),
setLogFontSize: (value) => set({ logFontSize: value }),
setSidebarWidth: (value) => set({ sidebarWidth: value }),
addLocalHost: (host) =>
set((state) => ({
localHosts: uniqueHosts([
{ ...host, source: "local" },
...state.localHosts,
...state.remoteHosts,
]).filter((item) => item.source === "local"),
})),
removeLocalHost: (host) =>
set((state) => ({
localHosts: state.localHosts.filter((item) => item.host !== host),
})),
setRemoteHosts: (hosts) =>
set({
remoteHosts: uniqueHosts(
hosts.map((item) => ({ ...item, source: "remote" as const })),
),
}),
}),
{
name: "settings-storage",
storage: createJSONStorage(() => localStorage),
},
),
);