From a1911573d19d41b3d2369cae0b6849ef8bb5c394 Mon Sep 17 00:00:00 2001 From: zhilv Date: Sun, 26 Apr 2026 17:36:02 +0800 Subject: [PATCH 1/4] 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 --- src/App.tsx | 12 +- src/components/account/AccountSidebar.tsx | 10 +- src/components/account/AddAccountDialog.tsx | 4 +- src/components/account/CourseWorkspace.tsx | 273 +++++++++++++++----- src/components/courseList/CourseList.tsx | 39 --- src/components/dialog/Dialog.tsx | 2 +- src/pages/accounts/Account.tsx | 143 +++++++--- src/pages/debug-logs/DebugLogs.tsx | 14 +- src/pages/logs/Logs.tsx | 2 +- src/pages/settings/Setting.tsx | 193 ++++++-------- src/service/wk.ts | 112 +++++++- src/store/account.ts | 58 ++++- src/store/settings.ts | 43 ++- 13 files changed, 603 insertions(+), 302 deletions(-) delete mode 100644 src/components/courseList/CourseList.tsx diff --git a/src/App.tsx b/src/App.tsx index c6e4953..a2a0c4e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -400,7 +400,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" } >
@@ -449,7 +449,7 @@ const App: ParentComponent = (props) => {
+
+ + 0}> +
+ 共 {props.workList.length} 条作业记录 +
+
+ 0}> +
+ 共 {props.examList.length} 条考试记录 +
+
{ !props.recordsLoading && !props.recordError && props.selectedCourse && + props.recordType === "" && filteredRecords().length === 0 } > @@ -370,59 +387,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 +593,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
-
-
-
-
-
-

本地数据

-

- 控制哪些数据会保存在本地,以及如何清理缓存 -

+
+
+
+
+
+
+

本地数据

+

+ 管理本地缓存数据 +

+
+ +
+ +
+
+
+

账号缓存

+

账号、课程和登录信息

+
+ +
+ +
+
+

记录缓存

+

课程记录和筛选类型

+
+ +
+ +
+
+

日志缓存

+

任务日志历史

+
+ +
-
-
-
-
-
-

账号缓存

-

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

-
- - settingsStore - .getState() - .setPersistSection( - "accounts", - event.currentTarget.checked, - ) - } - /> -
- -
- -
-
-
-

记录缓存

-

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

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

日志缓存

-

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

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

界面偏好

