- 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>
186 lines
5.5 KiB
TypeScript
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),
|
|
},
|
|
),
|
|
);
|