20260721223033
This commit is contained in:
1 parent
bab8e2972f
commit
e9f099f044
26 files changed
+1687
-349
No files matched your search
@@ -14,7 +14,7 @@ Supporting docs in the repo root (Chinese):
|
|||||||
- `新数据库结构.md` — the module-management table design (authoritative schema).
|
- `新数据库结构.md` — the module-management table design (authoritative schema).
|
||||||
- `旧数据库结构.md` — legacy schema, kept for reference only.
|
- `旧数据库结构.md` — legacy schema, kept for reference only.
|
||||||
- `模块管理实施计划.md` — phased plan for the module-management feature.
|
- `模块管理实施计划.md` — phased plan for the module-management feature.
|
||||||
- `fms-api/config/migrations/001_module_management.sql` — the only migration script.
|
- `fms-api/config/migrations/` — SQL Server schema migration scripts.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ Vue 3 `<script setup>` SFCs, Pinia for state, **antdv-next** components (auto-im
|
|||||||
Feature code should call these, or wrap them in a dedicated service like `src/services/moduleManagement.js` (e.g. `loadModuleConfiguration`, `nextIds`, `saveChanges`). Do not introduce new backend URLs from the frontend — reuse the generic endpoints.
|
Feature code should call these, or wrap them in a dedicated service like `src/services/moduleManagement.js` (e.g. `loadModuleConfiguration`, `nextIds`, `saveChanges`). Do not introduce new backend URLs from the frontend — reuse the generic endpoints.
|
||||||
|
|
||||||
### Routing & permissions
|
### Routing & permissions
|
||||||
- `src/router/index.js` registers view components via **static explicit `import()`**, not a dynamic whitelist. Each route carries `meta.moduleCode` (e.g. `M_MODULE`), plus `menuKey`, `title`, `requiresAuth`.
|
- `src/router/index.js` registers view components via **static explicit `import()`**, not a dynamic whitelist. Each route carries `meta.moduleCode` (e.g. `m_module`), plus `menuKey`, `title`, `requiresAuth`.
|
||||||
- A `beforeEach` guard checks auth, then `permissionStore.canModule(to.meta.moduleCode)`. `src/stores/permissions.js` loads `b_user_module` / `b_user_power` / `b_module_power` and gates page entry and button enable/disable. Currently permission enforcement is **frontend-only**; backend interception is not yet implemented.
|
- A `beforeEach` guard checks auth, then `permissionStore.canModule(to.meta.moduleCode)`. `src/stores/permissions.js` loads `b_user_module` / `b_user_power` / `b_module_power` and gates page entry and button enable/disable. Currently permission enforcement is **frontend-only**; backend interception is not yet implemented.
|
||||||
- `src/layouts/components/AppSidebar.vue` still uses a hardcoded `fallbackMenuItems` array. It should fall back to this static menu when the generic-interface module load fails.
|
- `src/layouts/components/AppSidebar.vue` still uses a hardcoded `fallbackMenuItems` array. It should fall back to this static menu when the generic-interface module load fails.
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ Feature code should call these, or wrap them in a dedicated service like `src/se
|
|||||||
- i18n: `app` store holds `locale` (default `zh-CN`); translation text comes from the DB `b_i18n` table (loaded via `loadModuleConfiguration`), not vue-i18n.
|
- i18n: `app` store holds `locale` (default `zh-CN`); translation text comes from the DB `b_i18n` table (loaded via `loadModuleConfiguration`), not vue-i18n.
|
||||||
|
|
||||||
### Module management feature
|
### Module management feature
|
||||||
`src/views/module-management/` holds `ModuleManagementView.vue` plus `components/` panels (`ModuleTreePanel`, `ModuleBasicInfoPanel`, `ModuleFieldDefinitionPanel`, `ModulePowerPanel`, `ModuleTranslationsPanel`). Demo data lives in `src/data/moduleDemo.js` — the real page must not depend on it. The table design (six tables: `b_module`, `b_module_field`, `b_module_power`, `b_user_module`, `b_user_power`, `b_i18n`) is in `新数据库结构.md`; follow that schema exactly. `b_module.b_id` is the module code (e.g. `M_BIZ`), immutable after creation; other `b_id`s are snowflake bigints returned as strings.
|
`src/views/module-management/` holds `ModuleManagementView.vue` plus `components/` panels (`ModuleTreePanel`, `ModuleBasicInfoPanel`, `ModuleFieldDefinitionPanel`, `ModulePowerPanel`, `ModuleTranslationsPanel`). Demo data lives in `src/data/moduleDemo.js` — the real page must not depend on it. The table design (six tables: `b_module`, `b_module_field`, `b_module_power`, `b_user_module`, `b_user_power`, `b_i18n`) is in `新数据库结构.md`; follow that schema exactly. `b_module.b_id` is an immutable snowflake ID; `b_module.b_code` is a mutable, unique lowercase code (e.g. `m_biz`). Bigint IDs are returned to the frontend as strings.
|
||||||
|
|
||||||
## Cross-cutting conventions
|
## Cross-cutting conventions
|
||||||
- Reuse existing components and antdv-next; extract shared logic into components/composables/services only when it genuinely reduces duplication. Keep new CSS scoped to the component.
|
- Reuse existing components and antdv-next; extract shared logic into components/composables/services only when it genuinely reduces duplication. Keep new CSS scoped to the component.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
### 核心原则:各自负责各自
|
### 核心原则:各自负责各自
|
||||||
|
|
||||||
1. **权限负责「有什么操作」**,只声明编码目录,不持有任何界面文案。
|
1. **权限负责「有什么操作」**,只声明编码目录,不持有任何界面文案。
|
||||||
2. **多语言负责「所有界面显示名」**,是唯一文案数据源;但**只覆盖菜单 / 模块 / 字段三类显示名,不覆盖权限项**(权限名是给管理员看的配置标签,不是给终端用户的按钮文案)。
|
2. **多语言负责「所有界面文案」**,是菜单、模块、权限、字段、提示、按钮和校验信息的统一文案数据源。
|
||||||
3. **字段定义驱动列表与表单**,自身不关心权限、不关心翻译落点。
|
3. **字段定义驱动列表与表单**,自身不关心权限、不关心翻译落点。
|
||||||
4. **菜单是功能树的可投影子集**,不单独存一份菜单数据。
|
4. **菜单是功能树的可投影子集**,不单独存一份菜单数据。
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
| 项 | 说明 |
|
| 项 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 模块编码 `b_id` | 唯一主键,如 `M_BIZ`、`M_USER` |
|
| 模块编码 `b_code` | 可修改的唯一小写编码,如 `m_biz`、`m_user` |
|
||||||
| 模块名称 | 在「多语言」中以 `module.<编码>` 维护(见 §5) |
|
| 模块名称 | 在「多语言」中以 `module.<编码>` 维护(见 §5) |
|
||||||
| 查询对象 `view_name` | 只读裸对象名(视图),如 `v_bs_business` |
|
| 查询对象 `view_name` | 只读裸对象名(视图),如 `v_bs_business` |
|
||||||
| 保存对象 `save_name` | 可写裸表名,如 `b_business`;只读模块允许为空 |
|
| 保存对象 `save_name` | 可写裸表名,如 `b_business`;只读模块允许为空 |
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
### 2.2 操作目录(权限项来源,见 §6)
|
### 2.2 操作目录(权限项来源,见 §6)
|
||||||
|
|
||||||
模块声明它拥有哪些操作:`actions: [{ code, name }, ...]`。例如 `M_BIZ` 声明 `1=新增 … 7=导出`。
|
模块声明它拥有哪些操作:`actions: [{ code, name }, ...]`。例如 `m_biz` 声明 `1=新增 … 7=导出`。
|
||||||
|
|
||||||
### 2.3 与其它能力的关联
|
### 2.3 与其它能力的关联
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ Module
|
|||||||
|
|
||||||
### 4.3 关键场景:同模块、不同页面、不同权限
|
### 4.3 关键场景:同模块、不同页面、不同权限
|
||||||
|
|
||||||
多个 `page` 节点可绑定**同一个模块**(如 `M_BIZ` 被 `海运业务单 / 业务单查询 / 业务单审核` 三个页面引用)。它们共享字段与操作目录,但:
|
多个 `page` 节点可绑定**同一个模块**(如 `m_biz` 被 `海运业务单 / 业务单查询 / 业务单审核` 三个页面引用)。它们共享字段与操作目录,但:
|
||||||
|
|
||||||
- 可见性各自独立(`业务单审核` 设 `showInMenu=false`,不进菜单);
|
- 可见性各自独立(`业务单审核` 设 `showInMenu=false`,不进菜单);
|
||||||
- 权限各自独立(操作员在「海运业务单」可提交,在「业务单审核」仅可审核)——授权由角色赋权页另配(不在本页面)。
|
- 权限各自独立(操作员在「海运业务单」可提交,在「业务单审核」仅可审核)——授权由角色赋权页另配(不在本页面)。
|
||||||
@@ -124,22 +124,25 @@ Module
|
|||||||
|
|
||||||
所有**界面显示名**唯一数据源,以稳定 `i18nKey` 为中心,多种语言并列。
|
所有**界面显示名**唯一数据源,以稳定 `i18nKey` 为中心,多种语言并列。
|
||||||
|
|
||||||
### 5.1 覆盖范围(仅三类显示名)
|
### 5.1 覆盖范围
|
||||||
|
|
||||||
| 命名空间 | i18nKey 规则 | 示例 |
|
| 命名空间 | i18nKey 规则 | 示例 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 菜单/功能树 | `menu.<节点id>` | `menu.page_biz` |
|
| 菜单/功能树 | `menu.<节点id>` | `menu.page_biz` |
|
||||||
| 模块 | `module.<模块编码>` | `module.M_BIZ` |
|
| 模块 | `module.<模块编码>` | `module.m_biz` |
|
||||||
| 字段 | `field.<模块>.<字段code>` | `field.M_BIZ.b_ywno` |
|
| 字段 | `field.<模块>.<字段code>` | `field.m_biz.b_ywno` |
|
||||||
|
| 权限 | `power.<模块>.<权限code>` | `power.m_biz.audit` |
|
||||||
> **权限项名称不在此列**(见 §6 说明)。
|
| 按钮 | `button.<模块>.<动作>` | `button.m_biz.approve` |
|
||||||
|
| 占位提示 | `placeholder.<模块>.<字段>` | `placeholder.m_biz.customer` |
|
||||||
|
| 校验提示 | `validation.<模块>.<规则>` | `validation.m_biz.customer_required` |
|
||||||
|
| 通用文案 | `common.<名称>` | `common.save` |
|
||||||
|
|
||||||
### 5.2 数据结构
|
### 5.2 数据结构
|
||||||
|
|
||||||
```js
|
```js
|
||||||
i18nData = {
|
i18nData = {
|
||||||
"menu.page_biz": { "zh-CN": "海运业务单", "en-US": "Sea Business Orders", "zh-TW": "海運業務單" },
|
"menu.page_biz": { "zh-CN": "海运业务单", "en-US": "Sea Business Orders", "zh-TW": "海運業務單" },
|
||||||
"field.M_BIZ.b_ywno": { "zh-CN": "业务单号", "en-US": "Order No.", "zh-TW": "業務單號" }
|
"field.m_biz.b_ywno": { "zh-CN": "业务单号", "en-US": "Order No.", "zh-TW": "業務單號" }
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -150,7 +153,8 @@ i18nData = {
|
|||||||
|
|
||||||
### 5.3 管理界面要求
|
### 5.3 管理界面要求
|
||||||
|
|
||||||
- 按命名空间类型筛选(菜单 / 模块 / 字段)。
|
- 「多语言管理」是主配置中心,模块下的多语言面板是按当前模块过滤后的快捷编辑入口;两处写入同一张多语言表。
|
||||||
|
- 支持按命名空间、模块、语言和缺失翻译状态筛选。
|
||||||
- 以「Key + 各语言列」并列编辑;某语言留空即回退简体。
|
- 以「Key + 各语言列」并列编辑;某语言留空即回退简体。
|
||||||
- **缺失翻译检测**:非默认语言留空时标红,并汇总「N 条缺失」,提示补录。
|
- **缺失翻译检测**:非默认语言留空时标红,并汇总「N 条缺失」,提示补录。
|
||||||
|
|
||||||
@@ -165,20 +169,21 @@ i18nData = {
|
|||||||
```js
|
```js
|
||||||
// 模块声明
|
// 模块声明
|
||||||
actions: [
|
actions: [
|
||||||
{ code: 1, name: "新增" },
|
{ code: 1, name: "新增", i18nKey: "power.m_biz.create" },
|
||||||
{ code: 5, name: "审核" },
|
{ code: 5, name: "审核", i18nKey: "power.m_biz.audit" },
|
||||||
{ code: 7, name: "导出" }
|
{ code: 7, name: "导出", i18nKey: "power.m_biz.export" }
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
- `code`:稳定操作编码,前端与后端统一引用(如 `5` 永远代表「审核」)。**授权与校验只认 `code`,不认名字。**
|
- `code`:稳定操作编码,前端与后端统一引用(如 `5` 永远代表「审核」)。**授权与校验只认 `code`,不认名字。**
|
||||||
- `name`:管理员可读的默认标签,仅一份(简体)。它是「这个编码是干嘛的」的配置注释,**不是终端用户的按钮文案**。
|
- `name`:管理员可读的默认中文回退标签。
|
||||||
|
- `i18nKey`:权限显示名绑定的多语言键;终端按钮如果需要不同文案,使用独立的 `button.*` Key。
|
||||||
|
|
||||||
### 6.2 为什么不进多语言
|
### 6.2 权限与按钮文案的边界
|
||||||
|
|
||||||
- 授权逻辑只认 `code`,名字不参与校验,因此没有翻译必要。
|
- 授权与校验始终只认 `code`,不依赖任何语言文本。
|
||||||
- 界面上的按钮文案常与权限名不同(权限叫「审核」,按钮可能写「通过」),把权限名塞进 i18n 反而把「权限定义」与「按钮文案」错误耦合。
|
- 权限名称和按钮文案都进入统一多语言资源,但使用不同 Key,避免把「审核」与「通过」错误耦合。
|
||||||
- 结论:**权限定义自洽于 `{code, name}`,与多语言彻底解耦**;终端用户看到的按钮文案由各自页面/组件自行在多语言里维护。
|
- 权限定义保持 `{code, name, i18nKey}` 自洽;按钮、提示和校验信息按各自命名空间独立维护。
|
||||||
|
|
||||||
### 6.3 与授权层的关系(边界)
|
### 6.3 与授权层的关系(边界)
|
||||||
|
|
||||||
@@ -203,10 +208,9 @@ actions: [
|
|||||||
├─ 分组 ─────────────► 组织 Form
|
├─ 分组 ─────────────► 组织 Form
|
||||||
└─ 操作目录(code) ────► 权限内核(角色赋权页另行消费)
|
└─ 操作目录(code) ────► 权限内核(角色赋权页另行消费)
|
||||||
▲
|
▲
|
||||||
│ 各自只声明,不持有文案
|
│ 各自只声明稳定编码并绑定 i18nKey
|
||||||
│
|
│
|
||||||
多语言 (i18nKey → 多语言值) ──► 菜单名 / 模块名 / 字段标题 的唯一文案源
|
多语言 (i18nKey → 多语言值) ──► 菜单 / 模块 / 权限 / 字段 / 按钮 / 提示 / 校验的唯一文案源
|
||||||
(权限名不在内)
|
|
||||||
|
|
||||||
菜单 = 功能树中 showInMenu=1 且角色有查看权限的节点投影
|
菜单 = 功能树中 showInMenu=1 且角色有查看权限的节点投影
|
||||||
```
|
```
|
||||||
@@ -218,7 +222,7 @@ actions: [
|
|||||||
| 设计项 | 原型位置 | 备注 |
|
| 设计项 | 原型位置 | 备注 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 功能树四类节点 | `functionTree` | 已是响应式可编辑 |
|
| 功能树四类节点 | `functionTree` | 已是响应式可编辑 |
|
||||||
| 模块基本信息/字段/分组 | `modules` | `M_MODULE/M_USER/M_BIZ/M_FEE` |
|
| 模块基本信息/字段/分组 | `modules` | `m_module/m_user/m_biz/m_fee` |
|
||||||
| 字段驱动列表/表单 | `字段配置` Tab | 列表/表单/必填开关 |
|
| 字段驱动列表/表单 | `字段配置` Tab | 列表/表单/必填开关 |
|
||||||
| 菜单投影 | `菜单预览` Tab | 按角色 + 语言渲染 |
|
| 菜单投影 | `菜单预览` Tab | 按角色 + 语言渲染 |
|
||||||
| 多语言 | `多语言` Tab | Key + 三语,含缺失标红 |
|
| 多语言 | `多语言` Tab | Key + 三语,含缺失标红 |
|
||||||
@@ -228,8 +232,8 @@ actions: [
|
|||||||
|
|
||||||
原型已按本文设计落地:
|
原型已按本文设计落地:
|
||||||
|
|
||||||
- **多语言仅覆盖菜单 / 模块 / 字段三类显示名**;`action.*` 条目已从 `i18nData` 彻底移除,权限项名不再进多语言(符合 §6)。
|
- **多语言覆盖所有界面文案**;权限名称使用 `power.*`,终端按钮使用独立的 `button.*`,两者不耦合。
|
||||||
- **权限项仅含 `{code, name}`**,`name` 为管理员可读标签,不在多语言维护;新增权限项不再生成 i18nKey。
|
- **权限项包含 `{code, name, i18nKey}`**;授权仍只认 `code`,`name` 作为默认中文回退。
|
||||||
- **字段多语言可在字段配置处点击设置**:标题单元格带多语言按钮,点击打开该字段的简体/English/繁體编辑框(i18nKey 形如 `field.<模块>.<字段>`)。
|
- **字段多语言可在字段配置处点击设置**:标题单元格带多语言按钮,点击打开该字段的简体/English/繁體编辑框(i18nKey 形如 `field.<模块>.<字段>`)。
|
||||||
- **字段 `i18nKey` 统一为 `field.<模块>.<字段>`**,与模块/菜单 `<id>` 前缀、整体「命名空间.容器.标识」规则一致。
|
- **字段 `i18nKey` 统一为 `field.<模块>.<字段>`**,与模块/菜单 `<id>` 前缀、整体「命名空间.容器.标识」规则一致。
|
||||||
- 授权 `roleNodePerm` 仍为写死 mock,仅用于菜单预览示意,符合 §6.3 边界(角色赋权在另一页面,不在模块管理内)。
|
- 授权 `roleNodePerm` 仍为写死 mock,仅用于菜单预览示意,符合 §6.3 边界(角色赋权在另一页面,不在模块管理内)。
|
||||||
@@ -244,7 +248,7 @@ actions: [
|
|||||||
| `s_module_field` | 字段配置(编码、标题键、类型、列表/表单/必填、分组) |
|
| `s_module_field` | 字段配置(编码、标题键、类型、列表/表单/必填、分组) |
|
||||||
| `s_module_group` | 表单分组 |
|
| `s_module_group` | 表单分组 |
|
||||||
| `s_function_node` | 功能树(目录/页面/分组/模块定位) |
|
| `s_function_node` | 功能树(目录/页面/分组/模块定位) |
|
||||||
| `s_i18n` | 多语言键值(key → 各语言值),覆盖菜单/模块/字段 |
|
| `s_i18n` | 统一多语言键值(key → 各语言值),覆盖所有界面文案 |
|
||||||
| 权限相关表 | 独立系统表,不属于 Table/Form 渲染配置 |
|
| 权限相关表 | 独立系统表,不属于 Table/Form 渲染配置 |
|
||||||
|
|
||||||
> 角色与权限内核使用独立系统表,不写入模块渲染配置;后端按 `module_id` + 操作 `code` 校验权限,不能只依赖前端隐藏。
|
> 角色与权限内核使用独立系统表,不写入模块渲染配置;后端按 `module_id` + 操作 `code` 校验权限,不能只依赖前端隐藏。
|
||||||
@@ -7,8 +7,9 @@ BEGIN TRY
|
|||||||
IF OBJECT_ID(N'dbo.b_module', N'U') IS NULL
|
IF OBJECT_ID(N'dbo.b_module', N'U') IS NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
CREATE TABLE dbo.b_module (
|
CREATE TABLE dbo.b_module (
|
||||||
b_id varchar(50) NOT NULL,
|
b_id bigint NOT NULL,
|
||||||
b_parent_id varchar(50) NULL,
|
b_code varchar(50) NOT NULL,
|
||||||
|
b_parent_id bigint NULL,
|
||||||
b_name nvarchar(100) NOT NULL,
|
b_name nvarchar(100) NOT NULL,
|
||||||
b_i18n varchar(150) NULL,
|
b_i18n varchar(150) NULL,
|
||||||
b_type varchar(20) NOT NULL,
|
b_type varchar(20) NOT NULL,
|
||||||
@@ -16,20 +17,19 @@ BEGIN TRY
|
|||||||
b_viewtable varchar(128) NULL,
|
b_viewtable varchar(128) NULL,
|
||||||
b_savetable varchar(128) NULL,
|
b_savetable varchar(128) NULL,
|
||||||
b_keyfield varchar(50) NULL,
|
b_keyfield varchar(50) NULL,
|
||||||
b_orderfield varchar(50) NULL,
|
b_orderfield varchar(500) NULL,
|
||||||
b_ordertype varchar(4) NULL,
|
|
||||||
b_canmenu int NOT NULL CONSTRAINT DF_b_module_canmenu DEFAULT (0),
|
b_canmenu int NOT NULL CONSTRAINT DF_b_module_canmenu DEFAULT (0),
|
||||||
b_canuse int NOT NULL CONSTRAINT DF_b_module_canuse DEFAULT (1),
|
b_canuse int NOT NULL CONSTRAINT DF_b_module_canuse DEFAULT (1),
|
||||||
b_xh int NOT NULL CONSTRAINT DF_b_module_xh DEFAULT (0),
|
b_xh int NOT NULL CONSTRAINT DF_b_module_xh DEFAULT (0),
|
||||||
CONSTRAINT PK_b_module PRIMARY KEY (b_id),
|
CONSTRAINT PK_b_module PRIMARY KEY (b_id),
|
||||||
CONSTRAINT FK_b_module_parent FOREIGN KEY (b_parent_id) REFERENCES dbo.b_module (b_id),
|
CONSTRAINT FK_b_module_parent FOREIGN KEY (b_parent_id) REFERENCES dbo.b_module (b_id),
|
||||||
CONSTRAINT CK_b_module_id CHECK (
|
CONSTRAINT UQ_b_module_code UNIQUE (b_code),
|
||||||
LEN(b_id) BETWEEN 2 AND 50
|
CONSTRAINT CK_b_module_code CHECK (
|
||||||
AND b_id LIKE '[A-Z]%'
|
LEN(b_code) BETWEEN 2 AND 50
|
||||||
AND b_id NOT LIKE '%[^A-Z0-9_]%'
|
AND b_code COLLATE Latin1_General_100_BIN2 LIKE '[a-z]%'
|
||||||
|
AND b_code COLLATE Latin1_General_100_BIN2 NOT LIKE '%[^a-z0-9_]%'
|
||||||
),
|
),
|
||||||
CONSTRAINT CK_b_module_type CHECK (b_type IN ('directory', 'page', 'data')),
|
CONSTRAINT CK_b_module_type CHECK (b_type IN ('directory', 'page', 'data')),
|
||||||
CONSTRAINT CK_b_module_ordertype CHECK (b_ordertype IS NULL OR b_ordertype IN ('ASC', 'DESC')),
|
|
||||||
CONSTRAINT CK_b_module_canmenu CHECK (b_canmenu IN (0, 1)),
|
CONSTRAINT CK_b_module_canmenu CHECK (b_canmenu IN (0, 1)),
|
||||||
CONSTRAINT CK_b_module_canuse CHECK (b_canuse IN (0, 1)),
|
CONSTRAINT CK_b_module_canuse CHECK (b_canuse IN (0, 1)),
|
||||||
CONSTRAINT CK_b_module_directory CHECK (
|
CONSTRAINT CK_b_module_directory CHECK (
|
||||||
@@ -49,7 +49,7 @@ BEGIN TRY
|
|||||||
BEGIN
|
BEGIN
|
||||||
CREATE TABLE dbo.b_module_field (
|
CREATE TABLE dbo.b_module_field (
|
||||||
b_id bigint NOT NULL,
|
b_id bigint NOT NULL,
|
||||||
b_module_id varchar(50) NOT NULL,
|
b_module_id bigint NOT NULL,
|
||||||
b_field varchar(50) NOT NULL,
|
b_field varchar(50) NOT NULL,
|
||||||
b_name nvarchar(100) NOT NULL,
|
b_name nvarchar(100) NOT NULL,
|
||||||
b_i18n varchar(150) NULL,
|
b_i18n varchar(150) NULL,
|
||||||
@@ -85,7 +85,7 @@ BEGIN TRY
|
|||||||
BEGIN
|
BEGIN
|
||||||
CREATE TABLE dbo.b_module_power (
|
CREATE TABLE dbo.b_module_power (
|
||||||
b_id bigint NOT NULL,
|
b_id bigint NOT NULL,
|
||||||
b_module_id varchar(50) NOT NULL,
|
b_module_id bigint NOT NULL,
|
||||||
b_code varchar(50) NOT NULL,
|
b_code varchar(50) NOT NULL,
|
||||||
b_name nvarchar(100) NOT NULL,
|
b_name nvarchar(100) NOT NULL,
|
||||||
b_i18n varchar(150) NULL,
|
b_i18n varchar(150) NULL,
|
||||||
@@ -106,7 +106,7 @@ BEGIN TRY
|
|||||||
CREATE TABLE dbo.b_user_module (
|
CREATE TABLE dbo.b_user_module (
|
||||||
b_id bigint NOT NULL,
|
b_id bigint NOT NULL,
|
||||||
b_user_id bigint NOT NULL,
|
b_user_id bigint NOT NULL,
|
||||||
b_module_id varchar(50) NOT NULL,
|
b_module_id bigint NOT NULL,
|
||||||
b_inputuser_id bigint NOT NULL,
|
b_inputuser_id bigint NOT NULL,
|
||||||
b_inputdatetime datetime NOT NULL CONSTRAINT DF_b_user_module_inputdatetime DEFAULT (GETDATE()),
|
b_inputdatetime datetime NOT NULL CONSTRAINT DF_b_user_module_inputdatetime DEFAULT (GETDATE()),
|
||||||
CONSTRAINT PK_b_user_module PRIMARY KEY (b_id),
|
CONSTRAINT PK_b_user_module PRIMARY KEY (b_id),
|
||||||
@@ -160,13 +160,16 @@ BEGIN TRY
|
|||||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.b_i18n') AND name = N'IX_b_i18n_key')
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.b_i18n') AND name = N'IX_b_i18n_key')
|
||||||
CREATE INDEX IX_b_i18n_key ON dbo.b_i18n (b_key, b_locale);
|
CREATE INDEX IX_b_i18n_key ON dbo.b_i18n (b_key, b_locale);
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_module WHERE b_id = 'M_SYSTEM')
|
DECLARE @SystemModuleId bigint = 205000000000000011;
|
||||||
INSERT INTO dbo.b_module (b_id, b_parent_id, b_name, b_i18n, b_type, b_canmenu, b_canuse, b_xh)
|
DECLARE @ModuleManagementId bigint = 205000000000000012;
|
||||||
VALUES ('M_SYSTEM', NULL, N'系统管理', 'module.M_SYSTEM', 'directory', 1, 1, 100);
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_module WHERE b_id = 'M_MODULE')
|
IF NOT EXISTS (SELECT 1 FROM dbo.b_module WHERE b_code = 'm_system')
|
||||||
INSERT INTO dbo.b_module (b_id, b_parent_id, b_name, b_i18n, b_type, b_route, b_canmenu, b_canuse, b_xh)
|
INSERT INTO dbo.b_module (b_id, b_code, b_parent_id, b_name, b_i18n, b_type, b_canmenu, b_canuse, b_xh)
|
||||||
VALUES ('M_MODULE', 'M_SYSTEM', N'模块管理', 'module.M_MODULE', 'page', '/system/modules', 1, 1, 10);
|
VALUES (@SystemModuleId, 'm_system', NULL, N'系统管理', 'module.m_system', 'directory', 1, 1, 100);
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.b_module WHERE b_code = 'm_module')
|
||||||
|
INSERT INTO dbo.b_module (b_id, b_code, b_parent_id, b_name, b_i18n, b_type, b_route, b_canmenu, b_canuse, b_xh)
|
||||||
|
VALUES (@ModuleManagementId, 'm_module', @SystemModuleId, N'模块管理', 'module.m_module', 'page', '/system/modules', 1, 1, 10);
|
||||||
|
|
||||||
DECLARE @Powers TABLE (b_id bigint, b_code varchar(50), b_name nvarchar(100), b_xh int);
|
DECLARE @Powers TABLE (b_id bigint, b_code varchar(50), b_name nvarchar(100), b_xh int);
|
||||||
INSERT INTO @Powers VALUES
|
INSERT INTO @Powers VALUES
|
||||||
@@ -176,11 +179,11 @@ BEGIN TRY
|
|||||||
(205000000000000004, 'grant', N'用户授权', 40);
|
(205000000000000004, 'grant', N'用户授权', 40);
|
||||||
|
|
||||||
INSERT INTO dbo.b_module_power (b_id, b_module_id, b_code, b_name, b_i18n, b_xh, b_canuse)
|
INSERT INTO dbo.b_module_power (b_id, b_module_id, b_code, b_name, b_i18n, b_xh, b_canuse)
|
||||||
SELECT p.b_id, 'M_MODULE', p.b_code, p.b_name, 'power.M_MODULE.' + p.b_code, p.b_xh, 1
|
SELECT p.b_id, @ModuleManagementId, p.b_code, p.b_name, 'power.m_module.' + p.b_code, p.b_xh, 1
|
||||||
FROM @Powers p
|
FROM @Powers p
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM dbo.b_module_power target
|
SELECT 1 FROM dbo.b_module_power target
|
||||||
WHERE target.b_module_id = 'M_MODULE' AND target.b_code = p.b_code
|
WHERE target.b_module_id = @ModuleManagementId AND target.b_code = p.b_code
|
||||||
);
|
);
|
||||||
|
|
||||||
EXEC sys.sp_set_session_context @key = N'allow_g3soft', @value = 1;
|
EXEC sys.sp_set_session_context @key = N'allow_g3soft', @value = 1;
|
||||||
@@ -188,14 +191,14 @@ BEGIN TRY
|
|||||||
IF @AdminId IS NULL
|
IF @AdminId IS NULL
|
||||||
THROW 51000, '未找到已启用的管理员账号 g3soft,模块管理初始化已取消。', 1;
|
THROW 51000, '未找到已启用的管理员账号 g3soft,模块管理初始化已取消。', 1;
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_user_module WHERE b_user_id = @AdminId AND b_module_id = 'M_MODULE')
|
IF NOT EXISTS (SELECT 1 FROM dbo.b_user_module WHERE b_user_id = @AdminId AND b_module_id = @ModuleManagementId)
|
||||||
INSERT INTO dbo.b_user_module (b_id, b_user_id, b_module_id, b_inputuser_id, b_inputdatetime)
|
INSERT INTO dbo.b_user_module (b_id, b_user_id, b_module_id, b_inputuser_id, b_inputdatetime)
|
||||||
VALUES (205000000000000101, @AdminId, 'M_MODULE', @AdminId, GETDATE());
|
VALUES (205000000000000101, @AdminId, @ModuleManagementId, @AdminId, GETDATE());
|
||||||
|
|
||||||
INSERT INTO dbo.b_user_power (b_id, b_user_id, b_power_id, b_inputuser_id, b_inputdatetime)
|
INSERT INTO dbo.b_user_power (b_id, b_user_id, b_power_id, b_inputuser_id, b_inputdatetime)
|
||||||
SELECT 205000000000000200 + ROW_NUMBER() OVER (ORDER BY p.b_id), @AdminId, p.b_id, @AdminId, GETDATE()
|
SELECT 205000000000000200 + ROW_NUMBER() OVER (ORDER BY p.b_id), @AdminId, p.b_id, @AdminId, GETDATE()
|
||||||
FROM dbo.b_module_power p
|
FROM dbo.b_module_power p
|
||||||
WHERE p.b_module_id = 'M_MODULE'
|
WHERE p.b_module_id = @ModuleManagementId
|
||||||
AND p.b_code IN ('create', 'update', 'delete', 'grant')
|
AND p.b_code IN ('create', 'update', 'delete', 'grant')
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM dbo.b_user_power target
|
SELECT 1 FROM dbo.b_user_power target
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.b_module', N'U') IS NOT NULL
|
||||||
|
AND COL_LENGTH(N'dbo.b_module', N'b_orderfield') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC sys.sp_executesql N'
|
||||||
|
ALTER TABLE dbo.b_module ALTER COLUMN b_orderfield varchar(500) NULL;
|
||||||
|
';
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.b_module', N'U') IS NOT NULL
|
||||||
|
AND COL_LENGTH(N'dbo.b_module', N'b_ordertype') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC sys.sp_executesql N'
|
||||||
|
UPDATE dbo.b_module
|
||||||
|
SET b_orderfield =
|
||||||
|
CASE
|
||||||
|
WHEN NULLIF(LTRIM(RTRIM(b_orderfield)), '''') IS NULL THEN NULL
|
||||||
|
WHEN UPPER(RIGHT(LTRIM(RTRIM(b_orderfield)), 4)) = '' ASC''
|
||||||
|
OR UPPER(RIGHT(LTRIM(RTRIM(b_orderfield)), 5)) = '' DESC''
|
||||||
|
THEN LTRIM(RTRIM(b_orderfield))
|
||||||
|
WHEN UPPER(LTRIM(RTRIM(b_ordertype))) IN (''ASC'', ''DESC'')
|
||||||
|
THEN LTRIM(RTRIM(b_orderfield)) + '' '' + UPPER(LTRIM(RTRIM(b_ordertype)))
|
||||||
|
ELSE LTRIM(RTRIM(b_orderfield))
|
||||||
|
END;
|
||||||
|
';
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.CK_b_module_ordertype', N'C') IS NOT NULL
|
||||||
|
ALTER TABLE dbo.b_module DROP CONSTRAINT CK_b_module_ordertype;
|
||||||
|
|
||||||
|
ALTER TABLE dbo.b_module DROP COLUMN b_ordertype;
|
||||||
|
END;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
IF @@TRANCOUNT > 0
|
||||||
|
ROLLBACK TRANSACTION;
|
||||||
|
THROW;
|
||||||
|
END CATCH;
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys.columns c
|
||||||
|
JOIN sys.types t ON t.user_type_id = c.user_type_id
|
||||||
|
WHERE c.object_id = OBJECT_ID(N'dbo.b_module')
|
||||||
|
AND c.name = N'b_id'
|
||||||
|
AND t.name <> N'bigint'
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE #ModuleIdMap (
|
||||||
|
old_id varchar(50) NOT NULL PRIMARY KEY,
|
||||||
|
new_id bigint NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO #ModuleIdMap (old_id, new_id)
|
||||||
|
SELECT
|
||||||
|
b_id,
|
||||||
|
CASE b_id
|
||||||
|
WHEN 'M_SYSTEM' THEN 205000000000000011
|
||||||
|
WHEN 'M_MODULE' THEN 205000000000000012
|
||||||
|
ELSE 206000000000000000 + ROW_NUMBER() OVER (ORDER BY b_id)
|
||||||
|
END
|
||||||
|
FROM dbo.b_module;
|
||||||
|
|
||||||
|
ALTER TABLE dbo.b_module ADD
|
||||||
|
b_new_id bigint NULL,
|
||||||
|
b_code varchar(50) NULL,
|
||||||
|
b_new_parent_id bigint NULL;
|
||||||
|
ALTER TABLE dbo.b_module_field ADD b_new_module_id bigint NULL;
|
||||||
|
ALTER TABLE dbo.b_module_power ADD b_new_module_id bigint NULL;
|
||||||
|
ALTER TABLE dbo.b_user_module ADD b_new_module_id bigint NULL;
|
||||||
|
|
||||||
|
EXEC sys.sp_executesql N'
|
||||||
|
UPDATE target
|
||||||
|
SET
|
||||||
|
b_new_id = id_map.new_id,
|
||||||
|
b_code = LOWER(target.b_id),
|
||||||
|
b_new_parent_id = parent_map.new_id
|
||||||
|
FROM dbo.b_module target
|
||||||
|
JOIN #ModuleIdMap id_map ON id_map.old_id = target.b_id
|
||||||
|
LEFT JOIN #ModuleIdMap parent_map ON parent_map.old_id = target.b_parent_id;
|
||||||
|
|
||||||
|
UPDATE target
|
||||||
|
SET b_new_module_id = id_map.new_id
|
||||||
|
FROM dbo.b_module_field target
|
||||||
|
JOIN #ModuleIdMap id_map ON id_map.old_id = target.b_module_id;
|
||||||
|
|
||||||
|
UPDATE target
|
||||||
|
SET b_new_module_id = id_map.new_id
|
||||||
|
FROM dbo.b_module_power target
|
||||||
|
JOIN #ModuleIdMap id_map ON id_map.old_id = target.b_module_id;
|
||||||
|
|
||||||
|
UPDATE target
|
||||||
|
SET b_new_module_id = id_map.new_id
|
||||||
|
FROM dbo.b_user_module target
|
||||||
|
JOIN #ModuleIdMap id_map ON id_map.old_id = target.b_module_id;
|
||||||
|
|
||||||
|
ALTER TABLE dbo.b_module ALTER COLUMN b_new_id bigint NOT NULL;
|
||||||
|
ALTER TABLE dbo.b_module ALTER COLUMN b_code varchar(50) NOT NULL;
|
||||||
|
ALTER TABLE dbo.b_module_field ALTER COLUMN b_new_module_id bigint NOT NULL;
|
||||||
|
ALTER TABLE dbo.b_module_power ALTER COLUMN b_new_module_id bigint NOT NULL;
|
||||||
|
ALTER TABLE dbo.b_user_module ALTER COLUMN b_new_module_id bigint NOT NULL;
|
||||||
|
';
|
||||||
|
|
||||||
|
ALTER TABLE dbo.b_module DROP CONSTRAINT FK_b_module_parent;
|
||||||
|
ALTER TABLE dbo.b_module_field DROP CONSTRAINT FK_b_module_field_module;
|
||||||
|
ALTER TABLE dbo.b_module_power DROP CONSTRAINT FK_b_module_power_module;
|
||||||
|
ALTER TABLE dbo.b_user_module DROP CONSTRAINT FK_b_user_module_module;
|
||||||
|
ALTER TABLE dbo.b_module_field DROP CONSTRAINT UQ_b_module_field;
|
||||||
|
ALTER TABLE dbo.b_module_power DROP CONSTRAINT UQ_b_module_power;
|
||||||
|
ALTER TABLE dbo.b_user_module DROP CONSTRAINT UQ_b_user_module;
|
||||||
|
ALTER TABLE dbo.b_module DROP CONSTRAINT CK_b_module_id;
|
||||||
|
ALTER TABLE dbo.b_module DROP CONSTRAINT PK_b_module;
|
||||||
|
|
||||||
|
DROP INDEX IX_b_module_parent ON dbo.b_module;
|
||||||
|
DROP INDEX IX_b_module_field_module ON dbo.b_module_field;
|
||||||
|
DROP INDEX IX_b_module_power_module ON dbo.b_module_power;
|
||||||
|
DROP INDEX IX_b_user_module_module ON dbo.b_user_module;
|
||||||
|
|
||||||
|
ALTER TABLE dbo.b_module_field DROP COLUMN b_module_id;
|
||||||
|
ALTER TABLE dbo.b_module_power DROP COLUMN b_module_id;
|
||||||
|
ALTER TABLE dbo.b_user_module DROP COLUMN b_module_id;
|
||||||
|
ALTER TABLE dbo.b_module DROP COLUMN b_parent_id, b_id;
|
||||||
|
|
||||||
|
EXEC sys.sp_rename N'dbo.b_module.b_new_id', N'b_id', N'COLUMN';
|
||||||
|
EXEC sys.sp_rename N'dbo.b_module.b_new_parent_id', N'b_parent_id', N'COLUMN';
|
||||||
|
EXEC sys.sp_rename N'dbo.b_module_field.b_new_module_id', N'b_module_id', N'COLUMN';
|
||||||
|
EXEC sys.sp_rename N'dbo.b_module_power.b_new_module_id', N'b_module_id', N'COLUMN';
|
||||||
|
EXEC sys.sp_rename N'dbo.b_user_module.b_new_module_id', N'b_module_id', N'COLUMN';
|
||||||
|
|
||||||
|
EXEC sys.sp_executesql N'
|
||||||
|
ALTER TABLE dbo.b_module ADD
|
||||||
|
CONSTRAINT PK_b_module PRIMARY KEY (b_id),
|
||||||
|
CONSTRAINT FK_b_module_parent FOREIGN KEY (b_parent_id) REFERENCES dbo.b_module (b_id),
|
||||||
|
CONSTRAINT UQ_b_module_code UNIQUE (b_code),
|
||||||
|
CONSTRAINT CK_b_module_code CHECK (
|
||||||
|
LEN(b_code) BETWEEN 2 AND 50
|
||||||
|
AND b_code COLLATE Latin1_General_100_BIN2 LIKE ''[a-z]%''
|
||||||
|
AND b_code COLLATE Latin1_General_100_BIN2 NOT LIKE ''%[^a-z0-9_]%''
|
||||||
|
);
|
||||||
|
ALTER TABLE dbo.b_module_field ADD
|
||||||
|
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);
|
||||||
|
ALTER TABLE dbo.b_module_power ADD
|
||||||
|
CONSTRAINT FK_b_module_power_module FOREIGN KEY (b_module_id) REFERENCES dbo.b_module (b_id),
|
||||||
|
CONSTRAINT UQ_b_module_power UNIQUE (b_module_id, b_code);
|
||||||
|
ALTER TABLE dbo.b_user_module ADD
|
||||||
|
CONSTRAINT FK_b_user_module_module FOREIGN KEY (b_module_id) REFERENCES dbo.b_module (b_id),
|
||||||
|
CONSTRAINT UQ_b_user_module UNIQUE (b_user_id, b_module_id);
|
||||||
|
|
||||||
|
CREATE INDEX IX_b_module_parent ON dbo.b_module (b_parent_id, b_xh, b_id);
|
||||||
|
CREATE INDEX IX_b_module_field_module ON dbo.b_module_field (b_module_id, b_xh, b_id);
|
||||||
|
CREATE INDEX IX_b_module_power_module ON dbo.b_module_power (b_module_id, b_xh, b_id);
|
||||||
|
CREATE INDEX IX_b_user_module_module ON dbo.b_user_module (b_module_id, b_user_id);
|
||||||
|
';
|
||||||
|
END;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
IF @@TRANCOUNT > 0
|
||||||
|
ROLLBACK TRANSACTION;
|
||||||
|
THROW;
|
||||||
|
END CATCH;
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
DECLARE @SystemModuleId bigint = (SELECT TOP (1) b_id FROM dbo.b_module WHERE b_code = 'm_system');
|
||||||
|
DECLARE @I18nModuleId bigint = 205000000000000013;
|
||||||
|
|
||||||
|
IF @SystemModuleId IS NULL
|
||||||
|
THROW 51000, '未找到系统管理模块 m_system,多语言管理初始化已取消。', 1;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.b_module WHERE b_code = 'm_i18n')
|
||||||
|
INSERT INTO dbo.b_module (
|
||||||
|
b_id, b_code, b_parent_id, b_name, b_i18n, b_type, b_route,
|
||||||
|
b_canmenu, b_canuse, b_xh
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@I18nModuleId, 'm_i18n', @SystemModuleId, N'多语言管理', 'module.m_i18n',
|
||||||
|
'page', '/system/i18n', 1, 1, 20
|
||||||
|
);
|
||||||
|
ELSE
|
||||||
|
SET @I18nModuleId = (SELECT TOP (1) b_id FROM dbo.b_module WHERE b_code = 'm_i18n');
|
||||||
|
|
||||||
|
DECLARE @Powers TABLE (
|
||||||
|
b_id bigint,
|
||||||
|
b_code varchar(50),
|
||||||
|
b_name nvarchar(100),
|
||||||
|
b_i18n varchar(150),
|
||||||
|
b_xh int
|
||||||
|
);
|
||||||
|
INSERT INTO @Powers VALUES
|
||||||
|
(205000000000000005, 'create', N'新增', 'power.m_i18n.create', 10),
|
||||||
|
(205000000000000006, 'update', N'修改', 'power.m_i18n.update', 20),
|
||||||
|
(205000000000000007, 'delete', N'删除', 'power.m_i18n.delete', 30);
|
||||||
|
|
||||||
|
INSERT INTO dbo.b_module_power (b_id, b_module_id, b_code, b_name, b_i18n, b_xh, b_canuse)
|
||||||
|
SELECT source.b_id, @I18nModuleId, source.b_code, source.b_name, source.b_i18n, source.b_xh, 1
|
||||||
|
FROM @Powers source
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.b_module_power target
|
||||||
|
WHERE target.b_module_id = @I18nModuleId
|
||||||
|
AND target.b_code = source.b_code
|
||||||
|
);
|
||||||
|
|
||||||
|
EXEC sys.sp_set_session_context @key = N'allow_g3soft', @value = 1;
|
||||||
|
DECLARE @AdminId bigint = (
|
||||||
|
SELECT TOP (1) b_id
|
||||||
|
FROM dbo.b_user
|
||||||
|
WHERE b_account = N'g3soft'
|
||||||
|
AND b_canuse = 1
|
||||||
|
);
|
||||||
|
IF @AdminId IS NULL
|
||||||
|
THROW 51000, '未找到已启用的管理员账号 g3soft,多语言管理初始化已取消。', 1;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.b_user_module
|
||||||
|
WHERE b_user_id = @AdminId
|
||||||
|
AND b_module_id = @I18nModuleId
|
||||||
|
)
|
||||||
|
INSERT INTO dbo.b_user_module (
|
||||||
|
b_id, b_user_id, b_module_id, b_inputuser_id, b_inputdatetime
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
205000000000000102, @AdminId, @I18nModuleId, @AdminId, GETDATE()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO dbo.b_user_power (b_id, b_user_id, b_power_id, b_inputuser_id, b_inputdatetime)
|
||||||
|
SELECT
|
||||||
|
CASE power.b_code
|
||||||
|
WHEN 'create' THEN 205000000000000211
|
||||||
|
WHEN 'update' THEN 205000000000000212
|
||||||
|
WHEN 'delete' THEN 205000000000000213
|
||||||
|
END,
|
||||||
|
@AdminId,
|
||||||
|
power.b_id,
|
||||||
|
@AdminId,
|
||||||
|
GETDATE()
|
||||||
|
FROM dbo.b_module_power power
|
||||||
|
WHERE power.b_module_id = @I18nModuleId
|
||||||
|
AND power.b_code IN ('create', 'update', 'delete')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.b_user_power target
|
||||||
|
WHERE target.b_user_id = @AdminId
|
||||||
|
AND target.b_power_id = power.b_id
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @Translations TABLE (
|
||||||
|
b_id bigint,
|
||||||
|
b_key varchar(150),
|
||||||
|
b_locale varchar(20),
|
||||||
|
b_value nvarchar(500)
|
||||||
|
);
|
||||||
|
INSERT INTO @Translations VALUES
|
||||||
|
(205000000000000301, 'module.m_i18n', 'zh-CN', N'多语言管理'),
|
||||||
|
(205000000000000302, 'module.m_i18n', 'en-US', N'Language Resources'),
|
||||||
|
(205000000000000303, 'module.m_i18n', 'zh-TW', N'多語言管理'),
|
||||||
|
(205000000000000304, 'power.m_i18n.create', 'zh-CN', N'新增'),
|
||||||
|
(205000000000000305, 'power.m_i18n.create', 'en-US', N'Create'),
|
||||||
|
(205000000000000306, 'power.m_i18n.create', 'zh-TW', N'新增'),
|
||||||
|
(205000000000000307, 'power.m_i18n.update', 'zh-CN', N'修改'),
|
||||||
|
(205000000000000308, 'power.m_i18n.update', 'en-US', N'Update'),
|
||||||
|
(205000000000000309, 'power.m_i18n.update', 'zh-TW', N'修改'),
|
||||||
|
(205000000000000310, 'power.m_i18n.delete', 'zh-CN', N'删除'),
|
||||||
|
(205000000000000311, 'power.m_i18n.delete', 'en-US', N'Delete'),
|
||||||
|
(205000000000000312, 'power.m_i18n.delete', 'zh-TW', N'刪除');
|
||||||
|
|
||||||
|
INSERT INTO dbo.b_i18n (
|
||||||
|
b_id, b_key, b_locale, b_value, b_inputuser_id, b_inputdatetime,
|
||||||
|
b_updateuser_id, b_updatedatetime
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
source.b_id, source.b_key, source.b_locale, source.b_value,
|
||||||
|
@AdminId, GETDATE(), NULL, NULL
|
||||||
|
FROM @Translations source
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.b_i18n target
|
||||||
|
WHERE target.b_key = source.b_key
|
||||||
|
AND target.b_locale = source.b_locale
|
||||||
|
);
|
||||||
|
|
||||||
|
EXEC sys.sp_set_session_context @key = N'allow_g3soft', @value = NULL;
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
|
||||||
|
BEGIN TRY
|
||||||
|
EXEC sys.sp_set_session_context @key = N'allow_g3soft', @value = NULL;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
END CATCH;
|
||||||
|
THROW;
|
||||||
|
END CATCH;
|
||||||
@@ -1,21 +1,200 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, ref, useSlots } from "vue";
|
import { computed, defineComponent, h, nextTick, ref, useSlots } from "vue";
|
||||||
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
||||||
|
import { Checkbox, Input, InputNumber, Select } from "antdv-next";
|
||||||
import { StkTable } from "stk-table-vue";
|
import { StkTable } from "stk-table-vue";
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false });
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
virtual: { type: Boolean, default: true },
|
virtual: { type: Boolean, default: true },
|
||||||
virtualX: { type: Boolean, default: true },
|
virtualX: { type: Boolean, default: true },
|
||||||
colResizable: { type: Boolean, default: true },
|
colResizable: { type: Boolean, default: true },
|
||||||
bordered: { type: [Boolean, String], default: true },
|
bordered: { type: [Boolean, String], default: true },
|
||||||
noDataFull: { type: Boolean, default: true },
|
noDataFull: { type: Boolean, default: true },
|
||||||
|
columns: { type: Array, default: () => [] },
|
||||||
|
dataSource: { type: Array, default: () => [] },
|
||||||
|
rowKey: { type: [String, Function], default: "id" },
|
||||||
|
rowSelection: { type: Object, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:columns"]);
|
||||||
|
|
||||||
|
function getRowKey(row) {
|
||||||
|
return typeof props.rowKey === "function" ? props.rowKey(row) : row?.[props.rowKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedRowKeySet = computed(
|
||||||
|
() => new Set(props.rowSelection?.selectedRowKeys || []),
|
||||||
|
);
|
||||||
|
|
||||||
|
function changeSelection(row, checked) {
|
||||||
|
const key = getRowKey(row);
|
||||||
|
const nextKeys = new Set(selectedRowKeySet.value);
|
||||||
|
if (checked) nextKeys.add(key);
|
||||||
|
else nextKeys.delete(key);
|
||||||
|
const keys = Array.from(nextKeys);
|
||||||
|
const rows = props.dataSource.filter((item) => nextKeys.has(getRowKey(item)));
|
||||||
|
props.rowSelection?.onChange?.(keys, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAllSelection(checked) {
|
||||||
|
const visibleKeys = props.dataSource.map(getRowKey);
|
||||||
|
const nextKeys = new Set(selectedRowKeySet.value);
|
||||||
|
visibleKeys.forEach((key) => {
|
||||||
|
if (checked) nextKeys.add(key);
|
||||||
|
else nextKeys.delete(key);
|
||||||
|
});
|
||||||
|
const keys = Array.from(nextKeys);
|
||||||
|
const rows = props.dataSource.filter((item) => nextKeys.has(getRowKey(item)));
|
||||||
|
props.rowSelection?.onChange?.(keys, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectionCell = defineComponent({
|
||||||
|
props: ["row"],
|
||||||
|
setup(cellProps) {
|
||||||
|
return () =>
|
||||||
|
h(Checkbox, {
|
||||||
|
checked: selectedRowKeySet.value.has(getRowKey(cellProps.row)),
|
||||||
|
"onUpdate:checked": (checked) => changeSelection(cellProps.row, checked),
|
||||||
|
onClick: (event) => event.stopPropagation(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const SelectionHeaderCell = defineComponent({
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
const visibleKeys = props.dataSource.map(getRowKey);
|
||||||
|
const selectedCount = visibleKeys.filter((key) => selectedRowKeySet.value.has(key)).length;
|
||||||
|
return h(Checkbox, {
|
||||||
|
checked: visibleKeys.length > 0 && selectedCount === visibleKeys.length,
|
||||||
|
indeterminate: selectedCount > 0 && selectedCount < visibleKeys.length,
|
||||||
|
"onUpdate:checked": changeAllSelection,
|
||||||
|
onClick: (event) => event.stopPropagation(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const EditableCell = defineComponent({
|
||||||
|
props: ["row", "col", "cellValue"],
|
||||||
|
setup(cellProps) {
|
||||||
|
function editorOptions() {
|
||||||
|
return typeof cellProps.col.editor === "string"
|
||||||
|
? { type: cellProps.col.editor }
|
||||||
|
: cellProps.col.editor || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValue(value) {
|
||||||
|
cellProps.row[cellProps.col.dataIndex] = value;
|
||||||
|
editorOptions().onChange?.(value, cellProps.row, cellProps.col);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const editor = editorOptions();
|
||||||
|
const disabled =
|
||||||
|
typeof editor.disabled === "function" ? editor.disabled(cellProps.row) : editor.disabled;
|
||||||
|
const commonProps = {
|
||||||
|
size: "small",
|
||||||
|
disabled,
|
||||||
|
style: { width: "100%" },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editor.type === "number") {
|
||||||
|
return h(InputNumber, {
|
||||||
|
...commonProps,
|
||||||
|
value: cellProps.row[cellProps.col.dataIndex],
|
||||||
|
min: editor.min,
|
||||||
|
max: editor.max,
|
||||||
|
precision: editor.precision,
|
||||||
|
"onUpdate:value": updateValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (editor.type === "select") {
|
||||||
|
return h(Select, {
|
||||||
|
...commonProps,
|
||||||
|
value: cellProps.row[cellProps.col.dataIndex],
|
||||||
|
options: editor.options || [],
|
||||||
|
"onUpdate:value": updateValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (editor.type === "checkbox") {
|
||||||
|
const checkedValue = editor.checkedValue ?? 1;
|
||||||
|
const uncheckedValue = editor.uncheckedValue ?? 0;
|
||||||
|
return h(Checkbox, {
|
||||||
|
disabled,
|
||||||
|
checked: cellProps.row[cellProps.col.dataIndex] === checkedValue,
|
||||||
|
"onUpdate:checked": (checked) => updateValue(checked ? checkedValue : uncheckedValue),
|
||||||
|
onClick: (event) => event.stopPropagation(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return h(Input, {
|
||||||
|
...commonProps,
|
||||||
|
value: cellProps.row[cellProps.col.dataIndex],
|
||||||
|
allowClear: editor.allowClear ?? true,
|
||||||
|
maxlength: editor.maxlength,
|
||||||
|
placeholder:
|
||||||
|
typeof editor.placeholder === "function"
|
||||||
|
? editor.placeholder(cellProps.row)
|
||||||
|
: editor.placeholder,
|
||||||
|
status: typeof editor.status === "function" ? editor.status(cellProps.row) : editor.status,
|
||||||
|
"onUpdate:value": updateValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// StkTable 的编辑单元格和多选列统一在封装层生成,业务页面只声明 editor / rowSelection。
|
||||||
|
const tableColumnsModel = computed({
|
||||||
|
get: () => {
|
||||||
|
const columns = props.columns.map((column) =>
|
||||||
|
column.editor && !column.customCell
|
||||||
|
? { ...column, customCell: EditableCell, __fmsEditor: true }
|
||||||
|
: column,
|
||||||
|
);
|
||||||
|
if (!props.rowSelection) return columns;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
dataIndex: "__fmsSelection",
|
||||||
|
key: "__fmsSelection",
|
||||||
|
title: "",
|
||||||
|
width: 44,
|
||||||
|
fixed: "left",
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
customCell: SelectionCell,
|
||||||
|
customHeaderCell: SelectionHeaderCell,
|
||||||
|
__fmsSelection: true,
|
||||||
|
},
|
||||||
|
...columns,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
set: (value) =>
|
||||||
|
emit(
|
||||||
|
"update:columns",
|
||||||
|
value
|
||||||
|
.filter((column) => !column.__fmsSelection)
|
||||||
|
.map((column) => {
|
||||||
|
if (!column.__fmsEditor) return column;
|
||||||
|
const rest = { ...column };
|
||||||
|
delete rest.customCell;
|
||||||
|
delete rest.__fmsEditor;
|
||||||
|
return rest;
|
||||||
|
}),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
const tableRef = ref(null);
|
const tableRef = ref(null);
|
||||||
|
|
||||||
|
// 表格样式
|
||||||
|
const customVars = ref({
|
||||||
|
"--header-row-height": "40px",
|
||||||
|
"--row-height": "34px",
|
||||||
|
"--footer-row-height": "36px",
|
||||||
|
});
|
||||||
|
|
||||||
const { resume: refreshLayout } = useRafFn(
|
const { resume: refreshLayout } = useRafFn(
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -49,10 +228,14 @@ const forwardSlots = computed(() => {
|
|||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
class="fms-table"
|
class="fms-table"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
|
:style="customVars"
|
||||||
:auto-resize="false"
|
:auto-resize="false"
|
||||||
:virtual="virtual"
|
:virtual="virtual"
|
||||||
:virtual-x="virtualX"
|
:virtual-x="virtualX"
|
||||||
:col-resizable="colResizable"
|
:col-resizable="colResizable"
|
||||||
|
v-model:columns="tableColumnsModel"
|
||||||
|
:data-source="dataSource"
|
||||||
|
:row-key="rowKey"
|
||||||
:bordered="bordered"
|
:bordered="bordered"
|
||||||
:no-data-full="noDataFull"
|
:no-data-full="noDataFull"
|
||||||
>
|
>
|
||||||
@@ -68,6 +251,8 @@ const forwardSlots = computed(() => {
|
|||||||
</StkTable>
|
</StkTable>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fms-table {
|
.fms-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function useLookupOptions(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { start: startSearchTimer, stop: stopSearchTimer } = useTimeoutFn(
|
const { start: startSearchTimer, stop: stopSearchTimer } = useTimeoutFn(
|
||||||
() => load(query.value),
|
(searchText) => load(searchText),
|
||||||
() => props.searchDelay,
|
() => props.searchDelay,
|
||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
);
|
);
|
||||||
@@ -76,8 +76,11 @@ export function useLookupOptions(props) {
|
|||||||
|
|
||||||
function search(value) {
|
function search(value) {
|
||||||
stopResetTimer();
|
stopResetTimer();
|
||||||
query.value = String(value ?? "");
|
const searchText = String(value ?? "");
|
||||||
startSearchTimer();
|
query.value = searchText;
|
||||||
|
requestId += 1;
|
||||||
|
loading.value = false;
|
||||||
|
startSearchTimer(searchText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ const fallbackMenuItems = [
|
|||||||
{ key: "/system/mappingConfig", label: "映射配置" },
|
{ key: "/system/mappingConfig", label: "映射配置" },
|
||||||
{ key: "/system/autoId", label: "自动编码" },
|
{ key: "/system/autoId", label: "自动编码" },
|
||||||
{ key: "/system/modules", label: "模块管理" },
|
{ key: "/system/modules", label: "模块管理" },
|
||||||
|
{ key: "/system/i18n", label: "多语言管理" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from "vue-router";
|
|||||||
import NProgress from "nprogress";
|
import NProgress from "nprogress";
|
||||||
import { message } from "antdv-next";
|
import { message } from "antdv-next";
|
||||||
import "nprogress/nprogress.css";
|
import "nprogress/nprogress.css";
|
||||||
|
import routes from "./routes";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { usePermissionStore } from "@/stores/permissions";
|
import { usePermissionStore } from "@/stores/permissions";
|
||||||
|
|
||||||
@@ -13,47 +14,6 @@ function getRedirectPath(value) {
|
|||||||
: "/dashboard";
|
: "/dashboard";
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
component: () => import("@/layouts/DefaultLayout.vue"),
|
|
||||||
meta: { requiresAuth: true },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
redirect: "/dashboard",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "dashboard",
|
|
||||||
name: "dashboard",
|
|
||||||
component: () => import("@/views/DashboardView.vue"),
|
|
||||||
meta: { title: "工作台", menuKey: "/dashboard" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "system/modules",
|
|
||||||
name: "system-modules",
|
|
||||||
component: () => import("@/views/module-management/ModuleManagementView.vue"),
|
|
||||||
meta: {
|
|
||||||
title: "模块管理",
|
|
||||||
section: "系统管理",
|
|
||||||
menuKey: "/system/modules",
|
|
||||||
moduleCode: "M_MODULE",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/login",
|
|
||||||
name: "login",
|
|
||||||
component: () => import("@/views/LoginView.vue"),
|
|
||||||
meta: { title: "登录" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/:pathMatch(.*)*",
|
|
||||||
redirect: "/dashboard",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes,
|
routes,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
component: () => import("@/layouts/DefaultLayout.vue"),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
redirect: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "dashboard",
|
||||||
|
name: "dashboard",
|
||||||
|
component: () => import("@/views/dashboard/index.vue"),
|
||||||
|
meta: { title: "工作台", menuKey: "/dashboard" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "system/modules",
|
||||||
|
name: "system-modules",
|
||||||
|
component: () => import("@/views/module-management/index.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "模块管理",
|
||||||
|
section: "系统管理",
|
||||||
|
menuKey: "/system/modules",
|
||||||
|
moduleCode: "m_module",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "system/i18n",
|
||||||
|
name: "system-i18n",
|
||||||
|
component: () => import("@/views/i18n-management/index.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "多语言管理",
|
||||||
|
section: "系统管理",
|
||||||
|
menuKey: "/system/i18n",
|
||||||
|
moduleCode: "m_i18n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "login",
|
||||||
|
component: () => import("@/views/login/index.vue"),
|
||||||
|
meta: { title: "登录" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/:pathMatch(.*)*",
|
||||||
|
redirect: "/dashboard",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -11,26 +11,46 @@ export const usePermissionStore = defineStore("permissions", () => {
|
|||||||
|
|
||||||
const moduleSet = computed(() => new Set(moduleCodes.value));
|
const moduleSet = computed(() => new Set(moduleCodes.value));
|
||||||
const powerSet = computed(() => new Set(powerCodes.value));
|
const powerSet = computed(() => new Set(powerCodes.value));
|
||||||
|
const isG3soft = computed(() => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
return String(authStore.user?.account || "").trim().toLowerCase() === "g3soft";
|
||||||
|
});
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const userId = String(authStore.user?.id || "");
|
const userId = String(authStore.user?.id || "");
|
||||||
|
if (isG3soft.value) {
|
||||||
|
loadedUserId.value = `g3soft:${userId}`;
|
||||||
|
moduleCodes.value = [];
|
||||||
|
powerCodes.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!/^\d+$/.test(userId)) throw new Error("登录用户 ID 无效,请重新登录");
|
if (!/^\d+$/.test(userId)) throw new Error("登录用户 ID 无效,请重新登录");
|
||||||
if (loadedUserId.value === userId) return;
|
if (loadedUserId.value === userId) return;
|
||||||
if (loadingPromise.value) return loadingPromise.value;
|
if (loadingPromise.value) return loadingPromise.value;
|
||||||
|
|
||||||
loadingPromise.value = Promise.all([
|
loadingPromise.value = Promise.all([
|
||||||
|
loadDataApi("b_module", "b_canuse = 1", "b_id ASC"),
|
||||||
loadDataApi("b_user_module", `b_user_id = ${userId}`, "b_id ASC"),
|
loadDataApi("b_user_module", `b_user_id = ${userId}`, "b_id ASC"),
|
||||||
loadDataApi("b_user_power", `b_user_id = ${userId}`, "b_id ASC"),
|
loadDataApi("b_user_power", `b_user_id = ${userId}`, "b_id ASC"),
|
||||||
loadDataApi("b_module_power", "b_canuse = 1", "b_id ASC"),
|
loadDataApi("b_module_power", "b_canuse = 1", "b_id ASC"),
|
||||||
])
|
])
|
||||||
.then(([userModules, userPowers, powers]) => {
|
.then(([modules, userModules, userPowers, powers]) => {
|
||||||
|
const moduleCodeById = new Map(
|
||||||
|
modules.data.map((module) => [String(module.b_id), module.b_code]),
|
||||||
|
);
|
||||||
const powerById = new Map(powers.data.map((power) => [String(power.b_id), power]));
|
const powerById = new Map(powers.data.map((power) => [String(power.b_id), power]));
|
||||||
moduleCodes.value = userModules.data.map((item) => item.b_module_id);
|
moduleCodes.value = userModules.data
|
||||||
|
.map((item) => moduleCodeById.get(String(item.b_module_id)))
|
||||||
|
.filter(Boolean);
|
||||||
powerCodes.value = userPowers.data
|
powerCodes.value = userPowers.data
|
||||||
.map((item) => powerById.get(String(item.b_power_id)))
|
.map((item) => powerById.get(String(item.b_power_id)))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((power) => `${power.b_module_id}.${power.b_code}`);
|
.map((power) => {
|
||||||
|
const moduleCode = moduleCodeById.get(String(power.b_module_id));
|
||||||
|
return moduleCode ? `${moduleCode}.${power.b_code}` : "";
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
loadedUserId.value = userId;
|
loadedUserId.value = userId;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -40,11 +60,14 @@ export const usePermissionStore = defineStore("permissions", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canModule(moduleCode) {
|
function canModule(moduleCode) {
|
||||||
return !moduleCode || moduleSet.value.has(moduleCode);
|
return isG3soft.value || !moduleCode || moduleSet.value.has(moduleCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function canPower(moduleCode, powerCode) {
|
function canPower(moduleCode, powerCode) {
|
||||||
return canModule(moduleCode) && powerSet.value.has(`${moduleCode}.${powerCode}`);
|
return (
|
||||||
|
isG3soft.value ||
|
||||||
|
(canModule(moduleCode) && powerSet.value.has(`${moduleCode}.${powerCode}`))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
@@ -53,5 +76,5 @@ export const usePermissionStore = defineStore("permissions", () => {
|
|||||||
powerCodes.value = [];
|
powerCodes.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return { load, canModule, canPower, clear, moduleCodes, powerCodes };
|
return { load, canModule, canPower, clear, moduleCodes, powerCodes, isG3soft };
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/* ============================================================
|
||||||
|
* 基础重置 / Base Reset
|
||||||
|
* ============================================================ */
|
||||||
:root {
|
:root {
|
||||||
color: #1f2329;
|
color: #1f2329;
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
@@ -39,6 +42,9 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 图标 / Icons
|
||||||
|
* ============================================================ */
|
||||||
.lucide {
|
.lucide {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -47,6 +53,9 @@ a {
|
|||||||
vertical-align: -0.125em;
|
vertical-align: -0.125em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* 主题变量 / Theme Tokens
|
||||||
|
* ============================================================ */
|
||||||
.theme-root {
|
.theme-root {
|
||||||
--fms-bg: #f5f7fa;
|
--fms-bg: #f5f7fa;
|
||||||
--fms-surface: #ffffff;
|
--fms-surface: #ffffff;
|
||||||
@@ -59,15 +68,4 @@ a {
|
|||||||
color: var(--fms-text);
|
color: var(--fms-text);
|
||||||
background: var(--fms-bg);
|
background: var(--fms-bg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
scroll-behavior: auto !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export const I18N_LOCALES = [
|
||||||
|
{ value: "zh-CN", label: "简体中文", shortLabel: "简体" },
|
||||||
|
{ value: "en-US", label: "English", shortLabel: "English" },
|
||||||
|
{ value: "zh-TW", label: "繁體中文", shortLabel: "繁體" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const I18N_LOCALE_CODES = I18N_LOCALES.map((item) => item.value);
|
||||||
|
|
||||||
|
export function getI18nNamespace(key) {
|
||||||
|
return String(key || "").split(".", 1)[0] || "common";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidI18nKey(key) {
|
||||||
|
return /^[A-Za-z][A-Za-z0-9_.:-]{1,149}$/.test(String(key || ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupI18nRows(rows) {
|
||||||
|
const groups = {};
|
||||||
|
rows.forEach((item) => {
|
||||||
|
const key = item.b_key;
|
||||||
|
groups[key] ||= { key, values: {}, records: {} };
|
||||||
|
groups[key].values[item.b_locale] = item.b_value;
|
||||||
|
groups[key].records[item.b_locale] = item;
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,533 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { App } from "antdv-next";
|
||||||
|
import { Languages, Plus, RefreshCw, RotateCcw, Save, Trash2 } from "@lucide/vue";
|
||||||
|
import FmsTable from "@/components/FmsTable.vue";
|
||||||
|
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { usePermissionStore } from "@/stores/permissions";
|
||||||
|
import {
|
||||||
|
I18N_LOCALES,
|
||||||
|
I18N_LOCALE_CODES,
|
||||||
|
getI18nNamespace,
|
||||||
|
groupI18nRows,
|
||||||
|
isValidI18nKey,
|
||||||
|
} from "@/utils/i18n";
|
||||||
|
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const route = useRoute();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const original = ref("");
|
||||||
|
const groups = ref({});
|
||||||
|
const selectedRowKeys = ref([]);
|
||||||
|
const keyModuleCodes = ref(new Map());
|
||||||
|
const data = reactive({ modules: [], fields: [], powers: [], translations: [] });
|
||||||
|
const filters = reactive({
|
||||||
|
keyword: "",
|
||||||
|
namespace: undefined,
|
||||||
|
moduleCode: undefined,
|
||||||
|
locale: undefined,
|
||||||
|
status: "all",
|
||||||
|
});
|
||||||
|
const createDialog = reactive({ open: false, key: "", values: {} });
|
||||||
|
const deleteDialogOpen = ref(false);
|
||||||
|
|
||||||
|
const canCreate = computed(() => permissionStore.canPower("m_i18n", "create"));
|
||||||
|
const canUpdate = computed(() => permissionStore.canPower("m_i18n", "update"));
|
||||||
|
const canDelete = computed(() => permissionStore.canPower("m_i18n", "delete"));
|
||||||
|
const dirty = computed(() => serializeGroups(groups.value) !== original.value);
|
||||||
|
const moduleOptions = computed(() =>
|
||||||
|
data.modules.map((item) => ({
|
||||||
|
value: item.b_code,
|
||||||
|
label: item.b_name + " (" + item.b_code + ")",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allRows = computed(() =>
|
||||||
|
Object.values(groups.value)
|
||||||
|
.map((group) => {
|
||||||
|
const missingLocales = I18N_LOCALE_CODES.filter((locale) => !group.values[locale]?.trim());
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
...Object.fromEntries(
|
||||||
|
I18N_LOCALE_CODES.map((locale) => [locale, group.values[locale] || ""]),
|
||||||
|
),
|
||||||
|
namespace: getI18nNamespace(group.key),
|
||||||
|
moduleCodes: Array.from(keyModuleCodes.value.get(group.key) || inferModuleCodes(group.key)),
|
||||||
|
moduleCodesText:
|
||||||
|
Array.from(keyModuleCodes.value.get(group.key) || inferModuleCodes(group.key)).join(
|
||||||
|
"、",
|
||||||
|
) || "通用 / 未绑定",
|
||||||
|
missingLocales,
|
||||||
|
statusText: missingLocales.length ? `缺 ${missingLocales.length}` : "完整",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((left, right) => left.key.localeCompare(right.key)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const namespaceOptions = computed(() =>
|
||||||
|
Array.from(new Set(allRows.value.map((item) => item.namespace)))
|
||||||
|
.sort()
|
||||||
|
.map((value) => ({ value, label: value })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredRows = computed(() => {
|
||||||
|
const keyword = filters.keyword.trim().toLowerCase();
|
||||||
|
return allRows.value.filter((row) => {
|
||||||
|
if (keyword) {
|
||||||
|
const searchText = [row.key, ...Object.values(row.values)].join(" ").toLowerCase();
|
||||||
|
if (!searchText.includes(keyword)) return false;
|
||||||
|
}
|
||||||
|
if (filters.namespace && row.namespace !== filters.namespace) return false;
|
||||||
|
if (filters.moduleCode && !row.moduleCodes.includes(filters.moduleCode)) return false;
|
||||||
|
|
||||||
|
const focusedLocales = filters.locale ? [filters.locale] : I18N_LOCALE_CODES;
|
||||||
|
const missingCount = focusedLocales.filter((locale) => !row.values[locale]?.trim()).length;
|
||||||
|
if (filters.status === "missing" && missingCount === 0) return false;
|
||||||
|
if (filters.status === "complete" && missingCount > 0) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleLocales = computed(() =>
|
||||||
|
filters.locale ? I18N_LOCALES.filter((item) => item.value === filters.locale) : I18N_LOCALES,
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
{ title: "多语言键", dataIndex: "key", key: "key", width: 280, fixed: "left" },
|
||||||
|
{ title: "命名空间", dataIndex: "namespace", key: "namespace", width: 110 },
|
||||||
|
{ title: "关联模块", dataIndex: "moduleCodesText", key: "moduleCodes", width: 190 },
|
||||||
|
...visibleLocales.value.map((locale) => ({
|
||||||
|
title: locale.label,
|
||||||
|
dataIndex: locale.value,
|
||||||
|
key: locale.value,
|
||||||
|
locale: locale.value,
|
||||||
|
width: 250,
|
||||||
|
editor: {
|
||||||
|
maxlength: 500,
|
||||||
|
disabled: (row) => !canEditRow(row),
|
||||||
|
placeholder: `填写 ${locale.value} 文案`,
|
||||||
|
status: (row) => (row[locale.value]?.trim() ? undefined : "warning"),
|
||||||
|
onChange: (value, row) => {
|
||||||
|
row.values[locale.value] = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{ title: "状态", dataIndex: "statusText", key: "status", width: 100, fixed: "right" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedRowKeys.value,
|
||||||
|
onChange: (keys) => {
|
||||||
|
selectedRowKeys.value = keys;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function serializeGroups(value) {
|
||||||
|
return JSON.stringify(
|
||||||
|
Object.values(value)
|
||||||
|
.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
values: Object.fromEntries(
|
||||||
|
I18N_LOCALE_CODES.map((locale) => [locale, item.values[locale] || ""]),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.sort((left, right) => left.key.localeCompare(right.key)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferModuleCodes(key) {
|
||||||
|
const parts = String(key).split(".");
|
||||||
|
return new Set(data.modules.map((item) => item.b_code).filter((code) => parts.includes(code)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildModuleReferences() {
|
||||||
|
const moduleById = new Map(data.modules.map((item) => [String(item.b_id), item.b_code]));
|
||||||
|
const result = new Map();
|
||||||
|
const bind = (key, moduleCode) => {
|
||||||
|
if (!key || !moduleCode) return;
|
||||||
|
if (!result.has(key)) result.set(key, new Set());
|
||||||
|
result.get(key).add(moduleCode);
|
||||||
|
};
|
||||||
|
data.modules.forEach((item) => bind(item.b_i18n, item.b_code));
|
||||||
|
data.fields.forEach((item) => bind(item.b_i18n, moduleById.get(String(item.b_module_id))));
|
||||||
|
data.powers.forEach((item) => bind(item.b_i18n, moduleById.get(String(item.b_module_id))));
|
||||||
|
keyModuleCodes.value = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDraft() {
|
||||||
|
const nextGroups = groupI18nRows(data.translations);
|
||||||
|
[...data.modules, ...data.fields, ...data.powers].forEach((item) => {
|
||||||
|
if (item.b_i18n) {
|
||||||
|
nextGroups[item.b_i18n] ||= { key: item.b_i18n, values: {}, records: {} };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
groups.value = nextGroups;
|
||||||
|
original.value = serializeGroups(groups.value);
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAll() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const [modules, fields, powers, translations] = await Promise.all([
|
||||||
|
loadDataApi("b_module", null, "b_xh ASC"),
|
||||||
|
loadDataApi("b_module_field", null, "b_xh ASC"),
|
||||||
|
loadDataApi("b_module_power", null, "b_xh ASC"),
|
||||||
|
loadDataApi("b_i18n", null, "b_key ASC"),
|
||||||
|
]);
|
||||||
|
Object.assign(data, {
|
||||||
|
modules: modules.data,
|
||||||
|
fields: fields.data,
|
||||||
|
powers: powers.data,
|
||||||
|
translations: translations.data,
|
||||||
|
});
|
||||||
|
rebuildModuleReferences();
|
||||||
|
resetDraft();
|
||||||
|
if (typeof route.query.module === "string") filters.moduleCode = route.query.module;
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "多语言资源加载失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
const modulePrefix = filters.moduleCode ? "." + filters.moduleCode + "." : ".";
|
||||||
|
createDialog.key = (filters.namespace || "common") + modulePrefix;
|
||||||
|
createDialog.values = Object.fromEntries(I18N_LOCALE_CODES.map((locale) => [locale, ""]));
|
||||||
|
createDialog.open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addKey() {
|
||||||
|
const key = createDialog.key.trim();
|
||||||
|
if (!isValidI18nKey(key)) {
|
||||||
|
message.warning("多语言键需以字母开头,只能包含字母、数字、点、冒号、短横线和下划线");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (groups.value[key]) {
|
||||||
|
message.warning("多语言键已经存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!I18N_LOCALE_CODES.some((locale) => createDialog.values[locale]?.trim())) {
|
||||||
|
message.warning("请至少填写一种语言的文案");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
groups.value = {
|
||||||
|
...groups.value,
|
||||||
|
[key]: { key, values: { ...createDialog.values }, records: {} },
|
||||||
|
};
|
||||||
|
createDialog.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestDeleteSelected() {
|
||||||
|
if (!selectedRowKeys.value.length) return;
|
||||||
|
deleteDialogOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
const selected = new Set(selectedRowKeys.value);
|
||||||
|
groups.value = Object.fromEntries(
|
||||||
|
Object.entries(groups.value).filter(([key]) => !selected.has(key)),
|
||||||
|
);
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
deleteDialogOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canEditRow(record) {
|
||||||
|
return Object.keys(record.records || {}).length ? canUpdate.value : canCreate.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTranslations() {
|
||||||
|
if (!dirty.value) return;
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
const oldMap = new Map(
|
||||||
|
data.translations.map((item) => [item.b_key + "\u0000" + item.b_locale, item]),
|
||||||
|
);
|
||||||
|
const desiredMap = new Map();
|
||||||
|
Object.values(groups.value).forEach((group) => {
|
||||||
|
I18N_LOCALE_CODES.forEach((locale) => {
|
||||||
|
const value = group.values[locale]?.trim();
|
||||||
|
if (value) {
|
||||||
|
desiredMap.set(group.key + "\u0000" + locale, { key: group.key, locale, value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const inserts = [];
|
||||||
|
const updates = [];
|
||||||
|
const deletes = [];
|
||||||
|
const now = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
|
||||||
|
for (const [compoundKey, item] of desiredMap) {
|
||||||
|
const old = oldMap.get(compoundKey);
|
||||||
|
if (!old) {
|
||||||
|
const id = String((await nextIdApi()).data);
|
||||||
|
inserts.push({
|
||||||
|
b_id: id,
|
||||||
|
b_key: item.key,
|
||||||
|
b_locale: item.locale,
|
||||||
|
b_value: item.value,
|
||||||
|
b_inputuser_id: String(authStore.user.id),
|
||||||
|
b_inputdatetime: now,
|
||||||
|
b_updateuser_id: null,
|
||||||
|
b_updatedatetime: null,
|
||||||
|
});
|
||||||
|
} else if (old.b_value !== item.value) {
|
||||||
|
updates.push({
|
||||||
|
...old,
|
||||||
|
b_value: item.value,
|
||||||
|
b_updateuser_id: String(authStore.user.id),
|
||||||
|
b_updatedatetime: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [compoundKey, old] of oldMap) {
|
||||||
|
const keyWasDeleted = !groups.value[old.b_key];
|
||||||
|
const managedLocale = I18N_LOCALE_CODES.includes(old.b_locale);
|
||||||
|
if (!desiredMap.has(compoundKey) && (keyWasDeleted || managedLocale)) {
|
||||||
|
deletes.push({ b_id: old.b_id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inserts.length && !updates.length && !deletes.length) {
|
||||||
|
resetDraft();
|
||||||
|
message.info("绑定中的空 Key 会继续保留,以便补充缺失翻译");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await saveObjectApi([{ table: "b_i18n", key_field: "b_id", inserts, updates, deletes }]);
|
||||||
|
message.success("多语言资源已保存");
|
||||||
|
await loadAll();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "多语言资源保存失败");
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
Object.assign(filters, {
|
||||||
|
keyword: "",
|
||||||
|
namespace: undefined,
|
||||||
|
moduleCode: undefined,
|
||||||
|
locale: undefined,
|
||||||
|
status: "all",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.module,
|
||||||
|
(value) => {
|
||||||
|
if (typeof value === "string") filters.moduleCode = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await permissionStore.load();
|
||||||
|
await loadAll();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="i18n-management">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<div class="page-title"><Languages />多语言资源中心</div>
|
||||||
|
<p>统一维护菜单、模块、权限、字段、提示、按钮与校验文案。</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a-button :disabled="!dirty" @click="resetDraft"><RotateCcw />撤销修改</a-button>
|
||||||
|
<a-button :loading="loading" @click="loadAll"><RefreshCw />刷新</a-button>
|
||||||
|
<a-button type="primary" :loading="saving" :disabled="!dirty" @click="saveTranslations">
|
||||||
|
<Save />保存
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="filter-bar">
|
||||||
|
<a-input-search v-model:value="filters.keyword" allow-clear placeholder="搜索 key 或文案" />
|
||||||
|
<a-select
|
||||||
|
v-model:value="filters.namespace"
|
||||||
|
allow-clear
|
||||||
|
placeholder="命名空间"
|
||||||
|
:options="namespaceOptions"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-model:value="filters.moduleCode"
|
||||||
|
show-search
|
||||||
|
allow-clear
|
||||||
|
placeholder="关联模块"
|
||||||
|
:options="moduleOptions"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-model:value="filters.locale"
|
||||||
|
allow-clear
|
||||||
|
placeholder="聚焦语言"
|
||||||
|
:options="I18N_LOCALES"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-model:value="filters.status"
|
||||||
|
:options="[
|
||||||
|
{ value: 'all', label: '全部状态' },
|
||||||
|
{ value: 'missing', label: '存在缺失' },
|
||||||
|
{ value: 'complete', label: '翻译完整' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<a-button @click="resetFilters">重置筛选</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-toolbar">
|
||||||
|
<div class="toolbar-actions">
|
||||||
|
<a-button type="primary" :disabled="!canCreate" @click="openCreateDialog">
|
||||||
|
<Plus />新增 Key
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
danger
|
||||||
|
:disabled="!canDelete || !selectedRowKeys.length"
|
||||||
|
@click="requestDeleteSelected"
|
||||||
|
>
|
||||||
|
<Trash2 />批量删除
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<span>共 {{ filteredRows.length }} 个 Key,已选择 {{ selectedRowKeys.length }} 个</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<FmsTable
|
||||||
|
row-key="key"
|
||||||
|
:col-resizable="false"
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="filteredRows"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
/>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:open="createDialog.open"
|
||||||
|
title="新增多语言 Key"
|
||||||
|
ok-text="添加"
|
||||||
|
cancel-text="取消"
|
||||||
|
@ok="addKey"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item
|
||||||
|
label="多语言键"
|
||||||
|
required
|
||||||
|
extra="建议使用 namespace.module_code.name,例如 field.m_biz.customer.placeholder"
|
||||||
|
>
|
||||||
|
<a-input v-model:value="createDialog.key" :maxlength="150" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-for="locale in I18N_LOCALES" :key="locale.value" :label="locale.label">
|
||||||
|
<a-input v-model:value="createDialog.values[locale.value]" :maxlength="500" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:open="deleteDialogOpen"
|
||||||
|
title="删除多语言 Key"
|
||||||
|
ok-text="删除"
|
||||||
|
cancel-text="取消"
|
||||||
|
ok-type="danger"
|
||||||
|
@ok="deleteSelected"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
确定删除选中的 {{ selectedRowKeys.length }} 个 Key 及其全部语言文本吗?已绑定这些 Key
|
||||||
|
的业务对象将显示回退文案。
|
||||||
|
</p>
|
||||||
|
</a-modal>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.i18n-management {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--fms-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--fms-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header,
|
||||||
|
.filter-bar,
|
||||||
|
.table-toolbar,
|
||||||
|
.header-actions,
|
||||||
|
.toolbar-actions,
|
||||||
|
.page-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header,
|
||||||
|
.table-toolbar {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--fms-text);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p,
|
||||||
|
.table-toolbar,
|
||||||
|
.muted {
|
||||||
|
color: var(--fms-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions,
|
||||||
|
.toolbar-actions,
|
||||||
|
.filter-bar {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--fms-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar :deep(.ant-input-search) {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar :deep(.ant-select) {
|
||||||
|
width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 420px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap :deep(.ant-spin-nested-loading),
|
||||||
|
.table-wrap :deep(.ant-spin-container),
|
||||||
|
.table-wrap :deep(.ant-spin) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap :deep(.stk-table .ant-input) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File renamed without changes.
@@ -1,11 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import ModuleTranslationEditor from "./ModuleTranslationEditor.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
directoryParentOptions: { type: Array, default: () => [] },
|
directoryParentOptions: { type: Array, default: () => [] },
|
||||||
isNew: Boolean,
|
|
||||||
parentOptions: { type: Array, default: () => [] },
|
parentOptions: { type: Array, default: () => [] },
|
||||||
});
|
});
|
||||||
const emit = defineEmits(["update-module-id"]);
|
const emit = defineEmits(["update-module-code"]);
|
||||||
const module = defineModel("module", { type: Object, default: null });
|
const module = defineModel("module", { type: Object, default: null });
|
||||||
|
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||||
|
|
||||||
function updateRoute(value) {
|
function updateRoute(value) {
|
||||||
module.value.b_route = value || null;
|
module.value.b_route = value || null;
|
||||||
@@ -18,7 +20,7 @@ function updateRoute(value) {
|
|||||||
<a-form v-if="module.b_type === 'directory'" layout="vertical" class="directory-form">
|
<a-form v-if="module.b_type === 'directory'" layout="vertical" class="directory-form">
|
||||||
<div class="directory-grid">
|
<div class="directory-grid">
|
||||||
<a-form-item label="模块编码" required>
|
<a-form-item label="模块编码" required>
|
||||||
<a-input :value="module.b_id" disabled />
|
<a-input :value="module.b_code" @update:value="emit('update-module-code', $event)" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="模块名称" required>
|
<a-form-item label="模块名称" required>
|
||||||
<a-input v-model:value="module.b_name" />
|
<a-input v-model:value="module.b_name" />
|
||||||
@@ -33,7 +35,14 @@ function updateRoute(value) {
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="多语言键">
|
<a-form-item label="多语言键">
|
||||||
<a-input v-model:value="module.b_i18n" />
|
<div class="i18n-editor-row">
|
||||||
|
<a-input v-model:value="module.b_i18n" />
|
||||||
|
<ModuleTranslationEditor
|
||||||
|
v-model:translations="translations"
|
||||||
|
:i18n-key="module.b_i18n"
|
||||||
|
:disabled="!module.b_i18n?.trim()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="显示顺序">
|
<a-form-item label="显示顺序">
|
||||||
<a-input-number v-model:value="module.b_xh" :min="0" />
|
<a-input-number v-model:value="module.b_xh" :min="0" />
|
||||||
@@ -44,11 +53,7 @@ function updateRoute(value) {
|
|||||||
<a-form v-else layout="vertical" class="basic-form">
|
<a-form v-else layout="vertical" class="basic-form">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<a-form-item label="模块编码" required>
|
<a-form-item label="模块编码" required>
|
||||||
<a-input
|
<a-input :value="module.b_code" @update:value="emit('update-module-code', $event)" />
|
||||||
:value="module.b_id"
|
|
||||||
:disabled="!isNew"
|
|
||||||
@update:value="emit('update-module-id', $event)"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="模块名称" required>
|
<a-form-item label="模块名称" required>
|
||||||
<a-input v-model:value="module.b_name" />
|
<a-input v-model:value="module.b_name" />
|
||||||
@@ -70,7 +75,14 @@ function updateRoute(value) {
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="多语言键">
|
<a-form-item label="多语言键">
|
||||||
<a-input v-model:value="module.b_i18n" />
|
<div class="i18n-editor-row">
|
||||||
|
<a-input v-model:value="module.b_i18n" />
|
||||||
|
<ModuleTranslationEditor
|
||||||
|
v-model:translations="translations"
|
||||||
|
:i18n-key="module.b_i18n"
|
||||||
|
:disabled="!module.b_i18n?.trim()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="查询表或视图">
|
<a-form-item label="查询表或视图">
|
||||||
<a-input v-model:value="module.b_viewtable" />
|
<a-input v-model:value="module.b_viewtable" />
|
||||||
@@ -81,12 +93,9 @@ function updateRoute(value) {
|
|||||||
<a-form-item label="主键字段">
|
<a-form-item label="主键字段">
|
||||||
<a-input v-model:value="module.b_keyfield" />
|
<a-input v-model:value="module.b_keyfield" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="排序字段">
|
<a-form-item label="排序条件">
|
||||||
<a-input v-model:value="module.b_orderfield" />
|
<a-input v-model:value="module.b_orderfield" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="排序方向">
|
|
||||||
<a-segmented v-model:value="module.b_ordertype" :options="['ASC', 'DESC']" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="显示顺序">
|
<a-form-item label="显示顺序">
|
||||||
<a-input-number v-model:value="module.b_xh" :min="0" />
|
<a-input-number v-model:value="module.b_xh" :min="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -100,7 +109,7 @@ function updateRoute(value) {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-top: 10px;
|
padding-top: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.directory-form {
|
.directory-form {
|
||||||
@@ -124,6 +133,17 @@ function updateRoute(value) {
|
|||||||
gap: 0 10px;
|
gap: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.i18n-editor-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.i18n-editor-row :deep(.ant-input) {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-form-item) {
|
:deep(.ant-form-item) {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
+156
-13
@@ -1,29 +1,165 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { App } from "antdv-next";
|
||||||
|
import { Plus, Trash2 } from "@lucide/vue";
|
||||||
import FmsTable from "@/components/FmsTable.vue";
|
import FmsTable from "@/components/FmsTable.vue";
|
||||||
|
import { nextIdApi } from "@/services/api";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
moduleId: { type: [String, Number], default: "" },
|
||||||
|
moduleCode: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
|
||||||
const fields = defineModel("fields", { type: Array, default: () => [] });
|
const fields = defineModel("fields", { type: Array, default: () => [] });
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const adding = ref(false);
|
||||||
|
const selectedRowKeys = ref([]);
|
||||||
|
|
||||||
const columns = [
|
const rowSelection = computed(() => ({
|
||||||
{ dataIndex: "b_field", title: "字段", width: 150 },
|
selectedRowKeys: selectedRowKeys.value,
|
||||||
{ dataIndex: "b_name", title: "名称", width: 160 },
|
onChange: (keys) => {
|
||||||
{ dataIndex: "b_type", title: "类型", width: 120 },
|
selectedRowKeys.value = keys;
|
||||||
{ dataIndex: "b_canlist", title: "列表", width: 64, align: "center", headerAlign: "center" },
|
},
|
||||||
{ dataIndex: "b_canform", title: "表单", width: 64, align: "center", headerAlign: "center" },
|
}));
|
||||||
{ dataIndex: "b_must", title: "必填", width: 64, align: "center", headerAlign: "center" },
|
|
||||||
{ dataIndex: "b_canquery", title: "查询", width: 64, align: "center", headerAlign: "center" },
|
watch(
|
||||||
{ dataIndex: "b_readonly", title: "只读", width: 64, align: "center", headerAlign: "center" },
|
() => fields.value.map((item) => String(item.b_id)),
|
||||||
{ dataIndex: "b_width", title: "宽度", width: 90 },
|
(keys) => {
|
||||||
{ dataIndex: "b_xh", title: "顺序", width: 80 },
|
const available = new Set(keys);
|
||||||
];
|
selectedRowKeys.value = selectedRowKeys.value.filter((key) => available.has(String(key)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function nextFieldCode() {
|
||||||
|
const existing = new Set(fields.value.map((item) => item.b_field));
|
||||||
|
let index = 1;
|
||||||
|
while (existing.has(`b_new_${index}`)) index += 1;
|
||||||
|
return `b_new_${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAdd() {
|
||||||
|
if (!props.moduleId || adding.value) return;
|
||||||
|
adding.value = true;
|
||||||
|
try {
|
||||||
|
const code = nextFieldCode();
|
||||||
|
const id = String((await nextIdApi()).data);
|
||||||
|
fields.value = [
|
||||||
|
...fields.value,
|
||||||
|
{
|
||||||
|
b_id: id,
|
||||||
|
b_module_id: props.moduleId,
|
||||||
|
b_field: code,
|
||||||
|
b_name: "新字段",
|
||||||
|
b_i18n: props.moduleCode ? `field.${props.moduleCode}.${code}` : null,
|
||||||
|
b_type: "string",
|
||||||
|
b_canlist: 1,
|
||||||
|
b_canform: 1,
|
||||||
|
b_must: 0,
|
||||||
|
b_canquery: 0,
|
||||||
|
b_readonly: 0,
|
||||||
|
b_width: 120,
|
||||||
|
b_value: null,
|
||||||
|
b_xh: (fields.value.length + 1) * 10,
|
||||||
|
b_canuse: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "字段创建失败");
|
||||||
|
} finally {
|
||||||
|
adding.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBatchDelete() {
|
||||||
|
const selected = new Set(selectedRowKeys.value.map(String));
|
||||||
|
fields.value = fields.value.filter((item) => !selected.has(String(item.b_id)));
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = ref([
|
||||||
|
{ dataIndex: "b_field", title: "字段", width: 150, editor: { maxlength: 50 } },
|
||||||
|
{ dataIndex: "b_name", title: "名称", width: 150, editor: { maxlength: 100 } },
|
||||||
|
{ dataIndex: "b_i18n", title: "多语言键", width: 230, editor: { maxlength: 150 } },
|
||||||
|
{
|
||||||
|
dataIndex: "b_type",
|
||||||
|
title: "类型",
|
||||||
|
width: 120,
|
||||||
|
editor: {
|
||||||
|
type: "select",
|
||||||
|
options: ["string", "text", "number", "select", "date", "boolean"].map((value) => ({
|
||||||
|
value,
|
||||||
|
label: value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "b_canlist",
|
||||||
|
title: "列表",
|
||||||
|
width: 64,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
editor: "checkbox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "b_canform",
|
||||||
|
title: "表单",
|
||||||
|
width: 64,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
editor: {
|
||||||
|
type: "checkbox",
|
||||||
|
onChange: (value, row) => {
|
||||||
|
if (value === 0) row.b_must = 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "b_must",
|
||||||
|
title: "必填",
|
||||||
|
width: 64,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
editor: { type: "checkbox", disabled: (row) => row.b_canform !== 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "b_canquery",
|
||||||
|
title: "查询",
|
||||||
|
width: 64,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
editor: "checkbox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: "b_readonly",
|
||||||
|
title: "只读",
|
||||||
|
width: 64,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
editor: "checkbox",
|
||||||
|
},
|
||||||
|
{ dataIndex: "b_width", title: "宽度", width: 90, editor: { type: "number", min: 1 } },
|
||||||
|
{ dataIndex: "b_xh", title: "顺序", width: 80, editor: { type: "number", min: 0 } },
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="table-pane">
|
<section class="table-pane">
|
||||||
|
<div class="toolbar">
|
||||||
|
<a-button type="primary" :loading="adding" @click="onAdd">
|
||||||
|
<template #icon><Plus /></template>
|
||||||
|
新增
|
||||||
|
</a-button>
|
||||||
|
<a-button danger :disabled="!selectedRowKeys.length" @click="onBatchDelete">
|
||||||
|
<template #icon><Trash2 /></template>
|
||||||
|
批量删除
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
<FmsTable
|
<FmsTable
|
||||||
class="sub-table"
|
class="sub-table"
|
||||||
row-key="b_id"
|
row-key="b_id"
|
||||||
:columns="columns"
|
v-model:columns="columns"
|
||||||
:data-source="fields"
|
:data-source="fields"
|
||||||
|
:row-selection="rowSelection"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -37,6 +173,13 @@ const columns = [
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex: none;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sub-table {
|
.sub-table {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -1,24 +1,107 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { App } from "antdv-next";
|
||||||
|
import { Plus, Trash2 } from "@lucide/vue";
|
||||||
import FmsTable from "@/components/FmsTable.vue";
|
import FmsTable from "@/components/FmsTable.vue";
|
||||||
|
import { nextIdApi } from "@/services/api";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
moduleId: { type: [String, Number], default: "" },
|
||||||
|
moduleCode: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
|
||||||
const powers = defineModel("powers", { type: Array, default: () => [] });
|
const powers = defineModel("powers", { type: Array, default: () => [] });
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const adding = ref(false);
|
||||||
|
const selectedRowKeys = ref([]);
|
||||||
|
|
||||||
const columns = [
|
const rowSelection = computed(() => ({
|
||||||
{ dataIndex: "b_code", title: "权限代码", width: 180 },
|
selectedRowKeys: selectedRowKeys.value,
|
||||||
{ dataIndex: "b_name", title: "名称", width: 200 },
|
onChange: (keys) => {
|
||||||
{ dataIndex: "b_i18n", title: "多语言键", width: 200 },
|
selectedRowKeys.value = keys;
|
||||||
{ dataIndex: "b_canuse", title: "启用", width: 74, align: "center", headerAlign: "center" },
|
},
|
||||||
{ dataIndex: "b_xh", title: "顺序", width: 90 },
|
}));
|
||||||
];
|
|
||||||
|
watch(
|
||||||
|
() => powers.value.map((item) => String(item.b_id)),
|
||||||
|
(keys) => {
|
||||||
|
const available = new Set(keys);
|
||||||
|
selectedRowKeys.value = selectedRowKeys.value.filter((key) => available.has(String(key)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function nextPowerCode() {
|
||||||
|
const existing = new Set(powers.value.map((item) => item.b_code));
|
||||||
|
let index = 1;
|
||||||
|
while (existing.has(`new_${index}`)) index += 1;
|
||||||
|
return `new_${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAdd() {
|
||||||
|
if (!props.moduleId || adding.value) return;
|
||||||
|
adding.value = true;
|
||||||
|
try {
|
||||||
|
const code = nextPowerCode();
|
||||||
|
const id = String((await nextIdApi()).data);
|
||||||
|
powers.value = [
|
||||||
|
...powers.value,
|
||||||
|
{
|
||||||
|
b_id: id,
|
||||||
|
b_module_id: props.moduleId,
|
||||||
|
b_code: code,
|
||||||
|
b_name: "新权限",
|
||||||
|
b_i18n: props.moduleCode ? `power.${props.moduleCode}.${code}` : null,
|
||||||
|
b_canuse: 1,
|
||||||
|
b_xh: (powers.value.length + 1) * 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "权限创建失败");
|
||||||
|
} finally {
|
||||||
|
adding.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBatchDelete() {
|
||||||
|
const selected = new Set(selectedRowKeys.value.map(String));
|
||||||
|
powers.value = powers.value.filter((item) => !selected.has(String(item.b_id)));
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = ref([
|
||||||
|
{ dataIndex: "b_code", title: "权限代码", width: 180, editor: { maxlength: 50 } },
|
||||||
|
{ dataIndex: "b_name", title: "名称", width: 180, editor: { maxlength: 100 } },
|
||||||
|
{ dataIndex: "b_i18n", title: "多语言键", width: 240, editor: { maxlength: 150 } },
|
||||||
|
{
|
||||||
|
dataIndex: "b_canuse",
|
||||||
|
title: "启用",
|
||||||
|
width: 74,
|
||||||
|
align: "center",
|
||||||
|
headerAlign: "center",
|
||||||
|
editor: "checkbox",
|
||||||
|
},
|
||||||
|
{ dataIndex: "b_xh", title: "顺序", width: 90, editor: { type: "number", min: 0 } },
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="table-pane">
|
<section class="table-pane">
|
||||||
|
<div class="toolbar">
|
||||||
|
<a-button type="primary" :loading="adding" @click="onAdd">
|
||||||
|
<template #icon><Plus /></template>
|
||||||
|
新增
|
||||||
|
</a-button>
|
||||||
|
<a-button danger :disabled="!selectedRowKeys.length" @click="onBatchDelete">
|
||||||
|
<template #icon><Trash2 /></template>
|
||||||
|
批量删除
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
<FmsTable
|
<FmsTable
|
||||||
class="sub-table"
|
class="sub-table"
|
||||||
row-key="b_id"
|
row-key="b_id"
|
||||||
:columns="columns"
|
v-model:columns="columns"
|
||||||
:data-source="powers"
|
:data-source="powers"
|
||||||
|
:row-selection="rowSelection"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -32,6 +115,13 @@ const columns = [
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex: none;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sub-table {
|
.sub-table {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive } from "vue";
|
||||||
|
import { App } from "antdv-next";
|
||||||
|
import { Languages } from "@lucide/vue";
|
||||||
|
import { I18N_LOCALES, isValidI18nKey } from "@/utils/i18n";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
i18nKey: { type: String, default: "" },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const dialog = reactive({ open: false, key: "", values: {} });
|
||||||
|
|
||||||
|
function openEditor() {
|
||||||
|
const key = props.i18nKey.trim();
|
||||||
|
if (!isValidI18nKey(key)) {
|
||||||
|
message.warning("请先填写正确的多语言键");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialog.key = key;
|
||||||
|
dialog.values = Object.fromEntries(
|
||||||
|
I18N_LOCALES.map((locale) => [locale.value, translations.value[key]?.[locale.value] || ""]),
|
||||||
|
);
|
||||||
|
dialog.open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTranslations() {
|
||||||
|
translations.value = {
|
||||||
|
...translations.value,
|
||||||
|
[dialog.key]: { ...dialog.values },
|
||||||
|
};
|
||||||
|
dialog.open = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-button :disabled="disabled" @click="openEditor">
|
||||||
|
<template #icon><Languages /></template>
|
||||||
|
多语言
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:open="dialog.open"
|
||||||
|
title="更新多语言"
|
||||||
|
ok-text="更新"
|
||||||
|
cancel-text="取消"
|
||||||
|
@ok="saveTranslations"
|
||||||
|
>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="多语言键">
|
||||||
|
<a-input :value="dialog.key" disabled />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-for="locale in I18N_LOCALES" :key="locale.value" :label="locale.label">
|
||||||
|
<a-input v-model:value="dialog.values[locale.value]" :maxlength="500" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from "vue";
|
|
||||||
import FmsTable from "@/components/FmsTable.vue";
|
|
||||||
|
|
||||||
const LOCALES = [
|
|
||||||
{ value: "zh-CN", label: "简体中文" },
|
|
||||||
{ value: "en-US", label: "English" },
|
|
||||||
{ value: "zh-TW", label: "繁體中文" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
keys: { type: Array, default: () => [] },
|
|
||||||
});
|
|
||||||
const translations = defineModel("translations", { type: Object, default: () => ({}) });
|
|
||||||
|
|
||||||
const rows = computed(() =>
|
|
||||||
props.keys.map((key) => {
|
|
||||||
const entry = translations.value[key] || {};
|
|
||||||
const row = { key };
|
|
||||||
LOCALES.forEach((locale) => {
|
|
||||||
row[locale.value] = entry[locale.value];
|
|
||||||
});
|
|
||||||
return row;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ dataIndex: "key", title: "多语言键", width: 280, fixed: "left" },
|
|
||||||
...LOCALES.map((locale) => ({ dataIndex: locale.value, title: locale.label, width: 220 })),
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="translation-pane">
|
|
||||||
<FmsTable
|
|
||||||
class="sub-table"
|
|
||||||
row-key="key"
|
|
||||||
:columns="columns"
|
|
||||||
:data-source="rows"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.translation-pane {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-table {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -5,6 +5,7 @@ import FmsTree from "@/components/FmsTree.vue";
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modules: { type: Array, required: true },
|
modules: { type: Array, required: true },
|
||||||
|
protectedKeys: { type: Array, default: () => [] },
|
||||||
selectedKey: { type: String, default: "" },
|
selectedKey: { type: String, default: "" },
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
canCreate: Boolean,
|
canCreate: Boolean,
|
||||||
@@ -16,15 +17,16 @@ const contextNode = ref(null);
|
|||||||
|
|
||||||
const treeData = computed(() => {
|
const treeData = computed(() => {
|
||||||
const keyword = searchText.value.trim().toLowerCase();
|
const keyword = searchText.value.trim().toLowerCase();
|
||||||
|
const codeById = new Map(props.modules.map((item) => [item.b_id, item.b_code]));
|
||||||
const visible = keyword
|
const visible = keyword
|
||||||
? new Set(
|
? new Set(
|
||||||
props.modules
|
props.modules
|
||||||
.filter(
|
.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
String(item.b_id).toLowerCase().includes(keyword) ||
|
String(item.b_code).toLowerCase().includes(keyword) ||
|
||||||
String(item.b_name).toLowerCase().includes(keyword),
|
String(item.b_name).toLowerCase().includes(keyword),
|
||||||
)
|
)
|
||||||
.flatMap((item) => [item.b_id, item.b_parent_id]),
|
.flatMap((item) => [item.b_code, codeById.get(item.b_parent_id)].filter(Boolean)),
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
const children = new Map();
|
const children = new Map();
|
||||||
@@ -35,11 +37,11 @@ const treeData = computed(() => {
|
|||||||
});
|
});
|
||||||
const build = (parent) =>
|
const build = (parent) =>
|
||||||
(children.get(parent) || [])
|
(children.get(parent) || [])
|
||||||
.filter((item) => !visible || visible.has(item.b_id))
|
.filter((item) => !visible || visible.has(item.b_code))
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const nodeChildren = build(item.b_id);
|
const nodeChildren = build(item.b_id);
|
||||||
return {
|
return {
|
||||||
key: item.b_id,
|
key: item.b_code,
|
||||||
title: item.b_name,
|
title: item.b_name,
|
||||||
children: nodeChildren,
|
children: nodeChildren,
|
||||||
isLeaf: nodeChildren.length === 0,
|
isLeaf: nodeChildren.length === 0,
|
||||||
@@ -69,7 +71,7 @@ const contextMenu = computed(() => {
|
|||||||
key: "delete",
|
key: "delete",
|
||||||
label: "删除",
|
label: "删除",
|
||||||
danger: true,
|
danger: true,
|
||||||
disabled: !node || !props.canDelete || ["M_SYSTEM", "M_MODULE"].includes(node.key),
|
disabled: !node || !props.canDelete || props.protectedKeys.includes(String(node.key)),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onClick: ({ key }) => {
|
onClick: ({ key }) => {
|
||||||
|
|||||||
+96
-118
@@ -1,18 +1,18 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { App } from "antdv-next";
|
import { App } from "antdv-next";
|
||||||
import { Database, FileText, Languages, RotateCcw, Save, ShieldCheck, Trash2 } from "@lucide/vue";
|
import { Database, FileText, RotateCcw, Save, ShieldCheck, Trash2 } from "@lucide/vue";
|
||||||
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { usePermissionStore } from "@/stores/permissions";
|
import { usePermissionStore } from "@/stores/permissions";
|
||||||
|
import { I18N_LOCALE_CODES } from "@/utils/i18n";
|
||||||
import ModuleBasicInfoPanel from "./components/ModuleBasicInfoPanel.vue";
|
import ModuleBasicInfoPanel from "./components/ModuleBasicInfoPanel.vue";
|
||||||
import ModuleFieldDefinitionPanel from "./components/ModuleFieldDefinitionPanel.vue";
|
import ModuleFieldDefinitionPanel from "./components/ModuleFieldDefinitionPanel.vue";
|
||||||
import ModulePowerPanel from "./components/ModulePowerPanel.vue";
|
import ModulePowerPanel from "./components/ModulePowerPanel.vue";
|
||||||
import ModuleTranslationsPanel from "./components/ModuleTranslationsPanel.vue";
|
|
||||||
import ModuleTreePanel from "./components/ModuleTreePanel.vue";
|
import ModuleTreePanel from "./components/ModuleTreePanel.vue";
|
||||||
|
|
||||||
|
const PROTECTED_MODULE_CODES = ["m_module", "m_i18n", "m_biz"];
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const LOCALES = ["zh-CN", "en-US", "zh-TW"];
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const permissionStore = usePermissionStore();
|
const permissionStore = usePermissionStore();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -23,6 +23,7 @@ const isNew = ref(false);
|
|||||||
const deleteConfirmOpen = ref(false);
|
const deleteConfirmOpen = ref(false);
|
||||||
const pendingDeleteKey = ref("");
|
const pendingDeleteKey = ref("");
|
||||||
const original = ref("");
|
const original = ref("");
|
||||||
|
const originalTranslationKeys = ref([]);
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
modules: [],
|
modules: [],
|
||||||
fields: [],
|
fields: [],
|
||||||
@@ -33,28 +34,27 @@ const data = reactive({
|
|||||||
});
|
});
|
||||||
const draft = reactive({ module: null, fields: [], powers: [], translations: {} });
|
const draft = reactive({ module: null, fields: [], powers: [], translations: {} });
|
||||||
|
|
||||||
const canCreate = computed(() => permissionStore.canPower("M_MODULE", "create"));
|
const canCreate = computed(() => permissionStore.canPower("m_module", "create"));
|
||||||
const canUpdate = computed(() => permissionStore.canPower("M_MODULE", "update"));
|
const canUpdate = computed(() => permissionStore.canPower("m_module", "update"));
|
||||||
const canDelete = computed(() => permissionStore.canPower("M_MODULE", "delete"));
|
const canDelete = computed(() => permissionStore.canPower("m_module", "delete"));
|
||||||
const canSave = computed(() => (isNew.value ? canCreate.value : canUpdate.value));
|
const canSave = computed(() => (isNew.value ? canCreate.value : canUpdate.value));
|
||||||
const dirty = computed(() => Boolean(draft.module) && serializeDraft() !== original.value);
|
const dirty = computed(() => Boolean(draft.module) && serializeDraft() !== original.value);
|
||||||
const parentOptions = computed(() =>
|
const parentOptions = computed(() =>
|
||||||
data.modules
|
data.modules
|
||||||
.filter((item) => item.b_id !== draft.module?.b_id && item.b_type !== "data")
|
.filter((item) => item.b_id !== draft.module?.b_id && item.b_type !== "data")
|
||||||
.map((item) => ({ value: item.b_id, label: item.b_name + " (" + item.b_id + ")" })),
|
.map((item) => ({ value: item.b_id, label: item.b_name + " (" + item.b_code + ")" })),
|
||||||
);
|
);
|
||||||
const directoryParentOptions = computed(() =>
|
const directoryParentOptions = computed(() =>
|
||||||
data.modules
|
data.modules
|
||||||
.filter((item) => item.b_type === "directory")
|
.filter((item) => item.b_type === "directory")
|
||||||
.map((item) => ({ value: item.b_id, label: item.b_name + " (" + item.b_id + ")" })),
|
.map((item) => ({ value: item.b_id, label: item.b_name + " (" + item.b_code + ")" })),
|
||||||
);
|
);
|
||||||
const moduleKindLabel = computed(() => {
|
const moduleKindLabel = computed(() => {
|
||||||
if (draft.module?.b_type === "directory") return "模块";
|
if (draft.module?.b_type === "directory") return "模块";
|
||||||
return draft.module?.b_route?.trim() ? "页面模块" : "数据模块";
|
return draft.module?.b_route?.trim() ? "页面模块" : "数据模块";
|
||||||
});
|
});
|
||||||
const translationKeys = computed(() => rowKeys());
|
|
||||||
const pendingDeleteModule = computed(() =>
|
const pendingDeleteModule = computed(() =>
|
||||||
data.modules.find((item) => item.b_id === pendingDeleteKey.value),
|
data.modules.find((item) => item.b_code === pendingDeleteKey.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
function clone(value) {
|
function clone(value) {
|
||||||
@@ -86,19 +86,23 @@ function clearDraft() {
|
|||||||
draft.powers = [];
|
draft.powers = [];
|
||||||
draft.translations = {};
|
draft.translations = {};
|
||||||
original.value = "";
|
original.value = "";
|
||||||
|
originalTranslationKeys.value = [];
|
||||||
activeTab.value = "basic";
|
activeTab.value = "basic";
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectModule(code) {
|
function selectModule(moduleCode) {
|
||||||
const module = data.modules.find((item) => item.b_id === code);
|
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||||||
if (!module) return;
|
if (!module) return;
|
||||||
|
|
||||||
selectedKey.value = code;
|
selectedKey.value = moduleCode;
|
||||||
isNew.value = false;
|
isNew.value = false;
|
||||||
draft.module = clone(module);
|
draft.module = clone(module);
|
||||||
draft.fields = clone(data.fields.filter((item) => item.b_module_id === code));
|
draft.fields = clone(data.fields.filter((item) => item.b_module_id === module.b_id));
|
||||||
draft.powers = clone(data.powers.filter((item) => item.b_module_id === code));
|
draft.powers = clone(data.powers.filter((item) => item.b_module_id === module.b_id));
|
||||||
const keys = new Set(rowKeys());
|
const keys = new Set(rowKeys());
|
||||||
|
data.translations.forEach((item) => {
|
||||||
|
if (String(item.b_key).split(".").includes(module.b_code)) keys.add(item.b_key);
|
||||||
|
});
|
||||||
|
|
||||||
draft.translations = {};
|
draft.translations = {};
|
||||||
data.translations
|
data.translations
|
||||||
@@ -108,6 +112,7 @@ function selectModule(code) {
|
|||||||
draft.translations[item.b_key][item.b_locale] = item.b_value;
|
draft.translations[item.b_key][item.b_locale] = item.b_value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
originalTranslationKeys.value = Array.from(keys);
|
||||||
original.value = serializeDraft();
|
original.value = serializeDraft();
|
||||||
if (module.b_type === "directory") activeTab.value = "basic";
|
if (module.b_type === "directory") activeTab.value = "basic";
|
||||||
}
|
}
|
||||||
@@ -131,7 +136,7 @@ async function loadAll(preferredKey = selectedKey.value) {
|
|||||||
userModules: userModules.data,
|
userModules: userModules.data,
|
||||||
userPowers: userPowers.data,
|
userPowers: userPowers.data,
|
||||||
});
|
});
|
||||||
const target = data.modules.some((item) => item.b_id === preferredKey) ? preferredKey : "";
|
const target = data.modules.some((item) => item.b_code === preferredKey) ? preferredKey : "";
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
selectModule(target);
|
selectModule(target);
|
||||||
@@ -146,75 +151,63 @@ async function loadAll(preferredKey = selectedKey.value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextModuleCode(prefix) {
|
function nextModuleCode(prefix) {
|
||||||
const existing = new Set(data.modules.map((item) => item.b_id));
|
const existing = new Set(data.modules.map((item) => item.b_code));
|
||||||
let index = 1;
|
let index = 1;
|
||||||
while (existing.has(prefix + index)) index += 1;
|
while (existing.has(prefix + index)) index += 1;
|
||||||
return prefix + index;
|
return prefix + index;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parentForNew(contextKey) {
|
function parentForNew(contextKey) {
|
||||||
const context = data.modules.find((item) => item.b_id === contextKey);
|
const context = data.modules.find((item) => item.b_code === contextKey);
|
||||||
if (!context) return null;
|
if (!context) return null;
|
||||||
return context.b_type === "directory" ? context.b_id : context.b_parent_id || null;
|
return context.b_type === "directory" ? context.b_id : context.b_parent_id || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addModule(kind, contextKey) {
|
async function addModule(kind, contextKey) {
|
||||||
const isDirectory = kind === "directory";
|
const isDirectory = kind === "directory";
|
||||||
const id = nextModuleCode(isDirectory ? "M_DIR_NEW_" : "M_PAGE_NEW_");
|
const id = String((await nextIdApi()).data);
|
||||||
|
const code = nextModuleCode(isDirectory ? "m_dir_new_" : "m_page_new_");
|
||||||
const parentId = parentForNew(contextKey);
|
const parentId = parentForNew(contextKey);
|
||||||
const route = isDirectory ? null : "/" + id.toLowerCase().replace(/^m_/, "").replaceAll("_", "-");
|
const route = isDirectory ? null : "/" + code.replace(/^m_/, "").replaceAll("_", "-");
|
||||||
|
|
||||||
draft.module = {
|
draft.module = {
|
||||||
b_id: id,
|
b_id: id,
|
||||||
|
b_code: code,
|
||||||
b_parent_id: parentId,
|
b_parent_id: parentId,
|
||||||
b_name: isDirectory ? "新模块" : "新页面",
|
b_name: isDirectory ? "新模块" : "新页面",
|
||||||
b_i18n: "module." + id,
|
b_i18n: "module." + code,
|
||||||
b_type: isDirectory ? "directory" : "page",
|
b_type: isDirectory ? "directory" : "page",
|
||||||
b_route: route,
|
b_route: route,
|
||||||
b_viewtable: null,
|
b_viewtable: null,
|
||||||
b_savetable: null,
|
b_savetable: null,
|
||||||
b_keyfield: isDirectory ? null : "b_id",
|
b_keyfield: isDirectory ? null : "b_id",
|
||||||
b_orderfield: isDirectory ? null : "b_id",
|
b_orderfield: isDirectory ? null : "b_id DESC",
|
||||||
b_ordertype: isDirectory ? null : "DESC",
|
|
||||||
b_canmenu: isDirectory || route ? 1 : 0,
|
b_canmenu: isDirectory || route ? 1 : 0,
|
||||||
b_canuse: 1,
|
b_canuse: 1,
|
||||||
b_xh: 100,
|
b_xh: 100,
|
||||||
};
|
};
|
||||||
draft.fields = [];
|
draft.fields = [];
|
||||||
draft.powers = [];
|
draft.powers = [];
|
||||||
|
originalTranslationKeys.value = [];
|
||||||
draft.translations = {
|
draft.translations = {
|
||||||
["module." + id]: { "zh-CN": draft.module.b_name },
|
["module." + code]: { "zh-CN": draft.module.b_name },
|
||||||
};
|
};
|
||||||
selectedKey.value = id;
|
selectedKey.value = code;
|
||||||
isNew.value = true;
|
isNew.value = true;
|
||||||
original.value = "";
|
original.value = "";
|
||||||
activeTab.value = "basic";
|
activeTab.value = "basic";
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAdd(kind, contextKey) {
|
async function handleAdd(kind, contextKey) {
|
||||||
addModule(kind === "module" ? "directory" : "page", contextKey);
|
try {
|
||||||
|
await addModule(kind === "module" ? "directory" : "page", contextKey);
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "模块创建失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateModuleId(value) {
|
function updateModuleCode(value) {
|
||||||
if (!isNew.value) return;
|
draft.module.b_code = String(value || "").toLowerCase();
|
||||||
|
|
||||||
const oldId = draft.module.b_id;
|
|
||||||
const oldKey = draft.module.b_i18n;
|
|
||||||
const newId = String(value || "").toUpperCase();
|
|
||||||
draft.module.b_id = newId;
|
|
||||||
if (oldKey === "module." + oldId) {
|
|
||||||
const newKey = "module." + newId;
|
|
||||||
draft.module.b_i18n = newKey;
|
|
||||||
draft.translations[newKey] = draft.translations[oldKey] || {};
|
|
||||||
delete draft.translations[oldKey];
|
|
||||||
}
|
|
||||||
selectedKey.value = newId;
|
|
||||||
draft.fields.forEach((item) => {
|
|
||||||
item.b_module_id = newId;
|
|
||||||
});
|
|
||||||
draft.powers.forEach((item) => {
|
|
||||||
item.b_module_id = newId;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeModuleType() {
|
function normalizeModuleType() {
|
||||||
@@ -225,9 +218,9 @@ function normalizeModuleType() {
|
|||||||
|
|
||||||
function validate() {
|
function validate() {
|
||||||
const module = draft.module;
|
const module = draft.module;
|
||||||
if (!/^[A-Z][A-Z0-9_]{1,49}$/.test(module.b_id)) return "模块编码格式不正确";
|
if (!/^[a-z][a-z0-9_]{1,49}$/.test(module.b_code)) return "模块编码格式不正确";
|
||||||
if (!module.b_name?.trim()) return "模块名称不能为空";
|
if (!module.b_name?.trim()) return "模块名称不能为空";
|
||||||
if (isNew.value && data.modules.some((item) => item.b_id === module.b_id)) {
|
if (data.modules.some((item) => item.b_code === module.b_code && item.b_id !== module.b_id)) {
|
||||||
return "模块编码已经存在";
|
return "模块编码已经存在";
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -238,15 +231,20 @@ function validate() {
|
|||||||
}
|
}
|
||||||
if (module.b_type === "data" && module.b_canmenu === 1) return "数据模块不能显示在菜单";
|
if (module.b_type === "data" && module.b_canmenu === 1) return "数据模块不能显示在菜单";
|
||||||
|
|
||||||
const identifiers = [
|
const identifiers = [module.b_viewtable, module.b_savetable, module.b_keyfield].filter(Boolean);
|
||||||
module.b_viewtable,
|
|
||||||
module.b_savetable,
|
|
||||||
module.b_keyfield,
|
|
||||||
module.b_orderfield,
|
|
||||||
].filter(Boolean);
|
|
||||||
if (identifiers.some((item) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))) {
|
if (identifiers.some((item) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))) {
|
||||||
return "表名和字段名只能包含字母、数字和下划线";
|
return "表名和字段名只能包含字母、数字和下划线";
|
||||||
}
|
}
|
||||||
|
const orderItemPattern =
|
||||||
|
/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*(?:\s+(?:ASC|DESC))?$/i;
|
||||||
|
if (
|
||||||
|
module.b_orderfield &&
|
||||||
|
String(module.b_orderfield)
|
||||||
|
.split(",")
|
||||||
|
.some((item) => !orderItemPattern.test(item.trim()))
|
||||||
|
) {
|
||||||
|
return "排序条件格式不正确";
|
||||||
|
}
|
||||||
|
|
||||||
const fieldCodes = draft.fields.map((item) => item.b_field);
|
const fieldCodes = draft.fields.map((item) => item.b_field);
|
||||||
if (new Set(fieldCodes).size !== fieldCodes.length) return "字段编码不能重复";
|
if (new Set(fieldCodes).size !== fieldCodes.length) return "字段编码不能重复";
|
||||||
@@ -281,14 +279,14 @@ function diffRows(current, previous) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function translationChanges(previousKeys) {
|
async function translationChanges(previousKeys) {
|
||||||
const currentKeys = new Set(rowKeys());
|
const currentKeys = new Set([...rowKeys(), ...Object.keys(draft.translations)]);
|
||||||
const relevantKeys = new Set([...previousKeys, ...currentKeys]);
|
const relevantKeys = new Set([...previousKeys, ...currentKeys]);
|
||||||
const oldRows = data.translations.filter((item) => relevantKeys.has(item.b_key));
|
const oldRows = data.translations.filter((item) => relevantKeys.has(item.b_key));
|
||||||
const desired = [];
|
const desired = [];
|
||||||
const missing = [];
|
const missing = [];
|
||||||
|
|
||||||
for (const key of currentKeys) {
|
for (const key of currentKeys) {
|
||||||
for (const locale of LOCALES) {
|
for (const locale of I18N_LOCALE_CODES) {
|
||||||
const value = draft.translations[key]?.[locale]?.trim();
|
const value = draft.translations[key]?.[locale]?.trim();
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
const old = oldRows.find((item) => item.b_key === key && item.b_locale === locale);
|
const old = oldRows.find((item) => item.b_key === key && item.b_locale === locale);
|
||||||
@@ -299,6 +297,9 @@ async function translationChanges(previousKeys) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
oldRows
|
||||||
|
.filter((item) => !I18N_LOCALE_CODES.includes(item.b_locale))
|
||||||
|
.forEach((item) => desired.push(item));
|
||||||
|
|
||||||
const ids = [];
|
const ids = [];
|
||||||
for (let index = 0; index < missing.length; index += 1) {
|
for (let index = 0; index < missing.length; index += 1) {
|
||||||
@@ -340,7 +341,7 @@ async function saveModule() {
|
|||||||
try {
|
try {
|
||||||
const oldModule = isNew.value
|
const oldModule = isNew.value
|
||||||
? null
|
? null
|
||||||
: data.modules.find((item) => item.b_id === selectedKey.value);
|
: data.modules.find((item) => item.b_code === selectedKey.value);
|
||||||
const oldFields = oldModule
|
const oldFields = oldModule
|
||||||
? data.fields.filter((item) => item.b_module_id === oldModule.b_id)
|
? data.fields.filter((item) => item.b_module_id === oldModule.b_id)
|
||||||
: [];
|
: [];
|
||||||
@@ -349,6 +350,7 @@ async function saveModule() {
|
|||||||
: [];
|
: [];
|
||||||
const previousKeys = new Set(
|
const previousKeys = new Set(
|
||||||
[
|
[
|
||||||
|
...originalTranslationKeys.value,
|
||||||
oldModule?.b_i18n,
|
oldModule?.b_i18n,
|
||||||
...oldFields.map((item) => item.b_i18n),
|
...oldFields.map((item) => item.b_i18n),
|
||||||
...oldPowers.map((item) => item.b_i18n),
|
...oldPowers.map((item) => item.b_i18n),
|
||||||
@@ -391,7 +393,7 @@ async function saveModule() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const savedKey = draft.module.b_id;
|
const savedKey = draft.module.b_code;
|
||||||
message.success("模块配置已保存");
|
message.success("模块配置已保存");
|
||||||
await loadAll(savedKey);
|
await loadAll(savedKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -401,38 +403,31 @@ async function saveModule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestDeleteModule(moduleId) {
|
function requestDeleteModule(moduleCode) {
|
||||||
const module = data.modules.find((item) => item.b_id === moduleId);
|
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||||||
if (!module || ["M_SYSTEM", "M_MODULE"].includes(moduleId)) return;
|
if (!module || PROTECTED_MODULE_CODES.includes(moduleCode)) return;
|
||||||
if (data.modules.some((item) => item.b_parent_id === moduleId)) {
|
if (data.modules.some((item) => item.b_parent_id === module.b_id)) {
|
||||||
message.warning("请先删除下级模块");
|
message.warning("请先删除下级模块");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingDeleteKey.value = moduleId;
|
pendingDeleteKey.value = moduleCode;
|
||||||
deleteConfirmOpen.value = true;
|
deleteConfirmOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteModule(moduleId = draft.module?.b_id) {
|
async function deleteModule(moduleCode = draft.module?.b_code) {
|
||||||
const module = data.modules.find((item) => item.b_id === moduleId);
|
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||||||
if (!module || ["M_SYSTEM", "M_MODULE"].includes(moduleId)) return;
|
if (!module || PROTECTED_MODULE_CODES.includes(moduleCode)) return;
|
||||||
if (data.modules.some((item) => item.b_parent_id === moduleId)) {
|
if (data.modules.some((item) => item.b_parent_id === module.b_id)) {
|
||||||
message.warning("请先删除下级模块");
|
message.warning("请先删除下级模块");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
const powers = data.powers.filter((item) => item.b_module_id === moduleId);
|
const powers = data.powers.filter((item) => item.b_module_id === module.b_id);
|
||||||
const powerIds = new Set(powers.map((item) => String(item.b_id)));
|
const powerIds = new Set(powers.map((item) => String(item.b_id)));
|
||||||
const fields = data.fields.filter((item) => item.b_module_id === moduleId);
|
const fields = data.fields.filter((item) => item.b_module_id === module.b_id);
|
||||||
const keys = new Set(
|
|
||||||
[
|
|
||||||
module.b_i18n,
|
|
||||||
...fields.map((item) => item.b_i18n),
|
|
||||||
...powers.map((item) => item.b_i18n),
|
|
||||||
].filter(Boolean),
|
|
||||||
);
|
|
||||||
|
|
||||||
const requests = [
|
const requests = [
|
||||||
{
|
{
|
||||||
@@ -450,7 +445,7 @@ async function deleteModule(moduleId = draft.module?.b_id) {
|
|||||||
inserts: [],
|
inserts: [],
|
||||||
updates: [],
|
updates: [],
|
||||||
deletes: data.userModules
|
deletes: data.userModules
|
||||||
.filter((item) => item.b_module_id === moduleId)
|
.filter((item) => item.b_module_id === module.b_id)
|
||||||
.map((item) => ({ b_id: item.b_id })),
|
.map((item) => ({ b_id: item.b_id })),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -467,21 +462,12 @@ async function deleteModule(moduleId = draft.module?.b_id) {
|
|||||||
updates: [],
|
updates: [],
|
||||||
deletes: fields.map((item) => ({ b_id: item.b_id })),
|
deletes: fields.map((item) => ({ b_id: item.b_id })),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
table: "b_i18n",
|
|
||||||
key_field: "b_id",
|
|
||||||
inserts: [],
|
|
||||||
updates: [],
|
|
||||||
deletes: data.translations
|
|
||||||
.filter((item) => keys.has(item.b_key))
|
|
||||||
.map((item) => ({ b_id: item.b_id })),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
table: "b_module",
|
table: "b_module",
|
||||||
key_field: "b_id",
|
key_field: "b_id",
|
||||||
inserts: [],
|
inserts: [],
|
||||||
updates: [],
|
updates: [],
|
||||||
deletes: [{ b_id: moduleId }],
|
deletes: [{ b_id: module.b_id }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await saveObjectApi(
|
await saveObjectApi(
|
||||||
@@ -493,7 +479,7 @@ async function deleteModule(moduleId = draft.module?.b_id) {
|
|||||||
message.success("模块已删除");
|
message.success("模块已删除");
|
||||||
deleteConfirmOpen.value = false;
|
deleteConfirmOpen.value = false;
|
||||||
pendingDeleteKey.value = "";
|
pendingDeleteKey.value = "";
|
||||||
await loadAll(selectedKey.value === moduleId ? "" : selectedKey.value);
|
await loadAll(selectedKey.value === moduleCode ? "" : selectedKey.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "模块删除失败");
|
message.error(error.message || "模块删除失败");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -523,6 +509,7 @@ onMounted(async () => {
|
|||||||
<div class="module-management">
|
<div class="module-management">
|
||||||
<ModuleTreePanel
|
<ModuleTreePanel
|
||||||
:modules="data.modules"
|
:modules="data.modules"
|
||||||
|
:protected-keys="PROTECTED_MODULE_CODES"
|
||||||
:selected-key="selectedKey"
|
:selected-key="selectedKey"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:can-create="canCreate"
|
:can-create="canCreate"
|
||||||
@@ -540,7 +527,7 @@ onMounted(async () => {
|
|||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<div class="title-line">
|
<div class="title-line">
|
||||||
<h1>{{ draft.module.b_name }}</h1>
|
<h1>{{ draft.module.b_name }}</h1>
|
||||||
<a-tag>{{ draft.module.b_id }}</a-tag>
|
<a-tag>{{ draft.module.b_code }}</a-tag>
|
||||||
<a-tag color="blue">{{ moduleKindLabel }}</a-tag>
|
<a-tag color="blue">{{ moduleKindLabel }}</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -572,14 +559,14 @@ onMounted(async () => {
|
|||||||
<a-popconfirm
|
<a-popconfirm
|
||||||
title="确定删除当前模块吗?"
|
title="确定删除当前模块吗?"
|
||||||
:disabled="
|
:disabled="
|
||||||
isNew || !canDelete || ['M_SYSTEM', 'M_MODULE'].includes(draft.module.b_id)
|
isNew || !canDelete || PROTECTED_MODULE_CODES.includes(draft.module.b_code)
|
||||||
"
|
"
|
||||||
@confirm="deleteModule()"
|
@confirm="deleteModule()"
|
||||||
>
|
>
|
||||||
<a-button
|
<a-button
|
||||||
danger
|
danger
|
||||||
:disabled="
|
:disabled="
|
||||||
isNew || !canDelete || ['M_SYSTEM', 'M_MODULE'].includes(draft.module.b_id)
|
isNew || !canDelete || PROTECTED_MODULE_CODES.includes(draft.module.b_code)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Trash2 :size="15" />删除
|
<Trash2 :size="15" />删除
|
||||||
@@ -596,51 +583,41 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ModuleBasicInfoPanel
|
<a-tabs v-model:active-key="activeTab" class="workspace-tabs">
|
||||||
v-if="draft.module.b_type === 'directory'"
|
|
||||||
v-model:module="draft.module"
|
|
||||||
:directory-parent-options="directoryParentOptions"
|
|
||||||
:is-new="isNew"
|
|
||||||
:parent-options="parentOptions"
|
|
||||||
@update-module-id="updateModuleId"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<a-tabs v-else v-model:active-key="activeTab" class="workspace-tabs">
|
|
||||||
<a-tab-pane key="basic">
|
<a-tab-pane key="basic">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<span class="tab-label"><FileText :size="15" />基本信息</span>
|
<span class="tab-label"><FileText :size="15" />基本信息</span>
|
||||||
</template>
|
</template>
|
||||||
<ModuleBasicInfoPanel
|
<ModuleBasicInfoPanel
|
||||||
v-model:module="draft.module"
|
v-model:module="draft.module"
|
||||||
|
v-model:translations="draft.translations"
|
||||||
:directory-parent-options="directoryParentOptions"
|
:directory-parent-options="directoryParentOptions"
|
||||||
:is-new="isNew"
|
|
||||||
:parent-options="parentOptions"
|
:parent-options="parentOptions"
|
||||||
@update-module-id="updateModuleId"
|
@update-module-code="updateModuleCode"
|
||||||
/>
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="fields">
|
<a-tab-pane v-if="draft.module.b_type !== 'directory'" key="fields">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<span class="tab-label">
|
<span class="tab-label">
|
||||||
<Database :size="15" />字段定义 <a-badge :count="draft.fields.length" />
|
<Database :size="15" />字段定义 <a-badge :count="draft.fields.length" />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<ModuleFieldDefinitionPanel v-model:fields="draft.fields" />
|
<ModuleFieldDefinitionPanel
|
||||||
|
v-model:fields="draft.fields"
|
||||||
|
:module-id="draft.module.b_id"
|
||||||
|
:module-code="draft.module.b_code"
|
||||||
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="powers">
|
<a-tab-pane v-if="draft.module.b_type !== 'directory'" key="powers">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<span class="tab-label">
|
<span class="tab-label">
|
||||||
<ShieldCheck :size="15" />操作权限 <a-badge :count="draft.powers.length" />
|
<ShieldCheck :size="15" />操作权限 <a-badge :count="draft.powers.length" />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<ModulePowerPanel v-model:powers="draft.powers" />
|
<ModulePowerPanel
|
||||||
</a-tab-pane>
|
v-model:powers="draft.powers"
|
||||||
<a-tab-pane key="i18n">
|
:module-id="draft.module.b_id"
|
||||||
<template #tab>
|
:module-code="draft.module.b_code"
|
||||||
<span class="tab-label"><Languages :size="15" />多语言</span>
|
|
||||||
</template>
|
|
||||||
<ModuleTranslationsPanel
|
|
||||||
v-model:translations="draft.translations"
|
|
||||||
:keys="translationKeys"
|
|
||||||
/>
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
@@ -664,7 +641,8 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
确定删除
|
确定删除
|
||||||
{{ pendingDeleteModule?.b_name || pendingDeleteKey }} 吗?关联字段、权限和多语言将一并删除。
|
{{ pendingDeleteModule?.b_name || pendingDeleteKey }}
|
||||||
|
吗?关联字段和权限将一并删除;共享多语言资源会保留在多语言中心。
|
||||||
</p>
|
</p>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
+11
-11
@@ -3,8 +3,8 @@
|
|||||||
## 设计约定
|
## 设计约定
|
||||||
|
|
||||||
- 表名和字段名沿用旧系统的 `b_` 风格。
|
- 表名和字段名沿用旧系统的 `b_` 风格。
|
||||||
- `b_module.b_id` 直接使用模块代码,例如 `M_BIZ`,创建后不可修改。
|
- 所有表的 `b_id` 使用 `bigint` 雪花 ID,返回前端时序列化为字符串。
|
||||||
- 其他表的 `b_id` 使用 `bigint` 雪花 ID,返回前端时序列化为字符串。
|
- `b_module.b_code` 是可修改的模块编码,使用小写字母、数字和下划线,例如 `m_biz`。
|
||||||
- 布尔字段使用 `int`:`1` 表示是,`0` 表示否,并限制只能写入 `0` 或 `1`。
|
- 布尔字段使用 `int`:`1` 表示是,`0` 表示否,并限制只能写入 `0` 或 `1`。
|
||||||
- `b_xh` 表示显示顺序,数值越小越靠前。
|
- `b_xh` 表示显示顺序,数值越小越靠前。
|
||||||
- 当前按机构使用独立数据库,表中不保存机构编号。
|
- 当前按机构使用独立数据库,表中不保存机构编号。
|
||||||
@@ -31,8 +31,9 @@ b_module / b_module_field / b_module_power
|
|||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `b_id` | `varchar(50)` | 主键,模块代码,例如 `M_BIZ` |
|
| `b_id` | `bigint` | 不可变雪花 ID 主键 |
|
||||||
| `b_parent_id` | `varchar(50)` | 上级模块代码,根节点为空 |
|
| `b_code` | `varchar(50)` | 可修改的唯一模块编码,例如 `m_biz` |
|
||||||
|
| `b_parent_id` | `bigint` | 上级模块 ID,根节点为空 |
|
||||||
| `b_name` | `nvarchar(100)` | 默认中文名称 |
|
| `b_name` | `nvarchar(100)` | 默认中文名称 |
|
||||||
| `b_i18n` | `varchar(150)` | 多语言键 |
|
| `b_i18n` | `varchar(150)` | 多语言键 |
|
||||||
| `b_type` | `varchar(20)` | `directory`、`page` 或 `data` |
|
| `b_type` | `varchar(20)` | `directory`、`page` 或 `data` |
|
||||||
@@ -40,8 +41,7 @@ b_module / b_module_field / b_module_power
|
|||||||
| `b_viewtable` | `varchar(128)` | 查询表或视图 |
|
| `b_viewtable` | `varchar(128)` | 查询表或视图 |
|
||||||
| `b_savetable` | `varchar(128)` | 保存目标表 |
|
| `b_savetable` | `varchar(128)` | 保存目标表 |
|
||||||
| `b_keyfield` | `varchar(50)` | 主键字段 |
|
| `b_keyfield` | `varchar(50)` | 主键字段 |
|
||||||
| `b_orderfield` | `varchar(50)` | 排序字段 |
|
| `b_orderfield` | `varchar(500)` | 排序条件,例如 `b_xh ASC, b_id DESC` |
|
||||||
| `b_ordertype` | `varchar(4)` | `ASC` 或 `DESC` |
|
|
||||||
| `b_canmenu` | `int` | 是否显示在菜单,默认 `0` |
|
| `b_canmenu` | `int` | 是否显示在菜单,默认 `0` |
|
||||||
| `b_canuse` | `int` | 是否启用,默认 `1` |
|
| `b_canuse` | `int` | 是否启用,默认 `1` |
|
||||||
| `b_xh` | `int` | 同级显示顺序 |
|
| `b_xh` | `int` | 同级显示顺序 |
|
||||||
@@ -49,7 +49,7 @@ b_module / b_module_field / b_module_power
|
|||||||
约束:
|
约束:
|
||||||
|
|
||||||
- `b_parent_id` 外键引用 `b_module.b_id`。
|
- `b_parent_id` 外键引用 `b_module.b_id`。
|
||||||
- `b_id` 格式为 `^[A-Z][A-Z0-9_]{1,49}$`。
|
- `b_code` 唯一,格式为 `^[a-z][a-z0-9_]{1,49}$`。
|
||||||
- `b_route` 非空时唯一。
|
- `b_route` 非空时唯一。
|
||||||
- 目录不能配置路由和数据表;数据模块不能显示在菜单中。
|
- 目录不能配置路由和数据表;数据模块不能显示在菜单中。
|
||||||
- 表名、视图名和字段名必须由后端校验,不能直接拼接 SQL。
|
- 表名、视图名和字段名必须由后端校验,不能直接拼接 SQL。
|
||||||
@@ -61,7 +61,7 @@ b_module / b_module_field / b_module_power
|
|||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||||
| `b_module_id` | `varchar(50)` | 所属模块代码 |
|
| `b_module_id` | `bigint` | 所属模块 ID |
|
||||||
| `b_field` | `varchar(50)` | 数据库字段名 |
|
| `b_field` | `varchar(50)` | 数据库字段名 |
|
||||||
| `b_name` | `nvarchar(100)` | 默认中文名称 |
|
| `b_name` | `nvarchar(100)` | 默认中文名称 |
|
||||||
| `b_i18n` | `varchar(150)` | 多语言键 |
|
| `b_i18n` | `varchar(150)` | 多语言键 |
|
||||||
@@ -90,7 +90,7 @@ b_module / b_module_field / b_module_power
|
|||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||||
| `b_module_id` | `varchar(50)` | 所属模块代码 |
|
| `b_module_id` | `bigint` | 所属模块 ID |
|
||||||
| `b_code` | `varchar(50)` | 操作编码,例如 `create`、`update`、`delete` |
|
| `b_code` | `varchar(50)` | 操作编码,例如 `create`、`update`、`delete` |
|
||||||
| `b_name` | `nvarchar(100)` | 操作名称 |
|
| `b_name` | `nvarchar(100)` | 操作名称 |
|
||||||
| `b_i18n` | `varchar(150)` | 多语言键 |
|
| `b_i18n` | `varchar(150)` | 多语言键 |
|
||||||
@@ -111,7 +111,7 @@ b_module / b_module_field / b_module_power
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||||
| `b_user_id` | `bigint` | 用户雪花 ID |
|
| `b_user_id` | `bigint` | 用户雪花 ID |
|
||||||
| `b_module_id` | `varchar(50)` | 模块代码 |
|
| `b_module_id` | `bigint` | 模块 ID |
|
||||||
| `b_inputuser_id` | `bigint` | 授权人雪花 ID |
|
| `b_inputuser_id` | `bigint` | 授权人雪花 ID |
|
||||||
| `b_inputdatetime` | `datetime` | 授权时间 |
|
| `b_inputdatetime` | `datetime` | 授权时间 |
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ b_module / b_module_field / b_module_power
|
|||||||
|
|
||||||
### `dbo.b_i18n`
|
### `dbo.b_i18n`
|
||||||
|
|
||||||
保存模块、字段和按钮的多语言文本。
|
统一保存菜单、模块、权限、字段、按钮、提示和校验信息等界面文案;业务对象只绑定稳定的 `b_key`。
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
fms-api/config/migrations/001_module_management.sql
|
fms-api/config/migrations/001_module_management.sql
|
||||||
|
fms-api/config/migrations/002_module_order_condition.sql
|
||||||
|
fms-api/config/migrations/003_module_surrogate_key.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
按以下顺序建立表:
|
按以下顺序建立表:
|
||||||
@@ -49,10 +51,10 @@ fms-api/config/migrations/001_module_management.sql
|
|||||||
|
|
||||||
| 模块 | 类型 | 路由 |
|
| 模块 | 类型 | 路由 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `M_SYSTEM` | `directory` | - |
|
| `m_system` | `directory` | - |
|
||||||
| `M_MODULE` | `page` | `/system/modules` |
|
| `m_module` | `page` | `/system/modules` |
|
||||||
|
|
||||||
为 `M_MODULE` 初始化按钮:
|
为 `m_module` 初始化按钮:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
create 新增模块
|
create 新增模块
|
||||||
@@ -104,8 +106,9 @@ grant 用户授权
|
|||||||
- `b_module_field.b_id`
|
- `b_module_field.b_id`
|
||||||
- `b_module_power.b_id`
|
- `b_module_power.b_id`
|
||||||
- `b_i18n.b_id`
|
- `b_i18n.b_id`
|
||||||
|
- `b_module.b_id`
|
||||||
|
|
||||||
`b_module.b_id` 直接使用模块代码,不调用取号接口。
|
`b_module.b_id` 使用不可变雪花 ID;可修改的小写模块编码保存在 `b_module.b_code`。
|
||||||
|
|
||||||
### 模块保存
|
### 模块保存
|
||||||
|
|
||||||
@@ -139,7 +142,7 @@ b_i18n
|
|||||||
|
|
||||||
前端保存前校验:
|
前端保存前校验:
|
||||||
|
|
||||||
- 模块代码格式正确且创建后不可修改。
|
- 模块代码使用小写字母、数字和下划线,且在模块间唯一。
|
||||||
- 父节点存在且不会形成循环。
|
- 父节点存在且不会形成循环。
|
||||||
- 目录、页面、数据模块字段组合合法。
|
- 目录、页面、数据模块字段组合合法。
|
||||||
- 同一模块字段名和按钮编码不重复。
|
- 同一模块字段名和按钮编码不重复。
|
||||||
@@ -149,7 +152,7 @@ b_i18n
|
|||||||
|
|
||||||
### 模块删除
|
### 模块删除
|
||||||
|
|
||||||
- `M_SYSTEM` 和 `M_MODULE` 在前端禁止删除。
|
- `m_system` 和 `m_module` 在前端禁止删除。
|
||||||
- 存在下级模块时禁止删除。
|
- 存在下级模块时禁止删除。
|
||||||
- 普通模块删除时,一次 `/data/saveobjt` 按依赖顺序删除:
|
- 普通模块删除时,一次 `/data/saveobjt` 按依赖顺序删除:
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user