diff --git a/.gitignore b/.gitignore index ae31696..dd89608 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ dist-ssr *.sln *.sw? doc + +# Claude Code +.claude/ diff --git a/package.json b/package.json index 345d4c7..91cf99d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.1.3", + "version": "0.1.4", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.tsx b/src/App.tsx index c6e4953..656f9f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -137,9 +137,6 @@ const renderInlineLinks = (text: string): JSX.Element[] => { const App: ParentComponent = (props) => { const location = useLocation(); const [version] = createResource(versionApi); - const [copyState, setCopyState] = createSignal<"idle" | "done" | "error">( - "idle", - ); const [updateDialogOpen, setUpdateDialogOpen] = createSignal(false); const [updateCheckState, setUpdateCheckState] = createSignal("idle"); @@ -197,31 +194,6 @@ const App: ParentComponent = (props) => { return error instanceof Error ? error.message : "版本信息获取失败"; }); - const versionPayloadText = createMemo(() => - [ - `Version: ${versionText()}`, - `Mode: ${modeText()}`, - `Commit: ${commitText()}`, - `Build: ${buildText()}`, - `Author: ${authorText()}`, - `Email: ${emailText()}`, - ].join("\n"), - ); - const updateSummaryText = createMemo(() => { - if (updateCheckState() === "checking") { - return "更新检查中..."; - } - if (updateCheckState() === "available") { - return `发现新版本:${latestRelease()?.tag_name ?? "-"}`; - } - if (updateCheckState() === "latest") { - return "已是最新版本"; - } - if (updateCheckState() === "error") { - return updateCheckError() || "更新检查失败"; - } - return "未检查更新"; - }); const releaseNotesBlocks = createMemo(() => parseMarkdownBlocks(latestRelease()?.body ?? ""), ); @@ -231,6 +203,14 @@ const App: ParentComponent = (props) => { const releaseLink = createMemo( () => latestRelease()?.html_url || RELEASES_PAGE_URL, ); + const safeValue = (value: string) => (value === "unknown" ? "-" : value); + const hasUpdateBadge = createMemo(() => updateCheckState() === "available"); + const updateDialogTitle = createMemo(() => { + if (updateCheckState() === "available") { + return `发现更新 ${latestRelease()?.tag_name ?? ""}`; + } + return "更新信息"; + }); onMount(() => { const unsubscribe = settingsStore.subscribe((state) => { @@ -242,16 +222,6 @@ const App: ParentComponent = (props) => { }); }); - const handleCopyVersion = async () => { - try { - await navigator.clipboard.writeText(versionPayloadText()); - setCopyState("done"); - } catch { - setCopyState("error"); - } - - window.setTimeout(() => setCopyState("idle"), 1800); - }; const performUpdateCheck = async (manual = false) => { if (updateCheckState() === "checking") { return; @@ -276,17 +246,17 @@ const App: ParentComponent = (props) => { setRuntimeTarget(target); const hasNewVersion = isRemoteVersionNewer(versionText(), release.tag_name); + setLatestRelease(release); + setMatchedAsset(resolveAssetForRuntime(release.assets, target)); + if (!hasNewVersion) { setUpdateCheckState("latest"); if (manual) { - setLatestRelease(release); - setMatchedAsset(resolveAssetForRuntime(release.assets, target)); + setUpdateDialogOpen(true); } return; } - setLatestRelease(release); - setMatchedAsset(resolveAssetForRuntime(release.assets, target)); setUpdateCheckState("available"); setUpdateDialogOpen(true); } catch (error) { @@ -400,7 +370,7 @@ const App: ParentComponent = (props) => { class={ active ? "min-w-fit whitespace-nowrap rounded-2xl border border-cyan-200 bg-[linear-gradient(135deg,_rgba(34,211,238,0.18),_rgba(34,197,94,0.18))] px-4 py-3 text-sm font-medium text-zinc-900 shadow-sm" - : "min-w-fit whitespace-nowrap rounded-2xl border border-transparent px-4 py-3 text-sm font-medium text-zinc-600 transition hover:border-zinc-200 hover:bg-zinc-50/80 hover:text-zinc-900" + : "min-w-fit whitespace-nowrap rounded-2xl border border-transparent px-4 py-3 text-sm font-medium text-zinc-600 transition active:scale-[0.98] hover:border-zinc-200 hover:bg-zinc-50/80 hover:text-zinc-900" } >
@@ -425,48 +395,48 @@ const App: ParentComponent = (props) => {

{asideList().find((item) => isActive(item.url))?.label ?? "账号"}

-

- 模式: {modeText()} -

-

- 调试: {isDebugMode() ? "已开启" : "已关闭"} -

-

- Runtime -

-
-

Version: {versionText()}

-

Commit: {commitText()}

-

Build: {buildText()}

-

Author: {authorText()}

-

Email: {emailText()}

-
-

- 更新: {updateSummaryText()} -

-
+
- +
+ + 系统诊断信息 + +
+

+ Mode: {safeValue(modeText())} +

+

Commit: {safeValue(commitText())}

+

Build: {safeValue(buildText())}

+

Author: {safeValue(authorText())}

+

Email: {safeValue(emailText())}

+
+
+ {updateCheckState() === "error" ? ( +

+ {updateCheckError() || "更新检查失败"} +

+ ) : null} {versionErrorText() ? (

{versionErrorText()}

) : null} @@ -488,14 +458,14 @@ const App: ParentComponent = (props) => { } setUpdateDialogOpen(false); }} - title={`发现更新 ${latestRelease()?.tag_name ?? ""}`} + title={updateDialogTitle()} widthClass="max-w-3xl" closeOnOverlay={downloadState() !== "downloading"} footer={ <>
+
+ + 0}> +
+ 共 {props.workList.length} 条作业记录 +
+
+ 0}> +
+ 共 {props.examList.length} 条考试记录 +
+
{ !props.recordsLoading && !props.recordError && props.selectedCourse && + props.recordType === "" && filteredRecords().length === 0 } > @@ -370,59 +393,195 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => { -
- - {(record) => { - const stateText = - props.renderRecordState(record.state) || "未知状态"; - const learned = - stateText.includes("已学") || record.progress === "1.00"; + + 当前课程下没有作业记录。 + - return ( -
-
-
-

- {record.name} -

-

- 记录 ID:{record.id} | 章节:{record.chapterId} -

+ + 当前课程下没有考试记录。 + + + +
+ + {(record) => { + const stateText = + props.renderRecordState(record.state) || "未知状态"; + const learned = + stateText.includes("已学") || record.progress === "1.00"; + + return ( +
+
+
+

+ {record.name} +

+

+ 记录 ID:{record.id} | 章节:{record.chapterId} +

+
+ + {stateText} +
- - {stateText} - -
-
-

视频时长:{record.videoDuration}

-

学习秒数:{record.duration}

-

学习进度:{record.progress}

-

开始时间:{record.beginTime || "-"}

-

完成时间:{record.finalTime || "-"}

-

查看次数:{record.viewCount}

+
+

视频时长:{record.videoDuration}

+

学习秒数:{record.duration}

+

学习进度:{record.progress}

+

开始时间:{record.beginTime || "-"}

+

完成时间:{record.finalTime || "-"}

+

查看次数:{record.viewCount}

+
-
- ); - }} - -
+ ); + }} + +
+ + + +
+ + {(work) => { + const stateRaw = stripHtml(work.state); + const stateText = stateRaw || "未做"; + const done = stateRaw.includes("已阅") || stateRaw.includes("已提交") || stateRaw.includes("已完成"); + + return ( +
+
+
+

+ {work.title || work.name} +

+

+ 作业 ID:{work.id} | 章节:{work.chapterId} +

+
+ + {stateText} + +
+ +
+

类型:{work.typeName || work.type || "-"}

+

总分:{work.score || "-"}

+

得分:{stripHtml(work.finalScore) || "-"}

+

题目数:{work.topicNumber || "-"}

+

添加时间:{work.addTime || "-"}

+

完成时间:{work.finishTime !== "-" ? work.finishTime : "-"}

+
+
+ ); + }} +
+
+
+ + +
+ + {(exam) => { + const stateRaw = stripHtml(exam.state); + const stateText = stateRaw || "未做"; + const done = stateRaw.includes("已阅") || stateRaw.includes("已提交") || stateRaw.includes("已完成"); + + return ( +
+
+
+

+ {exam.title || exam.name} +

+

+ 考试 ID:{exam.id} | 章节:{exam.chapterId} +

+
+ + {stateText} + +
+ +
+

限时:{exam.limitedTime ? `${exam.limitedTime}分钟` : "-"}

+

总分:{exam.score || "-"}

+

得分:{stripHtml(exam.finalScore) || "-"}

+

题目数:{exam.topicNumber || "-"}

+

添加时间:{exam.addTime || "-"}

+

完成时间:{exam.finishTime !== "-" ? exam.finishTime : "-"}

+
+
+ ); + }} +
+
+
@@ -440,7 +599,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
+
+ +
+
+
+
+

账号缓存

+

账号、课程和登录信息

+
+ +
+
+ +
+
+
+

记录缓存

+

课程记录和筛选类型

+
+ +
+
+ +
+
+
+

日志缓存

+

任务日志历史

+
+ +
+
+
+ + +
+
+

界面偏好

- 控制哪些数据会保存在本地,以及如何清理缓存 + 根据使用习惯调整日志行为、展示密度和侧栏尺寸

- -
-
-
-
-
-

账号缓存

-

- 保留账号、课程和登录相关信息 +

+
+
+
+

手动开启调试

+

+ 开启后显示后端日志页,并应用本地代理 / SSL 调试配置 +

+
+ + void handleDebugToggle(event.currentTarget.checked) + } + /> +
+
+

