Skip to the content.

界面实现映射

本文是 界面规范 的落地层:规范回答”应该怎样”,本文回答”这件事写在哪个文件的哪一段”。

规范与本文配套。改了界面就同时改这两份;UiContractTest 会校验本文提到的令牌与类名 是否真的在代码里。


1. 文件职责

文件 行数 职责 不要在这里做的事
src/main/resources/static/index.html ~1253 静态骨架:图标 sprite、外壳三列、9 个 section.page、6 个浮层、首帧主题脚本 不要写业务渲染逻辑;不要新增内联样式(现有 20+ 处是历史遗留)
src/main/resources/static/style.css ~2049 全部令牌(浅/深两套)+ 全部组件规则 + 5 个断点 不要出现十六进制颜色字面量(令牌定义处除外)
src/main/resources/static/app.js ~5771 渲染(字符串拼 HTML 写 innerHTML)、交互、轮询、主题与导航状态 不要硬编码颜色(走 themeColor());不要引入框架或构建工具
src/main/resources/static/vendor/cytoscape.min.js — 依赖图渲染库,唯一第三方运行时依赖 不要改;不要升级到需要构建的版本
src/main/resources/static/favicon.svg — 站点图标 —

零依赖、零构建是硬约束:改完刷新即生效,没有编译步骤。


2. 令牌映射

2.1 分组 → CSS 变量 → 主要落点

规范分组 CSS 变量 主要使用选择器
表面 --app-bg --surface --surface-2 --surface-3 --surface-hover .app(径向渐变中间色)、.card、.stat、.callout、.tr:hover
品牌 --primary --primary-600 --primary-700 --primary-50 --primary-100 .sidebar .nav-item.active、.btn-primary、.pill.brand、.tag-chip、:focus-visible
文字 --text --text-2 --text-3 --text-body body、.card-sub、.breadcrumb、.code
线条 --border --border-strong --card-border .tr + .tr、.icon-btn、.card 描边
状态 --success --warning --danger --danger-strong + 各 -bg -line -fg .pill.*、.callout.*、.stat-icon.*、.modal-icon.*
风险色阶 --risk-critical --risk-high --risk-medium --risk-low --risk-info .legend-dot.risk-*;app.js 的 riskColor()
依赖图 --map-edge --map-arrow --map-edge-label --map-label-bg 只被 app.js 的 themeColor() 读取(cytoscape 无法继承 CSS 变量)
代码 diff --code-add-fg/bg --code-del-fg/bg .code .add / .code .del
几何 --r-card --r-tile --r-control --r-pill .card、.stat、.btn / input / select、.pill / .tag-chip / .tabs
阴影 --shadow-card --shadow-soft --shadow-hover --shadow-pop --shadow-drawer --app-shadow .card、.nav-item.active、.card-row:hover、.modal、.drawer、.app
动效 --ease --t-fast --t-base 37 / 43 / 10 处 transition 与 animation
布局 --sidebar-w --sidebar-w-collapsed .app 的两列网格;收起态换 --sidebar-w-collapsed
字体 --font --mono body;.mono、.code、.path-sym、.tr .sym、.ev-*、.rel-version
外壳 --rail-grad-* --sidenav-bg --nav-hover-bg --nav-count-bg --rail-btn-* 等 .sidebar、.nav-item、.sidebar-tool
控件 --focus-ring --focus-ring-soft --select-arrow --input-focus-bg --ring-track 等 表单控件、.search、.ring
浮层 --overlay-bg --toast-* --scrollbar-* .overlay、.toast、::-webkit-scrollbar

2.2 深色块未覆盖的令牌

:root[data-theme="dark"] 定义了 98 个变量,浅色块 109 个。未覆盖的 11 个全部不是颜色:

令牌 类别 为什么不需要覆盖
--r-card --r-tile --r-control --r-pill 几何 圆角与主题无关
--ease --t-fast --t-base 动效 手感与主题无关
--sidebar-w --sidebar-w-collapsed 布局 尺寸与主题无关
--font --mono 字体 字体栈与主题无关

