From 58555c5043a9883cbf022757f08ed5a4233e7235 Mon Sep 17 00:00:00 2001 From: zhilv Date: Thu, 2 Apr 2026 22:33:04 +0800 Subject: [PATCH] feat(release): bump version to 0.1.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 详细信息 - 升级项目版本号到 0.1.2 - 增强刷课稳定性(失败重试、心跳检测、状态自动纠正) - 优化账号页与工作台紧凑布局,增加已学/未学区分与记录筛选 - 新增首次进入更新检查、更新日志弹窗与在线下载/回退 Release 跳转 --- package.json | 2 +- src/App.tsx | 593 +++++++++++++++++---- src/components/account/AccountSidebar.tsx | 54 +- src/components/account/CourseWorkspace.tsx | 245 ++++++--- src/pages/accounts/Account.tsx | 172 +++++- src/pages/logs/Logs.tsx | 280 ++++------ src/service/http.ts | 26 +- src/service/update.ts | 302 +++++++++++ src/service/wk.ts | 101 +++- src/store/account.ts | 124 ++++- src/store/settings.ts | 79 ++- vite.config.ts | 4 + 12 files changed, 1569 insertions(+), 413 deletions(-) create mode 100644 src/service/update.ts diff --git a/package.json b/package.json index ddb674e..dff997c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.0.0", + "version": "0.1.2", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.tsx b/src/App.tsx index 9318fdd..da3edc0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,27 @@ -import type { ParentComponent } from "solid-js"; -import { createMemo, createResource, createSignal } from "solid-js"; +import type { JSX, ParentComponent } from "solid-js"; +import { + For, + Show, + createEffect, + createMemo, + createResource, + createSignal, + onCleanup, +} from "solid-js"; import { A, useLocation } from "@solidjs/router"; +import Dialog from "~/components/dialog/Dialog"; +import { + RELEASES_PAGE_URL, + type LatestRelease, + type ReleaseAsset, + type RuntimeTarget, + detectRuntimeTarget, + downloadReleaseAsset, + fetchLatestRelease, + isRemoteVersionNewer, + resolveAssetForRuntime, + resolveReleaseLink, +} from "~/service/update"; import { versionApi } from "~/service/wk"; const asideList = [ @@ -9,12 +30,138 @@ const asideList = [ { label: "设置", url: "/setting" }, ]; +type DownloadState = "idle" | "downloading" | "done" | "error"; +type UpdateCheckState = + | "idle" + | "checking" + | "latest" + | "available" + | "error"; +type MarkdownBlock = + | { type: "heading"; level: 1 | 2 | 3 | 4 | 5 | 6; text: string } + | { type: "paragraph"; text: string } + | { type: "list"; items: string[] }; + +const normalizeMarkdown = (value: string) => value.replace(/\r\n?/g, "\n"); + +const parseMarkdownBlocks = (markdown: string): MarkdownBlock[] => { + const lines = normalizeMarkdown(markdown).split("\n"); + const blocks: MarkdownBlock[] = []; + let index = 0; + + while (index < lines.length) { + const line = lines[index].trim(); + if (!line) { + index += 1; + continue; + } + + const headingMatch = line.match(/^(#{1,6})\s+(.*)$/); + if (headingMatch) { + const level = Math.min( + 6, + Math.max(1, headingMatch[1].length), + ) as 1 | 2 | 3 | 4 | 5 | 6; + blocks.push({ + type: "heading", + level, + text: headingMatch[2].trim(), + }); + index += 1; + continue; + } + + if (line.startsWith("- ")) { + const items: string[] = []; + while (index < lines.length) { + const nextLine = lines[index].trim(); + if (!nextLine.startsWith("- ")) { + break; + } + items.push(nextLine.slice(2).trim()); + index += 1; + } + blocks.push({ type: "list", items }); + continue; + } + + const paragraphLines: string[] = [line]; + index += 1; + while (index < lines.length) { + const nextLine = lines[index].trim(); + if (!nextLine || nextLine.startsWith("- ") || /^#{1,6}\s+/.test(nextLine)) { + break; + } + paragraphLines.push(nextLine); + index += 1; + } + blocks.push({ + type: "paragraph", + text: paragraphLines.join(" ").trim(), + }); + } + + return blocks; +}; + +const renderInlineLinks = (text: string): JSX.Element[] => { + const parts: JSX.Element[] = []; + const regex = /\[([^\]]+)\]\(([^)]+)\)/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(text.slice(lastIndex, match.index)); + } + + const label = match[1] || "链接"; + const href = resolveReleaseLink(match[2] || ""); + parts.push( + + {label} + , + ); + lastIndex = regex.lastIndex; + } + + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + + return parts.length > 0 ? parts : [text]; +}; + const App: ParentComponent = (props) => { const location = useLocation(); const [version] = createResource(versionApi); const [copyState, setCopyState] = createSignal<"idle" | "done" | "error">( "idle", ); + const [updateDialogOpen, setUpdateDialogOpen] = createSignal(false); + const [updateCheckState, setUpdateCheckState] = + createSignal("idle"); + const [hasAutoCheckedUpdate, setHasAutoCheckedUpdate] = createSignal(false); + const [latestRelease, setLatestRelease] = createSignal( + null, + ); + const [runtimeTarget, setRuntimeTarget] = createSignal({ + os: "unknown", + arch: "unknown", + }); + const [matchedAsset, setMatchedAsset] = createSignal( + null, + ); + const [updateCheckError, setUpdateCheckError] = createSignal(""); + const [downloadState, setDownloadState] = createSignal("idle"); + const [downloadProgress, setDownloadProgress] = createSignal(0); + const [downloadError, setDownloadError] = createSignal(""); + let updateAbortController: AbortController | null = null; const isActive = (url: string) => location.pathname === url || @@ -45,6 +192,30 @@ const App: ParentComponent = (props) => { `Email: ${emailText()}`, ].join("\n"), ); + const updateSummaryText = createMemo(() => { + if (updateCheckState() === "checking") { + return "更新检查中..."; + } + if (updateCheckState() === "available") { + return `发现新版本:${latestRelease()?.tag_name ?? "-"}`; + } + if (updateCheckState() === "latest") { + return "已是最新版本"; + } + if (updateCheckState() === "error") { + return updateCheckError() || "更新检查失败"; + } + return "未检查更新"; + }); + const releaseNotesBlocks = createMemo(() => + parseMarkdownBlocks(latestRelease()?.body ?? ""), + ); + const runtimeTargetText = createMemo( + () => `${runtimeTarget().os} / ${runtimeTarget().arch}`, + ); + const releaseLink = createMemo( + () => latestRelease()?.html_url || RELEASES_PAGE_URL, + ); const handleCopyVersion = async () => { try { @@ -56,109 +227,349 @@ const App: ParentComponent = (props) => { window.setTimeout(() => setCopyState("idle"), 1800); }; + const performUpdateCheck = async (manual = false) => { + if (updateCheckState() === "checking") { + return; + } + + setUpdateCheckState("checking"); + setUpdateCheckError(""); + setDownloadState("idle"); + setDownloadProgress(0); + setDownloadError(""); + + if (updateAbortController) { + updateAbortController.abort(); + } + updateAbortController = new AbortController(); + + try { + const [release, target] = await Promise.all([ + fetchLatestRelease(updateAbortController.signal), + detectRuntimeTarget(), + ]); + setRuntimeTarget(target); + + const hasNewVersion = isRemoteVersionNewer(versionText(), release.tag_name); + if (!hasNewVersion) { + setUpdateCheckState("latest"); + if (manual) { + setLatestRelease(release); + setMatchedAsset(resolveAssetForRuntime(release.assets, target)); + } + return; + } + + setLatestRelease(release); + setMatchedAsset(resolveAssetForRuntime(release.assets, target)); + setUpdateCheckState("available"); + setUpdateDialogOpen(true); + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + return; + } + + const message = error instanceof Error ? error.message : "更新检查失败"; + setUpdateCheckError(message); + setUpdateCheckState("error"); + } + }; + const openReleasePage = () => { + window.open(releaseLink(), "_blank", "noopener,noreferrer"); + }; + const handleDownloadUpdate = async () => { + const asset = matchedAsset(); + if (!asset) { + openReleasePage(); + return; + } + + setDownloadState("downloading"); + setDownloadProgress(0); + setDownloadError(""); + try { + await downloadReleaseAsset(asset, (progress) => { + setDownloadProgress(progress); + }); + setDownloadState("done"); + } catch (error) { + const message = error instanceof Error ? error.message : "下载失败"; + setDownloadError(message); + setDownloadState("error"); + openReleasePage(); + } + }; + + createEffect(() => { + version.loading; + if (hasAutoCheckedUpdate() || version.loading) { + return; + } + + setHasAutoCheckedUpdate(true); + void performUpdateCheck(false); + }); + + onCleanup(() => { + if (updateAbortController) { + updateAbortController.abort(); + updateAbortController = null; + } + }); return ( -
-
-
-
- - WK - -
+ <> +
+
+
+
- 网课控制台 + WK -

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

