20260723173317
This commit is contained in:
1 parent
59b3ccfb1c
commit
5f73b54802
33 files changed
+3064
-2108
No files matched your search
@@ -0,0 +1,183 @@
|
||||
---
|
||||
name: module-management-full-implementation
|
||||
overview: 基于新数据库结构.md和开发规范.md完整实现模块管理页面。左侧模块树+右侧7个子Tab,子Tab操作按钮靠左。不创建独立的service文件,数据加载/保存逻辑直接写在index.vue中。
|
||||
todos:
|
||||
- id: create-main-page
|
||||
content: 重写 index.vue 主页面:左侧 a-tree 模块树 + 右侧 a-tabs 布局,编写全部数据加载/保存逻辑(loadDataApi、saveObjectApi、diff、typeFromSqlType)
|
||||
status: completed
|
||||
- id: create-basic-panel
|
||||
content: 使用 [skill:antdv-next] 创建 ModuleBasicPanel.vue 基本信息表单,根据模块类型控制字段可见性,支持新增和编辑
|
||||
status: completed
|
||||
dependencies:
|
||||
- create-main-page
|
||||
- id: create-fields-panel
|
||||
content: 创建 ModuleFieldsPanel.vue 字段定义面板,FmsTable 展示字段列表,实现「同步字段」功能,操作按钮靠左
|
||||
status: completed
|
||||
dependencies:
|
||||
- create-main-page
|
||||
- id: create-list-edit-query-panels
|
||||
content: 创建 ModuleListConfigPanel.vue、ModuleEditConfigPanel.vue、ModuleQueryConfigPanel.vue 三个配置面板,FmsTable 展示数据,操作按钮靠左
|
||||
status: completed
|
||||
dependencies:
|
||||
- create-main-page
|
||||
- id: create-group-manager
|
||||
content: 创建 FieldGroupManager.vue 共用分组管理弹窗,a-modal + a-tree 管理分组树增删改
|
||||
status: completed
|
||||
dependencies:
|
||||
- create-list-edit-query-panels
|
||||
- id: create-powers-i18n-panels
|
||||
content: 创建 ModulePowersPanel.vue 权限配置面板和 ModuleI18nPanel.vue 多语言面板,FmsTable 管理数据,操作按钮靠左
|
||||
status: completed
|
||||
dependencies:
|
||||
- create-main-page
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
根据新数据库结构.md和开发规范.md,完整实现模块管理页面。当前页面是空占位,需要构建左侧模块树+右侧子Tab布局。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 主页面布局
|
||||
|
||||
- 左侧:使用 `a-tree` 展示模块树(directory/page/data 三级),选中节点加载详情
|
||||
- 右侧:使用 `a-tabs` 展示子Tab,根据选中模块的 `b_module_type` 动态显示对应Tab
|
||||
|
||||
### 模块类型与Tab对应
|
||||
|
||||
| 模块类型 | 可见Tab |
|
||||
| --- | --- |
|
||||
| directory | 基本信息、权限配置、多语言 |
|
||||
| page | 基本信息、权限配置、多语言 |
|
||||
| data | 基本信息、字段定义、列表配置、编辑配置、查询配置、权限配置、多语言 |
|
||||
|
||||
|
||||
### 各子Tab功能
|
||||
|
||||
1. **基本信息**:`a-form` 编辑 `b_module` 核心字段(b_code、b_name、b_module_type、b_parent_id、b_route、b_viewtable、b_savetable、b_keyfield、b_orderfield、b_canmenu、b_canuse、b_xh),根据模块类型控制字段可见性。新增模块先调 `nextIdApi`
|
||||
2. **字段定义**:`FmsTable` 展示 `b_module_field`,不可手工增删行。提供「同步字段」按钮调用 `describeApi` 获取 `b_viewtable` 列结构并与已有字段合并(保留已有配置、追加新列、标记已删列为不可用)。可编辑 `b_name`、`b_type`、`b_default_value`、`b_options`、`b_canuse`
|
||||
3. **列表配置**:`FmsTable` 展示 `b_module_field_list`,支持新增/删除行。可选字段来自 `b_module_field`(b_canuse=1)。`FieldGroupManager` 弹窗管理 `b_module_field_group` 分组树
|
||||
4. **编辑配置**:`FmsTable` 展示 `b_module_field_edit`,支持新增/删除行。同样通过 `FieldGroupManager` 管理分组
|
||||
5. **查询配置**:`FmsTable` 展示 `b_module_field_query`(b_component、b_operator 下拉选择),支持新增/删除行
|
||||
6. **权限配置**:`FmsTable` 展示 `b_module_power`(b_code、b_name),支持新增/删除行
|
||||
7. **多语言**:`FmsTable` 展示 `b_i18n`(b_key、b_locale、b_value),支持新增/删除行
|
||||
|
||||
### 操作按钮排列
|
||||
|
||||
所有子Tab面板内,新增、删除、保存等操作按钮**靠左对齐**(`justify-content: flex-start`)
|
||||
|
||||
### 数据架构(不创建独立 service 文件)
|
||||
|
||||
- `index.vue` 承担全部数据职责:加载模块树、加载完整配置、维护草稿、对比diff、调用 `saveObjectApi`
|
||||
- 子组件只通过 `props` 接收数据,通过 `emit('update', newRows)` 上报变更
|
||||
- `index.vue` 保存流程:`diffRows(draft, original)` → `allocateTemporaryIds` → `saveObjectApi`
|
||||
|
||||
## 技术方案
|
||||
|
||||
### 实现策略
|
||||
|
||||
**数据流集中管理**:`index.vue` 作为数据中枢,直接调用 `loadDataApi`、`saveObjectApi`、`nextIdApi`、`describeApi` 等通用接口。所有子组件是无状态的展示/编辑组件,仅通过 props/emit 与父组件通信。
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
index.vue 选中模块
|
||||
→ loadDataApi 加载 b_module + 各子表(Promise.all)
|
||||
→ cloneData 生成 originals(原始快照)和 drafts(编辑草稿)
|
||||
→ 各子组件接收 drafts 对应部分,用户编辑后 emit('update', newRows)
|
||||
→ index.vue 更新对应 draft
|
||||
→ 用户点击「保存」
|
||||
→ 遍历各子表 diffRows(draft, original)
|
||||
→ allocateTemporaryIds 分配临时ID
|
||||
→ 构建 saveObjectApi 请求数组
|
||||
→ 提交保存 → 重新加载刷新 originals
|
||||
```
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
1. **无独立 service 文件**:所有 `loadDataApi`/`saveObjectApi` 调用直接写在 `index.vue` 的 `<script setup>` 中
|
||||
2. **组件纯展示**:所有子组件只负责展示和编辑,不调用 API
|
||||
3. **SQL 类型映射**(字段同步用):`typeFromSqlType()` 作为 `index.vue` 内的普通函数
|
||||
4. **分组管理复用**:`FieldGroupManager.vue` 接收 groups 数组并通过 emit 返回变更,不直接操作 API
|
||||
5. **b_module_field 不手工增删**:同步通过 `describeApi`,字段定义面板中只有编辑功能无新增/删除按钮
|
||||
|
||||
### 实现要点
|
||||
|
||||
- b_id 全部使用 `String()` 包裹进行比对
|
||||
- 布尔字段为 int 0/1(b_canuse、b_visible、b_sortable、b_required、b_readonly、b_disabled、b_canmenu)
|
||||
- 子表新增行用 `createTempId()`,保存前 `allocateTemporaryIds()` 换真实ID
|
||||
- 主表新增模块用 `nextIdApi`
|
||||
- 操作按钮在子Tab面板内靠左(`a-space` 或 flex 布局 `justify-content: flex-start`)
|
||||
- 样式使用 SCSS scoped,遵循 `--fms-*` CSS 变量主题
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
fms-vue/src/views/module-management/
|
||||
├── index.vue # [MODIFY] 主页面
|
||||
│ - 左侧 a-tree 模块树,右侧 a-tabs
|
||||
│ - 全部数据加载/保存逻辑(loadDataApi、saveObjectApi、nextIdApi、describeApi)
|
||||
│ - 维护 drafts 和 originals,构建 diff 和 saveObjectApi 请求
|
||||
│ - typeFromSqlType() 字段类型映射函数
|
||||
│ - 选中节点变更、Tab切换、保存按钮事件处理
|
||||
└── components/
|
||||
├── ModuleBasicPanel.vue # [NEW] 基本信息表单
|
||||
│ - a-form 编辑 b_module 字段
|
||||
│ - props: moduleId, data(object), loading
|
||||
│ - emit: update(modifiedData)
|
||||
│ - 根据 b_module_type 控制字段可见性
|
||||
├── ModuleFieldsPanel.vue # [NEW] 字段定义表格
|
||||
│ - FmsTable 展示 b_module_field 列表
|
||||
│ - 工具栏「同步字段」+「保存」按钮靠左
|
||||
│ - props: moduleId, fields(array), viewTable, loading
|
||||
│ - emit: update(newFields), sync(viewTable)
|
||||
│ - 不可新增/删除行,可编辑 b_name/b_type/b_default_value/b_options/b_canuse
|
||||
├── ModuleListConfigPanel.vue # [NEW] 列表配置表格
|
||||
│ - FmsTable 展示 b_module_field_list
|
||||
│ - 工具栏新增/删除/保存按钮靠左
|
||||
│ - props: moduleId, listConfig(array), fields(array), groups(array), loading
|
||||
│ - emit: update(newListConfig), openGroupManager
|
||||
│ - 字段选择器展示可用字段,分组列可点击打开分组管理
|
||||
├── ModuleEditConfigPanel.vue # [NEW] 编辑配置表格
|
||||
│ - FmsTable 展示 b_module_field_edit
|
||||
│ - 工具栏新增/删除/保存按钮靠左
|
||||
│ - props: moduleId, editConfig(array), fields(array), groups(array), loading
|
||||
│ - emit: update(newEditConfig), openGroupManager
|
||||
├── ModuleQueryConfigPanel.vue # [NEW] 查询配置表格
|
||||
│ - FmsTable 展示 b_module_field_query
|
||||
│ - 工具栏新增/删除/保存按钮靠左
|
||||
│ - props: moduleId, queryConfig(array), fields(array), loading
|
||||
│ - emit: update(newQueryConfig)
|
||||
│ - b_component、b_operator 使用下拉选择
|
||||
├── ModulePowersPanel.vue # [NEW] 权限配置表格
|
||||
│ - FmsTable 展示 b_module_power
|
||||
│ - 工具栏新增/删除/保存按钮靠左
|
||||
│ - props: moduleId, powers(array), loading
|
||||
│ - emit: update(newPowers)
|
||||
├── ModuleI18nPanel.vue # [NEW] 多语言表格
|
||||
│ - FmsTable 展示 b_i18n
|
||||
│ - 工具栏新增/删除/保存按钮靠左
|
||||
│ - props: moduleId, i18n(array), loading
|
||||
│ - emit: update(newI18n)
|
||||
│ - 支持按 b_locale 筛选
|
||||
└── FieldGroupManager.vue # [NEW] 共用分组管理弹窗
|
||||
- a-modal 内含 a-tree 展示分组树
|
||||
- 支持新增/编辑/删除分组节点
|
||||
- props: visible, groups(array), moduleId
|
||||
- emit: update:visible, update(newGroups)
|
||||
- 被 ModuleListConfigPanel 和 ModuleEditConfigPanel 引用
|
||||
```
|
||||
|
||||
## 使用的扩展
|
||||
|
||||
### Skill
|
||||
|
||||
- **antdv-next**
|
||||
- 目的:查询 a-tree、a-tabs、a-form、a-form-item、a-modal、a-select、a-input、a-input-number、a-switch、a-button、a-space、a-empty 等组件的 API 文档
|
||||
- 预期结果:获取准确的 props/events/slots 用法,确保模板中 API 使用正确
|
||||
|
||||
### SubAgent
|
||||
|
||||
- **code-explorer**
|
||||
- 目的:在实现过程中快速查找项目中 FmsTable 的 columns 配置模式、dataChanges.js 的 diffRows 调用方式、现有表单校验示例等
|
||||
- 预期结果:获得准确的现有代码引用,确保新代码风格与项目一致
|
||||
@@ -0,0 +1,292 @@
|
||||
---
|
||||
name: module-management-full-implementation
|
||||
overview: 基于新数据库结构.md和开发规范.md完整实现模块管理页面。左侧模块树+右侧7个子Tab(基本信息、字段定义、列表配置、编辑配置、查询配置、权限配置、多语言),子Tab操作按钮靠左。
|
||||
todos:
|
||||
- id: create-service
|
||||
content: 创建 src/services/moduleManagement.js 服务层,封装 loadModuleTree、loadModuleConfig、saveModuleConfig、syncFields、typeFromSqlType 等方法
|
||||
status: pending
|
||||
- id: create-main-page
|
||||
content: 重写 src/views/module-management/index.vue 主页面,实现左侧 FmsTree 模块树 + 右侧 a-tabs 动态Tab布局,集成数据加载与保存流程
|
||||
status: pending
|
||||
dependencies:
|
||||
- create-service
|
||||
- id: create-basic-panel
|
||||
content: 使用 [skill:antdv-next] 创建 ModuleBasicPanel.vue 基本信息面板,表单编辑 b_module 字段,根据模块类型控制字段可见性
|
||||
status: pending
|
||||
dependencies:
|
||||
- create-main-page
|
||||
- id: create-fields-panel
|
||||
content: 创建 ModuleFieldsPanel.vue 字段定义面板,FmsTable 展示字段列表,实现「同步字段」功能(调用 describeApi 合并视图列)
|
||||
status: pending
|
||||
dependencies:
|
||||
- create-main-page
|
||||
- id: create-list-edit-query-panels
|
||||
content: 创建 ModuleListConfigPanel.vue、ModuleEditConfigPanel.vue、ModuleQueryConfigPanel.vue 三个配置面板,FmsTable 展示各自数据,操作按钮靠左
|
||||
status: pending
|
||||
dependencies:
|
||||
- create-main-page
|
||||
- id: create-group-manager
|
||||
content: 创建 FieldGroupManager.vue 共用分组管理组件,弹窗内管理 b_module_field_group 树形分组增删改
|
||||
status: pending
|
||||
dependencies:
|
||||
- create-list-edit-query-panels
|
||||
- id: create-powers-i18n-panels
|
||||
content: 创建 ModulePowersPanel.vue 权限配置面板和 ModuleI18nPanel.vue 多语言面板,FmsTable 管理 b_module_power 和 b_i18n 数据
|
||||
status: pending
|
||||
dependencies:
|
||||
- create-main-page
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
根据新数据库结构.md和开发规范.md,完整实现模块管理页面。当前模块管理页面只有一个空占位(`<el-button>`),需要构建完整的左侧模块树+右侧子Tab布局。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 主页面布局
|
||||
|
||||
- **左侧模块树**:以树形结构展示所有模块(directory/page/data),支持选中节点后在右侧加载详情
|
||||
- **右侧详情区**:使用 `a-tabs` 展示子Tab,根据选中模块的 `b_module_type` 动态显示对应Tab
|
||||
|
||||
### 模块类型与Tab对应关系
|
||||
|
||||
| 模块类型 | 可用Tab |
|
||||
| --- | --- |
|
||||
| directory | 基本信息、权限配置、多语言 |
|
||||
| page | 基本信息、权限配置、多语言 |
|
||||
| data | 基本信息、字段定义、列表配置、编辑配置、查询配置、权限配置、多语言 |
|
||||
|
||||
|
||||
### 各子Tab功能
|
||||
|
||||
1. **基本信息**:编辑 `b_module` 的核心字段(b_code、b_name、b_module_type、b_route、b_viewtable、b_savetable、b_keyfield、b_orderfield、b_parent_id、b_canmenu、b_canuse、b_xh),表单保存
|
||||
2. **字段定义**:展示 `b_module_field` 列表,**不可手工增删**。提供「同步字段」按钮,调用 `describeApi` 获取 `b_viewtable` 的列结构,与已有字段合并(保留已有配置、追加新列、标记已删列为不可用)。支持编辑 `b_name`、`b_type`、`b_default_value`、`b_options`、`b_canuse`
|
||||
3. **列表配置**:管理 `b_module_field_list`(字段列配置)和 `b_module_field_group`(多级表头分组)。表格展示列表配置行,支持新增/删除。分组管理弹窗支持树形分组增删改
|
||||
4. **编辑配置**:管理 `b_module_field_edit`(编辑属性)和 `b_module_field_group`(编辑分组)。表格展示编辑配置行,支持新增/删除。共享分组管理组件
|
||||
5. **查询配置**:管理 `b_module_field_query`,表格展示查询条件配置,支持新增/删除
|
||||
6. **权限配置**:管理 `b_module_power`,表格展示操作权限(b_code、b_name),支持新增/删除
|
||||
7. **多语言**:管理 `b_i18n`,表格展示翻译条目(b_key、b_locale、b_value),支持新增/删除
|
||||
|
||||
### 操作按钮排列
|
||||
|
||||
所有子Tab面板内,新增、删除、保存等操作按钮栏**靠左对齐**显示。
|
||||
|
||||
### 数据保存策略
|
||||
|
||||
- **主表 b_module**:选中模块后直接编辑表单并保存,新增模块先调用 `nextIdApi` 获取雪花ID
|
||||
- **子表**(b_module_field/b_module_field_list等):新增行使用 `createTempId()` 生成临时负数ID,调用 `saveObjectApi` 统一保存变更
|
||||
- **保存时**:使用 `diffRows()` 对比草稿与原始数据,通过 `tableChange()` 构建变更请求,多表变更一次提交到 `saveObjectApi`
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- 前端框架:Vue 3(`<script setup>` SFCs)
|
||||
- 状态管理:组件内部 ref/reactive(不引入额外 Pinia store)
|
||||
- UI 组件库:antdv-next(自动导入)+ 项目封装组件(FmsTable、FmsTree、FmsSelect)
|
||||
- 样式:SCSS scoped + CSS 变量主题(`--fms-*`)
|
||||
- 数据获取:复用现有通用接口 `loadDataApi`、`saveObjectApi`、`nextIdApi`、`describeApi`
|
||||
- 工具函数:`createTempId`(临时ID)、`diffRows`/`allocateTemporaryIds`/`remapForeignKeys`/`tableChange`/`hasChanges`(变更追踪)
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
### 整体策略
|
||||
|
||||
采用**声明式数据驱动**模式:每个子Tab面板作为独立组件接收模块ID和完整配置数据,内部维护编辑态草稿(`cloneData`深拷贝),通过 `diffRows` 计算变更后交给父组件统一保存。
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
1. **数据加载集中在 Service 层**:`moduleManagement.js` 封装 `loadModuleTree()`(加载树)、`loadModuleConfig(moduleId)`(加载单个模块的完整配置)、`saveModuleConfig(moduleId, changes)`(保存变更),各面板组件调用 Sem 方法获取和回存数据。
|
||||
|
||||
2. **字段同步采用合并策略**(与记忆 ID: 93228844 一致):调用 `describeApi(b_viewtable)` 获取视图列 → 按列名匹配已有 `b_module_field` → 视图中已存在的保留原配置不覆盖、新增列追加默认行、视图中不存在的已有字段标记 `b_canuse=0`。
|
||||
|
||||
3. **子Tab操作按钮左对齐**:每个面板组件内部在表格上方设置工具栏,使用 `justify-content: flex-start` 确保按钮靠左排列。
|
||||
|
||||
4. **分组管理复用**:列表配置和编辑配置共享 `b_module_field_group`,通过 `FieldGroupManager.vue` 组件统一管理分组树增删改。
|
||||
|
||||
5. **保存事务性**:所有变更通过 `saveObjectApi` 一次提交多表变更数组,后端在同一事务内执行,任何表失败整体回滚。
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- **b_id 全部字符串化**:后端返回的 bigint 在 `http.js` 拦截器中统一转为字符串,前端所有ID比较使用 `String()` 包裹
|
||||
- **布尔字段为 int 0/1**:b_canuse、b_visible、b_sortable、b_required、b_readonly、b_disabled、b_canmenu 均使用 0/1
|
||||
- **b_module_field 同步后不覆盖已有配置**:b_name、b_type、b_options 等由管理员手动维护的值在同步时保留
|
||||
- **临时ID流转**:新增子表行 → `createTempId()` → 保存时 `allocateTemporaryIds()` 请求后端真实ID → 保存完成后 `loadAll` 重建草稿
|
||||
|
||||
## Architecture Design
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph 主页面
|
||||
A[index.vue<br/>左侧树 + 右侧Tab]
|
||||
end
|
||||
|
||||
subgraph Service
|
||||
S[moduleManagement.js<br/>loadModuleTree<br/>loadModuleConfig<br/>saveModuleConfig]
|
||||
end
|
||||
|
||||
subgraph 组件
|
||||
B1[ModuleBasicPanel<br/>基本信息表单]
|
||||
B2[ModuleFieldsPanel<br/>字段定义表格+同步]
|
||||
B3[ModuleListConfigPanel<br/>列表配置表格]
|
||||
B4[ModuleEditConfigPanel<br/>编辑配置表格]
|
||||
B5[ModuleQueryConfigPanel<br/>查询配置表格]
|
||||
B6[ModulePowersPanel<br/>权限表格]
|
||||
B7[ModuleI18nPanel<br/>多语言表格]
|
||||
G[FieldGroupManager<br/>共用分组管理]
|
||||
end
|
||||
|
||||
subgraph API
|
||||
API1[loadDataApi]
|
||||
API2[saveObjectApi]
|
||||
API3[nextIdApi]
|
||||
API4[describeApi]
|
||||
end
|
||||
|
||||
A --> S
|
||||
S --> API1
|
||||
S --> API2
|
||||
S --> API3
|
||||
S --> API4
|
||||
A --> B1
|
||||
A --> B2
|
||||
A --> B3
|
||||
A --> B4
|
||||
A --> B5
|
||||
A --> B6
|
||||
A --> B7
|
||||
B3 --> G
|
||||
B4 --> G
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
fms-vue/src/
|
||||
├── services/
|
||||
│ └── moduleManagement.js # [NEW] 模块管理服务层
|
||||
│ # - loadModuleTree(): 加载所有模块并构建树形数据
|
||||
│ # - loadModuleConfig(moduleId): 加载单个模块的完整配置
|
||||
│ # (b_module + b_module_field + b_module_field_list
|
||||
│ # + b_module_field_edit + b_module_field_query
|
||||
│ # + b_module_field_group + b_module_power + b_i18n)
|
||||
│ # - saveModuleConfig(moduleId, changes, oldData): 保存模块配置
|
||||
│ # 对比草稿和原始数据,构建多表变更请求
|
||||
│ # - syncFields(moduleId, viewTable): 同步字段
|
||||
│ # 调用describeApi获取列结构并合并
|
||||
│ # - typeFromSqlType(sqlType): SQL类型到b_type映射
|
||||
├── views/
|
||||
│ └── module-management/
|
||||
│ ├── index.vue # [MODIFY] 主页面容器
|
||||
│ │ # - 左侧 FmsTree 展示模块树(directory/page/data)
|
||||
│ │ # - 右侧 a-tabs 根据 b_module_type 动态显示子Tab
|
||||
│ │ # - 选中节点加载完整配置,传递给各子面板
|
||||
│ │ # - 统一收集各面板变更并调用保存
|
||||
│ └── components/
|
||||
│ ├── ModuleBasicPanel.vue # [NEW] 基本信息面板
|
||||
│ │ # - 表单编辑 b_module 核心字段
|
||||
│ │ # - b_code/b_name/b_module_type/b_parent_id 必填
|
||||
│ │ # - directory类型隐藏 route/viewtable/savetable
|
||||
│ │ # - page类型必须填写 route,data类型必须填写 viewtable
|
||||
│ │ # - 新增模块先调用 nextIdApi 获取ID
|
||||
│ ├── ModuleFieldsPanel.vue # [NEW] 字段定义面板
|
||||
│ │ # - FmsTable 展示 b_module_field 列表
|
||||
│ │ # - 工具栏「同步字段」按钮 + 保存按钮(靠左)
|
||||
│ │ # - 同步逻辑:describeApi获取列 → 合并已有字段
|
||||
│ │ # - 不可手工新增/删除行,仅可编辑属性
|
||||
│ ├── ModuleListConfigPanel.vue # [NEW] 列表配置面板
|
||||
│ │ # - FmsTable 展示 b_module_field_list
|
||||
│ │ # - 工具栏新增/删除/保存按钮(靠左)
|
||||
│ │ # - 可选字段来自 b_module_field(b_canuse=1)
|
||||
│ │ # - 分组列可打开 FieldGroupManager 弹窗
|
||||
│ ├── ModuleEditConfigPanel.vue # [NEW] 编辑配置面板
|
||||
│ │ # - FmsTable 展示 b_module_field_edit
|
||||
│ │ # - 工具栏新增/删除/保存按钮(靠左)
|
||||
│ │ # - 可选字段来自 b_module_field(b_canuse=1)
|
||||
│ │ # - 分组列可打开 FieldGroupManager 弹窗
|
||||
│ ├── ModuleQueryConfigPanel.vue # [NEW] 查询配置面板
|
||||
│ │ # - FmsTable 展示 b_module_field_query
|
||||
│ │ # - 工具栏新增/删除/保存按钮(靠左)
|
||||
│ │ # - b_component和b_operator使用下拉选择
|
||||
│ ├── ModulePowersPanel.vue # [NEW] 权限配置面板
|
||||
│ │ # - FmsTable 展示 b_module_power
|
||||
│ │ # - 工具栏新增/删除/保存按钮(靠左)
|
||||
│ │ # - b_code 唯一校验
|
||||
│ ├── ModuleI18nPanel.vue # [NEW] 多语言面板
|
||||
│ │ # - FmsTable 展示 b_i18n
|
||||
│ │ # - 工具栏新增/删除/保存按钮(靠左)
|
||||
│ │ # - 支持 zh-CN/en-US 等语言切换筛选
|
||||
│ └── FieldGroupManager.vue # [NEW] 共用分组管理组件
|
||||
│ # - 弹窗内展示分组树(b_module_field_group)
|
||||
│ # - 支持新增/编辑/删除分组节点
|
||||
│ # - 被 ModuleListConfigPanel 和 ModuleEditConfigPanel 引用
|
||||
```
|
||||
|
||||
## Key Code Structures
|
||||
|
||||
### moduleManagement.js 核心接口
|
||||
|
||||
```js
|
||||
// 加载模块树(用于左侧树展示)
|
||||
export async function loadModuleTree()
|
||||
|
||||
// 加载单个模块的完整配置(所有关联表数据)
|
||||
export async function loadModuleConfig(moduleId)
|
||||
// 返回: { module, fields, fieldList, fieldEdit, fieldQuery, groups, powers, i18n }
|
||||
|
||||
// 保存模块配置变更
|
||||
export async function saveModuleConfig(moduleId, draftData, originalData)
|
||||
// draftData 和 originalData 结构同 loadModuleConfig 返回值
|
||||
// 内部使用 diffRows 对比各子表,构建 saveObjectApi 请求
|
||||
|
||||
// 同步字段:从 b_viewtable 拉取列结构,与已有字段合并
|
||||
export async function syncFields(moduleId, viewTable, existingFields)
|
||||
|
||||
// SQL类型到 b_type 的映射
|
||||
export function typeFromSqlType(sqlType)
|
||||
// varchar/char/nvarchar → input, text/ntext → textarea
|
||||
// int/bigint/decimal/numeric/float → number, money/smallmoney → money
|
||||
// date → date, datetime/datetime2 → datetime, bit → switch
|
||||
```
|
||||
|
||||
### 各面板组件统一 Props/Emits 模式
|
||||
|
||||
```js
|
||||
// 所有面板组件遵循相同的接口约定
|
||||
defineProps({
|
||||
moduleId: { type: String, required: true }, // 当前模块ID
|
||||
data: { type: Array, required: true }, // 面板对应的数据行数组
|
||||
loading: { type: Boolean, default: false }, // 加载状态
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update']) // 数据变更时触发,传递新的行数组
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
index.vue 选中模块
|
||||
→ loadModuleConfig(moduleId) 获取原始数据
|
||||
→ cloneData 生成 draftData(编辑态草稿)
|
||||
→ 各面板接收 draftData 对应部分,用户编辑后 emit('update', newRows)
|
||||
→ index.vue 更新 draftData
|
||||
→ 用户点击「保存全部」
|
||||
→ saveModuleConfig(moduleId, draftData, originalData)
|
||||
→ diffRows 对比各子表变更
|
||||
→ allocateTemporaryIds 分配临时ID为真实ID
|
||||
→ 构建 saveObjectApi 请求数组
|
||||
→ 提交保存
|
||||
→ 重新 loadModuleConfig 刷新原始数据
|
||||
```
|
||||
|
||||
## Agent Extensions
|
||||
|
||||
### Skill
|
||||
|
||||
- **antdv-next**
|
||||
- Purpose: 查询 antdv-next 组件库的 API 文档,确认 a-tree、a-tabs、a-form、a-modal、a-select、a-button、a-space 等组件的 props/events/slots 用法
|
||||
- Expected outcome: 获取准确的组件属性定义和事件签名,确保模板中使用正确的 API
|
||||
|
||||
### SubAgent
|
||||
|
||||
- **code-explorer**
|
||||
- Purpose: 在实现过程中需要快速搜索项目中的代码模式(如 FmsTable 使用方式、dataChanges.js 调用示例、现有表单校验模式等)时使用
|
||||
- Expected outcome: 获得准确的现有代码引用,确保新代码风格一致
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
name: module-management-page
|
||||
overview: 基于新数据库结构与开发规范,从零重建 module-management 页面(替换当前占位页),并把路由 /system/metadata 改为 /system/module-management。页面以「左侧模块树 + 右侧多 Tab 配置」组织,覆盖 b_module 及其字段/分组/列表/编辑/查询/权限/多语言配置,复用现有通用接口与工具函数,不新增后端端点与服务文件。
|
||||
design:
|
||||
styleKeywords:
|
||||
- Enterprise Admin
|
||||
- Clean Professional
|
||||
- Ant Design Vue
|
||||
- Two-column Tree + Tabs
|
||||
- Subtle Blue Accent
|
||||
- Desktop-first
|
||||
fontSystem:
|
||||
fontFamily: PingFang SC, Microsoft YaHei
|
||||
heading:
|
||||
size: 16px
|
||||
weight: 600
|
||||
subheading:
|
||||
size: 14px
|
||||
weight: 500
|
||||
body:
|
||||
size: 13px
|
||||
weight: 400
|
||||
colorSystem:
|
||||
primary:
|
||||
- "#1677ff"
|
||||
- "#4096ff"
|
||||
- "#0958d9"
|
||||
background:
|
||||
- "#ffffff"
|
||||
- "#f7f8fa"
|
||||
- "#f0f5ff"
|
||||
text:
|
||||
- "#1f2329"
|
||||
- "#8a8f99"
|
||||
- "#ffffff"
|
||||
functional:
|
||||
- "#52c41a"
|
||||
- "#ff4d4f"
|
||||
- "#faad14"
|
||||
- "#e5e7eb"
|
||||
todos:
|
||||
- id: update-route-menu
|
||||
content: 修改 routes.js 与 AppSidebar.vue,将路由 /system/metadata 改为 /system/module-management(moduleCode 保留 m_metadata)
|
||||
status: completed
|
||||
- id: build-view-shell
|
||||
content: 新建 index.vue 与 composables/useModuleManagement.js:左树右 Tabs 布局、草稿加载/保存编排、权限受控工具条
|
||||
status: completed
|
||||
dependencies:
|
||||
- update-route-menu
|
||||
- id: build-basic-tree
|
||||
content: 实现 ModuleTreePanel 与 ModuleBasicInfoPanel:模块树增删改、基本信息表单与类型联动校验
|
||||
status: completed
|
||||
dependencies:
|
||||
- build-view-shell
|
||||
- id: build-field-group
|
||||
content: 实现 ModuleFieldDefinitionPanel(describe 同步合并)与 ModuleGroupPanel(多级分组树)
|
||||
status: completed
|
||||
dependencies:
|
||||
- build-view-shell
|
||||
- id: build-list-edit-query
|
||||
content: 实现 ModuleListPanel/ModuleEditPanel/ModuleQueryPanel,用 FmsTable 配置列表/编辑/查询场景
|
||||
status: completed
|
||||
dependencies:
|
||||
- build-view-shell
|
||||
- id: build-power-i18n
|
||||
content: 实现 ModulePowerPanel 与 ModuleTranslationsPanel,维护操作权限与多语言文案
|
||||
status: completed
|
||||
dependencies:
|
||||
- build-view-shell
|
||||
- id: polish-verify
|
||||
content: 用 [skill:antdv-next] 核对组件用法,补全 scoped scss 与空态,说明未执行构建/测试
|
||||
status: completed
|
||||
dependencies:
|
||||
- build-basic-tree
|
||||
- build-field-group
|
||||
- build-list-edit-query
|
||||
- build-power-i18n
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
- 依据 `新数据库结构.md` 的 schema 从零重建「模块管理」页面(替换当前占位页 `index.vue`),覆盖 `b_module` 及其字段定义、字段分组、列表/编辑/查询配置、操作权限、多语言等完整配置能力。
|
||||
- 路由由 `/system/metadata` 改为 `/system/module-management`(仅改路径与 `menuKey`,`moduleCode` 仍保留 `m_metadata` 以匹配数据库种子与权限)。
|
||||
- 严格遵循既有设计决策:字段定义来自模块查询视图(经 `POST /data/describe` 同步,不可手工增删);子表新增行用前端本地临时负数 ID(`createTempId`),主表 `b_module` 保留 `nextIdApi` 取号;保存后重建草稿使临时 ID 被真实 ID 覆盖。
|
||||
- 复用项目现有通用接口(`loadDataApi`/`saveObjectApi`/`nextIdApi`/`describeApi`)、`FmsTable`、antdv-next 组件,以及 `utils/dataChanges.js`、`utils/tempId.js`、`utils/i18n.js`,不新增后端端点与服务文件。
|
||||
- 遵守开发规范:表格尽量用 `fms-table`,桌面端实现,scoped scss 复用现有 `--fms-*` token,不执行构建/测试。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- 左侧模块树(目录/页面/数据三类,支持增删改与选中)。
|
||||
- 右侧多 Tab:基本信息、字段定义(含视图同步)、字段分组、列表配置、编辑配置、查询配置、操作权限、多语言。
|
||||
- 顶部工具条(保存/刷新)与权限受控按钮(create/update/delete 来自 `b_module_power`,sync 为同步字段)。
|
||||
- 基于通用接口与工具函数的「按模块加载草稿 → 本地编辑 → 差异保存(整事务)」工作流。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 前端:Vue 3 `<script setup>` + Vite + Pinia;UI 组件库 **antdv-next**(项目已通过 `vite.config.js` 自动按需引入,禁止手动 import 已由 resolver 处理的组件);表格统一使用项目封装的 `FmsTable`。
|
||||
- 后端:完全复用现有通用端点,不新增任何接口或服务文件。
|
||||
- 状态/工具:`stores/permissions.js`(权限门控)、`utils/dataChanges.js`、`utils/tempId.js`、`utils/i18n.js`。
|
||||
|
||||
## 实施方法
|
||||
|
||||
整体采用「单一编排视图 + 子面板」架构:`ModuleManagementView`(`index.vue`)负责选中模块、加载全量草稿、保存编排;各 `components/*.vue` 面板只负责自身区块的展示与编辑,所有可变数据集中在视图层响应式 `draft` 中,保存时由编排层统一 `diffRows` + `saveObjectApi`。
|
||||
|
||||
### 数据编排(核心)
|
||||
|
||||
1. **加载草稿 `loadDraft(moduleId)`**:并发 `loadDataApi` 拉取该模块相关的 8 张表:`b_module`(按 id)、`b_module_field`、`b_module_field_group`、`b_module_field_list`、`b_module_field_edit`、`b_module_field_query`、`b_module_power`,以及 `b_i18n`(按 `module.`/字段键前缀过滤)。结果存入响应式 `draft`,并深拷贝一份 `snapshot` 供保存时差异比对。
|
||||
2. **本地编辑**:面板直接 `push`/`splice`/`Object.assign` 修改 `draft` 中的数组与对象;子表新增行 `b_id = createTempId()`(负数字符串)。
|
||||
3. **保存 `saveDraft()`**:
|
||||
|
||||
- 主表 `b_module` 新增:先 `nextIdApi()` 取真实 ID 写入 `draft.b_module.b_id`。
|
||||
- 对每张表 `diffRows(draft[table], snapshot[table])` → `tableChange(table, diff)`,汇聚为 `requests[]`。
|
||||
- 子表 insert 含临时负数 ID 时,调用 `allocateTemporaryIds([...所有子表 insert 行])` 由后端生成真实 ID 并就地替换;随后 `remapForeignKeys` 把 `b_field_id`/`b_group_id` 指向的临时 ID 同步映射到真实 ID。
|
||||
- 单事务 `saveObjectApi(requests)`;成功后 `snapshot = cloneData(draft)`,临时 ID 已被真实 ID 覆盖。
|
||||
|
||||
4. **视图同步 `syncFields(module)`**:`describeApi(module.b_viewtable)` 返回列 `[{name,type,nullable,size}]`;按 `(b_module_id,b_field)` 合并——视图已有列保留原 `b_name/b_type/b_options/b_default_value` 不覆盖;视图新增列追加默认行(`createTempId`,`b_type` 由 SQL 类型映射:字符串→input、长文本→textarea、数值→number、金额→money、date→date、datetime→datetime、bit→switch);视图已删列保留配置但 `b_canuse=0`。该按钮受 `canPower('m_metadata','sync')` 门控,仅 `data` 且配置了 `b_viewtable` 时可用。
|
||||
|
||||
### 关键设计决策与权衡
|
||||
|
||||
- **集中草稿 + 差异保存**:复用现有 `dataChanges.js`,避免各面板各自拼请求,降低出错面;保存走 `saveobjt` 整事务,任一表失败全回滚,符合设计决策。
|
||||
- **临时 ID 策略**:严格遵循记忆决策——子表用 `createTempId`,主表用 `nextIdApi`;FK 映射在保存前完成,杜绝脏引用。
|
||||
- **FmsTable 复用**:列表/编辑/查询/权限/多语言等表格统一用 `FmsTable`,单元格编辑沿用 `TableDemo.vue` 的 `makeCellEditor` 模式(编辑行渲染控件、其余行纯文本),不重复实现表格能力,契合新规范。
|
||||
- **约束校验前置**:`b_module_type` 与字段联动校验在表单层做(directory 不可配 route/viewtable/savetable;page 必配 route;data 不可菜单/不可 route 且必配 viewtable),保存前校验 `b_code` 正则 `^[a-z][a-z0-9_]{1,49},减少后端往返报错。
|
||||
- **性能**:每模块仅加载自身数据,树节点选中才加载草稿;`describe` 仅在点击同步时调用;表格虚拟滚动(`FmsTable` 默认开启)支撑大数据量。
|
||||
|
||||
### 架构关系
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
View[ModuleManagementView index.vue] -->|load/save 编排| Draft[(响应式 draft + snapshot)]
|
||||
View --> Tree[ModuleTreePanel]
|
||||
View --> Tabs[Tab 区]
|
||||
Tabs --> Basic[ModuleBasicInfoPanel]
|
||||
Tabs --> Field[ModuleFieldDefinitionPanel 含 sync]
|
||||
Tabs --> Group[ModuleGroupPanel]
|
||||
Tabs --> List[ModuleListPanel]
|
||||
Tabs --> Edit[ModuleEditPanel]
|
||||
Tabs --> Query[ModuleQueryPanel]
|
||||
Tabs --> Power[ModulePowerPanel]
|
||||
Tabs --> I18n[ModuleTranslationsPanel]
|
||||
Draft -.diffRows/tableChange.-> Save[saveObjectApi 单事务]
|
||||
Field -.describeApi.-> Sync[视图列合并]
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
仅修改 2 个文件、新建 1 个视图与 8 个面板组件(含 1 个编排 composable):
|
||||
|
||||
```
|
||||
fms-vue/src/
|
||||
├── router/routes.js # [MODIFY] path "/system/metadata"→"/system/module-management",
|
||||
│ # name→system-module-management,meta.menuKey 同步,moduleCode 保留 "m_metadata"
|
||||
├── layouts/components/AppSidebar.vue # [MODIFY] systemPages 的 key 改为 "/system/module-management"
|
||||
└── views/module-management/
|
||||
├── index.vue # [NEW] ModuleManagementView:左树+右 Tabs 布局、工具条、
|
||||
│ # 草稿加载/保存编排、权限受控按钮(create/update/delete/sync)
|
||||
├── composables/useModuleManagement.js # [NEW] 编排逻辑:loadDraft/saveDraft/syncFields、
|
||||
│ # diffRows+tableChange+allocateTemporaryIds+remapForeignKeys、SQL 类型→b_type 映射
|
||||
└── components/
|
||||
├── ModuleTreePanel.vue # [NEW] 模块树(目录/页面/数据),选中/新增/删除,b_xh 排序
|
||||
├── ModuleBasicInfoPanel.vue # [NEW] 基本信息表单:b_code/b_name/b_module_type/b_route/
|
||||
│ # b_viewtable/b_savetable/b_keyfield/b_orderfield/b_canmenu/b_canuse/b_xh
|
||||
│ # + 类型联动校验
|
||||
├── ModuleFieldDefinitionPanel.vue # [NEW] 字段定义 FmsTable + 「同步字段」按钮(describe 合并)
|
||||
├── ModuleGroupPanel.vue # [NEW] 字段分组树(b_module_field_group 自引用),列表/编辑共用
|
||||
├── ModuleListPanel.vue # [NEW] 列表配置 FmsTable(b_module_field_list,引用 group)
|
||||
├── ModuleEditPanel.vue # [NEW] 编辑配置 FmsTable(b_module_field_edit,required/readonly/disabled/group)
|
||||
├── ModuleQueryPanel.vue # [NEW] 查询配置 FmsTable(b_module_field_query,component/operator)
|
||||
├── ModulePowerPanel.vue # [NEW] 操作权限 FmsTable(b_module_power)
|
||||
└── ModuleTranslationsPanel.vue # [NEW] 多语言 FmsTable(b_i18n,按 locale 分列编辑)
|
||||
```
|
||||
|
||||
## 关键代码结构
|
||||
|
||||
- 编排 composable 暴露:`loadDraft(moduleId)`、`saveDraft()`、`syncFields()`、`draft`(响应式)、`currentModuleId`、`permissions`(canPower 结果);各面板通过 `props` 接收对应数组、`emits` 回传变更,避免跨面板直接耦合。
|
||||
- `b_type` SQL 类型映射函数(放置于 composable):`mapSqlTypeToFieldType(typeName) → 'input'|'textarea'|'number'|'money'|'date'|'datetime'|'switch'`。
|
||||
|
||||
## 设计风格
|
||||
|
||||
企业级元数据管理后台,采用「左侧模块树 + 右侧多 Tab 配置」的经典后台布局,桌面端优先。视觉以 Ant Design Vue 设计语言为基底,主色沿用项目侧栏蓝 `#1677ff`,辅以 `--fms-*` 设计变量(边框 `#e5e7eb`、正文 `#1f2329`、次要文字 `#8a8f99`、表面 `#ffffff`/`#f7f8fa`)。整体克制、清晰、信息密度适中,重点突出当前选中模块与可编辑表格,hover/选中态用浅蓝底与蓝色描边强化。
|
||||
|
||||
## 页面结构(自上而下分区)
|
||||
|
||||
1. **顶栏工具条**:左侧显示当前模块名与 `b_code` 面包屑;右侧权限受控按钮——新增模块(create)、保存(update)、同步字段(sync)、删除(delete),按钮按 `m_metadata` 的 `b_module_power` 与 `canPower` 显隐禁用。
|
||||
2. **主体双栏**:
|
||||
|
||||
- 左栏 `ModuleTreePanel`:模块树(目录可展开,页面/数据为叶子),顶部「新建目录/页面/数据」入口,节点 hover 显示删除。选中节点高亮蓝底。
|
||||
- 右栏 Tab 区:基本信息 / 字段定义 / 字段分组 / 列表配置 / 编辑配置 / 查询配置 / 操作权限 / 多语言,共 8 个标签页,内容区统一白底卡片 + `FmsTable`。
|
||||
|
||||
3. **表单与表格**:基本信息用 antdv-next `Form`(标签右对齐、栅格两列);各配置 Tab 用 `FmsTable`,编辑行内联控件(Input/InputNumber/Select/Switch),非编辑行纯文本;分组树用 `FmsTree` 或 `Tree` 组件展示多级 `b_module_field_group`。
|
||||
4. **空态**:未选中模块时主区展示引导空态(图标 + 「请选择或新建模块」),不显示报错。
|
||||
|
||||
## 交互与动效
|
||||
|
||||
- 树节点选中平滑高亮;Tab 切换保留各面板本地编辑(草稿集中在视图层,不随 Tab 卸载丢失)。
|
||||
- 表格编辑行浅蓝底(`rgba(22,119,255,0.08)`),hover 行浅灰底;按钮、输入框 focus 蓝色描边过渡 0.2s。
|
||||
- 保存成功 `message.success` 轻提示;删除/同步等有副作用操作使用 `Popconfirm` 二次确认。
|
||||
- 响应式:固定双栏,左栏 260px 可随侧栏收起;右栏内容随容器宽度自适应,表格横向虚拟滚动。
|
||||
|
||||
## 可复用的智能体扩展
|
||||
|
||||
### Skill
|
||||
|
||||
- **antdv-next**
|
||||
- 目的:在编写 `ModuleBasicInfoPanel` 表单、`FmsTable` 单元格编辑器、树与弹窗等 UI 时,查询 antdv-next 组件的 API、props/events/slots 与用法示例,确保组件用法正确。
|
||||
- 预期结果:各面板正确使用 antdv-next 组件(Form/Select/Switch/Tree/Popconfirm/Tabs 等),避免属性误用;保证 UI 实现符合组件库规范且与项目自动引入配置兼容。
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: module-management-subtable-layout
|
||||
overview: 修复 module-management 模块管理页子表(字段/列表/表单/查询/权限/翻译)的布局:1) 让子表区域通过 flex:1 正确填满可用高度(表格宽高 100%);2) 缩小各子表列宽,避免列沾满整个页面。
|
||||
todos:
|
||||
- id: fix-container-height
|
||||
content: 修复 index.vue 容器高度链,使子表区可纵向撑满
|
||||
status: completed
|
||||
- id: constrain-table-width
|
||||
content: 调整 MetadataTablePanel 约束表格宽度跟随内容,避免列拉伸占满
|
||||
status: completed
|
||||
dependencies:
|
||||
- fix-container-height
|
||||
- id: shrink-column-widths
|
||||
content: 缩小字段/列表/表单/权限/翻译/查询六个子表面板列宽至紧凑值
|
||||
status: completed
|
||||
dependencies:
|
||||
- constrain-table-width
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
module-management(模块配置)页面中涉及子表的地方布局不合理,需要修正:
|
||||
|
||||
1. 子表区域通过外层 `flex:1` 撑满可用高度,表格宽高 `100%`。
|
||||
2. 各子表列宽缩小、不要过大,且不要拉伸占满整个页面。
|
||||
|
||||
## 产品概述
|
||||
|
||||
优化模块管理页各子表面板(字段定义、列表配置、表单配置、查询配置、操作权限、翻译)的布局与列宽。这些面板共用 `MetadataTablePanel` + 共享组件 `FmsTable`(基于 stk-table-vue),目标是让子表纵向铺满、列宽紧凑、不拉伸占满整页。基本信息(表单)不在本次范围。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- 子表纵向填满:修复 index.vue 容器高度链,使表格区随内容区高度自适应铺满。
|
||||
- 列宽紧凑化:缩小各子表列定义的 `width` 数值,避免列过宽。
|
||||
- 表格不拉伸:确保列宽合计小于页面时,表格宽度跟随内容,不自动拉伸占满整页。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 前端:Vue 3 `<script setup>` + Vite + antdv-next + SCSS(scoped)
|
||||
- 表格:共享组件 `FmsTable`(封装 `stk-table-vue` 的 `StkTable`),子表统一通过 `MetadataTablePanel` 使用
|
||||
- 仅调整样式与列宽数值,不新增/修改任何后端接口或业务逻辑
|
||||
|
||||
## 实施方案
|
||||
|
||||
### 1. 高度链修复(主因)
|
||||
|
||||
当前 `.metadata-page` 是 grid,但只定义了 `grid-template-columns`,未定义行,导致 `.workspace` 高度塌缩为内容高度;其下游 `.config-tabs` 的 `height: calc(100% - 58px)` 与 `MetadataTablePanel` 的 `height:100%` / `.table-body` 的 `flex:1` 全部失效,表格区拿不到高度。
|
||||
|
||||
修复方式(仅 CSS):
|
||||
|
||||
- `.metadata-page` 增加 `grid-template-rows: minmax(0, 1fr)`,让第二列行撑满。
|
||||
- `.workspace` 改为 `display:flex; flex-direction:column; height:100%`,使 header + tabs 纵向堆叠并铺满。
|
||||
- `.workspace-spin` 及其 `.ant-spin-container`(deep)改为 `display:flex; flex-direction:column; flex:1; min-height:0`。
|
||||
- `.config-tabs` 用 `display:flex; flex-direction:column; flex:1; min-height:0` 替代 `height: calc(100% - 58px)`。
|
||||
- `MetadataTablePanel` 结构已正确(`.metadata-table-panel` flex 列 + 无 height prop 时 `height:100%`;`.table-body` `flex:1`;`FmsTable` 已是 `width/height:100%`),高度链补通后即可纵向铺满。
|
||||
|
||||
### 2. 宽度不拉伸约束
|
||||
|
||||
`FmsTable` 是共享组件(还被 authorization-management / i18n-management / dashboard 使用),**不可全局改宽**。在 `MetadataTablePanel` 内以 scoped `:deep` 选择器约束表格宽度跟随内容,确保列宽合计小于页面时不被拉伸占满:
|
||||
|
||||
```
|
||||
.table-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
:deep(.fms-table) {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 列宽紧凑化
|
||||
|
||||
在各子表面板中把列 `width` 改为紧凑值,规则:顺序/开关类 `60–80`、短文本(编码/类型/字段名/标题/语言)`120–150`、中长文本(名称/默认值/提示/多语言键)`160–220`、长文本(说明/JSON/翻译内容)`220–300`。示例目标值:
|
||||
|
||||
- `ModuleFieldsPanel`:字段 130 / 默认名称 150 / 多语言键 200 / 字段类型 110 / 字典编码 130 / 默认值 150 / 说明 200 / 启用 64
|
||||
- `ModuleListPanel`:节点类型 90 / 关联字段 170 / 上级分组 140 / 分组·列标题 160 / 标题多语言键 200 / 显示 64 / 顺序 70 / 列宽 70 / 显示类型 110 / 排序 64 / 表内编辑 80
|
||||
- `ModuleEditPanel`:分组表(分组编码 140 / 默认标题 160 / 标题多语言键 200 / 顺序 70 / 启用 64);编辑表(字段 170 / 编辑器 100 / 表单分组 140 / 输入提示 160 / 必填·只读·禁用·Form 各 64 / 顺序 70 / 栅格 70 / 编辑器JSON 240)
|
||||
- `ModulePowersPanel`:操作编码 140 / 默认名称 160 / 多语言键 220 / 顺序 70 / 启用 64
|
||||
- `ModuleTranslationsPanel`:多语言键 260 / 语言 130 / 翻译内容 300
|
||||
- `ModuleQueryPanel`:字段 180 / 查询组件 120 / 操作符 90 / 默认值 200 / 顺序 70 / 启用 64
|
||||
|
||||
## 实施注意
|
||||
|
||||
- 仅改样式与列 `width` 数值,不改动保存/同步/临时 ID/校验等任何业务逻辑,保留用户未提交改动。
|
||||
- 不修改 `FmsTable.vue` 全局宽度,避免影响其他管理页。
|
||||
- 浏览器核对(不自行 build):切换字段/列表/表单/查询/权限/翻译各 Tab 时表格纵向铺满、列不拉伸占满、必要时出现横向滚动而非撑满整页。
|
||||
- 按开发规范,完成后不执行构建/测试,交付时说明未执行。
|
||||
|
||||
## 架构设计
|
||||
|
||||
布局容器链(自上而下高度传递):
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[.metadata-page grid 行=1fr] --> B[.workspace flex column]
|
||||
B --> C[.workspace-spin flex:1]
|
||||
C --> D[header.workspace-header 固定]
|
||||
C --> E[.config-tabs flex:1]
|
||||
E --> F[.ant-tabs-tabpane height:100%]
|
||||
F --> G[MetadataTablePanel height:100% flex column]
|
||||
G --> H[.panel-toolbar 固定]
|
||||
G --> I[.table-body flex:1]
|
||||
I --> J[FmsTable width:fit-content height:100%]
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
本次仅修改前端 module-management 下的样式与列宽,不影响后端与其他页面。
|
||||
|
||||
```
|
||||
fms-vue/src/views/module-management/
|
||||
├── index.vue # [MODIFY] 修复容器高度链:.metadata-page 增加 grid-template-rows;.workspace 改 flex column;.workspace-spin 与 .config-tabs 用 flex:1 替代 calc,确保子表纵向铺满
|
||||
└── components/
|
||||
├── MetadataTablePanel.vue # [MODIFY] 以 scoped :deep(.fms-table) 约束表格 width:fit-content / max-width:100% / height:100%,防止列拉伸占满整页
|
||||
├── ModuleFieldsPanel.vue # [MODIFY] 缩小字段定义各列 width 至紧凑值
|
||||
├── ModuleListPanel.vue # [MODIFY] 缩小列表列/字段节点各列 width 至紧凑值
|
||||
├── ModuleEditPanel.vue # [MODIFY] 缩小表单分组表与编辑字段表各列 width 至紧凑值
|
||||
├── ModulePowersPanel.vue # [MODIFY] 缩小操作权限各列 width 至紧凑值
|
||||
├── ModuleTranslationsPanel.vue # [MODIFY] 缩小翻译各列 width 至紧凑值
|
||||
└── ModuleQueryPanel.vue # [MODIFY] 缩小查询条件各列 width 至紧凑值
|
||||
```
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: 重构元数据数据库结构
|
||||
overview: 按 新数据库结构.md 重写 fms-api/config/migrations/000_reset_metadata.sql,使表结构完全符合目标 schema;b_user 表保持不变,迁移脚本末尾仅种子 m_system 与 m_metadata。
|
||||
todos:
|
||||
- id: rewrite-b-module-field
|
||||
content: 重写 b_module_field:b_type/b_default_value/b_options 列与约束,删冗余列
|
||||
status: pending
|
||||
- id: add-b-module-field-group
|
||||
content: 新增 b_module_field_group 表(含自引用树),取代 b_module_field_edit_group
|
||||
status: pending
|
||||
- id: refactor-b-module-field-list
|
||||
content: 重构 b_module_field_list:引用分组表,移除 b_parent_id/b_kind/display/canedit
|
||||
status: pending
|
||||
dependencies:
|
||||
- add-b-module-field-group
|
||||
- id: refactor-b-module-field-edit
|
||||
content: 重构 b_module_field_edit:引用分组表,精简编辑器列,统一 b_xh
|
||||
status: pending
|
||||
dependencies:
|
||||
- add-b-module-field-group
|
||||
- id: update-seed-data
|
||||
content: 收敛种子数据仅保留 m_system 与 m_metadata
|
||||
status: pending
|
||||
dependencies:
|
||||
- rewrite-b-module-field
|
||||
- refactor-b-module-field-list
|
||||
- refactor-b-module-field-edit
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
根据 `新数据库结构.md` 重构元数据数据库结构,使表结构完全符合目标 schema;`b_user` 表保持不变;迁移脚本末尾的种子数据仅保留"系统管理"目录与"模块管理"模块,移除多语言管理与用户授权相关全部数据。
|
||||
|
||||
## 产品概述
|
||||
|
||||
本次为纯数据库结构重构,不涉及前后端功能页面。成果为重写 `fms-api/config/migrations/000_reset_metadata.sql` 迁移脚本,使其中所有元数据表(除 b_user 外)与目标设计文档逐一对应,并收敛种子数据。
|
||||
|
||||
## 核心改动
|
||||
|
||||
- `b_module_field`:重命名列并采用新控件类型集合,删除冗余列。
|
||||
- `b_module_field_list`:移除自包含树(b_parent_id/b_kind/b_display_type/b_canedit),改为引用统一分组表 `b_module_field_group`。
|
||||
- 新增 `b_module_field_group`:统一承载列表多级表头与编辑分组的布局分组树(取代旧的 `b_module_field_edit_group`)。
|
||||
- `b_module_field_edit`:剔除编辑器细节列,改为引用分组表的 `b_group_id`,统一显示顺序列为 `b_xh`。
|
||||
- `b_module_field_query`、`b_module_power`、`b_user_module`、`b_user_power`、`b_i18n` 已符合目标,无需改动。
|
||||
- 种子数据仅保留 `m_system` 目录与 `m_metadata` 模块及其操作权限、管理员授权;删除 `m_i18n`、`m_authorization` 全部相关行。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 目标数据库:SQL Server(Druid 连接池,按机构独立库)。
|
||||
- 变更载体:单文件 SQL 迁移脚本 `fms-api/config/migrations/000_reset_metadata.sql`,属于"RESET"脚本(执行前删除除 b_user 外的全部用户表、保留视图,再重建并插入种子)。
|
||||
- 不涉及后端 Java / 前端 Vue 代码:已 grep 确认旧列名仅在迁移脚本内出现,通用接口为表/列透传,模块管理前端当前为空占位页。
|
||||
|
||||
## 实施方法
|
||||
|
||||
完全重写 `000_reset_metadata.sql` 的建表与种子部分,使每个元数据表逐字段对齐 `新数据库结构.md`。脚本开头的"删除旧表 + 保留视图 + 重置管理员会话上下文"逻辑保持不变(它依赖 b_user,符合"b_user 不改"约束)。
|
||||
|
||||
### 关键修改点(按目标文档)
|
||||
|
||||
**1. b_module_field(混合旧列拆分)**
|
||||
|
||||
- `b_field_type varchar(30) NOT NULL` → 重命名为 `b_type`,CHECK 改为 `('input','textarea','number','money','date','datetime','switch','select')`。
|
||||
- `b_value nvarchar(500) NULL` → 重命名为 `b_default_value nvarchar(500) NULL`。
|
||||
- `b_dict_code varchar(100) NULL` → 重命名为 `b_options nvarchar(max) NULL`,新增 `CHECK (b_options IS NULL OR ISJSON(b_options) = 1)`。
|
||||
- 删除 `b_remark`、`b_inputdatetime` 两列。
|
||||
- 保留 `b_id/b_module_id/b_field/b_name/b_i18n/b_canuse` 及外键、唯一索引 `(b_module_id, b_field)`、索引 `(b_module_id, b_canuse, b_id)`。
|
||||
|
||||
**2. b_module_field_group(新增,取代 b_module_field_edit_group)**
|
||||
|
||||
- 列:`b_id bigint NOT NULL`、`b_module_id bigint NOT NULL`、`b_parent_id bigint NULL`、`b_title nvarchar(100) NOT NULL`、`b_i18n varchar(150) NULL`、`b_xh int NOT NULL DEFAULT 0`、`b_canuse int NOT NULL DEFAULT 1`。
|
||||
- 外键:`FK..._module` → b_module(b_id);`FK..._parent` → 自引用 b_module_field_group(b_id)。
|
||||
- 约束:`CK..._canuse CHECK (b_canuse IN (0,1))`;索引 `IX (b_module_id, b_canuse, b_xh, b_id)`。
|
||||
- 注意:旧的 `b_code` 列不再需要(被 b_id 稳定关联键取代),不保留。
|
||||
|
||||
**3. b_module_field_list(引用统一分组表)**
|
||||
|
||||
- 删除 `b_parent_id`、`b_kind`、`b_display_type`、`b_canedit` 及其自引用 FK `FK_b_module_field_list_parent`、shape CHECK `CK_b_module_field_list_kind` / `CK_b_module_field_list_shape` / `CK_b_module_field_list_canedit` / `CK_b_module_field_list_width`(宽度校验可保留为简单 `>0`)。
|
||||
- 新增 `b_group_id bigint NULL`,`FK_b_module_field_list_group` → b_module_field_group(b_id)(同模块分组;为空时为顶层列)。
|
||||
- 保留 `b_id/b_module_id/b_field_id/b_title/b_i18n/b_visible/b_xh/b_width/b_sortable` 及 CHECK `b_visible/b_sortable IN (0,1)`。
|
||||
- 唯一索引 `UX (b_module_id, b_field_id)`;索引 `IX (b_module_id, b_group_id, b_xh, b_id)`。
|
||||
|
||||
**4. b_module_field_edit(通用编辑配置)**
|
||||
|
||||
- 删除 `b_editor`、`b_edit_format`、`b_editor_options`、`b_placeholder`、`b_canform`、`b_edit_group`、`b_edit_span` 七列。
|
||||
- `b_edit_xh` → 重命名为 `b_xh`(DEFAULT 0)。
|
||||
- 新增 `b_group_id bigint NULL`,`FK_b_module_field_edit_group` → b_module_field_group(b_id)。
|
||||
- 保留 `b_id/b_module_id/b_field_id/b_required/b_readonly/b_disabled` 及对应 CHECK;唯一 `(b_module_id, b_field_id)`;索引 `(b_module_id, b_xh, b_id)`。
|
||||
|
||||
**5. 无需改动(已符合目标)**
|
||||
|
||||
- `b_module`(含模块类型校验、目录/页面/数据三类约束)、`b_module_field_query`、`b_module_power`、`b_user_module`、`b_user_power`、`b_i18n` 结构、约束、索引保持不变。
|
||||
|
||||
### 建表顺序(满足外键依赖)
|
||||
|
||||
`b_module` → `b_module_field_group` → `b_module_field` → `b_module_field_list` / `b_module_field_edit` / `b_module_field_query` → `b_module_power` → `b_user_module` / `b_user_power` → `b_i18n` → 种子插入。
|
||||
说明:`b_module_field_group` 必须在 `b_module_field` 之前建(list/edit 的 group_id 引用它);`b_module_field_list` 与 `b_module_field_edit` 的 `b_field_id` 依赖 `b_module_field`。
|
||||
|
||||
### 种子数据(按"只保留元数据")
|
||||
|
||||
- 保留模块:`m_system`(directory,b_canmenu=1)、`m_metadata`(page,/system/metadata,b_canmenu=1)。
|
||||
- 保留操作权限(b_module_power):`m_metadata` 的 create/update/delete/sync 四条。
|
||||
- 保留授权(b_user_module / b_user_power):管理员对 `m_metadata` 的模块权限及上述四条操作权限。
|
||||
- 移除:`m_i18n`、`m_authorization` 模块及其全部 `b_module_power`、`b_user_module`、`b_user_power` 行。
|
||||
- 固定雪花种子 ID(9 开头)相应精简为 system/metadata 两套,避免与已删除模块 ID 冲突。
|
||||
|
||||
## 实施注意
|
||||
|
||||
- 脚本头部 DROP 逻辑保留:仅删用户表(排除 b_user)、保留视图,重置 admin 会话上下文(依赖 b_user,符合约束)。
|
||||
- 保持 `SET XACT_ABORT ON` 与事务包裹,确保失败时回滚。
|
||||
- 不引入任何前端/后端代码改动;现有通用接口对列名透传,无需适配。
|
||||
- 按开发规范,不主动执行脚本或构建;交付时说明未执行,建议用户在选定的组织库(g3hd)手动执行验证。
|
||||
|
||||
## 目录结构
|
||||
|
||||
本次仅修改一个文件:
|
||||
|
||||
```
|
||||
fms-api/config/migrations/
|
||||
└── 000_reset_metadata.sql # [MODIFY] 重写建表与种子部分,对齐 新数据库结构.md:
|
||||
# - b_module_field 列重命名/精简
|
||||
# - 新增 b_module_field_group 取代 b_module_field_edit_group
|
||||
# - b_module_field_list 改引用分组表、移除自包含树
|
||||
# - b_module_field_edit 精简并引用分组表
|
||||
# - 种子仅保留 m_system + m_metadata
|
||||
# 头部 DROP 与 admin 上下文逻辑保持不变
|
||||
```
|
||||
@@ -2,6 +2,9 @@
|
||||
WARNING: This script removes every user table except dbo.b_user.
|
||||
Database views are intentionally preserved. Run it only against a selected
|
||||
organization database after confirming the target connection.
|
||||
|
||||
The metadata tables are rebuilt from scratch to match 新数据库结构.md.
|
||||
Only the b_user table is preserved; every other user table is dropped.
|
||||
*/
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
@@ -15,7 +18,7 @@ BEGIN TRANSACTION;
|
||||
WHERE b_account = N'g3soft' AND b_canuse = 1
|
||||
);
|
||||
IF @AdminId IS NULL
|
||||
THROW 51000, '未找到启用的 g3soft 用户,已取消重置。', 1;
|
||||
THROW 51000, N'未找到启用的 g3soft 用户,已取消重置。', 1;
|
||||
EXEC sys.sp_set_session_context @key = N'reset_admin_id', @value = @AdminId;
|
||||
|
||||
DECLARE @Sql nvarchar(max) = N'';
|
||||
@@ -26,7 +29,7 @@ BEGIN TRANSACTION;
|
||||
FROM sys.foreign_keys
|
||||
WHERE OBJECT_NAME(parent_object_id) <> N'b_user'
|
||||
OR OBJECT_NAME(referenced_object_id) <> N'b_user';
|
||||
IF @Sql <> N'' EXEC sys.sp_executesql @Sql;
|
||||
IF @Sql <> N'' EXEC sys.sp_executesql @Sql;
|
||||
|
||||
SET @Sql = N'';
|
||||
SELECT @Sql = @Sql + N'DROP TABLE '
|
||||
@@ -39,7 +42,7 @@ GO
|
||||
|
||||
DECLARE @AdminId bigint = TRY_CONVERT(bigint, SESSION_CONTEXT(N'reset_admin_id'));
|
||||
IF @AdminId IS NULL
|
||||
THROW 51001, '重置会话缺少管理员 ID。', 1;
|
||||
THROW 51001, N'重置会话缺少管理员 ID。', 1;
|
||||
|
||||
CREATE TABLE dbo.b_module (
|
||||
b_id bigint NOT NULL,
|
||||
@@ -69,7 +72,7 @@ GO
|
||||
CONSTRAINT CK_b_module_canuse CHECK (b_canuse IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_directory CHECK (b_module_type <> 'directory' OR (b_route IS NULL AND b_viewtable IS NULL AND b_savetable IS NULL)),
|
||||
CONSTRAINT CK_b_module_page CHECK (b_module_type <> 'page' OR b_route IS NOT NULL),
|
||||
CONSTRAINT CK_b_module_data CHECK (b_module_type <> 'data' OR b_canmenu = 0)
|
||||
CONSTRAINT CK_b_module_data CHECK (b_module_type <> 'data' OR (b_canmenu = 0 AND b_route IS NULL))
|
||||
);
|
||||
CREATE UNIQUE INDEX UX_b_module_route ON dbo.b_module (b_route) WHERE b_route IS NOT NULL;
|
||||
CREATE INDEX IX_b_module_parent ON dbo.b_module (b_parent_id, b_xh, b_id);
|
||||
@@ -80,93 +83,75 @@ GO
|
||||
b_field varchar(50) NOT NULL,
|
||||
b_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_field_type varchar(30) NOT NULL,
|
||||
b_value nvarchar(500) NULL,
|
||||
b_dict_code varchar(100) NULL,
|
||||
b_type varchar(30) NOT NULL,
|
||||
b_default_value nvarchar(500) NULL,
|
||||
b_options nvarchar(max) NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_b_module_field_canuse DEFAULT (1),
|
||||
b_remark nvarchar(500) NULL,
|
||||
b_inputdatetime datetime NOT NULL CONSTRAINT DF_b_module_field_inputdatetime DEFAULT (GETDATE()),
|
||||
CONSTRAINT PK_b_module_field PRIMARY KEY (b_id),
|
||||
CONSTRAINT FK_b_module_field_module FOREIGN KEY (b_module_id) REFERENCES dbo.b_module (b_id),
|
||||
CONSTRAINT UQ_b_module_field UNIQUE (b_module_id, b_field),
|
||||
CONSTRAINT CK_b_module_field_type CHECK (b_field_type IN ('string', 'text', 'number', 'date', 'datetime', 'boolean', 'select_id', 'select_input')),
|
||||
CONSTRAINT CK_b_module_field_canuse CHECK (b_canuse IN (0, 1))
|
||||
CONSTRAINT CK_b_module_field_type CHECK (b_type IN ('input', 'textarea', 'number', 'money', 'date', 'datetime', 'switch', 'select')),
|
||||
CONSTRAINT CK_b_module_field_canuse CHECK (b_canuse IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_field_options CHECK (b_options IS NULL OR ISJSON(b_options) = 1)
|
||||
);
|
||||
CREATE INDEX IX_b_module_field_module ON dbo.b_module_field (b_module_id, b_canuse, b_id);
|
||||
|
||||
CREATE TABLE dbo.b_module_field_group (
|
||||
b_id bigint NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_parent_id bigint NULL,
|
||||
b_title nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_xh int NOT NULL CONSTRAINT DF_b_module_field_group_xh DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_b_module_field_group_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_b_module_field_group PRIMARY KEY (b_id),
|
||||
CONSTRAINT FK_b_module_field_group_module FOREIGN KEY (b_module_id) REFERENCES dbo.b_module (b_id),
|
||||
CONSTRAINT FK_b_module_field_group_parent FOREIGN KEY (b_parent_id) REFERENCES dbo.b_module_field_group (b_id),
|
||||
CONSTRAINT CK_b_module_field_group_canuse CHECK (b_canuse IN (0, 1))
|
||||
);
|
||||
CREATE INDEX IX_b_module_field_group_module ON dbo.b_module_field_group (b_module_id, b_canuse, b_xh, b_id);
|
||||
|
||||
CREATE TABLE dbo.b_module_field_list (
|
||||
b_id bigint NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_parent_id bigint NULL,
|
||||
b_kind varchar(20) NOT NULL,
|
||||
b_field_id bigint NULL,
|
||||
b_group_id bigint NULL,
|
||||
b_title nvarchar(100) NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_visible int NOT NULL CONSTRAINT DF_b_module_field_list_visible DEFAULT (1),
|
||||
b_xh int NOT NULL CONSTRAINT DF_b_module_field_list_xh DEFAULT (0),
|
||||
b_width int NULL,
|
||||
b_sortable int NOT NULL CONSTRAINT DF_b_module_field_list_sortable DEFAULT (0),
|
||||
b_display_type varchar(30) NULL,
|
||||
b_canedit int NOT NULL CONSTRAINT DF_b_module_field_list_canedit DEFAULT (0),
|
||||
CONSTRAINT PK_b_module_field_list PRIMARY KEY (b_id),
|
||||
CONSTRAINT FK_b_module_field_list_module FOREIGN KEY (b_module_id) REFERENCES dbo.b_module (b_id),
|
||||
CONSTRAINT FK_b_module_field_list_parent FOREIGN KEY (b_parent_id) REFERENCES dbo.b_module_field_list (b_id),
|
||||
CONSTRAINT FK_b_module_field_list_field FOREIGN KEY (b_field_id) REFERENCES dbo.b_module_field (b_id),
|
||||
CONSTRAINT CK_b_module_field_list_kind CHECK (b_kind IN ('group', 'field')),
|
||||
CONSTRAINT CK_b_module_field_list_shape CHECK (
|
||||
(b_kind = 'group' AND b_field_id IS NULL AND b_width IS NULL AND b_display_type IS NULL AND b_canedit = 0)
|
||||
OR (b_kind = 'field' AND b_field_id IS NOT NULL AND b_display_type IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT FK_b_module_field_list_group FOREIGN KEY (b_group_id) REFERENCES dbo.b_module_field_group (b_id),
|
||||
CONSTRAINT CK_b_module_field_list_visible CHECK (b_visible IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_field_list_sortable CHECK (b_sortable IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_field_list_canedit CHECK (b_canedit IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_field_list_width CHECK (b_width IS NULL OR b_width > 0)
|
||||
);
|
||||
CREATE UNIQUE INDEX UX_b_module_field_list_field ON dbo.b_module_field_list (b_module_id, b_field_id) WHERE b_field_id IS NOT NULL;
|
||||
CREATE INDEX IX_b_module_field_list_tree ON dbo.b_module_field_list (b_module_id, b_parent_id, b_xh, b_id);
|
||||
|
||||
CREATE TABLE dbo.b_module_field_edit_group (
|
||||
b_id bigint NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_code varchar(100) NOT NULL,
|
||||
b_title nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_xh int NOT NULL CONSTRAINT DF_b_module_field_edit_group_xh DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_b_module_field_edit_group_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_b_module_field_edit_group PRIMARY KEY (b_id),
|
||||
CONSTRAINT FK_b_module_field_edit_group_module FOREIGN KEY (b_module_id) REFERENCES dbo.b_module (b_id),
|
||||
CONSTRAINT UQ_b_module_field_edit_group UNIQUE (b_module_id, b_code),
|
||||
CONSTRAINT CK_b_module_field_edit_group_canuse CHECK (b_canuse IN (0, 1))
|
||||
);
|
||||
CREATE INDEX IX_b_module_field_edit_group_module ON dbo.b_module_field_edit_group (b_module_id, b_canuse, b_xh, b_id);
|
||||
CREATE INDEX IX_b_module_field_list_tree ON dbo.b_module_field_list (b_module_id, b_group_id, b_xh, b_id);
|
||||
|
||||
CREATE TABLE dbo.b_module_field_edit (
|
||||
b_id bigint NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_field_id bigint NOT NULL,
|
||||
b_editor varchar(30) NOT NULL,
|
||||
b_edit_format varchar(100) NULL,
|
||||
b_editor_options nvarchar(max) NULL,
|
||||
b_required int NOT NULL CONSTRAINT DF_b_module_field_edit_required DEFAULT (0),
|
||||
b_readonly int NOT NULL CONSTRAINT DF_b_module_field_edit_readonly DEFAULT (0),
|
||||
b_disabled int NOT NULL CONSTRAINT DF_b_module_field_edit_disabled DEFAULT (0),
|
||||
b_placeholder nvarchar(200) NULL,
|
||||
b_canform int NOT NULL CONSTRAINT DF_b_module_field_edit_canform DEFAULT (1),
|
||||
b_edit_group varchar(100) NULL,
|
||||
b_edit_xh int NOT NULL CONSTRAINT DF_b_module_field_edit_xh DEFAULT (0),
|
||||
b_edit_span int NOT NULL CONSTRAINT DF_b_module_field_edit_span DEFAULT (24),
|
||||
b_group_id bigint NULL,
|
||||
b_xh int NOT NULL CONSTRAINT DF_b_module_field_edit_xh DEFAULT (0),
|
||||
CONSTRAINT PK_b_module_field_edit PRIMARY KEY (b_id),
|
||||
CONSTRAINT FK_b_module_field_edit_module FOREIGN KEY (b_module_id) REFERENCES dbo.b_module (b_id),
|
||||
CONSTRAINT FK_b_module_field_edit_field FOREIGN KEY (b_field_id) REFERENCES dbo.b_module_field (b_id),
|
||||
CONSTRAINT FK_b_module_field_edit_group FOREIGN KEY (b_group_id) REFERENCES dbo.b_module_field_group (b_id),
|
||||
CONSTRAINT UQ_b_module_field_edit UNIQUE (b_module_id, b_field_id),
|
||||
CONSTRAINT CK_b_module_field_edit_required CHECK (b_required IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_field_edit_readonly CHECK (b_readonly IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_field_edit_disabled CHECK (b_disabled IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_field_edit_canform CHECK (b_canform IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_field_edit_span CHECK (b_edit_span BETWEEN 1 AND 24),
|
||||
CONSTRAINT CK_b_module_field_edit_options CHECK (b_editor_options IS NULL OR ISJSON(b_editor_options) = 1)
|
||||
CONSTRAINT CK_b_module_field_edit_disabled CHECK (b_disabled IN (0, 1))
|
||||
);
|
||||
CREATE INDEX IX_b_module_field_edit_module ON dbo.b_module_field_edit (b_module_id, b_edit_xh, b_id);
|
||||
CREATE INDEX IX_b_module_field_edit_module ON dbo.b_module_field_edit (b_module_id, b_xh, b_id);
|
||||
|
||||
CREATE TABLE dbo.b_module_field_query (
|
||||
b_id bigint NOT NULL,
|
||||
@@ -238,26 +223,18 @@ GO
|
||||
|
||||
DECLARE @SystemId bigint = 910000000000000001;
|
||||
DECLARE @MetadataId bigint = 910000000000000002;
|
||||
DECLARE @I18nId bigint = 910000000000000003;
|
||||
DECLARE @AuthorizationId bigint = 910000000000000004;
|
||||
INSERT INTO dbo.b_module (b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type, b_route, b_canmenu, b_canuse, b_xh) VALUES
|
||||
(@SystemId, 'm_system', NULL, N'系统管理', 'module.m_system', 'directory', NULL, 1, 1, 100),
|
||||
(@MetadataId, 'm_metadata', @SystemId, N'元数据配置', 'module.m_metadata', 'page', '/system/metadata', 1, 1, 10),
|
||||
(@I18nId, 'm_i18n', @SystemId, N'多语言管理', 'module.m_i18n', 'page', '/system/i18n', 1, 1, 20),
|
||||
(@AuthorizationId, 'm_authorization', @SystemId, N'用户授权', 'module.m_authorization', 'page', '/system/authorization', 1, 1, 30);
|
||||
(@MetadataId, 'm_metadata', @SystemId, N'元数据配置', 'module.m_metadata', 'page', '/system/metadata', 1, 1, 10);
|
||||
|
||||
INSERT INTO dbo.b_module_power (b_id, b_module_id, b_code, b_name, b_i18n, b_xh, b_canuse) VALUES
|
||||
(910000000000000101, @MetadataId, 'create', N'新增', 'power.m_metadata.create', 10, 1),
|
||||
(910000000000000102, @MetadataId, 'update', N'保存', 'power.m_metadata.update', 20, 1),
|
||||
(910000000000000103, @MetadataId, 'delete', N'删除', 'power.m_metadata.delete', 30, 1),
|
||||
(910000000000000104, @MetadataId, 'sync', N'同步字段', 'power.m_metadata.sync', 40, 1),
|
||||
(910000000000000105, @I18nId, 'update', N'保存', 'power.m_i18n.update', 10, 1),
|
||||
(910000000000000106, @AuthorizationId, 'grant', N'授权', 'power.m_authorization.grant', 10, 1);
|
||||
(910000000000000104, @MetadataId, 'sync', N'同步字段', 'power.m_metadata.sync', 40, 1);
|
||||
|
||||
INSERT INTO dbo.b_user_module (b_id, b_user_id, b_module_id, b_inputuser_id) VALUES
|
||||
(910000000000000201, @AdminId, @MetadataId, @AdminId),
|
||||
(910000000000000202, @AdminId, @I18nId, @AdminId),
|
||||
(910000000000000203, @AdminId, @AuthorizationId, @AdminId);
|
||||
(910000000000000201, @AdminId, @MetadataId, @AdminId);
|
||||
INSERT INTO dbo.b_user_power (b_id, b_user_id, b_power_id, b_inputuser_id)
|
||||
SELECT 910000000000000300 + ROW_NUMBER() OVER (ORDER BY b_id), @AdminId, b_id, @AdminId
|
||||
FROM dbo.b_module_power;
|
||||
|
||||
@@ -8,11 +8,13 @@ import cn.g3soft.fmsapi.utils.snowflake.idgen.IdGenerator;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -56,8 +58,15 @@ public class DataController {
|
||||
}
|
||||
|
||||
@GetMapping("/nextid")
|
||||
public ApiResponse<Long> nextId() {
|
||||
return ApiResponse.success(IdGenerator.nextId());
|
||||
public ApiResponse<?> nextId(@RequestParam(required = false, defaultValue = "1") int count) {
|
||||
if (count <= 1) {
|
||||
return ApiResponse.success(IdGenerator.nextId());
|
||||
}
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
ids.add(IdGenerator.nextId());
|
||||
}
|
||||
return ApiResponse.success(ids);
|
||||
}
|
||||
|
||||
@PostMapping("/describe")
|
||||
|
||||
@@ -76,7 +76,7 @@ public class DbUtils {
|
||||
sql.append(" WHERE ").append(searchCondition.trim());
|
||||
}
|
||||
if (hasText(orderBy)) {
|
||||
sql.append(" ORDER BY ").append(buildOrderBy(orderBy));
|
||||
sql.append(" ORDER BY ").append(buildPageOrderBy(orderBy));
|
||||
}
|
||||
|
||||
String querySql = sql.toString();
|
||||
|
||||
@@ -14,9 +14,7 @@ const openKeys = ref(["system"]);
|
||||
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
|
||||
const menuItems = computed(() => {
|
||||
const systemPages = [
|
||||
{ key: "/system/metadata", label: "元数据配置", moduleCode: "m_metadata" },
|
||||
{ key: "/system/i18n", label: "多语言管理", moduleCode: "m_i18n" },
|
||||
{ key: "/system/authorization", label: "用户授权", moduleCode: "m_authorization" },
|
||||
{ key: "/system/module-management", label: "模块管理", moduleCode: "m_metadata" },
|
||||
].filter((item) => permissionStore.canModule(item.moduleCode));
|
||||
const items = [{ key: "/dashboard", icon: menuIcon(House), label: "工作台" }];
|
||||
if (systemPages.length)
|
||||
|
||||
@@ -175,7 +175,7 @@ function toggleMaximize() {
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<span class="tab-action__divider" />
|
||||
|
||||
<a-tooltip title="刷新当前页" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
@@ -186,7 +186,7 @@ function toggleMaximize() {
|
||||
<RotateCw />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<span class="tab-action__divider" />
|
||||
|
||||
<a-tooltip title="返回控制台" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
@@ -357,7 +357,7 @@ function toggleMaximize() {
|
||||
.tab-nav-button {
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 26px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
@@ -386,23 +386,21 @@ function toggleMaximize() {
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
background: var(--fms-surface);
|
||||
overflow: hidden;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tab-action {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--fms-text-secondary);
|
||||
background: var(--tabs-idle-bg);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
@@ -414,19 +412,8 @@ function toggleMaximize() {
|
||||
}
|
||||
|
||||
.lucide {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-action__divider {
|
||||
flex: none;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: var(--fms-border);
|
||||
}
|
||||
|
||||
.tab-maximize {
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -25,38 +25,16 @@ const routes = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "system/metadata",
|
||||
name: "system-metadata",
|
||||
path: "system/module-management",
|
||||
name: "system-module-management",
|
||||
component: () => import("@/views/module-management/index.vue"),
|
||||
meta: {
|
||||
title: "元数据配置",
|
||||
title: "模块管理",
|
||||
section: "系统管理",
|
||||
menuKey: "/system/metadata",
|
||||
menuKey: "/system/module-management",
|
||||
moduleCode: "m_metadata",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "system/authorization",
|
||||
name: "system-authorization",
|
||||
component: () => import("@/views/authorization-management/index.vue"),
|
||||
meta: {
|
||||
title: "用户授权",
|
||||
section: "系统管理",
|
||||
menuKey: "/system/authorization",
|
||||
moduleCode: "m_authorization",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "system/i18n",
|
||||
name: "system-i18n",
|
||||
component: () => import("@/views/i18n-management/index.vue"),
|
||||
meta: {
|
||||
title: "多语言管理",
|
||||
section: "系统管理",
|
||||
menuKey: "/system/i18n",
|
||||
moduleCode: "m_i18n",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -89,11 +89,12 @@ export function saveObjectApi(requests) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个雪花 ID。
|
||||
* @returns {Promise<ApiResult<string>>} 取号响应,雪花 ID 使用字符串避免精度丢失。
|
||||
* 获取下一个雪花 ID;传入 count 可批量获取指定数量的 ID(返回数组)。
|
||||
* @param {number} [count=1] 需要获取的 ID 数量,缺省 1。
|
||||
* @returns {Promise<ApiResult<string|string[]>>} count 为 1 时 data 为单个 ID 字符串,大于 1 时为 ID 字符串数组。
|
||||
*/
|
||||
export function nextIdApi() {
|
||||
return get("/data/nextid");
|
||||
export function nextIdApi(count = 1) {
|
||||
return get("/data/nextid", { params: { count } });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,11 +25,13 @@ export async function allocateTemporaryIds(rowGroups) {
|
||||
rowGroups
|
||||
.flat()
|
||||
.map((row) => String(row?.b_id ?? ""))
|
||||
.filter((id) => /^-\d+$/.test(id)),
|
||||
.filter((id) => /^-\d+$/.test(id))
|
||||
),
|
||||
];
|
||||
const generated = await Promise.all(temporaryIds.map(() => nextIdApi()));
|
||||
const mapping = new Map(temporaryIds.map((id, index) => [id, String(generated[index].data)]));
|
||||
if (!temporaryIds.length) return new Map();
|
||||
const res = await nextIdApi(temporaryIds.length);
|
||||
const ids = Array.isArray(res.data) ? res.data : [res.data];
|
||||
const mapping = new Map(temporaryIds.map((id, index) => [id, String(ids[index])]));
|
||||
rowGroups.flat().forEach((row) => {
|
||||
const id = mapping.get(String(row?.b_id));
|
||||
if (id) row.b_id = id;
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, ref } from "vue";
|
||||
import { App, Checkbox } from "antdv-next";
|
||||
import { RefreshCw, Save, ShieldCheck } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { allocateTemporaryIds, diffRows, tableChange } from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const { message } = App.useApp();
|
||||
const authStore = useAuthStore();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const users = ref([]);
|
||||
const modules = ref([]);
|
||||
const powers = ref([]);
|
||||
const selectedUserId = ref();
|
||||
const moduleIds = ref([]);
|
||||
const powerIds = ref([]);
|
||||
const originalModules = ref([]);
|
||||
const originalPowers = ref([]);
|
||||
const userOptions = computed(() =>
|
||||
users.value.map((item) => ({
|
||||
value: item.b_id,
|
||||
label: `${item.b_name} (${item.b_account})`,
|
||||
})),
|
||||
);
|
||||
const moduleRows = computed(() =>
|
||||
modules.value.filter((item) => item.b_module_type !== "directory"),
|
||||
);
|
||||
const powersByModule = computed(() => {
|
||||
const result = new Map();
|
||||
powers.value.forEach((power) => {
|
||||
const key = String(power.b_module_id);
|
||||
if (!result.has(key)) result.set(key, []);
|
||||
result.get(key).push(power);
|
||||
});
|
||||
return result;
|
||||
});
|
||||
const moduleIdSet = computed(() => new Set(moduleIds.value.map(String)));
|
||||
const powerIdSet = computed(() => new Set(powerIds.value.map(String)));
|
||||
const columns = computed(() => [
|
||||
{ dataIndex: "b_name", title: "模块", width: 220 },
|
||||
{ dataIndex: "b_code", title: "模块编码", width: 160 },
|
||||
{
|
||||
dataIndex: "__moduleAccess",
|
||||
title: "访问",
|
||||
width: 80,
|
||||
align: "center",
|
||||
customCell: ({ row }) =>
|
||||
h(Checkbox, {
|
||||
checked: moduleIdSet.value.has(String(row.b_id)),
|
||||
"onUpdate:checked": (checked) => toggleModule(row, checked),
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "__powers",
|
||||
title: "操作权限",
|
||||
width: 520,
|
||||
customCell: ({ row }) =>
|
||||
h(
|
||||
"div",
|
||||
{ class: "power-options" },
|
||||
(powersByModule.value.get(String(row.b_id)) || []).map((power) =>
|
||||
h(
|
||||
Checkbox,
|
||||
{
|
||||
checked: powerIdSet.value.has(String(power.b_id)),
|
||||
"onUpdate:checked": (checked) => togglePower(row, power, checked),
|
||||
},
|
||||
() => power.b_name,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
async function loadBase() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [userResult, moduleResult, powerResult] = await Promise.all([
|
||||
loadDataApi("b_user", "b_canuse = 1", "b_name ASC", [
|
||||
"b_id",
|
||||
"b_account",
|
||||
"b_name",
|
||||
"b_canuse",
|
||||
]),
|
||||
loadDataApi("b_module", "b_canuse = 1", "b_xh ASC"),
|
||||
loadDataApi("b_module_power", "b_canuse = 1", "b_xh ASC"),
|
||||
]);
|
||||
users.value = userResult.data || [];
|
||||
modules.value = moduleResult.data || [];
|
||||
powers.value = powerResult.data || [];
|
||||
if (!selectedUserId.value && users.value.length) selectedUserId.value = users.value[0].b_id;
|
||||
await loadPermissions();
|
||||
} catch (error) {
|
||||
message.error(error.message || "授权数据加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPermissions() {
|
||||
if (!selectedUserId.value) return;
|
||||
const userId = String(selectedUserId.value);
|
||||
if (!/^\d+$/.test(userId)) return message.warning("用户 ID 格式不正确");
|
||||
try {
|
||||
const [moduleResult, powerResult] = await Promise.all([
|
||||
loadDataApi("b_user_module", `b_user_id = ${userId}`, "b_id ASC"),
|
||||
loadDataApi("b_user_power", `b_user_id = ${userId}`, "b_id ASC"),
|
||||
]);
|
||||
originalModules.value = moduleResult.data || [];
|
||||
originalPowers.value = powerResult.data || [];
|
||||
moduleIds.value = originalModules.value.map((item) => item.b_module_id);
|
||||
powerIds.value = originalPowers.value.map((item) => item.b_power_id);
|
||||
} catch (error) {
|
||||
message.error(error.message || "用户权限加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleModule(module, checked) {
|
||||
const values = new Set(moduleIds.value.map(String));
|
||||
if (checked) values.add(String(module.b_id));
|
||||
else {
|
||||
values.delete(String(module.b_id));
|
||||
const modulePowerIds = new Set(
|
||||
(powersByModule.value.get(String(module.b_id)) || []).map((item) => String(item.b_id)),
|
||||
);
|
||||
powerIds.value = powerIds.value.filter((value) => !modulePowerIds.has(String(value)));
|
||||
}
|
||||
moduleIds.value = [...values];
|
||||
}
|
||||
|
||||
function togglePower(module, power, checked) {
|
||||
if (checked && !moduleIdSet.value.has(String(module.b_id))) {
|
||||
moduleIds.value = [...moduleIds.value, module.b_id];
|
||||
}
|
||||
const values = new Set(powerIds.value.map(String));
|
||||
if (checked) values.add(String(power.b_id));
|
||||
else values.delete(String(power.b_id));
|
||||
powerIds.value = [...values];
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!selectedUserId.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const currentModules = moduleIds.value.map((moduleId) => {
|
||||
const old = originalModules.value.find(
|
||||
(item) => String(item.b_module_id) === String(moduleId),
|
||||
);
|
||||
return (
|
||||
old || {
|
||||
b_id: createTempId(),
|
||||
b_user_id: selectedUserId.value,
|
||||
b_module_id: moduleId,
|
||||
b_inputuser_id: authStore.user.id,
|
||||
b_inputdatetime: new Date().toISOString().slice(0, 19).replace("T", " "),
|
||||
}
|
||||
);
|
||||
});
|
||||
const currentPowers = powerIds.value.map((powerId) => {
|
||||
const old = originalPowers.value.find((item) => String(item.b_power_id) === String(powerId));
|
||||
return (
|
||||
old || {
|
||||
b_id: createTempId(),
|
||||
b_user_id: selectedUserId.value,
|
||||
b_power_id: powerId,
|
||||
b_inputuser_id: authStore.user.id,
|
||||
b_inputdatetime: new Date().toISOString().slice(0, 19).replace("T", " "),
|
||||
}
|
||||
);
|
||||
});
|
||||
await allocateTemporaryIds([currentModules, currentPowers]);
|
||||
const moduleChanges = diffRows(currentModules, originalModules.value);
|
||||
const powerChanges = diffRows(currentPowers, originalPowers.value);
|
||||
await saveObjectApi([
|
||||
tableChange("b_user_power", powerChanges),
|
||||
tableChange("b_user_module", moduleChanges),
|
||||
]);
|
||||
message.success("用户授权已保存");
|
||||
await loadPermissions();
|
||||
} catch (error) {
|
||||
message.error(error.message || "授权保存失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadBase);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="authorization-page">
|
||||
<header class="page-header">
|
||||
<div class="page-title">
|
||||
<h1><ShieldCheck />用户授权</h1>
|
||||
<span>模块访问与操作权限</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button :loading="loading" @click="loadBase"><RefreshCw />刷新</a-button>
|
||||
<a-button type="primary" :loading="saving" :disabled="!selectedUserId" @click="save">
|
||||
<Save />保存授权
|
||||
</a-button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="user-bar">
|
||||
<span>用户</span>
|
||||
<a-select
|
||||
v-model:value="selectedUserId"
|
||||
show-search
|
||||
:options="userOptions"
|
||||
@change="loadPermissions"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<a-spin :spinning="loading">
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="moduleRows"
|
||||
row-key="b_id"
|
||||
:col-resizable="false"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.authorization-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--fms-border);
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.page-title,
|
||||
.page-title h1,
|
||||
.header-actions,
|
||||
.user-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex: none;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.page-title h1,
|
||||
.header-actions,
|
||||
.user-bar {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-title h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.page-title span {
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-bar {
|
||||
flex: none;
|
||||
|
||||
.ant-select {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.power-options) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,193 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { App } from "antdv-next";
|
||||
import { Languages, Plus, RefreshCw, Save, Trash2 } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { allocateTemporaryIds, cloneData, diffRows, tableChange } from "@/utils/dataChanges";
|
||||
import { I18N_LOCALES } from "@/utils/i18n";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const { message } = App.useApp();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const keyword = ref("");
|
||||
const rows = ref([]);
|
||||
const original = ref([]);
|
||||
const activeRowKey = ref(null);
|
||||
const selectedRowKeys = ref([]);
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
const columns = [
|
||||
{ dataIndex: "b_key", title: "多语言键", width: 320, editor: { maxlength: 150 } },
|
||||
{
|
||||
dataIndex: "b_locale",
|
||||
title: "语言",
|
||||
width: 150,
|
||||
editor: { type: "select", options: I18N_LOCALES },
|
||||
},
|
||||
{ dataIndex: "b_value", title: "翻译内容", width: 420, editor: { maxlength: 500 } },
|
||||
];
|
||||
const filteredRows = computed(() => {
|
||||
const value = keyword.value.trim().toLowerCase();
|
||||
if (!value) return rows.value;
|
||||
return rows.value.filter((row) =>
|
||||
`${row.b_key} ${row.b_locale} ${row.b_value}`.toLowerCase().includes(value),
|
||||
);
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
rows.value = (await loadDataApi("b_i18n", null, "b_key ASC")).data || [];
|
||||
original.value = cloneData(rows.value);
|
||||
selectedRowKeys.value = [];
|
||||
activeRowKey.value = null;
|
||||
} catch (error) {
|
||||
message.error(error.message || "多语言加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function add() {
|
||||
const row = { b_id: createTempId(), b_key: "", b_locale: "en-US", b_value: "" };
|
||||
rows.value.unshift(row);
|
||||
activeRowKey.value = row.b_id;
|
||||
}
|
||||
|
||||
function removeSelected() {
|
||||
const selected = new Set(selectedRowKeys.value.map(String));
|
||||
rows.value = rows.value.filter((row) => !selected.has(String(row.b_id)));
|
||||
selectedRowKeys.value = [];
|
||||
activeRowKey.value = null;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (rows.value.some((row) => !row.b_key?.trim() || !row.b_locale || !row.b_value?.trim())) {
|
||||
return message.warning("多语言键、语言和翻译内容不能为空");
|
||||
}
|
||||
const compound = rows.value.map((row) => `${row.b_key}\0${row.b_locale}`);
|
||||
if (new Set(compound).size !== compound.length) return message.warning("多语言键和语言不能重复");
|
||||
saving.value = true;
|
||||
try {
|
||||
await allocateTemporaryIds([rows.value]);
|
||||
const changes = diffRows(rows.value, original.value);
|
||||
if (!changes.inserts.length && !changes.updates.length && !changes.deletes.length) {
|
||||
return message.info("没有需要保存的修改");
|
||||
}
|
||||
await saveObjectApi([tableChange("b_i18n", changes)]);
|
||||
message.success("多语言已保存");
|
||||
await load();
|
||||
} catch (error) {
|
||||
message.error(error.message || "保存失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="i18n-page">
|
||||
<header class="page-header">
|
||||
<div class="page-title">
|
||||
<h1><Languages />多语言管理</h1>
|
||||
<span>{{ rows.length }} 条翻译</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button :loading="loading" @click="load"><RefreshCw />刷新</a-button>
|
||||
<a-button @click="add"><Plus />新增</a-button>
|
||||
<a-button danger :disabled="!selectedRowKeys.length" @click="removeSelected">
|
||||
<Trash2 />删除
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="save"><Save />保存全部</a-button>
|
||||
</div>
|
||||
</header>
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
placeholder="搜索多语言键、语言或内容"
|
||||
class="search"
|
||||
/>
|
||||
<div class="table-body">
|
||||
<a-spin :spinning="loading">
|
||||
<FmsTable
|
||||
:columns="columns"
|
||||
:data-source="filteredRows"
|
||||
row-key="b_id"
|
||||
:col-resizable="false"
|
||||
:row-selection="rowSelection"
|
||||
:active-row-key="activeRowKey"
|
||||
@row-click="(_event, row) => (activeRowKey = row.b_id)"
|
||||
/>
|
||||
</a-spin>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.i18n-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--fms-border);
|
||||
background: var(--fms-surface);
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.page-title,
|
||||
.page-title h1,
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex: none;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.page-title h1,
|
||||
.header-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-title h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.page-title span {
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 360px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,256 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import { saveObjectApi } from "@/services/api";
|
||||
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
moduleId: { type: String, required: true },
|
||||
groups: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "update"]);
|
||||
|
||||
const localGroups = ref([]);
|
||||
const selectedGroupId = ref(null);
|
||||
const editForm = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.groups,
|
||||
(val) => {
|
||||
localGroups.value = cloneData(val || []);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const treeData = computed(() => {
|
||||
return buildTreeData(localGroups.value);
|
||||
});
|
||||
|
||||
function buildTreeData(items) {
|
||||
const map = new Map();
|
||||
const roots = [];
|
||||
items.forEach((item) => {
|
||||
item.key = String(item.b_id);
|
||||
map.set(item.key, item);
|
||||
});
|
||||
items.forEach((item) => {
|
||||
if (item.b_parent_id && map.has(String(item.b_parent_id))) {
|
||||
const parent = map.get(String(item.b_parent_id));
|
||||
if (!parent.children) parent.children = [];
|
||||
parent.children.push(item);
|
||||
} else {
|
||||
roots.push(item);
|
||||
}
|
||||
});
|
||||
return roots.map((r) => ({
|
||||
key: r.key,
|
||||
title: r.b_title || `分组-${r.b_id}`,
|
||||
children: r.children?.map((c) => ({
|
||||
key: c.key,
|
||||
title: c.b_title || `分组-${c.b_id}`,
|
||||
children: c.children?.map((gc) => ({
|
||||
key: gc.key,
|
||||
title: gc.b_title || `分组-${gc.b_id}`,
|
||||
})),
|
||||
})) || [],
|
||||
}));
|
||||
}
|
||||
|
||||
const selectedGroup = computed(() =>
|
||||
localGroups.value.find((g) => String(g.b_id) === selectedGroupId.value),
|
||||
);
|
||||
|
||||
function onTreeSelect(keys) {
|
||||
if (keys.length > 0) {
|
||||
selectedGroupId.value = String(keys[0]);
|
||||
const g = selectedGroup.value;
|
||||
editForm.value = g ? { ...g } : null;
|
||||
} else {
|
||||
selectedGroupId.value = null;
|
||||
editForm.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addGroup(parentId) {
|
||||
const newGroup = {
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_parent_id: parentId || null,
|
||||
b_title: "",
|
||||
b_i18n: null,
|
||||
b_xh: localGroups.value.length * 10,
|
||||
b_canuse: 1,
|
||||
};
|
||||
localGroups.value.push(newGroup);
|
||||
selectedGroupId.value = String(newGroup.b_id);
|
||||
editForm.value = { ...newGroup };
|
||||
}
|
||||
|
||||
function deleteGroup() {
|
||||
if (!selectedGroupId.value) return;
|
||||
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== selectedGroupId.value);
|
||||
selectedGroupId.value = null;
|
||||
editForm.value = null;
|
||||
}
|
||||
|
||||
async function saveAllGroups() {
|
||||
const original = cloneData(props.groups || []);
|
||||
const changes = diffRows(localGroups.value, original);
|
||||
const req = tableChange("b_module_field_group", changes);
|
||||
if (!hasChanges(req)) {
|
||||
message.info("没有需要保存的变更");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
||||
await saveObjectApi([req]);
|
||||
emit("update", cloneData(localGroups.value));
|
||||
message.success("分组已保存");
|
||||
emit("update:visible", false);
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
:open="visible"
|
||||
title="分组管理"
|
||||
width="650px"
|
||||
@cancel="onClose"
|
||||
@ok="saveAllGroups"
|
||||
>
|
||||
<div class="group-manager">
|
||||
<div class="group-sidebar">
|
||||
<div class="group-sidebar-header">
|
||||
<span>分组树</span>
|
||||
<a-button size="small" type="primary" @click="addGroup(null)">新增</a-button>
|
||||
</div>
|
||||
<FmsTree
|
||||
v-if="treeData.length > 0"
|
||||
class="group-tree"
|
||||
:tree-data="treeData"
|
||||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||||
:selected-keys="selectedGroupId ? [selectedGroupId] : []"
|
||||
@select="onTreeSelect"
|
||||
>
|
||||
<template #title="{ key, title: nodeTitle }">
|
||||
<span class="group-tree-node">
|
||||
<span>{{ nodeTitle }}</span>
|
||||
<span class="group-tree-node-actions">
|
||||
<a-button size="small" type="text" @click.stop="addGroup(key)">+</a-button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
<a-empty v-else description="暂无分组" />
|
||||
</div>
|
||||
<div class="group-detail" v-if="editForm">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="分组标题" required>
|
||||
<a-input
|
||||
:value="editForm.b_title"
|
||||
@update:value="(v) => editForm.b_title = v"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="多语言键">
|
||||
<a-input
|
||||
:value="editForm.b_i18n"
|
||||
@update:value="(v) => editForm.b_i18n = v || null"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序号">
|
||||
<a-input-number
|
||||
:value="editForm.b_xh"
|
||||
:min="0"
|
||||
@update:value="(v) => editForm.b_xh = v"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch
|
||||
:checked="editForm.b_canuse === 1"
|
||||
@update:checked="(v) => editForm.b_canuse = v ? 1 : 0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button danger size="small" @click="deleteGroup">删除此分组</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<a-empty v-else description="选择左侧分组节点编辑" class="group-detail-empty" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.group-manager {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.group-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: none;
|
||||
width: 240px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group-sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: none;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.group-tree {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.group-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.group-tree-node-actions {
|
||||
display: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.group-tree-node:hover .group-tree-node-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.group-detail-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { Plus, Trash2 } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, required: true },
|
||||
columns: { type: Array, required: true },
|
||||
addText: { type: String, default: "新增" },
|
||||
canAdd: { type: Boolean, default: true },
|
||||
canDelete: { type: Boolean, default: true },
|
||||
height: { type: [String, Number], default: null },
|
||||
});
|
||||
const rows = defineModel("rows", { type: Array, default: () => [] });
|
||||
const emit = defineEmits(["add", "remove"]);
|
||||
const activeRowKey = ref(null);
|
||||
const selectedRowKeys = ref([]);
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
|
||||
function editRow(_event, row) {
|
||||
activeRowKey.value = String(row.b_id);
|
||||
}
|
||||
|
||||
function removeSelected() {
|
||||
const selected = new Set(selectedRowKeys.value.map(String));
|
||||
const removed = rows.value.filter((row) => selected.has(String(row.b_id)));
|
||||
rows.value = rows.value.filter((row) => !selected.has(String(row.b_id)));
|
||||
if (selected.has(String(activeRowKey.value))) activeRowKey.value = null;
|
||||
selectedRowKeys.value = [];
|
||||
emit("remove", removed);
|
||||
}
|
||||
|
||||
async function addRow() {
|
||||
emit("add");
|
||||
await nextTick();
|
||||
const row = rows.value.at(-1);
|
||||
if (row?.b_id != null) activeRowKey.value = String(row.b_id);
|
||||
}
|
||||
|
||||
defineExpose({ editRow: (row) => (activeRowKey.value = String(row.b_id)) });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="metadata-table-panel" :style="height ? null : { height: '100%' }">
|
||||
<header class="panel-toolbar">
|
||||
<strong>{{ title }}</strong>
|
||||
<div class="panel-actions">
|
||||
<slot name="actions" />
|
||||
<a-button v-if="canAdd" type="primary" @click="addRow"> <Plus />{{ addText }} </a-button>
|
||||
<a-button
|
||||
v-if="canDelete"
|
||||
danger
|
||||
:disabled="!selectedRowKeys.length"
|
||||
@click="removeSelected"
|
||||
>
|
||||
<Trash2 />删除
|
||||
</a-button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="table-body"
|
||||
:style="
|
||||
height
|
||||
? { flex: 'none', height: typeof height === 'number' ? `${height}px` : height }
|
||||
: null
|
||||
"
|
||||
>
|
||||
<FmsTable
|
||||
:columns="props.columns"
|
||||
:col-resizable="false"
|
||||
:data-source="rows"
|
||||
row-key="b_id"
|
||||
:row-selection="canDelete ? rowSelection : null"
|
||||
:active-row-key="activeRowKey"
|
||||
@row-click="editRow"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.metadata-table-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.panel-toolbar,
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
min-height: 40px;
|
||||
flex: none;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +1,174 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
parentOptions: { type: Array, default: () => [] },
|
||||
routeOptions: { type: Array, default: () => [] },
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
data: { type: Object, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
const module = defineModel("module", { type: Object, required: true });
|
||||
function setBoolean(field, checked) {
|
||||
module.value[field] = checked ? 1 : 0;
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
const formData = ref(null);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
formData.value = val ? { ...val } : null;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const isDirectory = computed(() => formData.value?.b_module_type === "directory");
|
||||
const isPage = computed(() => formData.value?.b_module_type === "page");
|
||||
const isData = computed(() => formData.value?.b_module_type === "data");
|
||||
|
||||
function emitUpdate() {
|
||||
if (formData.value) emit("update", { ...formData.value });
|
||||
}
|
||||
|
||||
function setField(field, value) {
|
||||
if (!formData.value) return;
|
||||
formData.value[field] = value === "" ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-form layout="vertical" class="basic-form">
|
||||
<a-form-item label="模块类型">
|
||||
<a-segmented
|
||||
v-model:value="module.b_module_type"
|
||||
:options="[
|
||||
{ label: '目录', value: 'directory' },
|
||||
{ label: '页面', value: 'page' },
|
||||
{ label: '数据', value: 'data' },
|
||||
]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="模块编码"
|
||||
><a-input v-model:value="module.b_code" :maxlength="50"
|
||||
/></a-form-item>
|
||||
<a-form-item label="默认名称"
|
||||
><a-input v-model:value="module.b_name" :maxlength="100"
|
||||
/></a-form-item>
|
||||
<a-form-item label="多语言键"
|
||||
><a-input v-model:value="module.b_i18n" :maxlength="150"
|
||||
/></a-form-item>
|
||||
<a-form-item label="上级模块">
|
||||
<a-select v-model:value="module.b_parent_id" allow-clear :options="parentOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="module.b_module_type === 'page'" label="已注册页面">
|
||||
<a-select v-model:value="module.b_route" :options="routeOptions" />
|
||||
</a-form-item>
|
||||
<template v-if="module.b_module_type === 'data'">
|
||||
<a-form-item label="查询视图"><a-input v-model:value="module.b_viewtable" /></a-form-item>
|
||||
<a-form-item label="保存表"><a-input v-model:value="module.b_savetable" /></a-form-item>
|
||||
<a-form-item label="主键字段"><a-input v-model:value="module.b_keyfield" /></a-form-item>
|
||||
<a-form-item label="默认排序"><a-input v-model:value="module.b_orderfield" /></a-form-item>
|
||||
</template>
|
||||
<a-form-item label="同级顺序"
|
||||
><a-input-number v-model:value="module.b_xh" :min="0"
|
||||
/></a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch :checked="module.b_canuse === 1" @change="setBoolean('b_canuse', $event)" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="module.b_module_type !== 'data'" label="显示菜单">
|
||||
<a-switch :checked="module.b_canmenu === 1" @change="setBoolean('b_canmenu', $event)" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="basic-panel" v-if="formData">
|
||||
<div class="panel-body">
|
||||
<a-form layout="vertical" class="basic-form">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块编码" required>
|
||||
<a-input
|
||||
:value="formData.b_code"
|
||||
placeholder="如 m_sea"
|
||||
@update:value="(v) => setField('b_code', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块名称" required>
|
||||
<a-input
|
||||
:value="formData.b_name"
|
||||
placeholder="模块显示名称"
|
||||
@update:value="(v) => setField('b_name', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="模块类型" required>
|
||||
<a-select
|
||||
:value="formData.b_module_type"
|
||||
:options="[
|
||||
{ label: '目录', value: 'directory' },
|
||||
{ label: '页面', value: 'page' },
|
||||
{ label: '数据', value: 'data' },
|
||||
]"
|
||||
@update:value="(v) => { formData.b_module_type = v; emitUpdate(); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="上级模块">
|
||||
<a-input :value="formData.b_parent_id" disabled placeholder="通过左侧树选中后自动设置" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="多语言键">
|
||||
<a-input
|
||||
:value="formData.b_i18n"
|
||||
placeholder="如 module.m_sea"
|
||||
@update:value="(v) => setField('b_i18n', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item label="排序号">
|
||||
<a-input-number
|
||||
:value="formData.b_xh"
|
||||
:min="0"
|
||||
@update:value="(v) => setField('b_xh', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 页面/数据 专用字段 -->
|
||||
<template v-if="!isDirectory">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8" v-if="isPage">
|
||||
<a-form-item label="页面路由" required>
|
||||
<a-input
|
||||
:value="formData.b_route"
|
||||
placeholder="如 /sea/list"
|
||||
@update:value="(v) => setField('b_route', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8" v-if="isData">
|
||||
<a-form-item label="查询视图" required>
|
||||
<a-input
|
||||
:value="formData.b_viewtable"
|
||||
placeholder="如 v_sea"
|
||||
@update:value="(v) => setField('b_viewtable', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8" v-if="isData">
|
||||
<a-form-item label="保存表">
|
||||
<a-input
|
||||
:value="formData.b_savetable"
|
||||
placeholder="如 d_sea"
|
||||
@update:value="(v) => setField('b_savetable', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16" v-if="isData">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="主键字段">
|
||||
<a-input
|
||||
:value="formData.b_keyfield"
|
||||
placeholder="如 b_id"
|
||||
@update:value="(v) => setField('b_keyfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="默认排序">
|
||||
<a-input
|
||||
:value="formData.b_orderfield"
|
||||
placeholder="如 b_xh ASC"
|
||||
@update:value="(v) => setField('b_orderfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.basic-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.basic-form {
|
||||
display: grid;
|
||||
max-width: 1080px;
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
gap: 0 16px;
|
||||
padding-top: 14px;
|
||||
max-width: 960px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
editConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "updateGroups"]);
|
||||
|
||||
const localEdit = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.editConfig,
|
||||
(val) => {
|
||||
localEdit.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localEdit.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
(props.groups || [])
|
||||
.filter((g) => g.b_canuse === 1)
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
localEdit.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: null,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_group_id: null,
|
||||
b_xh: localEdit.value.length * 10,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
localEdit.value = localEdit.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function makeSwitchCol(field, title, width) {
|
||||
return {
|
||||
dataIndex: field,
|
||||
title,
|
||||
width: width || 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell(field, (row) => setField(row, field, row[field] === 1 ? 0 : 1)),
|
||||
};
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
},
|
||||
makeSwitchCol("b_required", "必填"),
|
||||
makeSwitchCol("b_readonly", "只读"),
|
||||
makeSwitchCol("b_disabled", "禁用"),
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "编辑分组",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_group_id",
|
||||
getOptions: () => groupOptions.value,
|
||||
onChange: (row, v) => setField(row, "b_group_id", v),
|
||||
placeholder: "选择分组",
|
||||
}),
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="edit-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增行</a-button>
|
||||
<a-button @click="groupManagerVisible = true">分组管理</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localEdit" row-key="b_id" />
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
v-model:visible="groupManagerVisible"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
@update="(v) => emit('updateGroups', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-config-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const props = defineProps({ fields: { type: Array, default: () => [] } });
|
||||
const groups = defineModel("groups", { type: Array, default: () => [] });
|
||||
const edits = defineModel("edits", { type: Array, default: () => [] });
|
||||
const fieldOptions = computed(() =>
|
||||
props.fields.map((item) => ({ value: item.b_id, label: `${item.b_name} (${item.b_field})` })),
|
||||
);
|
||||
const groupOptions = computed(() =>
|
||||
groups.value.map((item) => ({ value: item.b_code, label: item.b_title })),
|
||||
);
|
||||
const groupColumns = [
|
||||
{ dataIndex: "b_code", title: "分组编码", width: 150, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_title", title: "默认标题", width: 180, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "标题多语言键", width: 250, editor: { maxlength: 150 } },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80, editor: { type: "number", min: 0 } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", editor: "checkbox" },
|
||||
];
|
||||
const editColumns = computed(() => [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 190,
|
||||
editor: { type: "select", options: fieldOptions.value },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_editor",
|
||||
title: "编辑器",
|
||||
width: 110,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["input", "textarea", "select", "date", "switch"].map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_edit_group",
|
||||
title: "表单分组",
|
||||
width: 150,
|
||||
editor: { type: "select", options: groupOptions.value },
|
||||
},
|
||||
{ dataIndex: "b_placeholder", title: "输入提示", width: 180, editor: { maxlength: 200 } },
|
||||
{ dataIndex: "b_required", title: "必填", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_readonly", title: "只读", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_disabled", title: "禁用", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_canform", title: "Form", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_edit_xh", title: "顺序", width: 75, editor: { type: "number", min: 0 } },
|
||||
{
|
||||
dataIndex: "b_edit_span",
|
||||
title: "栅格",
|
||||
width: 75,
|
||||
editor: { type: "number", min: 1, max: 24 },
|
||||
},
|
||||
{ dataIndex: "b_editor_options", title: "编辑器 JSON", width: 260, editor: {} },
|
||||
]);
|
||||
|
||||
function addGroup() {
|
||||
groups.value.push({
|
||||
b_id: createTempId(),
|
||||
b_code: `group_${groups.value.length + 1}`,
|
||||
b_title: "新分组",
|
||||
b_i18n: null,
|
||||
b_xh: Math.max(0, ...groups.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
|
||||
function addEdit() {
|
||||
const used = new Set(edits.value.map((item) => String(item.b_field_id)));
|
||||
const field = props.fields.find((item) => !used.has(String(item.b_id)));
|
||||
if (!field) return;
|
||||
edits.value.push({
|
||||
b_id: createTempId(),
|
||||
b_field_id: field.b_id,
|
||||
b_editor: "input",
|
||||
b_edit_format: null,
|
||||
b_editor_options: null,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_placeholder: null,
|
||||
b_canform: 1,
|
||||
b_edit_group: null,
|
||||
b_edit_xh: Math.max(0, ...edits.value.map((item) => Number(item.b_edit_xh) || 0)) + 10,
|
||||
b_edit_span: 24,
|
||||
});
|
||||
}
|
||||
|
||||
function handleGroupRemove(removed) {
|
||||
const removedCodes = new Set(removed.map((item) => item.b_code));
|
||||
edits.value.forEach((edit) => {
|
||||
if (removedCodes.has(edit.b_edit_group)) edit.b_edit_group = null;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="edit-panels">
|
||||
<MetadataTablePanel
|
||||
v-model:rows="groups"
|
||||
title="表单分组"
|
||||
add-text="新增分组"
|
||||
:columns="groupColumns"
|
||||
height="220px"
|
||||
@add="addGroup"
|
||||
@remove="handleGroupRemove"
|
||||
/>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="edits"
|
||||
title="编辑字段"
|
||||
add-text="新增字段"
|
||||
:columns="editColumns"
|
||||
@add="addEdit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-panels {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
grid-template-rows: auto minmax(260px, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +1,116 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { RefreshCw } from "@lucide/vue";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
defineProps({ syncing: Boolean });
|
||||
defineEmits(["sync"]);
|
||||
const fields = defineModel("fields", { type: Array, default: () => [] });
|
||||
const columns = computed(() => [
|
||||
{ dataIndex: "b_field", title: "字段", width: 150 },
|
||||
{ dataIndex: "b_name", title: "默认名称", width: 160, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 240, editor: { maxlength: 150 } },
|
||||
{
|
||||
dataIndex: "b_field_type",
|
||||
title: "字段类型",
|
||||
width: 130,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: [
|
||||
"string",
|
||||
"text",
|
||||
"number",
|
||||
"date",
|
||||
"datetime",
|
||||
"boolean",
|
||||
"select_id",
|
||||
"select_input",
|
||||
].map((value) => ({ value, label: value })),
|
||||
},
|
||||
const FIELD_TYPES = [
|
||||
{ label: "输入框", value: "input" },
|
||||
{ label: "文本域", value: "textarea" },
|
||||
{ label: "数字", value: "number" },
|
||||
{ label: "金额", value: "money" },
|
||||
{ label: "日期", value: "date" },
|
||||
{ label: "日期时间", value: "datetime" },
|
||||
{ label: "开关", value: "switch" },
|
||||
{ label: "下拉选择", value: "select" },
|
||||
];
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
fields: { type: Array, default: () => [] },
|
||||
viewTable: { type: String, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "sync"]);
|
||||
|
||||
const localFields = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.fields,
|
||||
(val) => {
|
||||
localFields.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ dataIndex: "b_dict_code", title: "字典编码", width: 150, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_value", title: "默认值", width: 160, editor: { maxlength: 500 } },
|
||||
{ dataIndex: "b_remark", title: "说明", width: 220, editor: { maxlength: 500 } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", editor: "checkbox" },
|
||||
]);
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localFields.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === "" ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_field", title: "字段名", width: 140 },
|
||||
{ dataIndex: "b_name", title: "字段名称", width: 140 },
|
||||
{
|
||||
dataIndex: "b_type",
|
||||
title: "控件类型",
|
||||
width: 130,
|
||||
customCell: createSelectCell({
|
||||
value: "b_type",
|
||||
getOptions: () => FIELD_TYPES,
|
||||
onChange: (row, v) => setField(row, "b_type", v),
|
||||
}),
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认值", width: 120 },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 140 },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 70,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => setField(row, "b_canuse", row.b_canuse === 1 ? 0 : 1)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="fields"
|
||||
title="字段定义"
|
||||
:columns="columns"
|
||||
:can-add="false"
|
||||
:can-delete="false"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button type="primary" :loading="syncing" @click="$emit('sync')">
|
||||
<RefreshCw />从视图同步
|
||||
</a-button>
|
||||
</template>
|
||||
</MetadataTablePanel>
|
||||
<div class="fields-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="viewTable"
|
||||
type="primary"
|
||||
@click="emit('sync')"
|
||||
>
|
||||
同步字段 ({{ viewTable }})
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localFields" row-key="b_id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fields-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from "@/utils/dataChanges";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createDeleteCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const i18nData = ref([]);
|
||||
const originals = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const searchKey = ref("");
|
||||
|
||||
async function loadI18n() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n", null, "b_key ASC, b_locale ASC");
|
||||
originals.value = res.data || [];
|
||||
i18nData.value = cloneData(originals.value);
|
||||
} catch (e) {
|
||||
message.error("加载多语言失败:" + (e.message || e));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.moduleId, loadI18n, { immediate: true });
|
||||
|
||||
function addRow() {
|
||||
i18nData.value.push({
|
||||
b_id: createTempId(),
|
||||
b_key: "",
|
||||
b_locale: "zh-CN",
|
||||
b_value: "",
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
i18nData.value = i18nData.value.filter((r) => r.b_id !== record.b_id);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const changes = diffRows(i18nData.value, originals.value);
|
||||
const req = tableChange("b_i18n", changes);
|
||||
if (!hasChanges(req)) {
|
||||
message.info("没有需要保存的变更");
|
||||
return;
|
||||
}
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
|
||||
await saveObjectApi([req]);
|
||||
originals.value = cloneData(i18nData.value);
|
||||
message.success("保存成功");
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const displayedData = computed(() => {
|
||||
if (!searchKey.value) return i18nData.value;
|
||||
const kw = searchKey.value.toLowerCase();
|
||||
return i18nData.value.filter(
|
||||
(r) =>
|
||||
(r.b_key || "").toLowerCase().includes(kw) ||
|
||||
(r.b_value || "").toLowerCase().includes(kw),
|
||||
);
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_key", title: "键", width: 200 },
|
||||
{ dataIndex: "b_locale", title: "语言", width: 100 },
|
||||
{ dataIndex: "b_value", title: "翻译文本", width: 300 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="i18n-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model:value="searchKey"
|
||||
placeholder="搜索键或值"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
/>
|
||||
<a-button type="primary" @click="addRow">新增翻译</a-button>
|
||||
<a-button :loading="isSaving" @click="save">保存</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="displayedData" row-key="b_id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.i18n-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import FieldGroupManager from "./FieldGroupManager.vue";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
listConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update", "updateGroups"]);
|
||||
|
||||
const localList = ref([]);
|
||||
const groupManagerVisible = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.listConfig,
|
||||
(val) => {
|
||||
localList.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localList.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
(props.groups || [])
|
||||
.filter((g) => g.b_canuse === 1)
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
localList.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: null,
|
||||
b_group_id: null,
|
||||
b_title: null,
|
||||
b_i18n: null,
|
||||
b_visible: 1,
|
||||
b_xh: localList.value.length * 10,
|
||||
b_width: null,
|
||||
b_sortable: 0,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
localList.value = localList.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
},
|
||||
{ dataIndex: "b_title", title: "列标题", width: 120 },
|
||||
{ dataIndex: "b_width", title: "列宽", width: 80 },
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "分组",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_group_id",
|
||||
getOptions: () => groupOptions.value,
|
||||
onChange: (row, v) => setField(row, "b_group_id", v),
|
||||
placeholder: "选择分组",
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_visible",
|
||||
title: "显示",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_visible", (row) => setField(row, "b_visible", row.b_visible === 1 ? 0 : 1)),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_sortable",
|
||||
title: "排序",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_sortable", (row) => setField(row, "b_sortable", row.b_sortable === 1 ? 0 : 1)),
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增列</a-button>
|
||||
<a-button @click="groupManagerVisible = true">分组管理</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localList" row-key="b_id" />
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
v-model:visible="groupManagerVisible"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
@update="(v) => emit('updateGroups', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.list-config-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const props = defineProps({ fields: { type: Array, default: () => [] } });
|
||||
const nodes = defineModel("nodes", { type: Array, default: () => [] });
|
||||
const fieldOptions = computed(() =>
|
||||
props.fields.map((item) => ({ value: item.b_id, label: `${item.b_name} (${item.b_field})` })),
|
||||
);
|
||||
const groupOptions = computed(() => [
|
||||
{ value: null, label: "顶层" },
|
||||
...nodes.value
|
||||
.filter((item) => item.b_kind === "group")
|
||||
.map((item) => ({ value: item.b_id, label: item.b_title || "未命名分组" })),
|
||||
]);
|
||||
const columns = computed(() => [
|
||||
{
|
||||
dataIndex: "b_kind",
|
||||
title: "节点类型",
|
||||
width: 100,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "group", label: "分组" },
|
||||
{ value: "field", label: "字段" },
|
||||
],
|
||||
onChange: (value, row) => {
|
||||
if (value === "group") {
|
||||
row.b_field_id = null;
|
||||
row.b_width = null;
|
||||
row.b_display_type = null;
|
||||
row.b_canedit = 0;
|
||||
} else {
|
||||
row.b_display_type ||= "text";
|
||||
row.b_width ||= 120;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "关联字段",
|
||||
width: 190,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: fieldOptions.value,
|
||||
disabled: (row) => row.b_kind === "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_parent_id",
|
||||
title: "上级分组",
|
||||
width: 150,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: (row) =>
|
||||
groupOptions.value.filter((item) => String(item.value) !== String(row.b_id)),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_title", title: "分组/列标题", width: 170, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "标题多语言键", width: 230, editor: { maxlength: 150 } },
|
||||
{ dataIndex: "b_visible", title: "显示", width: 65, align: "center", editor: "checkbox" },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 75, editor: { type: "number", min: 0 } },
|
||||
{
|
||||
dataIndex: "b_width",
|
||||
title: "列宽",
|
||||
width: 80,
|
||||
editor: { type: "number", min: 40, disabled: (row) => row.b_kind === "group" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_display_type",
|
||||
title: "显示类型",
|
||||
width: 120,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["text", "money", "date", "datetime", "tag"].map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})),
|
||||
disabled: (row) => row.b_kind === "group",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_sortable",
|
||||
title: "排序",
|
||||
width: 65,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", disabled: (row) => row.b_kind === "group" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canedit",
|
||||
title: "表内编辑",
|
||||
width: 80,
|
||||
align: "center",
|
||||
editor: { type: "checkbox", disabled: (row) => row.b_kind === "group" },
|
||||
},
|
||||
]);
|
||||
|
||||
function addNode() {
|
||||
nodes.value.push({
|
||||
b_id: createTempId(),
|
||||
b_parent_id: null,
|
||||
b_kind: "field",
|
||||
b_field_id: null,
|
||||
b_title: null,
|
||||
b_i18n: null,
|
||||
b_visible: 1,
|
||||
b_xh: Math.max(0, ...nodes.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_width: 120,
|
||||
b_sortable: 0,
|
||||
b_display_type: "text",
|
||||
b_canedit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemove(removed) {
|
||||
const removedIds = new Set(removed.map((item) => String(item.b_id)));
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
nodes.value.forEach((node) => {
|
||||
if (removedIds.has(String(node.b_parent_id)) && !removedIds.has(String(node.b_id))) {
|
||||
removedIds.add(String(node.b_id));
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
nodes.value = nodes.value.filter((node) => !removedIds.has(String(node.b_id)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="nodes"
|
||||
title="列表列与多级表头"
|
||||
add-text="新增节点"
|
||||
:columns="columns"
|
||||
@add="addNode"
|
||||
@remove="handleRemove"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,34 +1,112 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
import { createDeleteCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const powers = defineModel("powers", { type: Array, default: () => [] });
|
||||
const columns = [
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 150, editor: { maxlength: 50 } },
|
||||
{ dataIndex: "b_name", title: "默认名称", width: 180, editor: { maxlength: 100 } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 260, editor: { maxlength: 150 } },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80, editor: { type: "number", min: 0 } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", editor: "checkbox" },
|
||||
];
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
powers: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
function addPower() {
|
||||
powers.value.push({
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
const localPowers = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.powers,
|
||||
(val) => {
|
||||
localPowers.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localPowers.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
localPowers.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_code: "",
|
||||
b_name: "新操作",
|
||||
b_name: "",
|
||||
b_i18n: null,
|
||||
b_xh: Math.max(0, ...powers.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_xh: localPowers.value.length * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
localPowers.value = localPowers.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 160 },
|
||||
{ dataIndex: "b_name", title: "操作名称", width: 160 },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 160 },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80 },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 70,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => {
|
||||
row.b_canuse = row.b_canuse === 1 ? 0 : 1;
|
||||
emitUpdate();
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="powers"
|
||||
title="模块操作权限"
|
||||
add-text="新增操作"
|
||||
:columns="columns"
|
||||
@add="addPower"
|
||||
/>
|
||||
<div class="powers-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增权限</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localPowers" row-key="b_id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.powers-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { createDeleteCell, createSelectCell, createToggleCell } from "./cellRenderers";
|
||||
|
||||
const COMPONENT_OPTIONS = [
|
||||
{ label: "输入框", value: "input" },
|
||||
{ label: "下拉选择", value: "select" },
|
||||
{ label: "日期选择", value: "date-picker" },
|
||||
{ label: "数字范围", value: "number-range" },
|
||||
];
|
||||
|
||||
const OPERATOR_OPTIONS = [
|
||||
{ label: "等于 (=)", value: "=" },
|
||||
{ label: "模糊 (like)", value: "like" },
|
||||
{ label: "范围 (between)", value: "between" },
|
||||
{ label: "包含 (in)", value: "in" },
|
||||
];
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
queryConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
|
||||
const localQuery = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.queryConfig,
|
||||
(val) => {
|
||||
localQuery.value = val?.map((r) => ({ ...r })) || [];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function emitUpdate() {
|
||||
emit("update", localQuery.value.map((r) => ({ ...r })));
|
||||
}
|
||||
|
||||
function setField(row, field, value) {
|
||||
row[field] = value === undefined || value === null ? null : value;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
|
||||
function addRow() {
|
||||
localQuery.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_field_id: null,
|
||||
b_component: "input",
|
||||
b_operator: "=",
|
||||
b_default_value: null,
|
||||
b_xh: localQuery.value.length * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
localQuery.value = localQuery.value.filter((r) => r.b_id !== record.b_id);
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 160,
|
||||
customCell: createSelectCell({
|
||||
value: "b_field_id",
|
||||
getOptions: () => availableFields.value,
|
||||
onChange: (row, v) => setField(row, "b_field_id", v),
|
||||
placeholder: "选择字段",
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_component",
|
||||
title: "查询组件",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_component",
|
||||
getOptions: () => COMPONENT_OPTIONS,
|
||||
onChange: (row, v) => setField(row, "b_component", v),
|
||||
}),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_operator",
|
||||
title: "操作符",
|
||||
width: 140,
|
||||
customCell: createSelectCell({
|
||||
value: "b_operator",
|
||||
getOptions: () => OPERATOR_OPTIONS,
|
||||
onChange: (row, v) => setField(row, "b_operator", v),
|
||||
}),
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认查询值", width: 140 },
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createToggleCell("b_canuse", (row) => setField(row, "b_canuse", row.b_canuse === 1 ? 0 : 1)),
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序号", width: 80 },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 60,
|
||||
align: "center",
|
||||
customCell: createDeleteCell((row) => deleteRow(row)),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="query-config-panel">
|
||||
<div class="panel-toolbar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addRow">新增查询条件</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable :columns="columns" :data-source="localQuery" row-key="b_id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.query-config-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-body > :deep(.fms-table) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const props = defineProps({ fields: { type: Array, default: () => [] } });
|
||||
const queries = defineModel("queries", { type: Array, default: () => [] });
|
||||
const fieldOptions = computed(() =>
|
||||
props.fields.map((item) => ({ value: item.b_id, label: `${item.b_name} (${item.b_field})` })),
|
||||
);
|
||||
const columns = computed(() => [
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
title: "字段",
|
||||
width: 210,
|
||||
editor: { type: "select", options: fieldOptions.value },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_component",
|
||||
title: "查询组件",
|
||||
width: 130,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["input", "select", "date", "date-range"].map((value) => ({ value, label: value })),
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "b_operator",
|
||||
title: "操作符",
|
||||
width: 100,
|
||||
editor: {
|
||||
type: "select",
|
||||
options: ["=", "like", "between", "in"].map((value) => ({ value, label: value })),
|
||||
},
|
||||
},
|
||||
{ dataIndex: "b_default_value", title: "默认值", width: 220, editor: { maxlength: 500 } },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80, editor: { type: "number", min: 0 } },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 70, align: "center", editor: "checkbox" },
|
||||
]);
|
||||
|
||||
function addQuery() {
|
||||
const used = new Set(queries.value.map((item) => String(item.b_field_id)));
|
||||
const field = props.fields.find((item) => !used.has(String(item.b_id)));
|
||||
if (!field) return;
|
||||
queries.value.push({
|
||||
b_id: createTempId(),
|
||||
b_field_id: field.b_id,
|
||||
b_component: "input",
|
||||
b_operator: "like",
|
||||
b_default_value: null,
|
||||
b_xh: Math.max(0, ...queries.value.map((item) => Number(item.b_xh) || 0)) + 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="queries"
|
||||
title="列表查询条件"
|
||||
add-text="新增条件"
|
||||
:columns="columns"
|
||||
@add="addQuery"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,44 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { createTempId } from "@/utils/tempId";
|
||||
import { I18N_LOCALES } from "@/utils/i18n";
|
||||
import MetadataTablePanel from "./MetadataTablePanel.vue";
|
||||
|
||||
const props = defineProps({ keys: { type: Array, default: () => [] } });
|
||||
const translations = defineModel("translations", { type: Array, default: () => [] });
|
||||
const columns = computed(() => [
|
||||
{
|
||||
dataIndex: "b_key",
|
||||
title: "多语言键",
|
||||
width: 300,
|
||||
editor: { type: "select", options: props.keys.map((value) => ({ value, label: value })) },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_locale",
|
||||
title: "语言",
|
||||
width: 150,
|
||||
editor: { type: "select", options: I18N_LOCALES },
|
||||
},
|
||||
{ dataIndex: "b_value", title: "翻译内容", width: 380, editor: { maxlength: 500 } },
|
||||
]);
|
||||
|
||||
function addTranslation() {
|
||||
translations.value.push({
|
||||
b_id: createTempId(),
|
||||
b_key: props.keys[0] || "",
|
||||
b_locale: "en-US",
|
||||
b_value: "",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MetadataTablePanel
|
||||
v-model:rows="translations"
|
||||
title="模块关联翻译"
|
||||
add-text="新增翻译"
|
||||
:columns="columns"
|
||||
:can-delete="false"
|
||||
@add="addTranslation"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,164 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { RefreshCw, Search } from "@lucide/vue";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
|
||||
const props = defineProps({
|
||||
modules: { type: Array, required: true },
|
||||
protectedKeys: { type: Array, default: () => [] },
|
||||
selectedKey: { type: String, default: "" },
|
||||
loading: Boolean,
|
||||
canCreate: Boolean,
|
||||
canDelete: Boolean,
|
||||
});
|
||||
const emit = defineEmits(["select", "add", "delete", "refresh"]);
|
||||
const searchText = ref("");
|
||||
const contextNode = ref(null);
|
||||
|
||||
const treeData = computed(() => {
|
||||
const keyword = searchText.value.trim().toLowerCase();
|
||||
const codeById = new Map(props.modules.map((item) => [item.b_id, item.b_code]));
|
||||
const visible = keyword
|
||||
? new Set(
|
||||
props.modules
|
||||
.filter(
|
||||
(item) =>
|
||||
String(item.b_code).toLowerCase().includes(keyword) ||
|
||||
String(item.b_name).toLowerCase().includes(keyword),
|
||||
)
|
||||
.flatMap((item) => [item.b_code, codeById.get(item.b_parent_id)].filter(Boolean)),
|
||||
)
|
||||
: null;
|
||||
const children = new Map();
|
||||
props.modules.forEach((item) => {
|
||||
const parent = item.b_parent_id || "__root__";
|
||||
if (!children.has(parent)) children.set(parent, []);
|
||||
children.get(parent).push(item);
|
||||
});
|
||||
const build = (parent) =>
|
||||
(children.get(parent) || [])
|
||||
.filter((item) => !visible || visible.has(item.b_code))
|
||||
.map((item) => {
|
||||
const nodeChildren = build(item.b_id);
|
||||
return {
|
||||
key: item.b_code,
|
||||
title: item.b_name,
|
||||
children: nodeChildren,
|
||||
isLeaf: nodeChildren.length === 0,
|
||||
};
|
||||
});
|
||||
|
||||
return build("__root__");
|
||||
});
|
||||
|
||||
function onSelect(keys) {
|
||||
if (keys[0]) emit("select", keys[0]);
|
||||
}
|
||||
|
||||
function onRightClick({ node }) {
|
||||
contextNode.value = node;
|
||||
emit("select", node.key);
|
||||
}
|
||||
|
||||
const contextMenu = computed(() => {
|
||||
const node = contextNode.value;
|
||||
return {
|
||||
items: [
|
||||
{ key: "directory", label: "新增目录", disabled: !props.canCreate },
|
||||
{ key: "page", label: "新增页面", disabled: !props.canCreate },
|
||||
{ key: "data", label: "新增数据模块", disabled: !props.canCreate },
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
danger: true,
|
||||
disabled: !node || !props.canDelete || props.protectedKeys.includes(String(node.key)),
|
||||
},
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (!node) return;
|
||||
if (key === "delete") {
|
||||
emit("delete", node.key);
|
||||
return;
|
||||
}
|
||||
|
||||
emit("add", key, node.key);
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="module-tree-panel">
|
||||
<header>
|
||||
<div>
|
||||
<strong>模块</strong>
|
||||
<span>{{ modules.length }}</span>
|
||||
</div>
|
||||
<a-tooltip title="刷新">
|
||||
<a-button type="text" size="small" :loading="loading" @click="emit('refresh')">
|
||||
<RefreshCw :size="16" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</header>
|
||||
|
||||
<a-input v-model:value="searchText" allow-clear placeholder="搜索名称或编码">
|
||||
<template #prefix><Search :size="15" /></template>
|
||||
</a-input>
|
||||
|
||||
<a-spin :spinning="loading" class="tree-spin">
|
||||
<a-dropdown :menu="contextMenu" :trigger="['contextmenu']">
|
||||
<FmsTree
|
||||
block-node
|
||||
default-expand-all
|
||||
:tree-data="treeData"
|
||||
:selected-keys="selectedKey ? [selectedKey] : []"
|
||||
@right-click="onRightClick"
|
||||
@select="onSelect"
|
||||
/>
|
||||
</a-dropdown>
|
||||
</a-spin>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.module-tree-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
background: #fff;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
header,
|
||||
header > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
header > div {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
header span {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-spin {
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container) {
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { h } from "vue";
|
||||
import { Button, Popconfirm, Select } from "antdv-next";
|
||||
|
||||
// 单元格渲染器统一采用 stk-table-vue 的 customCell 写法:
|
||||
// customCell 会被当作函数式组件调用(props 含 row/col/rowIndex/cellValue),
|
||||
// 必须返回 h(...) 生成的 VNode,返回普通对象会渲染成 [object Object]。
|
||||
|
||||
// 删除操作单元格
|
||||
export function createDeleteCell(onDelete) {
|
||||
return (props) =>
|
||||
h(
|
||||
Popconfirm,
|
||||
{
|
||||
title: "确定删除该记录?",
|
||||
okText: "删除",
|
||||
okButtonProps: { danger: true },
|
||||
onConfirm: () => onDelete(props.row),
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{ type: "link", size: "small", danger: true, onClick: (e) => e.stopPropagation() },
|
||||
"删除",
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 是否(0/1)切换单元格
|
||||
export function createToggleCell(field, onChange) {
|
||||
return (props) => {
|
||||
const checked = props.row[field] === 1;
|
||||
return h(
|
||||
"span",
|
||||
{
|
||||
style: { cursor: "pointer" },
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onChange(props.row, field);
|
||||
},
|
||||
},
|
||||
checked ? "是" : "否",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 下拉选择单元格。getOptions 返回最新选项(保持响应式)。
|
||||
export function createSelectCell({ value, getOptions, onChange, placeholder, allowClear }) {
|
||||
return (props) =>
|
||||
h(Select, {
|
||||
value:
|
||||
props.row[value] != null && props.row[value] !== ""
|
||||
? String(props.row[value])
|
||||
: undefined,
|
||||
size: "small",
|
||||
style: { width: "100%" },
|
||||
options: getOptions(),
|
||||
allowClear: allowClear !== false,
|
||||
placeholder: placeholder || "请选择",
|
||||
"onUpdate:value": (v) => onChange(props.row, v),
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,6 +2,7 @@
|
||||
|
||||
## 一、构建与测试
|
||||
|
||||
- **不要过多封装**
|
||||
- 完成功能后不主动执行构建和测试。
|
||||
- 只有用户明确要求时,才执行构建、测试或启动服务。
|
||||
- 交付时说明本次未执行构建和测试。
|
||||
@@ -27,6 +28,7 @@
|
||||
- 不使用 CSS 重复实现组件已有的尺寸、状态、交互和响应式能力。
|
||||
- 新增 CSS 应限制在当前组件作用域内,避免影响其他页面。
|
||||
- 不需要兼容移动端,按桌面端实现即可。
|
||||
- 表格尽量使用 `fms-table` 组件(项目封装的表格组件),不重复从零实现表格能力。
|
||||
|
||||
## 四、变更范围
|
||||
|
||||
|
||||
+85
-91
@@ -6,6 +6,7 @@
|
||||
- 字段来源于数据模块配置的查询视图。系统不创建业务字段,只同步视图字段,并维护其展示、编辑、查询和权限配置。
|
||||
- 页面和业务逻辑不完全元数据化。海运等复杂业务页面由 Vue 页面显式组合多个数据模块,元数据只提供 Table、Form 和 Query 配置。
|
||||
- 字段基础定义、列表、编辑、查询四个职责分离;一个字段可以只参与其中任一场景。
|
||||
- 编辑规则与列表布局解耦:`b_module_field_edit` 是通用编辑配置,FmsForm 和 FmsTable 可按自身规则消费;`b_module_field_list` 只描述列表展示,不保存编辑状态。
|
||||
- 模块权限控制访问,操作权限控制按钮动作;操作权限必须依赖模块访问权限。
|
||||
- 表名和字段名沿用旧系统的 `b_` 风格。全部 `b_id` 使用 `bigint` 雪花 ID,返回前端时序列化为字符串。
|
||||
- 布尔字段统一使用 `int`,`1` 表示是、`0` 表示否;`b_xh` 数值越小越靠前。
|
||||
@@ -17,16 +18,16 @@
|
||||
b_module
|
||||
|-- parent_id --> b_module
|
||||
|-- 1:N --------> b_module_field
|
||||
|-- 1:N --------> b_module_field_edit_group
|
||||
|-- 1:N --------> b_module_field_group -- parent_id --> b_module_field_group
|
||||
|-- 1:N --------> b_module_power ---- 1:N ----> b_user_power
|
||||
`-- 1:N --------> b_user_module
|
||||
|
||||
b_module_field
|
||||
|-- 0:N --------> b_module_field_list -- parent_id --> b_module_field_list
|
||||
|-- 0:1 --------> b_module_field_edit
|
||||
|-- 0:N --------> b_module_field_list -- group_id --> b_module_field_group
|
||||
|-- 0:1 --------> b_module_field_edit -- group_id --> b_module_field_group
|
||||
`-- 0:1 --------> b_module_field_query
|
||||
|
||||
b_module / b_module_field / b_module_field_list / b_module_field_edit_group / b_module_power
|
||||
b_module / b_module_field / b_module_field_list / b_module_field_group / b_module_power
|
||||
`-- b_i18n --> b_i18n.b_key
|
||||
```
|
||||
|
||||
@@ -58,15 +59,15 @@ b_module / b_module_field / b_module_field_list / b_module_field_edit_group / b_
|
||||
- `b_parent_id` 外键引用 `b_module.b_id`;保存服务校验父节点不形成循环。
|
||||
- `b_code` 唯一,格式为 `^[a-z][a-z0-9_]{1,49}$`;`b_route` 非空时唯一。
|
||||
- `directory` 不能配置 `b_route`、`b_viewtable` 或 `b_savetable`。
|
||||
- `page` 必须配置 `b_route`,用于路由、菜单和页面编排;其业务数据由页面组合的数据模块提供。
|
||||
- `data` 不能显示菜单;应配置 `b_viewtable`,保存场景再按需配置 `b_savetable` 与 `b_keyfield`。
|
||||
- `page` 必须配置 `b_route`;其业务数据由页面组合的数据模块提供。
|
||||
- `data` 不能显示菜单且不能配置 `b_route`;必须配置 `b_viewtable`,保存场景再按需配置 `b_savetable` 与 `b_keyfield`。
|
||||
- `b_canmenu`、`b_canuse` 只能为 `0` 或 `1`。建议建立 `(b_parent_id, b_xh, b_id)` 索引。
|
||||
|
||||
## 字段来源与同步
|
||||
|
||||
### `dbo.b_module_field`
|
||||
|
||||
保存字段的基础定义,不保存列宽、表单位置、是否显示或查询位置等场景属性。字段来自数据模块的 `b_viewtable`,并以 `(b_module_id, b_field)` 作为稳定标识。
|
||||
保存字段的基础定义。字段来自数据模块的 `b_viewtable`,并以 `(b_module_id, b_field)` 作为稳定标识;列表、编辑和查询场景属性均由独立配置表维护。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
@@ -75,19 +76,18 @@ b_module / b_module_field / b_module_field_list / b_module_field_edit_group / b_
|
||||
| `b_field` | `varchar(50)` | 视图字段名 |
|
||||
| `b_name` | `nvarchar(100)` | 默认字段名称,由管理员维护 |
|
||||
| `b_i18n` | `varchar(150)` | 多语言键 |
|
||||
| `b_field_type` | `varchar(30)` | `string`、`text`、`number`、`date`、`datetime`、`boolean`、`select_id` 或 `select_input` |
|
||||
| `b_value` | `nvarchar(500)` | 新增时默认值 |
|
||||
| `b_dict_code` | `varchar(100)` | 字典编码;非字典字段为空 |
|
||||
| `b_type` | `varchar(30)` | 字段控件类型:`input`、`textarea`、`number`、`money`、`date`、`datetime`、`switch` 或 `select` |
|
||||
| `b_default_value` | `nvarchar(500)` | 新增记录时的默认值 |
|
||||
| `b_options` | `nvarchar(max)` | 类型专属 JSON 配置,例如选择器来源、日期格式或金额精度 |
|
||||
| `b_canuse` | `int` | 是否启用 |
|
||||
| `b_remark` | `nvarchar(500)` | 字段说明 |
|
||||
| `b_inputdatetime` | `datetime` | 首次同步创建时间 |
|
||||
|
||||
约束:
|
||||
|
||||
- `b_module_id` 外键引用 `b_module.b_id`,`(b_module_id, b_field)` 唯一。
|
||||
- `b_canuse` 只能为 `0` 或 `1`。建议建立 `(b_module_id, b_canuse, b_id)` 索引。
|
||||
- `b_canuse` 只能为 `0` 或 `1`;`b_options` 非空时通过 `ISJSON` 校验。建议建立 `(b_module_id, b_canuse, b_id)` 索引。
|
||||
- 字段没有 list、edit 或 query 配置记录时,不参与对应场景。
|
||||
- `select_id` 保存选项 ID;`select_input` 支持从选项中选择或直接输入文本。字典型选择器使用 `b_dict_code`;视图型选择器的来源配置保存在 `b_module_field_edit.b_editor_options`。
|
||||
- `b_type` 是展示和编辑的唯一类型来源:Table 按它渲染默认显示器,Form 或可编辑 Table 按它选择控件。
|
||||
- `select` 是否允许自由输入、选项来源、值字段和显示字段均由 `b_options` 控制。
|
||||
|
||||
### 视图同步规则
|
||||
|
||||
@@ -95,90 +95,82 @@ b_module / b_module_field / b_module_field_list / b_module_field_edit_group / b_
|
||||
|
||||
1. 后端校验视图名并读取列名、SQL 类型和可空性等结构信息。
|
||||
2. 按列名查找同模块的 `b_module_field`;新列创建字段基础记录。
|
||||
3. 同步更新 `b_field`、`b_field_type` 和 `b_canuse`。SQL 类型映射为 `string`、`text`、`number`、`date`、`datetime`、`boolean` 等基础类型;`select_id`、`select_input` 由管理员在同步后按业务语义调整。
|
||||
3. 新字段按 SQL 类型初始化 `b_type`,例如字符串映射为 `input`、长文本映射为 `textarea`、数值映射为 `number`、日期映射为 `date` 或 `datetime`、位值映射为 `switch`。已有字段不覆盖管理员维护的 `b_type` 与 `b_options`;`select`、`money` 等业务类型由管理员按语义调整。
|
||||
4. 本次视图中不存在的已有字段保留其记录和全部场景配置,但将 `b_canuse` 置为 `0`。
|
||||
5. 不覆盖人工维护的 `b_name`、`b_i18n`、`b_value`、`b_dict_code` 和 `b_remark`,不物理删除字段或场景配置。
|
||||
5. 不覆盖人工维护的 `b_name`、`b_i18n`、`b_default_value` 与 `b_options`,不物理删除字段或场景配置。
|
||||
|
||||
## 列表配置
|
||||
|
||||
### `dbo.b_module_field_list`
|
||||
|
||||
控制 Table 列、显示方式和多级表头。分组节点与字段节点通过 `b_parent_id` 组成任意层级的表头树;只读列表和可编辑 Table 共用本表。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||
| `b_module_id` | `bigint` | 所属模块 ID |
|
||||
| `b_parent_id` | `bigint` | 上级表头分组 ID;顶层为空 |
|
||||
| `b_kind` | `varchar(20)` | `group` 或 `field` |
|
||||
| `b_field_id` | `bigint` | 字段节点关联 `b_module_field.b_id`;分组节点为空 |
|
||||
| `b_title` | `nvarchar(100)` | 分组标题或字段标题覆盖值;字段为空时使用默认字段名称 |
|
||||
| `b_i18n` | `varchar(150)` | 分组多语言键或字段标题多语言键覆盖值 |
|
||||
| `b_visible` | `int` | 是否显示,默认 `1` |
|
||||
| `b_xh` | `int` | 同一父节点下的显示顺序,默认 `0` |
|
||||
| `b_width` | `int` | 字段列宽;分组节点为空 |
|
||||
| `b_sortable` | `int` | 是否允许排序,默认 `0` |
|
||||
| `b_display_type` | `varchar(30)` | 显示器类型,例如 `text`、`money`、`date`、`datetime`、`tag`;前端按类型使用统一显示规则 |
|
||||
| `b_canedit` | `int` | 是否允许表格单元格编辑,默认 `0` |
|
||||
|
||||
约束:
|
||||
|
||||
- `b_module_id` 外键引用 `b_module.b_id`,`b_parent_id` 外键引用本表 `b_id`,`b_field_id` 外键引用 `b_module_field.b_id`。
|
||||
- `group` 节点必须没有 `b_field_id`、`b_width`,且 `b_canedit = 0`;`field` 节点必须有 `b_field_id`。
|
||||
- 同一字段在同一模块的列树中最多出现一次,使用非空 `b_field_id` 的过滤唯一索引约束。
|
||||
- `b_parent_id` 必须指向同模块的 `group` 节点,且不能形成循环,由保存服务校验。
|
||||
- `b_visible`、`b_sortable`、`b_canedit` 只能为 `0` 或 `1`;`b_canedit = 1` 时必须存在对应编辑规则。
|
||||
- `b_display_type` 决定显示器及其格式化规则,例如 `money`、`date`、`datetime` 均由前端按统一规则渲染。建议建立 `(b_module_id, b_parent_id, b_xh, b_id)` 索引。
|
||||
- 不保存 `level`、`colspan`、`rowspan`;前端一次加载全部节点后,按 `b_parent_id`、`b_xh` 组装嵌套 `children` 并计算表头。隐藏分组的后代不显示;没有可见字段后代的分组不渲染。
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
费用(group)
|
||||
|-- 人工费(field)
|
||||
`-- 海运费(field)
|
||||
```
|
||||
|
||||
## 编辑配置
|
||||
|
||||
### `dbo.b_module_field_edit`
|
||||
|
||||
控制 Form 和可编辑 Table 的共享编辑规则,以及字段在 Form 中的布局。表单分组仅是视觉分组,不表示页签、动态页面区域或主子表关系;分组标题和多语言由独立分组表维护。
|
||||
控制列表字段列。多级表头由 `b_module_field_group` 的 `list` 分组树表达;本表不保存编辑状态。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||
| `b_module_id` | `bigint` | 所属模块 ID |
|
||||
| `b_field_id` | `bigint` | 关联 `b_module_field.b_id` |
|
||||
| `b_editor` | `varchar(30)` | 例如 `input`、`textarea`、`select`、`date`、`switch` |
|
||||
| `b_edit_format` | `varchar(100)` | 编辑值格式 |
|
||||
| `b_editor_options` | `nvarchar(max)` | 编辑器专属 JSON 配置 |
|
||||
| `b_required` | `int` | 是否必填,默认 `0` |
|
||||
| `b_readonly` | `int` | 是否只读,默认 `0` |
|
||||
| `b_disabled` | `int` | 是否禁用,默认 `0` |
|
||||
| `b_placeholder` | `nvarchar(200)` | 输入提示 |
|
||||
| `b_canform` | `int` | 是否进入 Form,默认 `1` |
|
||||
| `b_edit_group` | `varchar(100)` | 关联 `b_module_field_edit_group.b_code`;不分组时为空 |
|
||||
| `b_edit_xh` | `int` | 编辑区域中的全局顺序,默认 `0` |
|
||||
| `b_edit_span` | `int` | Form 栅格宽度,默认 `24`,范围 `1` 至 `24` |
|
||||
| `b_group_id` | `bigint` | 所属分组 ID;为空时为顶层列 |
|
||||
| `b_title` | `nvarchar(100)` | 字段标题覆盖值;为空时使用默认字段名称 |
|
||||
| `b_i18n` | `varchar(150)` | 字段标题多语言键覆盖值 |
|
||||
| `b_visible` | `int` | 是否显示,默认 `1` |
|
||||
| `b_xh` | `int` | 同一分组内的显示顺序,默认 `0` |
|
||||
| `b_width` | `int` | 字段列宽 |
|
||||
| `b_sortable` | `int` | 是否允许排序,默认 `0` |
|
||||
|
||||
约束:
|
||||
|
||||
- `b_module_id` 外键引用 `b_module.b_id`,`b_field_id` 外键引用 `b_module_field.b_id`;`(b_module_id, b_field_id)` 唯一,保存服务校验字段属于同一模块。
|
||||
- `b_required`、`b_readonly`、`b_disabled`、`b_canform` 只能为 `0` 或 `1`;`b_editor_options` 非空时通过 `ISJSON` 校验。
|
||||
- 非空 `b_edit_group` 必须引用同模块的启用分组;保存服务校验模块一致性。
|
||||
- 已分组字段按所属分组的 `b_xh` 排列,组内按 `b_edit_xh` 排列;未分组字段作为末尾默认区域,按 `b_edit_xh` 排列。建议建立 `(b_module_id, b_edit_xh, b_id)` 索引。
|
||||
- `b_canform = 0` 时,编辑规则仍可由可编辑 Table 使用。
|
||||
- `b_module_id` 外键引用 `b_module.b_id`,`b_field_id` 外键引用 `b_module_field.b_id`,`b_group_id` 引用同模块的 `b_module_field_group`。
|
||||
- `b_field_id` 必填;同一字段在同一模块的列表中最多出现一次,使用 `(b_module_id, b_field_id)` 唯一约束。
|
||||
- `b_visible`、`b_sortable` 只能为 `0` 或 `1`。
|
||||
- 字段列的显示器由关联字段的 `b_type` 与 `b_options` 决定,例如 `money`、`date`、`datetime` 均由前端按统一规则渲染。建议建立 `(b_module_id, b_group_id, b_xh, b_id)` 索引。
|
||||
- 不保存 `level`、`colspan`、`rowspan`;前端加载 Table 分组树和字段列后,按分组的 `b_parent_id`、`b_xh` 组装嵌套表头。隐藏分组的后代不显示;没有可见字段后代的分组不渲染。
|
||||
|
||||
### `dbo.b_module_field_edit_group`
|
||||
示例:
|
||||
|
||||
保存 Form 的视觉分组标题和多语言配置。一个模块可以有多个分组,字段通过 `b_edit_group` 引用其稳定编码。
|
||||
```text
|
||||
费用(`b_module_field_group`)
|
||||
|-- 人工费(`b_module_field_list.b_group_id` 指向费用)
|
||||
`-- 海运费(`b_module_field_list.b_group_id` 指向费用)
|
||||
```
|
||||
|
||||
## 编辑配置
|
||||
|
||||
### `dbo.b_module_field_edit`
|
||||
|
||||
控制字段的通用编辑属性与编辑分组。控件类型、显示规则和类型专属参数统一由 `b_module_field.b_type`、`b_options` 决定;FmsForm 与 FmsTable 自行决定何时使用本配置。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||
| `b_module_id` | `bigint` | 所属模块 ID |
|
||||
| `b_code` | `varchar(100)` | 分组稳定编码,例如 `basic`、`booking` |
|
||||
| `b_field_id` | `bigint` | 关联 `b_module_field.b_id` |
|
||||
| `b_required` | `int` | 是否必填,默认 `0` |
|
||||
| `b_readonly` | `int` | 是否只读,默认 `0` |
|
||||
| `b_disabled` | `int` | 是否禁用,默认 `0` |
|
||||
| `b_group_id` | `bigint` | 所属编辑分组 ID;不分组时为空 |
|
||||
| `b_xh` | `int` | 编辑场景中的显示顺序,默认 `0` |
|
||||
|
||||
约束:
|
||||
|
||||
- `b_module_id` 外键引用 `b_module.b_id`,`b_field_id` 外键引用 `b_module_field.b_id`;`(b_module_id, b_field_id)` 唯一,保存服务校验字段属于同一模块。
|
||||
- `b_required`、`b_readonly`、`b_disabled` 只能为 `0` 或 `1`。
|
||||
- `b_group_id` 必须关联同模块的 `b_module_field_group`;分组为空时字段进入默认编辑区域。
|
||||
- 已分组字段按分组的 `b_xh` 排列,组内按 `b_xh` 排列;默认区域字段按 `b_xh` 排列。
|
||||
|
||||
## 布局分组
|
||||
|
||||
### `dbo.b_module_field_group`
|
||||
|
||||
统一保存列表多级表头和编辑分组。分组只负责布局,不定义编辑能力;列表或编辑用途由引用它的配置表决定。
|
||||
|
||||
被 `b_module_field_list` 引用时,分组组成同一份 FmsTable 的多级表头;被 `b_module_field_edit` 引用时,分组组成同一份编辑界面中的业务区块,例如提单、订舱、报关、备注,不代表拆分为多个 Form。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||
| `b_module_id` | `bigint` | 所属模块 ID |
|
||||
| `b_parent_id` | `bigint` | 上级分组 ID;顶层为空 |
|
||||
| `b_title` | `nvarchar(100)` | 默认分组标题 |
|
||||
| `b_i18n` | `varchar(150)` | 分组标题多语言键 |
|
||||
| `b_xh` | `int` | 分组显示顺序,默认 `0` |
|
||||
@@ -186,11 +178,13 @@ b_module / b_module_field / b_module_field_list / b_module_field_edit_group / b_
|
||||
|
||||
约束:
|
||||
|
||||
- `b_module_id` 外键引用 `b_module.b_id`,`(b_module_id, b_code)` 唯一。
|
||||
- `b_module_id` 外键引用 `b_module.b_id`;`b_id` 是分组的稳定关联键。
|
||||
- `b_parent_id` 必须引用同模块的分组且不能形成循环。
|
||||
- 多级分组可用于 FmsTable 表头;FmsForm 与 FmsTable 如何消费编辑分组由各自实现决定。
|
||||
- `b_canuse` 只能为 `0` 或 `1`。建议建立 `(b_module_id, b_canuse, b_xh, b_id)` 索引。
|
||||
- 无字段引用的分组不渲染;分组停用时,其关联字段不在 Form 中显示。
|
||||
- 无字段引用的分组不渲染;分组停用时,其关联字段或列不显示。
|
||||
|
||||
视图型选择器的 `b_editor_options` 示例:
|
||||
视图型选择器的 `b_options` 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -202,7 +196,7 @@ b_module / b_module_field / b_module_field_list / b_module_field_edit_group / b_
|
||||
}
|
||||
```
|
||||
|
||||
`select_id` 使用 `allowInput: false` 并保存 `valueField` 的值;`select_input` 可使用 `allowInput: true` 并保存选择值或用户输入文本。视图名、值字段和标签字段均须由后端校验。
|
||||
`select` 使用 `allowInput: false` 时保存 `valueField` 的值;设为 `true` 时可保存选择值或用户输入文本。视图名、值字段和标签字段均须由后端校验。
|
||||
|
||||
## 查询配置
|
||||
|
||||
@@ -299,13 +293,13 @@ b_module / b_module_field / b_module_field_list / b_module_field_edit_group / b_
|
||||
|
||||
```text
|
||||
海运页面 m_sea(page:路由、菜单、权限、页面编排)
|
||||
|-- 海运主单 d_sea(data:Table / Form / Query)
|
||||
|-- 费用 d_charge(data:可编辑 Table)
|
||||
|-- 订舱 d_booking(data:独立表和主键时)
|
||||
`-- 箱明细 d_container(data:可编辑 Table)
|
||||
|-- 海运主单 d_sea(data)
|
||||
|-- 费用 d_charge(data)
|
||||
|-- 订舱 d_booking(data,独立表和主键时)
|
||||
`-- 箱明细 d_container(data)
|
||||
```
|
||||
|
||||
若订舱字段实际保存于海运主单表,不创建 `d_booking`,而在 `d_sea` 的 Form 中用 `b_module_field_edit_group` 和 `b_edit_group` 表达视觉分组。主子表关联、加载时机、保存顺序和页面布局均由业务 Vue 页面明确实现。
|
||||
若订舱字段实际保存于海运主单表,不创建 `d_booking`,而在 `d_sea` 的编辑配置中用 `b_group_id` 和 `b_xh` 定义编辑分组与顺序,分组标题来自 `b_module_field_group`。主子表关联、加载时机、保存顺序和页面布局均由业务 Vue 页面明确实现。
|
||||
|
||||
## 现有字段迁移
|
||||
|
||||
@@ -313,19 +307,19 @@ b_module / b_module_field / b_module_field_list / b_module_field_edit_group / b_
|
||||
|
||||
| 当前字段 | 目标位置 |
|
||||
| --- | --- |
|
||||
| `b_type` | `b_module_field.b_field_type` |
|
||||
| `b_type` | `b_module_field.b_type` |
|
||||
| `b_canlist` | `b_module_field_list.b_visible` |
|
||||
| `b_width` | `b_module_field_list.b_width` |
|
||||
| `b_xh` | 初始化 `b_module_field_list.b_xh`、`b_module_field_edit.b_edit_xh` 与 `b_module_field_query.b_xh` |
|
||||
| `b_canform` | `b_module_field_edit.b_canform` |
|
||||
| `b_xh` | 初始化 `b_module_field_list.b_xh`、`b_module_field_edit.b_xh` 与 `b_module_field_query.b_xh` |
|
||||
| `b_canform` | 为原 Form 字段创建 `b_module_field_edit` 记录 |
|
||||
| `b_must` | `b_module_field_edit.b_required` |
|
||||
| `b_readonly` | `b_module_field_edit.b_readonly` |
|
||||
| `b_canquery` | 创建 `b_module_field_query` 记录 |
|
||||
| `b_value` | 保留在 `b_module_field.b_value` |
|
||||
| `b_value` | 迁移为 `b_module_field.b_default_value` |
|
||||
|
||||
迁移步骤:
|
||||
|
||||
1. 新建 list、edit、edit_group、query 表和必要索引,新增 `b_module.b_module_type` 与 `b_module_field.b_field_type` 的兼容列。
|
||||
2. 为原有字段创建顶层 `field` 列配置;根据原 `b_canform`、`b_canquery` 创建编辑和查询配置;初始 `b_canedit = 0`。
|
||||
1. 新建 list、edit、group、query 表和必要索引,新增 `b_module.b_module_type` 与 `b_module_field.b_type` 的兼容列。
|
||||
2. 为原有字段创建列表配置;根据原 `b_canform`、`b_canquery` 初始化编辑和查询配置。
|
||||
3. 前端和后端改为读取新场景表,查询不再读取 list 配置;模块类型切换为读取 `b_module_type`。
|
||||
4. 完成视图同步服务并验证新结构的读写稳定后,删除旧混合列和兼容字段。
|
||||
Reference in new issue
Block a user