feat: build account dashboard and settings workspace

This commit is contained in:
2026-03-26 23:03:45 +08:00
parent be4bc8a3af
commit 8ee9a696b4
18 changed files with 2468 additions and 54 deletions

140
src/store/account.ts Normal file
View File

@@ -0,0 +1,140 @@
import { createStore } from "zustand/vanilla";
import { persist, createJSONStorage } from "zustand/middleware";
import type { CourseKind, RecordItem, RecordType } from "~/service/wk";
import type { CourseType } from "~/types/Course";
import type { userInfoType } from "~/types/Userinfo";
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[];
};
type AccountState = {
accounts: AccountItem[];
selectedAccountId: string;
expandedAccountId: string;
selectedCourseId: number | null;
recordType: RecordType;
records: RecordItem[];
studyLogsMap: Record<string, string[]>;
runningStudyMap: Record<string, boolean>;
setSelectedAccountId: (accountId: string) => void;
setExpandedAccountId: (accountId: string) => void;
setSelectedCourseId: (courseId: number | null) => void;
setRecordType: (recordType: RecordType) => void;
setRecords: (records: RecordItem[]) => void;
setAccountRunningStudy: (accountId: string, value: boolean) => void;
appendStudyLog: (accountId: string, message: string) => void;
clearStudyLogs: (accountId: string) => void;
upsertAccount: (account: AccountItem) => void;
removeAccount: (accountId: string) => void;
};
export const accountStore = createStore<AccountState>()(
persist(
(set) => ({
accounts: [],
selectedAccountId: "",
expandedAccountId: "",
selectedCourseId: null,
recordType: "",
records: [],
studyLogsMap: {},
runningStudyMap: {},
setSelectedAccountId: (accountId) =>
set({ selectedAccountId: accountId }),
setExpandedAccountId: (accountId) =>
set({ expandedAccountId: accountId }),
setSelectedCourseId: (courseId) => set({ selectedCourseId: courseId }),
setRecordType: (recordType) => set({ recordType }),
setRecords: (records) => set({ records }),
setAccountRunningStudy: (accountId, value) =>
set((state) => ({
runningStudyMap: {
...state.runningStudyMap,
[accountId]: value,
},
})),
appendStudyLog: (accountId, message) =>
set((state) => ({
studyLogsMap: {
...state.studyLogsMap,
[accountId]: [...(state.studyLogsMap[accountId] ?? []), message],
},
})),
clearStudyLogs: (accountId) =>
set((state) => ({
studyLogsMap: {
...state.studyLogsMap,
[accountId]: [],
},
})),
upsertAccount: (account) =>
set((state) => ({
accounts: [
account,
...state.accounts.filter((item) => item.id !== account.id),
],
selectedAccountId: account.id,
expandedAccountId: account.id,
})),
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,
studyLogsMap: Object.fromEntries(
Object.entries(state.studyLogsMap).filter(
([key]) => key !== accountId,
),
),
runningStudyMap: Object.fromEntries(
Object.entries(state.runningStudyMap).filter(
([key]) => key !== accountId,
),
),
};
}),
}),
{
name: "account-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
accounts: state.accounts,
selectedAccountId: state.selectedAccountId,
expandedAccountId: state.expandedAccountId,
selectedCourseId: state.selectedCourseId,
recordType: state.recordType,
records: state.records,
studyLogsMap: state.studyLogsMap,
runningStudyMap: state.runningStudyMap,
}),
},
),
);

127
src/store/settings.ts Normal file
View File

@@ -0,0 +1,127 @@
import { createStore } from "zustand/vanilla";
import { createJSONStorage, persist } from "zustand/middleware";
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;
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;
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 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,
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 });
},
clearPersistedSection: (section) => {
if (section === "accounts") {
localStorage.removeItem(accountStorageKey);
}
if (section === "records") {
set({ persistRecords: false });
queueMicrotask(() => set({ persistRecords: true }));
}
if (section === "logs") {
set({ persistLogs: false });
queueMicrotask(() => set({ persistLogs: true }));
}
},
clearAllPersistedData: () => {
localStorage.removeItem(accountStorageKey);
get().clearPersistedSection("records");
get().clearPersistedSection("logs");
},
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),
},
),
);