后端状态:{backendDebugState()?.enabled ? "已开启" : "已关闭"}

+

编译模式:{backendDebugState()?.buildMode ?? "-"}

+

+ 本地代理: + {backendDebugState()?.proxyConfigured + ? backendDebugState()?.proxy + : "未配置"} +

+

+ 跳过 SSL 校验: + {backendDebugState()?.skipSSLVerify ? "已配置" : "未配置"}

- - settingsStore - .getState() - .setPersistSection( - "accounts", - event.currentTarget.checked, - ) - } - /> + {debugError() ? ( +
+ {debugError()} +
+ ) : null}
- -
-
-
-
-

记录缓存

-

- 保留当前课程记录和筛选类型 -

+
+
+
+

日志自动滚动

+

新日志出现时自动滚动到底部

+
+ + settingsStore + .getState() + .setAutoScrollLogs(event.currentTarget.checked) + } + />
- - settingsStore - .getState() - .setPersistSection( - "records", - event.currentTarget.checked, - ) - } - />
- -
-
-
-
-

日志缓存

-

- 保留任务日志历史,刷新后继续查看 -

+
+
+
+

显示日志时间戳

+

日志输出追加格式化时间

+
+ + settingsStore + .getState() + .setShowLogTimestamps(event.currentTarget.checked) + } + />
- - settingsStore - .getState() - .setPersistSection("logs", event.currentTarget.checked) - } - />
- + +
+ +
+ +
+ +
+ +
+ +
-
+
-
-
-

