diff --git a/src/App.tsx b/src/App.tsx index c6e4953..a2a0c4e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -400,7 +400,7 @@ const App: ParentComponent = (props) => { class={ active ? "min-w-fit whitespace-nowrap rounded-2xl border border-cyan-200 bg-[linear-gradient(135deg,_rgba(34,211,238,0.18),_rgba(34,197,94,0.18))] px-4 py-3 text-sm font-medium text-zinc-900 shadow-sm" - : "min-w-fit whitespace-nowrap rounded-2xl border border-transparent px-4 py-3 text-sm font-medium text-zinc-600 transition hover:border-zinc-200 hover:bg-zinc-50/80 hover:text-zinc-900" + : "min-w-fit whitespace-nowrap rounded-2xl border border-transparent px-4 py-3 text-sm font-medium text-zinc-600 transition active:scale-[0.98] hover:border-zinc-200 hover:bg-zinc-50/80 hover:text-zinc-900" } >
@@ -449,7 +449,7 @@ const App: ParentComponent = (props) => {
+
+ + 0}> +
+ 共 {props.workList.length} 条作业记录 +
+
+ 0}> +
+ 共 {props.examList.length} 条考试记录 +
+
{ !props.recordsLoading && !props.recordError && props.selectedCourse && + props.recordType === "" && filteredRecords().length === 0 } > @@ -370,59 +387,195 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => { -
- - {(record) => { - const stateText = - props.renderRecordState(record.state) || "未知状态"; - const learned = - stateText.includes("已学") || record.progress === "1.00"; + + 当前课程下没有作业记录。 + - return ( -
-
-
-

- {record.name} -

-

- 记录 ID:{record.id} | 章节:{record.chapterId} -

+ + 当前课程下没有考试记录。 + + + +
+ + {(record) => { + const stateText = + props.renderRecordState(record.state) || "未知状态"; + const learned = + stateText.includes("已学") || record.progress === "1.00"; + + return ( +
+
+
+

+ {record.name} +

+

+ 记录 ID:{record.id} | 章节:{record.chapterId} +

+
+ + {stateText} +
- - {stateText} - -
-
-

视频时长:{record.videoDuration}

-

学习秒数:{record.duration}

-

学习进度:{record.progress}

-

开始时间:{record.beginTime || "-"}

-

完成时间:{record.finalTime || "-"}

-

查看次数:{record.viewCount}

+
+

视频时长:{record.videoDuration}

+

学习秒数:{record.duration}

+

学习进度:{record.progress}

+

开始时间:{record.beginTime || "-"}

+

完成时间:{record.finalTime || "-"}

+

查看次数:{record.viewCount}

+
-
- ); - }} - -
+ ); + }} + +
+ + + +
+ + {(work) => { + const stateRaw = stripHtml(work.state); + const stateText = stateRaw || "未做"; + const done = stateRaw.includes("已阅") || stateRaw.includes("已提交") || stateRaw.includes("已完成"); + + return ( +
+
+
+

+ {work.title || work.name} +

+

+ 作业 ID:{work.id} | 章节:{work.chapterId} +

+
+ + {stateText} + +
+ +
+

类型:{work.typeName || work.type || "-"}

+

总分:{work.score || "-"}

+

得分:{stripHtml(work.finalScore) || "-"}

+

题目数:{work.topicNumber || "-"}

+

添加时间:{work.addTime || "-"}

+

完成时间:{work.finishTime !== "-" ? work.finishTime : "-"}

+
+
+ ); + }} +
+
+
+ + +
+ + {(exam) => { + const stateRaw = stripHtml(exam.state); + const stateText = stateRaw || "未做"; + const done = stateRaw.includes("已阅") || stateRaw.includes("已提交") || stateRaw.includes("已完成"); + + return ( +
+
+
+

+ {exam.title || exam.name} +

+

+ 考试 ID:{exam.id} | 章节:{exam.chapterId} +

+
+ + {stateText} + +
+ +
+

限时:{exam.limitedTime ? `${exam.limitedTime}分钟` : "-"}

+

总分:{exam.score || "-"}

+

得分:{stripHtml(exam.finalScore) || "-"}

+

题目数:{exam.topicNumber || "-"}

+

添加时间:{exam.addTime || "-"}

+

完成时间:{exam.finishTime !== "-" ? exam.finishTime : "-"}

+
+
+ ); + }} +
+
+
@@ -440,7 +593,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
-
-
-
-
-
-

本地数据

-

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

+
+
+
+
+
+
+

本地数据

+

+ 管理本地缓存数据 +

+
+ +
+ +
+
+
+

账号缓存

+

账号、课程和登录信息

+
+ +
+ +
+
+

记录缓存

+

课程记录和筛选类型

+
+ +
+ +
+
+

日志缓存

+

任务日志历史

+
+ +
-
-
-
-
-
-

账号缓存

-

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

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

记录缓存

-

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

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

日志缓存

-

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

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

界面偏好

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

-
+
-
+

Host 配置策略

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

添加本地 Host

- + ); diff --git a/src/service/wk.ts b/src/service/wk.ts index 4f5e16c..c65b6da 100644 --- a/src/service/wk.ts +++ b/src/service/wk.ts @@ -1,6 +1,5 @@ import type { Accessor } from "solid-js"; import http, { - DEFAULT_HTTP_TIMEOUT_MS, createHttpClient, type HttpClient, } from "~/service/http"; @@ -13,6 +12,80 @@ export type RecordType = "" | "/work" | "/exam" | "/discuss"; export type StudyStatus = 1 | 2 | 3; +export type WorkListItem = { + id: string; + userId: string | number | null; + title: string; + topicNumber: string; + score: string; + type: string; + remarks: string; + addTime: string; + sequence: string; + nodeId: string; + courseId: string; + startTime: string; + endTime: string; + paperId: string; + createUserId: string; + isPrivate: string; + classList: string; + teacherType: string; + allow: string; + frequency: string; + scoringRules: string; + hasCollect: string; + lock: string | number | null; + schoolId: string; + parsing: string; + addDate: string; + name: string; + chapterId: string; + state: string; + submitTime: string; + finalScore: string; + typeName: string; + finishTime: string; + url: string; +}; + +export type ExamListItem = { + id: string; + userId: string | number | null; + title: string; + topicNumber: string; + score: string; + addTime: string; + nodeId: string; + courseId: string; + limitedTime: string; + sequence: string; + remarks: string; + paperId: string; + startTime: string; + endTime: string; + createUserId: string; + classList: string; + isPrivate: string; + teacherType: string; + allow: string; + frequency: string; + hasCollect: string; + schoolId: string; + parsing: string; + addDate: string; + random: string; + randData: unknown; + randNumber: string; + name: string; + chapterId: string; + state: string; + submitTime: string; + finalScore: string; + finishTime: string; + url: string; +}; + type ApiResponse = { code: number; message: string; @@ -165,16 +238,33 @@ export type StudyRunnerPayload = { onLog?: (message: string, accoundID: string) => void; }; +export type WorkListData = { + list: WorkListItem[]; + page_info: PageInfo; +}; + +export type WorkListRes = ApiResponse; + +export type ExamListData = { + list: ExamListItem[]; + page_info: PageInfo; +}; + +export type ExamListRes = ApiResponse; + export type WkClient = { userInfoApi: () => Promise; courseApi: (payload: CourseReq) => Promise; recordApi: (payload: RecordReq) => Promise; + workListApi: (payload: RecordReq) => Promise; + examListApi: (payload: RecordReq) => Promise; studyApi: (payload: StudyReq) => Promise; logoutApi: () => Promise; }; const RECORD_API_TIMEOUT_MS = 60000; -const COURSE_API_TIMEOUT_MS = Math.max(DEFAULT_HTTP_TIMEOUT_MS, 30000); +// Course list can be slow on large accounts, use a longer timeout than default +const COURSE_API_TIMEOUT_MS = 30000; export const loginApi = async (payload: LoginReq) => { const res = await http.post("/api/login", payload); @@ -195,6 +285,22 @@ const createWkClientFromHttp = (client: HttpClient): WkClient => ({ timeout: RECORD_API_TIMEOUT_MS, }); }, + workListApi(payload) { + return client.post("/api/v2/record", { + ...payload, + record_type: "/work", + }, { + timeout: RECORD_API_TIMEOUT_MS, + }); + }, + examListApi(payload) { + return client.post("/api/v2/record", { + ...payload, + record_type: "/exam", + }, { + timeout: RECORD_API_TIMEOUT_MS, + }); + }, studyApi(payload) { return client.post("/api/v2/study", payload); }, @@ -313,7 +419,7 @@ export const runStudyQueue = async (_payload: StudyRunnerPayload) => { continue; } - if (resp.data.state != 0) { + if (resp.data.state !== 0) { _payload.onLog?.(`⛔ 自动停止: ${resp.data.msg}`, _payload.accountId); _payload.setIsRunningStudy(); return; diff --git a/src/store/account.ts b/src/store/account.ts index b24f540..a4de26b 100644 --- a/src/store/account.ts +++ b/src/store/account.ts @@ -1,6 +1,6 @@ import { createStore } from "zustand/vanilla"; import { persist, createJSONStorage } from "zustand/middleware"; -import type { CourseKind, RecordItem, RecordType } from "~/service/wk"; +import type { CourseKind, RecordItem, RecordType, WorkListItem, ExamListItem } from "~/service/wk"; import type { CourseType } from "~/types/Course"; import type { userInfoType } from "~/types/Userinfo"; @@ -39,6 +39,8 @@ export type AccountItem = { export type RecordCacheMap = Record; +export type WorkExamCacheMap = Record; + type PersistPreferences = { persistAccounts: boolean; persistRecords: boolean; @@ -87,6 +89,9 @@ type AccountState = { recordType: RecordType; records: RecordItem[]; recordCacheMap: RecordCacheMap; + workList: WorkListItem[]; + examList: ExamListItem[]; + workExamCacheMap: WorkExamCacheMap; studyLogsMap: Record; runningStudyMap: Record; studyHeartbeatMap: Record; @@ -97,6 +102,9 @@ type AccountState = { setRecordType: (recordType: RecordType) => void; setRecords: (records: RecordItem[]) => void; setRecordCache: (cacheKey: string, records: RecordItem[]) => void; + setWorkList: (list: WorkListItem[]) => void; + setExamList: (list: ExamListItem[]) => void; + setWorkExamCache: (cacheKey: string, data: WorkListItem[] | ExamListItem[]) => void; setAccountRunningStudy: (accountId: string, value: boolean) => void; touchStudyHeartbeat: (accountId: string, timestamp?: number) => void; clearStudyHeartbeat: (accountId: string) => void; @@ -107,6 +115,9 @@ type AccountState = { upsertAccount: (account: AccountItem) => void; setAccountCourses: (accountId: string, courses: CourseType[]) => void; removeAccount: (accountId: string) => void; + clearAllData: () => void; + clearRecordsData: () => void; + clearAccountsData: () => void; }; export const accountStore = createStore()( @@ -120,6 +131,9 @@ export const accountStore = createStore()( recordType: "", records: [], recordCacheMap: {}, + workList: [], + examList: [], + workExamCacheMap: {}, studyLogsMap: {}, runningStudyMap: {}, studyHeartbeatMap: {}, @@ -138,6 +152,15 @@ export const accountStore = createStore()( [cacheKey]: records, }, })), + setWorkList: (list) => set({ workList: list }), + setExamList: (list) => set({ examList: list }), + setWorkExamCache: (cacheKey, data) => + set((state) => ({ + workExamCacheMap: { + ...state.workExamCacheMap, + [cacheKey]: data, + }, + })), setAccountRunningStudy: (accountId, value) => set((state) => ({ runningStudyMap: { @@ -189,6 +212,39 @@ export const accountStore = createStore()( set({ studyLogsMap: {}, }), + clearAllData: () => + set({ + accounts: [], + selectedAccountId: "", + expandedAccountId: "", + selectedCourseId: null, + courseKind: "run" as CourseKind, + recordType: "" as RecordType, + records: [], + recordCacheMap: {}, + workList: [], + examList: [], + workExamCacheMap: {}, + studyLogsMap: {}, + runningStudyMap: {}, + studyHeartbeatMap: {}, + }), + clearRecordsData: () => + set({ + records: [], + recordCacheMap: {}, + workList: [], + examList: [], + workExamCacheMap: {}, + selectedCourseId: null, + }), + clearAccountsData: () => + set({ + accounts: [], + selectedAccountId: "", + expandedAccountId: "", + selectedCourseId: null, + }), upsertAccount: (account) => set((state) => ({ accounts: [ diff --git a/src/store/settings.ts b/src/store/settings.ts index ceed1f8..8441379 100644 --- a/src/store/settings.ts +++ b/src/store/settings.ts @@ -1,5 +1,6 @@ import { createStore } from "zustand/vanilla"; import { createJSONStorage, persist } from "zustand/middleware"; +import { accountStore } from "~/store/account"; export type HostOption = { label: string; @@ -37,6 +38,7 @@ type SettingsState = { }; const accountStorageKey = "account-storage"; +const settingsStorageKey = "settings-storage"; type PersistedStorage = { state?: Record; version?: number; @@ -115,53 +117,40 @@ export const settingsStore = createStore()( } }, clearPersistedSection: (section) => { + const store = accountStore.getState(); + if (section === "accounts") { - patchAccountStorage((state) => { - const { - accounts, - selectedAccountId, - expandedAccountId, - selectedCourseId, - records, - recordCacheMap, - studyLogsMap, - runningStudyMap, - ...rest - } = state; - void accounts; - void selectedAccountId; - void expandedAccountId; - void selectedCourseId; - void records; - void recordCacheMap; - void studyLogsMap; - void runningStudyMap; - return rest; - }); + store.clearAccountsData(); + localStorage.removeItem(accountStorageKey); } if (section === "records") { + store.clearRecordsData(); patchAccountStorage((state) => { - const { records, recordCacheMap, selectedCourseId, ...rest } = - state; + const { records, recordCacheMap, workList, examList, workExamCacheMap, ...rest } = state; void records; void recordCacheMap; - void selectedCourseId; + void workList; + void examList; + void workExamCacheMap; return rest; }); } if (section === "logs") { + store.clearAllStudyLogs(); patchAccountStorage((state) => { - const { studyLogsMap, runningStudyMap, ...rest } = state; + const { studyLogsMap, ...rest } = state; void studyLogsMap; - void runningStudyMap; return rest; }); } }, clearAllPersistedData: () => { + accountStore.getState().clearAllData(); localStorage.removeItem(accountStorageKey); + localStorage.removeItem(settingsStorageKey); + window.location.reload(); }, setDebugEnabled: (value) => set({ debugEnabled: value }), setAutoScrollLogs: (value) => set({ autoScrollLogs: value }),