根据你的使用习惯调整侧栏、日志和展示密度 @@ -419,9 +374,9 @@ const Setting = () => {

-
+
-
+

Host 配置策略

@@ -436,7 +391,7 @@ const Setting = () => {

添加本地 Host

- + ); 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 }), -- 2.49.1 From 5d4e0f493c58fedbad0da478bc29e9579bd08e28 Mon Sep 17 00:00:00 2001 From: zhilv Date: Sun, 26 Apr 2026 17:39:50 +0800 Subject: [PATCH 2/4] chore: add .claude/ to .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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/ -- 2.49.1 From 0c0d2a0292fdcf0ed991320059020899e7b1e596 Mon Sep 17 00:00:00 2001 From: zhilv Date: Sun, 26 Apr 2026 17:55:27 +0800 Subject: [PATCH 3/4] feat: add silent audio playback to prevent browser tab throttling during study Play a nearly inaudible Web Audio API signal when study starts, stop it when study completes, is stopped, or fails. This prevents browsers from throttling timers and network requests in background tabs. Co-Authored-By: Claude Opus 4.6 --- src/pages/accounts/Account.tsx | 5 +++ src/service/silentAudio.ts | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/service/silentAudio.ts diff --git a/src/pages/accounts/Account.tsx b/src/pages/accounts/Account.tsx index c46587b..3ff9dd6 100644 --- a/src/pages/accounts/Account.tsx +++ b/src/pages/accounts/Account.tsx @@ -24,6 +24,7 @@ import { type ExamListItem, } from "~/service/wk"; import { setUnauthorizedHandler } from "~/service/http"; +import { startSilentAudio, stopSilentAudio } from "~/service/silentAudio"; import { accountStore, type AccountItem } from "~/store/account"; import { getMergedHosts, settingsStore } from "~/store/settings"; import type { CourseType } from "~/types/Course"; @@ -722,6 +723,7 @@ const Account = () => { try { accountStore.getState().setAccountRunningStudy(account.id, true); touchStudyHeartbeat(account.id); + startSilentAudio(); appendStudyLog(`开始刷课:${course.name}`, account.id); await runStudyQueue({ accountId: account.id, @@ -734,6 +736,7 @@ const Account = () => { setIsRunningStudy: () => { accountStore.getState().setAccountRunningStudy(account.id, false); clearStudyHeartbeat(account.id); + stopSilentAudio(); }, onLog: (message: string, accoundID: string) => { touchStudyHeartbeat(accoundID); @@ -751,6 +754,7 @@ const Account = () => { } finally { accountStore.getState().setAccountRunningStudy(account.id, false); clearStudyHeartbeat(account.id); + stopSilentAudio(); } }; @@ -762,6 +766,7 @@ const Account = () => { accountStore.getState().setAccountRunningStudy(account.id, false); clearStudyHeartbeat(account.id); + stopSilentAudio(); appendStudyLog(`已发送停止刷课指令:${account.user.name}`, account.id); }; 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; +}; -- 2.49.1 From a182c64f82757e29bb63033d27e732ed5254d810 Mon Sep 17 00:00:00 2001 From: zhilv Date: Sun, 26 Apr 2026 20:45:39 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=96=20release(v0.1.4):=20bump=20ve?= =?UTF-8?q?rsion=20and=20UI=20optimizations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused version display logic and update summary - Add silent audio playback to prevent browser tab throttling - Update CourseWorkspace and Setting components - Bump version from 0.1.3 to 0.1.4 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/App.tsx | 126 ++--- src/components/account/CourseWorkspace.tsx | 8 +- src/pages/settings/Setting.tsx | 614 ++++++++++----------- 4 files changed, 362 insertions(+), 388 deletions(-) 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 a2a0c4e..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) { @@ -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,7 +458,7 @@ const App: ParentComponent = (props) => { } setUpdateDialogOpen(false); }} - title={`发现更新 ${latestRelease()?.tag_name ?? ""}`} + title={updateDialogTitle()} widthClass="max-w-3xl" closeOnOverlay={downloadState() !== "downloading"} footer={ diff --git a/src/components/account/CourseWorkspace.tsx b/src/components/account/CourseWorkspace.tsx index 3e1125e..d14c7f8 100644 --- a/src/components/account/CourseWorkspace.tsx +++ b/src/components/account/CourseWorkspace.tsx @@ -283,7 +283,13 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => { > {(item) => ( - + )} diff --git a/src/pages/settings/Setting.tsx b/src/pages/settings/Setting.tsx index 8ff17e6..1639086 100644 --- a/src/pages/settings/Setting.tsx +++ b/src/pages/settings/Setting.tsx @@ -1,4 +1,4 @@ -import { createMemo, createSignal, For, onCleanup, onMount } from "solid-js"; +import { createMemo, createSignal, For, onCleanup, onMount } from "solid-js"; import { fetchDebugConfig, updateDebugConfig } from "~/service/debugLog"; import { hostApi } from "~/service/wk"; import { getMergedHosts, settingsStore } from "~/store/settings"; @@ -117,367 +117,365 @@ const Setting = () => { void refreshDebugConfig(); }); + const panelClass = + "rounded-[28px] border border-white/80 bg-white/85 p-5 shadow-[0_18px_50px_-28px_rgba(15,23,42,0.25)]"; + const sectionCardClass = "rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4"; + return (
-
+
-

- Settings Center +

+ Preference Center

偏好设置

- 管理本地缓存、界面偏好和 Host 来源策略 + 统一管理本地缓存、界面体验和 Host 来源策略

-
-

本地优先

-

+

+

策略:本地优先

+

远端 Host 获取后会与本地列表合并去重

-
-
-
-
+
+
+
+
-

本地数据

-

- 管理本地缓存数据 -

+

本地数据

+

管理本地缓存数据

-
-
-
-

账号缓存

-

账号、课程和登录信息

+
+
+
+
+

账号缓存

+

账号、课程和登录信息

+
+
-
-
-
-

记录缓存

-

课程记录和筛选类型

+
+
+
+

记录缓存

+

课程记录和筛选类型

+
+
-
-
-
-

日志缓存

-

任务日志历史

+
+
+
+

日志缓存

+

任务日志历史

+
+
-
-
+
-
-

界面偏好

-

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

-
+
+
+

界面偏好

+

+ 根据使用习惯调整日志行为、展示密度和侧栏尺寸 +

+
-
-
-
-
-

手动开启调试

-

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

+
+
+
+

手动开启调试

+

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

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

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

+

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

+

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

+

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

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

日志自动滚动

+

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

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

显示日志时间戳

+

日志输出追加格式化时间

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

Host 配置策略

+

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

+
+ +
+
+

添加本地 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" ? "本地优先" : "远端"} +

+
+ )} +
+
-
+
-- 2.49.1