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` — legacy schema, kept for reference only.
|
||||
- `模块管理实施计划.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
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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.
|
||||
- `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.
|
||||
|
||||
### 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
|
||||
- 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. **权限负责「有什么操作」**,只声明编码目录,不持有任何界面文案。
|
||||
2. **多语言负责「所有界面显示名」**,是唯一文案数据源;但**只覆盖菜单 / 模块 / 字段三类显示名,不覆盖权限项**(权限名是给管理员看的配置标签,不是给终端用户的按钮文案)。
|
||||
2. **多语言负责「所有界面文案」**,是菜单、模块、权限、字段、提示、按钮和校验信息的统一文案数据源。
|
||||
3. **字段定义驱动列表与表单**,自身不关心权限、不关心翻译落点。
|
||||
4. **菜单是功能树的可投影子集**,不单独存一份菜单数据。
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
| 项 | 说明 |
|
||||
| --- | --- |
|
||||
| 模块编码 `b_id` | 唯一主键,如 `M_BIZ`、`M_USER` |
|
||||
| 模块编码 `b_code` | 可修改的唯一小写编码,如 `m_biz`、`m_user` |
|
||||
| 模块名称 | 在「多语言」中以 `module.<编码>` 维护(见 §5) |
|
||||
| 查询对象 `view_name` | 只读裸对象名(视图),如 `v_bs_business` |
|
||||
| 保存对象 `save_name` | 可写裸表名,如 `b_business`;只读模块允许为空 |
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
### 2.2 操作目录(权限项来源,见 §6)
|
||||
|
||||
模块声明它拥有哪些操作:`actions: [{ code, name }, ...]`。例如 `M_BIZ` 声明 `1=新增 … 7=导出`。
|
||||
模块声明它拥有哪些操作:`actions: [{ code, name }, ...]`。例如 `m_biz` 声明 `1=新增 … 7=导出`。
|
||||
|
||||
### 2.3 与其它能力的关联
|
||||
|
||||
@@ -112,7 +112,7 @@ Module
|
||||
|
||||
### 4.3 关键场景:同模块、不同页面、不同权限
|
||||
|
||||
多个 `page` 节点可绑定**同一个模块**(如 `M_BIZ` 被 `海运业务单 / 业务单查询 / 业务单审核` 三个页面引用)。它们共享字段与操作目录,但:
|
||||
多个 `page` 节点可绑定**同一个模块**(如 `m_biz` 被 `海运业务单 / 业务单查询 / 业务单审核` 三个页面引用)。它们共享字段与操作目录,但:
|
||||
|
||||
- 可见性各自独立(`业务单审核` 设 `showInMenu=false`,不进菜单);
|
||||
- 权限各自独立(操作员在「海运业务单」可提交,在「业务单审核」仅可审核)——授权由角色赋权页另配(不在本页面)。
|
||||
@@ -124,22 +124,25 @@ Module
|
||||
|
||||
所有**界面显示名**唯一数据源,以稳定 `i18nKey` 为中心,多种语言并列。
|
||||
|
||||
### 5.1 覆盖范围(仅三类显示名)
|
||||
### 5.1 覆盖范围
|
||||
|
||||
| 命名空间 | i18nKey 规则 | 示例 |
|
||||
| --- | --- | --- |
|
||||
| 菜单/功能树 | `menu.<节点id>` | `menu.page_biz` |
|
||||
| 模块 | `module.<模块编码>` | `module.M_BIZ` |
|
||||
| 字段 | `field.<模块>.<字段code>` | `field.M_BIZ.b_ywno` |
|
||||
|
||||
> **权限项名称不在此列**(见 §6 说明)。
|
||||
| 模块 | `module.<模块编码>` | `module.m_biz` |
|
||||
| 字段 | `field.<模块>.<字段code>` | `field.m_biz.b_ywno` |
|
||||
| 权限 | `power.<模块>.<权限code>` | `power.m_biz.audit` |
|
||||
| 按钮 | `button.<模块>.<动作>` | `button.m_biz.approve` |
|
||||
| 占位提示 | `placeholder.<模块>.<字段>` | `placeholder.m_biz.customer` |
|
||||
| 校验提示 | `validation.<模块>.<规则>` | `validation.m_biz.customer_required` |
|
||||
| 通用文案 | `common.<名称>` | `common.save` |
|
||||
|
||||
### 5.2 数据结构
|
||||
|
||||
```js
|
||||
i18nData = {
|
||||
"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 管理界面要求
|
||||
|
||||
- 按命名空间类型筛选(菜单 / 模块 / 字段)。
|
||||
- 「多语言管理」是主配置中心,模块下的多语言面板是按当前模块过滤后的快捷编辑入口;两处写入同一张多语言表。
|
||||
- 支持按命名空间、模块、语言和缺失翻译状态筛选。
|
||||
- 以「Key + 各语言列」并列编辑;某语言留空即回退简体。
|
||||
- **缺失翻译检测**:非默认语言留空时标红,并汇总「N 条缺失」,提示补录。
|
||||
|
||||
@@ -165,20 +169,21 @@ i18nData = {
|
||||
```js
|
||||
// 模块声明
|
||||
actions: [
|
||||
{ code: 1, name: "新增" },
|
||||
{ code: 5, name: "审核" },
|
||||
{ code: 7, name: "导出" }
|
||||
{ code: 1, name: "新增", i18nKey: "power.m_biz.create" },
|
||||
{ code: 5, name: "审核", i18nKey: "power.m_biz.audit" },
|
||||
{ code: 7, name: "导出", i18nKey: "power.m_biz.export" }
|
||||
]
|
||||
```
|
||||
|
||||
- `code`:稳定操作编码,前端与后端统一引用(如 `5` 永远代表「审核」)。**授权与校验只认 `code`,不认名字。**
|
||||
- `name`:管理员可读的默认标签,仅一份(简体)。它是「这个编码是干嘛的」的配置注释,**不是终端用户的按钮文案**。
|
||||
- `name`:管理员可读的默认中文回退标签。
|
||||
- `i18nKey`:权限显示名绑定的多语言键;终端按钮如果需要不同文案,使用独立的 `button.*` Key。
|
||||
|
||||
### 6.2 为什么不进多语言
|
||||
### 6.2 权限与按钮文案的边界
|
||||
|
||||
- 授权逻辑只认 `code`,名字不参与校验,因此没有翻译必要。
|
||||
- 界面上的按钮文案常与权限名不同(权限叫「审核」,按钮可能写「通过」),把权限名塞进 i18n 反而把「权限定义」与「按钮文案」错误耦合。
|
||||
- 结论:**权限定义自洽于 `{code, name}`,与多语言彻底解耦**;终端用户看到的按钮文案由各自页面/组件自行在多语言里维护。
|
||||
- 授权与校验始终只认 `code`,不依赖任何语言文本。
|
||||
- 权限名称和按钮文案都进入统一多语言资源,但使用不同 Key,避免把「审核」与「通过」错误耦合。
|
||||
- 权限定义保持 `{code, name, i18nKey}` 自洽;按钮、提示和校验信息按各自命名空间独立维护。
|
||||
|
||||
### 6.3 与授权层的关系(边界)
|
||||
|
||||
@@ -203,10 +208,9 @@ actions: [
|
||||
├─ 分组 ─────────────► 组织 Form
|
||||
└─ 操作目录(code) ────► 权限内核(角色赋权页另行消费)
|
||||
▲
|
||||
│ 各自只声明,不持有文案
|
||||
│ 各自只声明稳定编码并绑定 i18nKey
|
||||
│
|
||||
多语言 (i18nKey → 多语言值) ──► 菜单名 / 模块名 / 字段标题 的唯一文案源
|
||||
(权限名不在内)
|
||||
多语言 (i18nKey → 多语言值) ──► 菜单 / 模块 / 权限 / 字段 / 按钮 / 提示 / 校验的唯一文案源
|
||||
|
||||
菜单 = 功能树中 showInMenu=1 且角色有查看权限的节点投影
|
||||
```
|
||||
@@ -218,7 +222,7 @@ actions: [
|
||||
| 设计项 | 原型位置 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| 功能树四类节点 | `functionTree` | 已是响应式可编辑 |
|
||||
| 模块基本信息/字段/分组 | `modules` | `M_MODULE/M_USER/M_BIZ/M_FEE` |
|
||||
| 模块基本信息/字段/分组 | `modules` | `m_module/m_user/m_biz/m_fee` |
|
||||
| 字段驱动列表/表单 | `字段配置` Tab | 列表/表单/必填开关 |
|
||||
| 菜单投影 | `菜单预览` Tab | 按角色 + 语言渲染 |
|
||||
| 多语言 | `多语言` Tab | Key + 三语,含缺失标红 |
|
||||
@@ -228,8 +232,8 @@ actions: [
|
||||
|
||||
原型已按本文设计落地:
|
||||
|
||||
- **多语言仅覆盖菜单 / 模块 / 字段三类显示名**;`action.*` 条目已从 `i18nData` 彻底移除,权限项名不再进多语言(符合 §6)。
|
||||
- **权限项仅含 `{code, name}`**,`name` 为管理员可读标签,不在多语言维护;新增权限项不再生成 i18nKey。
|
||||
- **多语言覆盖所有界面文案**;权限名称使用 `power.*`,终端按钮使用独立的 `button.*`,两者不耦合。
|
||||
- **权限项包含 `{code, name, i18nKey}`**;授权仍只认 `code`,`name` 作为默认中文回退。
|
||||
- **字段多语言可在字段配置处点击设置**:标题单元格带多语言按钮,点击打开该字段的简体/English/繁體编辑框(i18nKey 形如 `field.<模块>.<字段>`)。
|
||||
- **字段 `i18nKey` 统一为 `field.<模块>.<字段>`**,与模块/菜单 `<id>` 前缀、整体「命名空间.容器.标识」规则一致。
|
||||
- 授权 `roleNodePerm` 仍为写死 mock,仅用于菜单预览示意,符合 §6.3 边界(角色赋权在另一页面,不在模块管理内)。
|
||||
@@ -244,7 +248,7 @@ actions: [
|
||||
| `s_module_field` | 字段配置(编码、标题键、类型、列表/表单/必填、分组) |
|
||||
| `s_module_group` | 表单分组 |
|
||||
| `s_function_node` | 功能树(目录/页面/分组/模块定位) |
|
||||
| `s_i18n` | 多语言键值(key → 各语言值),覆盖菜单/模块/字段 |
|
||||
| `s_i18n` | 统一多语言键值(key → 各语言值),覆盖所有界面文案 |
|
||||
| 权限相关表 | 独立系统表,不属于 Table/Form 渲染配置 |
|
||||
|
||||
> 角色与权限内核使用独立系统表,不写入模块渲染配置;后端按 `module_id` + 操作 `code` 校验权限,不能只依赖前端隐藏。
|
||||
@@ -7,8 +7,9 @@ BEGIN TRY
|
||||
IF OBJECT_ID(N'dbo.b_module', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.b_module (
|
||||
b_id varchar(50) NOT NULL,
|
||||
b_parent_id varchar(50) NULL,
|
||||
b_id bigint NOT NULL,
|
||||
b_code varchar(50) NOT NULL,
|
||||
b_parent_id bigint NULL,
|
||||
b_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_type varchar(20) NOT NULL,
|
||||
@@ -16,20 +17,19 @@ BEGIN TRY
|
||||
b_viewtable varchar(128) NULL,
|
||||
b_savetable varchar(128) NULL,
|
||||
b_keyfield varchar(50) NULL,
|
||||
b_orderfield varchar(50) NULL,
|
||||
b_ordertype varchar(4) NULL,
|
||||
b_orderfield varchar(500) NULL,
|
||||
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_xh int NOT NULL CONSTRAINT DF_b_module_xh DEFAULT (0),
|
||||
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 CK_b_module_id CHECK (
|
||||
LEN(b_id) BETWEEN 2 AND 50
|
||||
AND b_id LIKE '[A-Z]%'
|
||||
AND b_id NOT LIKE '%[^A-Z0-9_]%'
|
||||
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_]%'
|
||||
),
|
||||
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_canuse CHECK (b_canuse IN (0, 1)),
|
||||
CONSTRAINT CK_b_module_directory CHECK (
|
||||
@@ -49,7 +49,7 @@ BEGIN TRY
|
||||
BEGIN
|
||||
CREATE TABLE dbo.b_module_field (
|
||||
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_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
@@ -85,7 +85,7 @@ BEGIN TRY
|
||||
BEGIN
|
||||
CREATE TABLE dbo.b_module_power (
|
||||
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_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
@@ -106,7 +106,7 @@ BEGIN TRY
|
||||
CREATE TABLE dbo.b_user_module (
|
||||
b_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_inputdatetime datetime NOT NULL CONSTRAINT DF_b_user_module_inputdatetime DEFAULT (GETDATE()),
|
||||
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')
|
||||
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')
|
||||
INSERT INTO dbo.b_module (b_id, b_parent_id, b_name, b_i18n, b_type, b_canmenu, b_canuse, b_xh)
|
||||
VALUES ('M_SYSTEM', NULL, N'系统管理', 'module.M_SYSTEM', 'directory', 1, 1, 100);
|
||||
DECLARE @SystemModuleId bigint = 205000000000000011;
|
||||
DECLARE @ModuleManagementId bigint = 205000000000000012;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_module WHERE b_id = 'M_MODULE')
|
||||
INSERT INTO dbo.b_module (b_id, b_parent_id, b_name, b_i18n, b_type, b_route, b_canmenu, b_canuse, b_xh)
|
||||
VALUES ('M_MODULE', 'M_SYSTEM', N'模块管理', 'module.M_MODULE', 'page', '/system/modules', 1, 1, 10);
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_module WHERE b_code = 'm_system')
|
||||
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 (@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);
|
||||
INSERT INTO @Powers VALUES
|
||||
@@ -176,11 +179,11 @@ BEGIN TRY
|
||||
(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)
|
||||
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
|
||||
WHERE NOT EXISTS (
|
||||
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;
|
||||
@@ -188,14 +191,14 @@ BEGIN TRY
|
||||
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 = '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)
|
||||
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)
|
||||
SELECT 205000000000000200 + ROW_NUMBER() OVER (ORDER BY p.b_id), @AdminId, p.b_id, @AdminId, GETDATE()
|
||||
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 NOT EXISTS (
|
||||
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>
|
||||
import { computed, nextTick, ref, useSlots } from "vue";
|
||||
import { computed, defineComponent, h, nextTick, ref, useSlots } from "vue";
|
||||
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
||||
import { Checkbox, Input, InputNumber, Select } from "antdv-next";
|
||||
import { StkTable } from "stk-table-vue";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
virtual: { type: Boolean, default: true },
|
||||
virtualX: { type: Boolean, default: true },
|
||||
colResizable: { type: Boolean, default: true },
|
||||
bordered: { type: [Boolean, String], 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 tableRef = ref(null);
|
||||
|
||||
// 表格样式
|
||||
const customVars = ref({
|
||||
"--header-row-height": "40px",
|
||||
"--row-height": "34px",
|
||||
"--footer-row-height": "36px",
|
||||
});
|
||||
|
||||
const { resume: refreshLayout } = useRafFn(
|
||||
async () => {
|
||||
await nextTick();
|
||||
@@ -49,10 +228,14 @@ const forwardSlots = computed(() => {
|
||||
ref="tableRef"
|
||||
class="fms-table"
|
||||
v-bind="$attrs"
|
||||
:style="customVars"
|
||||
:auto-resize="false"
|
||||
:virtual="virtual"
|
||||
:virtual-x="virtualX"
|
||||
:col-resizable="colResizable"
|
||||
v-model:columns="tableColumnsModel"
|
||||
:data-source="dataSource"
|
||||
:row-key="rowKey"
|
||||
:bordered="bordered"
|
||||
:no-data-full="noDataFull"
|
||||
>
|
||||
@@ -68,6 +251,8 @@ const forwardSlots = computed(() => {
|
||||
</StkTable>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.fms-table {
|
||||
width: 100%;
|
||||
|
||||
@@ -66,7 +66,7 @@ export function useLookupOptions(props) {
|
||||
}
|
||||
|
||||
const { start: startSearchTimer, stop: stopSearchTimer } = useTimeoutFn(
|
||||
() => load(query.value),
|
||||
(searchText) => load(searchText),
|
||||
() => props.searchDelay,
|
||||
{ immediate: false },
|
||||
);
|
||||
@@ -76,8 +76,11 @@ export function useLookupOptions(props) {
|
||||
|
||||
function search(value) {
|
||||
stopResetTimer();
|
||||
query.value = String(value ?? "");
|
||||
startSearchTimer();
|
||||
const searchText = String(value ?? "");
|
||||
query.value = searchText;
|
||||
requestId += 1;
|
||||
loading.value = false;
|
||||
startSearchTimer(searchText);
|
||||
}
|
||||
|
||||
function open() {
|
||||
|
||||
@@ -142,6 +142,7 @@ const fallbackMenuItems = [
|
||||
{ key: "/system/mappingConfig", label: "映射配置" },
|
||||
{ key: "/system/autoId", label: "自动编码" },
|
||||
{ key: "/system/modules", label: "模块管理" },
|
||||
{ key: "/system/i18n", label: "多语言管理" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from "vue-router";
|
||||
import NProgress from "nprogress";
|
||||
import { message } from "antdv-next";
|
||||
import "nprogress/nprogress.css";
|
||||
import routes from "./routes";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
|
||||
@@ -13,47 +14,6 @@ function getRedirectPath(value) {
|
||||
: "/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({
|
||||
history: createWebHistory(),
|
||||
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 powerSet = computed(() => new Set(powerCodes.value));
|
||||
const isG3soft = computed(() => {
|
||||
const authStore = useAuthStore();
|
||||
return String(authStore.user?.account || "").trim().toLowerCase() === "g3soft";
|
||||
});
|
||||
|
||||
async function load() {
|
||||
const authStore = useAuthStore();
|
||||
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 (loadedUserId.value === userId) return;
|
||||
if (loadingPromise.value) return loadingPromise.value;
|
||||
|
||||
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_power", `b_user_id = ${userId}`, "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]));
|
||||
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
|
||||
.map((item) => powerById.get(String(item.b_power_id)))
|
||||
.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;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -40,11 +60,14 @@ export const usePermissionStore = defineStore("permissions", () => {
|
||||
}
|
||||
|
||||
function canModule(moduleCode) {
|
||||
return !moduleCode || moduleSet.value.has(moduleCode);
|
||||
return isG3soft.value || !moduleCode || moduleSet.value.has(moduleCode);
|
||||
}
|
||||
|
||||
function canPower(moduleCode, powerCode) {
|
||||
return canModule(moduleCode) && powerSet.value.has(`${moduleCode}.${powerCode}`);
|
||||
return (
|
||||
isG3soft.value ||
|
||||
(canModule(moduleCode) && powerSet.value.has(`${moduleCode}.${powerCode}`))
|
||||
);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
@@ -53,5 +76,5 @@ export const usePermissionStore = defineStore("permissions", () => {
|
||||
powerCodes.value = [];
|
||||
}
|
||||
|
||||
return { load, canModule, canPower, clear, moduleCodes, powerCodes };
|
||||
return { load, canModule, canPower, clear, moduleCodes, powerCodes, isG3soft };
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
/* ============================================================
|
||||
* 基础重置 / Base Reset
|
||||
* ============================================================ */
|
||||
:root {
|
||||
color: #1f2329;
|
||||
background: #f5f7fa;
|
||||
@@ -39,6 +42,9 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 图标 / Icons
|
||||
* ============================================================ */
|
||||
.lucide {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -47,6 +53,9 @@ a {
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 主题变量 / Theme Tokens
|
||||
* ============================================================ */
|
||||
.theme-root {
|
||||
--fms-bg: #f5f7fa;
|
||||
--fms-surface: #ffffff;
|
||||
@@ -59,15 +68,4 @@ a {
|
||||
color: var(--fms-text);
|
||||
background: var(--fms-bg);
|
||||
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>
|
||||
import ModuleTranslationEditor from "./ModuleTranslationEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
directoryParentOptions: { type: Array, default: () => [] },
|
||||
isNew: Boolean,
|
||||
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 translations = defineModel("translations", { type: Object, default: () => ({}) });
|
||||
|
||||
function updateRoute(value) {
|
||||
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">
|
||||
<div class="directory-grid">
|
||||
<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 label="模块名称" required>
|
||||
<a-input v-model:value="module.b_name" />
|
||||
@@ -33,7 +35,14 @@ function updateRoute(value) {
|
||||
/>
|
||||
</a-form-item>
|
||||
<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 label="显示顺序">
|
||||
<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">
|
||||
<div class="form-grid">
|
||||
<a-form-item label="模块编码" required>
|
||||
<a-input
|
||||
:value="module.b_id"
|
||||
:disabled="!isNew"
|
||||
@update:value="emit('update-module-id', $event)"
|
||||
/>
|
||||
<a-input :value="module.b_code" @update:value="emit('update-module-code', $event)" />
|
||||
</a-form-item>
|
||||
<a-form-item label="模块名称" required>
|
||||
<a-input v-model:value="module.b_name" />
|
||||
@@ -70,7 +75,14 @@ function updateRoute(value) {
|
||||
/>
|
||||
</a-form-item>
|
||||
<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 label="查询表或视图">
|
||||
<a-input v-model:value="module.b_viewtable" />
|
||||
@@ -81,12 +93,9 @@ function updateRoute(value) {
|
||||
<a-form-item label="主键字段">
|
||||
<a-input v-model:value="module.b_keyfield" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序字段">
|
||||
<a-form-item label="排序条件">
|
||||
<a-input v-model:value="module.b_orderfield" />
|
||||
</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-input-number v-model:value="module.b_xh" :min="0" />
|
||||
</a-form-item>
|
||||
@@ -100,7 +109,7 @@ function updateRoute(value) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-top: 10px;
|
||||
padding-top: 10px 0;
|
||||
}
|
||||
|
||||
.directory-form {
|
||||
@@ -124,6 +133,17 @@ function updateRoute(value) {
|
||||
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) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
+156
-13
@@ -1,29 +1,165 @@
|
||||
<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 { nextIdApi } from "@/services/api";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: [String, Number], default: "" },
|
||||
moduleCode: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const fields = defineModel("fields", { type: Array, default: () => [] });
|
||||
const { message } = App.useApp();
|
||||
const adding = ref(false);
|
||||
const selectedRowKeys = ref([]);
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_field", title: "字段", width: 150 },
|
||||
{ dataIndex: "b_name", title: "名称", width: 160 },
|
||||
{ dataIndex: "b_type", title: "类型", width: 120 },
|
||||
{ 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" },
|
||||
{ dataIndex: "b_readonly", title: "只读", width: 64, align: "center", headerAlign: "center" },
|
||||
{ dataIndex: "b_width", title: "宽度", width: 90 },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 80 },
|
||||
];
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => fields.value.map((item) => String(item.b_id)),
|
||||
(keys) => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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
|
||||
class="sub-table"
|
||||
row-key="b_id"
|
||||
:columns="columns"
|
||||
v-model:columns="columns"
|
||||
:data-source="fields"
|
||||
:row-selection="rowSelection"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
@@ -37,6 +173,13 @@ const columns = [
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
||||
@@ -1,24 +1,107 @@
|
||||
<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 { nextIdApi } from "@/services/api";
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: [String, Number], default: "" },
|
||||
moduleCode: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const powers = defineModel("powers", { type: Array, default: () => [] });
|
||||
const { message } = App.useApp();
|
||||
const adding = ref(false);
|
||||
const selectedRowKeys = ref([]);
|
||||
|
||||
const columns = [
|
||||
{ dataIndex: "b_code", title: "权限代码", width: 180 },
|
||||
{ dataIndex: "b_name", title: "名称", width: 200 },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 200 },
|
||||
{ dataIndex: "b_canuse", title: "启用", width: 74, align: "center", headerAlign: "center" },
|
||||
{ dataIndex: "b_xh", title: "顺序", width: 90 },
|
||||
];
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
}));
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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
|
||||
class="sub-table"
|
||||
row-key="b_id"
|
||||
:columns="columns"
|
||||
v-model:columns="columns"
|
||||
:data-source="powers"
|
||||
:row-selection="rowSelection"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
@@ -32,6 +115,13 @@ const columns = [
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
flex: 1 1 auto;
|
||||
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({
|
||||
modules: { type: Array, required: true },
|
||||
protectedKeys: { type: Array, default: () => [] },
|
||||
selectedKey: { type: String, default: "" },
|
||||
loading: Boolean,
|
||||
canCreate: Boolean,
|
||||
@@ -16,15 +17,16 @@ const contextNode = ref(null);
|
||||
|
||||
const treeData = computed(() => {
|
||||
const keyword = searchText.value.trim().toLowerCase();
|
||||
const codeById = new Map(props.modules.map((item) => [item.b_id, item.b_code]));
|
||||
const visible = keyword
|
||||
? new Set(
|
||||
props.modules
|
||||
.filter(
|
||||
(item) =>
|
||||
String(item.b_id).toLowerCase().includes(keyword) ||
|
||||
String(item.b_code).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;
|
||||
const children = new Map();
|
||||
@@ -35,11 +37,11 @@ const treeData = computed(() => {
|
||||
});
|
||||
const build = (parent) =>
|
||||
(children.get(parent) || [])
|
||||
.filter((item) => !visible || visible.has(item.b_id))
|
||||
.filter((item) => !visible || visible.has(item.b_code))
|
||||
.map((item) => {
|
||||
const nodeChildren = build(item.b_id);
|
||||
return {
|
||||
key: item.b_id,
|
||||
key: item.b_code,
|
||||
title: item.b_name,
|
||||
children: nodeChildren,
|
||||
isLeaf: nodeChildren.length === 0,
|
||||
@@ -69,7 +71,7 @@ const contextMenu = computed(() => {
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
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 }) => {
|
||||
|
||||
+96
-118
@@ -1,18 +1,18 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
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 { useAuthStore } from "@/stores/auth";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import { I18N_LOCALE_CODES } from "@/utils/i18n";
|
||||
import ModuleBasicInfoPanel from "./components/ModuleBasicInfoPanel.vue";
|
||||
import ModuleFieldDefinitionPanel from "./components/ModuleFieldDefinitionPanel.vue";
|
||||
import ModulePowerPanel from "./components/ModulePowerPanel.vue";
|
||||
import ModuleTranslationsPanel from "./components/ModuleTranslationsPanel.vue";
|
||||
import ModuleTreePanel from "./components/ModuleTreePanel.vue";
|
||||
|
||||
const PROTECTED_MODULE_CODES = ["m_module", "m_i18n", "m_biz"];
|
||||
const { message } = App.useApp();
|
||||
const LOCALES = ["zh-CN", "en-US", "zh-TW"];
|
||||
const authStore = useAuthStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const loading = ref(false);
|
||||
@@ -23,6 +23,7 @@ const isNew = ref(false);
|
||||
const deleteConfirmOpen = ref(false);
|
||||
const pendingDeleteKey = ref("");
|
||||
const original = ref("");
|
||||
const originalTranslationKeys = ref([]);
|
||||
const data = reactive({
|
||||
modules: [],
|
||||
fields: [],
|
||||
@@ -33,28 +34,27 @@ const data = reactive({
|
||||
});
|
||||
const draft = reactive({ module: null, fields: [], powers: [], translations: {} });
|
||||
|
||||
const canCreate = computed(() => permissionStore.canPower("M_MODULE", "create"));
|
||||
const canUpdate = computed(() => permissionStore.canPower("M_MODULE", "update"));
|
||||
const canDelete = computed(() => permissionStore.canPower("M_MODULE", "delete"));
|
||||
const canCreate = computed(() => permissionStore.canPower("m_module", "create"));
|
||||
const canUpdate = computed(() => permissionStore.canPower("m_module", "update"));
|
||||
const canDelete = computed(() => permissionStore.canPower("m_module", "delete"));
|
||||
const canSave = computed(() => (isNew.value ? canCreate.value : canUpdate.value));
|
||||
const dirty = computed(() => Boolean(draft.module) && serializeDraft() !== original.value);
|
||||
const parentOptions = computed(() =>
|
||||
data.modules
|
||||
.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(() =>
|
||||
data.modules
|
||||
.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(() => {
|
||||
if (draft.module?.b_type === "directory") return "模块";
|
||||
return draft.module?.b_route?.trim() ? "页面模块" : "数据模块";
|
||||
});
|
||||
const translationKeys = computed(() => rowKeys());
|
||||
const pendingDeleteModule = computed(() =>
|
||||
data.modules.find((item) => item.b_id === pendingDeleteKey.value),
|
||||
data.modules.find((item) => item.b_code === pendingDeleteKey.value),
|
||||
);
|
||||
|
||||
function clone(value) {
|
||||
@@ -86,19 +86,23 @@ function clearDraft() {
|
||||
draft.powers = [];
|
||||
draft.translations = {};
|
||||
original.value = "";
|
||||
originalTranslationKeys.value = [];
|
||||
activeTab.value = "basic";
|
||||
}
|
||||
|
||||
function selectModule(code) {
|
||||
const module = data.modules.find((item) => item.b_id === code);
|
||||
function selectModule(moduleCode) {
|
||||
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||||
if (!module) return;
|
||||
|
||||
selectedKey.value = code;
|
||||
selectedKey.value = moduleCode;
|
||||
isNew.value = false;
|
||||
draft.module = clone(module);
|
||||
draft.fields = clone(data.fields.filter((item) => item.b_module_id === code));
|
||||
draft.powers = clone(data.powers.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 === module.b_id));
|
||||
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 = {};
|
||||
data.translations
|
||||
@@ -108,6 +112,7 @@ function selectModule(code) {
|
||||
draft.translations[item.b_key][item.b_locale] = item.b_value;
|
||||
});
|
||||
|
||||
originalTranslationKeys.value = Array.from(keys);
|
||||
original.value = serializeDraft();
|
||||
if (module.b_type === "directory") activeTab.value = "basic";
|
||||
}
|
||||
@@ -131,7 +136,7 @@ async function loadAll(preferredKey = selectedKey.value) {
|
||||
userModules: userModules.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) {
|
||||
selectModule(target);
|
||||
@@ -146,75 +151,63 @@ async function loadAll(preferredKey = selectedKey.value) {
|
||||
}
|
||||
|
||||
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;
|
||||
while (existing.has(prefix + index)) index += 1;
|
||||
return prefix + index;
|
||||
}
|
||||
|
||||
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;
|
||||
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 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 route = isDirectory ? null : "/" + id.toLowerCase().replace(/^m_/, "").replaceAll("_", "-");
|
||||
const route = isDirectory ? null : "/" + code.replace(/^m_/, "").replaceAll("_", "-");
|
||||
|
||||
draft.module = {
|
||||
b_id: id,
|
||||
b_code: code,
|
||||
b_parent_id: parentId,
|
||||
b_name: isDirectory ? "新模块" : "新页面",
|
||||
b_i18n: "module." + id,
|
||||
b_i18n: "module." + code,
|
||||
b_type: isDirectory ? "directory" : "page",
|
||||
b_route: route,
|
||||
b_viewtable: null,
|
||||
b_savetable: null,
|
||||
b_keyfield: isDirectory ? null : "b_id",
|
||||
b_orderfield: isDirectory ? null : "b_id",
|
||||
b_ordertype: isDirectory ? null : "DESC",
|
||||
b_orderfield: isDirectory ? null : "b_id DESC",
|
||||
b_canmenu: isDirectory || route ? 1 : 0,
|
||||
b_canuse: 1,
|
||||
b_xh: 100,
|
||||
};
|
||||
draft.fields = [];
|
||||
draft.powers = [];
|
||||
originalTranslationKeys.value = [];
|
||||
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;
|
||||
original.value = "";
|
||||
activeTab.value = "basic";
|
||||
}
|
||||
|
||||
function handleAdd(kind, contextKey) {
|
||||
addModule(kind === "module" ? "directory" : "page", contextKey);
|
||||
async function handleAdd(kind, contextKey) {
|
||||
try {
|
||||
await addModule(kind === "module" ? "directory" : "page", contextKey);
|
||||
} catch (error) {
|
||||
message.error(error.message || "模块创建失败");
|
||||
}
|
||||
}
|
||||
|
||||
function updateModuleId(value) {
|
||||
if (!isNew.value) return;
|
||||
|
||||
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 updateModuleCode(value) {
|
||||
draft.module.b_code = String(value || "").toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeModuleType() {
|
||||
@@ -225,9 +218,9 @@ function normalizeModuleType() {
|
||||
|
||||
function validate() {
|
||||
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 (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 "模块编码已经存在";
|
||||
}
|
||||
if (
|
||||
@@ -238,15 +231,20 @@ function validate() {
|
||||
}
|
||||
if (module.b_type === "data" && module.b_canmenu === 1) return "数据模块不能显示在菜单";
|
||||
|
||||
const identifiers = [
|
||||
module.b_viewtable,
|
||||
module.b_savetable,
|
||||
module.b_keyfield,
|
||||
module.b_orderfield,
|
||||
].filter(Boolean);
|
||||
const identifiers = [module.b_viewtable, module.b_savetable, module.b_keyfield].filter(Boolean);
|
||||
if (identifiers.some((item) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))) {
|
||||
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);
|
||||
if (new Set(fieldCodes).size !== fieldCodes.length) return "字段编码不能重复";
|
||||
@@ -281,14 +279,14 @@ function diffRows(current, previous) {
|
||||
}
|
||||
|
||||
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 oldRows = data.translations.filter((item) => relevantKeys.has(item.b_key));
|
||||
const desired = [];
|
||||
const missing = [];
|
||||
|
||||
for (const key of currentKeys) {
|
||||
for (const locale of LOCALES) {
|
||||
for (const locale of I18N_LOCALE_CODES) {
|
||||
const value = draft.translations[key]?.[locale]?.trim();
|
||||
if (!value) continue;
|
||||
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 = [];
|
||||
for (let index = 0; index < missing.length; index += 1) {
|
||||
@@ -340,7 +341,7 @@ async function saveModule() {
|
||||
try {
|
||||
const oldModule = isNew.value
|
||||
? null
|
||||
: data.modules.find((item) => item.b_id === selectedKey.value);
|
||||
: data.modules.find((item) => item.b_code === selectedKey.value);
|
||||
const oldFields = oldModule
|
||||
? data.fields.filter((item) => item.b_module_id === oldModule.b_id)
|
||||
: [];
|
||||
@@ -349,6 +350,7 @@ async function saveModule() {
|
||||
: [];
|
||||
const previousKeys = new Set(
|
||||
[
|
||||
...originalTranslationKeys.value,
|
||||
oldModule?.b_i18n,
|
||||
...oldFields.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("模块配置已保存");
|
||||
await loadAll(savedKey);
|
||||
} catch (error) {
|
||||
@@ -401,38 +403,31 @@ async function saveModule() {
|
||||
}
|
||||
}
|
||||
|
||||
function requestDeleteModule(moduleId) {
|
||||
const module = data.modules.find((item) => item.b_id === moduleId);
|
||||
if (!module || ["M_SYSTEM", "M_MODULE"].includes(moduleId)) return;
|
||||
if (data.modules.some((item) => item.b_parent_id === moduleId)) {
|
||||
function requestDeleteModule(moduleCode) {
|
||||
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||||
if (!module || PROTECTED_MODULE_CODES.includes(moduleCode)) return;
|
||||
if (data.modules.some((item) => item.b_parent_id === module.b_id)) {
|
||||
message.warning("请先删除下级模块");
|
||||
return;
|
||||
}
|
||||
|
||||
pendingDeleteKey.value = moduleId;
|
||||
pendingDeleteKey.value = moduleCode;
|
||||
deleteConfirmOpen.value = true;
|
||||
}
|
||||
|
||||
async function deleteModule(moduleId = draft.module?.b_id) {
|
||||
const module = data.modules.find((item) => item.b_id === moduleId);
|
||||
if (!module || ["M_SYSTEM", "M_MODULE"].includes(moduleId)) return;
|
||||
if (data.modules.some((item) => item.b_parent_id === moduleId)) {
|
||||
async function deleteModule(moduleCode = draft.module?.b_code) {
|
||||
const module = data.modules.find((item) => item.b_code === moduleCode);
|
||||
if (!module || PROTECTED_MODULE_CODES.includes(moduleCode)) return;
|
||||
if (data.modules.some((item) => item.b_parent_id === module.b_id)) {
|
||||
message.warning("请先删除下级模块");
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
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 fields = data.fields.filter((item) => item.b_module_id === moduleId);
|
||||
const keys = new Set(
|
||||
[
|
||||
module.b_i18n,
|
||||
...fields.map((item) => item.b_i18n),
|
||||
...powers.map((item) => item.b_i18n),
|
||||
].filter(Boolean),
|
||||
);
|
||||
const fields = data.fields.filter((item) => item.b_module_id === module.b_id);
|
||||
|
||||
const requests = [
|
||||
{
|
||||
@@ -450,7 +445,7 @@ async function deleteModule(moduleId = draft.module?.b_id) {
|
||||
inserts: [],
|
||||
updates: [],
|
||||
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 })),
|
||||
},
|
||||
{
|
||||
@@ -467,21 +462,12 @@ async function deleteModule(moduleId = draft.module?.b_id) {
|
||||
updates: [],
|
||||
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",
|
||||
key_field: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: [{ b_id: moduleId }],
|
||||
deletes: [{ b_id: module.b_id }],
|
||||
},
|
||||
];
|
||||
await saveObjectApi(
|
||||
@@ -493,7 +479,7 @@ async function deleteModule(moduleId = draft.module?.b_id) {
|
||||
message.success("模块已删除");
|
||||
deleteConfirmOpen.value = false;
|
||||
pendingDeleteKey.value = "";
|
||||
await loadAll(selectedKey.value === moduleId ? "" : selectedKey.value);
|
||||
await loadAll(selectedKey.value === moduleCode ? "" : selectedKey.value);
|
||||
} catch (error) {
|
||||
message.error(error.message || "模块删除失败");
|
||||
} finally {
|
||||
@@ -523,6 +509,7 @@ onMounted(async () => {
|
||||
<div class="module-management">
|
||||
<ModuleTreePanel
|
||||
:modules="data.modules"
|
||||
:protected-keys="PROTECTED_MODULE_CODES"
|
||||
:selected-key="selectedKey"
|
||||
:loading="loading"
|
||||
:can-create="canCreate"
|
||||
@@ -540,7 +527,7 @@ onMounted(async () => {
|
||||
<div class="title-block">
|
||||
<div class="title-line">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -572,14 +559,14 @@ onMounted(async () => {
|
||||
<a-popconfirm
|
||||
title="确定删除当前模块吗?"
|
||||
:disabled="
|
||||
isNew || !canDelete || ['M_SYSTEM', 'M_MODULE'].includes(draft.module.b_id)
|
||||
isNew || !canDelete || PROTECTED_MODULE_CODES.includes(draft.module.b_code)
|
||||
"
|
||||
@confirm="deleteModule()"
|
||||
>
|
||||
<a-button
|
||||
danger
|
||||
: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" />删除
|
||||
@@ -596,51 +583,41 @@ onMounted(async () => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ModuleBasicInfoPanel
|
||||
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-tabs v-model:active-key="activeTab" class="workspace-tabs">
|
||||
<a-tab-pane key="basic">
|
||||
<template #tab>
|
||||
<span class="tab-label"><FileText :size="15" />基本信息</span>
|
||||
</template>
|
||||
<ModuleBasicInfoPanel
|
||||
v-model:module="draft.module"
|
||||
v-model:translations="draft.translations"
|
||||
:directory-parent-options="directoryParentOptions"
|
||||
:is-new="isNew"
|
||||
:parent-options="parentOptions"
|
||||
@update-module-id="updateModuleId"
|
||||
@update-module-code="updateModuleCode"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="fields">
|
||||
<a-tab-pane v-if="draft.module.b_type !== 'directory'" key="fields">
|
||||
<template #tab>
|
||||
<span class="tab-label">
|
||||
<Database :size="15" />字段定义 <a-badge :count="draft.fields.length" />
|
||||
</span>
|
||||
</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 key="powers">
|
||||
<a-tab-pane v-if="draft.module.b_type !== 'directory'" key="powers">
|
||||
<template #tab>
|
||||
<span class="tab-label">
|
||||
<ShieldCheck :size="15" />操作权限 <a-badge :count="draft.powers.length" />
|
||||
</span>
|
||||
</template>
|
||||
<ModulePowerPanel v-model:powers="draft.powers" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="i18n">
|
||||
<template #tab>
|
||||
<span class="tab-label"><Languages :size="15" />多语言</span>
|
||||
</template>
|
||||
<ModuleTranslationsPanel
|
||||
v-model:translations="draft.translations"
|
||||
:keys="translationKeys"
|
||||
<ModulePowerPanel
|
||||
v-model:powers="draft.powers"
|
||||
:module-id="draft.module.b_id"
|
||||
:module-code="draft.module.b_code"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
@@ -664,7 +641,8 @@ onMounted(async () => {
|
||||
>
|
||||
<p>
|
||||
确定删除
|
||||
{{ pendingDeleteModule?.b_name || pendingDeleteKey }} 吗?关联字段、权限和多语言将一并删除。
|
||||
{{ pendingDeleteModule?.b_name || pendingDeleteKey }}
|
||||
吗?关联字段和权限将一并删除;共享多语言资源会保留在多语言中心。
|
||||
</p>
|
||||
</a-modal>
|
||||
</div>
|
||||
+11
-11
@@ -3,8 +3,8 @@
|
||||
## 设计约定
|
||||
|
||||
- 表名和字段名沿用旧系统的 `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`。
|
||||
- `b_xh` 表示显示顺序,数值越小越靠前。
|
||||
- 当前按机构使用独立数据库,表中不保存机构编号。
|
||||
@@ -31,8 +31,9 @@ b_module / b_module_field / b_module_power
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `b_id` | `varchar(50)` | 主键,模块代码,例如 `M_BIZ` |
|
||||
| `b_parent_id` | `varchar(50)` | 上级模块代码,根节点为空 |
|
||||
| `b_id` | `bigint` | 不可变雪花 ID 主键 |
|
||||
| `b_code` | `varchar(50)` | 可修改的唯一模块编码,例如 `m_biz` |
|
||||
| `b_parent_id` | `bigint` | 上级模块 ID,根节点为空 |
|
||||
| `b_name` | `nvarchar(100)` | 默认中文名称 |
|
||||
| `b_i18n` | `varchar(150)` | 多语言键 |
|
||||
| `b_type` | `varchar(20)` | `directory`、`page` 或 `data` |
|
||||
@@ -40,8 +41,7 @@ b_module / b_module_field / b_module_power
|
||||
| `b_viewtable` | `varchar(128)` | 查询表或视图 |
|
||||
| `b_savetable` | `varchar(128)` | 保存目标表 |
|
||||
| `b_keyfield` | `varchar(50)` | 主键字段 |
|
||||
| `b_orderfield` | `varchar(50)` | 排序字段 |
|
||||
| `b_ordertype` | `varchar(4)` | `ASC` 或 `DESC` |
|
||||
| `b_orderfield` | `varchar(500)` | 排序条件,例如 `b_xh ASC, b_id DESC` |
|
||||
| `b_canmenu` | `int` | 是否显示在菜单,默认 `0` |
|
||||
| `b_canuse` | `int` | 是否启用,默认 `1` |
|
||||
| `b_xh` | `int` | 同级显示顺序 |
|
||||
@@ -49,7 +49,7 @@ b_module / b_module_field / b_module_power
|
||||
约束:
|
||||
|
||||
- `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` 非空时唯一。
|
||||
- 目录不能配置路由和数据表;数据模块不能显示在菜单中。
|
||||
- 表名、视图名和字段名必须由后端校验,不能直接拼接 SQL。
|
||||
@@ -61,7 +61,7 @@ b_module / b_module_field / b_module_power
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||
| `b_module_id` | `varchar(50)` | 所属模块代码 |
|
||||
| `b_module_id` | `bigint` | 所属模块 ID |
|
||||
| `b_field` | `varchar(50)` | 数据库字段名 |
|
||||
| `b_name` | `nvarchar(100)` | 默认中文名称 |
|
||||
| `b_i18n` | `varchar(150)` | 多语言键 |
|
||||
@@ -90,7 +90,7 @@ b_module / b_module_field / b_module_power
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||
| `b_module_id` | `varchar(50)` | 所属模块代码 |
|
||||
| `b_module_id` | `bigint` | 所属模块 ID |
|
||||
| `b_code` | `varchar(50)` | 操作编码,例如 `create`、`update`、`delete` |
|
||||
| `b_name` | `nvarchar(100)` | 操作名称 |
|
||||
| `b_i18n` | `varchar(150)` | 多语言键 |
|
||||
@@ -111,7 +111,7 @@ b_module / b_module_field / b_module_power
|
||||
| --- | --- | --- |
|
||||
| `b_id` | `bigint` | 雪花 ID 主键 |
|
||||
| `b_user_id` | `bigint` | 用户雪花 ID |
|
||||
| `b_module_id` | `varchar(50)` | 模块代码 |
|
||||
| `b_module_id` | `bigint` | 模块 ID |
|
||||
| `b_inputuser_id` | `bigint` | 授权人雪花 ID |
|
||||
| `b_inputdatetime` | `datetime` | 授权时间 |
|
||||
|
||||
@@ -140,7 +140,7 @@ b_module / b_module_field / b_module_power
|
||||
|
||||
### `dbo.b_i18n`
|
||||
|
||||
保存模块、字段和按钮的多语言文本。
|
||||
统一保存菜单、模块、权限、字段、按钮、提示和校验信息等界面文案;业务对象只绑定稳定的 `b_key`。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
```text
|
||||
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_MODULE` | `page` | `/system/modules` |
|
||||
| `m_system` | `directory` | - |
|
||||
| `m_module` | `page` | `/system/modules` |
|
||||
|
||||
为 `M_MODULE` 初始化按钮:
|
||||
为 `m_module` 初始化按钮:
|
||||
|
||||
```text
|
||||
create 新增模块
|
||||
@@ -104,8 +106,9 @@ grant 用户授权
|
||||
- `b_module_field.b_id`
|
||||
- `b_module_power.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` 按依赖顺序删除:
|
||||
|
||||
|
||||
Reference in new issue
Block a user