diff --git a/src/App.tsx b/src/App.tsx index a6b2567..9318fdd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,7 @@ import type { ParentComponent } from "solid-js"; +import { createMemo, createResource, createSignal } from "solid-js"; import { A, useLocation } from "@solidjs/router"; +import { versionApi } from "~/service/wk"; const asideList = [ { label: "账号", url: "/account" }, @@ -9,41 +11,75 @@ const asideList = [ const App: ParentComponent = (props) => { const location = useLocation(); + const [version] = createResource(versionApi); + const [copyState, setCopyState] = createSignal<"idle" | "done" | "error">( + "idle", + ); const isActive = (url: string) => location.pathname === url || (location.pathname === "/" && url === "/account"); + const versionText = createMemo(() => version()?.data.Version ?? "unknown"); + const commitText = createMemo(() => { + const commit = version()?.data.GitCommit ?? "unknown"; + return commit === "unknown" ? commit : commit.slice(0, 7); + }); + const buildText = createMemo(() => version()?.data.BuildAt ?? "unknown"); + const authorText = createMemo(() => version()?.data.GitAuthor ?? "unknown"); + const emailText = createMemo(() => version()?.data.GitEmail ?? "unknown"); + const versionErrorText = createMemo(() => { + const error = version.error; + if (!error) { + return ""; + } + + return error instanceof Error ? error.message : "版本信息获取失败"; + }); + const versionPayloadText = createMemo(() => + [ + `Version: ${versionText()}`, + `Commit: ${commitText()}`, + `Build: ${buildText()}`, + `Author: ${authorText()}`, + `Email: ${emailText()}`, + ].join("\n"), + ); + + const handleCopyVersion = async () => { + try { + await navigator.clipboard.writeText(versionPayloadText()); + setCopyState("done"); + } catch { + setCopyState("error"); + } + + window.setTimeout(() => setCopyState("idle"), 1800); + }; + return (
-
-
+
+
WK
网课控制台 -

+

管理账号、课程记录与任务日志

- -
-

学习控制中枢

-

- 多账号管理 / 课程记录 / 日志面板 -

-
@@ -92,6 +128,28 @@ const App: ParentComponent = (props) => {

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

+

+ Runtime +

+

Version: {versionText()}

+

Commit: {commitText()}

+

Build: {buildText()}

+

Author: {authorText()}

+

Email: {emailText()}

+ + {versionErrorText() ? ( +

{versionErrorText()}

+ ) : null}
diff --git a/src/components/account/AccountSidebar.tsx b/src/components/account/AccountSidebar.tsx index 4112d03..9be109a 100644 --- a/src/components/account/AccountSidebar.tsx +++ b/src/components/account/AccountSidebar.tsx @@ -12,6 +12,7 @@ interface AccountSidebarProps { selectedAccountId: string; expandedAccountId: string; statusOptions: StatusOption[]; + currentCourseKind: CourseKind; hostLabels: Record; isRefreshingAccount: boolean; loggingOutId: string; @@ -66,6 +67,16 @@ const AccountSidebar = (props: AccountSidebarProps) => { const statusLabel = props.statusOptions.find((item) => item.value === account.status) ?.label ?? account.status; + const currentCourseLabel = + props.statusOptions.find( + (item) => item.value === props.currentCourseKind, + )?.label ?? props.currentCourseKind; + const courseCountLabel = selected() + ? `当前筛选课程数:${account.courses.length}` + : `缓存课程数:${account.courses.length}`; + const courseTypeLabel = selected() + ? `当前筛选:${currentCourseLabel}` + : `登录类型:${statusLabel}`; return (
{

性别:{account.user.gender}

站点:{account.host}

账号:{account.username || "-"}

-

课程数:{account.courses.length}

-

课程类型:{statusLabel}

+

{courseCountLabel}

+

{courseTypeLabel}

+
{ : "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3" } > + + + 当前“{props.currentCourseKindLabel} + ”下没有课程,请切换分类或刷新课程列表。 + + + {(course) => { const selected = () => course.id === props.selectedCourseId; @@ -169,6 +212,13 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => { ? props.selectedCourse.name : "请选择课程"}

+ +

+ 当前显示的是本地缓存记录。 +

+
diff --git a/src/index.tsx b/src/index.tsx index 8fb2da2..1f1d0df 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,11 +13,11 @@ render( import("./pages/accouts/Account.tsx"))} + component={lazy(() => import("./pages/accounts/Account.tsx"))} /> import("./pages/accouts/Account.tsx"))} + component={lazy(() => import("./pages/accounts/Account.tsx"))} /> { return hours * 3600 + minutes * 60 + seconds; }; +const createRecordCacheKey = ( + accountId: string, + courseId: number, + recordType: RecordType, +) => `${accountId}::${courseId}::${recordType || "course"}`; + const Account = () => { const [storeState, setStoreState] = createSignal(accountStore.getState()); const [settingsState, setSettingsState] = createSignal( @@ -71,6 +78,7 @@ const Account = () => { const [recordsLoading, setRecordsLoading] = createSignal(false); const [recordError, setRecordError] = createSignal(""); const [isRefreshingRecords, setIsRefreshingRecords] = createSignal(false); + const [isRefreshingCourses, setIsRefreshingCourses] = createSignal(false); const [isRefreshingLogs, setIsRefreshingLogs] = createSignal(false); const [hasRestoredRecords, setHasRestoredRecords] = createSignal(false); const accountClients = new Map>(); @@ -95,8 +103,10 @@ const Account = () => { const selectedAccountId = createMemo(() => storeState().selectedAccountId); const expandedAccountId = createMemo(() => storeState().expandedAccountId); const selectedCourseId = createMemo(() => storeState().selectedCourseId); + const courseKind = createMemo(() => storeState().courseKind); const recordType = createMemo(() => storeState().recordType); const records = createMemo(() => storeState().records); + const recordCacheMap = createMemo(() => storeState().recordCacheMap); const studyLogsMap = createMemo(() => storeState().studyLogsMap); const runningStudyMap = createMemo(() => storeState().runningStudyMap); const mergedHostOptions = createMemo(() => @@ -111,6 +121,24 @@ const Account = () => { const defaultHost = createMemo( () => mergedHostOptions()[0]?.host ?? "cqcst.leykeji.com", ); + const currentCourseKindLabel = createMemo( + () => + statusOptions.find((item) => item.value === courseKind())?.label ?? + courseKind(), + ); + const showingCachedRecords = createMemo(() => { + const accountId = selectedAccountId(); + const courseId = selectedCourseId(); + + if (!accountId || !courseId || recordsLoading() || isRefreshingRecords()) { + return false; + } + + return ( + createRecordCacheKey(accountId, courseId, recordType()) in + recordCacheMap() + ); + }); const selectedAccount = createMemo(() => { return accounts().find((item) => item.id === selectedAccountId()) ?? null; @@ -203,8 +231,9 @@ const Account = () => { ...target, sessionId: res.data.session_id, user: res.data.user, - courses: res.data.courses, + courses: res.data.courses ?? target.courses, }); + await loadCourses(target.id, target.status); appendStudyLog(`会话失效,已重新登录:${target.user.name}`, target.id); return true; } catch (error) { @@ -265,10 +294,11 @@ const Account = () => { token: payload.token.trim(), }, user: res.data.user, - courses: res.data.courses, + courses: res.data.courses ?? [], }; accountStore.getState().upsertAccount(nextAccount); + await loadCourses(nextAccount.id, nextAccount.status); accountStore.getState().setSelectedCourseId(null); accountStore.getState().setRecords([]); setShowDialog(false); @@ -294,6 +324,50 @@ const Account = () => { accountStore.getState().setExpandedAccountId(nextId); }; + const loadCourses = async ( + accountId = selectedAccount()?.id, + status = courseKind(), + ) => { + if (!accountId) { + return; + } + + setIsRefreshingCourses(true); + setErrorMessage(""); + + try { + const res = await getAccountClient(accountId).courseApi({ status }); + const courses = res.data.courses ?? []; + const account = accountStore + .getState() + .accounts.find((item) => item.id === accountId); + + accountStore.getState().setAccountCourses(accountId, courses); + + const currentCourseId = accountStore.getState().selectedCourseId; + if (account?.id === accountStore.getState().selectedAccountId) { + const hasSelectedCourse = courses.some( + (item) => item.id === currentCourseId, + ); + if (!hasSelectedCourse) { + accountStore.getState().setSelectedCourseId(courses[0]?.id ?? null); + accountStore.getState().setRecords([]); + } + } + } catch (error) { + const message = + error instanceof Error ? error.message : "获取课程失败,请稍后重试。"; + setErrorMessage(message); + accountStore.getState().setAccountCourses(accountId, []); + if (accountStore.getState().selectedAccountId === accountId) { + accountStore.getState().setSelectedCourseId(null); + accountStore.getState().setRecords([]); + } + } finally { + setIsRefreshingCourses(false); + } + }; + const handleRefreshAccount = async () => { const account = selectedAccount(); if (!account) { @@ -317,8 +391,9 @@ const Account = () => { ...account, sessionId: res.data.session_id, user: res.data.user, - courses: res.data.courses, + courses: res.data.courses ?? account.courses, }); + await loadCourses(account.id, account.status); } catch (error) { const message = error instanceof Error ? error.message : "刷新账号失败,请稍后重试。"; @@ -374,6 +449,12 @@ const Account = () => { record_type: nextRecordType, }); accountStore.getState().setRecords(res.data.list); + accountStore + .getState() + .setRecordCache( + createRecordCacheKey(account.id, courseId, nextRecordType), + res.data.list, + ); } catch (error) { const message = error instanceof Error ? error.message : "获取记录失败,请稍后重试。"; @@ -385,9 +466,8 @@ const Account = () => { } }; - const handleSelectCourse = async (courseId: number) => { + const handleSelectCourse = (courseId: number) => { accountStore.getState().setSelectedCourseId(courseId); - await loadCourseRecords(courseId); }; const handleRefreshRecords = async () => { @@ -448,6 +528,8 @@ const Account = () => { client: getAccountClient(account.id), isRunningStudy: () => !!accountStore.getState().runningStudyMap[account.id], + setIsRunningStudy: () => + accountStore.getState().setAccountRunningStudy(account.id, false), onLog: (message: string, accoundID: string) => appendStudyLog(message, accoundID), }); @@ -474,15 +556,46 @@ const Account = () => { appendStudyLog(`已发送停止刷课指令:${account.user.name}`, account.id); }; + createEffect( + on([selectedAccountId, courseKind], ([accountId, kind]) => { + if (!accountId) { + return; + } + + void loadCourses(accountId, kind); + }), + ); + + createEffect( + on( + [selectedAccountId, selectedCourseId, recordType], + ([accountId, courseId, type]) => { + if (!accountId || !courseId) { + accountStore.getState().setRecords([]); + return; + } + + const cachedRecords = + recordCacheMap()[createRecordCacheKey(accountId, courseId, type)] ?? + []; + accountStore.getState().setRecords(cachedRecords); + }, + ), + ); + createEffect(() => { const courseId = selectedCourseId(); const account = selectedAccount(); const type = recordType(); + const cacheKey = account + ? createRecordCacheKey(account.id, courseId ?? 0, type) + : ""; + const hasCachedRecords = cacheKey ? cacheKey in recordCacheMap() : false; if (!hasRestoredRecords()) { setHasRestoredRecords(true); - if (courseId && account && records().length > 0) { + if (courseId && account && (records().length > 0 || hasCachedRecords)) { return; } } @@ -491,24 +604,30 @@ const Account = () => { return; } + if (hasCachedRecords) { + return; + } + void loadCourseRecords(courseId, type); }); return (
-
+

Account Center

-

账号管理

-

+

+ 账号管理 +

+

管理账号登录、课程记录与运行日志