结论:深色块零颜色遗漏,且没有引入浅色不存在的变量(dark \ light = ∅)。 这正是规范 2.1 那条纪律的直接验证:深色只换颜色,不动布局。

2.3 声明了但没有 CSS 引用的令牌

令牌 状态
--app-bg-tint --nav-bg --text-inverse --neutral 真死引用(浅深两套都定义,全仓零引用)
--map-edge --map-arrow --map-edge-label --map-label-bg canvas 专用:CSS 不引用,由 themeColor() 读取

清理死令牌时注意别误删后四个——它们在 app.js 里被按名字读取,grep CSS 找不到使用者。


3. 组件类名契约

每条都是「组件 → 根类名 → 必需子元素 → 状态类 → 生成它的代码」。新增组件必须五列齐全。

引用代码位置时优先写函数名,不写行号。app.js 是单文件 6000 行,行号在每次功能提交后 都会漂移;函数名稳定,且能在编辑器里直接跳转。本文只在 index.html 的骨架节点上保留行号, 因为那些位置几乎不变。

3.1 外壳与导航

组件 根类名 必需子元素 状态类 生成位置
应用外壳 .app .sidebar + .workspace .nav-collapsed index.html:76
侧栏 .sidebar .sidebar-top .sidebar-context .search .nav-scroll .sidebar-foot .app.nav-collapsed 换图标轨道 index.html:82
侧栏品牌 .sidebar-top .sidebar-logo .sidebar-brand .sidebar-toggle 收起态只剩 Logo 与展开按钮 index.html:83
导航项 .nav-item svg + .side-label + .count .active、.count.alert index.html:117
代码源条目 .nav-item(data-source-id) .row-icon + 名称 + .count .active;失败行 .row-icon.danger renderNavSources()
侧栏工具 .sidebar-tools .sidebar-tool .sidebar-avatar 收起态纵向排列 index.html:140
二级导航 .nav-sub .nav-item — renderNavSources()
标签分组头 .nav-tag-head .nav-tag-name .nav-tag-count .open renderNavSources()
图标按钮 .icon-btn svg :disabled index.html:112
搜索框 .search svg + input + kbd :focus-within index.html:120

3.2 页头与容器

组件 根类名 必需子元素 状态类 生成位置
页头 .page-head .breadcrumb + .title-row + .page-sub + .head-right — 每个 section.page 内
标题行 .title-row .title-badge + .page-title + .pill — 同上
页头指标 .head-metric .label + .value(small) — 同上
页头动作 .head-actions .btn — 同上
卡片 .card .card-head + 内容 .card.flush 各页与各 render*
卡片头 .card-head .card-title .card-sub .card-head-actions — 同上
统计块 .stats / .stat .stat-icon + .stat-body(.value .label) .stat-icon.danger/.success/.warning statTile()
栅格 .grid + .grid-2 / .grid-2-even / .grid-3 / .stack — — index.html

3.3 展示元素

组件 根类名 必需子元素 状态类 生成位置
徽章 .pill .dot(可选)+ 文本 .success .warning .danger .neutral .brand .outline pill(text, tone, withDot)
chip .chip / .chips .k(键)+ 值 — chip(text, key)
标签输入 .tag-input .tag-chips + input :focus-within renderTagInput()
标签芯片 .tag-chip 文本 + .tag-chip-x — 同上
标签候选 .tag-cands .tag-cands-label .tag-cand — renderTagInput()
行内标签 .tag-pill — .active renderSourcesPage()
提示块 .callout svg + 文本 .warning .danger .brand 各 render*
空态 .empty strong + 说明 — emptyBlock(title, sub)
代码块 .code .add / .del / .ctx 行 — renderAnalysis()
源码片段 .code-snippet .code-snippet-caption(片段上方的范围说明)+ .code-line(.code-n 行号 + .code-t 文本,引用行 .hl) — codeSnippetHtml() 异步注入 .code-snippet-host
证据 .evidence / .ev-row .ev-type .ev-main .ev-meta — renderReviewEvidence()
路径 .path / .path-step .path-rail(.path-dot .path-line)+ .path-body(.path-sym .path-rel) .path-dot.start renderReviewEvidence()
时间线 .timeline / .tl-item .tl-rail(.tl-dot .tl-line)+ .tl-body(.tl-title .tl-meta) .tl-dot.danger/.success renderOverview()
流程卡 .flow / .flow-card .flow-label .flow-value .flow-note .accent .ok、.flow-value.danger/.success renderImpact()
KV 列表 .kv-list / .kv-row .k + .v .v.mono .v.kv-wrap 设置页、导入结果

