diff --git a/.gitignore b/.gitignore index 4390830..69af76a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ biliup/**/public !package-lock.json qrcode.png *.log +*.tsbuildinfo /*.ts /biliup/stream-gears/ data/ diff --git a/app/(app)/bg-global.css b/app/(app)/bg-global.css new file mode 100644 index 0000000..2ecdcf5 --- /dev/null +++ b/app/(app)/bg-global.css @@ -0,0 +1,43 @@ +/* 全局自定义背景:当 body 带 app-has-bg 时生效 */ +html, +body { + overflow-x: hidden; +} + +body.app-has-bg::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: -2; + background-image: var(--app-bg-image); + background-repeat: no-repeat; + background-size: cover; + background-position: center; + pointer-events: none; +} + +/* 同色半透明遮罩,保证所有页面文字可读(明暗主题都跟随,透明度可调) */ +body.app-has-bg::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: var(--semi-color-bg-0); + opacity: var(--app-bg-mask-opacity, 0.35); + z-index: -1; + pointer-events: none; +} + +/* 让 Semi 顶层容器透出 body 背景(兼容旧布局) */ +body.app-has-bg .semi-layout, +body.app-has-bg .semi-sider, +body.app-has-bg .semi-nav, +body.app-has-bg .semi-layout-content, +body.app-has-bg .semi-layout-header { + background: transparent !important; +} diff --git a/app/(app)/changelog/changelog.module.scss b/app/(app)/changelog/changelog.module.scss new file mode 100644 index 0000000..0614206 --- /dev/null +++ b/app/(app)/changelog/changelog.module.scss @@ -0,0 +1,104 @@ +.content { + padding: 24px; + min-height: calc(100vh - 69px); +} + +.feed, +.center { + position: relative; + z-index: 1; +} + +.center { + display: flex; + align-items: center; + justify-content: center; + min-height: 320px; +} + +/* 版本卡片流:居中、限制最大阅读宽度 */ +.feed { + max-width: 820px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 16px; +} + +.card { + border: 1px solid var(--semi-color-fill-2); + border-radius: 12px; + background: var(--semi-color-bg-1); +} + +.cardHeader { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 16px 24px 0; +} + +.versionBlock { + display: flex; + align-items: center; + gap: 10px; +} + +.versionTitle { + margin: 0 !important; + color: var(--semi-color-text-0); + font-family: var(--semi-font-regular); +} + +.ul { + padding-left: 18px; + margin: 8px 0 14px; +} + +.li { + margin: 4px 0; + font-size: 13px; + line-height: 1.7; + color: var(--semi-color-text-1); +} + +.subhead { + font-size: 13px; + font-weight: 600; + margin: 14px 0 4px; + color: var(--semi-color-text-0); +} + +.subheadMuted { + font-size: 12px; + font-weight: 600; + margin: 16px 0 4px; + color: var(--semi-color-text-2); + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.subheadMuted + .ul .li { + color: var(--semi-color-text-2); +} + +.para { + font-size: 13px; + margin: 6px 0; + color: var(--semi-color-text-1); +} + +.link { + color: rgb(var(--semi-blue-4)); + text-decoration: none; +} + +.link:hover { + text-decoration: underline; +} + +/* 第一个(最新)版本卡片轻微强调 */ +.feed > .card:first-child { + border-color: var(--semi-color-primary); +} diff --git a/app/(app)/changelog/page.tsx b/app/(app)/changelog/page.tsx new file mode 100644 index 0000000..def8422 --- /dev/null +++ b/app/(app)/changelog/page.tsx @@ -0,0 +1,311 @@ +'use client' +import React from 'react' +import { Layout, Card, Button, Spin, Empty, Tag, Typography, Tooltip } from '@douyinfe/semi-ui' +import useSWR from 'swr' +import { IconBook, IconExport } from '@douyinfe/semi-icons' +import PageHeader from '../components/PageHeader' +import { fetcher } from '@/app/lib/api-streamer' +import { formatVersion } from '@/app/lib/status' +import { SLOW_REFRESH_MS } from '@/app/lib/use-dashboard' +import styles from './changelog.module.scss' + +const { Header, Content } = Layout +const { Title, Text } = Typography + +// 数据源:biliup 官方 CHANGELOG(随版本自动更新,零后端改动) +const CHANGELOG_URL = + 'https://raw.githubusercontent.com/biliup/biliup/master/docs/content/docs/guide/CHANGELOG.md' +const MAX_VERSIONS = 25 + +const CHANGELOG_CACHE_KEY = 'biliup_changelog_cache' + +/** + * 拉取官方 CHANGELOG:成功则写入 localStorage 作为离线缓存; + * 失败(离线 / GitHub 被墙 / 接口异常)时回退到上一次缓存, + * 避免页面直接白屏成「无法加载」。仅在既失败又无缓存时才真正报错。 + */ +async function fetchChangelog(url: string): Promise<{ text: string; stale: boolean }> { + try { + const r = await fetch(url) + if (!r.ok) throw new Error(`http ${r.status}`) + const text = await r.text() + try { + localStorage.setItem(CHANGELOG_CACHE_KEY, text) + } catch { + /* 隐私模式等场景忽略写入失败 */ + } + return { text, stale: false } + } catch { + let cached: string | null = null + try { + cached = localStorage.getItem(CHANGELOG_CACHE_KEY) + } catch { + cached = null + } + if (cached) return { text: cached, stale: true } + throw new Error('changelog fetch failed and no cache') + } +} + +export interface VersionEntry { + version: string + body: string + compareUrl?: string +} + +/** + * 把 CHANGELOG markdown 解析为版本数组。 + * - 跳过文档站 frontmatter(+++ ... +++) + * - 仅保留 `## 版本号` 段,过滤 `## 标签含义` 等噪声段 + * - 提取 `**Full Changelog**: [text](url)` 外链,并从正文移除 + * - 取最近 MAX_VERSIONS 个版本 + */ +function parseChangelog(md: string): VersionEntry[] { + const noFm = md.replace(/^\+\+\+[\s\S]*?\+\+\+\s*/, '') + const lines = noFm.split(/\r?\n/) + const out: VersionEntry[] = [] + let cur: { version: string; lines: string[]; compareUrl?: string } | null = null + + const fullChangelogRe = /^\s*\*\*Full Changelog\*\*\s*:\s*\[([^\]]+)\]\(([^)]+)\)\s*$/ + + for (const line of lines) { + const m = line.match(/^##\s+([\d.]+(-[\d.]+)?)\s*$/) + if (m) { + if (cur) out.push({ version: cur.version, body: cur.lines.join('\n').trim(), compareUrl: cur.compareUrl }) + cur = { version: m[1], lines: [] } + } else if (/^##\s+/.test(line)) { + // 非版本标题(如「标签含义」),结束当前收集 + cur = null + } else if (cur) { + const fm = line.match(fullChangelogRe) + if (fm && /^https?:\/\//i.test(fm[2])) { + cur.compareUrl = fm[2] + continue + } + cur.lines.push(line) + } + } + if (cur) out.push({ version: cur.version, body: cur.lines.join('\n').trim(), compareUrl: cur.compareUrl }) + return out.slice(0, MAX_VERSIONS) +} + +// 行内渲染:将 [text](url) 渲染为外链(仅放行 http/https,防注入),**bold** 加粗 +function renderInline(text: string, keyPrefix: string): React.ReactNode[] { + const out: React.ReactNode[] = [] + const linkRe = /\[([^\]]+)\]\(([^)\s]+)\)/g + let last = 0 + let m: RegExpExecArray | null + let i = 0 + const pushText = (t: string, k: string) => { + const parts = t.split(/\*\*([^*]+)\*\*/) + parts.forEach((p, idx) => { + if (p === '') return + out.push( + idx % 2 === 1 ? ( + {p} + ) : ( + {p} + ) + ) + }) + } + while ((m = linkRe.exec(text)) !== null) { + if (m.index > last) pushText(text.slice(last, m.index), `${keyPrefix}-${i}`) + const url = m[2] + const safe = /^https?:\/\//i.test(url) + out.push( + safe ? ( + + {m[1]} + + ) : ( + {m[1]} + ) + ) + last = m.index + m[0].length + i++ + } + if (last < text.length) pushText(text.slice(last), `${keyPrefix}-${i}`) + return out +} + +// 把一个版本的正文渲染成:小标题 / 列表 / 普通段落 +function renderBody(body: string, versionKey: string): React.ReactNode { + const lines = body.split(/\r?\n/) + const blocks: React.ReactNode[] = [] + let listBuf: string[] = [] + const flush = (k: string) => { + if (listBuf.length === 0) return + const items = listBuf + listBuf = [] + blocks.push( +
各平台下载设置
-