From 32d423b759433ec270ff575331337b228e37f676 Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Sat, 10 Oct 2026 17:33:59 +0800 Subject: [PATCH] u --- code/fms/FMS元数据驱动架构重构方案.md | 215 ++++++ code/fms/模块管理简化与架构收敛方案.md | 317 --------- code/g3soft-erp/docs/前端架构设计.md | 5 + .../web/src/composables/useFullscreen.ts | 124 ++++ code/g3soft-erp/web/src/constants/menu.ts | 12 +- .../web/src/layouts/DefaultLayout.vue | 202 ++++-- .../web/src/layouts/LayoutHeader.vue | 129 +++- .../web/src/layouts/LayoutHeaderMenu.vue | 229 ++++--- .../web/src/layouts/LayoutMixedColumn.vue | 202 ++++++ .../web/src/layouts/LayoutMixedRail.vue | 186 +++++ .../web/src/layouts/LayoutSidebar.vue | 571 ++++++++-------- .../web/src/layouts/LayoutTabbar.vue | 443 ++++++++++-- .../web/src/layouts/SidebarMenuItem.vue | 134 +++- .../web/src/layouts/SidebarPopupItem.vue | 73 -- .../web/src/layouts/useMixedMenu.ts | 178 +++++ .../web/src/layouts/useSidebarMenu.ts | 28 +- .../components/PreferencesDrawer.vue | 357 +++++++--- .../src/preferences/components/SwitchItem.vue | 10 +- .../src/preferences/components/ToggleItem.vue | 2 + .../layout-icons/LayoutHeaderNav.vue | 64 ++ .../layout-icons/LayoutSidebarMixedNav.vue | 83 +++ .../layout-icons/LayoutSidebarNav.vue | 68 ++ code/g3soft-erp/web/src/preferences/config.ts | 16 +- code/g3soft-erp/web/src/preferences/index.ts | 2 - code/g3soft-erp/web/src/preferences/store.ts | 8 +- code/g3soft-erp/web/src/preferences/types.ts | 52 +- code/g3soft-erp/web/src/router/index.ts | 30 +- .../web/src/router/modules/system.ts | 6 + code/g3soft-erp/web/src/stores/app.ts | 107 ++- code/g3soft-erp/web/src/styles/global.scss | 10 +- .../web/src/views/system/KeepAliveTest.vue | 80 +++ code/g3soft-libs/docs/.vitepress/config.mts | 1 + .../docs/content/ui/form/color-picker.md | 115 ++++ code/g3soft-libs/docs/content/ui/usage/rtl.md | 7 + .../docs/examples/color-picker/basic.vue | 11 + .../docs/examples/color-picker/presets.vue | 32 + .../docs/examples/color-picker/size.vue | 16 + .../packages/tokens/src/scss/theme.scss | 8 + .../ui/e2e/docs-hover-minimal.spec.ts | 94 +++ .../packages/ui/e2e/erp-layout-cards.spec.ts | 76 +++ .../ui/e2e/erp-mixed-nav-fixes.spec.ts | 135 ++++ .../packages/ui/e2e/erp-mixed-nav.spec.ts | 109 +++ .../ui/e2e/erp-mixed-rail-active.spec.ts | 135 ++++ .../packages/ui/e2e/erp-tabbar-reveal.spec.ts | 226 +++++++ code/g3soft-libs/packages/ui/e2e/rtl.spec.ts | 43 ++ .../packages/ui/playground/main.ts | 18 + .../packages/ui/src/_utils/color-convert.ts | 121 ++++ .../ui/src/color-picker/ColorPicker.vue | 639 ++++++++++++++++++ .../packages/ui/src/color-picker/index.ts | 2 + .../packages/ui/src/color-picker/types.ts | 30 + .../packages/ui/src/drawer/Drawer.vue | 6 +- .../packages/ui/src/dropdown/Dropdown.vue | 91 ++- .../src/dropdown/DropdownHoverFlash.test.ts | 128 ++++ .../packages/ui/src/dropdown/DropdownMenu.vue | 53 +- .../ui/src/dropdown/DropdownSubmenu.test.ts | 134 ++++ .../packages/ui/src/dropdown/context.ts | 12 +- .../packages/ui/src/dropdown/types.ts | 5 + code/g3soft-libs/packages/ui/src/index.ts | 3 + .../packages/ui/src/modal/Modal.vue | 6 +- 59 files changed, 5101 insertions(+), 1098 deletions(-) create mode 100644 code/fms/FMS元数据驱动架构重构方案.md delete mode 100644 code/fms/模块管理简化与架构收敛方案.md create mode 100644 code/g3soft-erp/web/src/composables/useFullscreen.ts create mode 100644 code/g3soft-erp/web/src/layouts/LayoutMixedColumn.vue create mode 100644 code/g3soft-erp/web/src/layouts/LayoutMixedRail.vue delete mode 100644 code/g3soft-erp/web/src/layouts/SidebarPopupItem.vue create mode 100644 code/g3soft-erp/web/src/layouts/useMixedMenu.ts create mode 100644 code/g3soft-erp/web/src/preferences/components/layout-icons/LayoutHeaderNav.vue create mode 100644 code/g3soft-erp/web/src/preferences/components/layout-icons/LayoutSidebarMixedNav.vue create mode 100644 code/g3soft-erp/web/src/preferences/components/layout-icons/LayoutSidebarNav.vue create mode 100644 code/g3soft-erp/web/src/views/system/KeepAliveTest.vue create mode 100644 code/g3soft-libs/docs/content/ui/form/color-picker.md create mode 100644 code/g3soft-libs/docs/examples/color-picker/basic.vue create mode 100644 code/g3soft-libs/docs/examples/color-picker/presets.vue create mode 100644 code/g3soft-libs/docs/examples/color-picker/size.vue create mode 100644 code/g3soft-libs/packages/ui/e2e/docs-hover-minimal.spec.ts create mode 100644 code/g3soft-libs/packages/ui/e2e/erp-layout-cards.spec.ts create mode 100644 code/g3soft-libs/packages/ui/e2e/erp-mixed-nav-fixes.spec.ts create mode 100644 code/g3soft-libs/packages/ui/e2e/erp-mixed-nav.spec.ts create mode 100644 code/g3soft-libs/packages/ui/e2e/erp-mixed-rail-active.spec.ts create mode 100644 code/g3soft-libs/packages/ui/e2e/erp-tabbar-reveal.spec.ts create mode 100644 code/g3soft-libs/packages/ui/src/_utils/color-convert.ts create mode 100644 code/g3soft-libs/packages/ui/src/color-picker/ColorPicker.vue create mode 100644 code/g3soft-libs/packages/ui/src/color-picker/index.ts create mode 100644 code/g3soft-libs/packages/ui/src/color-picker/types.ts create mode 100644 code/g3soft-libs/packages/ui/src/dropdown/DropdownHoverFlash.test.ts create mode 100644 code/g3soft-libs/packages/ui/src/dropdown/DropdownSubmenu.test.ts diff --git a/code/fms/FMS元数据驱动架构重构方案.md b/code/fms/FMS元数据驱动架构重构方案.md new file mode 100644 index 00000000..e5fbb627 --- /dev/null +++ b/code/fms/FMS元数据驱动架构重构方案.md @@ -0,0 +1,215 @@ +# FMS 元数据驱动架构整体重构方案 + +> 文档版本:V0.1(方案草案) +> 编制日期:2026-10-10 +> 范围:元数据模型、模块配置、模块运行时及相关数据库结构 +> 状态:待评审,尚未实施 + +## 1. 结论摘要 + +本次重构保留并强化**元数据驱动**这一核心概念。模块定义、字段语义、关系、页面视图和通用行为仍由元数据描述,通用运行时读取已发布的元数据运行。 + +项目属于草稿 / Demo 阶段,不要求兼容现有元数据表结构和配置 JSON。可以重做元数据数据库,并整体替换配置和运行时主链路;不需要新旧格式长期双写或并行维护。 + +```text +物理数据源与表结构 → 模块元数据 → 模板 / 默认值 → 校验 / 编译 / 发布 + ↓ + 统一模块运行时 → SQL 业务表 / 视图 +``` + +SQL Server 中的真实业务表和视图继续作为业务数据事实来源。元数据描述如何理解、呈现和操作这些数据;不采用 EAV 存储所有业务记录,也不让元数据引擎任意生成业务表。 + +## 2. 重构目标与边界 + +- 元数据仍是通用模块的运行时核心,不退化为页面备注或少量展示配置。 +- 常见模块通过数据源、模板和少量字段选择即可完成基础配置。 +- 一个字段只定义一次;列表、表单、查询、详情等视图引用字段,不重复维护语义。 +- 默认配置可推导、可重复生成;用户明确调整过的配置不被覆盖。 +- 元数据发布前经过统一校验,运行时消费同一份有效定义。 +- 元数据数据库可以整体重做,不受 `s_module`、`s_field`、`s_module_schema` 现有结构约束。 +- 复杂查询和业务动作保留 SQL / 后端扩展出口,不强迫所有逻辑都配置化。 +- 不将所有字段值改为 EAV 或通用 JSON;业务表仍通过 SQL migration / 数据库变更管理。 +- 不建设可执行任意脚本的规则引擎,不把库存、财务、结算等复杂事务塞进配置。 +- 不为本次目标无差别重写认证、文件、导出等稳定模块,只调整必要集成点。 +- 不要求兼容旧元数据格式;需要保留的 Demo 数据可一次性导入,否则重建 / 播种。 + +## 3. 总体架构 + +```text +┌──────────────────── 配置面 ────────────────────┐ +│ 元数据工作台 → 草稿定义 → 校验 / 预览 → 发布版本 │ +└────────────────────────┬───────────────────────┘ + ↓ + 元数据目录 / 发布快照 + ↓ +┌──────────────────── 运行面 ────────────────────┐ +│ 元数据解析与缓存 → 查询 / 保存 / 权限 / 页面渲染 │ +└────────────────────────┬───────────────────────┘ + ↓ + SQL Server 表、视图与只读查询 SQL + ↓ + 注册的后端业务动作及工作流扩展 +``` + +### 配置面 + +工作台负责选择数据源、导入物理字段、指定业务语义、应用模板、调整视图并预览。编辑内容为草稿,通过校验后才能发布。 + +### 元数据编译与校验 + +后端维护权威校验与解析逻辑。发布时将模块定义、模板默认值和用户覆盖项合成为有效定义,并检查字段引用、数据源、视图、关系、查询条件和动作引用。前端预览使用后端返回的有效定义,避免前后端分别推导默认规则。 + +### 运行面 + +通用运行时负责适合通用化的列表、过滤、排序、分页、字段呈现、标准保存和权限约束。查询值参数化;自定义查询入口仅用于只读查询。多表事务、库存过账、财务结算等动作由后端注册的业务处理器执行。元数据声明动作入口和参数契约,不保存可执行代码。 + +## 4. 元数据逻辑模型 + +运行时使用统一逻辑对象 `ModuleDefinition`。它是稳定契约,不要求与单张数据库表或单个 JSON 一一对应。 + +### 模块 + +包含稳定编码、名称、多语言键、类型、状态、组织层级、查询数据源、标准保存目标 / 主键、字段、关系、视图、动作引用、模板和发布版本。模块类型收敛为少数明确类别,如标准数据、只读查询、业务动作模块。 + +### 字段 + +区分物理事实和业务语义: + +- **物理事实**:来源表 / 视图、列名、SQL 类型、长度、精度、可空性等,优先由数据库结构检查得到,不重复手工维护。 +- **业务语义**:标签、多语言键、语义类型、关联目标、是否可编辑 / 列表展示 / 查询 / 排序 / 导出等,由元数据表达。 +- **界面属性**:控件、宽度、格式等仅在需要覆盖默认值时配置。 + +字段使用模块内稳定编码;视图引用字段编码,不复制物理属性和业务语义。 + +### 关系、视图与行为 + +- 关系元数据表达主从、引用和多对多关系,包含源 / 目标模块字段及规则,发布时校验完整性。 +- 表单、列表、查询、详情是不同视图类型,只保存顺序、分组、布局、列宽、排序、操作符等视图特有信息;布局采用可读、可校验的结构化定义,不保存画布坐标。 +- 字段参与视图的默认状态由字段能力、模板和默认策略推导;个性化差异作为显式覆盖。 +- 简单校验和通用 CRUD 可由元数据表达;复杂业务动作映射到稳定的后端处理器编码。 +- 工作流、权限、公式等能力通过稳定引用接入,不重复定义模块字段。 + +## 5. 元数据数据库重构 + +以下是逻辑实体职责,最终表名和列定义在元数据契约评审后确定。可重做元数据表结构,但业务记录仍存放于真实 SQL 表。 + +| 逻辑实体 | 职责 | 建议存储 | +|---|---|---| +| 模块 | 身份、类型、数据源、状态、组织关系 | 规范化关系表 | +| 模块字段 | 字段编码、物理映射、业务语义、能力标记 | 规范化关系表;模块内编码唯一 | +| 模块关系 | 关系类型及源 / 目标字段 | 规范化关系表;发布时校验引用 | +| 模块视图 | 表单、列表、查询、详情定义 | 按模块和视图类型区分;灵活布局可用 JSON | +| 模板 | 默认字段能力、视图结构、推荐规则 | 可版本化定义 | +| 发布版本 | 不可变快照、版本号、校验结果、发布时间 | 版本记录;运行时按已发布版本读取 | +| 动作注册 | 动作编码与后端处理器参数契约 | 只存稳定编码和契约,不存可执行代码 | + +### 对现有核心表的评估 + +**结论:需要重构元数据配置表,但不需要仅因配置器难用而整体重做所有业务数据表。** 当前表结构能表达基本模块、字段和关系;主要问题是职责混放、字段能力分散到视图 JSON,以及缺少可发布的版本边界。 + +- **`s_module`**:当前模块身份 / 树结构与查询表、保存表、主键、数据范围字段、默认排序 SQL、查询 SQL 和扩展 JSON 放在同一表。建议将模块身份与数据源 / 查询定义分开建模;移除缺少强约束的通用 `b_config_json`,把常用配置变为有类型的字段或关联实体。模块树优先只保留权威父级和同级顺序;`depth/path` 仅在查询性能有证据需要时保留为派生索引。模块草稿 / 发布时间应由版本模型管理,不靠一个模块级更新时间代表所有子配置状态。 +- **`s_field`**:当前主要保存名称、业务类型、启用和顺序,尚未明确区分物理列、查询别名、计算 / 虚拟字段。建议增加明确的字段来源种类与来源映射、语义类型和模块级能力;数据库物理类型由 SQL Server 结构检查获得。表单 / 列表 / 查询中的显示顺序和控件差异放到对应视图字段配置,不塞回字段主定义。 +- **`s_module_schema`**:当前每模块按 `view/edit/query` 保存整份 `nvarchar(max)` JSON,便于早期迭代,但字段参与、字段语义和复杂布局容易混在一起。既然不采用拖拽画布,建议将它替换为视图定义 + 结构化的视图字段 / 分组配置;常用属性使用有类型的列,只有少量确实灵活的扩展留在受校验 JSON。表单不保存行列像素坐标,列表不把所有列属性藏在任意 JSON 节点中。 +- **`s_relation`**:保留关系作为独立元数据,但建议引入稳定关系编码 / 标识,显式描述关系用途、基数、展示 / 加载规则,并对模块和字段引用建立数据库约束。现有删除策略等已表达的业务含义应迁移保留,不因改表而丢失。 +- **`s_user_module_pref`**:如保留个性化配置,只允许保存列表列、查询布局等用户偏好覆盖,不得改变模块字段语义、数据源、权限或保存行为。若 Demo 暂无真实个性化需求,可先不纳入新核心模型。 +- **新增发布版本实体**:保存草稿与已发布版本的边界、版本号、校验摘要和不可变运行快照;发布失败不替换当前有效版本。 + +元数据表之间应使用主键、唯一约束和可表达的外键保障内部引用完整性,并用发布校验补足跨 JSON / 动态数据源的检查。不要为了引用元数据而给所有动态业务表强行建立数据库外键。 + +### 规范化与 JSON 边界 + +- 模块、字段、关系、版本等有稳定身份、查询和引用的对象采用关系表。 +- 表单布局、列表列布局、查询 UI 等灵活结构可存 JSON,但其中只能引用已定义字段和动作。 +- 发布快照可采用 JSON 作为编译产物,不作为编辑态唯一事实来源。 +- 对模块编码、模块内字段编码、模块 / 视图类型、版本号等建立唯一约束;可表达的元数据引用建立外键或等效约束,并由发布校验再次检查。 +- 明确删除、改名和关系变更策略,不依赖前端操作顺序保证一致性。 +- 不将业务字段值存入 `entity_id / field_id / value` 三元 EAV 表;不把所有配置塞进单个无约束 JSON。 +- 不复制 SQL 物理列定义后再要求长期人工同步;物理结构优先读取,元数据维护业务语义。 + +### 草稿、发布与回滚 + +建议区分编辑草稿与当前发布版本。草稿保存不改变运行中配置;发布时完成校验、生成不可变快照并原子切换当前版本。保留基本发布历史用于问题定位和回滚。Demo 第一版不建设复杂审批、多用户锁或多环境发布平台。 + +## 6. 配置体验 + +配置流程采用“默认优先、逐步展开”: + +1. 选择模块模板和数据源类型。 +2. 选择表 / 视图,读取物理字段并映射业务语义。 +3. 确认字段能力,自动生成表单、列表、查询初始定义。 +4. 预览常用页面,再按需进入结构化的高级视图配置。 +5. 运行发布校验,通过后发布。 + +首批模板建议包含基础资料、单据主表、明细表、只读查询 / 报表。模板提供可编辑初始值,不是硬编码页面。 + +### 不采用拖拽式设计器 + +表单和列表不再采用画布式拖拽设计器,也不保留绝对坐标 / 自由定位配置。拖拽交互难以精确、重复配置,布局结构也不利于阅读、差异比较和长期维护。 + +- **表单**:采用模板、分组和字段清单配置。通过字段选择、分组设置、列数 / 排序等结构化属性定义布局;字段顺序使用排序值或批量排序操作,不依赖画布拖动。 +- **列表**:采用列配置表 / 字段矩阵,集中设置是否显示、顺序、宽度、格式、对齐、排序和汇总等属性;支持批量添加、移除和套用模板。 +- **预览**:配置面板旁提供实时预览,但预览仅用于检查结果,不作为拖放编辑画布。 +- **元数据**:保存语义结构和字段引用,不保存像素坐标、拖拽路径或依赖编辑器内部实现的状态。 +- **高级场景**:通过分组、布局模板、字段显示规则及明确的扩展配置解决;不以重新引入自由拖拽作为兜底。 + +默认规则: + +- 根据 SQL 类型推导候选字段类型和控件;语义不明确时要求确认,不做危险猜测。 +- 根据业务语义推导筛选组件、操作符、格式和建议列宽。 +- 新增字段补默认配置;已显式调整的配置不被重置;重置需显式操作。 +- 默认生成必须幂等,同一输入重复应用不产生重复节点或配置漂移。 +- 字段能力集中在矩阵中维护;布局通过模板、分组和排序等结构化配置调整,不使用拖拽画布。 + +## 7. 现状与重构方向 + +当前模块配置将字段、列表、表单、查询拆成不同面板,字段参与状态会修改多份界面 JSON;现有表单 / 列表设计器还采用拖拽式交互。本次重构不迁移现有画布和拖拽实现,改用模板、字段矩阵、分组 / 排序表格及实时只读预览。项目已有类型默认值和列表—查询联动逻辑,可保留其业务规则并重新组织。 + +- `fms-vue/src/views/module/module-management/ModuleFieldsPanel.vue:3`:字段定义与表单 / 列表 / 查询参与关系。 +- `fms-vue/src/views/module/module-management/components/fieldDefaults.js:1`:列表和查询默认值规则。 +- `fms-vue/src/views/module/module-management/components/design-editor/common/schemaModel.js:540`:视图字段节点新增 / 移除。 +- `fms-vue/src/views/module/module-management/index.vue:1431`:配置面板分区导航和绑定。 + +重构保留已验证的行为意图,但不要求保留当前组件边界、JSON 结构或数据库表结构。 + +## 8. 整体重构范围与顺序 + +不做新旧格式长期并行或渐进兼容;按依赖顺序拆分实施任务,便于验证: + +1. **确定元数据契约**:模块类型、字段语义、关系、视图、默认覆盖、发布和动作扩展模型。 +2. **重建元数据数据库**:新表、约束、种子 / migration;决定 Demo 配置重建还是一次性导入。 +3. **实现后端目录与编译器**:读取、校验、默认合成、发布、版本切换、缓存失效和回滚。 +4. **替换配置工作台**:数据源检查、字段导入、模板初始化、能力矩阵、预览、发布。 +5. **替换运行时消费链路**:列表、查询、表单、保存统一读取发布定义。 +6. **接入扩展能力**:权限、关系、公式、工作流、导出和业务动作。 +7. **删除旧路径**:完成导入或确认弃用后,删除旧 Schema 适配和重复逻辑。 + +虽然不需要渐进兼容,仍建议用一个基础资料、一个主从单据、一个只读查询模块验证新契约,再批量建立 Demo 配置。这是新架构验收,不是要求新旧架构并行运行。 + +## 9. 验收标准 + +- 新建基础资料模块,无需分别手工搭建字段、表单、列表和查询的基础配置。 +- 表单和列表均可通过结构化配置完成,不需要拖拽画布、绝对坐标或依赖设计器内部状态。 +- 列表列和表单字段可以通过清单 / 矩阵批量配置、排序和复用模板,配置差异可读、可校验。 +- 模块字段只有一个权威定义;多个视图引用字段,不重复维护语义。 +- 默认配置可重复生成,且不覆盖显式配置。 +- 无效字段引用、数据源列缺失、关系目标错误、非法查询规则在发布时给出明确错误。 +- 运行时只消费已发布版本;发布失败不影响当前可用版本。 +- 简单 CRUD 使用通用运行时;复杂业务动作由明确的后端处理器完成。 +- 真实 SQL Server 业务表仍是业务数据事实来源,不引入 EAV 业务数据主存储。 +- 旧元数据路径已删除或仅作为一次性导入工具,不存在双重事实来源。 + +## 10. 需要同步修订的文档 + +目前架构文档存在方向冲突:部分草案提出以业务域代码包取代元数据运行时,开发规范与现有系统仍要求模块 / 字段 / 界面元数据驱动通用模块。本方案评审通过后应统一修订: + +- `FMS新系统核心架构设计.md`:明确元数据驱动是通用模块核心,SQL / 后端动作是扩展机制。 +- `FMS新系统核心架构实施计划.md`:改为元数据数据库、编译器、工作台和运行时重构任务。 +- `FMS新系统核心表结构设计.md`:重新定义元数据表,不再将现有 `s_module`、`s_field`、`s_module_schema` 作为不可变前提。 +- `开发规范.md`:明确元数据契约、发布定义、查询 / 保存边界和动作扩展约定。 + +## 11. 假设与待定项 + +- 暂保留 SQL Server 作为数据库目标;允许重做元数据表,不主动改变租户数据库路由模式。 +- 元数据和 Demo 配置可重建;是否保留样例数据,实施前决定重建或一次性导入。 +- 本文确定架构方向,不冻结最终 DDL、API、类名或 Vue 目录;这些在元数据契约评审后设计。 +- 权限、工作流、公式、多语言是否随模块发布版本整体冻结,需结合实际使用方式决定,避免第一版过度设计。 diff --git a/code/fms/模块管理简化与架构收敛方案.md b/code/fms/模块管理简化与架构收敛方案.md deleted file mode 100644 index cfa38bf6..00000000 --- a/code/fms/模块管理简化与架构收敛方案.md +++ /dev/null @@ -1,317 +0,0 @@ -# FMS 模块管理简化与架构收敛方案 - -> 文档状态:方案评审稿 -> 目标:降低新增业务模块的配置成本,明确元数据配置与业务代码的边界 -> 约束:不迁移旧模块配置或旧业务数据;不提供旧表、旧接口、旧配置格式的兼容层 - -## 1. 结论摘要 - -当前模块管理的主要问题不是能力不足,而是把“登记业务对象”“定义字段”“设计页面”“配置查询”“配置权限”“定义删除规则”等不同职责,集中成了一个通用模块配置器。简单模块因此也要理解和维护一套近似低代码平台的概念。 - -建议将系统收敛为以下模式: - -```text -业务域代码包(表、SQL、服务、API、页面) - │ - ├── 代码清单登记业务对象、页面和权限点 - └── 数据库维护模块启停、菜单结构和角色授权 -``` - -核心决策: - -1. **业务域代码包成为业务实现的唯一中心。**业务表、查询、保存、校验、业务动作和页面由业务域负责。 -2. **模块管理不再设计业务页面。**它只管理代码中已经存在的业务对象及其启停、名称和顺序。 -3. **菜单和权限分别管理。**菜单只能指向代码清单中已登记的页面;权限由代码声明,数据库维护用户或角色授权。 -4. **保留共享组件,不保留通用业务解释器作为核心路径。**简单资料可复用列表、表单、查询组件;复杂行为进入明确的业务服务。 -5. **从全新数据库和新接口开始。**旧 `s_module` 配置、旧 schema JSON、旧通用模块运行时和旧接口不迁移、不兼容、不双写。 - -## 2. 当前设计的主要负担 - -当前配置模型大致是: - -```text -s_module - ├── s_field - ├── s_module_schema(view / edit / query JSON) - ├── s_autocode - ├── s_power - ├── s_relation - ├── s_delete_rule - └── s_i18n -``` - -这套设计能让通用列表和表单快速出现,但也产生了几类持续成本: - -- **配置入口太多。**新增模块要在多个面板间切换;字段定义与列表、表单、查询布局分别维护,用户需要理解内部元数据结构。 -- **模块概念混杂。**导航树节点、数据对象、虚拟查询对象和页面入口都被叫作“模块”,导致一个模块既像目录又像数据模型和页面定义。 -- **运行时过度依赖配置解释。**配置错误会在运行时才表现为页面空白、字段不可写或保存失败,而不是在编译、启动或发布阶段尽早暴露。 -- **业务规则被推向通用化。**关系、级联删除、公式、权限和 SQL 配置逐渐形成跨业务解释规则;规则越多,编辑器、校验器和运行时之间的耦合越高。 -- **安全边界需要后端兜底。**模块中心在前端限制管理员访问,但元数据和业务写入权限不能依赖路由隐藏或前端角色判断。 - -问题的根因是:**把少数简单 CRUD 的便利,扩展成所有业务都必须经过的运行时模型。** - -## 3. 目标与非目标 - -### 3.1 目标 - -- 新增业务对象时,业务规则和数据访问可以在代码中搜索、评审、测试和发布。 -- 管理员只做运行配置:菜单组织、对象启停、角色授权和少量展示偏好。 -- 简单资料复用统一组件;单据、库存、财务、审批等复杂业务使用专用 API 和服务。 -- 页面引用的数据字段、权限点和业务动作可由代码清单静态登记并校验。 -- 配置改动有明确的操作权限、审计信息和发布责任。 - -### 3.2 非目标 - -- 不建设新的通用低代码平台或通用 JSON 规则引擎。 -- 不让数据库配置动态创建业务表、推断保存逻辑或执行任意业务 SQL。 -- 不要求每个业务页面都可由管理员拖拽生成。 -- 不迁移旧模块配置、旧业务数据或旧接口调用方。 -- 不为兼容旧版而保留双运行时、双字段定义或旧配置转换器。 -- 第一阶段不做通用代码生成平台;先用一个真实业务域验证代码模板。 - -## 4. 目标概念模型 - -必须把四个概念分开: - -| 概念 | 含义 | 权威来源 | -| --- | --- | --- | -| 业务域 | 一组共同业务职责,如 customer、sales、inventory | 后端和前端代码目录 | -| 业务对象 | 可被查询或操作的对象,如 customer、sales_order | 业务域代码清单 | -| 页面 | 列表、详情、工作台等用户界面 | 前端代码路由/页面清单 | -| 菜单 | 用户进入页面的导航结构 | 数据库菜单配置 | - -菜单不再承担业务对象树的职责;业务对象不再通过树节点推断页面行为;页面也不能靠数据库中任意组件名动态加载。 - -### 4.1 业务域代码包 - -建议沿用新架构草案中的业务域组织方式: - -```text -domains/ - customer/ - migration/ - sql/ - repository/ - service/ - api/ - page/ - i18n/ - sales/ - inventory/ -``` - -每个业务域明确拥有自己的数据库变更、查询 SQL、保存方式、业务动作、API、页面和多语言资源。跨域调用通过接口或明确的业务关系完成,不通过任意 JSON 规则互相解释。 - -### 4.2 代码清单 - -每个业务域提供静态清单,列出可供系统登记的对象、页面和权限点。清单是可执行代码的一部分,不能由数据库传入任意类名或组件名覆盖。 - -示意: - -```text -customer 域 - 对象:customer - 页面:customer-list、customer-detail - 权限:customer.read、customer.create、customer.update、customer.disable -``` - -启动或发布校验至少检查: - -- 菜单引用的页面 key 必须存在于代码清单; -- 模块登记的业务对象必须存在于代码清单; -- 授权引用的权限 key 必须已声明; -- 一个页面声明使用的对象必须在清单中登记; -- 数据库表、视图和必要字段必须通过业务域校验。 - -## 5. 新的模块管理体验 - -将“模块管理”改名为 **业务对象与模块登记**,不再展示字段设计器、列表设计器、表单设计器、查询设计器、自动编码、模块关系和删除 SQL 规则等运行时配置面板。 - -### 5.1 业务对象登记 - -业务对象由代码包提供,管理页面只允许维护少量运行属性: - -- 显示名称或启用的多语言资源; -- 是否启用; -- 管理列表中的排序; -- 必要的说明信息。 - -不允许在这里填写保存表、查询 SQL、字段列表或业务规则。对象登记不是“凭空创建一个业务模块”:新增对象先有代码,再由清单登记。 - -### 5.2 菜单管理 - -菜单继续负责导航树和页面入口: - -- 只能从代码清单选择页面 key; -- 菜单层级、排序、名称和启停保存在数据库; -- 菜单到业务对象的绑定明确记录,不从父子树或模块类型推断; -- 不允许数据库配置任意前端路由组件。 - -### 5.3 权限管理 - -- 权限点由业务域清单声明,代码决定具体操作含义。 -- 管理界面只分配已声明的权限给角色或用户。 -- 页面入口权限、对象读写权限、业务动作权限分别判定。 -- 后端 API 对每次请求重新校验登录身份、对象/动作权限和业务数据范围;前端隐藏按钮只作为体验优化。 - -## 6. 页面和数据配置边界 - -### 6.1 业务页面 - -页面默认由代码实现,并复用现有通用列表、表单、查询、选择器和附件等 UI 组件。业务页面的字段、校验、动作和调用哪个 API 由页面和业务域代码明确声明。 - -建议的简单资料实现链路: - -```text -Migration -→ 业务表 / 查询视图 -→ list/detail/save SQL -→ repository / service -→ API -→ 共享列表与表单组件 -→ 页面 -``` - -复杂单据的提交、撤回、审批、过账、库存变更等动作必须由业务服务实现,不由表单 JSON、通用保存接口或可编辑 SQL 字段推断。 - -### 6.2 允许的展示偏好 - -只在多个业务域确实重复需要时,才保留受限展示配置,例如: - -- 用户个人列表列顺序、列宽; -- 少量查询默认值; -- 只影响展示的格式偏好。 - -这些配置不得改变字段真实类型、保存映射、数据权限、业务校验或事务行为。默认布局优先由代码提供;个人偏好是覆盖,不是创建业务页面的必要步骤。 - -### 6.3 数据库是事实来源 - -- Migration 定义真实表结构; -- SQL 文件或受控 repository 实现查询和保存; -- 代码模型定义 API 输入输出和字段语义; -- 代码或受控资源维护系统文案; -- 数据库运行配置只登记启停、菜单和授权。 - -可以保留 `describe` 能力用于开发期校验或脚手架辅助,但不把数据库实时反射变成生产运行时的字段定义器。 - -## 7. 数据表与接口调整方向 - -以下是职责方向,不要求为每个旧表设计转换路径: - -| 当前职责 | 目标处理方式 | -| --- | --- | -| `s_module` 中的模块树、视图表、保存表、通用 SQL | 模块登记表只保留稳定对象编码、业务域/对象 key、名称、启停和排序;业务 SQL 移入域代码 | -| `s_field` 运行时字段定义 | 不作为业务页面运行时字段事实来源;字段由业务 API 和页面代码定义 | -| `s_module_schema` 的 view/edit/query JSON | 移除通用解释器依赖;页面结构由代码定义,只可选保留受限展示偏好 | -| `s_autocode` | 编号规则归业务域所有,由业务服务原子获取和验证 | -| `s_relation` | 真实数据关系由 SQL 约束、查询和业务服务表达;不由全局模块关系解释器驱动 | -| `s_delete_rule` | 删除保护和级联行为由业务域服务及数据库约束明确实现 | -| `s_power` 权限点定义 | 权限 key 由代码清单声明,数据库维护角色授权;禁止客户端任意造权限 key | -| `s_menu` / 菜单绑定 | 保留导航职责,但页面目标必须指向代码清单中的安全 page key | -| `s_user_module_pref` | 如有真实用户偏好需求,可保留为展示偏好;不可承载权限或业务规则 | - -模块登记表的候选字段: - -```text -b_id 业务对象编码 -b_domain_code 业务域编码 -b_object_code 域内对象编码 -b_name 显示名称 -b_i18n 多语言资源 key -b_canuse 是否启用 -b_xh 管理列表排序 -b_bz 说明 -``` - -`(b_domain_code, b_object_code)` 应有唯一约束。最终字段名和表结构在确认代码清单、权限模型和菜单模型后再定;不要把 `view_table`、`save_table`、任意 SQL 或业务配置 JSON 原样搬进新表。 - -## 8. 安全要求 - -这次收敛必须同时明确服务端边界: - -1. 模块登记、菜单配置、角色授权的写接口必须有服务端系统管理员权限校验。 -2. 通用数据写入不能只按客户端传入的 `table` 和 `key_field` 决定是否允许写;应使用受控表清单或专用业务 API。 -3. 业务 API 必须校验用户对对象和动作的权限,以及公司、组织等数据范围。 -4. 权限定义和页面 key 来自代码清单,不接受页面传入任意字符串作为授权依据。 -5. SQL 参数值使用参数绑定;表、列和排序标识符必须来自服务端登记的标识符清单。 -6. 关键管理操作记录操作人、对象、变更前后值和请求追踪信息。 - -## 9. 全新替换策略 - -本方案不提供旧数据或旧接口的兼容路径: - -- 新环境从新的 Migration 基线建库; -- 不迁移旧 `s_module`、`s_field`、`s_module_schema` 或其他模块配置数据; -- 不实现旧 JSON 转换器、双读、双写、旧 API 代理或回退到旧页面的逻辑; -- 新版模块中心不读取旧配置表; -- 旧模块运行时、旧管理路由和仅服务旧运行时的接口随新实现整体移除; -- 旧业务数据迁移不属于本方案范围,需另立项目决策,不在新模块管理中隐式处理。 - -因此部署策略是新系统按新 schema 初始化,而不是在旧数据库上做无损升级。旧环境如需留档,由环境和备份策略处理,不由新版运行时兼容。 - -## 10. 实施顺序 - -### 阶段 A:锁定边界 - -- 定义业务域代码清单格式; -- 定义对象、页面和权限 key 的命名规则; -- 明确模块登记、菜单和角色授权三者关系; -- 明确简单 CRUD 与复杂业务 API 的使用边界; -- 完成后端管理员授权和通用写接口策略设计。 - -### 阶段 B:建立新内核 - -- 使用全新 Migration 建立最小模块登记、菜单、权限和审计结构; -- 实现代码清单校验; -- 实现服务端菜单/授权管理 API; -- 新模块中心只提供登记、启停、排序和菜单/授权入口。 - -### 阶段 C:验证一个简单业务域 - -选择客户或产品资料,完整实现: - -```text -Migration → SQL → repository/service → API → 页面 → 菜单 → 授权 → 审计 -``` - -该对象必须不依赖 `s_field`、`s_module_schema` 或通用模块解释器才能运行。 - -### 阶段 D:验证一个复杂业务动作 - -选择一个真实动作(例如单据提交或审批绑定),验证业务校验、事务和权限均在明确的业务服务中完成;工作流只调用已登记的业务动作。 - -### 阶段 E:移除旧模块中心实现 - -- 移除旧模块配置面板和仅供其使用的 JSON 解释逻辑; -- 移除旧运行时对模块字段/schema 的依赖; -- 删除旧模块专属接口与无调用点代码; -- 确认所有菜单页面来自代码清单,所有关键写操作经服务端权限校验。 - -不为旧数据补转换步骤,也不设置旧新并行期。 - -## 11. 验收标准 - -- 开发者新增业务对象时,有一致的业务域模板和单一 API/服务入口。 -- 管理员新增菜单时只能选择已编译、已登记的页面和对象。 -- 建立简单资料不需要分别维护字段表、列表 JSON、表单 JSON、查询 JSON 才能运行。 -- 复杂业务动作的校验、事务和 SQL 均可在对应业务域代码中定位。 -- 没有业务页面依赖全局模块 JSON 解释器才能启动。 -- 未授权用户直接调用 API 不能修改模块、菜单、权限或业务数据。 -- 新数据库初始化后,完整业务示例不需要导入任何旧模块配置即可工作。 -- 配置偏好只影响展示,不能放宽数据权限或改变保存行为。 - -## 12. 代价与风险 - -- **优点:**运行时更容易理解和测试;业务逻辑可代码评审;减少管理员配置步骤;SQL、权限和事务责任明确。 -- **代价:**新增业务对象需要开发和发布,不再承诺管理员零代码创建任意模块。 -- **控制新增代码成本:**先建立简单资料业务域模板并复用共享组件;只有至少两个业务域出现相同重复后,再抽取脚手架或通用能力。 -- **主要风险:**若大多数模块都要求管理员自行创建且业务结构高度同质,完全代码化会降低业务人员自助能力。应先用真实客户/产品模块验证开发成本,再决定是否增加“生成代码”的开发工具;不要因此恢复生产运行时的通用配置解释器。 - -## 13. 评审时需要确认 - -1. 新模块由开发者通过代码清单登记,管理员只能启停和配置菜单/授权,是否作为默认流程? -2. 列表列宽和列顺序是否只保留用户个人偏好,系统级默认布局是否完全由代码维护? -3. 简单 CRUD 的代码模板是否足够,是否先不做自动代码生成器? -4. 新数据库 schema 是否允许直接移除旧模块配置表和旧通用模块 API? - -以上四项确定后,再据此制定数据库表结构、API 清单和分阶段实施任务。本方案本身不改代码、不执行数据库变更,也不包含旧数据兼容或迁移实现。 diff --git a/code/g3soft-erp/docs/前端架构设计.md b/code/g3soft-erp/docs/前端架构设计.md index a6229743..61e143c1 100644 --- a/code/g3soft-erp/docs/前端架构设计.md +++ b/code/g3soft-erp/docs/前端架构设计.md @@ -113,6 +113,11 @@ web/src/ 4. **布局壳的样式不加 `scoped`**(`LayoutSidebar.vue` 等),因为需要被子元素感知尺寸;业务组件必须加 `scoped`。 +5. **不兼容历史持久化数据** + `localStorage` 里存过的偏好设置(`g3erp:preferences`)、页签(`g3erp:app`)等,**不做版本迁移、不做字段兜底**。 + 删字段或改字段名时直接删,不要写 `if (oldField)` 这类兼容分支——`store.ts` 的 `mergePreferences` 只遍历 `defaultPreferences` 的键,旧数据里多出来的键自然被忽略。 + **理由**:开发期数据结构还在变,为一次性数据写迁移是纯负债。用户清一次 localStorage 即可。 + ## 4. 菜单与路由:单一数据源 **这是骨架的核心设计。** diff --git a/code/g3soft-erp/web/src/composables/useFullscreen.ts b/code/g3soft-erp/web/src/composables/useFullscreen.ts new file mode 100644 index 00000000..6dc23e92 --- /dev/null +++ b/code/g3soft-erp/web/src/composables/useFullscreen.ts @@ -0,0 +1,124 @@ +import { onBeforeUnmount, readonly, ref, type Ref } from 'vue' + +/** + * 原生全屏封装。 + * + * 对照 vue-vben-admin 的 @core/composables/use-fullscreen.ts, + * 差异:vben 用 VueUse 的 useFullscreen 直接包一层,本仓库没有 VueUse 依赖, + * 这里手写等价实现(前缀处理 + 状态同步 + 卸载解绑)。 + * + * 只用原生 Fullscreen API,浏览器不支持时 isSupported 为 false, + * 调用 toggle 变成空操作 —— 按钮据此隐藏,不会出现「点了没反应」。 + */ + +/** 带浏览器前缀的全屏 API 名字(老 Safari / 老 Chrome 仍用 webkit 前缀) */ +interface VendorFullscreenDocument extends Document { + webkitFullscreenElement?: Element | null + webkitExitFullscreen?: () => Promise | void +} + +interface VendorFullscreenElement extends HTMLElement { + webkitRequestFullscreen?: () => Promise | void +} + +/** 当前处于全屏的元素(含前缀),未全屏时为 null */ +function getFullscreenElement(): Element | null { + const doc = document as VendorFullscreenDocument + return doc.fullscreenElement ?? doc.webkitFullscreenElement ?? null +} + +/** 请求全屏(含前缀回退) */ +async function requestFullscreen(target: HTMLElement): Promise { + const element = target as VendorFullscreenElement + const request = element.requestFullscreen ?? element.webkitRequestFullscreen + await request?.call(element) +} + +/** 退出全屏(含前缀回退) */ +async function exitFullscreen(): Promise { + const doc = document as VendorFullscreenDocument + const exit = doc.exitFullscreen ?? doc.webkitExitFullscreen + await exit?.call(doc) +} + +export interface UseFullscreenReturn { + /** 当前是否处于全屏 */ + isFullscreen: Readonly> + /** 浏览器是否支持全屏(不支持时调用方应隐藏入口) */ + isSupported: boolean + /** 进入全屏(已在全屏则忽略) */ + enter: () => Promise + /** 退出全屏 */ + exit: () => Promise + /** + * 切换全屏。 + * @param target 要全屏的元素,缺省用 document.documentElement(整页全屏) + */ + toggle: (target?: HTMLElement | null) => Promise +} + +/** + * @param target 可选的目标元素。传 ref 时,toggle 缺省就全屏它(如内容区 = 局部全屏); + * 不传则天然是整页全屏。目标元素不存在时退化为整页。 + */ +export function useFullscreen(target?: Ref): UseFullscreenReturn { + const isSupported = Boolean( + document.fullscreenEnabled ?? + (document as VendorFullscreenDocument & { webkitFullscreenEnabled?: boolean }) + .webkitFullscreenEnabled, + ) + + const isFullscreen = ref(getFullscreenElement() !== null) + + /** + * 状态同步以全屏事件为唯一来源,而不是在 enter/exit 里直接赋值: + * 用户按 Esc、F11、或浏览器自身退出时同样能拿到正确状态, + * 避免「界面显示已全屏、实际已退出」的不一致。 + */ + function sync(): void { + isFullscreen.value = getFullscreenElement() !== null + } + + document.addEventListener('fullscreenchange', sync) + document.addEventListener('webkitfullscreenchange', sync) + + onBeforeUnmount(() => { + document.removeEventListener('fullscreenchange', sync) + document.removeEventListener('webkitfullscreenchange', sync) + }) + + async function enter(): Promise { + if (!isSupported || getFullscreenElement()) return + // 全屏请求可能被浏览器拒绝(非用户手势触发、iframe 缺少 allow 权限), + // 这里吞掉 rejection:状态由 fullscreenchange 决定,界面不会误显示为已全屏。 + try { + await requestFullscreen(target?.value ?? document.documentElement) + } catch { + sync() + } + } + + async function exit(): Promise { + if (!getFullscreenElement()) return + try { + await exitFullscreen() + } catch { + sync() + } + } + + async function toggle(element?: HTMLElement | null): Promise { + if (getFullscreenElement()) { + await exit() + return + } + if (!isSupported) return + try { + await requestFullscreen(element ?? target?.value ?? document.documentElement) + } catch { + sync() + } + } + + return { isFullscreen: readonly(isFullscreen), isSupported, enter, exit, toggle } +} diff --git a/code/g3soft-erp/web/src/constants/menu.ts b/code/g3soft-erp/web/src/constants/menu.ts index a468dee0..a3a0383a 100644 --- a/code/g3soft-erp/web/src/constants/menu.ts +++ b/code/g3soft-erp/web/src/constants/menu.ts @@ -16,15 +16,8 @@ export const menuItems: MenuItem[] = [ path: '/ocean', icon: 'Ship', children: [ - { - key: 'ocean-booking-group', - title: '订舱业务', - path: '/ocean/booking', - children: [ - { key: 'ocean-booking', title: '订舱单', path: '/ocean/booking' }, - { key: 'ocean-bill', title: '提单管理', path: '/ocean/bill' }, - ], - }, + { key: 'ocean-booking', title: '订舱单', path: '/ocean/booking' }, + { key: 'ocean-bill', title: '提单管理', path: '/ocean/bill' }, { key: 'ocean-schedule', title: '船期查询', path: '/ocean/schedule' }, ], }, @@ -79,6 +72,7 @@ export const menuItems: MenuItem[] = [ { key: 'system-user', title: '用户管理', path: '/system/user' }, { key: 'system-role', title: '角色权限', path: '/system/role' }, { key: 'system-menu', title: '菜单配置', path: '/system/menu' }, + { key: 'system-keepalive-test', title: 'keep-alive 测试', path: '/system/keepalive-test' }, ], }, ] diff --git a/code/g3soft-erp/web/src/layouts/DefaultLayout.vue b/code/g3soft-erp/web/src/layouts/DefaultLayout.vue index 060db6c3..9e4665cf 100644 --- a/code/g3soft-erp/web/src/layouts/DefaultLayout.vue +++ b/code/g3soft-erp/web/src/layouts/DefaultLayout.vue @@ -1,5 +1,5 @@ @@ -116,6 +197,35 @@ watch( height: 100%; overflow: hidden; + /* ---- 侧栏共享调色板(浅色默认值) ---- + * 定义在**布局根**而不是 `.layout-sidebar` 上:因为双列菜单布局下 + * LayoutSidebar 根本不渲染,而图标栏 / 第二列是它的兄弟节点, + * 变量定义在 LayoutSidebar 里的话这两列一个都继承不到, + * 它们的 var(--sidebar-bg, #fff) 会永远吃到字面量兜底、深色模式失效。 + * 放在这里后,三种侧栏形态(单列 / 双列两列)共用同一套值。 */ + --sidebar-bg: var(--g3-color-sidebar, #fff); + --sidebar-border: var(--g3-color-border, #e5e6eb); + --sidebar-item-color: var(--g3-color-foreground, #1f2329); + --sidebar-item-hover-bg: var(--g3-color-accent, #f2f3f5); + --sidebar-item-active-bg: var(--g3-color-primary-soft, hsl(212 100% 45% / 15%)); + --sidebar-item-active-color: var(--g3-color-primary, #165dff); + --sidebar-muted-color: var(--g3-color-text-tertiary, #86909c); + --sidebar-title-color: var(--g3-color-foreground, #1f2329); + + /* 全局深色(html.dark):整片侧栏切深色。 + 与 LayoutSidebar 的处理保持一致;「深色侧栏」开关由各组件自己的 + .is-dark 覆盖(见 LayoutMixedRail / LayoutMixedColumn)。 */ + .dark & { + --sidebar-bg: #1d2129; + --sidebar-border: #2e3238; + --sidebar-item-color: rgb(255 255 255 / 80%); + --sidebar-item-hover-bg: #313438; + --sidebar-item-active-bg: #313438; + --sidebar-item-active-color: #f2f3f5; + --sidebar-muted-color: rgb(255 255 255 / 45%); + --sidebar-title-color: #f2f3f5; + } + &__main { display: flex; flex-direction: column; @@ -128,22 +238,18 @@ watch( flex: 1; overflow: auto; } +} - &__footer { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - font-size: 12px; - color: var(--g3-color-text-tertiary, #86909c); - border-top: 1px solid var(--g3-color-border, #e5e6eb); +// 内容区局部全屏:外壳已隐藏,把内容区的内边距收敛掉, +// 让页面真正铺满视口(否则四周仍留着一圈 contentPadding 的空白)。 +.layout--content-fullscreen .layout__content { + padding: 0; +} - &.is-fixed { - position: sticky; - bottom: 0; - background: var(--g3-color-bg-container, #fff); - } - } +// 命名包装组件的内层容器:撑满内容区高度,页面组件 flex:1 的高度链不被破坏。 +.layout-page-inner { + height: 100%; + min-height: 100%; } diff --git a/code/g3soft-erp/web/src/layouts/LayoutHeader.vue b/code/g3soft-erp/web/src/layouts/LayoutHeader.vue index 47229e65..935ade23 100644 --- a/code/g3soft-erp/web/src/layouts/LayoutHeader.vue +++ b/code/g3soft-erp/web/src/layouts/LayoutHeader.vue @@ -1,9 +1,19 @@