3.4 数据展示

组件 根类名 必需子元素 状态类 生成位置
表格 .table / .tr .cell .tr.head、.cell.actions .cell.optional .cell.wrap 各列表渲染
表格模板 .tr-sources .tr-history .tr-changes .tr-commits .tr-ledger — — 见规范 7.10
行卡片 .card-list / .card-row .row-icon .row-main(.row-title .row-desc .row-meta).row-right .selected renderReviewList()
连续列表 .list / .list-row 同上 .selected 各渲染函数
对比程序汇总 .peer-strip .pill(程序 · 契约数 · 影响处数;未比较时标原因) — renderOvPeerStrip() / renderReviewPeerStrip()
影响截断说明 #rv-impacts-note .callout.warning + svg + 文本(文案来自 report.impactsNote) .hidden(未截断时整块隐藏) renderReviewImpactsNote()
分页 .pager .pager-info .pager-pages .pager-gap .hidden(单页时) renderReviewPager()
进度条 .progress > span .thin、span.success/.danger/.warning、.indeterminate renderAnalysisProgress()
环形进度 .ring .ring-track .ring-value .ring-label — ringSvg(percent)
步骤条 .stepper / .step .step-rail(.step-dot .step-line)+ 标题 + 元信息 完成 / 进行中 / 失败 renderAnalysisProgress()
依赖图 .map cytoscape 画布 — renderMap()
图例 .map-legend / .legend-item .legend-dot.risk-* — renderMap()

3.5 浮层与反馈

组件 根类名 必需子元素 状态类 生成位置
遮罩 .overlay .modal 或 .drawer .hidden、.drawer-overlay index.html 底部 6 个
弹窗 .modal .modal-head .modal-body .modal-foot .modal.picker 同上
弹窗图标 .modal-icon svg .brand .danger 同上
抽屉 .drawer .drawer-head .drawer-body — openImpactDrawer() / openChangeDrawer()
轻提示 .toast-root / .toast svg + 文本 .success .error toast(message, tone)
选择器 .picker-list / .picker-row .nm .tags .empty-hint — renderPickerList()
提交选择 .commit-list / .commit-row .commit-sha .commit-subject .commit-meta + input[type="checkbox"] .selectable .selected loadRangePreview() / renderRangePreview()
提交选择动作 .commit-actions #mr-range-all #mr-range-none(.btn.btn-sm) .hidden(列表为空时) index.html 的 #mr-range-actions + bindEvents() 的全选 / 清空监听
对比程序 .peer-picker / .peer-row input + .nm + .tags .peer-empty renderPeers()
更新横幅 .upd-banner .upd-banner-icon .upd-banner-text .upd-banner-actions .hidden .failed renderUpdateNotice()
AI 审核卡 #an-ai-card #an-ai-body .ai-text .ai-foot .ai-actions .hidden(未配置时整卡隐藏) renderAnalysisAi(report)
AI 待验证标注 .ai-verified-note svg + 文本 — renderAnalysisAi()
AI 逐条意见 .ai-comments / .ai-comment .ai-comments-head .ai-comments-title;每条 .ai-comment-head(.ai-comment-sev + .ai-comment-cat + .ai-comment-title + .ai-comment-loc)、.ai-comment-body .ai-comment-suggestion .ai-comment-code .ai-comment-meta .sev-blocker / .sev-critical / .sev-major / .sev-minor / .sev-info(左边界与严重级标签同色);无评论时 .ai-comment-empty aiCommentsHtml() / aiCommentHtml()(由 renderAnalysisAi() 调用)
AI 评论统计 .ai-comment-stats / .ai-comment-note 一行计数 + 小字 notes — aiReviewStatsHtml()
AI 设置卡 #card-ai .ai-status-row .ai-presets #ai-test-result #ai-comments #ai-max-comments #ai-reflection — fillAiForm() / renderAiStatus()
GitLab 私有仓库卡 #card-gitlab .ai-status-row(复用 7.23 配方)+ #gl-status #gl-status-note #gl-token #gl-clear #gl-settings-file — fillGitLabForm() / renderGitLabStatus()
配置导入导出卡 #card-config .checkbox #cfg-secrets + .kv-list #cfg-transfer-hint + #cfg-result(.import-summary / .import-counts / .import-errors)+ #cfg-export #cfg-import #cfg-file .hidden(#cfg-file 常隐);结果区 .callout.danger renderConfigHint() / renderConfigResult()

