327 lines
19 KiB
Markdown
327 lines
19 KiB
Markdown
# 界面设计说明
|
||||
|
|
|
|||
|
|
给**维护界面的人**看:颜色和样式放在哪、加一个控件该怎么做、哪些约定不能破。
|
|||
|
|
|
|||
|
|
美化前的状态是"零样式基础设施":没有 QSS、没有主题、没有图标,
|
|||
|
|
只有 6 处内联 `setStyleSheet` 和一套硬编码的 `setFixedWidth(72/88/96/110/140)`。
|
|||
|
|
现在样式集中在两个模块里:
|
|||
|
|
|
|||
|
|
| 文件 | 职责 |
|
|||
|
|
| --- | --- |
|
|||
|
|
| `src/chongming_license_gui/theme.py` | 设计 token、全局 QSS、`apply_theme()`、语义化样式助手 |
|
|||
|
|
| `src/chongming_license_gui/icons.py` | 内联 SVG 图标 → `QIcon`(无二进制素材) |
|
|||
|
|
| `src/chongming_license_gui/common.py` | 控件工厂:`btn()` / `hint()` / `set_status()` / `flash()` / `section()` |
|
|||
|
|
| `src/chongming_license_gui/pg_store.py` | PostgreSQL 台账后端(可选)+ `PgConfig` |
|
|||
|
|
| `src/chongming_license_gui/pg_dialog.py` | PostgreSQL 连接设置对话框 |
|
|||
|
|
| `tools/screenshot.py` | 生成 `docs/images/` 里的文档截图 |
|
|||
|
|
| `tools/pg_check.py` | 对真实 PG 做契约一致性校验(事务内执行并回滚) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 设计 token
|
|||
|
|
|
|||
|
|
全部定义在 `theme.py` 顶部。**改主题只改这里**(或下面的 QSS 模板),
|
|||
|
|
不要在页面里写死十六进制值。
|
|||
|
|
|
|||
|
|
### 主色与中性色
|
|||
|
|
|
|||
|
|
| Token | 值 | 用途 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `ACCENT` | `#2563eb` | 主操作按钮、选中页签、卡片标题、焦点描边 |
|
|||
|
|
| `ACCENT_HOVER` / `ACCENT_PRESSED` | `#1d4ed8` / `#1e40af` | 主按钮悬停 / 按下 |
|
|||
|
|
| `ACCENT_SOFT` | `#eff6ff` | 表格与列表的选中底、页签悬停底、概览条底色 |
|
|||
|
|
| `BG` | `#f5f6f8` | 窗口与页签面板底色(卡片浮在它上面) |
|
|||
|
|
| `SURFACE` | `#ffffff` | 卡片、输入框、导出底 |
|
|||
|
|
| `SURFACE_ALT` / `SURFACE_SUNKEN` | `#fafbfc` / `#f7f8fa` | 表格斑马纹 / 表头与只读区 |
|
|||
|
|
| `BORDER` / `BORDER_STRONG` | `#e3e6ea` / `#cdd3da` | 卡片描边 / 输入框与次按钮描边 |
|
|||
|
|
| `TEXT` / `TEXT_MUTED` / `TEXT_DISABLED` | `#1f2328` / `#6b7280` / `#9ca3af` | 正文 / 说明文字 / 禁用 |
|
|||
|
|
|
|||
|
|
### 语义色
|
|||
|
|
|
|||
|
|
每个语义色都配一个浅底(`*_SOFT`)和描边(`*_BORDER`),用于横幅、胶囊、警示条。
|
|||
|
|
|
|||
|
|
| Token | 值 | 用途 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `OK` | `#15803d` | 校验通过、已加载、主色胶囊的"正常"态 |
|
|||
|
|
| `WARN` | `#b45309` | 安全提示、weak/无来源告警、未加载私钥胶囊、页脚安全条 |
|
|||
|
|
| `DANGER` | `#b91c1c` | 校验失败、已过期、危险按钮(删除记录) |
|
|||
|
|
| `INFO` | `#1d4ed8` | 台账"永久有效" |
|
|||
|
|
| `PERMANENT` | `#6d28d9` | 台账"未生效" |
|
|||
|
|
| `EXPIRING` | `#c2410c` | 台账"即将到期"。**注意**:原内联色 `#e65100` 在白底上对比度不足,这里刻意加深了一档 |
|
|||
|
|
|
|||
|
|
### 形状与字体
|
|||
|
|
|
|||
|
|
| 项 | 值 | 说明 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `RADIUS_CARD` / `RADIUS_CONTROL` | `8px` / `5px` | 卡片 / 输入框与按钮 |
|
|||
|
|
| 界面字体 | `Microsoft YaHei UI` → 苹方 → Noto CJK → Segoe UI,10pt | 由 `theme.ui_font()` + `QApplication.setFont()` 下发 |
|
|||
|
|
| 等宽字体 | `Cascadia Mono` → `Consolas` → `JetBrains Mono` → 系统 FixedFont | 由 `common.mono_font()`,用于机器码/指纹/申请码 |
|
|||
|
|
|
|||
|
|
> ⚠️ **样式表里不要写 `font-family`**。QSS 的字体会覆盖 per-widget 的 `setFont()`,
|
|||
|
|
> 而机器码、指纹这类要**逐字符核对**的文本必须保持等宽 —— 一旦被覆盖就会错位。
|
|||
|
|
> 界面字体统一走 `QApplication.setFont()`(作用范围是"默认值",不会压掉 per-widget 设置)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 控件规范
|
|||
|
|
|
|||
|
|
### 卡片(分区)
|
|||
|
|
|
|||
|
|
页面用 `common.section("标题")` 建卡片,返回 `(QGroupBox, QFormLayout)`。
|
|||
|
|
视觉全部由 QSS 负责(白底、1px 描边、8px 圆角、标题着主色),
|
|||
|
|
代码里只设间距(`section()` 内部设 `ContentsMargins(0,2,0,0)`、行距 9、列距 12)。
|
|||
|
|
|
|||
|
|
> 卡片留白由 QSS 的 `padding` 控制,**`section()` 里不要再设 padding** ——
|
|||
|
|
> 两处都设会叠加成过大的留白。
|
|||
|
|
|
|||
|
|
### 按钮
|
|||
|
|
|
|||
|
|
一律用 `common.btn(text, role=..., icon_name=..., min_width=...)`,它负责 role、
|
|||
|
|
图标着色与最小宽度。
|
|||
|
|
|
|||
|
|
| `role` | 外观 | 用在哪 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| 空 | 白底描边(次按钮) | 默认;浏览、刷新、复制等辅助操作 |
|
|||
|
|
| `"primary"` | 主色实心 + 白字 + 白图标 + 加粗 | **每页只放一个**主操作:签发并保存 / 查询 / 续期选中 / 校验 / 生成密钥对 |
|
|||
|
|
| `"danger"` | 白底 + 红字红描边,悬停浅红底 | 破坏性操作:删除记录 |
|
|||
|
|
| `"link"` | 无边框主色文字 | 文字链(QSS 已备好,暂未使用) |
|
|||
|
|
|
|||
|
|
- 用 `min_width`(`setMinimumWidth`)而不是 `setFixedWidth`:中文与高 DPI 下不会被挤压。
|
|||
|
|
- 主按钮图标自动用白色,次按钮用 `TEXT_MUTED`,危险按钮用 `DANGER` —— 不用手写颜色。
|
|||
|
|
|
|||
|
|
### 语义标签
|
|||
|
|
|
|||
|
|
| `role` | 外观 | 用途示例 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `hint` | 灰字 | 字段下方的说明 |
|
|||
|
|
| `warn` | 琥珀字 | 私钥告警、仅机器码的警示 |
|
|||
|
|
| `danger` / `ok` | 红字 / 绿字 | 加载失败 / 成功 |
|
|||
|
|
| `stats` / `stats-error` | 浅蓝底信息条 / 浅红底 | 台账概览(正常 / 打不开) |
|
|||
|
|
| `verdict-ok` / `verdict-bad` / `verdict-idle` | 绿/红/灰**横幅** | 校验结论 |
|
|||
|
|
| `chip-ok` / `chip-warn` | 圆角胶囊 | 页头的密钥状态 |
|
|||
|
|
| `flash` | 绿字 | 复制后的瞬时提示(由 `common.flash()` 用完自动清除) |
|
|||
|
|
|
|||
|
|
运行期改 `role` 后**必须**调用 `theme.set_role()` / `theme.repolish()`,
|
|||
|
|
否则 Qt 不会重算样式(`setProperty` 本身不触发重绘)。
|
|||
|
|
|
|||
|
|
### 状态色(台账)
|
|||
|
|
|
|||
|
|
`theme.STATUS_COLORS` 与 `chongming_license.STATUS_LABELS` 的键一一对应。
|
|||
|
|
表格里的状态列只改**呈现**(前景色 + 加粗),**单元格文本保持原样**
|
|||
|
|
(形如 `即将到期 (30d)`)—— 有回归断言盯着,别顺手改格式。
|
|||
|
|
|
|||
|
|
### 指示器(复选框/单选框)
|
|||
|
|
|
|||
|
|
QSS 画不了图形,只能用图片文件。做法是在 `theme._ensure_indicator_assets()` 里
|
|||
|
|
**按 token 生成 6 个 SVG 写进临时目录**,再由 QSS `image: url(...)` 引用:
|
|||
|
|
改主题色时指示器跟着变,仓库里也不用存二进制素材。
|
|||
|
|
|
|||
|
|
这道工序有两道防线,别删:
|
|||
|
|
|
|||
|
|
1. 生成的 SVG 必须能用 `QImage` 读出来,否则整套指示器样式都不生效(回落原生外观);
|
|||
|
|
2. 拼接后若还残留 `@PLACEHOLDER@`,整块指示器样式**整段丢弃**。
|
|||
|
|
|
|||
|
|
> 原因:QSS 的 `image` 加载失败时 **Qt 不报错**,指示器会直接变成空白 ——
|
|||
|
|
> 用户就看不见"签发后记入台账"到底勾没勾。开发过程中真的踩过一次
|
|||
|
|
> (占位符替换把 `url()` 嵌了两层),所以现在有两道防线 + 一条回归断言。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 页面骨架
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─ QMainWindow ────────────────────────────────────────────┐
|
|||
|
|
│ 页头 QFrame[role=header]:盾牌图标 + 标题/副标题 + 密钥状态胶囊 │
|
|||
|
|
├──────────────────────────────────────────────────────────┤
|
|||
|
|
│ QTabWidget:签发 / 台账 / 密钥 / 查看·校验(带图标) │
|
|||
|
|
│ └ 每个页签 = QScrollArea#pageScroll → 页面 QWidget │
|
|||
|
|
│ └ section() 卡片… │
|
|||
|
|
├──────────────────────────────────────────────────────────┤
|
|||
|
|
│ 页脚 QFrame[role=footer]:锁图标 + 私钥安全提示(琥珀条) │
|
|||
|
|
└──────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 为什么每页都套滚动容器
|
|||
|
|
|
|||
|
|
原实现是"页签直接装内容"。窗口 1180×940、签发页内容约 1360px,
|
|||
|
|
在 1366×768 的笔记本上**底部的「签发并保存…」被窗口切掉且无法滚到**。
|
|||
|
|
现在四个页签统一套 `QScrollArea(setWidgetResizable(True))`,内容再长也能滚到。
|
|||
|
|
|
|||
|
|
> 注意:套了容器后 `tabs.widget(index)` 返回的是 `QScrollArea` 而不是页面对象。
|
|||
|
|
> `MainWindow._on_tab_changed` 因此改成按 `self._pages[index]` 比对,
|
|||
|
|
> 页面 → 容器的映射在 `self._area_by_page`。新增页签时别忘了同步这两处。
|
|||
|
|
|
|||
|
|
### 打开时就用最小尺寸
|
|||
|
|
|
|||
|
|
窗口打开即最小尺寸(`resize(self.minimumSize())`,当前 940×640):小屏笔记本上
|
|||
|
|
不会顶出屏幕,需要更大自己拉。页签都在滚动容器里,窗口小只是多滚动,不会把按钮挤没。
|
|||
|
|
|
|||
|
|
> 代价:940 宽时台账表格(9 列合计 1322px)要横向滚动才能看到最右侧的列;
|
|||
|
|
> 表格一直是横向可滚的,只是露出多少列不同。改这个值时在 1366×768 上试一眼。
|
|||
|
|
> 文档截图仍按 1240 宽渲染(`tools/screenshot.py` 的 `WINDOW_WIDTH`),比实际窗口宽。
|
|||
|
|
|
|||
|
|
### 页头的密钥状态胶囊
|
|||
|
|
|
|||
|
|
显示"私钥已加载 · 指纹 xxxx"(绿)或"未加载私钥 · 点击到「密钥」页加载"(琥珀),
|
|||
|
|
点击跳转到密钥页。标题栏同时也带指纹(多开窗口/截图沟通时靠它区分)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 怎么改
|
|||
|
|
|
|||
|
|
| 想做的事 | 改哪里 |
|
|||
|
|
| --- | --- |
|
|||
|
|
| 换主色/配色 | `theme.py` 顶部的 token;QSS 与指示器 SVG 都会跟着变 |
|
|||
|
|
| 调卡片留白、圆角、按钮尺寸 | `theme.py` 的 `_QSS` 模板(`@TOKEN` 占位符) |
|
|||
|
|
| 加一个新图标 | `icons.py` 的 `_SHAPES` 加一条 24×24 描边路径,然后 `icons.icon("名字")` |
|
|||
|
|
| 加一种语义样式 | `theme.py` 的 `_TOKENS` 加色值 → `_QSS` 加 `QLabel[role="..."]` 规则 |
|
|||
|
|
| 换界面字体/字号 | `theme.ui_font()` |
|
|||
|
|
| 换等宽字体 | `common._MONO_CANDIDATES` |
|
|||
|
|
| 改台账的 PG 行为(SQL/方言/建表) | `pg_store.py`:SQL 提在模块级常量里,改完跑 `smoke_test.py` 的「PG 适配层」小节 |
|
|||
|
|
| 改 PG 连接对话框字段 | `pg_dialog.py` + `pg_store.PgConfig`(记得同步 `from_settings` / `save`) |
|
|||
|
|
| 重新生成文档截图 | `uv run python tools/screenshot.py` |
|
|||
|
|
|
|||
|
|
### QSS 模板的占位符
|
|||
|
|
|
|||
|
|
`_QSS` 里用 `@TOKEN` 写色值(**不用** `str.format`/f-string:样式表里的 CSS 花括号
|
|||
|
|
会被当成占位符)。`_substitute()` 按名字**从长到短**替换,避免 `@TEXT` 吃掉 `@TEXT_MUTED`;
|
|||
|
|
占位符结尾的 `@` 可写可不写(`@TEXT` 与 `@TEXT@` 都行)。
|
|||
|
|
|
|||
|
|
### 图标
|
|||
|
|
|
|||
|
|
单色描边图标(feather 风格),运行时用 `QSvgRenderer` 光栅化到 2× 位图并标记
|
|||
|
|
devicePixelRatio,按 `(名称, 颜色, 尺寸)` 缓存。`PySide6.QtSvg` 不可用时
|
|||
|
|
`icon()` 返回空 `QIcon` —— 少个图标不影响功能,绝不能因此报错。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 台账的双后端
|
|||
|
|
|
|||
|
|
台账可以存本地 SQLite,也可以存共享 PostgreSQL。设计上刻意让**页面完全不知道**
|
|||
|
|
后端是谁:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
RegistryTab ──> RegistryHolder ──┬─> chongming_license.Registry (本地 SQLite)
|
|||
|
|
└─> pg_store.PostgresRegistry (PostgreSQL)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`PostgresRegistry` **继承**库的 `Registry`,只重写与存储引擎相关的 6 个方法
|
|||
|
|
(`__init__` / `query` / `get` / `save` / `update_note` / `delete`),
|
|||
|
|
`stats` / `find_by_machine_code` / `export_licenses` / `export_csv` / `close` /
|
|||
|
|
`_row_to_record` **全部继承** —— 于是 CSV 列序、`.lic` 导出文件名与客户目录转义、
|
|||
|
|
状态分桶统计都不可能和 SQLite 版出现第二套实现。
|
|||
|
|
|
|||
|
|
几条必须保持的语义(改 PG 后端时逐条对照):
|
|||
|
|
|
|||
|
|
| 语义 | 为什么 |
|
|||
|
|
| --- | --- |
|
|||
|
|
| 状态筛选留在 Python 侧(用 `LicenseRecord.status`) | 状态有优先级,下推成 SQL 条件会互相重叠,`stats()` 各项之和会不等于总数 |
|
|||
|
|
| `ON CONFLICT (lic_id) DO UPDATE SET` 恰好 13 列,**不含 `created_at`** | 首次入库时间要保留;覆盖时只刷新 `updated_at` |
|
|||
|
|
| 覆盖已有记录时保留台账人工批注(除非 `overwrite_note=True`) | 载荷 `note` 是签名内容,台账 `note` 是厂商批注,同名不同用途 |
|
|||
|
|
| 日期列用 TEXT 存、`ORDER BY COALESCE(expires_at,'9999-12-31')` | 库依赖 ISO 日期字符串比较排序,NULL(永久)排最后 |
|
|||
|
|
| 过滤用 `ILIKE` 而不是 `LIKE` | SQLite 的 `LIKE` 对 ASCII 大小写不敏感,用 PG 的 `LIKE` 会让同一条件在两端结果不同 |
|
|||
|
|
| `autocommit=True`(界面路径) | 镜像 SQLite 的逐语句提交;否则长连接会长期 `idle in transaction` |
|
|||
|
|
| schema 名必须校验后加引号 | psycopg 的参数占位符不能用于标识符,不校验就是注入面 |
|
|||
|
|
|
|||
|
|
界面侧:「台账库」卡片里用 `setRowVisible` 在「路径」行与「连接」行之间切换
|
|||
|
|
(不用 `QStackedWidget`:那会破坏表单标签对齐)。切换后端时**主动断开**当前连接,
|
|||
|
|
避免"界面显示一个后端、实际读写另一个"。密码只存 `PgConfig`,且:
|
|||
|
|
|
|||
|
|
- 不进日志、不进标签、不进错误信息(`display()` 与连接 kwargs 都刻意排除它);
|
|||
|
|
- 只有勾选"记住密码"才写 QSettings,取消勾选即删除。
|
|||
|
|
|
|||
|
|
### 5.1 两个"重签"行操作
|
|||
|
|
|
|||
|
|
台账里能改授权内容的两个操作都是**重签**(签名覆盖整个载荷,改任何字段只能重签):
|
|||
|
|
|
|||
|
|
| 操作 | 变了什么 | `source` | 旧记录批注 |
|
|||
|
|
| --- | --- | --- | --- |
|
|||
|
|
| 续期选中…(`RenewDialog`) | 到期日(`not_before` 不传 = 从现在起算) | `renew` | `已续期 → <新编号>` |
|
|||
|
|
| 改插件…(`ChangePluginsDialog`) | 只改 `plugins`;`not_before`/`expires_at` **逐字写回** | `plugins` | `已变更插件 → <新编号>` |
|
|||
|
|
|
|||
|
|
两者共用 `RegistryTab._resign_and_save()`(选路径 → 复用台账指纹重签 → 写盘 → 入库
|
|||
|
|
→ 批注),区别只在传进去的日期与文案。改插件对话框刻意**不提供**有效期选项:
|
|||
|
|
要延长有效期就走续期,免得"只想加个插件"顺手把到期日也改了。
|
|||
|
|
|
|||
|
|
插件勾选由 `plugin_picker.PluginPicker` 提供(签发页与改插件对话框共用)。它按
|
|||
|
|
"构建器"形态接收调用方的 `QFormLayout` 并直接往里加行,签发页排版因此与抽出之前一致;
|
|||
|
|
`changed` 信号供对话框实时算"新增/移除"差异。老白名单靠 `ensure_types()` 补进列表 ——
|
|||
|
|
项目里已经删掉的插件 type 也必须看得见、勾得回来,否则操作者没法对照"原来授权了什么"。
|
|||
|
|
|
|||
|
|
对话框里两个刻意的取舍,改版式时别改回去:
|
|||
|
|
|
|||
|
|
- `show_root=False`:省掉「项目根」行与重新加载按钮。项目根是签发页的设置,
|
|||
|
|
两个地方都能改反而容易改岔;对话框里宽度与高度优先留给操作本身。
|
|||
|
|
- 原授权摘要压成 3 行等宽只读块(`make_mono_label`),与勾选列表同卡。
|
|||
|
|
这个对话框只干"换白名单"一件事,**高度要收在 ~570px 以内** ——
|
|||
|
|
768 高的笔记本屏上才放得下(对话框没有滚动区,超了就是按钮点不到)。
|
|||
|
|
|
|||
|
|
## 6. 回归断言(改界面时先看这里)
|
|||
|
|
|
|||
|
|
`smoke_test.py` 开头有一段"界面主题"检查,专门盯那些**出错时不会报错、只会静默变丑**的地方:
|
|||
|
|
|
|||
|
|
- 样式表已应用、无残留占位符;
|
|||
|
|
- 主操作用主色实心、次按钮保持白底(防 role 写错);
|
|||
|
|
- 危险按钮确实带危险色;
|
|||
|
|
- 勾选态指示器按主题渲染(防指示器变空白);
|
|||
|
|
- 主色/卡片底/页面底/警示色都真的画到了界面上;
|
|||
|
|
- 台账状态列用主题状态色、加粗、机器码列等宽;
|
|||
|
|
- 台账「改插件」对话框:默认预勾选原白名单、默认生成新编号、**没有**有效期选项、
|
|||
|
|
差异行按"与原来一致 / 新增 / 移除"变化并在移除时亮出拒载告警、一个都不勾时拒绝提交;
|
|||
|
|
- 改插件是**重签**:载荷里除 `lic_id`/`issued_at`/`note`/`plugins` 外逐字不变
|
|||
|
|
(`not_before`/`expires_at`/`sources` 原样写回),新文件可独立验签;
|
|||
|
|
- **配置隔离自证**:`settings.fileName()` 必须落在临时目录里。
|
|||
|
|
|
|||
|
|
末尾还有一段"PostgreSQL 后端"检查,**不需要数据库服务器**也能跑:
|
|||
|
|
|
|||
|
|
- SQL 形状(INSERT 15 列列序、`DO UPDATE SET` 13 列且不含 `created_at`、
|
|||
|
|
`COALESCE` 排序、四个条件都用 `ILIKE`);
|
|||
|
|
- 用假连接验证 `save()` 的两条硬语义(保留人工批注、保留 `created_at`);
|
|||
|
|
- 未装驱动时的可读提示、连不上时的可读提示且**不泄漏密码**;
|
|||
|
|
- 「存储」下拉的切换行为(行切换 + holder 目标后端 + 断开旧连接)。
|
|||
|
|
|
|||
|
|
改完界面跑一遍:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
QT_QPA_PLATFORM=offscreen uv run python smoke_test.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**要验 PostgreSQL 的真实读写**,用事务版校验(对真实库零残留):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
uv run python tools/pg_check.py --host 127.0.0.1 --port 5432 --dbname <库> --user <账号>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 维护红线
|
|||
|
|
|
|||
|
|
1. **页面里不写颜色、不写 `setFixedWidth`** —— 用 token 和 `common.btn()`。
|
|||
|
|
2. **不动控件属性名**:`smoke_test.py` 直接引用 `issue_btn`、`preview`、`verdict`、
|
|||
|
|
`table`、`records` 等属性;改名会静默破坏测试。
|
|||
|
|
3. **四个页签的文本与数量不要改**(`签发/台账/密钥/查看 / 校验`),
|
|||
|
|
台账表格保持 **9 列同序同文本**。
|
|||
|
|
4. **保持 `from .common import info, warn, error` + 裸调用**:
|
|||
|
|
测试靠替换各模块的同名全局来消除模态弹窗,改成 `common.info(...)` 会让弹窗
|
|||
|
|
阻塞无头测试。
|
|||
|
|
5. **QSS 不设全局 `font-family`**(见上文)。
|
|||
|
|
6. **指示器的两道防线别删**。
|
|||
|
|
7. **测试/截图/自检脚本必须用 `common.isolated_settings()` 建配置,并调用
|
|||
|
|
`common.assert_isolated_settings()` 自证落点。**
|
|||
|
|
|
|||
|
|
> 这条是踩坑换来的:`QSettings.setDefaultFormat(IniFormat)` 在本环境里**没有**
|
|||
|
|
> 改变两参数构造 `QSettings(org, app)` 的落点,`fileName()` 仍指向注册表
|
|||
|
|
> `HKCU\Software\chongming\license-gui`。于是脚本以为在写临时配置,实际把测试值
|
|||
|
|
> (甚至数据库密码)写进了用户真实配置,还顺手 `clear()` 掉了用户自己的设置。
|
|||
|
|
> 现在一律用四参数构造(format + scope 显式指定),并**回读 `fileName()` 断言**
|
|||
|
|
> 它在临时目录下 —— 隔离这件事不能靠"我以为"。
|
|||
|
|
|
|||
|
|
## 8. 已知取舍
|
|||
|
|
|
|||
|
|
- **基底样式用 Fusion**:本主题大量依赖 QSS,Windows 原生样式与 QSS 混用会出现
|
|||
|
|
"一半原生一半自绘";Fusion 完全可自绘、跨平台一致,也避免系统深色主题下
|
|||
|
|
浅色卡片"半明半暗"。代价是与系统原生控件观感略有差异。
|
|||
|
|
- **只做了浅色主题**。token 已按主题化组织,要加深色变体的话,把 `_TOKENS`
|
|||
|
|
换成另一套色值即可(`build_qss()` 不用改);指示器 SVG 也会跟着变。
|
|||
|
|
- **签发页内容偏长**(约 1360px),在 1080p 上仍需要少量滚动。若要进一步压缩,
|
|||
|
|
可以把"来源指纹"与"解析结果"并排放,或把「预览与签发」提到更靠上的位置 ——
|
|||
|
|
但那是信息架构调整,不是样式问题。
|