Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2f65842e6 |
@@ -1,5 +1,11 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-21 — 导出弹窗:没有英文时顶部提示一次,选中态用主题色
|
||||
|
||||
- 英文版不可用时,打开导出弹窗在顶部提示一次,弹窗里不再留那行说明。英文可用时,「建议选 English」仍在语言选项下面。
|
||||
- 「整篇报告 / 自选章节」的描边和圆点、勾选框、弹窗里的中英切换,选中时用主题珊瑚色,底色保持中性。深色模式下不再用深红褐色块。
|
||||
- Skill 版本不变(BUG-1408)。
|
||||
|
||||
## 2026-10-21 — 合盘时间线写出起止,删掉的模型改用默认模型
|
||||
|
||||
- 关系时间线每段标题写出起止年月。第一段从本月开始时,起点写「现在」或 Now。
|
||||
|
||||
@@ -37016,3 +37016,19 @@
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/synastry-staging-fix-20261021`。
|
||||
|
||||
## BUG-1408 | 导出弹窗把「没有英文」写成常驻说明,选中态没有主题色
|
||||
|
||||
- 状态:resolved(待 staging 真机复测)
|
||||
- 首次发现:2026-10-21
|
||||
- 最近更新:2026-10-21
|
||||
- 影响面:个人报告导出弹窗
|
||||
- 用户现象:英文版不可用时,弹窗里多一行灰字占位置。整篇 / 自选、勾选框和弹窗里的中英切换,选中和没选中都偏灰,不好分。
|
||||
- 触发条件:打开导出弹窗。英文版状态是没生成成功,或报告生成于英文版上线前。选中「整篇报告」「自选章节」、勾选章节,或在弹窗里切换中文 / English。
|
||||
- 根因:2026-09-30 把「为什么没有英文」写成了面板里的常驻说明。BUG-1078 为了躲开深色模式下 `--color-action-soft` 的深红褐色块,把选中态改成了中性灰加黑字。
|
||||
- 修复:英文不可用时,打开弹窗在 `showModal()` 之后用 `notifyInfo` 提示一次(固定 id `report-export-english`),弹窗内不再写这两句。选中描边、圆点、勾选框用 `--color-action-strong`,语言分段的选中文字用 `--color-action`,底色保持中性。英文可用时「建议选 English」仍留在语言选项下面。页面顶栏的中英切换没改。
|
||||
- 验证:`frontend/tests/report-export-drawer.test.tsx`、`frontend/tests/report-english-edition.test.tsx`、`frontend/tests/report-buttons-scope-20260928.test.ts`。
|
||||
- 防复发:上述测试锁定「每次打开只提示一次、弹窗内没有这两句」以及「选中规则不得出现 `--color-action-soft` / `--color-action-fill`」。
|
||||
- 相关记录:BUG-1078、BUG-1079、BUG-1121。issue #90。
|
||||
- 复发自:无。本条推翻 DESIGN.md「Selected states use `--color-canvas-muted` + `--color-ink`」这一句;BUG-1078 的主语(导出面板选中态不得用 `--color-action-soft` 深色块)保留。
|
||||
- 修复版本:分支 `codex/report-export-dialog-polish-20261021`。
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# 进度:导出弹窗提示与选中态(2026-10-21)
|
||||
|
||||
英文不可用时,打开导出弹窗会在顶部提示一次,弹窗里不再留那两句。选中态的描边、圆点和勾选框改用主题色,底色保持中性。英文可用时,「建议选 English」仍在语言选项下面。页面顶栏的中英切换没有改。
|
||||
|
||||
## 基线
|
||||
|
||||
- 开工时 `origin/staging` = `9812f31737b9e9fdecc75952b1bac1558333c685`。任务书里写的 `bd3ce0e7b` 之后,staging 上又有纯文档提交。
|
||||
- 分支 `codex/report-export-dialog-polish-20261021`,工作树 `.worktrees/report-export-dialog-polish-20261021`。
|
||||
- 另开一份干净基线工作树没有成功:路径过长,写不下 `sealed_holdout_rerun_cross_midnight_2026_09_20.final.freeze.json`。所以没有单独的基线测试数字。本分支上没改过的用例是通过的。
|
||||
|
||||
## 断言三栏
|
||||
|
||||
| 文件 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `frontend/tests/report-buttons-scope-20260928.test.ts` | 范围卡和语言分段 `doesNotMatch(/--color-action-soft\|--color-action\b/)`,并且 `match(/--color-ink\|--color-canvas/)` | 四条选中规则不得含 `--color-action-soft` / `--color-action-fill`;描边、圆点、勾选框和语言分段用 `--color-action-strong`(文字用 `--color-action`);底色只许 `transparent` / `--color-canvas*`,勾选框小方块的底可以是 `--color-action-strong` | issue #90 推翻 DESIGN 那一句。旧正则会把 `--color-action-strong` 判成违规。BUG-1078「不得用 soft / fill 深色块」仍在 |
|
||||
| `frontend/tests/report-english-edition.test.tsx` | legacy 时弹窗正文含「重新生成即可获得中英两版」 | 正文不含该句;`toast.info` 一次,文案为该句,id 为 `report-export-english`;仍没有 English 选项;英文可用时仍有「建议选 English」 | 说明改成打开时的顶部浮层 |
|
||||
|
||||
## BUG 编号
|
||||
|
||||
这份 `docs/BUG_HISTORY.md` 在 staging 上的最大号是 BUG-1403。#85 未合入分支已经写到 BUG-1407,任务书把 1404 起留给 #85。#86 未合入分支另写了一个内容不同的 BUG-1404。本单用 **BUG-1408**,追加在文件末尾。文件约第 18099 行还有一份重复的 Bug History,按任务书没有去重。
|
||||
|
||||
## 检查
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| tsc | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 |
|
||||
| lint | 在 `frontend` 下 `npm run lint` | 0 error,122 warning。改过的四个文件单独 lint 没有输出 |
|
||||
| 导出弹窗 | `node node_modules/tsx/dist/cli.mjs --test tests/report-export-drawer.test.tsx` | 8 pass,0 fail |
|
||||
| 英文版 | 同上 `tests/report-english-edition.test.tsx` | 10 pass,0 fail |
|
||||
| 选中态 | 同上 `tests/report-buttons-scope-20260928.test.ts` | 6 pass,0 fail |
|
||||
| 类名 | 同上 `tests/class-name-definition-contract.test.ts` | 3 pass,0 fail |
|
||||
| 深色 | 同上 `tests/dark-theme-contract.test.ts` | 5 pass,0 fail |
|
||||
| 色板 | 同上 `tests/design-token-contract.test.ts` | 4 pass,1 fail |
|
||||
| 隐私 | `.worktrees/agent-loop/.venv` 的 `python -m pytest tests/test_repo_privacy_markers.py -q` | 62 passed |
|
||||
|
||||
六份前端测试是一次一起跑的:37 条里 36 过、1 失败、0 cancelled。失败的那条是 `design-token-contract.test.ts`「every color token used as a Tailwind utility is exposed through @theme」。`sourceFiles` 把 `URL.pathname` 再交给 `path.join`,Windows 上拼成 `G:\G:\Ferti\...`,打开 `chart/page.tsx` 时 ENOENT。这条测试和 `chart/page.tsx` 本单都没改,token 值也没改。Linux 上由 Claude 复验。
|
||||
|
||||
全量 `npm test` 和 `next build` 按分工没有跑。没有动数据库,`test:db` 没跑。
|
||||
|
||||
## 偏离
|
||||
|
||||
- 真机清单只加了一句核对说明,九步没有改。
|
||||
- 工作树的 `frontend/node_modules` 是指到主检出的目录联接,没有单独安装。
|
||||
- 本工作树没有 `.venv`,隐私测试用的是 `.worktrees/agent-loop/.venv`。
|
||||
|
||||
## 没做
|
||||
|
||||
浅色、深色和手机宽度的真机走查。清单在 `docs/testing/report-export-dialog-20261021.md`。
|
||||
@@ -513,5 +513,5 @@
|
||||
| `TASK-synastry-report-chat-fix-20261020.md` | `PROGRESS-synastry-report-chat-20261018.md` | 大运核对要认报告里的「小运」和英文 mahadasha / antardasha;预填文案只留一句 | 已验收 | `549e37b99` |
|
||||
| `TASK-other-subject-birth-time-edit-20261021.md` | — | 其他人的出生时间可以修改(去掉 `chart_profiles_guard_birth` 的填报时间不可改;推翻 people-archive P1 §验收 2 该条;active 锁不变) | 排队(等 #86 合入) | `codex/other-subject-birth-time-edit-20261021`;动表;不进循环 |
|
||||
| `TASK-chart-ascendant-slash-20261021.md` | — | #87 上升状态格「不适用」改「/」+ 悬停 / 点按说明;报告上升行同改并在表下加说明(推翻 10-15 口径) | 待领取(等 #86 合入) | `codex/chart-ascendant-slash-20261021` |
|
||||
| `TASK-report-export-dialog-polish-20261021.md` | — | #90 导出弹窗:英文不可用说明改顶部浮层(每次打开一次);选中态描边 / 圆点 / 勾选框用主题色、底色中性(推翻 DESIGN「Selected states use canvas-muted + ink」,BUG-1078 主语保留) | 待领取(可与 #89 并行) | `codex/report-export-dialog-polish-20261021` |
|
||||
| `TASK-report-export-dialog-polish-20261021.md` | `PROGRESS-report-export-dialog-polish-20261021.md` | #90 导出弹窗:英文不可用说明改顶部浮层(每次打开一次);选中态描边 / 圆点 / 勾选框用主题色、底色中性(推翻 DESIGN「Selected states use canvas-muted + ink」,BUG-1078 主语保留) | 待验收 | `codex/report-export-dialog-polish-20261021` |
|
||||
| `TASK-report-english-han-leak-20261021.md` | `PROGRESS-report-english-han-leak-20261021.md` | #89 个人报告英文版约 4 成整份丢弃:漏译汉字源头补英文孪生字段、修吃空格;200 张固定种子回归 + 快速门 smoke(复发自 BUG-1272);中文逐字节不变 | 待领取(最先做;#91/#92 在后) | `codex/report-english-han-leak-20261021` |
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
环境:`https://staging.jyotisha.chat`,用受控测试账号。需要三类报告各一份:**有英文版**的新报告、**英文版没生成成功**的报告(如有)、**英文版上线前**生成的旧报告。没有某一类就写「无样本」,不算通过。每步在**浅色**、**深色**各走一遍;第 8 步用手机(或桌面浏览器窄到约 390px)。
|
||||
|
||||
2026-10-21 执行核对:步骤没有改。浮层固定 id 是 `report-export-english`,同一次打开只出一次。选中描边、圆点和勾选框用主题色,语言分段的选中文字也用主题色,底色保持中性。
|
||||
|
||||
| # | 操作 | 期望 | 浅色 | 深色 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 打开旧报告(英文版上线前),点「导出」 | 屏幕**顶部中间**出浮层「这份报告生成于英文版上线前,重新生成即可获得中英两版。」,约 3 秒自动消失;浮层在弹窗之上、没被遮罩盖住;弹窗里**没有**这句灰字,也没有中文 / English 选项 | | |
|
||||
|
||||
+1
-1
@@ -661,7 +661,7 @@ Action results and errors never take page layout. They are toasts from `src/lib/
|
||||
### Personal report reader
|
||||
|
||||
- **Buttons on the report surfaces:** the BUG-1078 report-only override (2026-09-28) was replaced the same day by the site-wide solid-button pair (BUG-1079, see Button); report buttons look as before. Outline buttons there (the header 「生成报告」 while submitting) now fade like outline buttons elsewhere instead of the report-only grey. The selected export preset is `--color-canvas-muted` + `--color-ink` + a `--color-ink-secondary` edge, not the `--color-action-soft` fill (a red-brown block in dark mode).
|
||||
- **导出报告 sheet (redesigned 2026-09-30, product: 「分块导出太丑」):** native modal `<dialog>`, bottom sheet below 860px (up to 85dvh) and a centred `min(520px, 92vw)` panel at ≥860px, app colours only. Top to bottom: title 「导出报告」 with a × (读屏名「关闭导出」); when the report has an English edition, a two-way segment 中文 / English (starts on the edition being read, does not switch the page) with one caption 「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」, otherwise one caption saying why there is no English; two radio cards 「整篇报告」 (subline counts 章正文 / 张分盘 / 张核对表) and 「自选章节」; only in 自选, a plain checklist grouped 正文 / 分盘 / 核对表, each group one tri-state checkbox (✓ / − / empty) plus a chevron, 正文 open and the other groups folded. Rows are borderless 44px lines, no per-block character counts. The file size appears once, on the footer button: 「导出整篇 · 约 195 KB」 / 「导出 6 项 · 约 80 KB」 / disabled 「先勾选要导出的部分」. Selected states use `--color-canvas-muted` + `--color-ink`, never `--color-action-soft` (BUG-1078). Escape, backdrop or × dismisses; focus returns without scrolling. No spinner, clipboard or extra fetch.
|
||||
- **导出报告 sheet (redesigned 2026-09-30, product: 「分块导出太丑」):** native modal `<dialog>`, bottom sheet below 860px (up to 85dvh) and a centred `min(520px, 92vw)` panel at ≥860px, app colours only. Top to bottom: title 「导出报告」 with a × (读屏名「关闭导出」); when the report has an English edition, a two-way segment 中文 / English (starts on the edition being read, does not switch the page) with one caption 「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」, otherwise no caption: opening the sheet raises one info toast (id `report-export-english`) saying why; two radio cards 「整篇报告」 (subline counts 章正文 / 张分盘 / 张核对表) and 「自选章节」; only in 自选, a plain checklist grouped 正文 / 分盘 / 核对表, each group one tri-state checkbox (✓ / − / empty) plus a chevron, 正文 open and the other groups folded. Rows are borderless 44px lines, no per-block character counts. The file size appears once, on the footer button: 「导出整篇 · 约 195 KB」 / 「导出 6 项 · 约 80 KB」 / disabled 「先勾选要导出的部分」. Selected states (issue #90, 2026-10-21, overrides the 2026-09-30 neutral rule): the scope card's border and radio dot, checked / mixed boxes, and the selected language segment use `--color-action-strong` (text `--color-action`); backgrounds stay neutral; never `--color-action-soft` or `--color-action-fill` (BUG-1078: no dark red-brown block in dark mode). Escape, backdrop or × dismisses; focus returns without scrolling. No spinner, clipboard or extra fetch.
|
||||
- **Export selection:** 整篇 = every block of the chosen edition; 自选 starts with all 正文 chapters ticked. Switching the sheet's language resets the checklist to that edition's chapters (block ids differ per edition). Presets 常用 / 全部分盘 / 仅核对表 and 全选 were retired with the redesign.
|
||||
- **Export content:** full body alone remains byte-identical to the retired ordinary Markdown export. Selected chart/table blocks append new content; selecting all groups is not byte-identical to body-only. D13 requires safe SVG in each chart, reusing the projected engine SVG or the existing hook-free Vedic drawing; offline fallback uses readable monochrome glyphs/legend and presentation attributes. No chart JSON fences or internal audit fields. Fact-table labels/precision/subtables match the reader. Full body uses the historical filename; excerpt adds `-节选`. The existing downloader appends `.md`.
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
## 报告分块导出与删除
|
||||
|
||||
- 列表已完成行只留「查看报告」。阅读页操作条只留「返回报告中心」、「中文 / English」切换(只在有英文版的报告上出现)与带字的「导出」按钮(读屏名「导出报告」,2026-09-29 起不再是只有图标的按钮)。不再显示「下载原始附录」或「打印」入口文案;浏览器 Ctrl+P 排版仍保留。
|
||||
- 导出面板(2026-09-30 重设计):标题「导出报告」;语言「中文 / English」下一句「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」;范围「整篇报告」(副行如「7 章正文 + 22 张分盘 + 8 张核对表」)/「自选章节」(副行「只带走需要的部分」);分组名「正文 / 分盘 / 核对表」带「n/N」;按钮「导出整篇 · 约 X KB」「导出 n 项 · 约 X KB」,一项没选时「先勾选要导出的部分」。成功提示「已导出」。没有英文版时只写一句:「这份报告生成于英文版上线前,重新生成即可获得中英两版。」或「这份报告的英文版没有生成成功,只能导出中文版。」
|
||||
- 导出面板(2026-09-30 重设计):标题「导出报告」;语言「中文 / English」下一句「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」;范围「整篇报告」(副行如「7 章正文 + 22 张分盘 + 8 张核对表」)/「自选章节」(副行「只带走需要的部分」);分组名「正文 / 分盘 / 核对表」带「n/N」;按钮「导出整篇 · 约 X KB」「导出 n 项 · 约 X KB」,一项没选时「先勾选要导出的部分」。成功提示「已导出」。没有英文版时弹窗内不写字,打开时顶部浮层提示一次:「这份报告生成于英文版上线前,重新生成即可获得中英两版。」或「这份报告的英文版没有生成成功,只能导出中文版。」
|
||||
- 删除确认:「确定删除这份报告?」;按钮:「删除 / 取消」;请求中:「删除中…」;失败:「报告没能删除,请稍后重试。」失败保留原行。
|
||||
- 日期中文全写;深度「简版 / 标准深度 / 深度 / 研究级」;主题复用现有中文映射,未知 id 原样回退,不造含义。
|
||||
|
||||
|
||||
@@ -4108,15 +4108,15 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.report-export-field { display: grid; gap: var(--space-2); }
|
||||
.report-export-segment { display: grid; grid-template-columns: 1fr 1fr; padding: 3px; border-radius: var(--radius-md); background: var(--color-canvas-muted); }
|
||||
.report-export-segment button { border: 0; border-radius: calc(var(--radius-md) - 2px); background: transparent; color: var(--color-ink-secondary); font: inherit; font-size: var(--type-body-sm); cursor: pointer; }
|
||||
.report-export-segment button[aria-checked="true"] { background: var(--color-canvas); color: var(--color-ink); font-weight: 600; box-shadow: 0 1px 2px color-mix(in srgb, var(--color-ink) 12%, transparent); }
|
||||
.report-export-segment button[aria-checked="true"] { background: var(--color-canvas); color: var(--color-action); font-weight: 600; box-shadow: 0 1px 2px color-mix(in srgb, var(--color-ink) 12%, transparent), inset 0 0 0 1px var(--color-action-strong); }
|
||||
.report-export-caption { margin: 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.5; }
|
||||
.report-export-modes { display: grid; gap: var(--space-2); }
|
||||
.report-export-mode { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: transparent; color: var(--color-ink); font: inherit; text-align: left; cursor: pointer; }
|
||||
.report-export-mode[aria-checked="true"] { border-color: var(--color-ink-secondary); background: var(--color-canvas-muted); }
|
||||
.report-export-mode[aria-checked="true"] { border-color: var(--color-action-strong); background: transparent; }
|
||||
.report-export-mode strong { display: block; font-size: var(--type-body-md); font-weight: 600; }
|
||||
.report-export-mode small { display: block; margin-top: 2px; color: var(--color-ink-tertiary); font-size: var(--type-caption); }
|
||||
.report-export-radio { flex-shrink: 0; width: 18px; height: 18px; border: 1.5px solid var(--color-border-strong); border-radius: 50%; }
|
||||
.report-export-mode[aria-checked="true"] .report-export-radio { border: 5px solid var(--color-ink); }
|
||||
.report-export-mode[aria-checked="true"] .report-export-radio { border: 5px solid var(--color-action-strong); }
|
||||
.report-export-list { display: grid; gap: var(--space-1); }
|
||||
.report-export-group + .report-export-group { border-top: 1px solid var(--color-border); padding-top: var(--space-1); }
|
||||
.report-export-group-head { display: flex; align-items: center; }
|
||||
@@ -4127,7 +4127,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.report-export-row small { margin-left: var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-caption); font-weight: 400; font-variant-numeric: tabular-nums; }
|
||||
.report-export-box { flex-shrink: 0; display: grid; place-items: center; width: 18px; height: 18px; border: 1.5px solid var(--color-border-strong); border-radius: 4px; color: var(--color-canvas); }
|
||||
.report-export-box svg { width: 13px; height: 13px; stroke-width: 3; }
|
||||
.report-export-row:is([aria-checked="true"], [aria-checked="mixed"]) .report-export-box { border-color: var(--color-ink); background: var(--color-ink); }
|
||||
.report-export-row:is([aria-checked="true"], [aria-checked="mixed"]) .report-export-box { border-color: var(--color-action-strong); background: var(--color-action-strong); }
|
||||
.report-export-expand { flex-shrink: 0; border: 0; background: transparent; color: var(--color-ink-secondary); cursor: pointer; }
|
||||
.report-export-expand svg { width: 18px; height: 18px; transition: transform 140ms ease-out; }
|
||||
.report-export-expand[aria-expanded="true"] svg { transform: rotate(90deg); }
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Check, ChevronRight, Minus, X } from "lucide-react";
|
||||
import { detectPrintRestriction } from "@/lib/client-report-export";
|
||||
import { downloadMarkdownReport } from "@/lib/consultation-report-export";
|
||||
import { notifyError, notifySuccess } from "@/lib/notify";
|
||||
import { notifyError, notifyInfo, notifySuccess } from "@/lib/notify";
|
||||
import { exportByteSize, exportFilename, formatExportSize, joinExportBlocks, type ReportExportBlock } from "@/lib/report-export-blocks";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { EnglishEditionStatus, ReportLanguage } from "@/lib/report-language";
|
||||
@@ -52,11 +52,22 @@ export function ReportExportDrawer({ editions, initialLanguage = "zh", englishEd
|
||||
const element = dialog.current;
|
||||
const previous = document.activeElement;
|
||||
element?.showModal();
|
||||
// Once per open. The sheet remounts every time it opens, so this mount effect
|
||||
// is the open. Later picks and language switches must not raise it again.
|
||||
// Same id collapses React Strict Mode's double invoke into one toast.
|
||||
if (!hasEnglish) {
|
||||
notifyInfo(
|
||||
englishEdition === "unavailable"
|
||||
? "这份报告的英文版没有生成成功,只能导出中文版。"
|
||||
: "这份报告生成于英文版上线前,重新生成即可获得中英两版。",
|
||||
{ id: "report-export-english" },
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
element?.close();
|
||||
if (previous instanceof HTMLElement && previous.isConnected) previous.focus({ preventScroll: true });
|
||||
};
|
||||
}, []);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- notify once per open, after showModal
|
||||
|
||||
function switchLanguage(next: ReportLanguage) {
|
||||
if (next === language) return;
|
||||
@@ -119,11 +130,7 @@ export function ReportExportDrawer({ editions, initialLanguage = "zh", englishEd
|
||||
</div>
|
||||
<p className="report-export-caption">拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="report-export-caption">
|
||||
{englishEdition === "unavailable" ? "这份报告的英文版没有生成成功,只能导出中文版。" : "这份报告生成于英文版上线前,重新生成即可获得中英两版。"}
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<div className="report-export-modes" role="radiogroup" aria-label="导出范围">
|
||||
<button type="button" role="radio" aria-checked={mode === "whole"} className="report-export-mode" onClick={() => setMode("whole")}>
|
||||
|
||||
@@ -110,12 +110,41 @@ test("the BUG-1078 report-only override is gone; the selected export preset stay
|
||||
assert.doesNotMatch(cssNoComments, /report-center-generate/);
|
||||
assert.doesNotMatch(generateSource, /report-center-generate/);
|
||||
assert.doesNotMatch(cssNoComments, /\[class~="bg-primary"\]/);
|
||||
// 原值:`.report-export-presets [aria-pressed="true"]` 用中性底 + ink,不用 action-soft
|
||||
// 新值:套餐已删(2026-09-30 导出面板重设计);同一约束落到新的选中态——范围单选卡与语言分段
|
||||
// 原因:BUG-1078 的主语(导出面板选中态不得用 --color-action-soft 深色块)不变
|
||||
for (const selector of ['.report-export-mode[aria-checked="true"]', '.report-export-segment button[aria-checked="true"]']) {
|
||||
const selected = rule(selector);
|
||||
assert.doesNotMatch(selected, /--color-action-soft|--color-action\b/);
|
||||
assert.match(selected, /--color-ink|--color-canvas/);
|
||||
// 原值:范围单选卡与语言分段 doesNotMatch(/--color-action-soft|--color-action\b/) 且 match(/--color-ink|--color-canvas/)
|
||||
// 新值:四条选中规则不得用 --color-action-soft / --color-action-fill;描边、圆点、勾选框和语言分段用 --color-action-strong(语言文字用 --color-action);底色只许 transparent / --color-canvas*(勾选框 18px 小方块的 background 可以是 --color-action-strong)
|
||||
// 原因:issue #90 推翻 DESIGN「Selected states use canvas-muted + ink」。BUG-1078 的主语(不得用 --color-action-soft 深色块)保留。旧正则 --color-action\b 会误伤 --color-action-strong
|
||||
const selected = {
|
||||
mode: rule('.report-export-mode[aria-checked="true"]'),
|
||||
radio: rule('.report-export-mode[aria-checked="true"] .report-export-radio'),
|
||||
box: rule('.report-export-row:is([aria-checked="true"], [aria-checked="mixed"]) .report-export-box'),
|
||||
segment: rule('.report-export-segment button[aria-checked="true"]'),
|
||||
};
|
||||
for (const body of Object.values(selected)) {
|
||||
assert.doesNotMatch(body, /--color-action-soft|--color-action-fill\b/);
|
||||
}
|
||||
assert.match(selected.mode, /border-color: var\(--color-action-strong\);/);
|
||||
assert.match(selected.mode, /background: transparent;/);
|
||||
assert.match(selected.radio, /border: 5px solid var\(--color-action-strong\);/);
|
||||
assert.match(selected.box, /border-color: var\(--color-action-strong\);/);
|
||||
assert.match(selected.box, /background: var\(--color-action-strong\);/);
|
||||
assert.doesNotMatch(selected.box, /(?<![\w-])color\s*:/);
|
||||
assert.match(rule(".report-export-box"), /color: var\(--color-canvas\);/);
|
||||
assert.match(selected.segment, /background: var\(--color-canvas\);/);
|
||||
assert.match(selected.segment, /color: var\(--color-action\);/);
|
||||
assert.match(selected.segment, /font-weight: 600;/);
|
||||
assert.match(selected.segment, /inset 0 0 0 1px var\(--color-action-strong\)/);
|
||||
const neutralBackground = /^(?:transparent|var\(--color-canvas(?:-[a-z]+)?\))$/;
|
||||
for (const [name, body] of [["mode", selected.mode], ["radio", selected.radio], ["segment", selected.segment]] as const) {
|
||||
for (const value of backgrounds(body)) assert.match(value, neutralBackground, name);
|
||||
}
|
||||
for (const value of backgrounds(selected.box)) {
|
||||
assert.match(value, /^(?:transparent|var\(--color-canvas(?:-[a-z]+)?\)|var\(--color-action-strong\))$/);
|
||||
}
|
||||
assert.equal(blockTokens(":root {\n color-scheme: light;").get("color-action-strong"), "#cc785c");
|
||||
assert.equal(blockTokens(':root:not([data-theme="light"]) {').get("color-action-strong"), "#d78064");
|
||||
assert.equal(blockTokens(':root[data-theme="dark"] {').get("color-action-strong"), "#d78064");
|
||||
});
|
||||
|
||||
function backgrounds(body: string): string[] {
|
||||
return [...body.matchAll(/background(?:-color)?\s*:\s*([^;]+);/g)].map((hit) => hit[1].trim());
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { ReportActions } from "../src/components/personal-report/report-actions";
|
||||
import { ReportExportDrawer } from "../src/components/personal-report/report-export-drawer";
|
||||
@@ -174,10 +175,9 @@ test("the language switch only exists when both editions do", async () => {
|
||||
});
|
||||
|
||||
test("the export drawer suggests English for AI only when it can switch", async () => {
|
||||
// 原值:中文时出现提示条 +「切到 English」按钮(切换整页语言);英文时说明句「导出的是英文版」
|
||||
// 新值:导出面板自带「中文 / English」选择(不切换整页),下方一句「拿去问 ChatGPT、Claude 等 AI,建议选 English」;
|
||||
// 选 English 后导出的就是英文块;没有英文版时只剩一句说明
|
||||
// 原因:2026-09-30 导出面板重设计(两步:语言 → 整篇 / 自选)
|
||||
// 原值:没有英文版时,弹窗正文含「重新生成即可获得中英两版」
|
||||
// 新值:弹窗正文不含该句;toast.info 调用一次,文案为该句,id 为 report-export-english;仍没有 English 选项;英文可用时仍有「建议选 English」
|
||||
// 原因:issue #90 把英文不可用说明改成打开弹窗时的顶部浮层。更早的一轮(2026-09-30)已经把整页「切到 English」改成弹窗内的语言选择
|
||||
const zhBlocks = buildReportExportBlocks({ markdown: zhReader.markdown });
|
||||
const enBlocks = buildReportExportBlocks({ markdown: `# Report\n\n## Summary\n\n${ENGLISH_PROSE}\n`, language: "en" });
|
||||
const h = createClientLifecycleHarness();
|
||||
@@ -194,9 +194,19 @@ test("the export drawer suggests English for AI only when it can switch", async
|
||||
await h.render(<ReportExportDrawer editions={{ zh: zhBlocks, en: enBlocks }} initialLanguage="en" onClose={() => {}} englishEdition="ready" />);
|
||||
assert.equal(h.elements().find(node => node.getAttribute("role") === "radio" && node.text === "English")!.getAttribute("aria-checked"), "true", "starts on the edition being read");
|
||||
await h.render(null);
|
||||
await h.render(<ReportExportDrawer editions={{ zh: zhBlocks }} onClose={() => {}} englishEdition="legacy" />);
|
||||
assert.ok(h.container.text.includes("重新生成即可获得中英两版"));
|
||||
assert.ok(!h.elements().some(node => node.getAttribute("role") === "radio" && node.text === "English"));
|
||||
const legacy = "这份报告生成于英文版上线前,重新生成即可获得中英两版。";
|
||||
const infos: { message: string; id?: string | number }[] = [];
|
||||
const previousInfo = toast.info;
|
||||
toast.info = (message, options) => {
|
||||
infos.push({ message: String(message), id: options?.id });
|
||||
return infos.length;
|
||||
};
|
||||
try {
|
||||
await h.render(<ReportExportDrawer editions={{ zh: zhBlocks }} onClose={() => {}} englishEdition="legacy" />);
|
||||
assert.equal(h.container.text.includes(legacy), false);
|
||||
assert.deepEqual(infos, [{ message: legacy, id: "report-export-english" }]);
|
||||
assert.ok(!h.elements().some(node => node.getAttribute("role") === "radio" && node.text === "English"));
|
||||
} finally { toast.info = previousInfo; }
|
||||
} finally { await h.close(); }
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ const blocks = buildReportExportBlocks({ markdown: reader.markdown, factTables:
|
||||
const editions = { zh: blocks };
|
||||
const source = readFileSync(new URL("../src/components/personal-report/report-export-drawer.tsx", import.meta.url), "utf8");
|
||||
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
// Opening a sheet without English notifies. Keep the real toaster out of these mounts.
|
||||
toast.info = () => 1;
|
||||
|
||||
type Harness = ReturnType<typeof createClientLifecycleHarness>;
|
||||
const button = (h: Harness, text: string) => { const node = h.elements().find(node => node.tagName === "BUTTON" && node.text.startsWith(text)); assert.ok(node, text); return node; };
|
||||
@@ -211,6 +213,52 @@ test("drawer contract uses native dialog, Escape/backdrop close and only allowed
|
||||
assert.match(css, /\.report-export-footer \{[^}]*flex-shrink: 0;/);
|
||||
});
|
||||
|
||||
test("an unavailable English edition is one info toast per open, not a caption", async () => {
|
||||
const legacy = "这份报告生成于英文版上线前,重新生成即可获得中英两版。";
|
||||
const failed = "这份报告的英文版没有生成成功,只能导出中文版。";
|
||||
const infos: { message: string; id?: string | number }[] = [];
|
||||
const previous = toast.info;
|
||||
const h = createClientLifecycleHarness();
|
||||
toast.info = (message, options) => {
|
||||
assert.equal(h.elements().find(node => node.tagName === "DIALOG")?.open, true, "notify after showModal");
|
||||
infos.push({ message: String(message), id: options?.id });
|
||||
return infos.length;
|
||||
};
|
||||
try {
|
||||
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="legacy" />);
|
||||
assert.deepEqual(infos, [{ message: legacy, id: "report-export-english" }]);
|
||||
assert.equal(h.container.text.includes(legacy), false);
|
||||
assert.equal(h.container.text.includes(failed), false);
|
||||
await h.event(radio(h, "自选章节"));
|
||||
const chapter = blocks.find(block => block.group === "body")!;
|
||||
await h.event(checks(h).find(node => node.text.startsWith(chapter.title))!);
|
||||
await h.event(checks(h).find(node => node.text.startsWith(chapter.title))!);
|
||||
const expand = h.elements().find(node => node.getAttribute("aria-label") === "展开分盘");
|
||||
assert.ok(expand, "展开分盘");
|
||||
await h.event(expand);
|
||||
assert.equal(infos.length, 1, "picking chapters does not notify again");
|
||||
await h.render(null);
|
||||
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="legacy" />);
|
||||
assert.equal(infos.length, 2, "opening again notifies once more");
|
||||
assert.equal(infos[1]?.id, "report-export-english");
|
||||
|
||||
await h.render(null);
|
||||
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="unavailable" />);
|
||||
assert.equal(infos.length, 3);
|
||||
assert.deepEqual(infos[2], { message: failed, id: "report-export-english" });
|
||||
assert.equal(h.container.text.includes(failed), false);
|
||||
assert.equal(h.container.text.includes(legacy), false);
|
||||
|
||||
await h.render(null);
|
||||
await h.render(<ReportExportDrawer editions={{ zh: blocks, en: blocks }} onClose={() => {}} englishEdition="ready" />);
|
||||
assert.equal(infos.length, 3, "a ready English edition does not notify");
|
||||
assert.ok(h.container.text.includes("建议选 English"));
|
||||
} finally {
|
||||
toast.info = previous;
|
||||
await h.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("the drawer shows the real download size, not a character count (BUG-1094)", async () => {
|
||||
const body = blocks.filter(block => block.group === "body");
|
||||
const charts = blocks.filter(block => block.group === "chart");
|
||||
|
||||
Reference in New Issue
Block a user