import { createEffect, createMemo, createSignal, on, onCleanup, onMount, } from "solid-js"; import AccountSidebar from "~/components/account/AccountSidebar"; import AddAccountDialog, { type LoginForm, } from "~/components/account/AddAccountDialog"; import CourseWorkspace from "~/components/account/CourseWorkspace"; import { createWkClient, loginApi, runStudyQueue, type CourseKind, type RecordType, } from "~/service/wk"; import { setUnauthorizedHandler } from "~/service/http"; import { accountStore, type AccountItem } from "~/store/account"; import { getMergedHosts, settingsStore } from "~/store/settings"; import type { CourseType } from "~/types/Course"; const statusOptions: { label: string; value: CourseKind }[] = [ { label: "我的课程", value: "run" }, { label: "已结束", value: "finish" }, { label: "报名中", value: "sign" }, ]; const recordTypeOptions: { label: string; value: RecordType }[] = [ { label: "课程", value: "" }, { label: "作业", value: "/work" }, { label: "考试", value: "/exam" }, { label: "讨论", value: "/discuss" }, ]; const createDefaultForm = (host: string): LoginForm => ({ username: "", password: "", token: "", status: "run", host, }); const stripHtml = (value: string) => value.replace(/<[^>]+>/g, "").trim(); const parseDurationToSeconds = (value: string) => { const parts = value.split(":").map(Number); if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) { return 0; } const [hours, minutes, seconds] = parts; 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( settingsStore.getState(), ); const [showDialog, setShowDialog] = createSignal(false); const [isSubmitting, setIsSubmitting] = createSignal(false); const [loggingOutId, setLoggingOutId] = createSignal(""); const [isRefreshingAccount, setIsRefreshingAccount] = createSignal(false); const [errorMessage, setErrorMessage] = createSignal(""); const [form, setForm] = createSignal( createDefaultForm("cqcst.leykeji.com"), ); 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>(); onMount(() => { const unsubscribeAccount = accountStore.subscribe((state) => { setStoreState(state); }); const unsubscribeSettings = settingsStore.subscribe((state) => { setSettingsState(state); }); setUnauthorizedHandler(reloginBySession); onCleanup(() => { unsubscribeAccount(); unsubscribeSettings(); setUnauthorizedHandler(null); }); }); const accounts = createMemo(() => storeState().accounts); 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(() => getMergedHosts(settingsState().localHosts, settingsState().remoteHosts), ); const hostLabels = createMemo( () => Object.fromEntries( mergedHostOptions().map((item) => [item.host, item.label]), ) as Record, ); 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; }); const selectedCourseList = createMemo(() => { return selectedAccount()?.courses ?? []; }); const selectedCourse = createMemo(() => { return ( selectedCourseList().find((item) => item.id === selectedCourseId()) ?? null ); }); const isRunningStudy = createMemo(() => { const account = selectedAccount(); if (!account) { return false; } return !!runningStudyMap()[account.id]; }); const studyLogs = createMemo(() => { const account = selectedAccount(); if (!account) { return []; } return studyLogsMap()[account.id] ?? []; }); const updateForm = ( key: K, value: LoginForm[K], ) => { setForm((prev) => ({ ...prev, [key]: value })); }; const appendStudyLog = (message: string, accountId?: string) => { const targetAccountId = accountId ?? selectedAccount()?.id; if (!targetAccountId) { return; } accountStore.getState().appendStudyLog(targetAccountId, message); }; const getAccountClient = (accountId: string) => { const existingClient = accountClients.get(accountId); if (existingClient) { return existingClient; } const client = createWkClient(() => { return accountStore .getState() .accounts.find((item) => item.id === accountId)?.sessionId; }); accountClients.set(accountId, client); return client; }; const reloginBySession = async (sessionId: string) => { if (!sessionId) { return false; } const target = accountStore .getState() .accounts.find((item) => item.sessionId === sessionId); if (!target) { return false; } try { const res = await loginApi({ username: target.username, password: target.auth.password, token: target.auth.token, status: target.status, host: target.host, }); accountStore.getState().upsertAccount({ ...target, sessionId: res.data.session_id, user: res.data.user, courses: res.data.courses ?? target.courses, }); await loadCourses(target.id, target.status); appendStudyLog(`会话失效,已重新登录:${target.user.name}`, target.id); return true; } catch (error) { const message = error instanceof Error ? error.message : "重新登录失败"; setErrorMessage(message); appendStudyLog(`重新登录失败:${message}`, target.id); return false; } }; const openDialog = () => { setErrorMessage(""); setForm(createDefaultForm(defaultHost())); setShowDialog(true); }; const closeDialog = () => { if (isSubmitting()) { return; } setShowDialog(false); setErrorMessage(""); }; const handleAddAccount = async () => { const payload = form(); const hasAccount = payload.username.trim() !== "" && payload.password.trim() !== ""; const hasCookie = payload.token.trim() !== ""; if (!hasAccount && !hasCookie) { setErrorMessage("请填写账号和密码,或者填写 Cookie。"); return; } setIsSubmitting(true); setErrorMessage(""); try { const res = await loginApi({ username: payload.username.trim(), password: payload.password.trim(), token: payload.token.trim(), status: payload.status, host: payload.host, }); const accountId = `${res.data.user.id}-${payload.host}-${payload.status}`; const nextAccount: AccountItem = { id: accountId, username: payload.username.trim() || res.data.user.id, host: payload.host, status: payload.status, sessionId: res.data.session_id, auth: { password: payload.password.trim(), token: payload.token.trim(), }, user: res.data.user, courses: res.data.courses ?? [], }; accountStore.getState().upsertAccount(nextAccount); await loadCourses(nextAccount.id, nextAccount.status); accountStore.getState().setSelectedCourseId(null); accountStore.getState().setRecords([]); setShowDialog(false); setForm(createDefaultForm(defaultHost())); } catch (error) { const message = error instanceof Error ? error.message : "登录失败,请检查输入信息。"; setErrorMessage(message); } finally { setIsSubmitting(false); } }; const handleSelectAccount = (accountId: string) => { accountStore.getState().setSelectedAccountId(accountId); accountStore.getState().setSelectedCourseId(null); accountStore.getState().setRecords([]); setRecordError(""); }; const handleToggleExpand = (accountId: string) => { const nextId = expandedAccountId() === accountId ? "" : accountId; 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) { setErrorMessage("请先选择账号。"); return; } setIsRefreshingAccount(true); setErrorMessage(""); try { const res = await loginApi({ username: account.username, password: account.auth.password, token: account.auth.token, status: account.status, host: account.host, }); accountStore.getState().upsertAccount({ ...account, sessionId: res.data.session_id, user: res.data.user, courses: res.data.courses ?? account.courses, }); await loadCourses(account.id, account.status); } catch (error) { const message = error instanceof Error ? error.message : "刷新账号失败,请稍后重试。"; setErrorMessage(message); } finally { setIsRefreshingAccount(false); } }; const handleLogout = async (accountId: string) => { setLoggingOutId(accountId); try { const target = accounts().find((item) => item.id === accountId); if (!target) { return; } await getAccountClient(accountId).logoutApi(); accountClients.delete(accountId); accountStore.getState().removeAccount(accountId); if (selectedAccountId() === accountId) { accountStore.getState().setSelectedCourseId(null); accountStore.getState().setRecords([]); } } catch (error) { const message = error instanceof Error ? error.message : "退出登录失败,请稍后重试。"; setErrorMessage(message); } finally { setLoggingOutId(""); } }; const loadCourseRecords = async ( courseId: number, nextRecordType = recordType(), ) => { const account = selectedAccount(); if (!account) { return; } setRecordsLoading(true); setIsRefreshingRecords(true); setRecordError(""); try { const res = await getAccountClient(account.id).recordApi({ course_id: String(courseId), page: 0, 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 : "获取记录失败,请稍后重试。"; setRecordError(message); accountStore.getState().setRecords([]); } finally { setRecordsLoading(false); setIsRefreshingRecords(false); } }; const handleSelectCourse = (courseId: number) => { accountStore.getState().setSelectedCourseId(courseId); }; const handleRefreshRecords = async () => { const courseId = selectedCourseId(); if (!courseId) { setRecordError("请先选择课程。"); return; } await loadCourseRecords(courseId); }; const handleClearLogs = () => { const account = selectedAccount(); if (!account) { return; } accountStore.getState().clearStudyLogs(account.id); }; const handleRefreshLogs = () => { setIsRefreshingLogs(true); appendStudyLog("手动刷新日志面板"); queueMicrotask(() => { setIsRefreshingLogs(false); }); }; const handleStartStudy = async () => { const account = selectedAccount(); const course = selectedCourse(); if (!account || !course) { setRecordError("请先选择账号和课程。"); return; } const queueItems = records() .filter((item) => item.progress !== "1.00") .map((item) => ({ nodeId: item.id, name: item.name, currentTime: Number(item.duration || 0), totalTime: parseDurationToSeconds(item.videoDuration), progress: item.progress, completed: stripHtml(item.state) === "已学", })); try { accountStore.getState().setAccountRunningStudy(account.id, true); appendStudyLog(`开始刷课:${course.name}`, account.id); await runStudyQueue({ accountId: account.id, courseId: course.id, intervalSeconds: 5, items: queueItems, 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), }); if (accountStore.getState().runningStudyMap[account.id]) { appendStudyLog(`刷课完成:${course.name}`, account.id); } } catch (error) { const message = error instanceof Error ? error.message : "刷课流程执行失败。"; appendStudyLog(`刷课失败:${message}`, account.id); setRecordError(message); } finally { accountStore.getState().setAccountRunningStudy(account.id, false); } }; const handleStopStudy = () => { const account = selectedAccount(); if (!account) { return; } accountStore.getState().setAccountRunningStudy(account.id, false); 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 || hasCachedRecords)) { return; } } if (!courseId || !account) { return; } if (hasCachedRecords) { return; } void loadCourseRecords(courseId, type); }); return (

Account Center

账号管理

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

void handleRefreshAccount()} onSelectAccount={handleSelectAccount} onToggleExpand={handleToggleExpand} onLogout={(accountId) => void handleLogout(accountId)} /> void handleSelectCourse(courseId)} onRefreshRecords={() => void handleRefreshRecords()} onRefreshCourseRecords={() => void loadCourses()} onChangeRecordType={(value) => accountStore.getState().setRecordType(value) } onChangeCourseRecordType={(value) => { accountStore.getState().setCourseKind(value); accountStore.getState().setSelectedCourseId(null); accountStore.getState().setRecords([]); }} onStartStudy={() => void handleStartStudy()} onStopStudy={handleStopStudy} onRefreshLogs={handleRefreshLogs} onClearLogs={handleClearLogs} renderRecordState={stripHtml} />
void handleAddAccount()} isSubmitting={isSubmitting()} errorMessage={errorMessage()} form={form()} statusOptions={statusOptions} hostOptions={mergedHostOptions().map((item) => ({ label: item.label, host: item.host, }))} updateForm={updateForm} />
); }; export default Account;