# DESIGN-UIUX-ADOPT:参考竞品(爱盯盘)UI/UX 纪律与令牌的**采纳矩阵** > 输入:`docs/RECON-52ETF.md`(只读侦察,含接口清单与 UI/UX 数值事实)、`docs/DESIGN-TOOL-PAGE-ALIGN.md`(我们自己的详情页规格)、 > `src/client/styles.ts`、`src/client/toolDetailTheme.ts`。 > 本文件是**我们自己的**规格:竞品只提供"数值事实与做法",**不抄它的代码结构、命名体系与配色**。 > 落地批次:v0.87.0(实现线一轮完成:评估 → 规格 → 实现)。 ## 0. 结论摘要 | 处置 | 条数 | 明细 | | --- | --- | --- | | **采纳** | 5 | 玻璃浮层做法(值映射到我们两档主题)、动效四条纪律、1px 间距粒度、自适应卡片 `minmax(148px,1fr)`、密集字号阶梯 | | **改造采纳** | 3 | 字号阶梯(对我们详情页**收敛**,并划出"展示级 ≥15px 不受限")、玻璃令牌(重命名 + 主题双档 + 单一来源)、多形态(只做"同一份数据 + 不重复请求"的**约定**,形态本身进未做项) | | **拒绝** | 4 | ① 接它的数据源(东财 `robots.txt = Disallow: /`)② 硬编码它的涨跌色/暗底 ③ 抄它的窗口体系(浏览器扩展形态)④ 引入 ECharts/D3 等图表库 | ## 1. 采纳矩阵(逐项:竞品做法 → 我们现状 → 处置 → 理由 → 可检查阈值 → 断言文件) ### 1.1 形态与窗口(`RECON-52ETF.md` §4.1、§4.7-1) | 竞品做法 | 我们现状 | 处置 | 理由 | 可检查阈值 | 断言文件 | | --- | --- | --- | --- | --- | --- | | 弹窗 / 800×620 独立窗 / 200×270 迷你窗 / 网页内左下角浮窗,四形态**共用同一份数据与缓存** | 侧栏主面板 + 详情抽屉(同一份 store / `/tradewatcher/*` 路由) | **改造采纳(约定层)**:把"多形态共用一份数据、**不许因为多一个形态就多打一次上游**"写成约定;形态本身(迷你窗/网页内浮窗)**不做**(见 §4-③) | 我们的宿主是 DSH 面板,不是浏览器扩展:没有 `content script` 注入面,也没有 `chrome.windows` 权限;但"同一份数据"这条我们有更好的基础设施(主机半区单例 + LKG 缓存) | 新增形态若出现"独立取数路径"⇒ 判据非空:`共享数据判据`(本次以**文档约定 + 现有断言**承载:`fetchDedupe.test.ts` 已锁"同一标的同一时刻只打一次上游") | `src/host/fetchDedupe.test.ts`(既有) | > **本次不写新代码**:这条的落地形态是"约定 + 引既有断言",因为新增形态的成本远超本批收益(见 §4)。 ### 1.2 玻璃浮层(`RECON-52ETF.md` §4.3) | 竞品做法 | 我们现状 | 处置 | 理由 | 可检查阈值 | 断言文件 | | --- | --- | --- | --- | --- | --- | | 面板底 `rgba(13,17,25,.92)` + `backdrop-filter: blur(20px) saturate(1.5)`;表面白 6%/4%;描边白 9% | 浮层是**不透明**卡片(`--tw-card`),无模糊 | **采纳做法 + 改造值** | 做法(半透明 + 背景模糊 + 饱和度)在"浮层叠在内容上"时明显更清晰;但它的值只适用于**它的**深色网页注入场景 —— 我们有两档主题,浅色档必须给对应值,否则浅色主题上出现一个脏暗块 | ① 令牌两档都有值,且 `GLASS.light.bg ≠ GLASS.dark.bg`;② 深色档**照录**竞品实测值(`rgba(13,17,25,.92)` / `20px` / `1.5`)供核对;③ `styles.ts` 的 `--tw-glass-*` 必须与令牌逐项一致(令牌是唯一来源) | `src/client/uiuxDiscipline.test.ts` ④ | **令牌映射表**(我们的命名 → 竞品事实值 → 我们的值) | 我们的令牌 | 竞品事实(暗色注入场景) | 我们深色档 | 我们浅色档 | 说明 | | --- | --- | --- | --- | --- | | `--tw-glass-bg` | `rgba(13,17,25,.92)` | `rgba(13,17,25,.92)` | `rgba(255,255,255,.86)` | 深色档沿用实测值;浅色档自定(白 86% ≈ 同一"近乎不透明"的观感) | | `--tw-glass-blur` | `20px` | `20px` | `20px` | 模糊半径照录 | | `--tw-glass-saturate` | `1.5` | `1.5` | `1.4` | 浅色档略低(浅底过饱和会发荧光) | | `--tw-glass-border` | `rgba(255,255,255,.09)` | `rgba(255,255,255,.09)` | `rgba(15,23,42,.08)` | 深色档用白 9%;浅色档用深色 8%(同一对比度思路) | | `--tw-ease` | `cubic-bezier(.16,1,.3,1)` | 同 | 同 | 缓动曲线照录(out 曲线) | **应用点**(浮层,不是卡片):图表悬停提示框(`.tw-chart-tip.tw-glass`)、行情悬浮卡(`.tw-pop.tw-glass`),都带 `data-open`。 ### 1.3 动效纪律(`RECON-52ETF.md` §4.3、§4.7-2) | 竞品做法 | 我们现状(改前) | 处置 | 理由 | 可检查阈值 | 断言文件 | | --- | --- | --- | --- | --- | --- | | 常驻动画**只用 `transform`/`opacity`** | 告警行呼吸条 `@keyframes tw-breathe` 动的是 **`background`**(每帧 paint) | **采纳** | 合成层属性不触发 layout/paint,密集列表里差别明显 | 所有 `@keyframes` 里出现的属性 ∈ `{transform, opacity}`;注入动 `background` 的样本 ⇒ 判据非空 | `uiuxDiscipline.test.ts` ⑤ | | 关闭即 `display:none`(不占图层) | 浮层是"条件渲染"(关闭=不在 DOM)✓,但没有成文约定 | **采纳** | 透明层会吃点击、也会占合成层 | `.tw-glass[data-open=false]{display:none}` 必须存在;删掉 ⇒ 判据非空 | 同上 | | 切后台暂停全部动画(`.ap-idle`) | 取数有暂停(`useQuoteEngine(..., visible)`),**动画没有** | **采纳** | 面板藏在后台时不该空跑动画 | 根节点 `data-idle` + `.tw-root[data-idle=true] *{animation-play-state:paused}`;删任一侧 ⇒ 判据非空;纯函数 `panelIdle()` 三态断言 | 同上 + `⑦` | | `prefers-reduced-motion` 降级 | 已有(工具页与告警行) | **采纳(沿用)** | 系统偏好优先 | `@media (prefers-reduced-motion: reduce)` 必须存在,且新动效也在覆盖内 | 同上 | > 口径澄清:**hover 变色**(`transition: background/color`)不在本纪律范围内 —— 它是"状态反馈"而非"常驻动画",且只发生一次、只在指针下。 ### 1.4 密度刻度:字号阶梯与间距(`RECON-52ETF.md` §4.2、§4.5) | 竞品做法 | 我们现状 | 处置 | 理由 | 可检查阈值 | 断言文件 | | --- | --- | --- | --- | --- | --- | | 字号阶梯 `12 / 12.5 / 13 / 13.5 / 14px`(0.5px 递增) | 详情页混用 `10.5 / 11 / 11.5 / 12 / 12.5 / 13`(17 处低于 12px) | **改造采纳(收敛到阶梯)** | 盯盘是密度优先场景,0.5px 阶梯够用;但下限 12px 是**可读性**下限,我们原来的 10.5–11.5px 在侧栏里偏小 ⇒ 全部抬到 12 及以上 | 详情页(`ToolsPage.tsx` / `toolDetailTheme.ts` / `.tw-tool-*` CSS)所有 CSS 字号 ∈ 阶梯 ∪ `{≥15px 展示级}`;注入 13.2px / 10.5px ⇒ 判据非空 | `uiuxDiscipline.test.ts` ① | | 1px 粒度间距(无中间刻度,全靠原子类) | 令牌已是整数(22/18/12/10/16/10),但没有"必须整数"的约束 | **采纳(成文 + 断言)** | 0.5px 间距看不出、还会把对齐算乱 | 所有间距令牌必须落在 1px 刻度(整数 px);注入 11.5 ⇒ 判据非空 | 同上 ② | | 栅格 `repeat(auto-fill, minmax(148px, 1fr))`(最小卡片宽 148px) | KPI 磁贴写死 `repeat(4, minmax(0,1fr))` + 窄屏特例 `repeat(2,…)` | **采纳** | 写死 4 列在窄侧栏里会把磁贴压扁;`auto-fit` 自适应更稳 | `.tw-tool-tiles` 必须 `repeat(auto-fit, minmax(148px,1fr))`;最小宽必须 = 148;注入写死 4 列 / 100px ⇒ 判据非空 | 同上 ③ | **展示级字号**(不受阶梯约束,另有边界):Hero 46 / 摘要数值 34 / 磁贴数值 20 / 卡标题 17 / 说明段 15 —— 判据里 `≥15px` 即视为展示级。 ### 1.5 配色(`RECON-52ETF.md` §4.2、§4.4) | 竞品做法 | 我们现状 | 处置 | 理由 | | --- | --- | --- | --- | | 涨跌 `#f22423` / `#00a000`;块底 `#e3534f` / `#56b779`;暗底 `#262931`;主色 `#3c7eff` | 跟随 DSH 主题令牌(`--tw-up`/`--tw-down`…)+ `prefs.redUp` 决定红涨还是绿涨 | **拒绝(硬编码)/采纳(做法)** | **不得硬编码它的色值**:我们的涨跌色必须跟随主题与用户的 `redUp` 偏好(见 README「红涨绿跌;浅色/深色/跟随系统主题可切」)。可采纳的只有**做法**:色相语义分离(涨/跌两套语义色 + 各自浅底)、块底比文字色更饱和一档 —— 这些我们已有(`--tw-up-bg`/`--tw-down-bg`) | 阈值:任何新增 CSS 里的涨跌色必须写成 `var(--tw-up)` / `var(--tw-down)`(既有断言覆盖:`styles.test.ts` 的主题令牌检查);本批新增的令牌(`--tw-glass-*`)**不含任何涨跌色**。 ### 1.6 图表与交互(`RECON-52ETF.md` §4.6) | 竞品做法 | 我们现状 | 处置 | 理由 | | --- | --- | --- | --- | | ECharts(K 线 + dataZoom)、D3(热力图) | 手写 SVG(K 线/分时/筹码/工具页曲线),零图表依赖 | **拒绝** | 红线:**不新增依赖**;且我们的图已覆盖所需交互(十字线、缩放档、回放) | | 迷你图/火花线(零线虚线 + 末点圆点) | 自选/持仓行已有分时缩略线(均价线 + 缺断不连) | **已具备**(无需改) | — | | 热力图(D3 TreeMap) | 云图页自绘 | **已具备**(无需改) | — | ## 2. 拒绝项(逐条写明理由) | # | 拒绝的项 | 理由 | | --- | --- | --- | | ① | **接它的数据源** | 东财 `robots.txt` 为 `Disallow: /`(`docs/RECON-52ETF.md` §5.1 的实测结论)⇒ 不实弹抓取。我们只能走**公开可用的行情接口**(新浪/腾讯/东财公开行情端点,README 已列),且不改动数据来源与口径 | | ② | **硬编码它的涨跌色与暗底** | 我们的配色必须跟随 DSH 主题与 `prefs.redUp`(用户可切红涨/绿涨)。写死 `#f22423`/`#00a000`/`#262931` 会把用户偏好和主题一起覆盖掉 ⇒ 只允许"映射到我们的令牌"(见 §1.5 与 §1.2 的映射表) | | ③ | **抄它的窗口体系**(弹窗/独立窗/迷你窗/网页内浮窗) | 那些形态依赖**浏览器扩展**能力(`chrome.windows`、`content script` 注入任意页面、`chrome.alarms`)。我们宿主是 DSH 面板,没有这些权限;硬做要么改宿主契约、要么引入扩展形态,都超出"样式与纪律"的本批范围(详见 §4 未做项) | | ④ | **引入 ECharts / D3 等图表库** | 红线:不新增依赖;现有手写 SVG 已覆盖交互需求(且包体、体积、离线可用性都更好) | ## 3. 本批新增的令牌与写面(`styles.ts` 可改,需说明) | 名称 | 值 | 用途 | 是否新增 | | --- | --- | --- | --- | | `--tw-glass-bg` / `--tw-glass-border` / `--tw-glass-blur` / `--tw-glass-saturate` | 见 §1.2 映射表(两档主题各一套) | 玻璃浮层(提示框/悬浮卡) | **新增 4 个** | | `--tw-ease` | `cubic-bezier(.16,1,.3,1)` | 浮层与工具页过渡的统一缓动 | **新增 1 个** | | `.tw-glass` / `.tw-glass[data-open=false]` | — | 玻璃浮层类 + 关闭态 `display:none` | 新增 2 条规则 | | `.tw-root[data-idle=true] *` | `animation-play-state:paused` | 后台暂停动画 | 新增 1 条规则 | | `@keyframes tw-breathe` | `opacity .3 → 1` | 告警行呼吸条(原来动 `background`) | **改写**(合规化) | | `.tw-tool-tiles` | `repeat(auto-fit, minmax(148px,1fr))` | KPI 磁贴自适应 | **改写**(去掉写死 4 列与窄屏特例) | 新增的**主题令牌只有 5 个**(4 玻璃 + 1 缓动),全部两档有值;其余改动是既有类/关键帧的合规化与栅格化。 ## 4. 未做项 1. **迷你窗 / 网页内玻璃浮窗形态**:不做。要做得先解决三件事(且都不在本批写面内):① 宿主是否允许第二个面板实例 / 浮层挂载点;② 状态同步走现有的主机半区 store(**不新增取数路径**,复用 LKG 与 `/tradewatcher/*` 路由);③ 浮层要能被用户"整页开关"。结论写在这里,等宿主契约明确后再评估。 2. **竞品的两套主题色板**($4.2 亮色 / $4.4 官网暗色)**只用做法不用值**:我们的主题令牌已经比它更完整(两档 + 用户偏好),引入它的色板只会造成"两套设计系统打架"。 3. **像素级观感确认**:玻璃浮层在真实面板里的观感(模糊强度、浅色档 86% 透底是否够实)需要用户看一眼;本批给的是令牌 + 断言 + 产物四层证据。 4. **字号阶梯的"全站收敛"**:本批只收敛**详情页**(`ToolsPage.tsx` / `toolDetailTheme.ts` / `.tw-tool-*`)。插件其余界面还混用 9–11.5px(历史演进结果),全站收敛会影响大量既有布局,另开一批做。 ## 5. 合规自查(竞品反面教材 → 我们的文案 vs 实际行为) 自查面:仓库里"能力说明 / 隐私相关"的文案 ↔ 代码实际行为。 | # | 文案(位置) | 实际行为(证据) | 结论 | | --- | --- | --- | --- | | 1 | README:「宿主侧只需 `webServer` + `webRuntime` 两个服务」 | `src/index.ts:26` `export const inject = ['webServer', 'webRuntime']` | ✅ 一致 | | 2 | README:「不依赖任何第三方侧边栏插件」 | 全仓 grep `betterSidebar` = 0 命中;`package.json` `dependencies = {}`(仅 devDeps:esbuild / typescript / @types) | ✅ 一致 | | 3 | README:「数据都在 `~/.dsh/dsh-tradewatcher/*.json`(明文)」+ 数据文件表 | 表里**漏了 3 个真实落盘文件**:`trading-calendar.json`、`global-flow.json`、`market-extras.json`(本批发现 ⇒ **改文案**:已补 3 行)+ 新增断言"表必须覆盖宿主代码里的每个 `*.json` 字面量"。⚠️ **2026-10-15 追记(REVIEW-4-RISK I09)**:这条断言起初只扫**硬编码的 8 个 host 文件**、且只匹配单引号小写字面量 ⇒ 把新文件写到别的文件里仍全绿,属"断言能力被夸大"。现改为**扫描生成**:遍历 `src/host/**/*.ts`(排除测试)取全部 `.json` 字面量,逐条要求被 README 数据文件表覆盖(模式行 `//<周期>/<口径>` 按**有语义**的占位符匹配;未识别占位符一律算未覆盖;排除项只能来自显式的 `NOT_DATA` 表并有理由) | ⚠️ **发现不一致 ⇒ 已修**(文案侧)+ ⚠️ **断言已加强** | | 4 | README:「数量与成本**不落盘**,全部由流水推导」 | `positions.json` 只存 `{id,groupId,secid,name,note?,createdAt}`;数量/成本由 `ledger.json` 核算(`src/host/portfolio.ts`) | ✅ 一致 | | 5 | README:「(金额遮罩)**只影响显示**,取数、告警、agent 工具返回完全不受影响」 | 遮罩/模糊全部在客户端(`.tw-root[data-view]` / `[data-blur=1]`),主机半区无任何遮罩分支 | ✅ 一致 | | 6 | README:「(不透明度/模糊)只作用于本插件面板」 | 不透明度挂在 `.tw-root` 的 `style.opacity`(`src/client/index.tsx`),模糊选择器全部以 `.tw-root[data-blur=1]` 开头 | ✅ 一致 | | 7 | README/ROADMAP:「明确不做账号体系与自建云同步」 | 全仓 grep `login`/`oauth`/`apiKey`/`telemetry`/`analytics` = 0 命中;出网主机只有行情源(新浪/腾讯/东财公开端点) | ✅ 一致 | | 8 | README:「引入 vitest / husky / CI 供应商:明确不做」 | `package.json` devDeps 里没有 vitest/husky;测试用 `node --test` | ✅ 一致 | | 9 | 本批新增:README「动效纪律」段 | 与 `styles.ts` 实际规则一一对应(`transform/opacity`、`display:none`、`data-idle`、`reduced-motion`),断言覆盖 | ✅ 一致(新增文案 + 新增断言) | 自查里唯一的不一致是 #3(数据文件表漏 3 行)⇒ 按"**改文案**"处置,并把它变成一条**可持续的断言**(`uiuxDiscipline.test.ts` ⑥):宿主代码新增落盘文件而 README 没写 ⇒ 测试红。 ## 6. 验证(本批) - 断言:`src/client/uiuxDiscipline.test.ts` 7 条(字号阶梯 / 间距刻度 / 自适应栅格 / 玻璃令牌 / 动效纪律 / 文案一致 / 接线),每条都带**反例注入**。 - **文件级反例注入 16/16 全部被抓住(A 14/14 + B 2/2)**(脚本 `/tmp/uiux-inject.py`,原始输出 `/tmp/uiux-inject-evidence.txt`): | 组 | 注入 | 首条失败断言 | | --- | --- | --- | | A(14/14) | 字号 13.2px | `字号越出阶梯:ToolsPage.tsx 有 13.2px` | | A | 间距 21.5px | `TOOL_SPACE.sectionGap 必须落在 1px 刻度上` | | A | 栅格写死四列 | `卡片栅格不合规` | | A | 浅色档玻璃抄深色 | `light 档缺少或写错令牌:--tw-glass-bg:rgba(255,255,255,.86)` | | A | 深色档 blur 改 6px | `深色档必须记录竞品实测值 blur/saturate 必须 …20px / 1.5` | | A | 关键帧动 background | `@keyframes tw-breathe:动画只允许 transform / opacity(实得「background」)` | | A | 关闭态不写 display:none | `玻璃浮层的关闭态必须是 display:none` | | A | 删后台暂停规则 | `页面切后台必须能暂停动画([data-idle=true] ⇒ animation-play-state:paused)` | | A | 提示框去掉玻璃类 | `图表悬停提示框必须用 tw-glass` | | A | 根节点删 data-idle | `根节点必须挂 data-idle` | | A | 悬浮卡去掉玻璃类 | `行情悬浮卡必须用 tw-glass` | | A | 玻璃规则挪到样式表最前 | `玻璃规则必须在 .tw-pop/.tw-chart-tip 之后`(表层规则被浮层规则覆盖) | | A | reduced-motion 不覆盖玻璃载体 | `reduced-motion 降级必须覆盖动效载体(.tw-glass 或通配符),只写别的组件不算` | | A | 硬编码竞品涨跌色 `#f22423` | `标题样式里不许出现竞品色值 #f22423(涨跌色必须跟主题与 prefs.redUp;暗底必须跟主题)` | | B(2/2) | README 删 global-flow.json 行 | `README 的数据文件表漏了 global-flow.json(文案必须与实际落盘一致)` | | B | README 删 market-extras.json 行 | 同上(该文件) | - 门禁:`npm run check` 两轮全绿(645 × 2,基线 639);`build.mjs` 零改动。