界面偏好

-

- 根据你的使用习惯调整侧栏、日志和展示密度 -

-
+
+
+
+

Host 配置策略

+

+ 远端 Host 后续通过接口请求,本地 Host 手动添加,最终合并并去重,优先保留本地配置。 +

+
-
-
+
-
-

手动开启调试

-

- 开启后显示后端日志页,并应用本地代理 / SSL 调试配置 -

-
+

添加本地 Host

+ +
+
- void handleDebugToggle(event.currentTarget.checked) - } + class="rounded-xl border border-zinc-200 bg-white px-4 py-3 transition outline-none focus:border-cyan-400" + value={hostLabel()} + onInput={(event) => setHostLabel(event.currentTarget.value)} + placeholder="名称,如:校内测试" + /> + setHostValue(event.currentTarget.value)} + placeholder="Host,如:example.com" />
-
-

后端状态:{backendDebugState()?.enabled ? "已开启" : "已关闭"}

-

- 编译模式:{backendDebugState()?.buildMode ?? "-"} -

-

- 本地代理: - {backendDebugState()?.proxyConfigured - ? backendDebugState()?.proxy - : "未配置"} -

-

- 跳过 SSL 校验: - {backendDebugState()?.skipSSLVerify ? "已配置" : "未配置"} -

-
- {debugError() ? ( + + + {hostError() ? (
- {debugError()} + {hostError()}
) : null}
+
-
-
-
-

日志自动滚动

-

- 新日志出现时自动滚动到最底部 -

-
- - settingsStore - .getState() - .setAutoScrollLogs(event.currentTarget.checked) - } - /> -
+
+
+

Host 列表

+

本地配置优先覆盖远端

-
-
-
-

显示日志时间戳

-

- 后续日志输出可以追加格式化时间 -

-
- - settingsStore - .getState() - .setShowLogTimestamps(event.currentTarget.checked) - } - /> -
-
- -
- -
- -
- -
- -
- -
-
-
- - -
-
-
-