+
+ + 网课控制台 + +

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

+
-
-
+ -
-
+ -
-

当前页面

-

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

-

- Runtime -

-

Version: {versionText()}

-

Commit: {commitText()}

-

Build: {buildText()}

-

Author: {authorText()}

-

Email: {emailText()}

- - {versionErrorText() ? ( -

{versionErrorText()}

- ) : null} -
- - -
- {props.children} -
+
+ {props.children} +
+
- + + { + if (downloadState() === "downloading") { + return; + } + setUpdateDialogOpen(false); + }} + title={`发现更新 ${latestRelease()?.tag_name ?? ""}`} + widthClass="max-w-3xl" + closeOnOverlay={downloadState() !== "downloading"} + footer={ + <> + + + + + } + > +
+
+

+ 当前版本:{versionText()} +

+

+ 最新版本: + + {latestRelease()?.tag_name ?? "-"} + +

+

+ 发布名称: + + {latestRelease()?.name ?? "-"} + +

+

+ 运行环境: + {runtimeTargetText()} +

+

+ 匹配资源: + + {matchedAsset()?.name ?? "未识别当前系统架构,请点击“打开 Release”手动下载"} + +

+
+ + +
+

+ {downloadState() === "downloading" + ? "下载进度" + : downloadState() === "done" + ? "下载完成" + : "下载失败"} +

