/* * 模块名称:UI Content Page (内容页框架) * 模块功能:菜单叶子节点进入的内容页(独立全屏页面)框架——全屏容器 * 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码),无子对象; * 只持机制:屏/容器按需建毁、焦点、按键分发、描述符驱动通用绘制; * 不持任何页面数据——各页数据/几何/钩子由 ui_page_xxx 模块经 * content_page_def_t 接入描述符表 s_defs[](架构见 内容页架构.md); * 进入时切屏并获焦、ESC 返回菜单屏并还原焦点; * 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21 实测几何) * 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义) * 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入 * ui_page_xxx 模块,本文件只留机制 + 描述符表;绘制/按键改 * 按描述符驱动(静态表/refresh + 4 钩子),行为零变化 * 2026-09-09 王建锋 密码页(用户密钥):绘制链支持每页 title_y/"浅 V"/页脚开关; * 按键分发支持每页 ← 拦截(ESC 恒不可拦截);新 API * ui_content_page_invalidate(表单页触发整页重绘) * 2026-09-09 王建锋 权限模型:open(id, perm) 记录授予权限(页面经 * ui_content_page_is_editable 查询:edit_capable 且修改权限) * 2026-09-10 王建锋 网口设置页(node 30):新增 CONTENT_PAGE_NET_CFG 入描述符表; * 新 API ui_content_page_selected_row(页面钩子感知选中行)+ * 描述符 init_cb(开页后调一次,页面模块重置运行态) * 2026-09-13 王建锋 cp_draw_cb 按区块拆为四个绘制子函数(cp_draw_deco/header/rows/footer), * 主函数仅编排(行数取一次供行窗口/页脚共用),行为零变化 */ /* 头文件包含区 */ #include #include "ui_content_page.h" #include "ui_page_device_info.h" #include "ui_page_ac.h" #include "ui_page_dc.h" #include "ui_page_password.h" #include "ui_page_net_cfg.h" #include "ui_pwd_def.h" #include "ui_menu_view.h" #include "ui_deco.h" #include "lvgl.h" #define DBG_TAG "[UI_CONTENT_PAGE]" #include "dbg_log.h" /* 私有宏定义区 */ /* * 布局常量(设备像素;参考图 10/20/21 实测,3x 放大图缩回 160x160 测量): * 标题 y=2(CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143; * 字形顶距(CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐 */ #define CP_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ #define CP_SCREEN_H 160 /* 屏幕高(px) */ #define CP_TITLE_Y 2 /* 标题放置 y(居中;墨迹自 y=3 起) */ #define CP_PAGE_LINE_Y 143 /* 页脚"第 X 页,共 Y 页"放置 y(x=6 左对齐) */ #define CP_ROW_BUF_LEN 32 /* 动态行栈缓冲长度(同帧同步光栅化;超出行带即被裁切) */ /* 静态变量定义区 */ /* * 描述符表:页 ID → 页面模块描述符(每页一行;新增页 = 枚举值 + 此处一行, * 框架机制不变,见 内容页架构.md §七) */ static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = { [CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc, [CONTENT_PAGE_AC] = &ui_page_ac_desc, [CONTENT_PAGE_DC] = &ui_page_dc_desc, [CONTENT_PAGE_PASSWORD] = &ui_page_password_desc, [CONTENT_PAGE_NET_CFG] = &ui_page_net_cfg_desc, }; static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */ static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续) */ static uint8_t s_perm; /* 本次授予权限(ui_perm_t;开页时装载,关页重置) */ static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */ static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */ /* flat:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的 card 样式(同 ui_menu_view.c) */ /* 用常量样式(LV_STYLE_CONST_INIT):属性存静态数组,零 LV_MEM 池分配; * 动态样式首次 set_prop 会从池分配 values_and_props 且永不释放(x64 实测 88B/个) */ static const lv_style_const_prop_t cp_flat_props[] = { { .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } }, { .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } }, { .prop = LV_STYLE_RADIUS, .value = { .num = 0 } }, { .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } }, { .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } }, { .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } }, { .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } }, { .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } }, { .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } }, LV_STYLE_CONST_PROPS_END }; LV_STYLE_CONST_INIT(cp_style_flat, cp_flat_props); /* 函数定义区 */ /* * 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头) * 入口参数:a - 区域 1 const lv_area_t * * b - 区域 2 const lv_area_t * * 返回值:true - 相交;false - 不相交 * 限定条件:无 * 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_menu_view.c 同实现) */ static bool cp_areas_intersect(const lv_area_t *a, const lv_area_t *b) { int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1; int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1; int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2; int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2; return (x1 <= x2 && y1 <= y2); } /* * 函数功能:取页面总行数(静态页 = 描述符 row_count,动态页 = count_cb) * 入口参数:d - 页面描述符 const content_page_def_t * * 返回值:总行数 uint16_t * 限定条件:d 非空 * 函数说明:行数唯一入口;绘制(窗口/页脚)与按键(移选取模)均经此取 */ static uint16_t cp_row_total(const content_page_def_t *d) { if (d->count_cb != NULL) { return d->count_cb(); } return d->row_count; } /* * 函数功能:失效当前打开的页面容器(触发整页重绘) * 入口参数:无 * 返回值:无 * 限定条件:页面打开时(s_cont 非空) * 函数说明:供页面模块(表单/操作页)自身状态变化后调用;无页打开时空操作 */ void ui_content_page_invalidate(void) { if (s_cont != NULL) { lv_obj_invalidate(s_cont); } } /* * 函数功能:绘制居中单行文本(标题/副标题) * 入口参数:layer - 绘制层 lv_layer_t * * text - 文本(Flash) const char * * y - 放置 y int32_t * 返回值:无 * 限定条件:text 非空;重绘区已含该行区域(调用方 culling) * 函数说明:x = (屏宽 - 文本实际步进)/2 居中;白色、静态字符串不拷内存 */ static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y) { lv_area_t area; lv_draw_label_dsc_t ld; area.x1 = 0; area.x2 = CP_SCREEN_W - 1; area.y1 = y; area.y2 = y + 15; /* 覆盖字体行高(13)+ 余量 */ lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = text; ld.text_static = 1; /* Flash 字符串:不拷内存 */ ld.color = lv_color_white(); /* 本 LVGL 版 lv_text_get_width 的 length=0 表示"不测量",必须传 strlen */ ld.ofs_x = (CP_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2; ld.ofs_y = 0; lv_draw_label(layer, &ld, &area); } /* * 函数功能:绘制"浅 V"装饰(共享 ui_deco 模块,与菜单根框同几何) * 入口参数:layer - 绘制层 lv_layer_t * * d - 页面描述符 const content_page_def_t * * 返回值:无 * 限定条件:页面已打开 * 函数说明:每页可关(hide_deco;密码页跟随实机:无装饰) */ static void cp_draw_deco(lv_layer_t *layer, const content_page_def_t *d) { if (d->hide_deco == 0u) { ui_deco_draw(layer, &layer->_clip_area); } } /* * 函数功能:绘制标题/副标题(居中;整宽带 + culling) * 入口参数:layer - 绘制层 lv_layer_t * * d - 页面描述符 const content_page_def_t * * 返回值:无 * 限定条件:页面已打开 * 函数说明:标题放置 y 每页可配(title_y,0 = 框架默认 CP_TITLE_Y); * 副标题仅信息页(subtitle 非空);重绘区不相交则跳过区块 */ static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) { lv_area_t t; /* 标题(居中;放置 y 每页可配,0 = 框架默认) */ { int32_t ty = (d->title_y != 0) ? d->title_y : CP_TITLE_Y; t.x1 = 0; t.x2 = CP_SCREEN_W - 1; t.y1 = ty; t.y2 = ty + 15; if (cp_areas_intersect(&t, &layer->_clip_area)) { cp_draw_centered_text(layer, d->title, ty); } } /* 副标题(仅信息页) */ if (d->subtitle != NULL) { t.x1 = 0; t.x2 = CP_SCREEN_W - 1; t.y1 = d->subtitle_y; t.y2 = d->subtitle_y + 15; if (cp_areas_intersect(&t, &layer->_clip_area)) { cp_draw_centered_text(layer, d->subtitle, d->subtitle_y); } } } /* * 函数功能:绘制行数据(列表页窗口跟随选中行所在页,信息页全量;逐行 culling) * 入口参数:layer - 绘制层 lv_layer_t * * d - 页面描述符 const content_page_def_t * * nrows - 总行数 int32_t(调用方取一次,与页脚同帧共用) * 返回值:无 * 限定条件:页面已打开 * 函数说明: 1. 列表页窗口顶行 = (sel/每页行数)*每页行数,末页不足整页截断 * 2. 行条带(整宽)不在重绘区则跳过(culling) * 3. 选中行:整行白带(x=2..156,高 band_h)+ 黑字,非选中白字 * 4. 行文本:静态 = Flash 表(不拷内存);动态 = refresh() 逐行取 * 入栈缓冲(同帧同步光栅化,安全) */ static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) { int32_t first = 0; int32_t cnt = nrows; if (d->selectable != 0u && d->rows_per_page > 0u) { first = (int32_t)(s_sel / d->rows_per_page) * d->rows_per_page; /* 窗口顶行 */ cnt = nrows - first; if (cnt > (int32_t)d->rows_per_page) { cnt = (int32_t)d->rows_per_page; /* 末页不足整页时截断 */ } } bool dynamic = (d->refresh != NULL); char dyn_name[CP_ROW_BUF_LEN]; /* 动态行栈缓冲(同帧同步光栅化) */ char dyn_value[CP_ROW_BUF_LEN]; for (int32_t i = 0; i < cnt; i++) { int32_t row_idx = first + i; /* 数据下标(跨页连续) */ int32_t slot_y = d->top_y + i * d->pitch; /* 行槽顶(窗口内位置固定) */ bool is_sel = (d->selectable != 0u) && (row_idx == (int32_t)s_sel); lv_color_t color = is_sel ? lv_color_black() : lv_color_white(); lv_area_t strip; /* 行条带(整宽)不在重绘区:跳过(culling) */ strip.x1 = 0; strip.x2 = CP_SCREEN_W - 1; strip.y1 = slot_y; strip.y2 = slot_y + d->pitch - 1; if (!cp_areas_intersect(&strip, &layer->_clip_area)) { continue; } /* 选中行:整行白带(x=2..156,高 band_h) */ if (is_sel && d->band_h > 0) { lv_draw_rect_dsc_t fd; lv_area_t band; band.x1 = 2; band.x2 = CP_SCREEN_W - 3; /* x=156(与参考图实测一致) */ band.y1 = slot_y; band.y2 = slot_y + d->band_h - 1; lv_draw_rect_dsc_init(&fd); fd.base.layer = layer; fd.bg_color = lv_color_white(); fd.bg_opa = LV_OPA_COVER; lv_draw_rect(layer, &fd, &band); } /* 行文本:静态 = Flash 表(不拷内存);动态 = refresh 逐行取(栈缓冲) */ const char *name; const char *value; if (dynamic) { d->refresh((uint16_t)row_idx, dyn_name, dyn_value); name = dyn_name; value = dyn_value; } else { name = d->rows[row_idx].name; value = d->rows[row_idx].value; } { lv_draw_label_dsc_t ld; /* ofs_x/ofs_y 相对 coords 原点(strip.y1=slot_y),勿再叠加 slot_y */ lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = name; ld.text_static = (uint32_t)(!dynamic); /* 静态不拷;动态缓冲同帧内有效 */ ld.color = color; /* 选中黑 / 非选中白 */ ld.ofs_x = d->left_x; ld.ofs_y = d->text_dy; lv_draw_label(layer, &ld, &strip); lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = value; ld.text_static = (uint32_t)(!dynamic); ld.color = color; ld.ofs_x = d->right_x; ld.ofs_y = d->text_dy; lv_draw_label(layer, &ld, &strip); } } } /* * 函数功能:绘制页脚"第 X 页,共 Y 页" * 入口参数:layer - 绘制层 lv_layer_t * * d - 页面描述符 const content_page_def_t * * nrows - 总行数 int32_t(调用方取一次,与行窗口同帧同值) * 返回值:无 * 限定条件:页面已打开 * 函数说明:列表页随选中行现算,信息页按参考图固定值;每页可关 * (hide_footer;密码页跟随实机) */ static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) { char buf[32]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */ uint8_t cur; /* 当前页码(1 基) */ uint8_t pages; /* 总页数 */ lv_area_t t; /* 页脚条带区域(整宽,放置于 CP_PAGE_LINE_Y) */ /* 页码计算,两种来源: * - 列表页(selectable):随选中行现算——当前页 = 选中行/每页行数 + 1 * (选中行所在页);总页数 = ceil(nrows/每页行数), * 写成经典向上取整 (n + p - 1) / p(nrows 恰为整页倍数时不多算一页); * nrows 由调用方取一次、与行窗口共用,保证同帧页码与窗口一致 * - 信息/表单页(非 selectable):取参考图固定值(fixed_cur_page / fixed_total_pages) */ if (d->selectable != 0u) { cur = (uint8_t)(s_sel / d->rows_per_page + 1u); pages = (uint8_t)((nrows + (int32_t)d->rows_per_page - 1) / d->rows_per_page); } else { cur = d->fixed_cur_page; pages = d->fixed_total_pages; } /* 页脚条带:整宽,放置于 CP_PAGE_LINE_Y(143,字形顶距已计入放置 y, * 墨迹与参考图对齐);高 15 = 字体行高 13 + 余量,整行文字一次覆盖 */ t.x1 = 0; t.x2 = CP_SCREEN_W - 1; t.y1 = CP_PAGE_LINE_Y; t.y2 = CP_PAGE_LINE_Y + 15; /* 双重门控:① 本页开启页脚(hide_footer;密码页跟随实机:无页码) * ② 页脚条带与重绘区相交(culling)——两者皆成立才格式化并绘制 */ if (d->hide_footer == 0u && cp_areas_intersect(&t, &layer->_clip_area)) { /* lv_snprintf 而非 snprintf:MSVC 对 printf 族做格式串扫描,中文字面量会误报 C4819 */ lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)pages); lv_draw_label_dsc_t ld; /* ofs 相对 coords 原点(t.x1=0,t.y1=CP_PAGE_LINE_Y): * 水平 6 = 左对齐、左留白 6px(与实机一致);垂直 0(顶距已计入放置 y) */ lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = buf; /* 栈缓冲:本页码每次现算,同帧内有效(同步光栅化) */ ld.color = lv_color_white(); /* 页码恒白字(不像选中行那样反色) */ ld.ofs_x = 6; ld.ofs_y = 0; lv_draw_label(layer, &ld, &t); } } /* * 函数功能:页面自绘(编排:浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码 + 页面钩子) * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:事件为 LV_EVENT_DRAW_MAIN,页面已打开 * 函数说明:行数取一次(cp_row_total)供行窗口与页脚页码共用(同帧同值); * 各区块由子函数绘制(deco/header/rows/footer),均按 culling 区 * 跳过不在重绘区的部分;末尾调 draw_cb(页自定义内容,在内置区块之后) */ static void cp_draw_cb(lv_event_t *e) { const content_page_def_t *d = s_defs[s_open]; lv_layer_t *layer = lv_event_get_layer(e); int32_t nrows = (int32_t)cp_row_total(d); /* 行数取一次:供行窗口与页脚页码共用 */ cp_draw_deco(layer, d); cp_draw_header(layer, d); cp_draw_rows(layer, d, nrows); cp_draw_footer(layer, d, nrows); /* 页面钩子:自定义内容(表单/操作页;在内置区块之后绘制) */ if (d->draw_cb != NULL) { d->draw_cb(layer, &layer->_clip_area); } } /* * 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回 / 页面钩子) * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:容器已加入默认输入组并获焦 * 函数说明: 1. ESC:恒关页返回菜单(框架保证,页面钩子不可拦截) * 2. ←:页面置拦截标志(key_intercept)时先交 key_cb(true = 页已处理不关页), * 否则关页返回(密码页光标 ←/→ 即由此处理) * 3. 其余键:key_cb 钩子先调(返回 true = 页已处理,框架跳过内置) * 4. 内置:↑/↓ 列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘; * 信息页不响应;动态行数可能缩小,选中行越界时重置回首行 */ static void cp_key_cb(lv_event_t *e) { const content_page_def_t *d = s_defs[s_open]; lv_key_t key = (lv_key_t)lv_event_get_key(e); /* ESC:关闭页面返回菜单屏(焦点/导航状态还原;恒不可拦截) */ if (key == LV_KEY_ESC) { ui_content_page_close(); return; } /* ←:页面置拦截标志且钩子已处理则不关页(密码页光标 ←/→);否则照旧返回 */ if (key == LV_KEY_LEFT) { if (d->key_intercept != 0u && d->key_cb != NULL && d->key_cb(key)) { return; } ui_content_page_close(); return; } /* 其余键:页面钩子先调:true = 页已处理,框架跳过内置语义 */ if (d->key_cb != NULL && d->key_cb(key)) { return; } switch (key) { case LV_KEY_UP: { uint16_t total = cp_row_total(d); if (d->selectable != 0u && total > 0u) { if (s_sel >= total) { s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */ } /* 上移、首尾环绕:先加 total 再减 1 保证无符号下不为负 */ s_sel = (uint16_t)((s_sel + total - 1u) % total); lv_obj_invalidate(s_cont); /* 重绘(选中带/窗口/页码可能变化) */ } break; } case LV_KEY_DOWN: { uint16_t total = cp_row_total(d); if (d->selectable != 0u && total > 0u) { if (s_sel >= total) { s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */ } s_sel = (uint16_t)((s_sel + 1u) % total); /* 下移、越尾绕回首行 */ lv_obj_invalidate(s_cont); } break; } default: break; /* 其余键值忽略 */ } } /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) * 入口参数:id - 内容页 ID content_page_id_t * perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY) * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象) * 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管 * 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原 * 4. 记录授予权限(页面经 ui_content_page_is_editable 查询) */ bool ui_content_page_open(content_page_id_t id, uint8_t perm) { if (id >= CONTENT_PAGE_COUNT || s_open != CONTENT_PAGE_COUNT) { return false; /* ID 无效或已有页面打开(防御) */ } const content_page_def_t *d = s_defs[id]; if (d == NULL) { DBG_ERROR("page open: id=%u no descriptor (enum/table drift)", (unsigned)id); return false; /* 枚举/描述符表漂移:拒开并留痕,不崩溃 */ } s_perm = perm; /* 本次授予权限(页面可编辑性 = edit_capable 且修改权限) */ s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕 */ if (s_scr == NULL) { return false; /* LVGL 池耗尽 */ } s_cont = lv_obj_create(s_scr); if (s_cont == NULL) { lv_obj_delete(s_scr); /* 回滚:屏已建、容器失败 */ s_scr = NULL; return false; } lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级布局 */ lv_obj_set_size(s_cont, CP_SCREEN_W, CP_SCREEN_H); /* 全屏覆盖 */ lv_obj_add_style(s_cont, &cp_style_flat, 0); /* 透明、无边框:剥离主题 card */ lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */ lv_obj_add_event_cb(s_cont, cp_key_cb, LV_EVENT_KEY, NULL); lv_obj_add_event_cb(s_cont, cp_draw_cb, LV_EVENT_DRAW_MAIN, NULL); s_open = id; s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */ if (d->init_cb != NULL) { d->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */ } /* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */ lv_group_t *g = lv_group_get_default(); if (g != NULL) { lv_group_add_obj(g, s_cont); lv_group_focus_obj(s_cont); } lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */ DBG_INFO("page open: id=%u rows=%u", (unsigned)id, (unsigned)cp_row_total(d)); return true; } /* * 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面) * 入口参数:无 * 返回值:无 * 限定条件:ui_content_page_is_open() 为 true * 函数说明:1. 先移焦到菜单容器再删屏——防输入组焦点悬空(删除自动移出组) * 2. 切回菜单屏后销毁页面屏幕,LVGL 池内存随即回落 */ void ui_content_page_close(void) { if (s_open == CONTENT_PAGE_COUNT || s_cont == NULL) { return; /* 未打开(防御) */ } lv_obj_t *menu_cont = ui_menu_view_container(); if (menu_cont != NULL) { lv_group_focus_obj(menu_cont); /* 焦点先回菜单容器 */ lv_screen_load(lv_obj_get_screen(menu_cont)); /* 切回菜单屏(对象未动,原样还原) */ } DBG_INFO("page close: id=%u", (unsigned)s_open); lv_obj_delete(s_cont); /* 先删容器 */ lv_obj_delete(s_scr); /* 再删屏 */ s_scr = NULL; s_cont = NULL; s_open = CONTENT_PAGE_COUNT; s_sel = 0u; s_perm = (uint8_t)UI_PERM_VIEW; /* 权限重置(卫生:不留上次进入的授权) */ } /* * 函数功能:查询是否有内容页打开 * 入口参数:无 * 返回值:true - 有页面打开;false - 无 * 限定条件:无 * 函数说明:供逻辑层/调试判断当前处于菜单屏还是内容页 */ bool ui_content_page_is_open(void) { return (s_open != CONTENT_PAGE_COUNT); } /* * 函数功能:查询当前打开的页面是否可编辑 * 入口参数:无 * 返回值:true - 页面支持编辑模式(edit_capable)且被授予修改权限;false - 其余 * 限定条件:无 * 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 open 的 perm 实参授予 */ bool ui_content_page_is_editable(void) { if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) { return false; /* 无页打开或无描述符(防御) */ } return (s_defs[s_open]->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY); } /* * 函数功能:查询当前列表页的选中行(全局下标,跨页连续) * 入口参数:无 * 返回值:选中行下标;无页打开/非列表页 = 0 uint16_t * 限定条件:无 * 函数说明:供页面钩子(key_cb/draw_cb)感知当前选中位置 */ uint16_t ui_content_page_selected_row(void) { if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) { return 0u; } if (s_defs[s_open]->selectable == 0u) { return 0u; /* 信息页/表单页无行选中概念 */ } return s_sel; }