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 = [ { label: "账号", url: "/account" }, { label: "日志", url: "/logs" }, { 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 || (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 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 { await navigator.clipboard.writeText(versionPayloadText()); setCopyState("done"); } catch { setCopyState("error"); } 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
网课控制台

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

{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)}

; }}
); }; export default App;