fix: 修复问题

- 修复刷课错误不会停止
- 添加课程刷新
- 添加课程列表、记录列表缓存
- 显示当前版本
This commit is contained in:
2026-03-28 19:22:31 +08:00
parent 6325b84ca0
commit 9e7131f210
8 changed files with 347 additions and 32 deletions

View File

@@ -1,5 +1,7 @@
import type { ParentComponent } from "solid-js"; import type { ParentComponent } from "solid-js";
import { createMemo, createResource, createSignal } from "solid-js";
import { A, useLocation } from "@solidjs/router"; import { A, useLocation } from "@solidjs/router";
import { versionApi } from "~/service/wk";
const asideList = [ const asideList = [
{ label: "账号", url: "/account" }, { label: "账号", url: "/account" },
@@ -9,41 +11,75 @@ const asideList = [
const App: ParentComponent = (props) => { const App: ParentComponent = (props) => {
const location = useLocation(); const location = useLocation();
const [version] = createResource(versionApi);
const [copyState, setCopyState] = createSignal<"idle" | "done" | "error">(
"idle",
);
const isActive = (url: string) => const isActive = (url: string) =>
location.pathname === url || location.pathname === url ||
(location.pathname === "/" && url === "/account"); (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 ( return (
<div class="flex h-screen w-full overflow-hidden bg-[radial-gradient(circle_at_top_left,_rgba(103,232,249,0.24),_transparent_28%),radial-gradient(circle_at_top_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#ecfeff_0%,_#f8fafc_52%,_#eef2ff_100%)] text-zinc-800"> <div class="flex h-screen w-full overflow-hidden bg-[radial-gradient(circle_at_top_left,_rgba(103,232,249,0.24),_transparent_28%),radial-gradient(circle_at_top_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#ecfeff_0%,_#f8fafc_52%,_#eef2ff_100%)] text-zinc-800">
<div class="flex min-h-0 w-full flex-col p-3 sm:p-4"> <div class="flex min-h-0 w-full flex-col p-3 sm:p-4">
<header class="flex shrink-0 items-center justify-between gap-4 rounded-[28px] border border-white/70 bg-white/75 px-5 py-4 shadow-[0_18px_50px_-22px_rgba(14,116,144,0.35)] backdrop-blur-xl"> <header class="flex shrink-0 items-center rounded-[24px] border border-white/70 bg-white/75 px-4 py-3 shadow-[0_18px_50px_-22px_rgba(14,116,144,0.35)] backdrop-blur-xl">
<div class="flex min-w-0 items-center gap-4"> <div class="flex min-w-0 items-center gap-3">
<A <A
href="/" href="/"
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[linear-gradient(135deg,_#06b6d4,_#22c55e)] text-lg font-semibold text-white shadow-lg shadow-cyan-500/20" class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[linear-gradient(135deg,_#06b6d4,_#22c55e)] text-base font-semibold text-white shadow-lg shadow-cyan-500/20"
> >
WK WK
</A> </A>
<div class="min-w-0"> <div class="min-w-0">
<A <A
href="/" href="/"
class="block truncate text-2xl font-semibold tracking-wide text-zinc-900" class="block truncate text-xl font-semibold tracking-wide text-zinc-900"
> >
</A> </A>
<p class="truncate text-sm text-zinc-500"> <p class="truncate text-xs text-zinc-500 sm:text-sm">
</p> </p>
</div> </div>
</div> </div>
<div class="rounded-2xl border border-zinc-200/80 bg-zinc-50/90 px-4 py-3 text-right shadow-sm">
<p class="text-sm font-medium text-zinc-800"></p>
<p class="mt-1 text-xs text-zinc-500">
/ /
</p>
</div>
</header> </header>
<div class="mt-4 flex min-h-0 min-w-0 flex-1 gap-4"> <div class="mt-4 flex min-h-0 min-w-0 flex-1 gap-4">
@@ -92,6 +128,28 @@ const App: ParentComponent = (props) => {
<p class="mt-1 text-sm text-zinc-500"> <p class="mt-1 text-sm text-zinc-500">
{asideList.find((item) => isActive(item.url))?.label ?? "账号"} {asideList.find((item) => isActive(item.url))?.label ?? "账号"}
</p> </p>
<p class="mt-3 text-xs font-medium tracking-[0.18em] text-cyan-700/75 uppercase">
Runtime
</p>
<p class="mt-2 text-xs text-zinc-500">Version: {versionText()}</p>
<p class="mt-1 text-xs text-zinc-500">Commit: {commitText()}</p>
<p class="mt-1 text-xs text-zinc-500">Build: {buildText()}</p>
<p class="mt-1 text-xs text-zinc-500">Author: {authorText()}</p>
<p class="mt-1 text-xs text-zinc-500">Email: {emailText()}</p>
<button
type="button"
class="mt-3 rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-xs text-zinc-700 transition hover:bg-zinc-100"
onClick={() => void handleCopyVersion()}
>
{copyState() === "done"
? "已复制"
: copyState() === "error"
? "复制失败"
: "复制版本信息"}
</button>
{versionErrorText() ? (
<p class="mt-2 text-xs text-rose-500">{versionErrorText()}</p>
) : null}
</div> </div>
</aside> </aside>

View File

@@ -12,6 +12,7 @@ interface AccountSidebarProps {
selectedAccountId: string; selectedAccountId: string;
expandedAccountId: string; expandedAccountId: string;
statusOptions: StatusOption[]; statusOptions: StatusOption[];
currentCourseKind: CourseKind;
hostLabels: Record<string, string>; hostLabels: Record<string, string>;
isRefreshingAccount: boolean; isRefreshingAccount: boolean;
loggingOutId: string; loggingOutId: string;
@@ -66,6 +67,16 @@ const AccountSidebar = (props: AccountSidebarProps) => {
const statusLabel = const statusLabel =
props.statusOptions.find((item) => item.value === account.status) props.statusOptions.find((item) => item.value === account.status)
?.label ?? 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 ( return (
<div <div
@@ -118,8 +129,8 @@ const AccountSidebar = (props: AccountSidebarProps) => {
<p>{account.user.gender}</p> <p>{account.user.gender}</p>
<p>{account.host}</p> <p>{account.host}</p>
<p>{account.username || "-"}</p> <p>{account.username || "-"}</p>
<p>{account.courses.length}</p> <p>{courseCountLabel}</p>
<p>{statusLabel}</p> <p>{courseTypeLabel}</p>
<div class="mt-2 flex gap-2"> <div class="mt-2 flex gap-2">
<button <button

View File

@@ -1,5 +1,5 @@
import { For, Show, createEffect, type JSX } from "solid-js"; import { For, Show, createEffect, type JSX } from "solid-js";
import type { RecordType } from "~/service/wk"; import type { CourseKind, RecordType } from "~/service/wk";
import type { AccountItem } from "~/store/account"; import type { AccountItem } from "~/store/account";
import type { CourseType } from "~/types/Course"; import type { CourseType } from "~/types/Course";
import type { RecordItem } from "~/service/wk"; import type { RecordItem } from "~/service/wk";
@@ -8,6 +8,10 @@ type RecordTypeOption = {
label: string; label: string;
value: RecordType; value: RecordType;
}; };
type CourseRecordTypeOption = {
label: string;
value: CourseKind;
};
interface CourseWorkspaceProps { interface CourseWorkspaceProps {
selectedAccount: AccountItem | null; selectedAccount: AccountItem | null;
@@ -15,12 +19,17 @@ interface CourseWorkspaceProps {
selectedCourseId: number | null; selectedCourseId: number | null;
selectedCourse: CourseType | null; selectedCourse: CourseType | null;
recordType: RecordType; recordType: RecordType;
courseKind: CourseKind;
currentCourseKindLabel: string;
showingCachedRecords: boolean;
recordTypeOptions: RecordTypeOption[]; recordTypeOptions: RecordTypeOption[];
courseRecordTypeOptions: CourseRecordTypeOption[];
records: RecordItem[]; records: RecordItem[];
studyLogs: string[]; studyLogs: string[];
recordsLoading: boolean; recordsLoading: boolean;
recordError: string; recordError: string;
isRefreshingRecords: boolean; isRefreshingRecords: boolean;
isRefreshingCourseRecords: boolean;
isRunningStudy: boolean; isRunningStudy: boolean;
isRefreshingLogs: boolean; isRefreshingLogs: boolean;
autoScrollLogs: boolean; autoScrollLogs: boolean;
@@ -29,7 +38,9 @@ interface CourseWorkspaceProps {
logFontSize: number; logFontSize: number;
onSelectCourse: (courseId: number) => void; onSelectCourse: (courseId: number) => void;
onRefreshRecords: () => void; onRefreshRecords: () => void;
onRefreshCourseRecords: () => void;
onChangeRecordType: (value: RecordType) => void; onChangeRecordType: (value: RecordType) => void;
onChangeCourseRecordType: (value: CourseKind) => void;
onStartStudy: () => void; onStartStudy: () => void;
onStopStudy: () => void; onStopStudy: () => void;
onRefreshLogs: () => void; onRefreshLogs: () => void;
@@ -108,9 +119,34 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
} }
> >
<div class="flex min-h-0 flex-col overflow-hidden rounded-3xl border border-zinc-200 bg-[linear-gradient(180deg,rgba(248,250,252,0.9),rgba(255,255,255,0.95))]"> <div class="flex min-h-0 flex-col overflow-hidden rounded-3xl border border-zinc-200 bg-[linear-gradient(180deg,rgba(248,250,252,0.9),rgba(255,255,255,0.95))]">
<div class="border-b border-zinc-200 px-4 py-3"> <div class="flex flex-wrap items-center justify-between gap-3 border-b border-zinc-200 px-4 py-3">
<p class="text-sm font-semibold text-zinc-800"></p> <div class="border-b border-zinc-200 px-4 py-3">
<p class="mt-1 text-xs text-zinc-500"></p> <p class="text-sm font-semibold text-zinc-800"></p>
<p class="mt-1 text-xs text-zinc-500"></p>
</div>
<div class="flex flex-wrap items-center gap-2">
<select
class="rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm transition outline-none focus:border-cyan-400"
value={props.courseKind}
onChange={(event) =>
props.onChangeCourseRecordType(
event.currentTarget.value as CourseKind,
)
}
>
<For each={props.courseRecordTypeOptions}>
{(item) => <option value={item.value}>{item.label}</option>}
</For>
</select>
<button
type="button"
class="rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
disabled={props.isRefreshingCourseRecords}
onClick={props.onRefreshCourseRecords}
>
{props.isRefreshingCourseRecords ? "刷新中..." : "刷新记录"}
</button>
</div>
</div> </div>
<div <div
@@ -120,6 +156,13 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3" : "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3"
} }
> >
<Show when={props.selectedCourseList.length === 0}>
<EmptyState>
{props.currentCourseKindLabel}
</EmptyState>
</Show>
<For each={props.selectedCourseList}> <For each={props.selectedCourseList}>
{(course) => { {(course) => {
const selected = () => course.id === props.selectedCourseId; const selected = () => course.id === props.selectedCourseId;
@@ -169,6 +212,13 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
? props.selectedCourse.name ? props.selectedCourse.name
: "请选择课程"} : "请选择课程"}
</p> </p>
<Show
when={props.showingCachedRecords && !props.recordsLoading}
>
<p class="mt-1 text-xs text-amber-600">
</p>
</Show>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">

View File

@@ -13,11 +13,11 @@ render(
<Route path="/" component={App}> <Route path="/" component={App}>
<Route <Route
path="" path=""
component={lazy(() => import("./pages/accouts/Account.tsx"))} component={lazy(() => import("./pages/accounts/Account.tsx"))}
/> />
<Route <Route
path="account" path="account"
component={lazy(() => import("./pages/accouts/Account.tsx"))} component={lazy(() => import("./pages/accounts/Account.tsx"))}
/> />
<Route <Route
path="logs" path="logs"

View File

@@ -2,6 +2,7 @@ import {
createEffect, createEffect,
createMemo, createMemo,
createSignal, createSignal,
on,
onCleanup, onCleanup,
onMount, onMount,
} from "solid-js"; } from "solid-js";
@@ -55,6 +56,12 @@ const parseDurationToSeconds = (value: string) => {
return hours * 3600 + minutes * 60 + seconds; return hours * 3600 + minutes * 60 + seconds;
}; };
const createRecordCacheKey = (
accountId: string,
courseId: number,
recordType: RecordType,
) => `${accountId}::${courseId}::${recordType || "course"}`;
const Account = () => { const Account = () => {
const [storeState, setStoreState] = createSignal(accountStore.getState()); const [storeState, setStoreState] = createSignal(accountStore.getState());
const [settingsState, setSettingsState] = createSignal( const [settingsState, setSettingsState] = createSignal(
@@ -71,6 +78,7 @@ const Account = () => {
const [recordsLoading, setRecordsLoading] = createSignal(false); const [recordsLoading, setRecordsLoading] = createSignal(false);
const [recordError, setRecordError] = createSignal(""); const [recordError, setRecordError] = createSignal("");
const [isRefreshingRecords, setIsRefreshingRecords] = createSignal(false); const [isRefreshingRecords, setIsRefreshingRecords] = createSignal(false);
const [isRefreshingCourses, setIsRefreshingCourses] = createSignal(false);
const [isRefreshingLogs, setIsRefreshingLogs] = createSignal(false); const [isRefreshingLogs, setIsRefreshingLogs] = createSignal(false);
const [hasRestoredRecords, setHasRestoredRecords] = createSignal(false); const [hasRestoredRecords, setHasRestoredRecords] = createSignal(false);
const accountClients = new Map<string, ReturnType<typeof createWkClient>>(); const accountClients = new Map<string, ReturnType<typeof createWkClient>>();
@@ -95,8 +103,10 @@ const Account = () => {
const selectedAccountId = createMemo(() => storeState().selectedAccountId); const selectedAccountId = createMemo(() => storeState().selectedAccountId);
const expandedAccountId = createMemo(() => storeState().expandedAccountId); const expandedAccountId = createMemo(() => storeState().expandedAccountId);
const selectedCourseId = createMemo(() => storeState().selectedCourseId); const selectedCourseId = createMemo(() => storeState().selectedCourseId);
const courseKind = createMemo(() => storeState().courseKind);
const recordType = createMemo(() => storeState().recordType); const recordType = createMemo(() => storeState().recordType);
const records = createMemo(() => storeState().records); const records = createMemo(() => storeState().records);
const recordCacheMap = createMemo(() => storeState().recordCacheMap);
const studyLogsMap = createMemo(() => storeState().studyLogsMap); const studyLogsMap = createMemo(() => storeState().studyLogsMap);
const runningStudyMap = createMemo(() => storeState().runningStudyMap); const runningStudyMap = createMemo(() => storeState().runningStudyMap);
const mergedHostOptions = createMemo(() => const mergedHostOptions = createMemo(() =>
@@ -111,6 +121,24 @@ const Account = () => {
const defaultHost = createMemo( const defaultHost = createMemo(
() => mergedHostOptions()[0]?.host ?? "cqcst.leykeji.com", () => 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(() => { const selectedAccount = createMemo(() => {
return accounts().find((item) => item.id === selectedAccountId()) ?? null; return accounts().find((item) => item.id === selectedAccountId()) ?? null;
@@ -203,8 +231,9 @@ const Account = () => {
...target, ...target,
sessionId: res.data.session_id, sessionId: res.data.session_id,
user: res.data.user, 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); appendStudyLog(`会话失效,已重新登录:${target.user.name}`, target.id);
return true; return true;
} catch (error) { } catch (error) {
@@ -265,10 +294,11 @@ const Account = () => {
token: payload.token.trim(), token: payload.token.trim(),
}, },
user: res.data.user, user: res.data.user,
courses: res.data.courses, courses: res.data.courses ?? [],
}; };
accountStore.getState().upsertAccount(nextAccount); accountStore.getState().upsertAccount(nextAccount);
await loadCourses(nextAccount.id, nextAccount.status);
accountStore.getState().setSelectedCourseId(null); accountStore.getState().setSelectedCourseId(null);
accountStore.getState().setRecords([]); accountStore.getState().setRecords([]);
setShowDialog(false); setShowDialog(false);
@@ -294,6 +324,50 @@ const Account = () => {
accountStore.getState().setExpandedAccountId(nextId); 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 handleRefreshAccount = async () => {
const account = selectedAccount(); const account = selectedAccount();
if (!account) { if (!account) {
@@ -317,8 +391,9 @@ const Account = () => {
...account, ...account,
sessionId: res.data.session_id, sessionId: res.data.session_id,
user: res.data.user, user: res.data.user,
courses: res.data.courses, courses: res.data.courses ?? account.courses,
}); });
await loadCourses(account.id, account.status);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : "刷新账号失败,请稍后重试。"; error instanceof Error ? error.message : "刷新账号失败,请稍后重试。";
@@ -374,6 +449,12 @@ const Account = () => {
record_type: nextRecordType, record_type: nextRecordType,
}); });
accountStore.getState().setRecords(res.data.list); accountStore.getState().setRecords(res.data.list);
accountStore
.getState()
.setRecordCache(
createRecordCacheKey(account.id, courseId, nextRecordType),
res.data.list,
);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.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); accountStore.getState().setSelectedCourseId(courseId);
await loadCourseRecords(courseId);
}; };
const handleRefreshRecords = async () => { const handleRefreshRecords = async () => {
@@ -448,6 +528,8 @@ const Account = () => {
client: getAccountClient(account.id), client: getAccountClient(account.id),
isRunningStudy: () => isRunningStudy: () =>
!!accountStore.getState().runningStudyMap[account.id], !!accountStore.getState().runningStudyMap[account.id],
setIsRunningStudy: () =>
accountStore.getState().setAccountRunningStudy(account.id, false),
onLog: (message: string, accoundID: string) => onLog: (message: string, accoundID: string) =>
appendStudyLog(message, accoundID), appendStudyLog(message, accoundID),
}); });
@@ -474,15 +556,46 @@ const Account = () => {
appendStudyLog(`已发送停止刷课指令:${account.user.name}`, account.id); 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(() => { createEffect(() => {
const courseId = selectedCourseId(); const courseId = selectedCourseId();
const account = selectedAccount(); const account = selectedAccount();
const type = recordType(); const type = recordType();
const cacheKey = account
? createRecordCacheKey(account.id, courseId ?? 0, type)
: "";
const hasCachedRecords = cacheKey ? cacheKey in recordCacheMap() : false;
if (!hasRestoredRecords()) { if (!hasRestoredRecords()) {
setHasRestoredRecords(true); setHasRestoredRecords(true);
if (courseId && account && records().length > 0) { if (courseId && account && (records().length > 0 || hasCachedRecords)) {
return; return;
} }
} }
@@ -491,24 +604,30 @@ const Account = () => {
return; return;
} }
if (hasCachedRecords) {
return;
}
void loadCourseRecords(courseId, type); void loadCourseRecords(courseId, type);
}); });
return ( return (
<div class="flex h-full min-h-0 flex-col overflow-hidden"> <div class="flex h-full min-h-0 flex-col overflow-hidden">
<div class="flex shrink-0 items-center justify-between gap-4 rounded-[28px] border border-white/80 bg-[linear-gradient(135deg,rgba(236,254,255,0.95),rgba(255,255,255,0.92))] px-5 py-4 shadow-[0_18px_45px_-28px_rgba(8,145,178,0.35)]"> <div class="flex shrink-0 items-center justify-between gap-3 rounded-[24px] border border-white/80 bg-[linear-gradient(135deg,rgba(236,254,255,0.95),rgba(255,255,255,0.92))] px-4 py-3 shadow-[0_18px_45px_-28px_rgba(8,145,178,0.35)]">
<div> <div>
<p class="text-xs font-medium tracking-[0.26em] text-cyan-700/75 uppercase"> <p class="text-xs font-medium tracking-[0.26em] text-cyan-700/75 uppercase">
Account Center Account Center
</p> </p>
<h1 class="mt-2 text-2xl font-semibold text-zinc-900"></h1> <h1 class="mt-1 text-xl font-semibold text-zinc-900 sm:text-2xl">
<p class="mt-1 text-sm text-zinc-500">
</h1>
<p class="mt-0.5 text-xs text-zinc-500 sm:text-sm">
</p> </p>
</div> </div>
<button <button
class="rounded-2xl bg-[linear-gradient(135deg,#06b6d4,#14b8a6)] px-4 py-3 text-sm font-medium text-white shadow-lg shadow-cyan-500/20 transition hover:-translate-y-px hover:shadow-cyan-500/30" class="rounded-xl bg-[linear-gradient(135deg,#06b6d4,#14b8a6)] px-3.5 py-2.5 text-sm font-medium text-white shadow-lg shadow-cyan-500/20 transition hover:-translate-y-px hover:shadow-cyan-500/30"
onClick={openDialog} onClick={openDialog}
> >
@@ -521,6 +640,7 @@ const Account = () => {
selectedAccountId={selectedAccountId()} selectedAccountId={selectedAccountId()}
expandedAccountId={expandedAccountId()} expandedAccountId={expandedAccountId()}
statusOptions={statusOptions} statusOptions={statusOptions}
currentCourseKind={courseKind()}
hostLabels={hostLabels()} hostLabels={hostLabels()}
isRefreshingAccount={isRefreshingAccount()} isRefreshingAccount={isRefreshingAccount()}
loggingOutId={loggingOutId()} loggingOutId={loggingOutId()}
@@ -538,12 +658,17 @@ const Account = () => {
selectedCourseId={selectedCourseId()} selectedCourseId={selectedCourseId()}
selectedCourse={selectedCourse()} selectedCourse={selectedCourse()}
recordType={recordType()} recordType={recordType()}
courseKind={courseKind()}
currentCourseKindLabel={currentCourseKindLabel()}
showingCachedRecords={showingCachedRecords()}
recordTypeOptions={recordTypeOptions} recordTypeOptions={recordTypeOptions}
courseRecordTypeOptions={statusOptions}
records={records()} records={records()}
studyLogs={studyLogs()} studyLogs={studyLogs()}
recordsLoading={recordsLoading()} recordsLoading={recordsLoading()}
recordError={recordError()} recordError={recordError()}
isRefreshingRecords={isRefreshingRecords()} isRefreshingRecords={isRefreshingRecords()}
isRefreshingCourseRecords={isRefreshingCourses()}
isRunningStudy={isRunningStudy()} isRunningStudy={isRunningStudy()}
isRefreshingLogs={isRefreshingLogs()} isRefreshingLogs={isRefreshingLogs()}
autoScrollLogs={settingsState().autoScrollLogs} autoScrollLogs={settingsState().autoScrollLogs}
@@ -552,9 +677,15 @@ const Account = () => {
logFontSize={settingsState().logFontSize} logFontSize={settingsState().logFontSize}
onSelectCourse={(courseId) => void handleSelectCourse(courseId)} onSelectCourse={(courseId) => void handleSelectCourse(courseId)}
onRefreshRecords={() => void handleRefreshRecords()} onRefreshRecords={() => void handleRefreshRecords()}
onRefreshCourseRecords={() => void loadCourses()}
onChangeRecordType={(value) => onChangeRecordType={(value) =>
accountStore.getState().setRecordType(value) accountStore.getState().setRecordType(value)
} }
onChangeCourseRecordType={(value) => {
accountStore.getState().setCourseKind(value);
accountStore.getState().setSelectedCourseId(null);
accountStore.getState().setRecords([]);
}}
onStartStudy={() => void handleStartStudy()} onStartStudy={() => void handleStartStudy()}
onStopStudy={handleStopStudy} onStopStudy={handleStopStudy}
onRefreshLogs={handleRefreshLogs} onRefreshLogs={handleRefreshLogs}

View File

@@ -65,7 +65,7 @@ const Setting = () => {
return ( return (
<div class="flex h-full min-h-0 flex-col overflow-hidden"> <div class="flex h-full min-h-0 flex-col overflow-hidden">
<div class="flex shrink-0 items-center justify-between gap-4 rounded-[28px] border border-white/80 bg-[linear-gradient(135deg,_rgba(255,255,255,0.92),_rgba(240,249,255,0.96))] px-5 py-4 shadow-[0_18px_45px_-28px_rgba(15,23,42,0.18)]"> <div class="flex shrink-0 items-center justify-between gap-4 rounded-[28px] border border-white/80 bg-[linear-gradient(135deg,rgba(255,255,255,0.92),rgba(240,249,255,0.96))] px-5 py-4 shadow-[0_18px_45px_-28px_rgba(15,23,42,0.18)]">
<div> <div>
<p class="text-xs font-medium tracking-[0.26em] text-cyan-700/75 uppercase"> <p class="text-xs font-medium tracking-[0.26em] text-cyan-700/75 uppercase">
Settings Center Settings Center

View File

@@ -24,7 +24,7 @@ export type LoginReq = {
}; };
export type LoginData = { export type LoginData = {
courses: CourseType[]; courses?: CourseType[];
session_id: string; session_id: string;
user: userInfoType; user: userInfoType;
}; };
@@ -103,12 +103,32 @@ export type HostRes = ApiResponse<{
list: HostItem[]; list: HostItem[];
}>; }>;
export type VersionData = {
BuildAt: string;
GitAuthor: string;
GitCommit: string;
GitEmail: string;
Version: string;
};
export type VersionRes = ApiResponse<VersionData>;
export type RecordReq = { export type RecordReq = {
course_id: string; course_id: string;
page: number; page: number;
record_type?: RecordType; record_type?: RecordType;
}; };
export type CourseReq = {
status: CourseKind;
};
export type CourseData = {
courses: CourseType[];
};
export type CourseRes = ApiResponse<CourseData>;
export type StudyReq = { export type StudyReq = {
node_id: string; node_id: string;
study_id: string; study_id: string;
@@ -131,11 +151,13 @@ export type StudyRunnerPayload = {
intervalSeconds: number; intervalSeconds: number;
items: StudyRunnerItem[]; items: StudyRunnerItem[];
isRunningStudy: Accessor<boolean>; isRunningStudy: Accessor<boolean>;
setIsRunningStudy: () => void;
client: WkClient; client: WkClient;
onLog?: (message: string, accoundID: string) => void; onLog?: (message: string, accoundID: string) => void;
}; };
export type WkClient = { export type WkClient = {
courseApi: (payload: CourseReq) => Promise<CourseRes>;
recordApi: (payload: RecordReq) => Promise<RecordRes>; recordApi: (payload: RecordReq) => Promise<RecordRes>;
studyApi: (payload: StudyReq) => Promise<StudyRes>; studyApi: (payload: StudyReq) => Promise<StudyRes>;
logoutApi: () => Promise<LogoutRes>; logoutApi: () => Promise<LogoutRes>;
@@ -147,6 +169,9 @@ export const loginApi = async (payload: LoginReq) => {
}; };
const createWkClientFromHttp = (client: HttpClient): WkClient => ({ const createWkClientFromHttp = (client: HttpClient): WkClient => ({
courseApi(payload) {
return client.post<CourseRes>("/api/v2/course", payload);
},
recordApi(payload) { recordApi(payload) {
return client.post<RecordRes>("/api/v2/record", payload); return client.post<RecordRes>("/api/v2/record", payload);
}, },
@@ -189,6 +214,12 @@ export const runStudyQueue = async (_payload: StudyRunnerPayload) => {
study_time: String(currentTime), study_time: String(currentTime),
status: count === 0 ? 1 : currentTime >= total ? 3 : 2, status: count === 0 ? 1 : currentTime >= total ? 3 : 2,
}); });
if (!resp.data.state) {
_payload.onLog?.(`⛔ 自动停止: ${resp.data.msg}`, _payload.accountId);
_payload.setIsRunningStudy();
return;
}
study_id = resp.data.studyId; study_id = resp.data.studyId;
if (currentTime === total) break; if (currentTime === total) break;
@@ -208,3 +239,7 @@ export const runStudyQueue = async (_payload: StudyRunnerPayload) => {
export const hostApi = async () => { export const hostApi = async () => {
return await http.get<HostRes>("/api/v1/host"); return await http.get<HostRes>("/api/v1/host");
}; };
export const versionApi = async () => {
return await http.get<VersionRes>("/api/version");
};

View File

@@ -36,25 +36,32 @@ export type AccountItem = {
courses: CourseType[]; courses: CourseType[];
}; };
export type RecordCacheMap = Record<string, RecordItem[]>;
type AccountState = { type AccountState = {
accounts: AccountItem[]; accounts: AccountItem[];
selectedAccountId: string; selectedAccountId: string;
expandedAccountId: string; expandedAccountId: string;
selectedCourseId: number | null; selectedCourseId: number | null;
courseKind: CourseKind;
recordType: RecordType; recordType: RecordType;
records: RecordItem[]; records: RecordItem[];
recordCacheMap: RecordCacheMap;
studyLogsMap: Record<string, string[]>; studyLogsMap: Record<string, string[]>;
runningStudyMap: Record<string, boolean>; runningStudyMap: Record<string, boolean>;
setSelectedAccountId: (accountId: string) => void; setSelectedAccountId: (accountId: string) => void;
setExpandedAccountId: (accountId: string) => void; setExpandedAccountId: (accountId: string) => void;
setSelectedCourseId: (courseId: number | null) => void; setSelectedCourseId: (courseId: number | null) => void;
setCourseKind: (courseKind: CourseKind) => void;
setRecordType: (recordType: RecordType) => void; setRecordType: (recordType: RecordType) => void;
setRecords: (records: RecordItem[]) => void; setRecords: (records: RecordItem[]) => void;
setRecordCache: (cacheKey: string, records: RecordItem[]) => void;
setAccountRunningStudy: (accountId: string, value: boolean) => void; setAccountRunningStudy: (accountId: string, value: boolean) => void;
appendStudyLog: (accountId: string, message: string) => void; appendStudyLog: (accountId: string, message: string) => void;
clearStudyLogs: (accountId: string) => void; clearStudyLogs: (accountId: string) => void;
clearAllStudyLogs: () => void; clearAllStudyLogs: () => void;
upsertAccount: (account: AccountItem) => void; upsertAccount: (account: AccountItem) => void;
setAccountCourses: (accountId: string, courses: CourseType[]) => void;
removeAccount: (accountId: string) => void; removeAccount: (accountId: string) => void;
}; };
@@ -65,8 +72,10 @@ export const accountStore = createStore<AccountState>()(
selectedAccountId: "", selectedAccountId: "",
expandedAccountId: "", expandedAccountId: "",
selectedCourseId: null, selectedCourseId: null,
courseKind: "run",
recordType: "", recordType: "",
records: [], records: [],
recordCacheMap: {},
studyLogsMap: {}, studyLogsMap: {},
runningStudyMap: {}, runningStudyMap: {},
setSelectedAccountId: (accountId) => setSelectedAccountId: (accountId) =>
@@ -74,8 +83,16 @@ export const accountStore = createStore<AccountState>()(
setExpandedAccountId: (accountId) => setExpandedAccountId: (accountId) =>
set({ expandedAccountId: accountId }), set({ expandedAccountId: accountId }),
setSelectedCourseId: (courseId) => set({ selectedCourseId: courseId }), setSelectedCourseId: (courseId) => set({ selectedCourseId: courseId }),
setCourseKind: (courseKind) => set({ courseKind }),
setRecordType: (recordType) => set({ recordType }), setRecordType: (recordType) => set({ recordType }),
setRecords: (records) => set({ records }), setRecords: (records) => set({ records }),
setRecordCache: (cacheKey, records) =>
set((state) => ({
recordCacheMap: {
...state.recordCacheMap,
[cacheKey]: records,
},
})),
setAccountRunningStudy: (accountId, value) => setAccountRunningStudy: (accountId, value) =>
set((state) => ({ set((state) => ({
runningStudyMap: { runningStudyMap: {
@@ -113,6 +130,12 @@ export const accountStore = createStore<AccountState>()(
selectedAccountId: account.id, selectedAccountId: account.id,
expandedAccountId: account.id, expandedAccountId: account.id,
})), })),
setAccountCourses: (accountId, courses) =>
set((state) => ({
accounts: state.accounts.map((item) =>
item.id === accountId ? { ...item, courses } : item,
),
})),
removeAccount: (accountId) => removeAccount: (accountId) =>
set((state) => { set((state) => {
const nextAccounts = state.accounts.filter( const nextAccounts = state.accounts.filter(
@@ -133,6 +156,11 @@ export const accountStore = createStore<AccountState>()(
? null ? null
: state.selectedCourseId, : state.selectedCourseId,
records: state.selectedAccountId === accountId ? [] : state.records, records: state.selectedAccountId === accountId ? [] : state.records,
recordCacheMap: Object.fromEntries(
Object.entries(state.recordCacheMap).filter(
([key]) => !key.startsWith(`${accountId}::`),
),
),
studyLogsMap: Object.fromEntries( studyLogsMap: Object.fromEntries(
Object.entries(state.studyLogsMap).filter( Object.entries(state.studyLogsMap).filter(
([key]) => key !== accountId, ([key]) => key !== accountId,
@@ -154,8 +182,10 @@ export const accountStore = createStore<AccountState>()(
selectedAccountId: state.selectedAccountId, selectedAccountId: state.selectedAccountId,
expandedAccountId: state.expandedAccountId, expandedAccountId: state.expandedAccountId,
selectedCourseId: state.selectedCourseId, selectedCourseId: state.selectedCourseId,
courseKind: state.courseKind,
recordType: state.recordType, recordType: state.recordType,
records: state.records, records: state.records,
recordCacheMap: state.recordCacheMap,
studyLogsMap: state.studyLogsMap, studyLogsMap: state.studyLogsMap,
runningStudyMap: state.runningStudyMap, runningStudyMap: state.runningStudyMap,
}), }),