20260729173317

This commit is contained in:
oneao committed 2026-07-29 17:33:21 +08:00
1 parent f6b79ea09a
commit 1c839b707a
26 files changed
+1902 -29

No files matched your search

@@ -80,6 +80,24 @@ GO
);
CREATE INDEX IX_s_module_field_module ON dbo.s_module_field (b_module_id, b_canuse, b_id);
CREATE TABLE dbo.s_module_auto_code (
b_id bigint NOT NULL,
b_module_id bigint NOT NULL,
b_field_id bigint NOT NULL,
b_prefix nvarchar(100) NOT NULL CONSTRAINT DF_s_module_auto_code_prefix DEFAULT (N''),
b_date_format varchar(30) NOT NULL CONSTRAINT DF_s_module_auto_code_date_format DEFAULT ('yyyyMM'),
b_separator nvarchar(10) NOT NULL CONSTRAINT DF_s_module_auto_code_separator DEFAULT (N'-'),
b_sequence_width int NOT NULL CONSTRAINT DF_s_module_auto_code_sequence_width DEFAULT (5),
b_reset_type varchar(10) NOT NULL CONSTRAINT DF_s_module_auto_code_reset_type DEFAULT ('month'),
b_start_value bigint NOT NULL CONSTRAINT DF_s_module_auto_code_start_value DEFAULT (1),
b_current_period varchar(8) NULL,
b_current_value bigint NOT NULL CONSTRAINT DF_s_module_auto_code_current_value DEFAULT (0),
b_canuse int NOT NULL CONSTRAINT DF_s_module_auto_code_canuse DEFAULT (1),
CONSTRAINT PK_s_module_auto_code PRIMARY KEY (b_id),
CONSTRAINT UQ_s_module_auto_code_module UNIQUE (b_module_id)
);
CREATE INDEX IX_s_module_auto_code_field ON dbo.s_module_auto_code (b_field_id);
CREATE TABLE dbo.s_module_field_group (
b_id bigint NOT NULL,
b_module_id bigint NOT NULL,
@@ -113,6 +131,7 @@ GO
b_required int NOT NULL CONSTRAINT DF_s_module_field_edit_required DEFAULT (0),
b_readonly int NOT NULL CONSTRAINT DF_s_module_field_edit_readonly DEFAULT (0),
b_disabled int NOT NULL CONSTRAINT DF_s_module_field_edit_disabled DEFAULT (0),
b_visible int NOT NULL CONSTRAINT DF_s_module_field_edit_visible DEFAULT (1),
b_group_id bigint NULL,
b_xh int NOT NULL CONSTRAINT DF_s_module_field_edit_xh DEFAULT (0),
CONSTRAINT PK_s_module_field_edit PRIMARY KEY (b_id),
@@ -0,0 +1,14 @@
/* 为编辑配置增加显示开关,存量配置默认显示。 */
SET NOCOUNT ON;
IF NOT EXISTS (
SELECT 1
FROM sys.columns
WHERE object_id = OBJECT_ID(N'dbo.s_module_field_edit')
AND name = N'b_visible'
)
BEGIN
ALTER TABLE dbo.s_module_field_edit
ADD b_visible int NOT NULL
CONSTRAINT DF_s_module_field_edit_visible DEFAULT (1) WITH VALUES;
END;
@@ -0,0 +1,25 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF OBJECT_ID(N'dbo.s_module_auto_code', N'U') IS NULL
BEGIN
CREATE TABLE dbo.s_module_auto_code (
b_id bigint NOT NULL,
b_module_id bigint NOT NULL,
b_field_id bigint NOT NULL,
b_prefix nvarchar(100) NOT NULL CONSTRAINT DF_s_module_auto_code_prefix DEFAULT (N''),
b_date_format varchar(30) NOT NULL CONSTRAINT DF_s_module_auto_code_date_format DEFAULT ('yyyyMM'),
b_separator nvarchar(10) NOT NULL CONSTRAINT DF_s_module_auto_code_separator DEFAULT (N'-'),
b_sequence_width int NOT NULL CONSTRAINT DF_s_module_auto_code_sequence_width DEFAULT (5),
b_reset_type varchar(10) NOT NULL CONSTRAINT DF_s_module_auto_code_reset_type DEFAULT ('month'),
b_start_value bigint NOT NULL CONSTRAINT DF_s_module_auto_code_start_value DEFAULT (1),
b_current_period varchar(8) NULL,
b_current_value bigint NOT NULL CONSTRAINT DF_s_module_auto_code_current_value DEFAULT (0),
b_canuse int NOT NULL CONSTRAINT DF_s_module_auto_code_canuse DEFAULT (1),
CONSTRAINT PK_s_module_auto_code PRIMARY KEY (b_id),
CONSTRAINT UQ_s_module_auto_code_module UNIQUE (b_module_id)
);
CREATE INDEX IX_s_module_auto_code_field
ON dbo.s_module_auto_code (b_field_id);
END;
@@ -0,0 +1,52 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF OBJECT_ID(N'dbo.s_module_auto_code', N'U') IS NOT NULL
BEGIN
IF COL_LENGTH(N'dbo.s_module_auto_code', N'b_prefix') IS NULL
ALTER TABLE dbo.s_module_auto_code ADD b_prefix nvarchar(100) NULL;
IF COL_LENGTH(N'dbo.s_module_auto_code', N'b_date_format') IS NULL
ALTER TABLE dbo.s_module_auto_code ADD b_date_format varchar(30) NULL;
IF COL_LENGTH(N'dbo.s_module_auto_code', N'b_separator') IS NULL
ALTER TABLE dbo.s_module_auto_code ADD b_separator nvarchar(10) NULL;
IF COL_LENGTH(N'dbo.s_module_auto_code', N'b_sequence_width') IS NULL
ALTER TABLE dbo.s_module_auto_code ADD b_sequence_width int NULL;
IF COL_LENGTH(N'dbo.s_module_auto_code', N'b_rule_config') IS NOT NULL
BEGIN
EXEC sys.sp_executesql N'
UPDATE dbo.s_module_auto_code
SET b_prefix = COALESCE(JSON_VALUE(b_rule_config, ''$.prefix''), N''''),
b_date_format = COALESCE(JSON_VALUE(b_rule_config, ''$.dateFormat''), ''''),
b_separator = COALESCE(JSON_VALUE(b_rule_config, ''$.separator''), N''''),
b_sequence_width = COALESCE(
TRY_CONVERT(int, JSON_VALUE(b_rule_config, ''$.sequenceWidth'')),
5
);';
END;
EXEC sys.sp_executesql N'
UPDATE dbo.s_module_auto_code
SET b_prefix = COALESCE(b_prefix, N''''),
b_date_format = COALESCE(b_date_format, ''''),
b_separator = COALESCE(b_separator, N''''),
b_sequence_width = COALESCE(b_sequence_width, 5);';
EXEC sys.sp_executesql N'
ALTER TABLE dbo.s_module_auto_code ALTER COLUMN b_prefix nvarchar(100) NOT NULL;
ALTER TABLE dbo.s_module_auto_code ALTER COLUMN b_date_format varchar(30) NOT NULL;
ALTER TABLE dbo.s_module_auto_code ALTER COLUMN b_separator nvarchar(10) NOT NULL;
ALTER TABLE dbo.s_module_auto_code ALTER COLUMN b_sequence_width int NOT NULL;';
IF OBJECT_ID(N'dbo.DF_s_module_auto_code_prefix', N'D') IS NULL
ALTER TABLE dbo.s_module_auto_code ADD CONSTRAINT DF_s_module_auto_code_prefix DEFAULT (N'') FOR b_prefix;
IF OBJECT_ID(N'dbo.DF_s_module_auto_code_date_format', N'D') IS NULL
ALTER TABLE dbo.s_module_auto_code ADD CONSTRAINT DF_s_module_auto_code_date_format DEFAULT ('yyyyMM') FOR b_date_format;
IF OBJECT_ID(N'dbo.DF_s_module_auto_code_separator', N'D') IS NULL
ALTER TABLE dbo.s_module_auto_code ADD CONSTRAINT DF_s_module_auto_code_separator DEFAULT (N'-') FOR b_separator;
IF OBJECT_ID(N'dbo.DF_s_module_auto_code_sequence_width', N'D') IS NULL
ALTER TABLE dbo.s_module_auto_code ADD CONSTRAINT DF_s_module_auto_code_sequence_width DEFAULT (5) FOR b_sequence_width;
IF COL_LENGTH(N'dbo.s_module_auto_code', N'b_rule_config') IS NOT NULL
EXEC sys.sp_executesql N'ALTER TABLE dbo.s_module_auto_code DROP COLUMN b_rule_config;';
END;
@@ -0,0 +1,156 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRANSACTION;
IF OBJECT_ID(N'dbo.b_othercompany', N'U') IS NULL
BEGIN
CREATE TABLE dbo.b_othercompany (
b_id varchar(50) NOT NULL,
b_pycode nvarchar(50) NULL,
b_type varchar(200) NULL,
b_name nvarchar(500) NULL,
b_shortname nvarchar(50) NULL,
b_linkman nvarchar(200) NULL,
b_phone nvarchar(500) NULL,
b_addr nvarchar(500) NULL,
b_firstlinkdate datetime NULL,
b_htno nvarchar(50) NULL,
b_htdate date NULL,
b_htexpiredate date NULL,
b_black varchar(1) NULL,
b_canuse varchar(1) NULL,
b_zl int NULL,
b_tel nvarchar(50) NULL,
b_fax nvarchar(500) NULL,
b_www nvarchar(50) NULL,
b_email nvarchar(50) NULL,
b_bankacount nvarchar(500) NULL,
b_invaccount nvarchar(500) NULL,
b_cate_id nvarchar(500) NULL,
b_bz1 nvarchar(500) NULL,
b_bz2 nvarchar(500) NULL,
b_bz3 nvarchar(500) NULL,
b_bz4 nvarchar(500) NULL,
b_province nvarchar(50) NULL,
b_city nvarchar(50) NULL,
b_area nvarchar(50) NULL,
b_country nvarchar(50) NULL,
b_edicode nvarchar(50) NULL,
b_showcolor int NULL,
b_type_id nvarchar(200) NULL,
b_from_id nvarchar(50) NULL,
b_type1_id nvarchar(50) NULL,
b_type2_id nvarchar(50) NULL,
b_type3_id nvarchar(50) NULL,
b_type4_id nvarchar(50) NULL,
b_type5_id nvarchar(50) NULL,
b_owner_id varchar(50) NULL,
b_inputuser_id varchar(50) NULL,
b_inputdatetime datetime NULL,
b_updateuser_id varchar(50) NULL,
b_updatedatetime datetime NULL,
b_password varchar(50) NULL,
b_state nvarchar(50) NULL,
b_bankacount_e nvarchar(500) NULL,
b_addr_e nvarchar(500) NULL,
b_name_e nvarchar(500) NULL,
CONSTRAINT PK_b_othercompany PRIMARY KEY (b_id)
);
CREATE INDEX IX_b_othercompany_name
ON dbo.b_othercompany (b_name, b_id);
CREATE INDEX IX_b_othercompany_pycode
ON dbo.b_othercompany (b_pycode, b_id);
CREATE INDEX IX_b_othercompany_owner
ON dbo.b_othercompany (b_owner_id, b_id);
END;
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.b_othercompany')
AND name = N'IX_b_othercompany_category'
)
CREATE INDEX IX_b_othercompany_category
ON dbo.b_othercompany (b_cate_id, b_id);
DECLARE @RebuildCategory bit = 0;
DECLARE @CategoryObjectId int = OBJECT_ID(N'dbo.b_othercompany_bmfl', N'U');
IF @CategoryObjectId IS NULL
SET @RebuildCategory = 1;
ELSE IF EXISTS (
SELECT 1
FROM sys.columns
WHERE object_id = @CategoryObjectId
AND name IN (
N'mc_a', N'bm_b', N'mc_b', N'js',
N'bm_1_b', N'mc_1_b', N'bm_2_b', N'mc_2_b',
N'bm_3_b', N'mc_3_b', N'bm_4_b', N'mc_4_b',
N'bm_5_b', N'mc_5_b', N'bm_6_b', N'mc_6_b'
)
)
SET @RebuildCategory = 1;
ELSE IF NOT EXISTS (
SELECT 1
FROM sys.columns
WHERE object_id = @CategoryObjectId
AND name = N'b_id'
AND system_type_id = TYPE_ID(N'bigint')
)
SET @RebuildCategory = 1;
ELSE IF EXISTS (
SELECT required.name
FROM (VALUES
(N'b_id'), (N'b_parent_id'), (N'b_code'), (N'b_name'),
(N'b_level'), (N'b_path'), (N'b_xh'), (N'b_canuse'),
(N'b_inputuser_id'), (N'b_inputdatetime'),
(N'b_updateuser_id'), (N'b_updatedatetime')
) AS required(name)
WHERE NOT EXISTS (
SELECT 1
FROM sys.columns AS actual
WHERE actual.object_id = @CategoryObjectId
AND actual.name = required.name
)
)
SET @RebuildCategory = 1;
IF @RebuildCategory = 1
BEGIN
DELETE FROM dbo.b_othercompany;
IF @CategoryObjectId IS NOT NULL
DROP TABLE dbo.b_othercompany_bmfl;
CREATE TABLE dbo.b_othercompany_bmfl (
b_id bigint NOT NULL,
b_parent_id bigint NULL,
b_code varchar(50) NOT NULL,
b_name nvarchar(150) NOT NULL,
b_level int NOT NULL CONSTRAINT DF_b_othercompany_bmfl_level DEFAULT (1),
b_path varchar(1000) NOT NULL,
b_xh int NOT NULL CONSTRAINT DF_b_othercompany_bmfl_xh DEFAULT (0),
b_canuse int NOT NULL CONSTRAINT DF_b_othercompany_bmfl_canuse DEFAULT (1),
b_inputuser_id bigint NULL,
b_inputdatetime datetime NULL,
b_updateuser_id bigint NULL,
b_updatedatetime datetime NULL,
CONSTRAINT PK_b_othercompany_bmfl PRIMARY KEY (b_id),
CONSTRAINT UQ_b_othercompany_bmfl_code UNIQUE (b_code),
CONSTRAINT CK_b_othercompany_bmfl_level CHECK (b_level >= 1),
CONSTRAINT CK_b_othercompany_bmfl_parent CHECK (
b_parent_id IS NULL OR b_parent_id <> b_id
),
CONSTRAINT FK_b_othercompany_bmfl_parent FOREIGN KEY (b_parent_id)
REFERENCES dbo.b_othercompany_bmfl (b_id)
);
CREATE INDEX IX_b_othercompany_bmfl_parent
ON dbo.b_othercompany_bmfl (b_parent_id, b_xh, b_id);
CREATE INDEX IX_b_othercompany_bmfl_path
ON dbo.b_othercompany_bmfl (b_path, b_xh, b_id);
END;
COMMIT TRANSACTION;
@@ -0,0 +1,252 @@
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF OBJECT_ID(N'dbo.b_othercompany', N'U') IS NULL
THROW 51210, N'dbo.b_othercompany does not exist. Run migration 011 first.', 1;
BEGIN TRANSACTION;
DECLARE @BaseId bigint = (
SELECT TOP (1) b_id
FROM dbo.s_module
WHERE b_code = 'base' AND b_module_type = 'directory'
ORDER BY b_id
);
IF @BaseId IS NULL
BEGIN
SET @BaseId = 920000000000001000;
IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @BaseId)
THROW 51211, N'The reserved base data directory ID is already in use.', 1;
INSERT INTO dbo.s_module (
b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type,
b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield,
b_canmenu, b_canuse, b_xh
) VALUES (
@BaseId, 'base', NULL, N'基础数据', 'menu.base', 'directory',
NULL, NULL, NULL, NULL, NULL, 1, 1, 20
);
END;
DECLARE @PageId bigint = (
SELECT TOP (1) b_id FROM dbo.s_module WHERE b_code = 'base_othercompany'
);
IF @PageId IS NULL
BEGIN
SET @PageId = 920000000000001001;
IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @PageId)
THROW 51212, N'The reserved other company page ID is already in use.', 1;
INSERT INTO dbo.s_module (
b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type,
b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield,
b_canmenu, b_canuse, b_xh
) VALUES (
@PageId, 'base_othercompany', @BaseId, N'合作伙伴',
'menu.base_othercompany', 'page', '/base/othercompany',
NULL, NULL, NULL, NULL, 1, 1, 10
);
END;
DECLARE @DataId bigint = (
SELECT TOP (1) b_id FROM dbo.s_module WHERE b_code = 'b_othercompany'
);
IF @DataId IS NULL
BEGIN
SET @DataId = 920000000000001002;
IF EXISTS (SELECT 1 FROM dbo.s_module WHERE b_id = @DataId)
THROW 51213, N'The reserved other company data ID is already in use.', 1;
INSERT INTO dbo.s_module (
b_id, b_code, b_parent_id, b_name, b_i18n, b_module_type,
b_route, b_viewtable, b_savetable, b_keyfield, b_orderfield,
b_canmenu, b_canuse, b_xh
) VALUES (
@DataId, 'b_othercompany', @PageId, N'合作伙伴列表',
'module.b_othercompany', 'data', NULL, 'b_othercompany',
'b_othercompany', 'b_id', 'b_name ASC, b_id ASC', 0, 1, 10
);
END;
DECLARE @Fields TABLE (
b_id bigint NOT NULL,
b_field varchar(50) NOT NULL,
b_name nvarchar(200) NOT NULL,
b_type varchar(30) NOT NULL,
b_xh int NOT NULL
);
INSERT INTO @Fields (b_id, b_field, b_name, b_type, b_xh) VALUES
(920000000000002001, 'b_id', N'系统编号', 'input', 10),
(920000000000002002, 'b_pycode', N'助记码', 'input', 20),
(920000000000002003, 'b_type', N'伙伴类型', 'input', 30),
(920000000000002004, 'b_name', N'公司全称', 'input', 40),
(920000000000002005, 'b_shortname', N'简称', 'input', 50),
(920000000000002006, 'b_linkman', N'主联系人', 'input', 60),
(920000000000002007, 'b_phone', N'联系电话', 'input', 70),
(920000000000002008, 'b_addr', N'详细地址', 'input', 80),
(920000000000002009, 'b_firstlinkdate', N'首次联系', 'datetime', 90),
(920000000000002010, 'b_htno', N'合同号', 'input', 100),
(920000000000002011, 'b_htdate', N'签订日期', 'date', 110),
(920000000000002012, 'b_htexpiredate', N'到期日期', 'date', 120),
(920000000000002013, 'b_black', N'黑名单', 'checkbox', 130),
(920000000000002014, 'b_canuse', N'启用', 'checkbox', 140),
(920000000000002015, 'b_zl', N'级别', 'number', 150),
(920000000000002016, 'b_tel', N'电话', 'input', 160),
(920000000000002017, 'b_fax', N'传真', 'input', 170),
(920000000000002018, 'b_www', N'网站', 'input', 180),
(920000000000002019, 'b_email', N'邮箱', 'input', 190),
(920000000000002020, 'b_bankacount', N'银行账号', 'input', 200),
(920000000000002021, 'b_invaccount', N'开票信息', 'input', 210),
(920000000000002022, 'b_cate_id', N'所属分类', 'input', 220),
(920000000000002023, 'b_bz1', N'备注1', 'input', 230),
(920000000000002024, 'b_bz2', N'备注2', 'input', 240),
(920000000000002025, 'b_bz3', N'备注3', 'input', 250),
(920000000000002026, 'b_bz4', N'备注4', 'input', 260),
(920000000000002027, 'b_province', N'省份', 'input', 270),
(920000000000002028, 'b_city', N'城市', 'input', 280),
(920000000000002029, 'b_area', N'区域', 'input', 290),
(920000000000002030, 'b_country', N'国家', 'input', 300),
(920000000000002031, 'b_edicode', N'EDI代码', 'input', 310),
(920000000000002032, 'b_showcolor', N'显示颜色', 'number', 320),
(920000000000002033, 'b_type_id', N'客户级别', 'input', 330),
(920000000000002034, 'b_from_id', N'客户来源', 'input', 340),
(920000000000002035, 'b_type1_id', N'分类1', 'input', 350),
(920000000000002036, 'b_type2_id', N'分类2', 'input', 360),
(920000000000002037, 'b_type3_id', N'分类3', 'input', 370),
(920000000000002038, 'b_type4_id', N'分类4', 'input', 380),
(920000000000002039, 'b_type5_id', N'分类5', 'input', 390),
(920000000000002040, 'b_owner_id', N'负责人', 'input', 400),
(920000000000002041, 'b_inputuser_id', N'录入账号', 'input', 410),
(920000000000002042, 'b_inputdatetime', N'录入时间', 'datetime', 420),
(920000000000002043, 'b_updateuser_id', N'修改账号', 'input', 430),
(920000000000002044, 'b_updatedatetime', N'修改时间', 'datetime', 440),
(920000000000002045, 'b_password', N'查询密码', 'input', 450),
(920000000000002046, 'b_state', N'审核状态', 'input', 460),
(920000000000002047, 'b_bankacount_e', N'英文银行账号', 'input', 470),
(920000000000002048, 'b_addr_e', N'英文地址', 'input', 480),
(920000000000002049, 'b_name_e', N'英文名称', 'input', 490);
IF EXISTS (
SELECT 1
FROM @Fields source
JOIN dbo.s_module_field target ON target.b_id = source.b_id
WHERE target.b_module_id <> @DataId OR target.b_field <> source.b_field
)
THROW 51214, N'A reserved other company field ID is already in use.', 1;
INSERT INTO dbo.s_module_field (
b_id, b_module_id, b_field, b_name, b_i18n, b_type, b_canuse
)
SELECT
source.b_id, @DataId, source.b_field, source.b_name,
'field.b_othercompany.' + source.b_field, source.b_type, 1
FROM @Fields source
WHERE NOT EXISTS (
SELECT 1 FROM dbo.s_module_field target
WHERE target.b_module_id = @DataId AND target.b_field = source.b_field
);
DECLARE @ListFields TABLE (
b_id bigint NOT NULL,
b_field varchar(50) NOT NULL,
b_xh int NOT NULL,
b_width int NOT NULL
);
INSERT INTO @ListFields (b_id, b_field, b_xh, b_width) VALUES
(920000000000003001, 'b_id', 10, 150),
(920000000000003002, 'b_name', 20, 220),
(920000000000003003, 'b_shortname', 30, 130),
(920000000000003004, 'b_pycode', 40, 100),
(920000000000003005, 'b_linkman', 50, 110),
(920000000000003006, 'b_phone', 60, 140),
(920000000000003007, 'b_type', 70, 120),
(920000000000003008, 'b_cate_id', 80, 140),
(920000000000003009, 'b_state', 90, 100),
(920000000000003010, 'b_canuse', 100, 76),
(920000000000003011, 'b_updatedatetime', 110, 150);
IF EXISTS (
SELECT 1 FROM @ListFields source
JOIN dbo.s_module_field_list target ON target.b_id = source.b_id
WHERE target.b_module_id <> @DataId
)
THROW 51215, N'A reserved other company list ID is already in use.', 1;
INSERT INTO dbo.s_module_field_list (
b_id, b_module_id, b_field_id, b_i18n, b_visible, b_xh, b_width
)
SELECT
source.b_id, @DataId, field.b_id, NULL, 1, source.b_xh, source.b_width
FROM @ListFields source
JOIN dbo.s_module_field field
ON field.b_module_id = @DataId AND field.b_field = source.b_field
WHERE NOT EXISTS (
SELECT 1 FROM dbo.s_module_field_list target
WHERE target.b_module_id = @DataId AND target.b_field_id = field.b_id
);
DECLARE @QueryFields TABLE (
b_id bigint NOT NULL,
b_field varchar(50) NOT NULL,
b_xh int NOT NULL
);
INSERT INTO @QueryFields (b_id, b_field, b_xh) VALUES
(920000000000004001, 'b_name', 10),
(920000000000004002, 'b_shortname', 20),
(920000000000004003, 'b_pycode', 30),
(920000000000004004, 'b_linkman', 40),
(920000000000004005, 'b_phone', 50);
IF EXISTS (
SELECT 1 FROM @QueryFields source
JOIN dbo.s_module_field_query target ON target.b_id = source.b_id
WHERE target.b_module_id <> @DataId
)
THROW 51216, N'A reserved other company query ID is already in use.', 1;
INSERT INTO dbo.s_module_field_query (
b_id, b_module_id, b_field_id, b_component, b_operator,
b_default_value, b_xh, b_canuse
)
SELECT
source.b_id, @DataId, field.b_id, 'input', 'like', NULL, source.b_xh, 1
FROM @QueryFields source
JOIN dbo.s_module_field field
ON field.b_module_id = @DataId AND field.b_field = source.b_field
WHERE NOT EXISTS (
SELECT 1 FROM dbo.s_module_field_query target
WHERE target.b_module_id = @DataId AND target.b_field_id = field.b_id
);
DECLARE @Powers TABLE (
b_id bigint NOT NULL,
b_code varchar(50) NOT NULL,
b_name nvarchar(200) NOT NULL,
b_xh int NOT NULL
);
INSERT INTO @Powers (b_id, b_code, b_name, b_xh) VALUES
(920000000000005001, 'create', N'新增', 10),
(920000000000005002, 'update', N'修改', 20),
(920000000000005003, 'delete', N'删除', 30);
IF EXISTS (
SELECT 1 FROM @Powers source
JOIN dbo.s_module_power target ON target.b_id = source.b_id
WHERE target.b_module_id <> @PageId OR target.b_code <> source.b_code
)
THROW 51217, N'A reserved other company power ID is already in use.', 1;
INSERT INTO dbo.s_module_power (
b_id, b_module_id, b_code, b_name, b_i18n, b_xh, b_canuse
)
SELECT
source.b_id, @PageId, source.b_code, source.b_name,
'power.base_othercompany.' + source.b_code, source.b_xh, 1
FROM @Powers source
WHERE NOT EXISTS (
SELECT 1 FROM dbo.s_module_power target
WHERE target.b_module_id = @PageId AND target.b_code = source.b_code
);
COMMIT TRANSACTION;
@@ -69,6 +69,18 @@ public class DataController {
return ApiResponse.success(ids);
}
@GetMapping("/nextcode")
public ApiResponse<?> nextCode(
@RequestParam String moduleId,
@RequestParam(required = false, defaultValue = "1") int count
) throws SQLException {
List<String> codes = dataSaveService.nextCode(moduleId, count);
if (codes.size() == 1) {
return ApiResponse.success(codes.get(0));
}
return ApiResponse.success(codes);
}
@PostMapping("/describe")
public ApiResponse<List<Map<String, Object>>> describe(
@RequestBody(required = false) Map<String, Object> params
@@ -9,11 +9,18 @@ import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringJoiner;
@Service
public class DataSaveService {
@@ -50,6 +57,327 @@ public class DataSaveService {
}
}
public List<String> nextCode(String moduleId, int count) throws SQLException {
if (moduleId == null || moduleId.isBlank()) {
throw new BusinessException("moduleId 不能为空");
}
if (count < 1 || count > 1000) {
throw new BusinessException("count 必须是 1 到 1000 之间的整数");
}
final long moduleIdValue;
try {
moduleIdValue = Long.parseLong(moduleId);
} catch (NumberFormatException exception) {
throw new BusinessException("moduleId 必须是有效的数字");
}
try (Connection connection = dataSource.getConnection()) {
boolean originalAutoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
try {
AutoCodeRule rule = loadAutoCodeRule(connection, moduleIdValue);
DbUtils.TableMetadata targetTable = dbUtils.loadTableMetadata(connection, rule.saveTable());
DbUtils.ColumnMetadata targetColumn = dbUtils.getColumn(targetTable, rule.fieldName());
if (targetColumn == null) {
throw new BusinessException(
"自动编码字段不存在: " + rule.saveTable() + "." + rule.fieldName()
);
}
if (!isTextColumn(targetColumn.typeName())) {
throw new BusinessException("自动编码字段必须是文本类型: " + rule.fieldName());
}
ensureUniqueCodeIndex(connection, targetTable, targetColumn);
if (rule.startValue() < 0) {
throw new BusinessException("自动编码起始值不能小于 0");
}
AutoCodeFormat format = createAutoCodeFormat(
rule.prefix(),
rule.dateFormat(),
rule.separator(),
rule.sequenceWidth()
);
validateResetFormat(rule.resetType(), format.dateFormat());
LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai"));
String period = buildPeriodKey(today, rule.resetType());
long nextValue = shouldReset(rule.currentPeriod(), period)
? rule.startValue()
: increment(rule.currentValue());
List<String> codes = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
CodeCandidate candidate = nextAvailableCode(
connection,
targetTable,
targetColumn,
format,
today,
nextValue
);
codes.add(candidate.code());
nextValue = increment(candidate.value());
}
long lastValue = nextValue - 1;
try (PreparedStatement statement = connection.prepareStatement(
"UPDATE dbo.s_module_auto_code "
+ "SET b_current_period = ?, b_current_value = ? "
+ "WHERE b_id = ?"
)) {
statement.setString(1, period);
statement.setLong(2, lastValue);
statement.setLong(3, rule.id());
statement.executeUpdate();
}
connection.commit();
return codes;
} catch (SQLException | RuntimeException exception) {
rollback(connection, exception);
throw exception;
} finally {
if (originalAutoCommit) {
connection.setAutoCommit(true);
}
}
}
}
private AutoCodeRule loadAutoCodeRule(Connection connection, long moduleId) throws SQLException {
String sql = "SELECT TOP 1 r.b_id, r.b_prefix, r.b_date_format, "
+ "r.b_separator, r.b_sequence_width, r.b_reset_type, "
+ "r.b_start_value, r.b_current_period, r.b_current_value, "
+ "m.b_savetable, f.b_field "
+ "FROM dbo.s_module_auto_code r WITH (UPDLOCK, HOLDLOCK, ROWLOCK) "
+ "INNER JOIN dbo.s_module m ON m.b_id = r.b_module_id "
+ "INNER JOIN dbo.s_module_field f ON f.b_id = r.b_field_id "
+ "WHERE r.b_module_id = ? AND r.b_canuse = 1 "
+ "AND m.b_module_type = 'data' AND f.b_canuse = 1";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setLong(1, moduleId);
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
throw new BusinessException("当前模块未配置启用的自动编码规则");
}
String saveTable = resultSet.getString("b_savetable");
String fieldName = resultSet.getString("b_field");
if (saveTable == null || saveTable.isBlank()) {
throw new BusinessException("模块未配置保存表,无法生成自动编号");
}
return new AutoCodeRule(
resultSet.getLong("b_id"),
resultSet.getString("b_prefix"),
resultSet.getString("b_date_format"),
resultSet.getString("b_separator"),
resultSet.getInt("b_sequence_width"),
resultSet.getString("b_reset_type"),
resultSet.getLong("b_start_value"),
resultSet.getString("b_current_period"),
resultSet.getLong("b_current_value"),
saveTable,
fieldName
);
}
}
}
private CodeCandidate nextAvailableCode(
Connection connection,
DbUtils.TableMetadata targetTable,
DbUtils.ColumnMetadata targetColumn,
AutoCodeFormat format,
LocalDate today,
long value
) throws SQLException {
long candidate = value;
for (int attempt = 0; attempt < 1000; attempt++) {
String code = formatCode(format, today, candidate);
if (targetColumn.size() > 0 && code.length() > targetColumn.size()) {
throw new BusinessException("自动编号长度超过目标字段长度");
}
if (!codeExists(connection, targetTable, targetColumn, code)) {
return new CodeCandidate(code, candidate);
}
candidate = increment(candidate);
}
throw new BusinessException("自动编号连续冲突,无法分配新的编号");
}
private boolean codeExists(
Connection connection,
DbUtils.TableMetadata targetTable,
DbUtils.ColumnMetadata targetColumn,
String code
) throws SQLException {
String sql = "SELECT TOP 1 1 FROM " + targetTable.quotedName()
+ " WHERE " + dbUtils.quoteQualifiedIdentifier(targetColumn.name()) + " = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, code);
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next();
}
}
}
private void ensureUniqueCodeIndex(
Connection connection,
DbUtils.TableMetadata targetTable,
DbUtils.ColumnMetadata targetColumn
) throws SQLException {
String sql = "SELECT TOP 1 1 "
+ "FROM sys.indexes i "
+ "INNER JOIN sys.index_columns ic "
+ "ON ic.object_id = i.object_id AND ic.index_id = i.index_id "
+ "INNER JOIN sys.columns c "
+ "ON c.object_id = ic.object_id AND c.column_id = ic.column_id "
+ "WHERE i.object_id = OBJECT_ID(?) AND i.is_unique = 1 AND i.is_disabled = 0 "
+ "AND ic.key_ordinal = 1 AND c.name = ? "
+ "AND NOT EXISTS ("
+ "SELECT 1 FROM sys.index_columns other "
+ "WHERE other.object_id = i.object_id AND other.index_id = i.index_id "
+ "AND other.key_ordinal > 1)";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, targetTable.requestedName());
statement.setString(2, targetColumn.name());
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
throw new BusinessException(
"自动编码字段必须建立单字段唯一索引: "
+ targetTable.requestedName() + "." + targetColumn.name()
);
}
}
}
}
private AutoCodeFormat createAutoCodeFormat(
String prefix,
String dateFormat,
String separator,
int sequenceWidth
) {
String normalizedPrefix = prefix == null ? "" : prefix;
String normalizedDateFormat = dateFormat == null ? "" : dateFormat.trim();
String normalizedSeparator = separator == null ? "" : separator;
if (sequenceWidth < 1 || sequenceWidth > 18) {
throw new BusinessException("流水号位数必须是 1 到 18 之间的整数");
}
if (!normalizedDateFormat.isEmpty()) {
dateFormatter(normalizedDateFormat);
}
if (normalizedSeparator.length() > 10) {
throw new BusinessException("分隔符长度不能超过 10 个字符");
}
if (normalizedPrefix.length() > 100) {
throw new BusinessException("固定前缀长度不能超过 100 个字符");
}
return new AutoCodeFormat(
normalizedPrefix,
normalizedDateFormat,
normalizedSeparator,
sequenceWidth
);
}
private String formatCode(AutoCodeFormat format, LocalDate today, long sequence) {
String sequenceText = Long.toString(sequence);
if (sequenceText.length() > format.sequenceWidth()) {
throw new BusinessException("流水号超过配置位数");
}
sequenceText = "0".repeat(format.sequenceWidth() - sequenceText.length()) + sequenceText;
StringJoiner parts = new StringJoiner(format.separator());
if (!format.prefix().isEmpty()) parts.add(format.prefix());
if (!format.dateFormat().isEmpty()) parts.add(today.format(dateFormatter(format.dateFormat())));
parts.add(sequenceText);
return parts.toString();
}
private DateTimeFormatter dateFormatter(String format) {
if (format.length() > 30) {
throw new BusinessException("日期格式长度不能超过 30 个字符");
}
String literals = format
.replace("yyyy", "")
.replace("yy", "")
.replace("MM", "")
.replace("dd", "");
if (literals.matches(".*[A-Za-z].*")) {
throw new BusinessException("日期格式只支持 yyyy、yy、MM 和 dd");
}
try {
return DateTimeFormatter.ofPattern(format);
} catch (IllegalArgumentException exception) {
throw new BusinessException("日期格式不正确: " + format);
}
}
private String buildPeriodKey(LocalDate date, String resetType) {
return switch (resetType == null ? "month" : resetType) {
case "none" -> "*";
case "year" -> date.format(DateTimeFormatter.ofPattern("yyyy"));
case "month" -> date.format(DateTimeFormatter.ofPattern("yyyyMM"));
case "day" -> date.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
default -> throw new BusinessException("不支持的重置周期: " + resetType);
};
}
private void validateResetFormat(String resetType, String dateFormat) {
String normalizedResetType = resetType == null ? "month" : resetType;
boolean hasYear = dateFormat.contains("yyyy") || dateFormat.contains("yy");
boolean hasMonth = dateFormat.contains("MM");
boolean hasDay = dateFormat.contains("dd");
if ("year".equals(normalizedResetType) && !hasYear) {
throw new BusinessException("按年重置时,日期格式必须包含年份");
}
if ("month".equals(normalizedResetType) && (!hasYear || !hasMonth)) {
throw new BusinessException("按月重置时,日期格式必须包含年份和月份");
}
if ("day".equals(normalizedResetType) && (!hasYear || !hasMonth || !hasDay)) {
throw new BusinessException("按日重置时,日期格式必须包含年月日");
}
}
private boolean shouldReset(String previousPeriod, String currentPeriod) {
return previousPeriod == null || !previousPeriod.equals(currentPeriod);
}
private long increment(long value) {
if (value == Long.MAX_VALUE) {
throw new BusinessException("自动编号流水值已达到最大值");
}
return value + 1;
}
private boolean isTextColumn(String typeName) {
String normalized = typeName == null ? "" : typeName.toLowerCase(Locale.ROOT);
return normalized.contains("char") || normalized.contains("text");
}
private record AutoCodeRule(
long id,
String prefix,
String dateFormat,
String separator,
int sequenceWidth,
String resetType,
long startValue,
String currentPeriod,
long currentValue,
String saveTable,
String fieldName
) {
}
private record AutoCodeFormat(
String prefix,
String dateFormat,
String separator,
int sequenceWidth
) {
}
private record CodeCandidate(String code, long value) {
}
private void saveTable(
Connection connection,
Map<String, Object> request,
@@ -1,6 +1,7 @@
package cn.g3soft.fmsapi.service;
import cn.g3soft.fmsapi.exception.SaveObjectException;
import cn.g3soft.fmsapi.exception.BusinessException;
import cn.g3soft.fmsapi.utils.DbUtils;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -12,6 +13,7 @@ import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@@ -24,6 +26,44 @@ import static org.mockito.Mockito.when;
class DataSaveServiceTests {
@Test
void formatsConfigWithCustomDateAndOptionalSeparator() {
DataSaveService service = new DataSaveService();
Object format = ReflectionTestUtils.invokeMethod(
service,
"createAutoCodeFormat",
"SO",
"yyyy.MM.dd",
"-",
5
);
String code = ReflectionTestUtils.invokeMethod(
service,
"formatCode",
format,
LocalDate.of(2026, 7, 29),
1L
);
assertThat(code).isEqualTo("SO-2026.07.29-00001");
}
@Test
void rejectsUnsupportedDateTokens() {
DataSaveService service = new DataSaveService();
assertThatThrownBy(() -> ReflectionTestUtils.invokeMethod(
service,
"createAutoCodeFormat",
"SO",
"yyyyHH",
"",
5
)).isInstanceOf(BusinessException.class)
.hasMessageContaining("日期格式只支持");
}
@Test
void savesDirectArrayInOneTransactionAndKeepsEmptyString() throws Exception {
JdbcFixture fixture = new JdbcFixture("b_id", "b_amount");
@@ -2,8 +2,8 @@
import { computed, h, nextTick, reactive, ref, shallowRef, useSlots, watch } from "vue";
import { useRoute } from "vue-router";
import { message } from "antdv-next";
import { Columns3, RefreshCw } from "@lucide/vue";
import { loadDataApi, pageDataApi } from "@/services/api";
import { Columns3, RefreshCw, Save } from "@lucide/vue";
import { loadDataApi, pageDataApi, saveObjectApi } from "@/services/api";
import { useAppStore } from "@/stores/app";
import {
identifierRe,
@@ -18,6 +18,7 @@ import FmsQueryToolbar from "./FmsQueryToolbar.vue";
const props = defineProps({
dataCode: { type: String, default: "" },
fixedSearchCondition: { type: String, default: "" },
columnExtensions: { type: Object, default: () => ({}) },
rowActions: { type: Array, default: () => [] },
rowSelection: { type: Object, default: null },
@@ -44,6 +45,7 @@ const searchConditions = shallowRef([]);
const keyField = ref("");
const configLoading = ref(false);
const dataLoading = ref(false);
const columnSaving = ref(false);
const configError = ref("");
const dataError = ref("");
const pagination = reactive({
@@ -51,6 +53,7 @@ const pagination = reactive({
pageSize: Number(props.pageSize) || 20,
total: 0,
});
let dataRequestVersion = 0;
const loading = computed(() => configLoading.value || dataLoading.value);
@@ -63,9 +66,15 @@ const moreMenuItems = computed(() => [
},
{
key: "reset-columns",
label: appStore.t("common.resetColumns", "恢复配置列宽"),
label: appStore.t("common.resetColumnSettings", "恢复栏位设置"),
icon: h(Columns3, { size: 14 }),
},
{
key: "save-column-settings",
label: appStore.t("common.saveColumnSettings", "保存栏位设置"),
icon: h(Save, { size: 14 }),
disabled: columnSaving.value,
},
]);
const cellSlotNames = computed(() => Object.keys(slots).filter((name) => name.startsWith("cell-")));
@@ -225,12 +234,17 @@ function resolveOrderBy() {
}
async function fetchData() {
if (!dataModule.value || !dataModule.value.b_viewtable || dataLoading.value) return;
if (!dataModule.value || !dataModule.value.b_viewtable) return;
const currentModule = dataModule.value;
const viewName = currentModule.b_viewtable;
const currentCode = getDataCode();
const currentPath = route.path;
const requestVersion = ++dataRequestVersion;
const conditions = [];
const fixedCondition = String(props.fixedSearchCondition || "").trim();
if (fixedCondition) conditions.push(`(${fixedCondition})`);
conditions.push(...searchConditions.value);
dataLoading.value = true;
dataError.value = "";
@@ -239,17 +253,18 @@ async function fetchData() {
viewName,
orderBy: resolveOrderBy(),
searchColumns: [],
searchCondition: searchConditions.value.join(" AND "),
searchCondition: conditions.join(" AND "),
pageNo: pagination.current,
pageSize: pagination.pageSize,
});
if (dataModule.value !== currentModule) return;
if (requestVersion !== dataRequestVersion || dataModule.value !== currentModule) return;
if (getDataCode() !== currentCode || route.path !== currentPath) return;
dataSource.value = res.data?.rows || [];
pagination.total = Number(res.data?.total ?? res.data?.row_count) || 0;
} catch (error) {
if (
requestVersion === dataRequestVersion &&
dataModule.value === currentModule &&
getDataCode() === currentCode &&
route.path === currentPath
@@ -260,6 +275,7 @@ async function fetchData() {
}
} finally {
if (
requestVersion === dataRequestVersion &&
dataModule.value === currentModule &&
getDataCode() === currentCode &&
route.path === currentPath
@@ -271,6 +287,7 @@ async function fetchData() {
// ── 公开方法 ──
async function initialize() {
dataRequestVersion++;
dataModule.value = null;
activeFields.value = [];
activeFieldById.value = new Map();
@@ -324,6 +341,56 @@ async function resetColumns() {
if (tableRef.value) tableRef.value.resetColumns();
}
async function saveColumnSettings() {
if (loading.value || columnSaving.value || !tableRef.value) return;
if (typeof tableRef.value.getColumnSettings !== "function") return;
const settings = tableRef.value.getColumnSettings();
if (!settings.length) {
message.info("当前没有可保存的栏位设置");
return;
}
const updates = [];
for (const row of settings) {
updates.push({
b_id: row.b_id,
b_xh: row.b_xh,
b_width: row.b_width,
});
}
columnSaving.value = true;
try {
await saveObjectApi([
{
table: "s_module_field_list",
key_field: "b_id",
updates,
},
]);
const currentById = new Map();
for (const row of listConfig.value) {
currentById.set(String(row.b_id), row);
}
const savedListConfig = [];
for (const saved of settings) {
const current = currentById.get(String(saved.b_id));
if (!current) continue;
savedListConfig.push({ ...current, b_xh: saved.b_xh, b_width: saved.b_width });
}
listConfig.value = savedListConfig;
message.success("栏位设置已保存");
} catch (error) {
let errorMessage = error;
if (error && error.message) errorMessage = error.message;
message.error("栏位设置保存失败:" + errorMessage);
} finally {
columnSaving.value = false;
}
}
function resolveRowKey(row) {
if (keyField.value) return row[keyField.value];
return JSON.stringify(row);
@@ -336,13 +403,26 @@ async function handleMoreAction({ key }) {
}
if (key === "reset-columns") {
await resetColumns();
message.success(appStore.t("common.columnsReset", "已恢复配置列宽"));
message.success(appStore.t("common.columnSettingsReset", "已恢复栏位设置"));
return;
}
if (key === "save-column-settings") {
await saveColumnSettings();
}
}
// ── 生命周期 ──
watch(() => [getDataCode(), route.path], initialize, { immediate: true });
watch(
() => props.fixedSearchCondition,
() => {
if (!dataModule.value) return;
pagination.current = 1;
fetchData();
},
);
watch(
() => dataSource.value,
(rows) => {
@@ -37,7 +37,7 @@ const expansionOptions = computed(() =>
const expandConfig = computed(() => ({ height: Number(expansionOptions.value.height) || 180 }));
function formatCellValue(value, field, optionInfo) {
if (value === null || value === undefined || value === "") return "-";
if (value === null || value === undefined || value === "") return "";
if (field.b_type === "date") return formatDateValue(value, false);
if (field.b_type === "datetime") return formatDateValue(value, true);
if (field.b_type === "checkbox") {
@@ -199,6 +199,35 @@ function resetColumns() {
tableRef.value?.initVirtualScroll?.();
}
function getColumnSettings() {
const configByFieldId = new Map();
for (const config of props.listConfig || []) {
configByFieldId.set(String(config.b_field_id), config);
}
const settings = [];
let order = 10;
for (const column of columns.value) {
if (!column.dataIndex || String(column.dataIndex).startsWith("__")) continue;
const field = props.fields.find((item) => item.b_field === column.dataIndex);
if (!field) continue;
const config = configByFieldId.get(String(field.b_id));
if (!config) continue;
const parsedWidth = Number.parseFloat(String(column.width));
let columnWidth = config.b_width;
if (Number.isFinite(parsedWidth) && parsedWidth > 0) {
columnWidth = Math.round(parsedWidth);
}
settings.push({
...config,
b_xh: order,
b_width: columnWidth,
});
order += 10;
}
return settings;
}
function handleCellClick(event, record, column, meta) {
if (!column?.dataIndex || String(column.dataIndex).startsWith("__")) return;
const field = props.fields.find((item) => item.b_field === column.dataIndex);
@@ -251,7 +280,11 @@ watch(
},
);
defineExpose({ resetColumns, clearSelectedArea: () => tableRef.value?.clearSelectedArea?.() });
defineExpose({
resetColumns,
getColumnSettings,
clearSelectedArea: () => tableRef.value?.clearSelectedArea?.(),
});
</script>
<template>
@@ -226,7 +226,7 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
<Search :size="15" />
{{ appStore.t("common.search", "查询") }}
</a-button>
<a-button :disabled="loading" @click="handleAdvancedReset">
<a-button type="text" :disabled="loading" @click="handleAdvancedReset">
<RotateCcw :size="15" />
{{ appStore.t("common.reset", "重置") }}
</a-button>
@@ -15,8 +15,10 @@ const props = defineProps({
noDataFull: { type: Boolean, default: true },
showOverflow: { type: Boolean, default: true },
columns: { type: Array, default: () => [] },
// 序号列:false=关闭;true=默认;{ title, width }=自定义
seq: { type: [Boolean, Object], default: false },
// 序号列默认显示;false=关闭;{ title, width }=自定义
seq: { type: [Boolean, Object], default: true },
// 列头拖拽默认开启;false=关闭;{ mode, disabled }=自定义
columnDraggable: { type: [Boolean, Object], default: true },
// 行多选:{ selectedRowKeys, onChange },提供时在最前列插入复选框列
rowSelection: { type: Object, default: null },
dataSource: { type: Array, default: () => [] },
@@ -137,6 +139,27 @@ const seqConfig = computed(() => {
width: Number(cfg.width ?? 50),
};
});
const columnDragConfig = computed(() => {
if (!props.columnDraggable) return false;
const configured = props.columnDraggable === true ? {} : props.columnDraggable;
return {
...configured,
disabled: (column) => {
if (
isSelectionColumn(column) ||
column.type === "seq" ||
column.type === "dragRow" ||
column.type === "expand" ||
column.dataIndex === "" ||
column.dataIndex === "__actions__"
) {
return true;
}
if (typeof configured.disabled === "function") return configured.disabled(column);
return Boolean(configured.disabled);
},
};
});
const hasRowSelection = computed(() => Boolean(props.rowSelection));
const dragRowColumn = {
type: "dragRow",
@@ -158,8 +181,6 @@ function getHeaderAlign(column) {
const displayColumns = computed(() => {
const cols = props.columns.map((col) => resolveCustomCell(col, editingCtx));
if (props.rowDraggable) cols.unshift(dragRowColumn);
if (hasRowSelection.value) cols.unshift(selectionColumn);
const seq = seqConfig.value;
if (seq.enabled) {
cols.unshift({
@@ -170,6 +191,8 @@ const displayColumns = computed(() => {
resizable: false,
});
}
if (props.rowDraggable) cols.unshift(dragRowColumn);
if (hasRowSelection.value) cols.unshift(selectionColumn);
return cols.map((column) => ({
...column,
headerAlign: getHeaderAlign(column),
@@ -435,6 +458,7 @@ defineExpose({
:virtual-x="virtualX"
:auto-resize="false"
:col-resizable="colResizable"
:header-drag="columnDragConfig"
:columns="displayColumns"
@update:columns="onColumnsUpdate"
:data-source="dataSource"
@@ -1,7 +1,7 @@
<script setup>
import { computed, h, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { House, PanelLeftClose, PanelLeftOpen, Settings } from "@lucide/vue";
import { Database, House, PanelLeftClose, PanelLeftOpen, Settings } from "@lucide/vue";
import { useAppStore } from "@/stores/app";
import { usePermissionStore } from "@/stores/permissions";
@@ -10,7 +10,7 @@ const router = useRouter();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
const openKeys = ref(["system"]);
const openKeys = ref(["base", "system"]);
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
function moduleLabel(moduleId, path, fallback) {
const module = permissionStore.modules.find(
@@ -29,6 +29,13 @@ function canAccessMenu(item) {
return true;
}
const menuItems = computed(() => {
const basePages = [
{
key: "/base/othercompany",
label: moduleLabel("base_othercompany", "/base/othercompany", "合作伙伴"),
moduleId: "base_othercompany",
},
].filter(canAccessMenu);
const systemPages = [
{
key: "/system/module-management",
@@ -53,6 +60,13 @@ const menuItems = computed(() => {
label: appStore.t("common.dashboard", "工作台"),
},
];
if (basePages.length)
items.push({
key: "base",
icon: menuIcon(Database),
label: directoryLabel("基础数据"),
children: basePages,
});
if (systemPages.length)
items.push({
key: "system",
@@ -66,6 +80,8 @@ const selectedKeys = computed(() => [route.meta.menuKey || route.path]);
watch(
() => route.path,
(path) => {
if (path.startsWith("/base") && !openKeys.value.includes("base"))
openKeys.value = [...openKeys.value, "base"];
if (path.startsWith("/system") && !openKeys.value.includes("system"))
openKeys.value = [...openKeys.value, "system"];
},
+13 -1
View File
@@ -14,6 +14,18 @@ const routes = [
component: () => import("@/views/dashboard/index.vue"),
meta: { title: "工作台", menuKey: "/dashboard" },
},
{
path: "base/othercompany",
name: "base-othercompany",
component: () => import("@/views/base/othercompany/index.vue"),
meta: {
title: "合作伙伴",
section: "基础数据",
menuKey: "/base/othercompany",
moduleId: "base_othercompany",
dataCode: "b_othercompany",
},
},
{
path: "system/module-management",
name: "system-module-management",
@@ -44,7 +56,7 @@ const routes = [
title: "登录日志",
section: "系统管理",
menuKey: "/system/login-log",
moduleId: "d_login_log",
moduleId: "s_login_log",
},
},
],
+10
View File
@@ -97,6 +97,16 @@ export function nextIdApi(count = 1) {
return get("/data/nextid", { params: { count } });
}
/**
* 按模块规则原子获取业务编号;count 大于 1 时一次返回多个编号。
* @param {string} moduleId 数据模块 ID。
* @param {number} [count=1] 需要获取的编号数量。
* @returns {Promise<ApiResult<string|string[]>>} 业务编号或编号数组。
*/
export function nextCodeApi(moduleId, count = 1) {
return get("/data/nextcode", { params: { moduleId: String(moduleId), count } });
}
/**
* 读取表或视图的列结构,用于同步字段等场景。
* @param {string} table 表名或视图名,必填,后端会校验标识符。
@@ -0,0 +1,250 @@
<script setup>
import { computed, onMounted, ref } from "vue";
import { message } from "antdv-next";
import { RefreshCw } from "@lucide/vue";
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
import FmsTree from "@/components/FmsTree.vue";
import { escapeSqlText } from "@/components/fms-module-list/moduleListUtils";
import { loadDataApi } from "@/services/api";
import { buildTree, sortTree } from "@/utils/tree";
const ALL_CATEGORIES_KEY = "__all_categories__";
const CATEGORY_COLUMNS = [
"b_id",
"b_parent_id",
"b_code",
"b_name",
"b_level",
"b_path",
"b_xh",
"b_canuse",
];
const categories = ref([]);
const categoryLoading = ref(false);
const selectedCategoryKey = ref(ALL_CATEGORIES_KEY);
const expandedCategoryKeys = ref([ALL_CATEGORIES_KEY]);
const categoryById = computed(
() => new Map(categories.value.map((item) => [String(item.b_id), item])),
);
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_xh: item.b_xh,
children: [],
}),
});
sortTree(roots);
return [
{
key: ALL_CATEGORIES_KEY,
title: "全部合作伙伴",
b_xh: 0,
children: roots,
},
];
});
const categoryFilterCondition = computed(() => {
if (selectedCategoryKey.value === ALL_CATEGORIES_KEY) return "";
const ids = collectDescendantIds(selectedCategoryKey.value);
const literals = ids.map((id) => `N'${escapeSqlText(id)}'`);
return literals.length ? `[b_cate_id] IN (${literals.join(", ")})` : "1 = 0";
});
const columnExtensions = computed(() => ({
b_cate_id: {
formatter: ({ value }) => categoryById.value.get(String(value || ""))?.b_name || value || "",
},
}));
function collectDescendantIds(rootId) {
const childrenByParent = new Map();
for (const item of categories.value) {
const parentId = item.b_parent_id == null ? "" : String(item.b_parent_id);
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);
childrenByParent.get(parentId).push(String(item.b_id));
}
const result = [];
const visited = new Set();
const pending = [String(rootId)];
while (pending.length) {
const id = pending.pop();
if (visited.has(id)) continue;
visited.add(id);
result.push(id);
for (const childId of childrenByParent.get(id) || []) pending.push(childId);
}
return result;
}
async function loadCategories() {
categoryLoading.value = true;
try {
const response = await loadDataApi(
"b_othercompany_bmfl",
"",
"b_path ASC, b_xh ASC, b_id ASC",
CATEGORY_COLUMNS,
);
categories.value = response.data || [];
const existingKeys = new Set(categories.value.map((item) => String(item.b_id)));
if (
selectedCategoryKey.value !== ALL_CATEGORIES_KEY &&
!existingKeys.has(selectedCategoryKey.value)
) {
selectedCategoryKey.value = ALL_CATEGORIES_KEY;
}
const parentKeys = categories.value
.map((item) => (item.b_parent_id == null ? "" : String(item.b_parent_id)))
.filter(Boolean);
expandedCategoryKeys.value = [
...new Set([...expandedCategoryKeys.value, ALL_CATEGORIES_KEY, ...parentKeys]),
];
} catch (error) {
message.error("分类加载失败:" + (error?.message || error));
} finally {
categoryLoading.value = false;
}
}
function onCategorySelect(keys) {
if (keys.length) selectedCategoryKey.value = String(keys[0]);
}
onMounted(loadCategories);
</script>
<template>
<div class="othercompany-page">
<a-splitter class="othercompany-splitter" lazy>
<a-splitter-panel :default-size="250" :min="210" :max="380" collapsible>
<aside class="category-panel">
<header class="category-header">
<div class="category-title">合作伙伴分类</div>
<a-tooltip title="刷新分类">
<a-button
type="text"
:disabled="categoryLoading"
aria-label="刷新分类"
@click="loadCategories"
>
<RefreshCw :size="15" />
</a-button>
</a-tooltip>
</header>
<div class="category-tree-stage">
<FmsTree
v-model:expanded-keys="expandedCategoryKeys"
:tree-data="categoryTree"
:selected-keys="[selectedCategoryKey]"
show-line
@select="onCategorySelect"
/>
<div v-if="categoryLoading" class="category-loading"><a-spin size="small" /></div>
</div>
</aside>
</a-splitter-panel>
<a-splitter-panel :min="520">
<main class="partner-panel">
<FmsModuleListPage
data-code="b_othercompany"
:fixed-search-condition="categoryFilterCondition"
:column-extensions="columnExtensions"
/>
</main>
</a-splitter-panel>
</a-splitter>
</div>
</template>
<style scoped lang="scss">
.othercompany-page,
.othercompany-splitter {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.othercompany-page {
overflow: hidden;
border: 1px solid var(--fms-border);
border-radius: 6px;
background: var(--fms-surface);
}
.category-panel,
.partner-panel {
display: flex;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
flex-direction: column;
}
.category-header {
display: flex;
min-height: 50px;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 6px 4px 12px;
border-bottom: 1px solid var(--fms-border);
}
.category-title {
min-width: 0;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
}
.category-header :deep(.ant-btn) {
width: 28px;
height: 28px;
padding: 0;
}
.category-tree-stage {
position: relative;
flex: 1;
min-height: 0;
overflow: auto;
padding: 6px 4px;
}
.category-tree-stage :deep(.ant-tree) {
min-width: 100%;
}
.category-tree-stage :deep(.ant-tree-node-content-wrapper) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.category-loading {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: rgb(255 255 255 / 70%);
}
.partner-panel :deep(.fms-module-list-page) {
border: 0;
border-radius: 0;
}
</style>
@@ -0,0 +1,28 @@
import { createTempId } from "@/utils/tempId";
function defaultFieldId(fields) {
const field = (fields || []).find(
(item) =>
Number(item.b_canuse) === 1 &&
item.b_field !== "b_id" &&
(item.b_type === "input" || item.b_type === "textarea"),
);
return field?.b_id != null ? String(field.b_id) : null;
}
export function createAutoCodeDraft(moduleId, fields, enabled = false) {
return {
b_id: createTempId(),
b_module_id: String(moduleId),
b_field_id: defaultFieldId(fields),
b_prefix: "",
b_date_format: "yyyyMM",
b_separator: "-",
b_sequence_width: 5,
b_reset_type: "month",
b_start_value: 1,
b_current_period: null,
b_current_value: 0,
b_canuse: enabled ? 1 : 0,
};
}
@@ -0,0 +1,230 @@
<script setup>
import { computed, ref, watch } from "vue";
import { createAutoCodeDraft } from "../autoCode";
const DATE_FORMAT_OPTIONS = [
{ label: "不使用日期", value: "" },
{ label: "yyyy", value: "yyyy" },
{ label: "yy", value: "yy" },
{ label: "yyyyMM", value: "yyyyMM" },
{ label: "yyMM", value: "yyMM" },
{ label: "yyyyMMdd", value: "yyyyMMdd" },
{ label: "yyyy-MM", value: "yyyy-MM" },
{ label: "yyyy-MM-dd", value: "yyyy-MM-dd" },
{ label: "yyyy/MM", value: "yyyy/MM" },
{ label: "yyyy/MM/dd", value: "yyyy/MM/dd" },
];
const RESET_OPTIONS = [
{ label: "不重置", value: "none" },
{ label: "每年", value: "year" },
{ label: "每月", value: "month" },
{ label: "每日", value: "day" },
];
const props = defineProps({
moduleId: { type: String, required: true },
autoCode: { type: Array, default: () => [] },
fields: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["update"]);
const localRow = ref(null);
const enabled = computed(() => Number(localRow.value?.b_canuse) === 1);
const fieldOptions = computed(() => {
const options = [];
for (const field of props.fields || []) {
if (Number(field.b_canuse) !== 1) continue;
if (field.b_field === "b_id") continue;
if (field.b_type !== "input" && field.b_type !== "textarea") continue;
options.push({
label: `${field.b_name || field.b_field} (${field.b_field})`,
value: String(field.b_id),
});
}
return options;
});
function notifyUpdate() {
emit("update", localRow.value ? [{ ...localRow.value }] : []);
}
watch(
() => props.autoCode,
(rows) => {
const row = rows && rows.length ? rows[0] : null;
localRow.value = row ? { ...row } : createAutoCodeDraft(props.moduleId, props.fields, false);
},
{ immediate: true },
);
function setRowValue(field, value) {
if (!localRow.value) return;
localRow.value[field] = value;
notifyUpdate();
}
function pad(value) {
return String(value).padStart(2, "0");
}
function formatDate(date, format) {
const yyyy = String(date.getFullYear());
const yy = yyyy.slice(-2);
const MM = pad(date.getMonth() + 1);
const dd = pad(date.getDate());
return String(format || "")
.replaceAll("yyyy", yyyy)
.replaceAll("yy", yy)
.replaceAll("MM", MM)
.replaceAll("dd", dd);
}
function currentPeriod(date, resetType) {
const yyyy = String(date.getFullYear());
const MM = pad(date.getMonth() + 1);
const dd = pad(date.getDate());
if (resetType === "none") return "*";
if (resetType === "year") return yyyy;
if (resetType === "day") return yyyy + MM + dd;
return yyyy + MM;
}
const preview = computed(() => {
if (!localRow.value) return "";
const today = new Date();
const startValue = Number(localRow.value.b_start_value) || 1;
const storedValue = Number(localRow.value.b_current_value) || 0;
const activePeriod = currentPeriod(today, localRow.value.b_reset_type);
const sequence = localRow.value.b_current_period === activePeriod ? storedValue + 1 : startValue;
const parts = [];
if (localRow.value.b_prefix) parts.push(localRow.value.b_prefix);
const dateText = formatDate(today, localRow.value.b_date_format);
if (dateText) parts.push(dateText);
parts.push(String(sequence).padStart(Number(localRow.value.b_sequence_width) || 1, "0"));
return parts.join(localRow.value.b_separator || "");
});
</script>
<template>
<div class="auto-code-panel">
<div class="panel-body">
<a-form layout="vertical" class="auto-code-form" :disabled="loading">
<template v-if="localRow">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="业务编号字段" :required="enabled">
<a-select
:value="String(localRow.b_field_id || '')"
:options="fieldOptions"
:disabled="loading"
@update:value="(value) => setRowValue('b_field_id', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="固定前缀">
<a-input
:value="localRow.b_prefix"
:disabled="loading"
:maxlength="100"
@update:value="(value) => setRowValue('b_prefix', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="日期格式">
<a-auto-complete
:value="localRow.b_date_format"
:options="DATE_FORMAT_OPTIONS"
:disabled="loading"
@update:value="(value) => setRowValue('b_date_format', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="分隔符">
<a-input
:value="localRow.b_separator"
:disabled="loading"
:maxlength="10"
@update:value="(value) => setRowValue('b_separator', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="流水号位数" required>
<a-input-number
:value="localRow.b_sequence_width"
:disabled="loading"
:min="1"
:max="18"
:precision="0"
@update:value="(value) => setRowValue('b_sequence_width', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="起始值" required>
<a-input-number
:value="localRow.b_start_value"
:disabled="loading"
:min="0"
:precision="0"
@update:value="(value) => setRowValue('b_start_value', value)"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="重置周期" required>
<a-select
:value="localRow.b_reset_type"
:options="RESET_OPTIONS"
:disabled="loading"
@update:value="(value) => setRowValue('b_reset_type', value)"
/>
</a-form-item>
</a-col>
<a-col :span="16">
<a-form-item label="编号预览">
<div class="code-preview">{{ preview }}</div>
</a-form-item>
</a-col>
</a-row>
</template>
</a-form>
</div>
</div>
</template>
<style scoped lang="scss">
.auto-code-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-body {
flex: 1;
overflow: auto;
padding: 16px 20px;
}
.auto-code-form {
max-width: 960px;
}
.code-preview {
min-height: 32px;
padding: 4px 11px;
border: 1px solid var(--fms-border, #d9d9d9);
border-radius: 6px;
background: var(--fms-bg, #f5f5f5);
color: var(--fms-text, #1f2937);
font-family: Consolas, monospace;
line-height: 22px;
}
</style>
@@ -47,7 +47,6 @@ function onRowOrderChange(rows) {
const availableFields = computed(() => {
const options = [];
for (const field of props.fields || []) {
if (field.b_canuse !== 1) continue;
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
}
return options;
@@ -78,6 +77,13 @@ const columns = shallowRef([
options: () => availableFields.value,
},
},
{
dataIndex: "b_visible",
title: "显示",
width: 60,
align: "center",
editor: { type: "checkbox" },
},
{
dataIndex: "b_required",
title: "必填",
@@ -46,7 +46,11 @@ watch(
);
function emitUpdate() {
emit("update", localFields.value);
const rows = [];
for (const row of localFields.value) {
rows.push({ ...row });
}
emit("update", rows);
}
// 编辑器已直接更新当前行,这里只向父级同步草稿。
@@ -47,7 +47,6 @@ function onRowOrderChange(rows) {
const availableFields = computed(() => {
const options = [];
for (const field of props.fields || []) {
if (field.b_canuse !== 1) continue;
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
}
return options;
@@ -59,7 +59,6 @@ function onRowOrderChange(rows) {
const availableFields = computed(() => {
const options = [];
for (const field of props.fields || []) {
if (field.b_canuse !== 1) continue;
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
}
return options;
@@ -27,8 +27,10 @@ import ModuleFieldsPanel from "./components/ModuleFieldsPanel.vue";
import ModuleListConfigPanel from "./components/ModuleListConfigPanel.vue";
import ModuleEditConfigPanel from "./components/ModuleEditConfigPanel.vue";
import ModuleQueryConfigPanel from "./components/ModuleQueryConfigPanel.vue";
import ModuleAutoCodePanel from "./components/ModuleAutoCodePanel.vue";
import ModulePowersPanel from "./components/ModulePowersPanel.vue";
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
import { createAutoCodeDraft } from "./autoCode";
// ---- 状态 ----
const appStore = useAppStore();
@@ -63,6 +65,7 @@ const originals = ref({
listConfig: [],
editConfig: [],
queryConfig: [],
autoCode: [],
powers: [],
});
@@ -74,6 +77,7 @@ const drafts = ref({
listConfig: [],
editConfig: [],
queryConfig: [],
autoCode: [],
powers: [],
});
@@ -90,6 +94,7 @@ function setI18nPanelRef(instance) {
// 当前选中模块对象(从 drafts.module 获取)
const selectedModule = computed(() => drafts.value.module);
const autoCodeEnabled = computed(() => Number(drafts.value.autoCode?.[0]?.b_canuse) === 1);
const busy = computed(() => configLoading.value || configSaving.value || configSyncing.value);
const syncDisabled = computed(
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
@@ -185,6 +190,7 @@ const visibleTabs = computed(() => {
{ key: "list", tab: "列表配置" },
{ key: "edit", tab: "编辑配置" },
{ key: "query", tab: "查询配置" },
{ key: "auto-code", tab: "自动编码" },
);
}
if (type === "page") {
@@ -252,6 +258,7 @@ async function loadModuleConfig(moduleId) {
let listResponse;
let editResponse;
let queryResponse;
let autoCodeResponse;
let powersResponse;
if (moduleType === "data") {
@@ -262,6 +269,7 @@ async function loadModuleConfig(moduleId) {
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
loadDataApi("s_module_auto_code", `b_module_id = ${id}`, "b_id ASC"),
]);
moduleResponse = responses[0];
fieldsResponse = responses[1];
@@ -269,6 +277,7 @@ async function loadModuleConfig(moduleId) {
listResponse = responses[3];
editResponse = responses[4];
queryResponse = responses[5];
autoCodeResponse = responses[6];
} else if (moduleType === "page") {
const responses = await Promise.all([
loadDataApi("s_module", `b_id = ${id}`),
@@ -289,6 +298,7 @@ async function loadModuleConfig(moduleId) {
listConfig: listResponse ? listResponse.data || [] : [],
editConfig: editResponse ? editResponse.data || [] : [],
queryConfig: queryResponse ? queryResponse.data || [] : [],
autoCode: autoCodeResponse ? autoCodeResponse.data || [] : [],
powers: powersResponse ? powersResponse.data || [] : [],
};
originals.value = original;
@@ -309,11 +319,20 @@ async function loadModuleConfig(moduleId) {
}
// ---- 树节点选择 ----
function onTreeSelect(keys) {
function onTreeSelect(keys, info) {
if (busy.value) return;
if (keys.length > 0) {
selectedModuleId.value = String(keys[0]);
}
const nodeKey = info?.node?.key ?? keys[0];
if (nodeKey == null) return;
const key = String(nodeKey);
selectedModuleId.value = key;
const node = findNode(key);
if (!node?.children?.length) return;
const expandedKeys = expandedTreeKeys.value.map(String);
expandedTreeKeys.value = expandedKeys.includes(key)
? expandedKeys.filter((item) => item !== key)
: [...expandedKeys, key];
}
watch(selectedModuleId, (id) => {
@@ -337,6 +356,128 @@ function onUpdate(section, newData) {
drafts.value[section] = newData;
}
function isFieldEnabled(field) {
if (!field) return false;
return Number(field.b_canuse) === 1;
}
function applyDisabledFieldDefaults(previousFields, nextFields) {
const previousEnabledStates = new Map();
for (const field of previousFields || []) {
if (field.b_id != null) {
previousEnabledStates.set(String(field.b_id), isFieldEnabled(field));
}
}
const disabledFieldIds = new Set();
for (const field of nextFields || []) {
if (field.b_id == null) continue;
const fieldId = String(field.b_id);
if (previousEnabledStates.get(fieldId) && !isFieldEnabled(field)) {
disabledFieldIds.add(fieldId);
}
}
if (disabledFieldIds.size === 0) return;
for (const row of drafts.value.listConfig || []) {
if (disabledFieldIds.has(String(row.b_field_id))) row.b_visible = 0;
}
for (const row of drafts.value.editConfig || []) {
if (disabledFieldIds.has(String(row.b_field_id))) row.b_visible = 0;
}
for (const row of drafts.value.queryConfig || []) {
if (disabledFieldIds.has(String(row.b_field_id))) row.b_canuse = 0;
}
}
function onFieldsUpdate(newFields) {
applyDisabledFieldDefaults(drafts.value.fields, newFields);
drafts.value.fields = newFields;
}
function onAutoCodeUpdate(rows) {
const previousRule = drafts.value.autoCode?.[0];
const previousFieldId =
previousRule && Number(previousRule.b_canuse) === 1 ? previousRule.b_field_id : null;
drafts.value.autoCode = rows;
const rule = rows && rows.length ? rows[0] : null;
const nextFieldId = rule && Number(rule.b_canuse) === 1 ? rule.b_field_id : null;
if (previousFieldId != null && String(previousFieldId) !== String(nextFieldId)) {
const original = (originals.value.editConfig || []).find(
(config) => String(config.b_field_id) === String(previousFieldId),
);
for (const config of drafts.value.editConfig || []) {
if (String(config.b_field_id) === String(previousFieldId)) {
config.b_readonly = Number(original?.b_readonly) === 1 ? 1 : 0;
}
}
}
if (nextFieldId == null) return;
for (const config of drafts.value.editConfig || []) {
if (String(config.b_field_id) === String(nextFieldId)) {
config.b_readonly = 1;
}
}
}
function setAutoCodeEnabled(enabled) {
const current = drafts.value.autoCode?.[0];
const rule = current
? { ...current, b_canuse: enabled ? 1 : 0 }
: createAutoCodeDraft(selectedModule.value?.b_id, drafts.value.fields, enabled);
onAutoCodeUpdate([rule]);
}
function validateAutoCode() {
const rows = drafts.value.autoCode || [];
const rule = rows.length ? rows[0] : null;
if (!rule || Number(rule.b_canuse) !== 1) return "";
if (!String(selectedModule.value?.b_savetable || "").trim()) {
return "启用自动编码前请先配置保存表";
}
const field = (drafts.value.fields || []).find(
(item) => String(item.b_id) === String(rule.b_field_id),
);
if (!field || Number(field.b_canuse) !== 1) return "请选择已启用的业务编号字段";
if (field.b_type !== "input" && field.b_type !== "textarea") {
return "业务编号字段必须使用文本控件类型";
}
if (!Number.isInteger(Number(rule.b_start_value)) || Number(rule.b_start_value) < 0) {
return "起始值必须是大于或等于 0 的整数";
}
if (!["none", "year", "month", "day"].includes(rule.b_reset_type)) {
return "请选择正确的重置周期";
}
const width = Number(rule.b_sequence_width);
if (!Number.isInteger(width) || width < 1 || width > 18) {
return "流水号位数必须是 1 到 18 之间的整数";
}
if (String(rule.b_prefix || "").length > 100) return "固定前缀长度不能超过 100 个字符";
if (String(rule.b_separator || "").length > 10) return "分隔符长度不能超过 10 个字符";
const dateFormat = String(rule.b_date_format || "").trim();
if (dateFormat.length > 30) return "日期格式长度不能超过 30 个字符";
const dateLiterals = dateFormat
.replaceAll("yyyy", "")
.replaceAll("yy", "")
.replaceAll("MM", "")
.replaceAll("dd", "");
if (/[A-Za-z]/.test(dateLiterals)) return "日期格式只支持 yyyy、yy、MM 和 dd";
const hasYear = dateFormat.includes("yyyy") || dateFormat.includes("yy");
const hasMonth = dateFormat.includes("MM");
const hasDay = dateFormat.includes("dd");
if (rule.b_reset_type === "year" && !hasYear) return "按年重置时,日期格式必须包含年份";
if (rule.b_reset_type === "month" && (!hasYear || !hasMonth)) {
return "按月重置时,日期格式必须包含年份和月份";
}
if (rule.b_reset_type === "day" && (!hasYear || !hasMonth || !hasDay)) {
return "按日重置时,日期格式必须包含年月日";
}
for (const config of drafts.value.editConfig || []) {
if (String(config.b_field_id) === String(rule.b_field_id)) config.b_readonly = 1;
}
return "";
}
function onSetField(field, value) {
if (drafts.value.module) {
drafts.value.module[field] = value;
@@ -388,6 +529,7 @@ async function onAddModule(parentId, moduleType = "directory") {
listConfig: [],
editConfig: [],
queryConfig: [],
autoCode: [],
powers: [],
};
drafts.value = cloneData(originals.value);
@@ -469,6 +611,7 @@ async function onDeleteNodeById(moduleId) {
deleteByModuleId("s_module_field_list"),
deleteByModuleId("s_module_field_edit"),
deleteByModuleId("s_module_field_query"),
deleteByModuleId("s_module_auto_code"),
deleteByModuleId("s_module_power"),
deleteByModuleId("s_module_field"),
deleteByModuleId("s_module_field_group"),
@@ -497,6 +640,12 @@ async function saveAll() {
try {
const saveReqs = [];
const autoCodeError = validateAutoCode();
if (autoCodeError) {
message.warning(autoCodeError);
return;
}
// s_module 主表
const moduleData = drafts.value.module;
const moduleOriginal = originals.value.module;
@@ -543,18 +692,25 @@ async function saveAll() {
saveReqs.push(moduleRequest);
}
// 子表:fields, listConfig, editConfig, queryConfig, powers
// 子表:fields, listConfig, editConfig, queryConfig, autoCode, powers
const subTables = [
["s_module_field", "fields"],
["s_module_field_list", "listConfig"],
["s_module_field_edit", "editConfig"],
["s_module_field_query", "queryConfig"],
["s_module_auto_code", "autoCode"],
["s_module_power", "powers"],
];
for (const [table, section] of subTables) {
const current = drafts.value[section] || [];
const previous = originals.value[section] || [];
const changes = diffRows(current, previous);
if (table === "s_module_auto_code") {
changes.updates = changes.updates.map((row) => {
const { b_current_period: _period, b_current_value: _value, ...config } = row;
return config;
});
}
const req = tableChange(table, changes);
if (hasChanges(req)) {
await allocateTemporaryIds([req.inserts || [], req.updates || [], req.deletes || []]);
@@ -654,6 +810,7 @@ function buildMissingDefaultConfigs(module, fields) {
b_required: 0,
b_readonly: 0,
b_disabled: 0,
b_visible: 1,
b_group_id: null,
b_xh: order,
});
@@ -787,6 +944,7 @@ async function syncFields() {
}
}
applyDisabledFieldDefaults(existingFields, mergedFields);
const generated = buildMissingDefaultConfigs(module, mergedFields);
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
let generatedCount = 0;
@@ -898,6 +1056,15 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
: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>
@@ -926,7 +1093,7 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
:module-code="selectedModule?.b_code || ''"
:fields="drafts.fields"
:loading="busy"
@update="(v) => onUpdate('fields', v)"
@update="onFieldsUpdate"
@i18n-saved="onI18nSaved"
/>
<ModuleListConfigPanel
@@ -959,6 +1126,14 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
: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)"
@@ -1,7 +1,93 @@
<script setup>
import { computed, ref } from "vue";
import { message, Modal } from "antdv-next";
import { Trash2 } from "@lucide/vue";
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
import { saveObjectApi } from "@/services/api";
import { useAppStore } from "@/stores/app";
const appStore = useAppStore();
const listRef = ref(null);
const selectedRowKeys = ref([]);
const saving = ref(false);
const rowSelection = computed(() => {
return {
selectedRowKeys: selectedRowKeys.value,
onChange: (keys) => {
selectedRowKeys.value = keys;
},
};
});
async function reloadList() {
selectedRowKeys.value = [];
if (listRef.value) await listRef.value.reload();
}
function batchDelete() {
if (saving.value || !selectedRowKeys.value.length) return;
const keys = [...selectedRowKeys.value];
Modal.confirm({
title: `${appStore.t("common.confirmBatchDelete", "确定删除选中的记录?")} (${keys.length})`,
okText: appStore.t("common.delete", "删除"),
okType: "danger",
cancelText: appStore.t("common.cancel", "取消"),
onOk: async () => {
saving.value = true;
try {
const deletes = keys.map((b_id) => ({ b_id }));
await saveObjectApi([
{
table: "s_log_login",
key_field: "b_id",
inserts: [],
updates: [],
deletes,
},
]);
message.success(appStore.t("common.deleteSuccess", "删除成功"));
await reloadList();
} catch (error) {
message.error(
appStore.t("common.deleteFailed", "删除失败") + ":" + (error?.message || error),
);
} finally {
saving.value = false;
}
},
});
}
</script>
<template>
<FmsModuleListPage />
<div class="login-log-page">
<FmsModuleListPage
ref="listRef"
:row-selection="rowSelection"
@loaded="selectedRowKeys = []"
>
<template #actions="{ loading }">
<a-space>
<a-button
danger
:disabled="loading || saving || !selectedRowKeys.length"
@click="batchDelete"
>
<Trash2 :size="16" />
{{ appStore.t("common.batchDelete", "批量删除")
}}{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : "" }}
</a-button>
</a-space>
</template>
</FmsModuleListPage>
</div>
</template>
<style scoped>
.login-log-page {
width: 100%;
height: 100%;
min-height: 0;
}
</style>
+24 -1
View File
@@ -18,6 +18,7 @@
s_module
|-- parent_id --> s_module
|-- 1:N --------> s_module_field
|-- 0:1 --------> s_module_auto_code --> s_module_field
|-- 1:N --------> s_module_field_group -- parent_id --> s_module_field_group
|-- 1:N --------> s_module_power ---- 1:N ----> b_user_power
`-- 1:N --------> b_user_module
@@ -89,6 +90,27 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
- `b_type` 是展示和编辑的唯一类型来源:Table 按它渲染默认显示器,Form 或可编辑 Table 按它选择控件。
- `select` 是否允许自由输入、选项来源、值字段和显示字段均由 `b_options` 控制。
### `dbo.s_module_auto_code`
每个数据模块最多绑定一条自动业务编号规则。`b_id` 仍是数据库技术主键,自动编码写入模块绑定的业务字段;业务字段自身应在实际保存表上建立唯一索引。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `b_id` | `bigint` | 雪花 ID 主键 |
| `b_module_id` | `bigint` | 所属数据模块 ID,唯一 |
| `b_field_id` | `bigint` | 写入业务编号的字段定义 ID |
| `b_prefix` | `nvarchar(100)` | 固定前缀,可为空 |
| `b_date_format` | `varchar(30)` | 日期格式,可为空 |
| `b_separator` | `nvarchar(10)` | 编码各部分之间的分隔符,可为空 |
| `b_sequence_width` | `int` | 流水号补零位数 |
| `b_reset_type` | `varchar(10)` | `none`、`year`、`month` 或 `day` |
| `b_start_value` | `bigint` | 流水号起始值 |
| `b_current_period` | `varchar(8)` | 当前计数周期 |
| `b_current_value` | `bigint` | 当前已分配流水值 |
| `b_canuse` | `int` | 是否启用 |
取号时锁定规则行并递增 `b_current_value`。编号生成后不允许业务用户修改;取号与业务保存分开时允许断号,但目标业务字段必须建立唯一索引。
### 视图同步规则
字段同步服务仅面向 `b_module_type = 'data'` 且配置了 `b_viewtable` 的模块执行:
@@ -135,7 +157,7 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
### `dbo.s_module_field_edit`
控制字段的通用编辑属性与编辑分组。控件类型、显示规则和类型专属参数统一由 `s_module_field.b_type`、`b_options` 决定;FmsForm 与 FmsTable 自行决定何时使用本配置。
控制字段的通用编辑属性、显示状态与编辑分组。控件类型和类型专属参数统一由 `s_module_field.b_type`、`b_options` 决定;FmsForm 与 FmsTable 自行决定何时使用本配置。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
@@ -145,6 +167,7 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
| `b_required` | `int` | 是否必填,默认 `0` |
| `b_readonly` | `int` | 是否只读,默认 `0` |
| `b_disabled` | `int` | 是否禁用,默认 `0` |
| `b_visible` | `int` | 是否显示,默认 `1` |
| `b_group_id` | `bigint` | 所属编辑分组 ID;不分组时为空 |
| `b_xh` | `int` | 编辑场景中的显示顺序,默认 `0` |