Host 配置策略

-

- 远端 Host 后续通过接口请求,本地 Host - 手动添加,最终合并并去重,优先保留本地配置。 -

-
- -
-
-

添加本地 Host

- -
-
- setHostLabel(event.currentTarget.value)} - placeholder="名称,如:校内测试" - /> - setHostValue(event.currentTarget.value)} - placeholder="Host,如:example.com" - /> -
- - - {hostError() ? ( -
- {hostError()} -
- ) : null} -
- -
-
-

本地 Host

-
- - {(item) => ( -
-
-
-

- {item.label} -

-

- {item.host} -

+
+
+

本地 Host

+
+ + {(item) => ( +
+
+
+

{item.label}

+

{item.host}

+
+
-
-
- )} - + )} + +
-
-
-

合并结果

-
- - {(item) => ( -
-

{item.label}

-

{item.host}

-

- 来源:{item.source === "local" ? "本地优先" : "远端"} -

-
- )} -
+
+

合并结果

+
+ + {(item) => ( +
+

{item.label}

+

{item.host}

+

+ 来源:{item.source === "local" ? "本地优先" : "远端"} +

+
+ )} +
+
-
+
- +
); diff --git a/src/service/silentAudio.ts b/src/service/silentAudio.ts new file mode 100644 index 0000000..25fc18a --- /dev/null +++ b/src/service/silentAudio.ts @@ -0,0 +1,57 @@ +/** + * Silent audio playback to prevent browser tab throttling during long-running tasks. + * + * Uses the Web Audio API to produce a nearly inaudible signal that keeps the + * browser from suspending the tab's timers and network requests. + */ + +let audioContext: AudioContext | null = null; +let oscillatorNode: OscillatorNode | null = null; +let gainNode: GainNode | null = null; + +/** + * Start playing silent audio. Safe to call multiple times — duplicate calls + * are ignored if audio is already playing. + */ +export const startSilentAudio = () => { + if (oscillatorNode) { + return; + } + + try { + audioContext = new AudioContext(); + gainNode = audioContext.createGain(); + gainNode.gain.value = 0.001; // Nearly silent + + oscillatorNode = audioContext.createOscillator(); + oscillatorNode.type = "sine"; + oscillatorNode.frequency.value = 1; // Sub-bass, inaudible + oscillatorNode.connect(gainNode); + gainNode.connect(audioContext.destination); + oscillatorNode.start(); + } catch { + // AudioContext may be unavailable in some environments; degrade silently. + oscillatorNode = null; + gainNode = null; + audioContext = null; + } +}; + +/** + * Stop playing silent audio and release resources. + */ +export const stopSilentAudio = () => { + try { + oscillatorNode?.stop(); + } catch { + // Already stopped or never started + } + + oscillatorNode?.disconnect(); + gainNode?.disconnect(); + audioContext?.close(); + + oscillatorNode = null; + gainNode = null; + audioContext = null; +}; diff --git a/src/service/wk.ts b/src/service/wk.ts index 4f5e16c..c65b6da 100644 --- a/src/service/wk.ts +++ b/src/service/wk.ts @@ -1,6 +1,5 @@ import type { Accessor } from "solid-js"; import http, { - DEFAULT_HTTP_TIMEOUT_MS, createHttpClient, type HttpClient, } from "~/service/http"; @@ -13,6 +12,80 @@ export type RecordType = "" | "/work" | "/exam" | "/discuss"; export type StudyStatus = 1 | 2 | 3; +export type WorkListItem = { + id: string; + userId: string | number | null; + title: string; + topicNumber: string; + score: string; + type: string; + remarks: string; + addTime: string; + sequence: string; + nodeId: string; + courseId: string; + startTime: string; + endTime: string; + paperId: string; + createUserId: string; + isPrivate: string; + classList: string; + teacherType: string; + allow: string; + frequency: string; + scoringRules: string; + hasCollect: string; + lock: string | number | null; + schoolId: string; + parsing: string; + addDate: string; + name: string; + chapterId: string; + state: string; + submitTime: string; + finalScore: string; + typeName: string; + finishTime: string; + url: string; +}; + +export type ExamListItem = { + id: string; + userId: string | number | null; + title: string; + topicNumber: string; + score: string; + addTime: string; + nodeId: string; + courseId: string; + limitedTime: string; + sequence: string; + remarks: string; + paperId: string; + startTime: string; + endTime: string; + createUserId: string; + classList: string; + isPrivate: string; + teacherType: string; + allow: string; + frequency: string; + hasCollect: string; + schoolId: string; + parsing: string; + addDate: string; + random: string; + randData: unknown; + randNumber: string; + name: string; + chapterId: string; + state: string; + submitTime: string; + finalScore: string; + finishTime: string; + url: string; +}; + type ApiResponse = { code: number; message: string; @@ -165,16 +238,33 @@ export type StudyRunnerPayload = { onLog?: (message: string, accoundID: string) => void; }; +export type WorkListData = { + list: WorkListItem[]; + page_info: PageInfo; +}; + +export type WorkListRes = ApiResponse; + +export type ExamListData = { + list: ExamListItem[]; + page_info: PageInfo; +}; + +export type ExamListRes = ApiResponse; + export type WkClient = { userInfoApi: () => Promise; courseApi: (payload: CourseReq) => Promise; recordApi: (payload: RecordReq) => Promise; + workListApi: (payload: RecordReq) => Promise; + examListApi: (payload: RecordReq) => Promise; studyApi: (payload: StudyReq) => Promise; logoutApi: () => Promise; }; const RECORD_API_TIMEOUT_MS = 60000; -const COURSE_API_TIMEOUT_MS = Math.max(DEFAULT_HTTP_TIMEOUT_MS, 30000); +// Course list can be slow on large accounts, use a longer timeout than default +const COURSE_API_TIMEOUT_MS = 30000; export const loginApi = async (payload: LoginReq) => { const res = await http.post("/api/login", payload); @@ -195,6 +285,22 @@ const createWkClientFromHttp = (client: HttpClient): WkClient => ({ timeout: RECORD_API_TIMEOUT_MS, }); }, + workListApi(payload) { + return client.post("/api/v2/record", { + ...payload, + record_type: "/work", + }, { + timeout: RECORD_API_TIMEOUT_MS, + }); + }, + examListApi(payload) { + return client.post("/api/v2/record", { + ...payload, + record_type: "/exam", + }, { + timeout: RECORD_API_TIMEOUT_MS, + }); + }, studyApi(payload) { return client.post("/api/v2/study", payload); }, @@ -313,7 +419,7 @@ export const runStudyQueue = async (_payload: StudyRunnerPayload) => { continue; } - if (resp.data.state != 0) { + if (resp.data.state !== 0) { _payload.onLog?.(`⛔ 自动停止: ${resp.data.msg}`, _payload.accountId); _payload.setIsRunningStudy(); return; diff --git a/src/store/account.ts b/src/store/account.ts index b24f540..a4de26b 100644 --- a/src/store/account.ts +++ b/src/store/account.ts @@ -1,6 +1,6 @@ import { createStore } from "zustand/vanilla"; import { persist, createJSONStorage } from "zustand/middleware"; -import type { CourseKind, RecordItem, RecordType } from "~/service/wk"; +import type { CourseKind, RecordItem, RecordType, WorkListItem, ExamListItem } from "~/service/wk"; import type { CourseType } from "~/types/Course"; import type { userInfoType } from "~/types/Userinfo"; @@ -39,6 +39,8 @@ export type AccountItem = { export type RecordCacheMap = Record; +export type WorkExamCacheMap = Record; + type PersistPreferences = { persistAccounts: boolean; persistRecords: boolean; @@ -87,6 +89,9 @@ type AccountState = { recordType: RecordType; records: RecordItem[]; recordCacheMap: RecordCacheMap; + workList: WorkListItem[]; + examList: ExamListItem[]; + workExamCacheMap: WorkExamCacheMap; studyLogsMap: Record; runningStudyMap: Record; studyHeartbeatMap: Record; @@ -97,6 +102,9 @@ type AccountState = { 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; @@ -107,6 +115,9 @@ type AccountState = { upsertAccount: (account: AccountItem) => void; setAccountCourses: (accountId: string, courses: CourseType[]) => void; removeAccount: (accountId: string) => void; + clearAllData: () => void; + clearRecordsData: () => void; + clearAccountsData: () => void; }; export const accountStore = createStore()( @@ -120,6 +131,9 @@ export const accountStore = createStore()( recordType: "", records: [], recordCacheMap: {}, + workList: [], + examList: [], + workExamCacheMap: {}, studyLogsMap: {}, runningStudyMap: {}, studyHeartbeatMap: {}, @@ -138,6 +152,15 @@ export const accountStore = createStore()( [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: { @@ -189,6 +212,39 @@ export const accountStore = createStore()( 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: [ diff --git a/src/store/settings.ts b/src/store/settings.ts index ceed1f8..8441379 100644 --- a/src/store/settings.ts +++ b/src/store/settings.ts @@ -1,5 +1,6 @@ import { createStore } from "zustand/vanilla"; import { createJSONStorage, persist } from "zustand/middleware"; +import { accountStore } from "~/store/account"; export type HostOption = { label: string; @@ -37,6 +38,7 @@ type SettingsState = { }; const accountStorageKey = "account-storage"; +const settingsStorageKey = "settings-storage"; type PersistedStorage = { state?: Record; version?: number; @@ -115,53 +117,40 @@ export const settingsStore = createStore()( } }, clearPersistedSection: (section) => { + const store = accountStore.getState(); + if (section === "accounts") { - patchAccountStorage((state) => { - const { - accounts, - selectedAccountId, - expandedAccountId, - selectedCourseId, - records, - recordCacheMap, - studyLogsMap, - runningStudyMap, - ...rest - } = state; - void accounts; - void selectedAccountId; - void expandedAccountId; - void selectedCourseId; - void records; - void recordCacheMap; - void studyLogsMap; - void runningStudyMap; - return rest; - }); + store.clearAccountsData(); + localStorage.removeItem(accountStorageKey); } if (section === "records") { + store.clearRecordsData(); patchAccountStorage((state) => { - const { records, recordCacheMap, selectedCourseId, ...rest } = - state; + const { records, recordCacheMap, workList, examList, workExamCacheMap, ...rest } = state; void records; void recordCacheMap; - void selectedCourseId; + void workList; + void examList; + void workExamCacheMap; return rest; }); } if (section === "logs") { + store.clearAllStudyLogs(); patchAccountStorage((state) => { - const { studyLogsMap, runningStudyMap, ...rest } = state; + const { studyLogsMap, ...rest } = state; void studyLogsMap; - void runningStudyMap; 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 }),