4. 交互行为 → 代码位置

行为 实现 位置        
主题三态 + 跟随系统 initTheme() / applyTheme(pref, opts) / toggleTheme() initTheme / applyTheme / toggleTheme        
首帧防闪 <head> 内联脚本读 ca.theme 并写 data-theme index.html <head> 内联脚本        
画布取色 themeColor(name, fallback) 读 getComputedStyle themeColor()        
导航收起 initNav() / applyNavCollapsed() / setNavCollapsed() / toggleNav() initNav / applyNavCollapsed / setNavCollapsed / toggleNav        
Ctrl/⌘ + B keydown 监听,输入控件内让位 initNav() 内的 keydown 监听        
Ctrl/⌘ + K 聚焦 #global-search 全局 keydown 监听        
页面切换 go(page) + state.page + ca.page go(page)        
标签 chip 输入 Enter / 逗号 / 退格 / 粘贴拆分 renderTagInput() 651        
标签分组折叠 toggleNavTagGroup(name) toggleNavTagGroup()        
文件夹选择器 openDirPicker() → GET /api/fs/dirs openDirPicker()        
复核决策 setDecision() + ca.review.v1 setDecision()        
证据源码片段 渲染后收集 .code-snippet-host,逐个请求 GET /api/analyses/{id}/code(固定 mode=block,取引用行所在代码块;整块超过 160 行时只取引用行附近),按 分析 mode peer 路径 行 缓存 hydrateCodeSnippets() / loadCodeSnippet() / codeSnippetHtml()
复核页回到契约命中 证据面板选中一条发现后显示证据链;「契约命中」按钮清空 state.selectedImpactId 并恢复默认态 data-clear-impact / renderReviewEvidence()        
影响清单截断说明 report.impactsNote 非空时在发现列表上方显示 .callout.warning,为空则整块 .hidden(换报告时同步清掉上一条);页头「N 项影响」读 report.impactsTotal(缺省回退 counts.total),列表条数仍用 counts.total renderReview() / renderReviewImpactsNote()        
分页 renderReviewPager(),REVIEW_PAGE_SIZE = 50 REVIEW_PAGE_SIZE / renderReviewPager()        
新建分析的模式字段 applyRunMode() 同步标签、占位符与显隐(范围模式隐藏基线输入、给 #mr-scope-row 加 .single、清空日期与勾选) applyRunMode()        
提交选择(分支 / 日期 / 勾选) scheduleRangePreview() 防抖 300ms → loadRangePreview();勾选只写 state.rangePicked(不重渲染整表);筛选后 pruneRangePicked() 收走看不见的勾选 loadRangePreview() / renderRangePreview() / renderRangePickHint() / pruneRangePicked() / clearRangeList()        
进度轮询 活跃时 2s;发现探测 15s pollUpdate 一类的活跃轮询        
图重建 renderMapsFor(page) → destroy + 重建 renderMapsFor() / renderMap()        
弹窗开关 openModal() / closeModal();遮罩点击 + Escape openModal() / closeModal() + 全局 keydown / 遮罩点击        
密度偏好 body.compact + ca.density 启动时的密度恢复        
导入导出 GET /api/sources/export / POST /api/sources/import 导出按钮的 window.location 跳转        
配置导入导出 GET /api/settings/export(?secrets=1 才含密钥)/ POST /api/settings/import;导出走 window.location,导入在浏览器解析 JSON 后 POST,8 MB 上限 exportConfig() / onConfigFileChosen() / importConfigPayload();事件走 data-config-export / data-config-import + #cfg-file 的 change        
配置导入后的刷新 清 state.ai.loaded / formFilled、state.gitlab.loaded / formFilled、state.health → renderSettings() → refreshAll(false) → loadTags() → renderNavSources() → renderSourcesPage() importConfigPayload()        
AI 配置表单 fillAiForm() 填静态骨架(不重渲染,否则会擦掉用户正在输入的内容) renderSettings() 每轮刷新时调用        
GitLab 令牌表单 ensureGitLabLoaded() 只拉一次;fillGitLabForm() 只在首载 / 保存 / 清除后回填,轮询只更新状态行;空输入 = 保持已保存令牌 renderSettings() → renderGitLabSettings()        
GitLab 令牌保存 / 清除 PUT /api/gitlab,body {token} 或 {clearToken: true};清除前确认 saveGitLabSettings() / clearGitLabToken()        
AI 生成 POST /api/analyses/{id}/ai-review → 写回报告 分析页卡片按钮        
AI 逐条意见渲染 aiCommentsHtml(section):有 comments 数组才渲染;空数组显示「本次变更未发现需要评论的问题」;没有该字段的老报告不新增区块 renderAnalysisAi() 内调用        
AI 意见开关 comments / maxComments / reflection 三个字段随 PUT /api/ai 一起保存并回填 aiFormBody() / fillAiForm()        

4.1 localStorage 键清单

键 值 用途
ca.theme system / light / dark 主题三态
ca.nav collapsed / expanded 菜单折叠
ca.page 页面 id 上次打开的页面
ca.density comfortable / compact 信息密度
ca.review.v1 影响 id → 决策 复核结果(仅本地,不回写服务端)
ca.navtags 折叠的标签分组名 侧栏标签分组
ca.update.skip.v1 版本号 忽略的版本

新增键必须:带 ca. 前缀、在本文登记、读取处包 try/catch(隐私模式下 localStorage 会抛)。


5. 数据契约

前端只通过 api(path, options) 访问后端:统一前缀 /api、 非 2xx 抛 Error(优先取响应体 error 字段)、204 返回 null。

5.1 端点

端点 驱动
GET /api/health 设置页服务状态
GET/POST/PUT/DELETE /api/sources 代码源列表与增删改
GET /api/sources/{id}/refs 分支 / 标签下拉
GET /api/sources/{id}/commits 提交选择列表。参数全部可选:ref(分支)、base + target(限定区间;界面不再使用)、limit(默认 30、上限 500,界面用 100)、since / until(yyyy-MM-dd,含两端,按提交时间)。响应 commits count ref base target since until more note:more=true 表示还有更早的匹配提交;坏日期与「起始晚于结束」仍是 HTTP 200 + note,不进错误态
GET /api/sources/export / POST /api/sources/import 导入导出卡片
GET /api/settings/export / POST /api/settings/import 配置导入导出卡片(设置项 + 全部代码源;?secrets=1 才导出密钥)
GET /api/tags 侧栏分组与标签候选
GET /api/analyses / /api/analyses/{id} 历史、报告、进度
GET /api/analyses/{id}/code 证据内联源码:path、line、mode(block = 引用行所在代码块,界面固定传;window = 旧行为,默认)、context(只对 window 生效,默认 3,上限 10)、peerId(省略 = 本程序);返回 lines / startLine / endLine / totalLines / block / blockLabel / blockStart / blockEnd / caption / revision / note;startLine / endLine 是实际返回的行范围(被 160 行上限截断时是展示范围,不是块的起止);只允许读取报告引用过的文件
GET /api/peers 对比程序多选
GET /api/fs/dirs 文件夹选择器
GET /api/update / progress / history / settings 版本卡、进度条、台账、开关
POST /api/update/check / download / apply / restart / rollback 更新动作
GET / PUT /api/ai AI 审核配置(Key 只回显掩码)
GET / PUT /api/gitlab GitLab 私有仓库卡(令牌只回显掩码;PUT 空令牌 = 保持,clearToken = 清除)
POST /api/analyses/{id}/ai-review 历史报告补生成 / 重新生成 AI 审核

5.2 报告字段 → 界面区块

字段 区块
report.summary 总览统计块、页头指标
report.impacts[] 影响审核列表、复核队列
report.peers[] 总览合并卡的程序 pill;影响审核发现列表顶部的程序汇总;未选中发现时右侧的契约命中与对方使用点;peerCommit 供证据源码按分析版本读取。peers[].impacts 不再随 JSON 下发,前端只用 report.impacts
report.impactsTotal / impactsShown / impactsTruncated / impactsNote 影响审核页头「N 项影响」(全量)与发现列表上方的截断说明;impactsNote 为空时整块隐藏。summary 的风险与影响计数仍是全量
report.changes[] 变更清单
report.moduleImpacts[] 模块影响明细表
report.moduleGraph 依赖图
report.commits / commitCount / rangeNote 提交范围卡片
report.diffs 代码差异折叠块
report.blindSpots 分析边界与盲区
report.notes / peersNote 结论说明
report.mode / baseLabel / headLabel / generatedAt 页头副标题与模式徽章
report.ai AI 审核卡片(与证据分区存放,失败不影响其他区块)
report.ai.comments[] 逐条评审意见列表(严重级配色、文件:行、正文、建议、代码引用、证据、定位方式)
report.ai.reviewStats 评论统计一行(审阅 / 跳过 / 保留 / 未定位丢弃 / 过滤 / 反思删除 / 合并 / 重定位 / tokens)。budgetSeconds / budgetExpired 是报告与 GET /api/analyses/{id}/report 里的诊断字段,解释「为什么意见不全」(预算耗尽后剩余调用被跳过);界面展示的是 reviewStats.notes 与 reviewError,不直接渲染这两个键
report.ai.reviewError 逐条评审部分失败的小字提示(摘要照常显示)

5.3 影响条目字段

id riskLevel riskScore confidence confidenceScore depth impactType changeType changedModule changedFile changedLine changedSymbol targetSignature peerModule peerSourceName targetFile targetLine targetSymbol path[] evidence[] reason riskFactors suggestedTests[] verificationStatus

5.3.1 AI 评论字段

id severity category title detail suggestion file startLine endLine side anchor existingCode evidence[] confidence。 行号由后端三层定位得出(anchor = HUNK / FILE / RELOCATED),前端只展示、不改写。

新增字段时:后端加字段 → app.js 渲染 → 规范补文案 → 本文补这一行。四步缺一不可。

5.4 进度字段

progressPercent progressStage status;更新流水线另有 phase(idle / downloading / verifying / applying / restarting / succeeded / failed)、bytes / totalBytes / percent、 automatic、message / error。percent = -1 表示总量未知,走 .indeterminate。

5.5 GitLab 设置字段

tokenSet tokenMasked configuredBy settingsFile。configuredBy 为 file(设置页快照) 或 application.yml;tokenMasked 在空令牌时为空串、令牌长度 ≤ 8 时为 ••••。 任何接口都不返回明文令牌,前端也只把掩码写进 placeholder。


6. 样式加载顺序与首帧

index.html <head>
  1. <meta charset> / <meta viewport>
  2. <title>
  3. <link rel="icon">
  4. preconnect fonts.googleapis / fonts.gstatic
  5. <link> Inter 400;500;550;600;700
  6. <script> 内联:读 ca.theme → 解析 system → 写 data-theme / data-theme-pref
  7. <link rel="stylesheet" href="/style.css">

第 6 步必须在第 7 步之前,否则深色会话会先画一帧白底再变深。这是规范 2.2 的硬约束, UiContractTest 会校验这个顺序。

字体是外链,因此:


7. 变更检查表

改界面的最小闭环是四处同改,加一次验证:

改完自检:按规范第 12 章的验收清单逐条过一遍。如果改动修掉了规范第 13 章里的某条偏差, 记得在同一次提交里删掉那一行。


8. 相关文档

文档 内容
界面规范 视觉与交互的唯一权威规范
架构设计 后端分层与数据流
核心概念 契约、传播、风险与置信度
设计取舍 能力边界