fix: 修复问题
- 修复刷课错误不会停止 - 添加课程刷新 - 添加课程列表、记录列表缓存 - 显示当前版本
This commit is contained in:
82
src/App.tsx
82
src/App.tsx
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user