feat(UI): W6 第一波——设计令牌层、页面注册表生成器与 UI 门禁补强

启动 docs/任务规划/README.md 的 W6 UI 扩展簇(用户裁定,覆盖 2026-10-11
的"本轮不排期")。四个工作包,全部验证通过。

## U1 设计令牌层(零行为变化)
新建 Drivers/BSP/UI/ui_tokens.h(20 个宏),框架 + 7 个页面 + App/task/
ui_task.c 全部接入。屏幕几何**复用 ui_port.h 既有 UI_SCREEN_W/H**,不重复
定义;令牌头自身只 include ui_port.h,不把 LVGL 拖进全部消费者。
补收了报告没列但代码里真重复的量:UI_TEXT_BAND_H 15(7 次)、列表左右边界
与每页行数(各 4 个页面逐字复制)、推流协议几何与显示几何同源(此前硬编码
3200 且注释写着"须与 UI_FB_SIZE 一致"——典型漂移隐患)、UI_TASK_TICK_MS
(ui_port_step 与 osDelay 必须同值,原先靠注释维系)。
故意不收页面私有几何与"同形不同义"的值(device_info 的 pitch=15 与列表
UI_ROW_PITCH=14 合并即改像素)。

## U3 页面注册表与生成器(删手工分发)
ui_page_registry.{c,h} 由生成器产出,**手工 menu_content_page_for() 已删
除**(仓库内仅存 2 处历史说明注释)。密码页按代码实况**单独出口**
(ui_page_registry_password())——它非节点绑定,任何 #P 叶子进同一页。
顺带落地此前判定"U1 阶段做不了"的共享 flat 样式 ui_style.{c,h}。
生成器除产注册表外**自动把新 .c 写进 uvprojx 与 CMakeLists.txt**,且
**双向对账**:加页打印 registered、删页打印 pruned,不留死条目;并双向
校验(#page 找不到实例报错、实例未被任何 #page 引用也报错)。

## U4 门禁补强(35 个钩子)
新增 font-charset 钩子(scripts/check_font_charset.py):复用
tools/font_gen 的同一套提取与 cmap 解析规则(importlib 直载,零第二套
规则),A 段查源码在用 vs 字符集、B 段查字符集 vs 字体 cmap。
registration-check 补第 ④ 类:实测原规则对"进了两端构建但没接描述符表"
的页面**全绿**——desc 被链接器剥离、编译链接全过、菜单永远打不开的死代码。
仍照不出的两项已如实写进文档(枚举漏加归 @build、菜单树漏挂归 golden)。

## 字体子集瘦身(D 案,4 留 3 删)
ui_cn_chars.txt 122→119 字(恰删 上/电/重,零新增)。异/常/片/芯 进
KEEP_EXTRA 带理由白名单:它们曾在 9eb1bd63 被写进中文自检描述落盘
/log/selftest.log(定长 64B 记录跨固件持久),删字形会使历史故障记录
渲染空白且不可逆。保留字按**必需**处理,反向有牙——谁只跑 extract_cn
把它们静默洗掉,钩子直接 FAIL 并逐字打印理由。
--strict 口径为"未知冗余 = 0"而非"冗余 = 0"。

## 验证
@build 0 错 0 警;ER_IROM1 232,052 B(较 HEAD −228 B)、RW_IRAM1
75,232 B **零增量**、RW_CCM 87.9% 未变。golden:菜单 8 状态全像素一致 +
内容页 11 页逐字节一致,**参考图 sha256 差异 0 条**(未用 --update 刷
基线);池水位菜单基线 5368 B、开页-关页往返 5448 B 与 U1 时相同。
38 个在途修改文件逐个核对 BOM/CRLF/U+FFFD 全部与 HEAD 一致。
L0 33 钩子 + L0.5 16 runner / 1104 断言全绿;check_doc_links --all
142 篇、无法判定 0;mem-budget / mem-numbers 已回填。

## 单位成本实测(U5 排期依据)
临时页 zzprobe 绑到"遥信量"叶子 → 只跑生成器 → 带页 @build 0/0 →
注册表出现 {5u, &ui_page_zzprobe_desc} → 回滚后 6 个产出文件 zzprobe
残留计数均 0、重编回基线。
**结论:新增一页 = 3 处人工(新 .c + 新 .h + 菜单树那一行加
 #page=<名>)+ 1 条生成器命令,手工编辑注册文件 0 处。**
据此修正报告原口径"8 处降到 1 处"——那个 1 只在"1 处对既有文件的修改"
意义下成立,页面自身 2 个文件本就要写,真实处数是 3。

## 文档同步与两处自我修正
§0.1 UI 行 B → B+;§6.1「内容页」「设计令牌」两行按实况回填;§6.5 U1/U3
状态更新;§6 顶部加"建议栏不可照抄"总警示(本轮四次证伪:ui_port.h 已有
同名宏、LV_COLOR_BLACK/WHITE 不存在且极性反了、27 页实为目标页数现存 7
个、生成器草图漏密码页非节点绑定)。
两处改口:① §6.5 U4 从我上一版**误写的 3/3 改回 2/3**——"golden 脚本早已
存在"不等于"已进自动门禁",两个 golden 脚本至今未挂 pre-commit,该缺口
单列为待办;② 原计划"分三笔提交"**作废**,U1 与 U3 改同一批文件、文件内
交叠,硬切会产生不可编译的中间态,故整波一笔。
This commit is contained in:
2026-10-20 21:14:14 +08:00
parent 9d06798f2d
commit dafc18f5ed
47 changed files with 1447 additions and 450 deletions
+19 -22
View File
@@ -4,7 +4,9 @@
* 屏/容器按需建毁生命周期 + 焦点 + 切屏、按键分发(内置 ↑↓/ESC + key_cb 钩子)、
* 描述符驱动通用绘制(浅 V + 标题/副标题 + 行 + 选中带 + 页脚 + draw_cb);
* 不持任何页面数据:各页数据/几何/钩子由 ui_page_xxx 模块经 content_page_def_t
* 接入框架描述符表(架构见 内容页架构.md);
* 提供,框架按**描述符指针**开页;"哪个叶子进哪一页"的绑定在生成物
* ui_page_registry.c(真源 = 菜单树 ui_menu_tree.txt 的 #page= 注记),
* 框架既无页 ID 枚举也无节点→页手写 case(架构见 内容页架构.md);
* 进入时切屏并获焦、ESC 返回菜单屏并还原焦点;
* 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
@@ -23,6 +25,11 @@
* 是否过密码页/所输密码决定);描述符新增 edit_capable(页面
* 是否支持编辑模式)+ 新 API ui_content_page_is_editable
* (页面钩子据以决定编辑行为:edit_capable 且修改权限)
* 2026-10-15 王建锋 U3 页面原型化:删 content_page_id_t 枚举与框架侧 s_defs[],
* open() 改收描述符指针;节点→页面绑定移交给生成物
* ui_page_registry.c(菜单树 #page= 注记为唯一真源),
* 逻辑层手写 menu_content_page_for() 随之删除;
* 描述符新增 id 字段(ASCII 页名,供日志/诊断定位是哪一页)
*/
#ifndef UI_CONTENT_PAGE_H
#define UI_CONTENT_PAGE_H
@@ -32,21 +39,6 @@
#include "lvgl.h"
/*
* 内容页 ID:与菜单叶子节点一一对应(映射见 ui_menu.c menu_content_page_for);
* CONTENT_PAGE_COUNT 兼作"无内容页/未打开"哨兵值
*/
typedef enum {
CONTENT_PAGE_DEVICE_INFO = 0u, /* 装置信息(node 1):静态键值行 + 副标题,单页 */
CONTENT_PAGE_AC, /* 交流量(node 3):数据列表,24 项 / 8 项每页 */
CONTENT_PAGE_DC, /* 直流量(node 4):数据列表,3 项单页 */
CONTENT_PAGE_PASSWORD, /* 用户密钥(密码页):3 位密码,表单/操作页首个实例 */
CONTENT_PAGE_NET_CFG, /* 网口设置(node 30):地址列表 + 保存行,可编辑(修改权限) */
CONTENT_PAGE_RUN_LOG, /* 运行记录(node 40):动态列表,每记录 2 行(时间戳+描述) */
CONTENT_PAGE_SELFTEST_LOG, /* 自检记录(node 39):动态列表,每记录 2 行(时间戳+故障) */
CONTENT_PAGE_COUNT /* 哨兵:无效 ID / 未打开 */
} content_page_id_t;
/* 行数据:左列名 + 右列值(Flash 字面量,不 strdup) */
typedef struct {
const char *name;
@@ -54,14 +46,18 @@ typedef struct {
} cp_row_t;
/*
* 页面描述符:一页 = 一个 Flash const 结构(几何 + 数据源 + 可选钩子)
* 由各 ui_page_xxx 页面模块提供,框架描述符表 s_defs[] 按页 ID 引用
* 页面描述符:一页 = 一个 Flash const 结构(身份 + 几何 + 数据源 + 可选钩子)
* 由各 ui_page_xxx 页面模块提供;"哪个叶子进哪一页"的绑定不在本头,也不在框架 .c,
* 而在生成物 ui_page_registry.c(唯一真源 = 菜单树 ui_menu_tree.txt 的 #page= 注记)。
* 框架按描述符**指针**开页,不再有页 ID 枚举与 s_defs[] 下标表——
* 少一处"枚举/表/节点下标三处必须对齐"的手工同步点。
*/
typedef struct {
const char *id; /* 页面模块名(ASCII,日志/诊断用;= ui_page_<id> 的 <id>) */
const char *title; /* 标题(浅 V 区居中) */
const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */
int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */
int32_t title_y; /* 标题放置 y;0 = 框架默认(CP_TITLE_Y) */
int32_t title_y; /* 标题放置 y;0 = 框架默认(UI_TITLE_Y) */
uint8_t hide_deco; /* 1 = 不绘"浅 V"装饰(密码页跟随实机) */
uint8_t hide_footer; /* 1 = 不绘页脚页码(密码页跟随实机:无页脚) */
uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */
@@ -88,17 +84,18 @@ typedef struct {
/*
* 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器)
* 入口参数:id - 内容页 ID content_page_id_t
* 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password)
* const content_page_def_t *
* perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY;
* 由逻辑层按是否过密码页/所输密码决定,页面经
* ui_content_page_is_editable 查询)
* 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败)
* 返回值:true - 成功;false - 失败(desc 为 NULL / 已有页面打开 / 对象创建失败)
* 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在)
* 函数说明:1. 新建独立屏幕,全屏容器自绘全部内容(无子对象,池开销极小)
* 2. 容器入默认输入组并获焦——按键由本模块处理(↑↓移选/ESC返回/页面钩子)
* 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原
*/
bool ui_content_page_open(content_page_id_t id, uint8_t perm);
bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm);
/*
* 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面)