发现与浏览界面
发现页从项目的 GitHub Releases 接口读取版本发布记录,提供按标签选择、发布日期展示及 Markdown 正文浏览;桌面和移动端使用不同的列表/详情布局。
Purpose and Scope
本文聚焦发现页的入口、远程数据加载、版本选择、正文渲染和移动端导航。归档页虽然复用部分布局类名和移动端列表/详情模式,但管理的是另一类结果,不在本文展开;分析、设置和关于页面也各有独立职责。发现页的远程发布记录不经过归档存储:页面组件直接调用 fetch,将响应保存在组件状态中。Discover.tsx
Overview
App 以 currentPage === "discover" 为条件挂载 Discover,并依据宽度 768 像素的断点决定是否传递移动子标签状态。移动端进入发现页时,App 将子标签设为 list;MobileNav 则提供发现页的 list、detail 子标签。发现组件请求固定仓库的 releases 列表,默认选择响应数组第一项的 tag_name,左侧/列表展示标签与日期,详情展示名称、标签、日期和渲染后的 body。App.tsx · App.tsx · App.tsx · MobileNav.tsx · Discover.tsx
Architecture
Sources: App.tsx, MobileNav.tsx, Discover.tsx
App 管理页面和移动子标签;Discover 自己管理请求、选择和显示状态;MarkdownRenderer 是共享的 Markdown 转换工具,而非发现页专属服务。接口 URL 在页面内直接给定,并未在已读实现中经过代理或配置注入。Discover.tsx · markdownRenderer.ts
实现与核心流程
获取与选择版本
组件初始化 releases=[]、activeTag=null、loading=true、error=null。useEffect 内开始请求前再次置 loading=true;非成功 HTTP 状态抛出带状态码的错误,成功则解析 JSON 并保存数组;仅在数组非空时把首条的 tag_name 设为选中标签。无论成功失败最终关闭加载状态。详情不是复制一份对象状态,而是用 releases.find 按 tag_name 从最新状态推导;同名标签将匹配首个。依赖数组为 [t]:翻译函数引用变化时 effect 会重新请求,并非显式的定时刷新。Discover.tsx
1useEffect(() => {
2 const fetchReleases = async () => {
3 try {
4 setLoading(true);
5 const res = await fetch(
6 "https://api.github.com/repos/LYOfficial/OneDocs/releases"
7 );
8 if (!res.ok) throw new Error(`HTTP ${res.status}`);
9 const data: GitHubRelease[] = await res.json();
10 setReleases(data);
11 if (data.length > 0) setActiveTag(data[0].tag_name);
12 } catch (err: any) {
13 setError(err.message || t("discover.fetchError"));
14 } finally {
15 setLoading(false);
16 }
17 };
18 fetchReleases();
19}, [t]);Source: Discover.tsx
桌面与移动呈现
桌面使用 aside.tools-sidebar 与 section.tools-content 同屏展示列表和详情。移动端则依据 mobileSubTab === 'list' 仅显示列表,否则仅显示详情;选择某个版本时更新 activeTag,若提供了移动切换回调则切换到 detail。App 的 handleMobileSubTabChange 对发现页没有 store 同步分支,子标签只在 App 的本地状态中变化;重进发现页且处于移动模式会复位为列表。Discover.tsx · Discover.tsx · App.tsx
1const handleReleaseSelect = (tag: string) => {
2 setActiveTag(tag);
3 if (isMobile && onMobileSubTabChange) {
4 onMobileSubTabChange('detail');
5 }
6};Source: Discover.tsx
MobileNav 提供显式的列表、详情切换,详情可在无选择时显示空状态。加载/错误/无记录的提示仅出现在列表区域;详情区通过 activeRelease 决定显示发布内容还是 discover.emptyTitle、discover.emptyBody。桌面列表按钮仅更新选中标签,移动列表按钮则调用上述处理函数;两者最终共用 activeRelease 的查找结果。MobileNav.tsx · Discover.tsx · Discover.tsx
Sources: App.tsx, Discover.tsx, Discover.tsx
上图最后的渲染步骤只在有 activeRelease 且详情区域被呈现时发生;失败时页面使用错误提示,而不会执行成功分支。Discover.tsx
数据结构与正文处理
GitHubRelease 是页面本地接口:id: number 用作列表项 key,tag_name: string 用于选择与回查,name 用作标题(为空时回退到标签),body 是 Markdown 文本,published_at 转换为浏览器区域设置的短月日期,html_url 虽在类型中声明但当前组件没有使用。请求后不做字段校验、排序或持久化;界面直接按 API 返回的数组顺序排列。Discover.tsx · Discover.tsx · Discover.tsx
renderReleaseBody 遇到空 body 返回空字符串;正常路径调用共享 MarkdownRenderer.render,如果调用抛出异常则先转义 &、<、>,再把换行改为 <br/>。详情通过 dangerouslySetInnerHTML 插入 HTML。须注意共享渲染器自身也捕获顶层异常并返回错误提示与 <pre> 内容,因此外层 catch 不一定能处理渲染器内部的失败。Discover.tsx · Discover.tsx · markdownRenderer.ts
1const renderReleaseBody = (body: string): string => {
2 if (!body) return "";
3 try {
4 return MarkdownRenderer.render(body);
5 } catch {
6 // Fallback to basic rendering if MarkdownRenderer fails
7 return body
8 .replace(/&/g, "&")
9 .replace(/</g, "<")
10 .replace(/>/g, ">")
11 .replace(/\n/g, "<br/>");
12 }
13};Source: Discover.tsx
共享渲染器开启 marked 的 breaks 与 gfm;渲染前提取数学表达式,渲染后以 KaTeX 替换占位符,并将本地图片源转换为 Tauri 可访问地址、给图片追加懒加载属性、处理外部链接。这些能力来自共享工具而不是发现页独立的发布内容规则。markdownRenderer.ts · markdownRenderer.ts · markdownRenderer.ts
Usage Examples
上述请求代码和移动选中处理函数就是应用中实际运行的使用方式。以下进一步展示页面如何把派生的版本详情写入 DOM;它依赖成功加载后得到的 activeRelease,不是独立的外部组件 API。
1{activeRelease ? (
2 <div className="discover-release">
3 <div className="discover-release-header">
4 <h2>{activeRelease.name || activeRelease.tag_name}</h2>
5 <div className="discover-release-meta">
6 <span className="discover-tag-badge">{activeRelease.tag_name}</span>
7 <span>{formatDate(activeRelease.published_at)}</span>
8 </div>
9 </div>
10 <div
11 className="discover-release-body markdown-body"
12 dangerouslySetInnerHTML={{ __html: renderReleaseBody(activeRelease.body) }}
13 />
14 </div>
15) : (
16 <div className="result-empty-card">
17 <h3>{t("discover.emptyTitle")}</h3>
18 <p>{t("discover.emptyBody")}</p>
19 </div>
20)}Source: Discover.tsx
配置与组件契约
| 项 | 类型/值 | 默认或来源 | 作用 |
|---|---|---|---|
isMobile | boolean | 组件默认 false;App 用 window.innerWidth < 768 设置 | 选择单栏或双栏布局,并控制选择版本后是否跳转详情。 |
mobileSubTab | string,界面使用 list/detail | 移动端发现页由 App 初始化为 list | 决定显示列表还是详情;非 list 值走详情分支。 |
onMobileSubTabChange | (subTab: string) => void | 可选;由移动端 App 传入 | 从选择版本动作切换到详情。 |
| Releases URL | 固定字符串 | https://api.github.com/repos/LYOfficial/OneDocs/releases | 浏览器端直接获取记录;当前代码没有可覆盖的配置项。 |
marked | breaks: true、gfm: true | 共享渲染器全局设置 | 控制 Markdown 的换行及 GitHub 风格语法。 |
依据:Discover.tsx · App.tsx · App.tsx · markdownRenderer.ts。这里的表格记录的是组件输入和写死的实现参数,不意味着存在独立的发现页配置文件。
API Reference
Discover: React.FC<DiscoverProps>:渲染发现页。isMobile?: boolean缺省为false;mobileSubTab?: string决定移动端视图;onMobileSubTabChange?: (subTab: string) => void在移动端点击版本时以detail调用。返回 React 元素。组件没有暴露手动重新加载方法。Discover.tsx · Discover.tsxMarkdownRenderer.render(content: string): string:将正文转换为 HTML;空输入直接返回"",顶层异常在方法内部捕获并返回错误 HTML。发现页只调用这个静态方法,不参与其数学公式及图片解析内部逻辑。markdownRenderer.ts · markdownRenderer.ts- 本地辅助函数
formatDate(iso: string):用new Date(iso).toLocaleDateString(undefined, {year: "numeric", month: "short", day: "numeric"})格式化日期;它不是对外导出 API。Discover.tsx
Failure Modes、边界与并发
- 请求失败:
fetch拒绝、非 2xx HTTP 响应、JSON 解析异常都进入catch;内部保存err.message或翻译的兜底信息,列表区域显示翻译后的错误标题及实际错误小字。没有重试按钮或自动退避逻辑。Discover.tsx · Discover.tsx - 空结果/无选中项:列表在
releases.length === 0时显示discover.noReleases;详情在找不到与activeTag匹配的记录时显示空卡片。API 响应是由 TypeScript 断言为GitHubRelease[]的 JSON,当前实现未做运行时结构验证。Discover.tsx · Discover.tsx · Discover.tsx - 重入与状态一致性:effect 无
AbortController和请求序号守卫;如果[t]触发多次请求,代码没有阻止较慢的旧请求覆盖较新的结果。请求前不会清除既有error;因此一次失败之后,即使随后成功更新releases,列表仍因error分支优先于列表分支而呈现错误。它们是从当前分支顺序和状态写入推导的行为,而非额外的恢复机制。Discover.tsx · Discover.tsx - HTML 信任边界:发布正文作为远程文本经过
MarkdownRenderer.render后送入dangerouslySetInnerHTML;阅读到的转换流程包含marked.parse,但没有在所读路径看到明确的 HTML 清洗调用。维护时不能把外层 catch 的转义回退误认为正常路径的安全保证,发布正文的 HTML 安全策略需要单独审查。Discover.tsx · Discover.tsx · markdownRenderer.ts
性能、运维与扩展点
页面挂载时发起一次列表请求;页面由 App 条件渲染,离开发现页时不再挂载组件,因此本地列表状态不是跨页面持久缓存。版本列表使用 map 全量生成按钮,当前组件没有分页、搜索或客户端筛选;releases.find 按标签线性查找选中详情。共享渲染器为图片追加 loading="lazy" 以降低多图片同时加载带来的 UI 压力。App.tsx · Discover.tsx · Discover.tsx · markdownRenderer.ts
如需更换发布源或加入重试,应从 fetchReleases 的固定 URL 与状态分支着手;如需改移动端切换规则,应同时检查 App 的子标签重置/回调和 Discover 的 handleReleaseSelect;如需改正文能力,应留意 MarkdownRenderer 是共享工具,修改可能影响其他调用方。Discover.tsx · App.tsx · Discover.tsx
Related Links
- 归档页入口:与发现页并列的页面入口;归档内容请见归档相关目录页。
- 移动导航:发现页列表/详情子标签定义。
- 共享 Markdown 渲染:发布正文所用的通用渲染能力,内部规则不属于发现页专属配置。