20260730231110
This commit is contained in:
1 parent
4929b78434
commit
318fa0d9e1
23 files changed
+4026
-506
No files matched your search
@@ -323,33 +323,15 @@ BEGIN TRANSACTION;
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.b_other', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.[b_other] (
|
||||
[b_cateid] varchar(50) NULL,
|
||||
[b_id] bigint NOT NULL,
|
||||
[b_name_c] nvarchar(50) NULL,
|
||||
[b_1] numeric(18,0) NULL,
|
||||
[b_2] varchar(50) NULL,
|
||||
[b_desc] nvarchar(255) NULL,
|
||||
[b_inputuser_id] bigint NULL,
|
||||
[b_inputdatetime] datetime2(0) NULL,
|
||||
[b_updateuser_id] bigint NULL,
|
||||
[b_updatedatetime] datetime2(0) NULL,
|
||||
[b_code] varchar(50) NOT NULL,
|
||||
CONSTRAINT [PK_b_other] PRIMARY KEY ([b_id]),
|
||||
CONSTRAINT [UQ_b_other_b_code] UNIQUE ([b_code])
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.b_other_bmfl', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.[b_other_bmfl] (
|
||||
b_id bigint NOT NULL,
|
||||
b_parent_id bigint NULL,
|
||||
b_name nvarchar(150) NOT NULL,
|
||||
b_level int NOT NULL CONSTRAINT [DF_b_other_bmfl_level] DEFAULT (1),
|
||||
b_path varchar(1000) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_node_type varchar(20) NOT NULL CONSTRAINT [DF_b_other_bmfl_node_type] DEFAULT ('category'),
|
||||
b_module_code varchar(100) NULL,
|
||||
b_xh int NOT NULL CONSTRAINT [DF_b_other_bmfl_xh] DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT [DF_b_other_bmfl_canuse] DEFAULT (1),
|
||||
b_inputuser_id bigint NULL,
|
||||
@@ -358,10 +340,15 @@ BEGIN TRANSACTION;
|
||||
b_updatedatetime datetime2(0) NULL,
|
||||
CONSTRAINT [PK_b_other_bmfl] PRIMARY KEY (b_id),
|
||||
CONSTRAINT [CK_b_other_bmfl_parent] CHECK (b_parent_id IS NULL OR b_parent_id <> b_id),
|
||||
CONSTRAINT [CK_b_other_bmfl_node_type] CHECK (b_node_type IN ('category', 'module')),
|
||||
CONSTRAINT [CK_b_other_bmfl_module_ref] CHECK (
|
||||
(b_node_type = 'category' AND b_module_code IS NULL)
|
||||
OR (b_node_type = 'module' AND b_module_code IS NOT NULL AND LEN(LTRIM(RTRIM(b_module_code))) > 0)
|
||||
),
|
||||
CONSTRAINT [FK_b_other_bmfl_parent] FOREIGN KEY (b_parent_id) REFERENCES dbo.[b_other_bmfl] (b_id)
|
||||
);
|
||||
CREATE INDEX [IX_b_other_bmfl_parent] ON dbo.[b_other_bmfl] (b_parent_id, b_xh, b_id);
|
||||
CREATE INDEX [IX_b_other_bmfl_path] ON dbo.[b_other_bmfl] (b_path, b_xh, b_id);
|
||||
CREATE INDEX [IX_b_other_bmfl_module] ON dbo.[b_other_bmfl] (b_node_type, b_module_code);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.b_othercompany_accountbank', N'U') IS NULL
|
||||
|
||||
@@ -17,10 +17,18 @@ BEGIN TRANSACTION;
|
||||
THROW 52001, N'base directory not found. Run 012 / 002 migration first.', 1;
|
||||
|
||||
/* =========================================================================
|
||||
2. 创建 page 模块: m_base_otherdata
|
||||
2. 创建 page 模块: base_otherdata
|
||||
========================================================================= */
|
||||
IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = 'm_base_otherdata')
|
||||
AND NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = 'base_otherdata')
|
||||
BEGIN
|
||||
UPDATE dbo.s_module
|
||||
SET b_code = 'base_otherdata'
|
||||
WHERE b_code = 'm_base_otherdata';
|
||||
END;
|
||||
|
||||
DECLARE @PageId bigint = 920000000000001100;
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = 'm_base_otherdata')
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = 'base_otherdata')
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @PageId)
|
||||
THROW 52002, N'The reserved otherdata page ID is already in use.', 1;
|
||||
@@ -30,13 +38,13 @@ BEGIN TRANSACTION;
|
||||
b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield,
|
||||
b_canmenu, b_canuse, b_xh
|
||||
) VALUES (
|
||||
@PageId, 'm_base_otherdata', @BaseId, N'其他数据',
|
||||
@PageId, 'base_otherdata', @BaseId, N'其他数据',
|
||||
'menu.base_otherdata', 'page', '/base/otherdata',
|
||||
NULL, NULL, NULL, NULL, 1, 1, 30
|
||||
);
|
||||
END
|
||||
ELSE
|
||||
SET @PageId = (SELECT b_id FROM dbo.s_module WHERE b_code = 'm_base_otherdata');
|
||||
SET @PageId = (SELECT b_id FROM dbo.s_module WHERE b_code = 'base_otherdata');
|
||||
|
||||
/* =========================================================================
|
||||
3. 创建 data 模块辅助存储过程
|
||||
|
||||
@@ -1,78 +1,175 @@
|
||||
/* 扩展 b_other_bmfl 表,添加 b_module_code 字段关联 s_module 数据模块。
|
||||
分类节点 b_module_code 为空(仅做分组),叶子节点指向对应的 data 模块。*/
|
||||
/* 其他数据分类树采用引用设计:
|
||||
category 节点只维护树结构和分类名称,module 节点只引用 s_module.data 的 b_code。*/
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
/* =========================================================================
|
||||
1. 添加 b_module_code 列
|
||||
========================================================================= */
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.columns
|
||||
WHERE object_id = OBJECT_ID(N'dbo.b_other_bmfl') AND name = 'b_module_code'
|
||||
)
|
||||
IF OBJECT_ID(N'dbo.b_other_bmfl', N'U') IS NULL
|
||||
THROW 52301, N'dbo.b_other_bmfl does not exist.', 1;
|
||||
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_node_type') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD b_module_code nvarchar(100) NULL;
|
||||
ADD b_node_type varchar(20) NOT NULL
|
||||
CONSTRAINT [DF_b_other_bmfl_node_type] DEFAULT ('category') WITH VALUES;
|
||||
END;
|
||||
|
||||
/* =========================================================================
|
||||
2. 插入演示分类数据(基础信息 / 财务信息 / 物流信息 分组)
|
||||
========================================================================= */
|
||||
DECLARE @CatBasic bigint = 930000000000000101;
|
||||
DECLARE @CatFinance bigint = 930000000000000102;
|
||||
DECLARE @CatLogistics bigint = 930000000000000103;
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_module_code') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD b_module_code varchar(100) NULL;
|
||||
END;
|
||||
|
||||
/* ---- 基础信息 ---- */
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatBasic)
|
||||
INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code)
|
||||
VALUES (@CatBasic, NULL, N'基础信息', 1, CAST(@CatBasic AS varchar(20)), 10, 1, NULL);
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM sys.indexes
|
||||
WHERE object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND name = N'IX_b_other_bmfl_path'
|
||||
)
|
||||
DROP INDEX [IX_b_other_bmfl_path] ON dbo.b_other_bmfl;
|
||||
|
||||
/* ---- 财务信息 ---- */
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatFinance)
|
||||
INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code)
|
||||
VALUES (@CatFinance, NULL, N'财务信息', 1, CAST(@CatFinance AS varchar(20)), 20, 1, NULL);
|
||||
DECLARE @DropSql nvarchar(max);
|
||||
|
||||
/* ---- 物流信息 ---- */
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatLogistics)
|
||||
INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code)
|
||||
VALUES (@CatLogistics, NULL, N'物流信息', 1, CAST(@CatLogistics AS varchar(20)), 30, 1, NULL);
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_level') IS NOT NULL
|
||||
BEGIN
|
||||
SET @DropSql = NULL;
|
||||
SELECT @DropSql = N'ALTER TABLE dbo.b_other_bmfl DROP CONSTRAINT [' + REPLACE(dc.name, N']', N']]') + N']'
|
||||
FROM sys.default_constraints dc
|
||||
JOIN sys.columns c
|
||||
ON c.object_id = dc.parent_object_id
|
||||
AND c.column_id = dc.parent_column_id
|
||||
WHERE dc.parent_object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND c.name = N'b_level';
|
||||
IF @DropSql IS NOT NULL EXEC sys.sp_executesql @DropSql;
|
||||
|
||||
/* =========================================================================
|
||||
3. 插入叶子节点(关联到已有 data 模块)
|
||||
========================================================================= */
|
||||
DECLARE @NodePort bigint = 930000000000000201;
|
||||
DECLARE @NodeCity bigint = 930000000000000202;
|
||||
DECLARE @NodeMoney bigint = 930000000000000203;
|
||||
DECLARE @NodeOceanline bigint = 930000000000000204;
|
||||
EXEC sys.sp_executesql N'ALTER TABLE dbo.b_other_bmfl DROP COLUMN b_level';
|
||||
END;
|
||||
|
||||
/* ---- 基础信息 / 港口 ---- */
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodePort)
|
||||
INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code)
|
||||
VALUES (@NodePort, @CatBasic, N'港口', 2,
|
||||
CAST(@CatBasic AS varchar(20)) + '/' + CAST(@NodePort AS varchar(20)),
|
||||
10, 1, 'd_other_port');
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_path') IS NOT NULL
|
||||
BEGIN
|
||||
SET @DropSql = NULL;
|
||||
SELECT @DropSql = N'ALTER TABLE dbo.b_other_bmfl DROP CONSTRAINT [' + REPLACE(dc.name, N']', N']]') + N']'
|
||||
FROM sys.default_constraints dc
|
||||
JOIN sys.columns c
|
||||
ON c.object_id = dc.parent_object_id
|
||||
AND c.column_id = dc.parent_column_id
|
||||
WHERE dc.parent_object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND c.name = N'b_path';
|
||||
IF @DropSql IS NOT NULL EXEC sys.sp_executesql @DropSql;
|
||||
|
||||
/* ---- 基础信息 / 城市 ---- */
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeCity)
|
||||
INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code)
|
||||
VALUES (@NodeCity, @CatBasic, N'城市', 2,
|
||||
CAST(@CatBasic AS varchar(20)) + '/' + CAST(@NodeCity AS varchar(20)),
|
||||
20, 1, 'd_other_city');
|
||||
EXEC sys.sp_executesql N'ALTER TABLE dbo.b_other_bmfl DROP COLUMN b_path';
|
||||
END;
|
||||
|
||||
/* ---- 财务信息 / 币种 ---- */
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeMoney)
|
||||
INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code)
|
||||
VALUES (@NodeMoney, @CatFinance, N'币种', 2,
|
||||
CAST(@CatFinance AS varchar(20)) + '/' + CAST(@NodeMoney AS varchar(20)),
|
||||
10, 1, 'd_other_money');
|
||||
EXEC sys.sp_executesql N'
|
||||
UPDATE dbo.b_other_bmfl
|
||||
SET b_module_code = NULLIF(LTRIM(RTRIM(COALESCE(b_module_code, ''''))), '''');
|
||||
|
||||
/* ---- 物流信息 / 航线 ---- */
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeOceanline)
|
||||
INSERT INTO dbo.b_other_bmfl (b_id, b_parent_id, b_name, b_level, b_path, b_xh, b_canuse, b_module_code)
|
||||
VALUES (@NodeOceanline, @CatLogistics, N'航线', 2,
|
||||
CAST(@CatLogistics AS varchar(20)) + '/' + CAST(@NodeOceanline AS varchar(20)),
|
||||
10, 1, 'd_other_oceanline');
|
||||
UPDATE dbo.b_other_bmfl
|
||||
SET b_node_type = CASE
|
||||
WHEN b_module_code IS NULL THEN ''category''
|
||||
ELSE ''module''
|
||||
END';
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.check_constraints
|
||||
WHERE parent_object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND name = N'CK_b_other_bmfl_node_type'
|
||||
)
|
||||
EXEC sys.sp_executesql N'
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD CONSTRAINT [CK_b_other_bmfl_node_type]
|
||||
CHECK (b_node_type IN (''category'', ''module''))';
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.check_constraints
|
||||
WHERE parent_object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND name = N'CK_b_other_bmfl_module_ref'
|
||||
)
|
||||
EXEC sys.sp_executesql N'
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD CONSTRAINT [CK_b_other_bmfl_module_ref]
|
||||
CHECK (
|
||||
(b_node_type = ''category'' AND b_module_code IS NULL)
|
||||
OR (
|
||||
b_node_type = ''module''
|
||||
AND b_module_code IS NOT NULL
|
||||
AND LEN(LTRIM(RTRIM(b_module_code))) > 0
|
||||
)
|
||||
)';
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.indexes
|
||||
WHERE object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND name = N'IX_b_other_bmfl_module'
|
||||
)
|
||||
EXEC sys.sp_executesql N'
|
||||
CREATE INDEX [IX_b_other_bmfl_module] ON dbo.b_other_bmfl (b_node_type, b_module_code)';
|
||||
|
||||
EXEC sys.sp_executesql N'
|
||||
DECLARE @CatBasic bigint = 930000000000000101;
|
||||
DECLARE @CatFinance bigint = 930000000000000102;
|
||||
DECLARE @CatLogistics bigint = 930000000000000103;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatBasic)
|
||||
INSERT INTO dbo.b_other_bmfl (
|
||||
b_id, b_parent_id, b_name, b_node_type, b_module_code,
|
||||
b_xh, b_canuse
|
||||
)
|
||||
VALUES (@CatBasic, NULL, N''基础信息'', ''category'', NULL, 10, 1);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatFinance)
|
||||
INSERT INTO dbo.b_other_bmfl (
|
||||
b_id, b_parent_id, b_name, b_node_type, b_module_code,
|
||||
b_xh, b_canuse
|
||||
)
|
||||
VALUES (@CatFinance, NULL, N''财务信息'', ''category'', NULL, 20, 1);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @CatLogistics)
|
||||
INSERT INTO dbo.b_other_bmfl (
|
||||
b_id, b_parent_id, b_name, b_node_type, b_module_code,
|
||||
b_xh, b_canuse
|
||||
)
|
||||
VALUES (@CatLogistics, NULL, N''物流信息'', ''category'', NULL, 30, 1);
|
||||
|
||||
DECLARE @NodePort bigint = 930000000000000201;
|
||||
DECLARE @NodeCity bigint = 930000000000000202;
|
||||
DECLARE @NodeMoney bigint = 930000000000000203;
|
||||
DECLARE @NodeOceanline bigint = 930000000000000204;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodePort)
|
||||
INSERT INTO dbo.b_other_bmfl (
|
||||
b_id, b_parent_id, b_name, b_node_type, b_module_code,
|
||||
b_xh, b_canuse
|
||||
)
|
||||
VALUES (
|
||||
@NodePort, @CatBasic, N''港口'', ''module'', ''d_other_port'', 10, 1
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeCity)
|
||||
INSERT INTO dbo.b_other_bmfl (
|
||||
b_id, b_parent_id, b_name, b_node_type, b_module_code,
|
||||
b_xh, b_canuse
|
||||
)
|
||||
VALUES (
|
||||
@NodeCity, @CatBasic, N''城市'', ''module'', ''d_other_city'', 20, 1
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeMoney)
|
||||
INSERT INTO dbo.b_other_bmfl (
|
||||
b_id, b_parent_id, b_name, b_node_type, b_module_code,
|
||||
b_xh, b_canuse
|
||||
)
|
||||
VALUES (
|
||||
@NodeMoney, @CatFinance, N''币种'', ''module'', ''d_other_money'', 10, 1
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_other_bmfl WHERE b_id = @NodeOceanline)
|
||||
INSERT INTO dbo.b_other_bmfl (
|
||||
b_id, b_parent_id, b_name, b_node_type, b_module_code,
|
||||
b_xh, b_canuse
|
||||
)
|
||||
VALUES (
|
||||
@NodeOceanline, @CatLogistics, N''航线'', ''module'', ''d_other_oceanline'', 10, 1
|
||||
);';
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
@@ -0,0 +1,13 @@
|
||||
/* 删除未使用的旧版其他数据通用表。当前其他数据使用 b_other_bmfl 分类树
|
||||
关联 s_module 数据节点,并由具体基础表保存数据。*/
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF OBJECT_ID(N'dbo.b_other', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.b_other;
|
||||
END;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
@@ -0,0 +1,29 @@
|
||||
/* 为其他数据分类树增加多语言键。分类节点归属 base_otherdata 页面模块;
|
||||
数据节点名称继续使用其关联 s_module data 节点的 b_i18n。*/
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF OBJECT_ID(N'dbo.b_other_bmfl', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.columns
|
||||
WHERE object_id = OBJECT_ID(N'dbo.b_other_bmfl') AND name = 'b_i18n'
|
||||
)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD b_i18n varchar(150) NULL;
|
||||
END;
|
||||
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_module_code') IS NOT NULL
|
||||
BEGIN
|
||||
EXEC sys.sp_executesql N'
|
||||
UPDATE dbo.b_other_bmfl
|
||||
SET b_i18n = ''category.base_otherdata.'' + CAST(b_id AS varchar(30))
|
||||
WHERE b_module_code IS NULL
|
||||
AND (b_i18n IS NULL OR b_i18n = '''')';
|
||||
END;
|
||||
END;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
@@ -0,0 +1,132 @@
|
||||
/* 规范其他数据分类树为引用设计。
|
||||
仅修正目标库元数据结构,不从模块管理同步树节点。*/
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = 'm_base_otherdata')
|
||||
AND NOT EXISTS (SELECT 1 FROM dbo.s_module WHERE b_code = 'base_otherdata')
|
||||
BEGIN
|
||||
UPDATE dbo.s_module
|
||||
SET b_code = 'base_otherdata'
|
||||
WHERE b_code = 'm_base_otherdata';
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.b_other_bmfl', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_node_type') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD b_node_type varchar(20) NOT NULL
|
||||
CONSTRAINT [DF_b_other_bmfl_node_type] DEFAULT ('category') WITH VALUES;
|
||||
END;
|
||||
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_module_code') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD b_module_code varchar(100) NULL;
|
||||
END;
|
||||
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_i18n') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD b_i18n varchar(150) NULL;
|
||||
END;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM sys.indexes
|
||||
WHERE object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND name = N'IX_b_other_bmfl_path'
|
||||
)
|
||||
DROP INDEX [IX_b_other_bmfl_path] ON dbo.b_other_bmfl;
|
||||
|
||||
EXEC sys.sp_executesql N'
|
||||
UPDATE dbo.b_other_bmfl
|
||||
SET b_module_code = NULLIF(LTRIM(RTRIM(COALESCE(b_module_code, ''''))), '''');
|
||||
|
||||
UPDATE dbo.b_other_bmfl
|
||||
SET b_node_type = CASE
|
||||
WHEN b_module_code IS NULL THEN ''category''
|
||||
ELSE ''module''
|
||||
END;
|
||||
|
||||
UPDATE dbo.b_other_bmfl
|
||||
SET b_i18n = ''category.base_otherdata.'' + CAST(b_id AS varchar(30))
|
||||
WHERE b_node_type = ''category''
|
||||
AND (b_i18n IS NULL OR b_i18n = '''');
|
||||
|
||||
UPDATE dbo.b_other_bmfl
|
||||
SET b_i18n = REPLACE(b_i18n, ''category.m_base_otherdata.'', ''category.base_otherdata.'')
|
||||
WHERE b_node_type = ''category''
|
||||
AND b_i18n LIKE ''category.m_base_otherdata.%''';
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.check_constraints
|
||||
WHERE parent_object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND name = N'CK_b_other_bmfl_node_type'
|
||||
)
|
||||
EXEC sys.sp_executesql N'
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD CONSTRAINT [CK_b_other_bmfl_node_type]
|
||||
CHECK (b_node_type IN (''category'', ''module''))';
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.check_constraints
|
||||
WHERE parent_object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND name = N'CK_b_other_bmfl_module_ref'
|
||||
)
|
||||
EXEC sys.sp_executesql N'
|
||||
ALTER TABLE dbo.b_other_bmfl
|
||||
ADD CONSTRAINT [CK_b_other_bmfl_module_ref]
|
||||
CHECK (
|
||||
(b_node_type = ''category'' AND b_module_code IS NULL)
|
||||
OR (
|
||||
b_node_type = ''module''
|
||||
AND b_module_code IS NOT NULL
|
||||
AND LEN(LTRIM(RTRIM(b_module_code))) > 0
|
||||
)
|
||||
)';
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM sys.indexes
|
||||
WHERE object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND name = N'IX_b_other_bmfl_module'
|
||||
)
|
||||
EXEC sys.sp_executesql N'
|
||||
CREATE INDEX [IX_b_other_bmfl_module] ON dbo.b_other_bmfl (b_node_type, b_module_code)';
|
||||
|
||||
DECLARE @DropSql nvarchar(max);
|
||||
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_level') IS NOT NULL
|
||||
BEGIN
|
||||
SET @DropSql = NULL;
|
||||
SELECT @DropSql = N'ALTER TABLE dbo.b_other_bmfl DROP CONSTRAINT [' + REPLACE(dc.name, N']', N']]') + N']'
|
||||
FROM sys.default_constraints dc
|
||||
JOIN sys.columns c
|
||||
ON c.object_id = dc.parent_object_id
|
||||
AND c.column_id = dc.parent_column_id
|
||||
WHERE dc.parent_object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND c.name = N'b_level';
|
||||
IF @DropSql IS NOT NULL EXEC sys.sp_executesql @DropSql;
|
||||
|
||||
EXEC sys.sp_executesql N'ALTER TABLE dbo.b_other_bmfl DROP COLUMN b_level';
|
||||
END;
|
||||
|
||||
IF COL_LENGTH(N'dbo.b_other_bmfl', N'b_path') IS NOT NULL
|
||||
BEGIN
|
||||
SET @DropSql = NULL;
|
||||
SELECT @DropSql = N'ALTER TABLE dbo.b_other_bmfl DROP CONSTRAINT [' + REPLACE(dc.name, N']', N']]') + N']'
|
||||
FROM sys.default_constraints dc
|
||||
JOIN sys.columns c
|
||||
ON c.object_id = dc.parent_object_id
|
||||
AND c.column_id = dc.parent_column_id
|
||||
WHERE dc.parent_object_id = OBJECT_ID(N'dbo.b_other_bmfl')
|
||||
AND c.name = N'b_path';
|
||||
IF @DropSql IS NOT NULL EXEC sys.sp_executesql @DropSql;
|
||||
|
||||
EXEC sys.sp_executesql N'ALTER TABLE dbo.b_other_bmfl DROP COLUMN b_path';
|
||||
END;
|
||||
END;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
public/file-viewer
|
||||
@@ -11,6 +11,7 @@
|
||||
"fmt:check": "oxfmt --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@file-viewer/vue3-full": "^2.2.4",
|
||||
"@lucide/vue": "^1.24.0",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"antdv-next": "^1.4.2",
|
||||
@@ -25,6 +26,7 @@
|
||||
"devDependencies": {
|
||||
"@antdv-next/auto-import-resolver": "^1.1.0",
|
||||
"@antdv-next/tailwind": "^1.1.1",
|
||||
"@file-viewer/vite-plugin": "^2.2.4",
|
||||
"@tailwindcss/vite": "4.3.2",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"less": "^4.7.0",
|
||||
|
||||
Generated
+1965
-3
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useLookupOptions } from "./select/useLookupOptions";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
@@ -15,69 +15,143 @@ const props = defineProps({
|
||||
orderBy: { type: String, default: "" },
|
||||
pageSize: { type: Number, default: 20 },
|
||||
searchDelay: { type: Number, default: 300 },
|
||||
allowClear: { type: Boolean, default: true },
|
||||
allowClear: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "update:label", "change"]);
|
||||
const lookup = useLookupOptions(props);
|
||||
const inputValue = ref("");
|
||||
const clearedByInput = ref(false);
|
||||
|
||||
const options = computed(() => {
|
||||
const items = lookup.rows.value.map((row) => ({
|
||||
value: row[props.valueKey],
|
||||
label: String(row[props.labelKey] ?? ""),
|
||||
record: row,
|
||||
}));
|
||||
const items = lookup.rows.value.map((row) => {
|
||||
const label = String(row[props.labelKey] ?? "");
|
||||
const value = row[props.valueKey];
|
||||
return {
|
||||
value: label,
|
||||
label,
|
||||
key: String(value ?? label),
|
||||
rawValue: value,
|
||||
record: row,
|
||||
};
|
||||
});
|
||||
|
||||
if (
|
||||
props.modelValue !== "" &&
|
||||
props.modelValue != null &&
|
||||
props.label &&
|
||||
!items.some((item) => item.value === props.modelValue)
|
||||
) {
|
||||
items.unshift({ value: props.modelValue, label: props.label, record: null });
|
||||
if (props.modelValue !== "" && props.modelValue != null && props.label) {
|
||||
const exists = items.some((item) => String(item.rawValue) === String(props.modelValue));
|
||||
if (!exists) {
|
||||
items.unshift({
|
||||
value: props.label,
|
||||
label: props.label,
|
||||
key: String(props.modelValue),
|
||||
rawValue: props.modelValue,
|
||||
record: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
function onOpenChange(open) {
|
||||
if (open) lookup.open();
|
||||
else lookup.resetAfterClose();
|
||||
watch(
|
||||
() => [props.modelValue, props.label],
|
||||
() => {
|
||||
if (props.modelValue === "" || props.modelValue == null) {
|
||||
if (!props.label) inputValue.value = "";
|
||||
return;
|
||||
}
|
||||
clearedByInput.value = false;
|
||||
inputValue.value = props.label || "";
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onSearch(value) {
|
||||
inputValue.value = String(value ?? "");
|
||||
if (!inputValue.value) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
lookup.search(value);
|
||||
}
|
||||
|
||||
function onSelect(value, option) {
|
||||
emit("update:modelValue", value);
|
||||
emit("update:label", String(option.label ?? ""));
|
||||
emit("change", option.record ?? null);
|
||||
function onInputChange(value) {
|
||||
inputValue.value = String(value ?? "");
|
||||
if (!inputValue.value) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
clearedByInput.value = false;
|
||||
}
|
||||
|
||||
function onChange(value) {
|
||||
if (value !== undefined && value !== null) return;
|
||||
function clearSelection() {
|
||||
inputValue.value = "";
|
||||
clearedByInput.value = true;
|
||||
emit("update:modelValue", "");
|
||||
emit("update:label", "");
|
||||
emit("change", null);
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
if (clearedByInput.value || !inputValue.value) return;
|
||||
inputValue.value = props.label || "";
|
||||
}
|
||||
|
||||
function onOpenChange(open) {
|
||||
if (open) lookup.open();
|
||||
else {
|
||||
lookup.resetAfterClose();
|
||||
if (clearedByInput.value || !inputValue.value) return;
|
||||
inputValue.value = props.label || "";
|
||||
}
|
||||
}
|
||||
|
||||
function onSelect(_value, option) {
|
||||
const record = option.record ?? null;
|
||||
const value = record ? record[props.valueKey] : option.rawValue;
|
||||
const label = record ? String(record[props.labelKey] ?? "") : String(option.label ?? "");
|
||||
inputValue.value = label;
|
||||
clearedByInput.value = false;
|
||||
emit("update:modelValue", value ?? "");
|
||||
emit("update:label", label);
|
||||
emit("change", record);
|
||||
}
|
||||
|
||||
function onInputKeydown(event) {
|
||||
if (event.key !== "Backspace" && event.key !== "Delete") return;
|
||||
const checkCleared = () => {
|
||||
const target = event.target;
|
||||
const value = target && "value" in target ? String(target.value ?? "") : inputValue.value;
|
||||
if (!value) clearSelection();
|
||||
};
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
requestAnimationFrame(checkCleared);
|
||||
} else {
|
||||
setTimeout(checkCleared, 0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-select
|
||||
<a-auto-complete
|
||||
v-bind="$attrs"
|
||||
class="fms-select"
|
||||
:value="modelValue === '' ? undefined : modelValue"
|
||||
:value="inputValue"
|
||||
:options="options"
|
||||
:loading="lookup.loading.value"
|
||||
:filter-option="false"
|
||||
:show-search="{ onSearch }"
|
||||
:not-found-content="lookup.loading.value ? '加载中...' : '暂无数据'"
|
||||
:allow-clear="allowClear"
|
||||
show-search
|
||||
@search="lookup.search"
|
||||
@change="onInputChange"
|
||||
@clear="clearSelection"
|
||||
@blur="onBlur"
|
||||
@focus="lookup.open"
|
||||
@input-keydown="onInputKeydown"
|
||||
@open-change="onOpenChange"
|
||||
@popup-scroll="lookup.onPopupScroll"
|
||||
@select="onSelect"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.fms-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, ref, useAttrs, watch } from "vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const attrs = useAttrs();
|
||||
const appStore = useAppStore();
|
||||
const handledEventAttrs = new Set([
|
||||
"onDrop",
|
||||
"onDragStart",
|
||||
@@ -36,7 +38,6 @@ const props = defineProps({
|
||||
draggable: { type: [Boolean, Function, Object], default: false },
|
||||
allowDrop: { type: Function, default: null },
|
||||
rootKey: { type: [String, Number], default: "__fms_tree_root__" },
|
||||
rootTitle: { type: String, default: "全部分类" },
|
||||
// 右键菜单生成函数:(node) => { items: [...] }
|
||||
contextMenu: { type: Function, default: null },
|
||||
contextMenuDisabled: { type: Boolean, default: false },
|
||||
@@ -48,13 +49,30 @@ const effectiveVirtual = computed(() => {
|
||||
return props.virtual;
|
||||
});
|
||||
|
||||
function getNodeTitle(node) {
|
||||
const fallback = node?.title || node?.b_name || node?.b_code || "";
|
||||
return appStore.t(node?.b_i18n, fallback);
|
||||
}
|
||||
|
||||
function localizeNodes(nodes) {
|
||||
const result = [];
|
||||
for (const node of nodes || []) {
|
||||
result.push({
|
||||
...node,
|
||||
title: getNodeTitle(node),
|
||||
children: localizeNodes(node.children),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const effectiveTreeData = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: props.rootKey,
|
||||
title: props.rootTitle,
|
||||
title: appStore.t("common.allCategories", "全部分类"),
|
||||
isFmsRoot: true,
|
||||
children: props.treeData,
|
||||
children: localizeNodes(props.treeData),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useTimeoutFn } from "@vueuse/core";
|
||||
import { pageDataApi } from "@/services/api";
|
||||
|
||||
@@ -19,10 +19,6 @@ export function useLookupOptions(props) {
|
||||
const total = ref(0);
|
||||
const query = ref("");
|
||||
|
||||
const searchColumns = computed(() => [
|
||||
...new Set([props.labelKey, props.valueKey, ...props.searchColumns].filter(Boolean)),
|
||||
]);
|
||||
|
||||
let requestId = 0;
|
||||
|
||||
async function load(searchText = query.value, append = false) {
|
||||
@@ -40,7 +36,7 @@ export function useLookupOptions(props) {
|
||||
const response = await pageDataApi({
|
||||
viewName: props.viewName,
|
||||
orderBy: props.orderBy || props.labelKey,
|
||||
searchColumns: searchColumns.value,
|
||||
searchColumns: props.searchColumns.length ? props.searchColumns : undefined,
|
||||
searchCondition: combineConditions(props.searchCondition, searchFilter),
|
||||
pageNo: requestedPage,
|
||||
pageSize: props.pageSize,
|
||||
|
||||
@@ -42,8 +42,8 @@ const menuItems = computed(() => {
|
||||
},
|
||||
{
|
||||
key: "/base/otherdata",
|
||||
label: moduleLabel("m_base_otherdata", "/base/otherdata", "其他数据"),
|
||||
moduleId: "m_base_otherdata",
|
||||
label: moduleLabel("base_otherdata", "/base/otherdata", "其他数据"),
|
||||
moduleId: "base_otherdata",
|
||||
},
|
||||
].filter(canAccessMenu);
|
||||
const systemPages = [
|
||||
|
||||
@@ -7,6 +7,7 @@ import "./styles/tailwind.css";
|
||||
import "./styles/main.scss";
|
||||
import "stk-table-vue/lib/style.css";
|
||||
import { registerFeature, useAreaSelection } from "stk-table-vue";
|
||||
import FileViewer from '@file-viewer/vue3-full'
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
@@ -16,4 +17,5 @@ registerFeature(useAreaSelection);
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(FileViewer);
|
||||
app.mount("#app");
|
||||
@@ -44,7 +44,7 @@ const routes = [
|
||||
title: "其他数据",
|
||||
section: "基础数据",
|
||||
menuKey: "/base/otherdata",
|
||||
moduleId: "m_base_otherdata",
|
||||
moduleId: "base_otherdata",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -113,14 +113,14 @@ const editingCategoryId = ref(null);
|
||||
const addForm = reactive({ b_name: "" });
|
||||
const addSaving = ref(false);
|
||||
const deleteSaving = ref(false);
|
||||
const categoryModalTitle = computed(() => (editingCategoryId.value ? "修改分类" : "新增分类"));
|
||||
const categoryModalTitle = computed(() => (editingCategoryId.value ? "修改分类" : "添加分类"));
|
||||
|
||||
function buildCategoryContextMenu(node) {
|
||||
if (!node) return { items: [] };
|
||||
const items = [
|
||||
{
|
||||
key: "add-child",
|
||||
label: node.isFmsRoot ? "新增根分类" : "添加子分类",
|
||||
label: "添加分类",
|
||||
icon: h(Plus, { size: 14 }),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,64 +5,158 @@ import { Pencil, Plus, RefreshCw, Trash2 } from "@lucide/vue";
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import { loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from "@/services/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { buildTree, sortTree } from "@/utils/tree";
|
||||
|
||||
const ALL_CATEGORIES_KEY = "__fms_tree_root__";
|
||||
const CATEGORY_COLUMNS = [
|
||||
"b_id", "b_parent_id", "b_name", "b_level", "b_path",
|
||||
"b_xh", "b_canuse", "b_module_code",
|
||||
];
|
||||
|
||||
const appStore = useAppStore();
|
||||
const categories = ref([]);
|
||||
const dataModules = ref([]);
|
||||
const categoryLoading = ref(false);
|
||||
const selectedCategoryKey = ref(ALL_CATEGORIES_KEY);
|
||||
const moduleLoading = ref(false);
|
||||
const selectedCategoryKey = ref("");
|
||||
const selectedModuleCode = ref("");
|
||||
const expandedCategoryKeys = ref([ALL_CATEGORIES_KEY]);
|
||||
const modalVisible = ref(false);
|
||||
const modalParentId = ref(null);
|
||||
const modalParentName = ref("");
|
||||
const editingCategoryId = ref(null);
|
||||
const modalType = ref("category");
|
||||
const saving = ref(false);
|
||||
const deleting = ref(false);
|
||||
const form = reactive({ b_name: "", b_module_code: "" });
|
||||
|
||||
const categoryById = computed(
|
||||
() => new Map(categories.value.map((item) => [String(item.b_id), item])),
|
||||
);
|
||||
const tableKey = computed(() => selectedModuleCode.value);
|
||||
const shouldLoadData = computed(() => Boolean(selectedModuleCode.value));
|
||||
const selectedCategoryKeys = computed(() =>
|
||||
selectedCategoryKey.value ? [selectedCategoryKey.value] : [],
|
||||
);
|
||||
const modalTitle = computed(() => {
|
||||
if (editingCategoryId.value) return modalType.value === "node" ? "修改节点" : "修改分类";
|
||||
return modalType.value === "node" ? "添加节点" : "添加分类";
|
||||
});
|
||||
const moduleOptions = computed(() => {
|
||||
const usedCodes = new Set();
|
||||
for (const item of categories.value) {
|
||||
if (String(item.b_id) === String(editingCategoryId.value || "")) continue;
|
||||
if (getNodeType(item) !== "module") continue;
|
||||
if (item.b_module_code) usedCodes.add(String(item.b_module_code));
|
||||
}
|
||||
|
||||
const options = [];
|
||||
for (const item of dataModules.value) {
|
||||
if (!item.b_code || usedCodes.has(String(item.b_code))) continue;
|
||||
options.push({
|
||||
label: `${appStore.t(item.b_i18n, item.b_name || item.b_code)}(${item.b_code})`,
|
||||
value: item.b_code,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
});
|
||||
const isNodeModal = computed(() => modalType.value === "node");
|
||||
const moduleByCode = computed(() => {
|
||||
const map = new Map();
|
||||
for (const item of dataModules.value) {
|
||||
if (item.b_code) map.set(String(item.b_code), item);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
const categoryTree = computed(() => {
|
||||
const roots = buildTree(categories.value, {
|
||||
mapNode: (item) => ({
|
||||
key: String(item.b_id),
|
||||
title: Number(item.b_canuse) === 1
|
||||
? item.b_name
|
||||
: `${item.b_name}(停用)`,
|
||||
b_id: String(item.b_id),
|
||||
b_parent_id: item.b_parent_id == null ? null : String(item.b_parent_id),
|
||||
b_module_code: item.b_module_code || "",
|
||||
b_xh: item.b_xh,
|
||||
children: [],
|
||||
}),
|
||||
mapNode: buildCategoryTreeNode,
|
||||
});
|
||||
sortTree(roots);
|
||||
return roots;
|
||||
});
|
||||
|
||||
function buildCategoryTreeNode(item) {
|
||||
let title = "";
|
||||
const moduleCode = item.b_module_code || "";
|
||||
const nodeType = getNodeType(item);
|
||||
if (nodeType === "module") {
|
||||
const module = moduleByCode.value.get(String(moduleCode));
|
||||
title = appStore.t(module?.b_i18n, module?.b_name || item.b_name || moduleCode);
|
||||
} else {
|
||||
title = appStore.t(item.b_i18n, item.b_name);
|
||||
}
|
||||
if (Number(item.b_canuse) !== 1) {
|
||||
title = `${title}(${appStore.t("common.disabled", "停用")})`;
|
||||
}
|
||||
return {
|
||||
key: String(item.b_id),
|
||||
title,
|
||||
b_id: String(item.b_id),
|
||||
b_parent_id: item.b_parent_id == null ? null : String(item.b_parent_id),
|
||||
b_i18n: item.b_i18n || "",
|
||||
b_node_type: nodeType,
|
||||
b_module_code: moduleCode,
|
||||
b_xh: item.b_xh,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
function getNodeType(item) {
|
||||
if (!item) return "category";
|
||||
if (item.b_node_type === "module") return "module";
|
||||
if (item.b_node_type === "category") return "category";
|
||||
if (item.b_module_code) return "module";
|
||||
return "category";
|
||||
}
|
||||
|
||||
function buildCategoryI18nKey(id) {
|
||||
return `category.base_otherdata.${id}`;
|
||||
}
|
||||
|
||||
function updateSelectedModule(key) {
|
||||
selectedCategoryKey.value = key;
|
||||
|
||||
if (!key) {
|
||||
selectedModuleCode.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === ALL_CATEGORIES_KEY) {
|
||||
selectedModuleCode.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const category = categoryById.value.get(key);
|
||||
if (!category || getNodeType(category) !== "module") {
|
||||
selectedModuleCode.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleCode = category.b_module_code || "";
|
||||
if (Number(category.b_canuse) !== 1 || !moduleByCode.value.has(String(moduleCode))) {
|
||||
selectedModuleCode.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
selectedModuleCode.value = moduleCode;
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
categoryLoading.value = true;
|
||||
try {
|
||||
const response = await loadDataApi(
|
||||
"b_other_bmfl",
|
||||
"",
|
||||
"b_path ASC, b_xh ASC, b_id ASC",
|
||||
CATEGORY_COLUMNS,
|
||||
);
|
||||
const response = await loadDataApi("b_other_bmfl", "", "b_parent_id ASC, b_xh ASC, b_id ASC");
|
||||
categories.value = response.data || [];
|
||||
|
||||
// 如果之前选中的节点被删了,回到根
|
||||
const existingKeys = new Set(categories.value.map((item) => String(item.b_id)));
|
||||
if (
|
||||
selectedCategoryKey.value &&
|
||||
selectedCategoryKey.value !== ALL_CATEGORIES_KEY &&
|
||||
!existingKeys.has(selectedCategoryKey.value)
|
||||
) {
|
||||
selectedCategoryKey.value = ALL_CATEGORIES_KEY;
|
||||
selectedModuleCode.value = "";
|
||||
updateSelectedModule("");
|
||||
} else {
|
||||
updateSelectedModule(selectedCategoryKey.value);
|
||||
}
|
||||
|
||||
// 展开所有有子节点的父节点
|
||||
const parentKeys = categories.value
|
||||
.map((item) => (item.b_parent_id == null ? "" : String(item.b_parent_id)))
|
||||
.filter(Boolean);
|
||||
@@ -76,53 +170,74 @@ async function loadCategories() {
|
||||
}
|
||||
}
|
||||
|
||||
function onCategorySelect(keys) {
|
||||
if (!keys.length) return;
|
||||
const key = String(keys[0]);
|
||||
selectedCategoryKey.value = key;
|
||||
async function loadDataModules() {
|
||||
moduleLoading.value = true;
|
||||
try {
|
||||
const pageRes = await loadDataApi(
|
||||
"s_module",
|
||||
"b_code = N'base_otherdata' AND b_module_type = 'page' AND b_canuse = 1",
|
||||
"b_id ASC",
|
||||
);
|
||||
const pageId = pageRes.data?.[0]?.b_id;
|
||||
if (!pageId) {
|
||||
dataModules.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断节点是否关联了数据模块
|
||||
const cat = categoryById.value.get(key);
|
||||
if (cat && cat.b_module_code) {
|
||||
selectedModuleCode.value = cat.b_module_code;
|
||||
} else {
|
||||
selectedModuleCode.value = "";
|
||||
const moduleRes = await loadDataApi(
|
||||
"s_module",
|
||||
`b_parent_id = ${pageId} AND b_module_type = 'data' AND b_canuse = 1`,
|
||||
"b_xh ASC, b_id ASC",
|
||||
);
|
||||
dataModules.value = moduleRes.data || [];
|
||||
updateSelectedModule(selectedCategoryKey.value);
|
||||
} catch (error) {
|
||||
message.error("数据节点加载失败:" + (error?.message || error));
|
||||
} finally {
|
||||
moduleLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 右键菜单 & 分类维护 ----
|
||||
const addModalVisible = ref(false);
|
||||
const addModalParentId = ref(null);
|
||||
const addModalParentName = ref("");
|
||||
const editingCategoryId = ref(null);
|
||||
const addForm = reactive({ b_name: "", b_module_code: "" });
|
||||
const addSaving = ref(false);
|
||||
const deleteSaving = ref(false);
|
||||
const categoryModalTitle = computed(() =>
|
||||
editingCategoryId.value ? "修改分类" : "新增分类",
|
||||
);
|
||||
function onCategorySelect(keys, info) {
|
||||
if (info?.node?.isFmsRoot) {
|
||||
updateSelectedModule(ALL_CATEGORIES_KEY);
|
||||
return;
|
||||
}
|
||||
if (keys.length) {
|
||||
updateSelectedModule(String(keys[0]));
|
||||
return;
|
||||
}
|
||||
updateSelectedModule("");
|
||||
}
|
||||
|
||||
function buildCategoryContextMenu(node) {
|
||||
if (!node) return { items: [] };
|
||||
const items = [
|
||||
{
|
||||
key: "add-child",
|
||||
label: node.isFmsRoot ? "新增根分类" : "添加子分类",
|
||||
icon: h(Plus, { size: 14 }),
|
||||
},
|
||||
];
|
||||
if (node.isFmsRoot) {
|
||||
const items = [];
|
||||
const isCategory = !node?.isFmsRoot && node?.b_node_type === "category";
|
||||
if (node?.isFmsRoot || isCategory) {
|
||||
items.push(
|
||||
{
|
||||
key: "add-category",
|
||||
label: "添加分类",
|
||||
icon: h(Plus, { size: 14 }),
|
||||
},
|
||||
{
|
||||
key: "add-node",
|
||||
label: "添加节点",
|
||||
icon: h(Plus, { size: 14 }),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (node?.isFmsRoot) {
|
||||
items.push({
|
||||
key: "refresh",
|
||||
label: "刷新分类",
|
||||
icon: h(RefreshCw, { size: 14 }),
|
||||
});
|
||||
}
|
||||
if (!node.isFmsRoot) {
|
||||
} else {
|
||||
items.push(
|
||||
{
|
||||
key: "edit",
|
||||
label: "修改分类",
|
||||
label: node?.b_node_type === "module" ? "修改节点" : "修改分类",
|
||||
icon: h(Pencil, { size: 14 }),
|
||||
},
|
||||
{ type: "divider" },
|
||||
@@ -134,31 +249,42 @@ function buildCategoryContextMenu(node) {
|
||||
},
|
||||
);
|
||||
}
|
||||
return { items };
|
||||
return {
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
function onCategoryMenuClick({ key, node }) {
|
||||
if (key === "add-child") {
|
||||
editingCategoryId.value = null;
|
||||
addModalParentId.value = node.isFmsRoot ? null : node.b_id || node.key;
|
||||
addModalParentName.value = node.isFmsRoot ? "" : node.title || "";
|
||||
addForm.b_name = "";
|
||||
addForm.b_module_code = "";
|
||||
addModalVisible.value = true;
|
||||
if (key === "refresh") {
|
||||
loadCategories();
|
||||
return;
|
||||
}
|
||||
if (key === "add-category") {
|
||||
openAddModal(node, "category");
|
||||
return;
|
||||
}
|
||||
if (key === "add-node") {
|
||||
openAddModal(node, "node");
|
||||
return;
|
||||
}
|
||||
if (key === "edit") {
|
||||
openEditCategoryModal(node);
|
||||
return;
|
||||
}
|
||||
if (key === "refresh") {
|
||||
loadCategories();
|
||||
openEditModal(node);
|
||||
return;
|
||||
}
|
||||
if (key === "delete") confirmDeleteCategory(node);
|
||||
}
|
||||
|
||||
function openEditCategoryModal(node) {
|
||||
function openAddModal(node, type) {
|
||||
editingCategoryId.value = null;
|
||||
modalType.value = type;
|
||||
modalParentId.value = node?.isFmsRoot ? null : String(node?.b_id || node?.key || "");
|
||||
modalParentName.value = node?.isFmsRoot ? "" : node?.title || "";
|
||||
form.b_name = "";
|
||||
form.b_module_code = "";
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditModal(node) {
|
||||
if (!node || node.isFmsRoot) return;
|
||||
const categoryId = String(node.b_id || node.key || "");
|
||||
const category = categoryById.value.get(categoryId);
|
||||
@@ -168,94 +294,111 @@ function openEditCategoryModal(node) {
|
||||
}
|
||||
|
||||
const parentId = category.b_parent_id == null ? null : String(category.b_parent_id);
|
||||
const parent = parentId ? categoryById.value.get(parentId) : null;
|
||||
editingCategoryId.value = categoryId;
|
||||
addModalParentId.value = parentId;
|
||||
addModalParentName.value = parent?.b_name || "";
|
||||
addForm.b_name = category.b_name || "";
|
||||
addForm.b_module_code = category.b_module_code || "";
|
||||
addModalVisible.value = true;
|
||||
modalType.value = getNodeType(category) === "module" ? "node" : "category";
|
||||
modalParentId.value = parentId;
|
||||
modalParentName.value = parentId ? categoryById.value.get(parentId)?.b_name || "" : "";
|
||||
form.b_name = category.b_name || "";
|
||||
form.b_module_code = category.b_module_code || "";
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function nextSort(parentId) {
|
||||
let max = 0;
|
||||
for (const item of categories.value) {
|
||||
const sameParent = parentId
|
||||
? String(item.b_parent_id) === String(parentId)
|
||||
: item.b_parent_id == null;
|
||||
if (sameParent) max = Math.max(max, Number(item.b_xh) || 0);
|
||||
}
|
||||
return max + 10;
|
||||
}
|
||||
|
||||
function buildNewCategory(id, name, moduleCode, nodeType) {
|
||||
const parentId = modalParentId.value;
|
||||
return {
|
||||
b_id: id,
|
||||
b_parent_id: parentId || null,
|
||||
b_name: name,
|
||||
b_i18n: nodeType === "category" ? buildCategoryI18nKey(id) : null,
|
||||
b_node_type: nodeType,
|
||||
b_xh: nextSort(parentId),
|
||||
b_canuse: 1,
|
||||
b_module_code: moduleCode,
|
||||
};
|
||||
}
|
||||
|
||||
function findDataModule(code) {
|
||||
for (const item of dataModules.value) {
|
||||
if (item.b_code === code) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function onModuleChange(code) {
|
||||
form.b_module_code = code || "";
|
||||
const module = findDataModule(form.b_module_code);
|
||||
if (module) form.b_name = module.b_name || module.b_code || "";
|
||||
}
|
||||
|
||||
async function saveCategory() {
|
||||
const name = (addForm.b_name || "").trim();
|
||||
if (!name) {
|
||||
let name = String(form.b_name || "").trim();
|
||||
if (!name && !isNodeModal.value) {
|
||||
message.warning("请填写名称");
|
||||
return;
|
||||
}
|
||||
const moduleCode = (addForm.b_module_code || "").trim() || null;
|
||||
addSaving.value = true;
|
||||
|
||||
let moduleCode = String(form.b_module_code || "").trim() || null;
|
||||
let nodeType = "category";
|
||||
if (isNodeModal.value) {
|
||||
const module = findDataModule(moduleCode);
|
||||
if (!module) {
|
||||
message.warning("请选择数据节点");
|
||||
return;
|
||||
}
|
||||
name = module.b_name || module.b_code || name;
|
||||
nodeType = "module";
|
||||
} else {
|
||||
moduleCode = null;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
if (editingCategoryId.value) {
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_other_bmfl",
|
||||
key_field: "b_id",
|
||||
updates: [{
|
||||
b_id: editingCategoryId.value,
|
||||
b_name: name,
|
||||
b_module_code: moduleCode,
|
||||
}],
|
||||
updates: [
|
||||
{
|
||||
b_id: editingCategoryId.value,
|
||||
b_name: name,
|
||||
b_i18n:
|
||||
nodeType === "category" ? buildCategoryI18nKey(editingCategoryId.value) : null,
|
||||
b_node_type: nodeType,
|
||||
b_module_code: moduleCode,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
message.success("分类已修改");
|
||||
addModalVisible.value = false;
|
||||
await loadCategories();
|
||||
|
||||
// 如果当前选中节点被修改了模块代码,更新选中
|
||||
if (selectedCategoryKey.value === editingCategoryId.value) {
|
||||
const cat = categoryById.value.get(editingCategoryId.value);
|
||||
selectedModuleCode.value = cat?.b_module_code || "";
|
||||
}
|
||||
return;
|
||||
message.success(isNodeModal.value ? "节点已修改" : "分类已修改");
|
||||
} else {
|
||||
const idRes = await nextIdApi();
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_other_bmfl",
|
||||
key_field: "b_id",
|
||||
inserts: [buildNewCategory(String(idRes.data), name, moduleCode, nodeType)],
|
||||
},
|
||||
]);
|
||||
message.success(isNodeModal.value ? "节点已添加" : "分类已添加");
|
||||
}
|
||||
|
||||
const idRes = await nextIdApi();
|
||||
const newId = String(idRes.data);
|
||||
|
||||
let level = 1;
|
||||
let path = newId;
|
||||
if (addModalParentId.value) {
|
||||
const parent = categoryById.value.get(String(addModalParentId.value));
|
||||
if (parent) {
|
||||
level = (Number(parent.b_level) || 0) + 1;
|
||||
path = (parent.b_path || String(parent.b_id)) + "/" + newId;
|
||||
}
|
||||
}
|
||||
|
||||
const siblings = categories.value.filter((item) =>
|
||||
addModalParentId.value
|
||||
? String(item.b_parent_id) === String(addModalParentId.value)
|
||||
: item.b_parent_id == null,
|
||||
);
|
||||
const maxXh = siblings.reduce((max, item) => Math.max(max, Number(item.b_xh) || 0), 0);
|
||||
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_other_bmfl",
|
||||
key_field: "b_id",
|
||||
inserts: [
|
||||
{
|
||||
b_id: newId,
|
||||
b_parent_id: addModalParentId.value || null,
|
||||
b_name: name,
|
||||
b_level: level,
|
||||
b_path: path,
|
||||
b_xh: maxXh + 10,
|
||||
b_canuse: 1,
|
||||
b_module_code: moduleCode,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
message.success("分类已添加");
|
||||
addModalVisible.value = false;
|
||||
modalVisible.value = false;
|
||||
await loadCategories();
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
} catch (error) {
|
||||
message.error("保存失败:" + (error?.message || error));
|
||||
} finally {
|
||||
addSaving.value = false;
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +413,6 @@ async function hasPersistedChildCategory(categoryId) {
|
||||
const response = await pageDataApi({
|
||||
viewName: "b_other_bmfl",
|
||||
orderBy: "b_id ASC",
|
||||
searchColumns: ["b_id"],
|
||||
searchCondition: `[b_parent_id] = ${categoryId}`,
|
||||
pageNo: 1,
|
||||
pageSize: 1,
|
||||
@@ -278,19 +420,21 @@ async function hasPersistedChildCategory(categoryId) {
|
||||
return Boolean(response.data?.rows?.length);
|
||||
}
|
||||
|
||||
async function confirmDeleteCategory(node) {
|
||||
if (!node || node.isFmsRoot || deleteSaving.value) return;
|
||||
function confirmDeleteCategory(node) {
|
||||
if (!node || node.isFmsRoot || deleting.value) return;
|
||||
const categoryId = String(node.b_id || node.key || "");
|
||||
if (!/^\d+$/.test(categoryId)) {
|
||||
message.error("分类 ID 无效,无法删除");
|
||||
return;
|
||||
}
|
||||
const category = categoryById.value.get(categoryId);
|
||||
const isNode = getNodeType(category) === "module";
|
||||
if (hasChildCategory(categoryId)) {
|
||||
message.warning("该分类下还有子分类,不能删除");
|
||||
return;
|
||||
}
|
||||
|
||||
deleteSaving.value = true;
|
||||
deleting.value = true;
|
||||
Modal.confirm({
|
||||
title: `确定删除「${node.title}」?`,
|
||||
content: "此操作不可恢复",
|
||||
@@ -298,16 +442,14 @@ async function confirmDeleteCategory(node) {
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onCancel: () => {
|
||||
deleteSaving.value = false;
|
||||
deleting.value = false;
|
||||
},
|
||||
onOk: async () => {
|
||||
try {
|
||||
const hasChild = await hasPersistedChildCategory(categoryId);
|
||||
if (hasChild) {
|
||||
if (await hasPersistedChildCategory(categoryId)) {
|
||||
message.warning("该分类下还有子分类,不能删除");
|
||||
return;
|
||||
}
|
||||
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_other_bmfl",
|
||||
@@ -315,23 +457,23 @@ async function confirmDeleteCategory(node) {
|
||||
deletes: [{ b_id: categoryId }],
|
||||
},
|
||||
]);
|
||||
if (selectedCategoryKey.value === categoryId) {
|
||||
selectedCategoryKey.value = ALL_CATEGORIES_KEY;
|
||||
selectedModuleCode.value = "";
|
||||
}
|
||||
message.success("分类已删除");
|
||||
if (selectedCategoryKey.value === categoryId) updateSelectedModule("");
|
||||
message.success(isNode ? "节点已删除" : "分类已删除");
|
||||
await loadCategories();
|
||||
} catch (error) {
|
||||
message.error("删除失败:" + (error?.message || error));
|
||||
throw error;
|
||||
} finally {
|
||||
deleteSaving.value = false;
|
||||
deleting.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadCategories);
|
||||
onMounted(() => {
|
||||
loadCategories();
|
||||
loadDataModules();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -343,10 +485,9 @@ onMounted(loadCategories);
|
||||
<FmsTree
|
||||
v-model:expanded-keys="expandedCategoryKeys"
|
||||
:tree-data="categoryTree"
|
||||
:selected-keys="[selectedCategoryKey]"
|
||||
root-title="其他数据"
|
||||
:selected-keys="selectedCategoryKeys"
|
||||
:context-menu="buildCategoryContextMenu"
|
||||
:context-menu-disabled="categoryLoading || addSaving || deleteSaving"
|
||||
:context-menu-disabled="categoryLoading || saving || deleting"
|
||||
@select="onCategorySelect"
|
||||
@menu-click="onCategoryMenuClick"
|
||||
/>
|
||||
@@ -360,36 +501,38 @@ onMounted(loadCategories);
|
||||
<a-splitter-panel :min="520">
|
||||
<main class="data-panel">
|
||||
<FmsModuleListPage
|
||||
v-if="selectedModuleCode"
|
||||
:key="selectedModuleCode"
|
||||
v-if="shouldLoadData"
|
||||
:key="tableKey"
|
||||
:data-code="selectedModuleCode"
|
||||
/>
|
||||
<a-empty
|
||||
v-else
|
||||
description="请选择具体数据项(叶子节点)"
|
||||
/>
|
||||
<div v-else class="data-empty">
|
||||
<a-empty description="暂无数据" />
|
||||
</div>
|
||||
</main>
|
||||
</a-splitter-panel>
|
||||
</a-splitter>
|
||||
|
||||
<a-modal
|
||||
v-model:open="addModalVisible"
|
||||
:title="categoryModalTitle"
|
||||
:confirm-loading="addSaving"
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="saving"
|
||||
@ok="saveCategory"
|
||||
>
|
||||
<a-form layout="vertical" style="margin-top: 16px">
|
||||
<a-form-item v-if="addModalParentName" label="上级分类">
|
||||
<a-input :value="addModalParentName" disabled />
|
||||
<a-form-item v-if="modalParentName" label="上级分类">
|
||||
<a-input :value="modalParentName" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="名称">
|
||||
<a-input v-model:value="addForm.b_name" :maxlength="150" />
|
||||
<a-form-item v-if="!isNodeModal" label="名称">
|
||||
<a-input v-model:value="form.b_name" :maxlength="150" />
|
||||
</a-form-item>
|
||||
<a-form-item label="数据模块代码">
|
||||
<a-input
|
||||
v-model:value="addForm.b_module_code"
|
||||
:maxlength="100"
|
||||
placeholder="留空则为分类分组节点;填入则为数据模块叶子节点(如 d_other_port)"
|
||||
<a-form-item v-if="isNodeModal" label="数据节点">
|
||||
<a-select
|
||||
:value="form.b_module_code || undefined"
|
||||
:options="moduleOptions"
|
||||
:loading="moduleLoading"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@update:value="onModuleChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -453,4 +596,12 @@ onMounted(loadCategories);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.data-empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup>
|
||||
import { h, shallowRef } from "vue";
|
||||
import { h, ref, shallowRef } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { Input, InputNumber, Select } from "antdv-next";
|
||||
import FmsSelect from "@/components/FmsSelect.vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { FileViewer, withFullViewerOptions } from "@file-viewer/vue3-full";
|
||||
|
||||
const COLUMN_COUNT = 20;
|
||||
const ROW_COUNT = 4000;
|
||||
@@ -47,6 +49,9 @@ function buildData() {
|
||||
}
|
||||
|
||||
const dataSource = shallowRef(buildData());
|
||||
const selectedLocale = ref("");
|
||||
const selectedLocaleLabel = ref("");
|
||||
const selectedLocaleRecord = shallowRef(null);
|
||||
|
||||
// 当前编辑行 key;进入编辑时留存快照,取消可还原
|
||||
const editingKey = shallowRef(null);
|
||||
@@ -170,6 +175,35 @@ function regenerate() {
|
||||
function rowClassName(row) {
|
||||
return row.id === editingKey.value ? "fms-row-editing" : "";
|
||||
}
|
||||
|
||||
function onLocaleChange(record) {
|
||||
selectedLocaleRecord.value = record;
|
||||
}
|
||||
|
||||
// ── 文件预览 Demo(@file-viewer/vue3-full) ──
|
||||
const fvOptions = withFullViewerOptions();
|
||||
const viewerVisible = ref(false);
|
||||
const viewerFile = ref(null);
|
||||
const viewerUrl = ref("");
|
||||
const viewerUrlInput = ref("");
|
||||
const viewerKey = ref(0);
|
||||
|
||||
function openViewer() {
|
||||
viewerVisible.value = true;
|
||||
}
|
||||
function onPickFile(file) {
|
||||
viewerFile.value = file;
|
||||
viewerUrl.value = "";
|
||||
viewerKey.value += 1;
|
||||
return false; // 阻止 antdv 实际上传,仅取本地 File
|
||||
}
|
||||
function applyUrl() {
|
||||
const u = viewerUrlInput.value.trim();
|
||||
if (!u) return;
|
||||
viewerUrl.value = u;
|
||||
viewerFile.value = null;
|
||||
viewerKey.value += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -187,9 +221,31 @@ function rowClassName(row) {
|
||||
<a-button :disabled="editingKey == null" type="primary" @click="saveEdit">保存</a-button>
|
||||
<a-button :disabled="editingKey == null" @click="cancelEdit">取消</a-button>
|
||||
<a-button @click="regenerate">重新生成</a-button>
|
||||
<a-button @click="openViewer">文件预览</a-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="select-demo">
|
||||
<div class="select-demo__field">
|
||||
<label class="select-demo__label">FmsSelect 远程选择</label>
|
||||
<FmsSelect
|
||||
v-model="selectedLocale"
|
||||
v-model:label="selectedLocaleLabel"
|
||||
view-name="b_i18n_type"
|
||||
label-key="b_name"
|
||||
value-key="b_code"
|
||||
order-by="b_code ASC"
|
||||
:page-size="10"
|
||||
@change="onLocaleChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="select-demo__value">
|
||||
<span>当前值:{{ selectedLocale || "-" }}</span>
|
||||
<span>显示文本:{{ selectedLocaleLabel || "-" }}</span>
|
||||
<span>记录编码:{{ selectedLocaleRecord?.b_code || "-" }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-demo__body">
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
@@ -200,10 +256,45 @@ function rowClassName(row) {
|
||||
@row-click="onRowClick"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<a-modal
|
||||
v-model:open="viewerVisible"
|
||||
title="文件预览(@file-viewer/vue3-full)"
|
||||
:width="960"
|
||||
:footer="null"
|
||||
>
|
||||
<div class="fv-demo">
|
||||
<div class="fv-bar">
|
||||
<a-upload :show-upload-list="false" :before-upload="onPickFile" accept="*">
|
||||
<a-button type="primary">选择本地文件</a-button>
|
||||
</a-upload>
|
||||
<a-input
|
||||
v-model:value="viewerUrlInput"
|
||||
placeholder="或粘贴文件 URL,如 https://example.com/demo.pdf"
|
||||
style="width: 380px"
|
||||
@press-enter="applyUrl"
|
||||
/>
|
||||
<a-button @click="applyUrl">预览 URL</a-button>
|
||||
</div>
|
||||
<div class="fv-body">
|
||||
<FileViewer
|
||||
v-if="viewerFile || viewerUrl"
|
||||
:key="viewerKey"
|
||||
:file="viewerFile || undefined"
|
||||
:url="viewerUrl || undefined"
|
||||
:options="fvOptions"
|
||||
/>
|
||||
<div v-else class="fv-empty">请选择本地文件或输入文件 URL 进行预览</div>
|
||||
</div>
|
||||
<p class="fv-tip">
|
||||
支持图片 / PDF / Office / Markdown / 文本 / 音视频等;PDF、Office 等格式由浏览器端 WASM 渲染,无需服务端转换。
|
||||
</p>
|
||||
</div>
|
||||
</a-modal>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.table-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -254,7 +345,72 @@ function rowClassName(row) {
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
.select-demo {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
padding: 12px 16px 0;
|
||||
}
|
||||
|
||||
.select-demo__field {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.select-demo__label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.select-demo__value {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
gap: 16px;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
:deep(.fms-row-editing) {
|
||||
background: rgba(22, 119, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
// ── 文件预览 Demo ──
|
||||
.fv-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.fv-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fv-body {
|
||||
height: 560px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.fv-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.fv-tip {
|
||||
margin: 0;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
@@ -40,6 +40,7 @@ const selectedModuleId = ref(null);
|
||||
const skipLoad = ref(false);
|
||||
const expandTimer = ref(null);
|
||||
const activeTab = ref("basic");
|
||||
let expandDelayResolve = null;
|
||||
function buildTreeContextMenu(node) {
|
||||
const items = [];
|
||||
if (!node) return { items };
|
||||
@@ -329,7 +330,8 @@ async function onTreeDrop(info) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (nodes[i].children && insertIntoTree(nodes[i].children, dropNodeKey, nodes[i])) return true;
|
||||
if (nodes[i].children && insertIntoTree(nodes[i].children, dropNodeKey, nodes[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -371,10 +373,23 @@ async function onTreeDrop(info) {
|
||||
}
|
||||
|
||||
// ---- 模块配置加载 ----
|
||||
async function loadModuleConfig(moduleId) {
|
||||
function waitForNextPaint() {
|
||||
if (typeof requestAnimationFrame !== "function") return Promise.resolve();
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadModuleConfig(moduleId, options = {}) {
|
||||
const id = String(moduleId);
|
||||
const applyReady = options.applyReady;
|
||||
if (options.waitForPaint) await waitForNextPaint();
|
||||
if (String(selectedModuleId.value) !== id) return;
|
||||
|
||||
configLoading.value = true;
|
||||
try {
|
||||
const id = String(moduleId);
|
||||
const moduleType = findNode(id)?.b_module_type;
|
||||
let moduleResponse;
|
||||
let fieldsResponse;
|
||||
@@ -413,6 +428,8 @@ async function loadModuleConfig(moduleId) {
|
||||
moduleResponse = await loadDataApi("s_module", `b_id = ${id}`);
|
||||
}
|
||||
|
||||
if (String(selectedModuleId.value) !== id) return;
|
||||
if (applyReady && !(await applyReady)) return;
|
||||
if (String(selectedModuleId.value) !== id) return;
|
||||
|
||||
const original = {
|
||||
@@ -448,6 +465,23 @@ function clearExpandTimer() {
|
||||
clearTimeout(expandTimer.value);
|
||||
expandTimer.value = null;
|
||||
}
|
||||
if (expandDelayResolve) {
|
||||
expandDelayResolve(false);
|
||||
expandDelayResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
function waitForExpandAnimation() {
|
||||
return new Promise((resolve) => {
|
||||
expandDelayResolve = (ready) => {
|
||||
resolve(ready);
|
||||
};
|
||||
expandTimer.value = setTimeout(() => {
|
||||
expandTimer.value = null;
|
||||
expandDelayResolve = null;
|
||||
resolve(true);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
function onTreeSelect(keys, info) {
|
||||
@@ -481,15 +515,15 @@ function onTreeSelect(keys, info) {
|
||||
expandedTreeKeys.value = expandedKeys.filter((item) => item !== key);
|
||||
selectedModuleId.value = key;
|
||||
} else {
|
||||
// 展开:立即高亮选中节点,动画完毕后再加载数据
|
||||
// 展开:立即高亮,下一帧发请求;结果回写最多等到展开动画结束。
|
||||
expandedTreeKeys.value = [...expandedKeys, key];
|
||||
skipLoad.value = true;
|
||||
selectedModuleId.value = key; // 立即高亮,watch 中 skipLoad=true 跳过加载
|
||||
expandTimer.value = setTimeout(() => {
|
||||
expandTimer.value = null;
|
||||
loadedModuleId.value = null;
|
||||
void loadModuleConfig(key);
|
||||
}, 300);
|
||||
selectedModuleId.value = key;
|
||||
loadedModuleId.value = null;
|
||||
void loadModuleConfig(key, {
|
||||
waitForPaint: true,
|
||||
applyReady: waitForExpandAnimation(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1056,9 +1090,7 @@ async function syncFields() {
|
||||
}
|
||||
// 多语言面板中移除旧字段对应的受管翻译行(_managedId 格式为 field:<b_id>)
|
||||
if (i18nPanelRef.value) {
|
||||
const managedIds = new Set(
|
||||
Array.from(removedFieldIds).map((id) => `field:${id}`),
|
||||
);
|
||||
const managedIds = new Set(Array.from(removedFieldIds).map((id) => `field:${id}`));
|
||||
i18nPanelRef.value.removeByManagedIds(managedIds);
|
||||
}
|
||||
}
|
||||
@@ -1151,192 +1183,196 @@ async function syncFields() {
|
||||
|
||||
// ---- 初始化 ----
|
||||
void loadTree().catch((e) => message.error("模块树加载失败:" + (e.message || e)));
|
||||
|
||||
onBeforeUnmount(clearExpandTimer);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mm-page">
|
||||
<a-splitter class="mm-splitter" lazy>
|
||||
<a-splitter-panel :default-size="260" :min="210" :max="440" collapsible>
|
||||
<!-- 左侧模块树 -->
|
||||
<aside class="mm-sidebar">
|
||||
<div class="mm-tree-stage">
|
||||
<div v-if="treeLoading" class="mm-tree-empty">
|
||||
<a-spin />
|
||||
</div>
|
||||
<FmsTree
|
||||
v-else
|
||||
:disabled="configSaving"
|
||||
:tree-data="treeData"
|
||||
v-model:expanded-keys="expandedTreeKeys"
|
||||
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
||||
:context-menu="buildTreeContextMenu"
|
||||
:context-menu-disabled="busy"
|
||||
@select="onTreeSelect"
|
||||
@menu-click="onTreeMenuClick"
|
||||
>
|
||||
<template #title="node">
|
||||
<span class="mm-tree-node">
|
||||
<span>{{ node.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
</div>
|
||||
</aside>
|
||||
</a-splitter-panel>
|
||||
|
||||
<a-splitter-panel :min="720">
|
||||
<!-- 右侧详情 -->
|
||||
<main class="mm-main">
|
||||
<FmsLoadingOverlay :loading="configLoading">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 页面头部 -->
|
||||
<div class="mm-header">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="configSaving"
|
||||
:disabled="configLoading || configSyncing"
|
||||
@click="saveAll"
|
||||
>保存</a-button
|
||||
>
|
||||
<a-button
|
||||
v-if="selectedModule.b_module_type === 'data'"
|
||||
:loading="configSyncing"
|
||||
:disabled="syncDisabled"
|
||||
@click="syncFields"
|
||||
>
|
||||
同步字段
|
||||
</a-button>
|
||||
</a-space>
|
||||
<div class="mm-header-center">
|
||||
<a-space size="middle">
|
||||
<span>启用</span>
|
||||
<a-checkbox
|
||||
:checked="drafts.module?.b_canuse === 1"
|
||||
:disabled="busy"
|
||||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-checkbox
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="busy || drafts.module?.b_module_type === 'data'"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
<template v-if="drafts.module?.b_module_type === 'data'">
|
||||
<a-divider type="vertical" />
|
||||
<span>自动编码</span>
|
||||
<a-checkbox
|
||||
:checked="autoCodeEnabled"
|
||||
:disabled="busy"
|
||||
@update:checked="setAutoCodeEnabled"
|
||||
/>
|
||||
</template>
|
||||
</a-space>
|
||||
<a-splitter-panel :default-size="260" :min="210" :max="440" collapsible>
|
||||
<!-- 左侧模块树 -->
|
||||
<aside class="mm-sidebar">
|
||||
<div class="mm-tree-stage">
|
||||
<div v-if="treeLoading" class="mm-tree-empty">
|
||||
<a-spin />
|
||||
</div>
|
||||
<FmsTree
|
||||
v-else
|
||||
:disabled="configSaving"
|
||||
:tree-data="treeData"
|
||||
v-model:expanded-keys="expandedTreeKeys"
|
||||
:selected-keys="selectedModuleId ? [selectedModuleId] : []"
|
||||
:context-menu="buildTreeContextMenu"
|
||||
:context-menu-disabled="busy"
|
||||
@select="onTreeSelect"
|
||||
@menu-click="onTreeMenuClick"
|
||||
>
|
||||
<template #title="node">
|
||||
<span class="mm-tree-node">
|
||||
<span>{{
|
||||
appStore.t(node.b_i18n, node.b_name || node.b_code || node.title)
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
</FmsTree>
|
||||
</div>
|
||||
</aside>
|
||||
</a-splitter-panel>
|
||||
|
||||
<!-- Tab 面板 -->
|
||||
<div class="mm-body">
|
||||
<a-tabs v-model:active-key="activeTab" class="mm-tabs">
|
||||
<a-tab-pane
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.key"
|
||||
:tab="tab.tab"
|
||||
:force-render="tab.key === 'i18n'"
|
||||
>
|
||||
<ModuleBasicPanel
|
||||
v-if="tab.key === 'basic'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:data="drafts.module"
|
||||
:parent-name="parentModuleName"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('module', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleFieldsPanel
|
||||
v-else-if="tab.key === 'fields'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:fields="drafts.fields"
|
||||
:loading="busy"
|
||||
@update="onFieldsUpdate"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleListConfigPanel
|
||||
v-else-if="tab.key === 'list'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:list-config="drafts.listConfig"
|
||||
:edit-config="drafts.editConfig"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('listConfig', v)"
|
||||
@groups-saved="onGroupsSaved"
|
||||
@sync-edit="(v) => onUpdate('editConfig', v)"
|
||||
@sync-query="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleEditConfigPanel
|
||||
v-else-if="tab.key === 'edit'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:edit-config="drafts.editConfig"
|
||||
:list-config="drafts.listConfig"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('editConfig', v)"
|
||||
@groups-saved="onGroupsSaved"
|
||||
@sync-list="(v) => onUpdate('listConfig', v)"
|
||||
@sync-query="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleQueryConfigPanel
|
||||
v-else-if="tab.key === 'query'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleAutoCodePanel
|
||||
v-else-if="tab.key === 'auto-code'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:auto-code="drafts.autoCode"
|
||||
:fields="drafts.fields"
|
||||
:loading="busy"
|
||||
@update="onAutoCodeUpdate"
|
||||
/>
|
||||
<ModulePowersPanel
|
||||
v-else-if="tab.key === 'powers'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:powers="drafts.powers"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('powers', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleI18nPanel
|
||||
v-else-if="tab.key === 'i18n'"
|
||||
:ref="setI18nPanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:i18n-items="i18nItems"
|
||||
:loading="busy"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<a-splitter-panel :min="720">
|
||||
<!-- 右侧详情 -->
|
||||
<main class="mm-main">
|
||||
<FmsLoadingOverlay :loading="configLoading">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 页面头部 -->
|
||||
<div class="mm-header">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="configSaving"
|
||||
:disabled="configLoading || configSyncing"
|
||||
@click="saveAll"
|
||||
>保存</a-button
|
||||
>
|
||||
<a-button
|
||||
v-if="selectedModule.b_module_type === 'data'"
|
||||
:loading="configSyncing"
|
||||
:disabled="syncDisabled"
|
||||
@click="syncFields"
|
||||
>
|
||||
同步字段
|
||||
</a-button>
|
||||
</a-space>
|
||||
<div class="mm-header-center">
|
||||
<a-space size="middle">
|
||||
<span>启用</span>
|
||||
<a-checkbox
|
||||
:checked="drafts.module?.b_canuse === 1"
|
||||
:disabled="busy"
|
||||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-checkbox
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="busy || drafts.module?.b_module_type === 'data'"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
/>
|
||||
<template v-if="drafts.module?.b_module_type === 'data'">
|
||||
<a-divider type="vertical" />
|
||||
<span>自动编码</span>
|
||||
<a-checkbox
|
||||
:checked="autoCodeEnabled"
|
||||
:disabled="busy"
|
||||
@update:checked="setAutoCodeEnabled"
|
||||
/>
|
||||
</template>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未选中模块 -->
|
||||
<div v-else class="mm-empty">
|
||||
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
||||
</div>
|
||||
</FmsLoadingOverlay>
|
||||
</main>
|
||||
</a-splitter-panel>
|
||||
<!-- Tab 面板 -->
|
||||
<div class="mm-body">
|
||||
<a-tabs v-model:active-key="activeTab" class="mm-tabs">
|
||||
<a-tab-pane
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.key"
|
||||
:tab="tab.tab"
|
||||
:force-render="tab.key === 'i18n'"
|
||||
>
|
||||
<ModuleBasicPanel
|
||||
v-if="tab.key === 'basic'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:data="drafts.module"
|
||||
:parent-name="parentModuleName"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('module', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleFieldsPanel
|
||||
v-else-if="tab.key === 'fields'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:fields="drafts.fields"
|
||||
:loading="busy"
|
||||
@update="onFieldsUpdate"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleListConfigPanel
|
||||
v-else-if="tab.key === 'list'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:list-config="drafts.listConfig"
|
||||
:edit-config="drafts.editConfig"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('listConfig', v)"
|
||||
@groups-saved="onGroupsSaved"
|
||||
@sync-edit="(v) => onUpdate('editConfig', v)"
|
||||
@sync-query="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleEditConfigPanel
|
||||
v-else-if="tab.key === 'edit'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:edit-config="drafts.editConfig"
|
||||
:list-config="drafts.listConfig"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:groups="drafts.groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('editConfig', v)"
|
||||
@groups-saved="onGroupsSaved"
|
||||
@sync-list="(v) => onUpdate('listConfig', v)"
|
||||
@sync-query="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleQueryConfigPanel
|
||||
v-else-if="tab.key === 'query'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:query-config="drafts.queryConfig"
|
||||
:fields="drafts.fields"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleAutoCodePanel
|
||||
v-else-if="tab.key === 'auto-code'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:auto-code="drafts.autoCode"
|
||||
:fields="drafts.fields"
|
||||
:loading="busy"
|
||||
@update="onAutoCodeUpdate"
|
||||
/>
|
||||
<ModulePowersPanel
|
||||
v-else-if="tab.key === 'powers'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="selectedModule?.b_code || ''"
|
||||
:powers="drafts.powers"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('powers', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleI18nPanel
|
||||
v-else-if="tab.key === 'i18n'"
|
||||
:ref="setI18nPanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:i18n-items="i18nItems"
|
||||
:loading="busy"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 未选中模块 -->
|
||||
<div v-else class="mm-empty">
|
||||
<a-result status="info" title="请选择模块" sub-title="从左侧树选择一个模块查看详情" />
|
||||
</div>
|
||||
</FmsLoadingOverlay>
|
||||
</main>
|
||||
</a-splitter-panel>
|
||||
</a-splitter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,429 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { Plus, ChevronRight, ChevronDown } from "@lucide/vue";
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
|
||||
const listRef = ref(null);
|
||||
|
||||
// ── Mock data ──
|
||||
const mockUser = {
|
||||
b_id: "1001",
|
||||
b_account: "zhangsan",
|
||||
b_name: "张三",
|
||||
b_phone: "13800138000",
|
||||
b_department: "技术部",
|
||||
b_canuse: "1",
|
||||
b_salesman: "0",
|
||||
b_docman: "0",
|
||||
b_dealman: "1",
|
||||
b_othermachinelogin: "0",
|
||||
b_visible_customer: "1",
|
||||
b_ismanager: "0",
|
||||
b_purview: "1",
|
||||
b_spview: "0",
|
||||
b_subcompany: "",
|
||||
b_inputuser: "admin",
|
||||
b_inputdatetime: "2025-01-15 10:30:00",
|
||||
};
|
||||
|
||||
// 模块+权限合并的树数据:每个模块挂载自己的 power 列表
|
||||
const mockModuleTree = [
|
||||
{
|
||||
b_id: "g1",
|
||||
b_name: "基础资料",
|
||||
children: [
|
||||
{
|
||||
b_id: "m11", b_code: "m_customer", b_name: "客户管理",
|
||||
powers: ["查看", "新增", "修改", "删除", "导出"],
|
||||
},
|
||||
{
|
||||
b_id: "m12", b_code: "m_supplier", b_name: "供应商管理",
|
||||
powers: ["查看", "新增", "修改", "删除"],
|
||||
},
|
||||
{
|
||||
b_id: "m13", b_code: "m_product", b_name: "产品管理",
|
||||
powers: ["查看", "新增", "修改"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b_id: "g2",
|
||||
b_name: "业务操作",
|
||||
children: [
|
||||
{
|
||||
b_id: "m21", b_code: "m_order", b_name: "订单管理",
|
||||
powers: ["查看", "新增", "修改", "删除", "导出", "审核"],
|
||||
},
|
||||
{
|
||||
b_id: "m22", b_code: "m_ship", b_name: "发货管理",
|
||||
powers: ["查看", "新增", "发货", "确认"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b_id: "g3",
|
||||
b_name: "报表与分析",
|
||||
children: [
|
||||
{
|
||||
b_id: "m31", b_code: "m_sales_report", b_name: "销售报表",
|
||||
powers: ["查看", "导出"],
|
||||
},
|
||||
{
|
||||
b_id: "m32", b_code: "m_stock_report", b_name: "库存报表",
|
||||
powers: ["查看"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 该用户当前的全部权限状态:模块授权 + 每个模块的操作权限
|
||||
// key = moduleId, value = Set of power names(含 "module" 表示模块已授权)
|
||||
const mockUserGrants = {
|
||||
m11: new Set(["module", "查看", "新增", "修改"]),
|
||||
m13: new Set(["module", "查看"]),
|
||||
m21: new Set(["module", "查看", "新增", "修改", "审核"]),
|
||||
};
|
||||
|
||||
// ── Modal state ──
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("新增用户");
|
||||
const saving = ref(false);
|
||||
const activeTab = ref("basic");
|
||||
|
||||
// Form data
|
||||
const formData = reactive({
|
||||
b_account: "",
|
||||
b_name: "",
|
||||
b_phone: "",
|
||||
b_department: "",
|
||||
b_canuse: true,
|
||||
b_salesman: false,
|
||||
b_docman: false,
|
||||
b_dealman: false,
|
||||
b_othermachinelogin: false,
|
||||
b_visible_customer: false,
|
||||
b_ismanager: false,
|
||||
b_purview: true,
|
||||
b_spview: false,
|
||||
});
|
||||
const password = ref("");
|
||||
const passwordConfirm = ref("");
|
||||
|
||||
// 合并后的权限状态:{ [moduleId]: Set<powerName> },"module" 表示已授权该模块
|
||||
const moduleGrants = ref({});
|
||||
|
||||
// 左侧菜单树:当前选中模块及分组展开状态
|
||||
const selectedModId = ref(null);
|
||||
const collapsedGroups = ref(new Set(["g3"])); // 默认折叠"报表与分析"
|
||||
const selectedModule = computed(() => {
|
||||
if (!selectedModId.value) return null;
|
||||
for (const group of mockModuleTree) {
|
||||
const found = group.children.find((m) => m.b_id === selectedModId.value);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
function selectModule(modId) {
|
||||
selectedModId.value = modId;
|
||||
}
|
||||
function toggleGroupExpand(gid) {
|
||||
const next = new Set(collapsedGroups.value);
|
||||
if (next.has(gid)) next.delete(gid);
|
||||
else next.add(gid);
|
||||
collapsedGroups.value = next;
|
||||
}
|
||||
|
||||
function isModuleGranted(modId) {
|
||||
return moduleGrants.value[modId]?.has("module") ?? false;
|
||||
}
|
||||
function isPowerGranted(modId, powerName) {
|
||||
return moduleGrants.value[modId]?.has(powerName) ?? false;
|
||||
}
|
||||
function toggleModuleGrant(modId) {
|
||||
const next = { ...moduleGrants.value };
|
||||
if (isModuleGranted(modId)) {
|
||||
// 取消模块授权 → 清除该模块所有权限
|
||||
delete next[modId];
|
||||
} else {
|
||||
next[modId] = new Set(["module"]);
|
||||
}
|
||||
moduleGrants.value = next;
|
||||
}
|
||||
function togglePower(modId, powerName) {
|
||||
const next = { ...moduleGrants.value };
|
||||
const current = next[modId];
|
||||
if (!current) return;
|
||||
const nextSet = new Set(current);
|
||||
if (nextSet.has(powerName)) nextSet.delete(powerName);
|
||||
else nextSet.add(powerName);
|
||||
next[modId] = nextSet;
|
||||
moduleGrants.value = next;
|
||||
}
|
||||
function toggleAllPowers(mod) {
|
||||
const modId = mod.b_id;
|
||||
const current = moduleGrants.value[modId];
|
||||
if (!current) return;
|
||||
const next = { ...moduleGrants.value };
|
||||
const nextSet = new Set(current);
|
||||
const allGranted = mod.powers.every((p) => nextSet.has(p));
|
||||
if (allGranted) {
|
||||
for (const p of mod.powers) nextSet.delete(p);
|
||||
} else {
|
||||
for (const p of mod.powers) nextSet.add(p);
|
||||
}
|
||||
next[modId] = nextSet;
|
||||
moduleGrants.value = next;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function resetForm() {
|
||||
formData.b_account = "";
|
||||
formData.b_name = "";
|
||||
formData.b_phone = "";
|
||||
formData.b_department = "";
|
||||
formData.b_canuse = true;
|
||||
formData.b_salesman = false;
|
||||
formData.b_docman = false;
|
||||
formData.b_dealman = false;
|
||||
formData.b_othermachinelogin = false;
|
||||
formData.b_visible_customer = false;
|
||||
formData.b_ismanager = false;
|
||||
formData.b_purview = true;
|
||||
formData.b_spview = false;
|
||||
password.value = "";
|
||||
passwordConfirm.value = "";
|
||||
moduleGrants.value = {};
|
||||
selectedModId.value = null;
|
||||
collapsedGroups.value = new Set(["g3"]);
|
||||
}
|
||||
|
||||
function fillEditForm(user) {
|
||||
formData.b_account = user.b_account || "";
|
||||
formData.b_name = user.b_name || "";
|
||||
formData.b_phone = user.b_phone || "";
|
||||
formData.b_department = user.b_department || "";
|
||||
formData.b_canuse = user.b_canuse === "1" || user.b_canuse === 1;
|
||||
formData.b_salesman = user.b_salesman === "1" || user.b_salesman === 1;
|
||||
formData.b_docman = user.b_docman === "1" || user.b_docman === 1;
|
||||
formData.b_dealman = user.b_dealman === "1" || user.b_dealman === 1;
|
||||
formData.b_othermachinelogin = user.b_othermachinelogin === "1" || user.b_othermachinelogin === 1;
|
||||
formData.b_visible_customer = user.b_visible_customer === "1" || user.b_visible_customer === 1;
|
||||
formData.b_ismanager = user.b_ismanager === "1" || user.b_ismanager === 1;
|
||||
formData.b_purview = user.b_purview === "1" || user.b_purview === 1;
|
||||
formData.b_spview = user.b_spview === "1" || user.b_spview === 1;
|
||||
password.value = "";
|
||||
passwordConfirm.value = "";
|
||||
}
|
||||
|
||||
// ── Handlers ──
|
||||
function handleAdd() {
|
||||
modalTitle.value = "新增用户";
|
||||
activeTab.value = "basic";
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
modalTitle.value = `编辑用户 - ${row.b_name || row.b_account || ""}`;
|
||||
activeTab.value = "basic";
|
||||
fillEditForm(mockUser);
|
||||
// 深拷贝 mock user grants
|
||||
const grants = {};
|
||||
for (const [modId, powers] of Object.entries(mockUserGrants)) {
|
||||
grants[modId] = new Set(powers);
|
||||
}
|
||||
moduleGrants.value = grants;
|
||||
selectedModId.value = mockModuleTree[0].children[0].b_id;
|
||||
collapsedGroups.value = new Set();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
const rowActions = [
|
||||
{ key: "edit", type: "action", label: "编辑", onClick: handleEdit },
|
||||
];
|
||||
|
||||
// Save (demo only)
|
||||
async function handleSave() {
|
||||
saving.value = true;
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
saving.value = false;
|
||||
modalVisible.value = false;
|
||||
message.success("(Demo)保存成功 — 未真实调用接口");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-page">
|
||||
<FmsModuleListPage data-code="b_user" />
|
||||
<FmsModuleListPage
|
||||
ref="listRef"
|
||||
data-code="b_user"
|
||||
:row-actions="rowActions"
|
||||
@row-action="({ record }) => { if (record?.action?.key === 'edit') handleEdit(record.record) }"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<Plus :size="15" />
|
||||
新增用户
|
||||
</a-button>
|
||||
</template>
|
||||
</FmsModuleListPage>
|
||||
|
||||
<!-- Demo Edit Modal -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:width="1000"
|
||||
:confirm-loading="saving"
|
||||
:body-style="{ padding: '0 24px 16px', maxHeight: '70vh', overflow: 'auto' }"
|
||||
@ok="handleSave"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-tabs v-model:active-key="activeTab">
|
||||
<!-- Tab 1: 基本信息(含密码) -->
|
||||
<a-tab-pane key="basic" tab="基本信息">
|
||||
<div class="demo-basic-info">
|
||||
<a-row :gutter="[24, 16]">
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">登录账号 <span class="demo-required">*</span></label>
|
||||
<a-input v-model:value="formData.b_account" placeholder="请输入登录账号" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">用户姓名 <span class="demo-required">*</span></label>
|
||||
<a-input v-model:value="formData.b_name" placeholder="请输入用户姓名" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">手机号</label>
|
||||
<a-input v-model:value="formData.b_phone" placeholder="请输入手机号" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">部门</label>
|
||||
<a-input v-model:value="formData.b_department" placeholder="请输入部门" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">新密码</label>
|
||||
<a-input-password v-model:value="password" placeholder="留空则不修改" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<label class="demo-field-label">确认密码</label>
|
||||
<a-input-password v-model:value="passwordConfirm" placeholder="请再次输入" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 标志位 -->
|
||||
<div class="demo-flags-card">
|
||||
<div class="demo-card-title">标志位</div>
|
||||
<a-row :gutter="[16, 12]">
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_canuse">启用</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_salesman">销售员</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_docman">单据主管</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_dealman">可处理事务</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_othermachinelogin">允许异地登录</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_visible_customer">可见客户</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_ismanager">管理员</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_purview">操作权限</a-checkbox>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-checkbox v-model:checked="formData.b_spview">审批权限</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Tab 2: 菜单权限(左树右操作列表) -->
|
||||
<a-tab-pane key="module-power" tab="菜单权限">
|
||||
<div class="perm-layout">
|
||||
<!-- 左侧:菜单树 -->
|
||||
<div class="perm-tree">
|
||||
<div v-for="group in mockModuleTree" :key="group.b_id" class="tree-group">
|
||||
<div class="tree-group-hd" @click="toggleGroupExpand(group.b_id)">
|
||||
<span class="tree-group-ic">
|
||||
<ChevronRight v-if="collapsedGroups.has(group.b_id)" :size="14" />
|
||||
<ChevronDown v-else :size="14" />
|
||||
</span>
|
||||
<span class="tree-group-label">{{ group.b_name }}</span>
|
||||
<span class="tree-group-count">{{ group.children.length }}</span>
|
||||
</div>
|
||||
<div v-show="!collapsedGroups.has(group.b_id)" class="tree-group-body">
|
||||
<div
|
||||
v-for="mod in group.children"
|
||||
:key="mod.b_id"
|
||||
class="tree-item"
|
||||
:class="{ 'is-active': selectedModId === mod.b_id }"
|
||||
@click="selectModule(mod.b_id)"
|
||||
>
|
||||
<a-checkbox
|
||||
:checked="isModuleGranted(mod.b_id)"
|
||||
@click.stop
|
||||
@change="toggleModuleGrant(mod.b_id)"
|
||||
/>
|
||||
<span class="tree-item-name">{{ mod.b_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:操作权限列表 -->
|
||||
<div class="perm-detail">
|
||||
<template v-if="selectedModule">
|
||||
<div class="perm-detail-hd">
|
||||
<span class="perm-detail-title">{{ selectedModule.b_name }}</span>
|
||||
<span class="perm-detail-sub">{{ selectedModule.b_code }}</span>
|
||||
</div>
|
||||
<div class="perm-detail-body">
|
||||
<template v-if="isModuleGranted(selectedModule.b_id)">
|
||||
<label class="perm-detail-all">
|
||||
<a-checkbox
|
||||
:checked="selectedModule.powers.every((p) => isPowerGranted(selectedModule.b_id, p))"
|
||||
:indeterminate="selectedModule.powers.some((p) => isPowerGranted(selectedModule.b_id, p)) && !selectedModule.powers.every((p) => isPowerGranted(selectedModule.b_id, p))"
|
||||
@change="toggleAllPowers(selectedModule)"
|
||||
/>
|
||||
<span>全选操作权限</span>
|
||||
</label>
|
||||
<div class="perm-pow-list">
|
||||
<label
|
||||
v-for="pow in selectedModule.powers"
|
||||
:key="pow"
|
||||
class="perm-pow-item"
|
||||
>
|
||||
<a-checkbox
|
||||
:checked="isPowerGranted(selectedModule.b_id, pow)"
|
||||
@change="togglePower(selectedModule.b_id, pow)"
|
||||
/>
|
||||
<span>{{ pow }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="perm-detail-tip">
|
||||
勾选左侧「{{ selectedModule.b_name }}」以授权访问,再配置具体操作权限
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="perm-detail-empty">
|
||||
请选择左侧菜单
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -22,4 +441,208 @@ import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vu
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
// ── Modal internal styles ──
|
||||
.demo-basic-info {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.demo-field-label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.demo-required {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.demo-flags-card {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.demo-card-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// ── Menu permission: left tree + right list ──
|
||||
.perm-layout {
|
||||
display: flex;
|
||||
height: 440px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 左侧:菜单树
|
||||
.perm-tree {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
padding: 6px 0;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
overflow-y: auto;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.tree-group-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f1f3;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-group-ic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tree-group-label {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.tree-group-count {
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.tree-group-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tree-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px 8px 30px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:hover {
|
||||
background: #ebedf0;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #e6f0ff;
|
||||
|
||||
&:hover {
|
||||
background: #dcebff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tree-item-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// 右侧:操作权限列表
|
||||
.perm-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.perm-detail-hd {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.perm-detail-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.perm-detail-sub {
|
||||
font-size: 12px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.perm-detail-body {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.perm-detail-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 6px;
|
||||
background: #f6f7f9;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.perm-pow-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.perm-pow-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 11px 12px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fafbfc;
|
||||
}
|
||||
}
|
||||
|
||||
.perm-detail-tip {
|
||||
margin-top: 8px;
|
||||
padding: 36px 16px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
background: #fafbfc;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.perm-detail-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ import vue from "@vitejs/plugin-vue";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { AntdvNextResolver } from "@antdv-next/auto-import-resolver";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import { fileViewerRenderers } from "@file-viewer/vite-plugin";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -16,6 +17,15 @@ export default defineConfig({
|
||||
host: "127.0.0.1",
|
||||
port: 5180,
|
||||
strictPort: true,
|
||||
// 排除静态资源目录的监听:full 包复制出的 WASM/字体体积大,
|
||||
// 在 Windows 下易被锁定导致 Vite 监听报 EBUSY 崩溃;public 下资源按需直接服务,无需监听
|
||||
watch: {
|
||||
ignored: [
|
||||
"**/node_modules/**",
|
||||
"**/.git/**",
|
||||
"**/public/file-viewer/**",
|
||||
],
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:8088",
|
||||
@@ -29,5 +39,6 @@ export default defineConfig({
|
||||
Components({
|
||||
resolvers: [AntdvNextResolver()],
|
||||
}),
|
||||
fileViewerRenderers({ copyAssets: true }),
|
||||
],
|
||||
});
|
||||
+4
-1
@@ -29,11 +29,14 @@
|
||||
- [NEVER] 修改通用接口的 URL、参数、返回值结构或处理逻辑。
|
||||
- [MUST] 如通用接口确实无法满足需求:先说明原因 + 拟修改内容 + 影响范围,取得用户确认后再实施。
|
||||
- [MUST] `bigint` 类型(如 `b_id`)序列化到前端时必须转为**字符串**。
|
||||
- [MUST] 通用查询默认**不传 `search_columns` / `searchColumns`**;除非明确需要限制返回字段且已确认字段存在,否则采用接口默认返回。
|
||||
- [MUST] `config/dbconfigs` 中的旧库仅用于参考表结构、历史数据和差异排查;除非用户明确指定,不修改旧库结构或数据。
|
||||
|
||||
## 4. 前端实现
|
||||
|
||||
- [MUST] 表格使用项目封装的 **`fms-table`** 组件,不从零实现表格。
|
||||
- [MUST] 表单输入控件**不添加 `placeholder`**,用 `label` 或说明文字表达意图。
|
||||
- [MUST] 表单输入控件**不添加 `placeholder`**,`label` 只写字段名称,不追加“留空则...”这类操作提示信息。
|
||||
- [MUST] 使用 **`FmsTree`** 时不设置 `root-title`,根节点标题由组件内部统一维护。
|
||||
- [MUST] 优先复用项目现有组件和 Antdv Next 组件能力,不重复造轮子。
|
||||
- [SHOULD] 页面中出现 ≥2 处相同/相近功能时,提取为可复用组件、composable 或 service 方法。
|
||||
- [SHOULD] 抽取复用以确有收益为前提,不为简单逻辑增加无意义的封装。
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# FMS 文件管理组件设计方案(三栏布局)
|
||||
|
||||
> 状态:待执行(计划稿)
|
||||
> 目标:参考旧系统 `g3file`,为新 FMS 系统设计可复用的文件管理组件。
|
||||
> 布局:左 = 分类树,中 = 文件列表 + 操作区,右 = 预览区。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与新旧差异
|
||||
|
||||
旧系统 `g3file` 是分两层的组件:
|
||||
- 参数化通用库 `file-preview/g3-file`(基于 Element Plus + 阿里云 OSS),支持 `modelValue`、`module-id`、`father`、`cate-id`、`readonly`、`search-condition`、`upload-config.aliyun`、`preview-url`、`global-permission`、`iconfont` 及多种回调。
|
||||
- 业务包装层 `g3hd/frontend/src/components/g3-file/index.vue`,业务页只用一行调用:`<G3File v-model="showFile" :father="id" module-id="oa_notices" />`。
|
||||
|
||||
新 FMS 的差异与决策:
|
||||
|
||||
| 维度 | 旧系统 | 新系统决策 |
|
||||
|------|--------|-----------|
|
||||
| 框架 | Vue3 + Element Plus | Vue3 + **antdv-next**(自动导入,勿手导) |
|
||||
| 上传存储 | 阿里云 OSS | 后端**本地磁盘**,文件名 `{subid}.{ext}`;预留 `OssStorageAdapter` 抽象便于将来切换 |
|
||||
| moduleid | 存字符串业务 code(如 `oa_notices`) | `bf_files.mx_moduleid` 为 **bigint 外键 → `s_module.b_id`** |
|
||||
| 预览 | 自研/第三方 | 复用已安装的 `@file-viewer/vue3-full`(`FileViewer`,dashboard 已验证 `withFullViewerOptions()`) |
|
||||
| 调用方式 | `module-id` 字符串 | **`module-code`**(如 `oa_notices`);组件内解析为 `b_id` 写入 `mx_moduleid` |
|
||||
|
||||
### 关于 moduleid 的关键决策
|
||||
- **保留 `mx_moduleid` 字段**,但它是**派生字段**,由组件根据 `module-code` 查 `s_module.b_id` 后写入。
|
||||
- 业务页**不单独传 moduleid**,只传 `module-code` 与 `father`。组件内部完成 code → b_id 解析并随上传/保存一并提交。
|
||||
- 说明:旧系统把字符串 code 直接当 moduleid 存,新系统 `mx_moduleid` 是指向 `s_module.b_id` 的 bigint 外键,二者语义不同;用 `module-code` 解析可兼容旧习惯又满足新外键约束。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库现状(已存在,无需新建表)
|
||||
|
||||
表 `bf_files` / `bf_files_cateid` / `bf_files_type` 已在迁移 `017_create_contract_file_tables.sql` 定义,字段如下(节选相关项):
|
||||
|
||||
`bf_files`:
|
||||
- `subid` bigint(主键,Snowflake ID)
|
||||
- `father` bigint(业务单据 ID,如合同/通知 b_id)
|
||||
- `mx_xh` int(显示序号)
|
||||
- `mx_filename` nvarchar(500)(原始文件名)
|
||||
- `mx_filesize` varchar(50)
|
||||
- `mx_fileext` varchar(50)
|
||||
- `mx_mapfilename` nvarchar(500)(**新系统存服务端存储文件名,如 `1234567890.pdf`,不再存阿里云 URL**)
|
||||
- `mx_moduleid` bigint(FK → `s_module.b_id`,派生字段)
|
||||
- `mx_cutfilename`、`mx_cutfilesize`(缩略图,可选)
|
||||
- `mx_cate_id` bigint(→ `bf_files_cateid.b_id`,分类)
|
||||
- `mx_bz` nvarchar(500)
|
||||
- `mx_orderid`、`mx_sh`、`mx_shdatetime`、`mx_shuser_id`
|
||||
- `mx_inputuser_id`、`mx_inputdatetime`、`mx_updateuser_id`、`mx_updatedatetime`
|
||||
|
||||
索引:`IX_bf_files_father`、`IX_bf_files_mx_cate_id`、`IX_bf_files_mx_shuser_id`。
|
||||
|
||||
`bf_files_cateid`:`b_id`、`b_name`、`b_xh`、`b_bz`、`b_canuse`、`b_files_type`、`b_code`(唯一)等。
|
||||
`bf_files_type`:`b_id`、`b_name`、`b_module_id`、`b_module_name`、`b_xh` 等。
|
||||
|
||||
> 注意:bigint ID(subid / father / mx_moduleid / mx_cate_id)经后端返回前端时**必须序列化为字符串**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端新增文件端点(按开发规范例外处理)
|
||||
|
||||
通用端点(`/api/data/...`)无法处理二进制流与 multipart 上传,因此按规范例外新增**与业务表无关的 3 个通用文件端点**。需在新增前于回复中说明原因(已在本文 §1 论证)。
|
||||
|
||||
### 3.1 `POST /api/file/upload`
|
||||
- 请求:`multipart/form-data`
|
||||
- `file`(必填,二进制)
|
||||
- `father`(必填,bigint 字符串,业务单据 ID)
|
||||
- `moduleCode`(必填,如 `oa_notices`)
|
||||
- `cateId`(可选,bigint 字符串 → `mx_cate_id`)
|
||||
- `bz`(可选)
|
||||
- 处理流程:
|
||||
1. 校验 JWT(过 `JwtAuthFilter`),取 `OrgContext.orgId` 选库。
|
||||
2. 用 `IdGenerator` 生成 `subid`(Snowflake)。
|
||||
3. 解析 `moduleCode` → `s_module.b_id`(查 `s_module` 表;查不到则报错 400)。
|
||||
4. 计算扩展名 `ext`、存储名 `{subid}.{ext}`,写字节到 `fms.file.storage-dir`。
|
||||
5. **只写磁盘文件,不写 `bf_files` 行**(行由前端用 `saveObjectApi` 批量保存,便于统一事务/权限)。
|
||||
- 响应(JSON):
|
||||
```json
|
||||
{
|
||||
"subid": "1234567890",
|
||||
"storedName": "1234567890.pdf",
|
||||
"mx_filename": "合同.pdf",
|
||||
"mx_filesize": "102400",
|
||||
"mx_fileext": "pdf",
|
||||
"mx_mapfilename": "1234567890.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 `GET /api/file/{subid}`
|
||||
- 查 `bf_files` 取 `mx_mapfilename`,从 `storage-dir` 流式返回字节。
|
||||
- 响应头:`Content-Type`(按扩展名映射)、`Content-Disposition: inline; filename*=UTF-8''<mx_filename>`(inline 便于浏览器/pdf 预览)。
|
||||
- 需过 `JwtAuthFilter`(带 Token 才能取)。
|
||||
|
||||
### 3.3 `DELETE /api/file/{subid}`
|
||||
- 删磁盘文件 + 删 `bf_files` 行(业务页也可改为走 `saveObjectApi` 的 `deletes[]`,二选一;推荐端点方式以便同时清理磁盘)。
|
||||
- 返回 `{ success: true }`。
|
||||
|
||||
### 3.4 配置
|
||||
`fms-api/src/main/resources/application.yaml` 新增:
|
||||
```yaml
|
||||
fms:
|
||||
file:
|
||||
storage-dir: ./data/files # 绝对或相对(相对应用工作目录),需 git-ignore
|
||||
```
|
||||
`storage-dir` 目录加入 `.gitignore`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端组件设计(三栏布局)
|
||||
|
||||
目录:`fms-vue/src/components/file/`
|
||||
|
||||
```
|
||||
FileManager.vue 三栏容器(左树 / 中列表操作 / 右预览)
|
||||
FileCategoryTree.vue 左栏:分类树(虚拟根“全部附件” + bf_files_cateid 节点)
|
||||
FileList.vue 中栏:文件列表 + 工具栏(上传/下载/删除/刷新)
|
||||
FileUpload.vue a-upload 包装(调用 fileService.upload)
|
||||
```
|
||||
|
||||
复用:`@file-viewer/vue3-full` 的 `FileViewer` 用于右栏预览;`antdv-next` 的 `a-upload` / `a-table` / `a-tree` / `a-button` 等(自动导入)。
|
||||
服务层:`fms-vue/src/services/fileService.js`。
|
||||
|
||||
### 4.1 FileManager.vue(容器)
|
||||
- 三栏 flex 布局:左 `FileCategoryTree`(宽 ~220px,受 `showTree` 控制),中 `FileList`(flex:1),右预览区(宽 ~40% 或固定 ~480px,仅当选中文件时显示)。
|
||||
- 选中文件(来自 `FileList` 的 `select` 事件)→ 右栏渲染 `FileViewer` 并传入预览 URL(`fileService.getContentUrl(subid)`)。
|
||||
- 负责 `moduleCode` → `moduleId`(b_id) 解析:挂载时调 `loadModuleConfiguration` / 通用 `loaddata` 查 `s_module` 得到 `b_id`,下传 `FileList` 备用(上传时带回)。
|
||||
- 工具栏按钮(上传/下载/删除)按 `moduleCode` 查 `b_user_power` 控制显隐/禁用(前端权限,沿用现有 `permissions` store 模式)。
|
||||
|
||||
**Props**
|
||||
| 名称 | 类型 | 必填 | 默认 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| `modelValue` | Boolean | 是 | — | 控制显隐(v-model) |
|
||||
| `moduleCode` | String | 是 | — | 模块 code,如 `oa_notices` |
|
||||
| `father` | [String, Number] | 是 | — | 业务单据 ID(bigint 字符串) |
|
||||
| `moduleId` | [String, Number] | 否 | — | 已解析的 `s_module.b_id`;不传则组件内解析 |
|
||||
| `cateId` | [String, Number] | 否 | — | 初始分类过滤 |
|
||||
| `categories` | Array | 否 | — | 预置分类列表,避免请求树 |
|
||||
| `readonly` | Boolean | 否 | false | 只读(隐藏上传/删除) |
|
||||
| `showTree` | Boolean | 否 | true | 是否显示左栏分类树 |
|
||||
|
||||
**Events**
|
||||
| 名称 | 载荷 | 说明 |
|
||||
|------|------|------|
|
||||
| `update:modelValue` | Boolean | v-model |
|
||||
| `uploaded` | fileRow | 上传完成(含回写 bf_files 后的行) |
|
||||
| `deleted` | subid | 删除完成 |
|
||||
| `selected` | fileRow \| null | 选中文件变化(驱动右栏预览) |
|
||||
| `error` | Error | 异常 |
|
||||
|
||||
### 4.2 FileCategoryTree.vue(左栏)
|
||||
- 虚拟根节点“全部附件”(值为 `null` 或 `'__all__'`),下挂 `bf_files_cateid` 节点(来自 `loaddata` 查 `bf_files_cateid`,按 `b_xh` 排序)。
|
||||
- 选中分类 emit `select(cateId)`,供 `FileList` 过滤。
|
||||
- 若 `categories` prop 已提供则直接用,否则自行加载。
|
||||
|
||||
### 4.3 FileList.vue(中栏)
|
||||
- `a-table` 展示当前 `father` +(可选)`cateId` 下的 `bf_files` 行(走 `pageDataApi` / `loadDataApi`,`view_name='bf_files'`)。
|
||||
- 工具栏:`FileUpload`(上传)、下载、删除、刷新。
|
||||
- 行选中 → emit `select(row)` → 容器驱动右栏。
|
||||
- 上传完成后调用 `saveObjectApi` 写 `bf_files` 行(inserts: `[{table:'bf_files', key_field:'subid', ...}]`,带 `father`/`mx_moduleid`/`mx_filename`/`mx_filesize`/`mx_fileext`/`mx_mapfilename`/`mx_cate_id`)。
|
||||
- 删除:调 `fileService.remove(subid)`(或 `saveObjectApi` deletes + 端点删磁盘)。
|
||||
|
||||
### 4.4 FileUpload.vue
|
||||
- 包装 `a-upload`(`:before-upload` 拦截,手动调 `fileService.upload`)。
|
||||
- 入参:`father`、`moduleCode`、`moduleId`、`cateId`;上传成功后把响应行 emit 给 `FileList` 继续写元数据。
|
||||
|
||||
### 4.5 fileService.js
|
||||
封装以下方法(基于 `src/services/http.js`):
|
||||
- `upload({file, father, moduleCode, cateId, bz})` → `POST /api/file/upload`(multipart)
|
||||
- `getContentUrl(subid)` → 返回 `GET /api/file/{subid}` 的可访问 URL(需带 Token;若 axios 拦截器不自动带,则改用带 Token 的 `fetch`/blob 下载后 `URL.createObjectURL` 喂给 `FileViewer`)
|
||||
- `remove(subid)` → `DELETE /api/file/{subid}`
|
||||
- `listFiles({father, cateId})` → `pageDataApi`(`bf_files`)
|
||||
- `saveMeta(rows)` → `saveObjectApi`
|
||||
|
||||
> 预览 URL 处理:因 `FileViewer` 需要可直接访问的资源地址,而 `/api/file/{subid}` 需鉴权,推荐在 `fileService` 内用带 `Authorization` 的 `fetch` 取 blob 再 `createObjectURL`,避免 Token 泄漏到 URL。
|
||||
|
||||
---
|
||||
|
||||
## 5. 业务页调用示例
|
||||
|
||||
```vue
|
||||
<!-- 业务页(如通知管理) -->
|
||||
<FileManager
|
||||
v-model="showFile"
|
||||
module-code="oa_notices"
|
||||
:father="rowId"
|
||||
/>
|
||||
```
|
||||
- 业务页**只需传 `module-code` 与 `father`**,不再传 moduleid。
|
||||
- 只读场景:`readonly` + `show-tree` 可组合使用。
|
||||
|
||||
---
|
||||
|
||||
## 6. 落地步骤清单(执行顺序)
|
||||
|
||||
1. **后端**
|
||||
- [ ] `application.yaml` 增加 `fms.file.storage-dir`,并把该目录加入 `.gitignore`。
|
||||
- [ ] 新建 `FileController`(或并入现有 controller 包),实现 `upload` / `GET {subid}` / `DELETE {subid}` 三个端点,复用 `OrgContext` + `DbUtils` + `IdGenerator`。
|
||||
- [ ] 实现本地磁盘存储(`StorageService`,预留 `OssStorageAdapter` 接口)。
|
||||
- [ ] `moduleCode → b_id` 解析逻辑(查 `s_module`)。
|
||||
2. **前端服务**
|
||||
- [ ] `src/services/fileService.js` 五个方法。
|
||||
3. **前端组件**
|
||||
- [ ] `FileCategoryTree.vue`(左树)
|
||||
- [ ] `FileList.vue`(中栏列表+操作,含写元数据/删除)
|
||||
- [ ] `FileUpload.vue`(a-upload 包装)
|
||||
- [ ] `FileManager.vue`(三栏容器 + 权限 + moduleCode 解析 + 右栏预览)
|
||||
4. **集成验证**
|
||||
- [ ] 选一个业务页(如通知 `oa_notices`)挂载 `<FileManager>`,验证上传/列表/分类过滤/预览/删除全链路。
|
||||
- [ ] 确认 bigint ID 前端为字符串、预览 URL 鉴权正常。
|
||||
|
||||
---
|
||||
|
||||
## 7. 注意事项(沿用开发规范)
|
||||
- 不新增业务专属数据端点(文件端点属通用基础设施例外,已在 §1 论证)。
|
||||
- 不手动 `import` antdv-next 组件(vite resolver 已自动导入)。
|
||||
- 不引入新 vue-i18n;文案沿用 DB 驱动 i18n(必要时走 `b_i18n`)。
|
||||
- 改动保持聚焦,不触碰无关文件;保留用户未提交的工作。
|
||||
- 交付时说明是否运行了测试、是否执行了 build(默认不主动 build/起服务,除非用户要求)。
|
||||
Reference in new issue
Block a user