在 self-hosted 数据管理器这类工具里,UI 重构往往被简化成“换个好看的前端皮肤”,但真正落地时会发现,重构牵动的不是按钮颜色或字体间距,而是数据接口、页面路由、缓存策略、部署方式和升级路径的整套绑定关系。标题里提到的 v2 重设计,本质就是把一个“能用就行”的工具型界面,逐步改成“可配置、可维护、可扩展”的产品型界面。本文围绕这条主线展开:先拆解 UI 重构要解决的真实问题,再给出一套可落地的技术选型、工程结构、核心页面实现、运行验证和排错方案,适合正在维护自托管数据类应用的开发者参考。
1. 理解 self-hosted 数据管理器 UI 重构的真实需求
1.1 工具型 UI 与产品型 UI 的差异
self-hosted 数据管理器通常承担日志查看、指标统计、配置管理、数据导入导出这些任务。v1 阶段的 UI 大多以工具型界面为主,特点是:页面能跑、数据能看、接口能调,但信息密度不够合理,页面之间跳转生硬,图表和表格的交互粒度也比较粗。用户在本地部署后,可能连续用几周都不打开设置项,因为界面虽然简陋,却勉强够用。
v2 重设计的价值不是把界面“画得更好看”,而是从交互链路入手,解决几个具体问题:
- 大量数据在同一屏展示时,如何避免卡顿。
- 用户自定义展示维度时,如何让配置持久化。
- 在 Docker、内网、NAS、树莓派等不同部署环境下,前端如何保持一致的构建和更新体验。
- 数据接口已经存在,前端如何用统一的请求层管理错误状态、加载状态和缓存。
产品型 UI 更看重状态一致性。比如用户切换主题后,刷新页面依旧保留选择;用户折叠侧边栏后,路由变化不会导致布局跳动;用户查看一段长时间范围的指标时,图表不是一次性加载所有点,而是按时间窗口聚合。这些细节才是 v2 的核心。
1.2 v2 重构要守住的三条边界
自托管软件和 SaaS 产品不同,它的 UI 重构不能只服务产品视觉效果,还要考虑部署复杂度。重构过程中最容易失控的是三条边界。
第一条是数据准确性。UI 可以重新设计,但后端返回的数据结构如果随意变化,会直接破坏已有用户脚本或 API 调用。v2 的前端应当尽量兼容旧接口字段,新增字段通过扩展方式提供。
第二条是部署简洁性。很多用户通过 Docker Compose 或单二进制运行自托管服务。UI 重构不能引入额外的数据库依赖或强依赖外部 CDN,否则离线环境可能无法加载静态资源。构建产物必须能完整随镜像分发。
第三条是可回滚性。UI 只是前端资源,如果 v2 出现兼容问题,应当能通过环境变量或配置开关切回 v1 资源路径,而不是只能修改代码后重新构建。
1.3 重构前先画数据链路图
动手改界面之前,建议先把现有数据链路画出来。以一个典型数据管理器为例,链路通常是:
浏览器页面 -> 前端请求层(统一封装 fetch 或 axios) -> 后端 API(/api/metrics、/api/records、/api/config) -> 数据存储(SQLite、PostgreSQL、ClickHouse 或其他时序库)画链路图时要标记:哪些接口响应体较大、哪些接口存在慢查询、哪些字段是前端展示后就不再使用、哪些接口需要轮询或 WebSocket 推送。这些结论直接影响 UI 组件的设计。比如某个列表接口一次返回十万行数据,那无论前端表格组件多强大,都不可能流畅渲染,必须先做服务端分页或前端虚拟滚动。
1.4 明确 v2 的展示和交互目标
综合实际项目经验,v2 UI 重设计通常聚焦四类页面:
| 页面类型 | v1 常见问题 | v2 改进方向 |
|---|---|---|
| 仪表盘 | 指标堆叠、无分组 | 卡片化展示,支持自定义排序 |
| 数据列表 | 一次加载全部数据,筛选弱 | 服务端分页、字段筛选、列展示开关 |
| 趋势图表 | 所有数据点一次性渲染 | 按时间聚合、范围选择、自动刷新 |
| 设置页面 | 配置项分散、保存反馈弱 | 分组配置、即时校验、保存状态提示 |
围绕这些目标,再进入技术选型和环境准备。
2. 技术选型和环境准备
2.1 前端技术栈选择
self-hosted 数据管理器的前端界面,可以选用 Vue 3 或 React,两者都能完成 v2 重构。如果团队更熟悉 Vue,推荐 Vue 3 + Vite + TypeScript + Element Plus 或 Naive UI;如果更熟悉 React,推荐 React + Vite + TypeScript + Ant Design 或 Tailwind CSS 组合。重点不是选哪个框架,而是选完之后保持一致。
以 React 方案为例,常用依赖如下:
{ "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.26.0", "@tanstack/react-query": "^5.51.0", "echarts": "^5.5.0", "zustand": "^4.5.0", "dayjs": "^1.11.12" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.0", "typescript": "^5.5.0", "vite": "^5.4.0", "tailwindcss": "^3.4.0", "eslint": "^9.0.0" } }选择这些依赖的理由:
@tanstack/react-query负责服务端状态缓存,避免每次页面切换都重新请求数据。zustand负责 UI 状态,例如侧边栏折叠、主题模式、当前筛选条件。echarts用于趋势图和分布图,图表性能和自定义能力比手写 SVG 更可控。dayjs统一处理时间格式化,避免不同组件各自格式化导致格式不一致。
如果使用 Vue 3 方案,可以把@tanstack/react-query换成vue-query或pinia,组件库换成 Element Plus。核心思路一致。
2.2 开发环境要求
v2 UI 的开发环境不需要太复杂,但版本要提前确认,否则容易出现依赖安装失败或语法兼容问题。一套常见环境如下:
| 项目 | 版本建议 | 说明 |
|---|---|---|
| Node.js | 18 或 20 LTS | 构建工具对 Node 版本有要求 |
| pnpm | 8 以上 | 比 npm 安装更快,磁盘占用更小 |
| Docker | 20.10 以上 | 用于本地模拟镜像部署环境 |
| 后端服务 | 与 v1 保持一致 | 前端开发阶段先对接现有接口 |
Node 版本容易忽略。Vite 5 要求 Node 18 以上,如果开发机还在用 Node 16,安装依赖和启动都会报错。建议先在命令行执行:
node -v npm -v pnpm -v确认版本后再开始初始化项目。
2.3 后端接口契约处理
self-hosted 数据管理器的后端接口通常不是专门为 v2 设计的,前端重构时不要轻易要求后端改接口。推荐做法是在前端增加一层接口网关模块,所有请求都通过该模块转发,字段映射也集中在这一层。
例如旧接口返回指标数据:
{ "id": 1024, "ts": 1725000000000, "cpu_usage": 45.2, "mem": 62.1 }新页面希望使用驼峰字段,同时需要时间格式化。可以写一个映射函数:
interface MetricResponse { id: number; ts: number; cpu_usage: number; mem: number; } export interface MetricDTO { id: number; timestamp: number; cpuUsage: number; memoryUsage: number; } export function mapMetric(raw: MetricResponse): MetricDTO { return { id: raw.id, timestamp: raw.ts, cpuUsage: raw.cpu_usage, memoryUsage: raw.mem }; }这样做的好处是:如果后端后续改字段名,只需改映射函数,页面组件不必大面积调整。
3. 搭建 UI v2 的工程骨架
3.1 目录结构
工程骨架决定了后续页面扩展的效率。自托管数据管理器的前端,建议按模块划分目录:
src ├── api │ ├── client.ts │ ├── metrics.ts │ └── records.ts ├── components │ ├── layout │ │ ├── AppLayout.tsx │ │ ├── Sidebar.tsx │ │ └── Header.tsx │ └── widgets │ ├── MetricCard.tsx │ ├── DataTable.tsx │ └── TrendChart.tsx ├── pages │ ├── dashboard │ │ └── DashboardPage.tsx │ ├── records │ │ └── RecordsPage.tsx │ └── settings │ └── SettingsPage.tsx ├── store │ └── uiStore.ts ├── styles │ ├── global.css │ └── theme.css └── main.tsx这里把api、components、pages、store、styles分开,是为了保证页面只依赖组件和接口层,组件只依赖接口层返回的 DTO,整个数据流是单向的。
3.2 配置 Vite 构建基础
Vite 配置需要考虑三点:本地开发代理、构建输出、自定义端口。
// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }, build: { outDir: 'dist', chunkSizeWarningLimit: 1024 } });本地开发时,浏览器访问http://localhost:5173/api/xxx,请求会被代理到8080端口,这样可以避免开发阶段的跨域问题。构建输出目录设为dist,后面 Docker 镜像可以直接复制该目录到 Nginx 静态目录中。
3.3 样式和主题系统
v2 的 UI 相比 v1 最大的提升之一就是主题统一。主题系统推荐使用 CSS 变量实现,用户在运行时切换主题时,组件不需要重新挂载,只是改变 CSS 变量的值。
在全局样式中定义主题变量:
:root { --color-bg-primary: #f8fafc; --color-bg-card: #ffffff; --color-text-primary: #0f172a; --color-text-secondary: #475569; --color-border: #e2e8f0; --color-accent: #2563eb; --color-success: #16a34a; --color-warning: #d97706; --color-danger: #dc2626; } [data-theme='dark'] { --color-bg-primary: #0f172a; --color-bg-card: #1e293b; --color-text-primary: #f1f5f9; --color-text-secondary: #94a3b8; --color-border: #334155; --color-accent: #3b82f6; }切换主题时,只需修改根节点的>// store/uiStore.ts import { create } from 'zustand'; interface UIState { theme: 'light' | 'dark'; sidebarCollapsed: boolean; toggleTheme: () => void; toggleSidebar: () => void; } export const useUIStore = create<UIState>((set) => ({ theme: 'light', sidebarCollapsed: false, toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })), toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })) }));
同时需要在应用启动时读取本地存储中的主题值:
const storedTheme = localStorage.getItem('ui-theme') || 'light'; document.documentElement.setAttribute('data-theme', storedTheme);这个方案的优势是改动成本低,图表组件、第三方组件、自定义组件全部能通过变量联动。
3.4 路由和菜单设计
自托管数据管理器的页面数量通常不多,但要注意菜单和路由的联动。建议在路由配置中直接生成菜单项,避免菜单和路由各自维护一份数据。
// router.tsx export const menuRoutes = [ { path: '/dashboard', label: '仪表盘', icon: 'DashboardOutlined', element: <DashboardPage /> }, { path: '/records', label: '数据记录', icon: 'TableOutlined', element: <RecordsPage /> }, { path: '/settings', label: '设置', icon: 'SettingOutlined', element: <SettingsPage /> } ];侧边栏组件根据menuRoutes渲染菜单,路由组件也根据它注册页面。这样新增页面时只改一份配置,不容易出现“菜单能看到但路由不存在”的问题。
4. 用核心页面验证 v2 是否真正可用
4.1 指标卡片组件
仪表盘页面通常包含多个指标卡,展示 CPU、内存、磁盘、在线设备等数据。v1 常见问题是每个指标卡单独请求接口,页面加载时需要等待多次请求完成。v2 可以改为聚合接口一次返回,或者使用数据请求层统一管理并发。
指标卡组件:
// components/widgets/MetricCard.tsx interface MetricCardProps { title: string; value: string; unit?: string; trend?: number; status?: 'success' | 'warning' | 'danger'; } export function MetricCard({ title, value, unit, trend, status }: MetricCardProps) { return ( <div className="rounded-lg border bg-card p-4 shadow-sm"> <div className="text-sm text-muted-foreground">{title}</div> <div className="mt-2 text-2xl font-semibold"> {value} {unit && <span className="ml-1 text-sm text-muted-foreground">{unit}</span>} </div> {trend !== undefined && ( <div className={`mt-2 text-sm ${trend >= 0 ? 'text-success' : 'text-danger'}`}> {trend >= 0 ? '+' : ''} {trend}% </div> )} </div> ); }这里的status字段用于后续扩展颜色状态,例如超过阈值显示红色。数据请求由父组件统一发起,再把结果传给卡片。
4.2 大数据量列表的表格组件
数据管理器中也许重要的页面是“数据记录”。v1 界面容易犯的错误是直接渲染全量数据。v2 推荐使用服务端分页加列配置的方式。列配置可以让用户选择显示哪些列,选择结果保存到本地存储。
// pages/records/RecordsPage.tsx import { useQuery } from '@tanstack/react-query'; import { fetchRecords } from '@/api/records'; import { DataTable } from '@/components/widgets/DataTable'; export function RecordsPage() { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [columns, setColumns] = useState(['id', 'timestamp', 'level', 'message']); const { data, isLoading } = useQuery({ queryKey: ['records', page, pageSize], queryFn: () => fetchRecords({ page, pageSize }) }); return ( <div className="p-6"> <DataTable columns={columns} data={data?.items ?? []} loading={isLoading} pagination={{ current: page, pageSize, total: data?.total ?? 0, onChange: (nextPage, nextSize) => { setPage(nextPage); setPageSize(nextSize); } }} /> </div> ); }这里可以加入列配置按钮,用户勾选显示列后,组件把columns写入localStorage,下次打开页面时读取。这样做对数据量大、字段多的场景非常实用。
4.3 图表组件的时间聚合
趋势图在 v2 中建议做两件事:时间范围选择和自动聚合。后端返回的数据点可能很多,如果前端直接把几千个点渲染到 ECharts 中,会浪费性能。可以先按时间范围粗化数据点。
以小时为单位展示一天指标为例:
import * as echarts from 'echarts'; export function renderTrendChart(container: HTMLElement, points: Array<{ time: number; value: number }>) { const chart = echarts.init(container); chart.setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'time' }, yAxis: { type: 'value' }, series: [ { type: 'line', data: points.map((p) => [p.time, p.value]), smooth: true, showSymbol: false } ] }); return chart; }使用 ECharts 时要注意:组件卸载时调用chart.dispose(),避免内存泄漏;窗口大小变化时调用chart.resize()。可以在自定义 Hook 中封装这两件事。
useEffect(() => { const chart = renderTrendChart(containerRef.current!, points); const handleResize = () => chart.resize(); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); chart.dispose(); }; }, [points]);4.4 设置页面的即时校验
设置页面的 UI 容易做成“一堆表单加一个保存按钮”。v2 可以加入即时校验,例如用户输入监听端口时,如果端口范围超出 1 到 65535,立即提示,而不是等到保存时才报错。校验逻辑可以直接放在表单状态层中。
const [port, setPort] = useState(8080); const [error, setError] = useState(''); function validatePort(value: number) { if (!Number.isInteger(value) || value < 1 || value > 65535) { setError('端口必须是 1 到 65535 之间的整数'); return false; } setError(''); return true; }保存请求发出后,按钮进入 loading 状态,防止用户重复点击。如果保存失败,显示后端返回的具体错误信息,而不是笼统的“保存失败”。
5. 构建、运行和验证 UI v2
5.1 本地开发启动
进入项目目录后执行:
pnpm install pnpm dev启动成功后终端会输出本地地址,例如http://localhost:5173。此时检查页面是否正常加载、接口是否通过代理打通、控制台是否有报错。
注意:不要只验证页面能打开,还要验证接口数据、空数据状态、加载状态、错误状态和主题切换是否都符合预期。
5.2 生产构建验证
生产构建命令:
pnpm build pnpm previewpreview会启动一个本地静态服务器,用来验证构建产物。此时重点检查三件事:
- 静态资源路径是否使用了相对路径,否则部署到子路径时可能白屏。
- CSS 和 JS 文件是否分包合理,首屏文件体积是否过大。
- 路由在刷新后是否正常,如果出现 404,需要在 Nginx 中配置 fallback。
如果需要在子路径部署,可以在vite.config.ts中设置base:
export default defineConfig({ base: '/manager/' });5.3 接口和性能验证
验证接口时使用 curl 命令检查返回结构和状态码:
curl -i http://localhost:8080/api/metrics curl -i "http://localhost:8080/api/records?page=1&pageSize=20"性能验证可以使用 Chrome DevTools 的 Lighthouse 面板生成报告,关注几个关键指标:
| 指标 | 期望范围 |
|---|---|
| First Contentful Paint | 小于 1.5 秒 |
| Largest Contentful Paint | 小于 2.5 秒 |
| Total Bundle Size | 不应该超过 1 MB(压缩前) |
| 长列表滚动帧率 | 稳定在 55 FPS 以上 |
如果首屏资源过大,可以分拆路由级代码块。React 中可以使用React.lazy实现按需加载:
const DashboardPage = React.lazy(() => import('@/pages/dashboard/DashboardPage'));路由配置时包裹Suspense,避免页面切换时白屏。
5.4 Docker 镜像构建和启动验证
self-hosted 工具最终通常会以镜像方式分发。前端构建产物放入 Nginx 镜像即可:
FROM node:20-alpine AS build WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build FROM nginx:1.27-alpine COPY nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80对应的 Nginx 配置:
server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }构建镜像:
docker build -t>const metricRaw = await response.json(); return { ...metricRaw, timestamp: metricRaw.ts || metricRaw.time || metricRaw.timestamp };这样即使后端返回多种字段命名,前端也能统一消费。
6.2 灰度发布和版本开关
如果用户群体对稳定要求较高,v2 可以先通过环境变量控制资源路径,而不是直接替换 v1 的静态资源。Nginx 中可以根据请求头或 Cookie 决定返回 v1 还是 v2:
location / { if ($cookie_ui_version = "v2") { try_files $uri $uri/ /index-v2.html; } try_files $uri $uri/ /index.html; }这种方法在自托管工具中不算复杂,却能降低一次性替换带来的风险。
6.3 回滚方案
回滚不只是“重新部署老版本镜像”。要提前准备好三样东西:
- 旧版本镜像的 tag 必须保留,不能每次构建都用
latest覆盖。 - 配置文件中与前端相关的版本号字段要保留,例如
ui_version: v1。 - 如果 v2 修改了 localStorage 中的字段名,回滚到 v1 时要处理旧字段残留。
推荐在发布文档中维护一份回滚检查单,至少包含以下步骤:
1. 确认当前版本号和发布镜像 tag。 2. 备份配置文件。 3. 停止新容器,启动旧容器。 4. 验证登录、列表、图表页面。 5. 如果配置加密,确认密钥没有变化。注意:回滚时最容易忽略的是数据表结构变更。如果 v2 版本同步更新了后端表结构,前端回滚可能不兼容新表结构。发布前必须确认 UI v2 是否依赖后端表变更。
7. 常见问题和排查链路
7.1 部署后页面白屏
现象:浏览器打开地址,只有空白页,控制台报错或静态资源 404。
排查顺序:
- 打开控制台 Network 面板,确认
index.html是否加载。 - 检查 CSS、JS 文件路径是否为绝对路径,若部署在子路径下导致 404,调整
base配置。 - 检查 Nginx 的
try_files是否把前端路由回退到index.html。 - 检查接口是否跨域,若浏览器报 CORS 错误,在 Nginx 中配置代理。
解决:如果使用 Docker 和子路径部署,确认vite.config.ts中base与 Nginx 路径一致。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 白屏 | 静态资源路径错误 | Network 面板检查 404 | 配置 base 路径 |
| 白屏 | 路由刷新 404 | 访问子页面刷新 | 配置 try_files fallback |
| 白屏 | API 请求失败导致页面崩溃 | 检查接口响应 | 统一请求层加错误处理 |
7.2 数据列表滚动卡顿
现象:列表滚动时明显掉帧,甚至浏览器无响应。
原因通常有两个:一是前端渲染了过多 DOM 节点,二是每个单元格渲染了高开销组件。可以先通过 Chrome DevTools 的 Performance 面板确认渲染耗时。
解决:
- 接口层做分页或后端聚合。
- 前端使用虚拟滚动组件。
- 表格单元格只渲染基础文本,不渲染复杂图标的频繁更新。
如果数据量达到十万行,优先考虑后端分页,而不是前端一次性加载。
7.3 主题切换后图表颜色不变化
现象:页面背景和卡片颜色随主题切换,但图表仍是旧颜色。
原因是 ECharts 实例不会自动感知 CSS 变量的变化。需要在主题切换后重新调用chart.setOption,或者直接销毁重建图表实例。
简单方案:
function applyChartTheme(chart: echarts.ECharts, theme: 'light' | 'dark') { const textColor = theme === 'dark' ? '#f1f5f9' : '#0f172a'; chart.setOption({ textStyle: { color: textColor }, xAxis: { axisLabel: { color: textColor } }, yAxis: { axisLabel: { color: textColor } } }); }在主题变化的副作用中调用该方法。
7.4 刷新页面后主题和布局丢失
现象:选择深色主题或折叠侧边栏后,刷新恢复默认值。
原因:主题和布局状态只存在于前端内存中,没有持久化到本地存储。
解决:在zustand状态更新时同步写入localStorage,初始化时读取。例如:
export const useUIStore = create<UIState>((set, get) => ({ theme: (localStorage.getItem('ui-theme') as 'light' | 'dark') || 'light', sidebarCollapsed: localStorage.getItem('ui-sidebar') === '1', toggleTheme: () => { const next = get().theme === 'light' ? 'dark' : 'light'; localStorage.setItem('ui-theme', next); set({ theme: next }); } }));这样刷新后状态可以恢复。
8. 最佳实践和后续扩展
8.1 v2 发布前检查清单
给出一份可执行的发布前检查清单,比临时思考更有价值:
1. 生产构建通过,预览模式验证无报错。 2. 接口使用相对路径或正确 base,子路径部署验证通过。 3. 主题切换后页面、表格、图表颜色一致。 4. 数据列表在最大数据量下滚动流畅。 5. 刷新页面后主题、布局、列配置保持。 6. 空数据、加载中、请求失败三种状态都有 UI 反馈。 7. Docker 镜像构建成功,Nginx 代理 API 正常。 8. 旧版本接口字段映射保持,无后端强依赖变更。 9. 打包产物体积在可接受范围,路由级代码分割生效。 10. 保留旧镜像 tag,回滚方案已确认。8.2 生产环境额外考虑
生产环境部署 v2 时,还要考虑资源占用和权限控制。自托管数据管理器可能运行在低配 NAS 或树莓派上,前端资源如果过大,会占用不必要的内存。建议构建后检查产物大小,如果超过 2 MB,优先检查是否误引入了大型依赖。
如果数据管理器涉及多用户,UI 还必须处理权限菜单。后端返回当前用户可访问的路由或按钮权限,前端动态渲染菜单,避免把没有权限的入口展示给普通用户。权限编码建议使用字符串常量而不是数字,例如dashboard:view、settings:edit。
8.3 后端配合优化的方向
UI v2 跑通后,后端可以做三件低成本但提升明显的事:
- 给列表接口增加
fields参数,前端只请求当前显示的列字段,减少响应体体积。 - 给指标接口增加
aggregate参数,前端按小时、天、周请求聚合数据,而不是全量数据。 - 增加统一的请求追踪 ID,前端请求头携带,后端日志输出,便于排查 UI 页面上的数据异常。
这些改动不改变老接口的默认行为,前端按需传参,是兼容性较好的优化路径。
8.4 后续版本可以扩展的方向
v2 的 UI 重构稳定后,下一步可以考虑:
- 多语言的国际化支持,把页面文案抽离到语言包。
- 自定义仪表盘布局,允许用户拖拽卡片位置。
- WebSocket 实时数据推送,替代高频轮询。
- 移动端适配,自托管管理器的手机查看体验也值得优化。
- 内置前端错误上报,用户遇到问题时能直接导出诊断信息。
这些扩展方向不是一上来就全部实现,而是根据实际使用反馈逐步推进。UI 重构本身不是终点,数据能看清、操作能顺畅、升级能回退,才是 v2 真正立住的标准。