+
+
+
+

+ {downloadState() === "downloading" + ? `已下载 ${downloadProgress()}%` + : downloadState() === "done" + ? "安装包已下载到浏览器默认下载目录,请替换本地程序后重启。" + : downloadError() || "下载失败,请改为 Release 页面手动下载。"} +

+
+ + +
+

更新日志

+
+ + {(block) => { + if (block.type === "heading") { + return ( +

+ {renderInlineLinks(block.text)} +

+ ); + } + + if (block.type === "list") { + return ( +
    + + {(item) =>
  • {renderInlineLinks(item)}
  • } +
    +
+ ); + } + + return

{renderInlineLinks(block.text)}

; + }} +
+
+
+
+
+ ); }; diff --git a/src/components/account/AccountSidebar.tsx b/src/components/account/AccountSidebar.tsx index 9be109a..7cd66b1 100644 --- a/src/components/account/AccountSidebar.tsx +++ b/src/components/account/AccountSidebar.tsx @@ -29,21 +29,23 @@ const AccountSidebar = (props: AccountSidebarProps) => { return (
-
+
-

账号信息

-

选择账号后查看课程与记录

+

账号信息

+

+ 选择账号后查看课程与记录 +

@@ -123,26 +135,26 @@ const AccountSidebar = (props: AccountSidebarProps) => {
-
+

学院:{account.user.dept}

班级:{account.user.class}

性别:{account.user.gender}

站点:{account.host}

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

{courseCountLabel}

-

{courseTypeLabel}

+

{courseTypeLabel}

-
+
-
+
当前“{props.currentCourseKindLabel} @@ -173,18 +206,18 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => { class={ selected() ? compact() - ? "rounded-[20px] border border-cyan-300 bg-[linear-gradient(145deg,rgba(236,254,255,0.95),rgba(240,253,244,0.95))] px-3 py-3 text-left shadow-sm" - : "rounded-[22px] border border-cyan-300 bg-[linear-gradient(145deg,rgba(236,254,255,0.95),rgba(240,253,244,0.95))] px-4 py-4 text-left shadow-sm" + ? "rounded-[16px] border border-cyan-300 bg-[linear-gradient(145deg,rgba(236,254,255,0.95),rgba(240,253,244,0.95))] px-2 py-2 text-left shadow-sm" + : "rounded-[18px] border border-cyan-300 bg-[linear-gradient(145deg,rgba(236,254,255,0.95),rgba(240,253,244,0.95))] px-2.5 py-2.5 text-left shadow-sm" : compact() - ? "rounded-[20px] border border-zinc-200 bg-white px-3 py-3 text-left shadow-sm transition hover:border-cyan-200 hover:bg-cyan-50/30" - : "rounded-[22px] border border-zinc-200 bg-white px-4 py-4 text-left shadow-sm transition hover:border-cyan-200 hover:bg-cyan-50/30" + ? "rounded-[16px] border border-zinc-200 bg-white px-2 py-2 text-left shadow-sm transition hover:border-cyan-200 hover:bg-cyan-50/30" + : "rounded-[18px] border border-zinc-200 bg-white px-2.5 py-2.5 text-left shadow-sm transition hover:border-cyan-200 hover:bg-cyan-50/30" } onClick={() => props.onSelectCourse(course.id)} > -

+

{course.name}

-
+

课程号:{course.id}

老师:{course.teacher}

进度:{course.progress}

@@ -199,14 +232,16 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
-
-
+
+
-

记录列表

+

+ 记录列表 +

{props.selectedCourse ? props.selectedCourse.name @@ -221,10 +256,10 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {

-
+