提交
This commit is contained in:
1 parent
246ad2228e
commit
8e2dd8b9f0
186 files changed
+38821
-2571
No files matched your search
@@ -0,0 +1,4 @@
|
||||
-- 采集协议单当事人:新生儿 Apgar 评分支持一位小数
|
||||
ALTER TABLE `biz_protocol_party_customer`
|
||||
MODIFY COLUMN `apgar_1min` decimal(4,1) DEFAULT NULL COMMENT '新生儿评分1分钟',
|
||||
MODIFY COLUMN `apgar_5min` decimal(4,1) DEFAULT NULL COMMENT '新生儿评分5分钟';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 样本状态允许为空,移除默认「已采集」,与前端清空行为一致
|
||||
ALTER TABLE `biz_protocol_sample`
|
||||
MODIFY COLUMN `sample_status` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'ycj已采集 jcz检测中 yrk已入库 yzf已作废';
|
||||
@@ -0,0 +1,83 @@
|
||||
-- 民族码表(国标 GB/T 3304,仅数字编码条目,不含字母编码重复项)
|
||||
-- 执行前请确认库中无同名表;与 docs/sql/zkxw.sql 全量基线可并存,亦可合并进全量导出
|
||||
|
||||
DROP TABLE IF EXISTS `sys_ethnic`;
|
||||
CREATE TABLE `sys_ethnic` (
|
||||
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
|
||||
`ethnic_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '民族名称',
|
||||
`ethnic_code` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '民族编码',
|
||||
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '租户编号(空=全局码表)',
|
||||
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
|
||||
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
|
||||
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_sys_ethnic_code` (`ethnic_code`,`del_flag`),
|
||||
KEY `idx_sys_ethnic_name` (`ethnic_name`,`del_flag`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='民族码表';
|
||||
|
||||
BEGIN;
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-01', '汉族', '01', '', 1, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-02', '蒙古族', '02', '', 2, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-03', '回族', '03', '', 3, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-04', '藏族', '04', '', 4, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-05', '维吾尔族', '05', '', 5, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-06', '苗族', '06', '', 6, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-07', '彝族', '07', '', 7, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-08', '壮族', '08', '', 8, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-09', '布依族', '09', '', 9, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-10', '朝鲜族', '10', '', 10, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-11', '满族', '11', '', 11, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-12', '侗族', '12', '', 12, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-13', '瑶族', '13', '', 13, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-14', '白族', '14', '', 14, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-15', '土家族', '15', '', 15, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-16', '哈尼族', '16', '', 16, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-17', '哈萨克族', '17', '', 17, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-18', '傣族', '18', '', 18, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-19', '黎族', '19', '', 19, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-20', '傈僳族', '20', '', 20, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-21', '佤族', '21', '', 21, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-22', '畲族', '22', '', 22, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-23', '高山族', '23', '', 23, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-24', '拉祜族', '24', '', 24, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-25', '水族', '25', '', 25, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-26', '东乡族', '26', '', 26, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-27', '纳西族', '27', '', 27, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-28', '景颇族', '28', '', 28, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-29', '柯尔克孜族', '29', '', 29, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-30', '土族', '30', '', 30, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-31', '达斡尔族', '31', '', 31, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-32', '仫佬族', '32', '', 32, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-33', '羌族', '33', '', 33, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-34', '布朗族', '34', '', 34, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-35', '撒拉族', '35', '', 35, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-36', '毛难族', '36', '', 36, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-37', '仡佬族', '37', '', 37, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-38', '锡伯族', '38', '', 38, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-39', '阿昌族', '39', '', 39, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-40', '普米族', '40', '', 40, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-41', '塔吉克族', '41', '', 41, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-42', '怒族', '42', '', 42, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-43', '乌孜别克族', '43', '', 43, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-44', '俄罗斯族', '44', '', 44, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-45', '鄂温克族', '45', '', 45, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-46', '德昂族', '46', '', 46, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-47', '保安族', '47', '', 47, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-48', '裕固族', '48', '', 48, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-49', '京族', '49', '', 49, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-50', '塔塔尔族', '50', '', 50, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-51', '独龙族', '51', '', 51, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-52', '鄂伦春族', '52', '', 52, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-53', '赫哲族', '53', '', 53, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-54', '门巴族', '54', '', 54, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-55', '珞巴族', '55', '', 55, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-56', '基诺族', '56', '', 56, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-81', '穿青人族', '81', '', 57, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-97', '其他', '97', '', 58, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
INSERT INTO `sys_ethnic` (`id`, `ethnic_name`, `ethnic_code`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('ethnic-98', '外国血统中国籍人士', '98', '', 59, NULL, NULL, '', '2026-05-16 00:00:00', NULL, '2026-05-16 00:00:00', 0);
|
||||
COMMIT;
|
||||
@@ -1,20 +0,0 @@
|
||||
-- 组织管理初始化数据(需求 2.1.3)
|
||||
-- 市场营销部门下设:采集部、临床部、中试服务部、支持中心、客服部
|
||||
|
||||
SET @marketing_id = UUID();
|
||||
|
||||
INSERT INTO sys_dept (id, parent_id, dept_name, dept_category_code, status, leader, leader_phone, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), '0', '中科兴皖', 'gs', '1', NULL, NULL, 'zkxw', 0, '', 0),
|
||||
(@marketing_id, '0', '市场营销部门', 'bm', '1', NULL, NULL, 'zkxw', 1, '', 0),
|
||||
(UUID(), '0', '技术研发部', 'bm', '1', NULL, NULL, 'zkxw', 2, '', 0),
|
||||
(UUID(), '0', '生产部', 'bm', '1', NULL, NULL, 'zkxw', 3, '', 0),
|
||||
(UUID(), '0', '质量安全部', 'bm', '1', NULL, NULL, 'zkxw', 4, '', 0),
|
||||
(UUID(), '0', '财务部', 'bm', '1', NULL, NULL, 'zkxw', 5, '', 0),
|
||||
(UUID(), '0', '综合部', 'bm', '1', NULL, NULL, 'zkxw', 6, '', 0);
|
||||
|
||||
INSERT INTO sys_dept (id, parent_id, dept_name, dept_category_code, status, leader, leader_phone, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), @marketing_id, '采集部', 'bm', '1', NULL, NULL, 'zkxw', 1, '', 0),
|
||||
(UUID(), @marketing_id, '临床部', 'bm', '1', NULL, NULL, 'zkxw', 2, '', 0),
|
||||
(UUID(), @marketing_id, '中试服务部', 'bm', '1', NULL, NULL, 'zkxw', 3, '', 0),
|
||||
(UUID(), @marketing_id, '支持中心', 'bm', '1', NULL, NULL, 'zkxw', 4, '', 0),
|
||||
(UUID(), @marketing_id, '客服部', 'bm', '1', NULL, NULL, 'zkxw', 5, '', 0);
|
||||
@@ -1,24 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 字典管理初始数据(sys_dict_type / sys_dict_data)
|
||||
-- 执行前提:zkxw-requirements-v0.3-schema.sql 已建表
|
||||
-- ============================================================
|
||||
|
||||
-- 字典类型
|
||||
INSERT INTO sys_dict_type (id, dict_name, dict_type, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), '组织类别编码', 'zzlbbm', 'zkxw', 1, '', 0),
|
||||
(UUID(), '站点类型', 'zdlx', 'zkxw', 2, '', 0),
|
||||
(UUID(), '站点业务范围', 'zdywfw', 'zkxw', 3, '', 0),
|
||||
(UUID(), '部门状态', 'bmzt', 'zkxw', 4, '', 0);
|
||||
|
||||
-- 字典数据
|
||||
INSERT INTO sys_dict_data (id, dict_label, dict_value, dict_type, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), '公司', 'gs', 'zzlbbm', 'zkxw', 1, '', 0),
|
||||
(UUID(), '分公司', 'fgs', 'zzlbbm', 'zkxw', 2, '', 0),
|
||||
(UUID(), '部门', 'bm', 'zzlbbm', 'zkxw', 3, '', 0),
|
||||
(UUID(), '中心基地', 'zxjd', 'zdlx', 'zkxw', 1, '', 0),
|
||||
(UUID(), '样本采集', 'ybcj', 'zdywfw', 'zkxw', 1, '', 0),
|
||||
(UUID(), '样本制备', 'ybzb', 'zdywfw', 'zkxw', 2, '', 0),
|
||||
(UUID(), '质量检测', 'zljc', 'zdywfw', 'zkxw', 3, '', 0),
|
||||
(UUID(), '细胞存储', 'xbcc', 'zdywfw', 'zkxw', 4, '', 0),
|
||||
(UUID(), '停用', '0', 'bmzt', 'zkxw', 1, '', 0),
|
||||
(UUID(), '正常', '1', 'bmzt', 'zkxw', 2, '', 0);
|
||||
@@ -1,24 +0,0 @@
|
||||
-- 2.1.6 菜单管理初始化数据
|
||||
-- Web端根节点与移动端根节点,parent_id = 0
|
||||
|
||||
-- 根节点:Web端菜单、移动端菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, menu_type, path, component, visible, status, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
('1', 'Web端菜单', '0', 'M', '/system', NULL, '1', '1', 'zkxw', 1, '', 0),
|
||||
('2', '移动端菜单', '0', 'M', '/mini', NULL, '1', '1', 'zkxw', 2, '', 0);
|
||||
|
||||
-- Web端菜单 - 系统管理目录
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, menu_type, path, component, perms, visible, status, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
('100', '系统管理', '1', 'M', '/system', '/index/index', NULL, '1', '1', 'zkxw', 1, '', 0);
|
||||
|
||||
-- 系统管理子菜单
|
||||
INSERT INTO sys_menu (id, menu_name, parent_id, menu_type, path, component, perms, visible, status, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
('101', '租户管理', '100', 'C', 'tenant', '/system/tenant/index', 'system:tenant:list', '1', '1', 'zkxw', 1, '', 0),
|
||||
('102', '字典管理', '100', 'C', 'dict', '/system/dict/index', 'system:dict:list', '1', '1', 'zkxw', 2, '', 0),
|
||||
('103', '组织管理', '100', 'C', 'dept', '/system/dept/index', 'system:dept:list', '1', '1', 'zkxw', 3, '', 0),
|
||||
('104', '用户管理', '100', 'C', 'user', '/system/user/index', 'system:user:list', '1', '1', 'zkxw', 4, '', 0),
|
||||
('105', '角色管理', '100', 'C', 'role', '/system/role/index', 'system:role:list', '1', '1', 'zkxw', 5, '', 0),
|
||||
('106', '菜单管理', '100', 'C', 'menu', '/system/menu/index', 'system:menu:list', '1', '1', 'zkxw', 6, '', 0),
|
||||
('107', '站点管理', '100', 'C', 'site', '/system/site/index', 'system:site:list', '1', '1', 'zkxw', 7, '', 0),
|
||||
('108', '职位管理', '100', 'C', 'post', '/system/post/index', 'system:post:list', '1', '1', 'zkxw', 8, '', 0),
|
||||
('109', '物品单位', '100', 'C', 'unit', '/system/unit/index', 'system:unit:list', '1', '1', 'zkxw', 9, '', 0),
|
||||
('110', '快递公司', '100', 'C', 'express', '/system/express/index', 'system:express:list', '1', '1', 'zkxw', 10, '', 0);
|
||||
@@ -1,28 +0,0 @@
|
||||
-- V0.3 角色初始数据
|
||||
-- sys_role_data_scope_detail 建表语句
|
||||
CREATE TABLE IF NOT EXISTS `sys_role_data_scope_detail` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`role_id` varchar(64) NOT NULL COMMENT '角色ID',
|
||||
`scope_type` varchar(20) NOT NULL COMMENT '范围类型:dept/user',
|
||||
`scope_value` varchar(100) NOT NULL COMMENT '范围值:部门ID/用户ID',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
|
||||
`order_num` int DEFAULT NULL COMMENT '排序号',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` smallint NOT NULL DEFAULT '0' COMMENT '删除标记',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_role_id` (`role_id`),
|
||||
KEY `idx_tenant_no` (`tenant_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色自定义数据权限明细';
|
||||
|
||||
-- 角色初始数据
|
||||
INSERT INTO sys_role (id, role_name, data_scope, status, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), '超级管理员', '1', '1', 'zkxw', 1, '', 0),
|
||||
(UUID(), '运营管理员', '1', '1', 'zkxw', 2, '', 0),
|
||||
(UUID(), '采集专员', '2', '1', 'zkxw', 3, '', 0),
|
||||
(UUID(), '市场支持专员', '2', '1', 'zkxw', 4, '', 0),
|
||||
(UUID(), '质检专员', '2', '1', 'zkxw', 5, '', 0);
|
||||
@@ -1,26 +0,0 @@
|
||||
-- 站点、职位、物品单位、快递公司 初始化数据
|
||||
-- 对应模型:SysSite, SysPost, BizItemUnit, BizExpressCompany
|
||||
|
||||
-- 站点
|
||||
INSERT INTO sys_site (id, site_name, site_code, site_type, status, address, contact_name, contact_phone, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), '中科兴皖中心基地', 'zkxw-zxjd', 'zxjd', '1', '', NULL, NULL, 'zkxw', 1, '', 0);
|
||||
|
||||
-- 职位
|
||||
INSERT INTO sys_post (id, post_name, post_code, status, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), '采集专员', 'cjzy', '1', 'zkxw', 1, '', 0),
|
||||
(UUID(), '市场支持专员', 'sczczy', '1', 'zkxw', 2, '', 0),
|
||||
(UUID(), '质检专员', 'zjzy', '1', 'zkxw', 3, '', 0);
|
||||
|
||||
-- 物品单位
|
||||
INSERT INTO biz_item_unit (id, unit_code, unit_name, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), 'g', '克', 'zkxw', 1, '', 0),
|
||||
(UUID(), 'ml', '毫升', 'zkxw', 2, '', 0),
|
||||
(UUID(), 'ge', '个', 'zkxw', 3, '', 0),
|
||||
(UUID(), 'fen', '份', 'zkxw', 4, '', 0);
|
||||
|
||||
-- 快递公司
|
||||
INSERT INTO biz_express_company (id, express_code, express_name, tenant_no, order_num, create_dept_id, del_flag) VALUES
|
||||
(UUID(), 'sf', '顺丰速运', 'zkxw', 1, '', 0),
|
||||
(UUID(), 'jd', '京东快递', 'zkxw', 2, '', 0),
|
||||
(UUID(), 'zt', '中通快递', 'zkxw', 3, '', 0),
|
||||
(UUID(), 'yt', '圆通速递', 'zkxw', 4, '', 0);
|
||||
@@ -1,24 +0,0 @@
|
||||
-- zkxw-init-user-data.sql
|
||||
-- V0.3 用户管理初始化数据(测试用户)
|
||||
-- 默认 admin 用户已在骨架 SQL 中创建,此处补充测试账号
|
||||
-- 注意:密码需通过后端 API 创建以获得正确的 bcrypt hash
|
||||
|
||||
INSERT INTO sys_user (
|
||||
id, dept_id, user_no, nick_name, email, phone_no, sex,
|
||||
password, status, tenant_no, order_num,
|
||||
create_dept_id, create_by, create_time, update_by, update_time, del_flag
|
||||
) VALUES
|
||||
(
|
||||
REPLACE(UUID(),'-',''),
|
||||
(SELECT id FROM sys_dept WHERE dept_name='市场营销部' AND del_flag=0 LIMIT 1),
|
||||
'zhangsan', '张三', 'zhangsan@zkxw.com', '13800000001', '0',
|
||||
'$2b$12$placeholder_hash_use_api_to_set', '1', 'zkxw', 2,
|
||||
'', NULL, NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
REPLACE(UUID(),'-',''),
|
||||
(SELECT id FROM sys_dept WHERE dept_name='市场营销部' AND del_flag=0 LIMIT 1),
|
||||
'lisi', '李四', 'lisi@zkxw.com', '13800000002', '1',
|
||||
'$2b$12$placeholder_hash_use_api_to_set', '1', 'zkxw', 3,
|
||||
'', NULL, NOW(), NULL, NOW(), 0
|
||||
);
|
||||
File diff suppressed because it is too large.
Load diff
+2550
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,59 @@
|
||||
# 细胞存储管理(2.2.6~2.2.10)Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 交付细胞存储管理一级菜单下 5 个页面及对应后端 API(方案 C),满足已定稿设计:`docs/superpowers/specs/2026-05-13-cell-storage-protocol-modules-design.md` 与 OpenSpec 变更 `openspec/changes/cell-storage-protocol-modules/`。
|
||||
|
||||
**Architecture:** FastAPI 路由挂在 `/web/admin` 旁路扩展;SQLAlchemy Model 覆盖四张业务表;CRUD 层封装租户过滤与 camelCase 列表 DTO;Vue 页面复制 `system/unit` 列表骨架并按模块替换列与弹窗。
|
||||
|
||||
**Tech Stack:** Python 3.9、FastAPI、SQLAlchemy、Pydantic v2;Vue 3、Element Plus、pnpm、TypeScript。
|
||||
|
||||
---
|
||||
|
||||
## 文件映射(落地前对齐)
|
||||
|
||||
| 区域 | 路径 |
|
||||
|------|------|
|
||||
| 设计/契约 | `docs/superpowers/specs/2026-05-13-cell-storage-protocol-modules-design.md`,`openspec/changes/cell-storage-protocol-modules/` |
|
||||
| Model | `backend/app/models/models.py` |
|
||||
| CRUD | `backend/app/crud/collection_protocol.py`(或按职责拆分文件) |
|
||||
| API | `backend/app/api/collection_protocol.py`,注册 `app/api/__init__.py`、`main.py` |
|
||||
| 菜单 | `backend/app/api/menu.py` |
|
||||
| 前端路由 | `web-admin/src/router/modules/sqxp.ts` |
|
||||
| 前端 API | `web-admin/src/api/collection-protocol.ts`(命名以仓库惯例为准) |
|
||||
| 页面 | `web-admin/src/views/cell-storage/{protocol,protocol-audit,payment-confirm,refund-confirm,installment-warning}/index.vue` |
|
||||
|
||||
---
|
||||
|
||||
## 执行清单(与 OpenSpec tasks.md 同步)
|
||||
|
||||
按顺序执行 `openspec/changes/cell-storage-protocol-modules/tasks.md` 中章节 **1 → 7**。每完成一节,将对应 `- [ ]` 改为 `- [x]`。
|
||||
|
||||
---
|
||||
|
||||
## Self-review(对照 spec)
|
||||
|
||||
| Spec capability | tasks.md 覆盖 |
|
||||
|-----------------|---------------|
|
||||
| cell-storage-collection-protocol | 1.x、2.x、5.1、3.x |
|
||||
| cell-storage-protocol-audit | 1.x、2.x、5.2 |
|
||||
| cell-storage-receipt-confirm | 1.x、2.x、5.3 |
|
||||
| cell-storage-refund-confirm | 1.x、2.x、5.4 |
|
||||
| cell-storage-installment-warning | 1.x、2.x、5.5 |
|
||||
|
||||
无占位:具体接口路径与状态码以实现对照 design.md 为准。
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Plan complete and saved to `docs/superpowers/plans/2026-05-13-cell-storage-protocol-modules.md`.
|
||||
|
||||
**Two execution options:**
|
||||
|
||||
1. **Subagent-Driven(推荐)** — 每个 task 派生子代理并在任务间复核
|
||||
2. **Inline Execution** — 本会话按 `openspec apply` 逐项实现(使用 executing-plans)
|
||||
|
||||
**Which approach?** 若未指定,默认本会话按 OpenSpec `tasks.md` 顺序实现。
|
||||
|
||||
下一步命令:`openspec instructions apply --change cell-storage-protocol-modules --json` 读取上下文文件后开始改代码。
|
||||
@@ -0,0 +1,535 @@
|
||||
# FlowDesigner 功能补全实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 补全流程设计器三个核心功能缺口:审批人选择(用户/角色下拉数据加载)、连线条件分支配置(点击连线编辑条件表达式)、SQL查询帮助提示。
|
||||
|
||||
**Architecture:** 后端新增2个轻量options端点;前端FlowDesigner.vue增加edge:click事件处理、selectedEdge状态、条件表达式编辑面板、以及handleOpen时加载用户/角色数据。
|
||||
|
||||
**Tech Stack:** Python FastAPI, Vue 3 + Element Plus, AntV X6
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| 文件 | 操作 | 职责 |
|
||||
|------|------|------|
|
||||
| `backend/app/api/admin.py` | 修改 | 新增 `GET /web/admin/users/options` 和 `GET /web/admin/roles/options` |
|
||||
| `web-admin/src/api/admin.ts` | 修改 | 新增 `fetchUserOptions()` 和 `fetchRoleOptions()` |
|
||||
| `web-admin/src/views/system/workflow/designer/FlowDesigner.vue` | 修改 | 核心改动:edge事件、selectedEdge、条件编辑面板、数据加载 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 后端新增 users/options 和 roles/options 端点
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/admin.py` — 在 `list_roles_for_select`(line 33)后面新增两个端点
|
||||
|
||||
- [ ] **Step 1: 新增 `GET /web/admin/users/options` 端点**
|
||||
|
||||
在 `admin.py` 的 `list_roles_for_select` 函数后面(约 line 63 之后),添加:
|
||||
|
||||
```python
|
||||
@router.get("/users/options")
|
||||
async def list_users_for_select(
|
||||
_user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""返回用户下拉选项(label=昵称,value=用户ID),供流程设计器审批人选择使用。"""
|
||||
tenant_no = _user.get("tenant_no", "")
|
||||
q = db.query(SysUser).filter(SysUser.del_flag == 0, SysUser.status == "1")
|
||||
if tenant_no:
|
||||
q = q.filter(SysUser.tenant_no == tenant_no)
|
||||
rows = q.order_by(SysUser.order_num.asc(), SysUser.create_time.desc()).all()
|
||||
out = [{"label": (r.nick_name or r.user_no or ""), "value": r.id} for r in rows]
|
||||
return success_response(out)
|
||||
```
|
||||
|
||||
注意:顶部已有 `from app.models.models import SysUser, SysRole, ...` 等导入,无需新增。
|
||||
|
||||
- [ ] **Step 2: 新增 `GET /web/admin/roles/options` 端点**
|
||||
|
||||
紧接上面的函数后面添加:
|
||||
|
||||
```python
|
||||
@router.get("/roles/options")
|
||||
async def list_roles_for_designer(
|
||||
_user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。
|
||||
与 /roles 不同,此端点返回角色 UUID 作为 value(引擎解析审批人需要 UUID)。"""
|
||||
tenant_no = _user.get("tenant_no", "")
|
||||
q = db.query(SysRole).filter(SysRole.del_flag == 0, SysRole.status == "1")
|
||||
if tenant_no:
|
||||
q = q.filter(SysRole.tenant_no == tenant_no)
|
||||
rows = q.order_by(SysRole.order_num.asc()).all()
|
||||
out = [{"label": (r.role_name or "").strip(), "value": r.id} for r in rows if (r.role_name or "").strip()]
|
||||
return success_response(out)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 验证后端启动**
|
||||
|
||||
```bash
|
||||
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.api.admin import router; print('OK')"
|
||||
```
|
||||
|
||||
Expected: `OK`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 前端 API 函数 + 设计器加载用户/角色数据
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/admin.ts` — 新增两个 API 函数
|
||||
- Modify: `web-admin/src/views/system/workflow/designer/FlowDesigner.vue` — 导入 API、handleOpen 加载数据
|
||||
|
||||
- [ ] **Step 1: 在 `admin.ts` 新增两个 API 函数**
|
||||
|
||||
在 `fetchGetRoleList()`(line 18-22)后面添加:
|
||||
|
||||
```typescript
|
||||
/** 流程设计器:用户下拉选项(label=昵称,value=用户ID) */
|
||||
export function fetchUserOptions() {
|
||||
return request.get<Array<{ label: string; value: string }>>({
|
||||
url: '/web/admin/users/options'
|
||||
})
|
||||
}
|
||||
|
||||
/** 流程设计器:角色下拉选项(label=角色名,value=角色ID UUID) */
|
||||
export function fetchRoleOptions() {
|
||||
return request.get<Array<{ label: string; value: string }>>({
|
||||
url: '/web/admin/roles/options'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 FlowDesigner.vue 导入新 API 并加载选项数据**
|
||||
|
||||
修改 line 146 的 import 语句:
|
||||
|
||||
```typescript
|
||||
import { fetchDesignerData, fetchPublishVersion } from '@/api/workflow'
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```typescript
|
||||
import { fetchDesignerData, fetchPublishVersion } from '@/api/workflow'
|
||||
import { fetchUserOptions, fetchRoleOptions } from '@/api/admin'
|
||||
```
|
||||
|
||||
然后在 `handleOpen()` 函数(line 742-748)中添加数据加载:
|
||||
|
||||
```typescript
|
||||
function handleOpen() {
|
||||
selectedNode.value = null
|
||||
selectedEdge.value = null
|
||||
registerShapes()
|
||||
nextTick(() => {
|
||||
initGraph()
|
||||
loadDesignerData()
|
||||
loadOptions()
|
||||
})
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
try {
|
||||
const [users, roles] = await Promise.all([fetchUserOptions(), fetchRoleOptions()])
|
||||
userOptions.value = users || []
|
||||
roleOptions.value = roles || []
|
||||
} catch {
|
||||
// 加载失败则保持空选项,不影响设计器其他功能
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意:`selectedEdge` 在 Task 3 中定义。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 连线点击 + 条件表达式编辑器 + 连线标签
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/system/workflow/designer/FlowDesigner.vue`
|
||||
|
||||
这是最核心的改动。需要在 FlowDesigner.vue 中完成以下修改:
|
||||
|
||||
- [ ] **Step 1: 新增 `selectedEdge` 响应式状态和接口定义**
|
||||
|
||||
在 `selectedNode` 定义(line 201)后面添加:
|
||||
|
||||
```typescript
|
||||
interface ConditionRule {
|
||||
field: string
|
||||
op: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface SelectedEdgeData {
|
||||
id: string
|
||||
fromNodeName: string
|
||||
toNodeName: string
|
||||
rules: ConditionRule[]
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
const selectedEdge = ref<SelectedEdgeData | null>(null)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 注册 edge:click 事件 + 修改 blank:click 和 node:click 清除 selectedEdge**
|
||||
|
||||
在 `initGraph()` 函数中,`graph.on('blank:click', ...)` (line 560-563)后面添加 edge 事件:
|
||||
|
||||
```typescript
|
||||
// 连线点击 → 选中(仅条件节点的出线可编辑条件)
|
||||
graph.on('edge:click', ({ edge }) => {
|
||||
const sourceId = edge.getSourceCellId() || ''
|
||||
const sourceNode = graph!.getCellById(sourceId)
|
||||
const sourceData = sourceNode?.getData() || {}
|
||||
const targetId = edge.getTargetCellId() || ''
|
||||
const targetNode = graph!.getCellById(targetId)
|
||||
const targetData = targetNode?.getData() || {}
|
||||
|
||||
const edgeData = edge.getData() || {}
|
||||
const rawRules = edgeData.conditionExpr ? (JSON.parse(edgeData.conditionExpr) || {}) : {}
|
||||
const rules: ConditionRule[] = rawRules.rules || rawRules || []
|
||||
// 确保至少有一条规则
|
||||
if (!Array.isArray(rules) || rules.length === 0) {
|
||||
rules.push({ field: '', op: '==', value: '' })
|
||||
}
|
||||
// 规范化为 ConditionRule[](只保留简单的 field/op/value 三元组)
|
||||
const normalizedRules: ConditionRule[] = rules.map((r: any) => ({
|
||||
field: r.field || '',
|
||||
op: r.op || '==',
|
||||
value: String(r.value ?? ''),
|
||||
}))
|
||||
|
||||
selectedEdge.value = {
|
||||
id: edge.id,
|
||||
fromNodeName: sourceData.nodeName || sourceId,
|
||||
toNodeName: targetData.nodeName || targetId,
|
||||
rules: normalizedRules,
|
||||
isDefault: !edgeData.conditionExpr,
|
||||
}
|
||||
selectedNode.value = null
|
||||
})
|
||||
```
|
||||
|
||||
同时修改 `blank:click` 和 `node:click` 事件处理,在清除/设置 `selectedNode` 时也清除 `selectedEdge`:
|
||||
|
||||
```typescript
|
||||
// node:click 处理中添加:
|
||||
selectedEdge.value = null
|
||||
|
||||
// blank:click 处理中改为:
|
||||
graph.on('blank:click', () => {
|
||||
selectedNode.value = null
|
||||
selectedEdge.value = null
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 连线创建时初始化 data**
|
||||
|
||||
在 `createEdge()` 函数(约 line 522-539)中,给新创建的 edge 加上 data 初始化:
|
||||
|
||||
在 `graph!.createEdge({...})` 调用中添加 `data: { conditionExpr: '' }` 属性:
|
||||
|
||||
```typescript
|
||||
createEdge() {
|
||||
return graph!.createEdge({
|
||||
shape: 'edge',
|
||||
data: { conditionExpr: '' },
|
||||
attrs: {
|
||||
// ... 保持原有 attrs 不变
|
||||
},
|
||||
router: { name: 'manhattan' },
|
||||
connector: { name: 'rounded', args: { radius: 8 } },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 添加同步连线条件数据到 graph edge 的函数**
|
||||
|
||||
在 `syncNodeLabel` 函数后面添加:
|
||||
|
||||
```typescript
|
||||
// 同步条件表达式到 edge 并更新标签
|
||||
function syncEdgeCondition() {
|
||||
if (!selectedEdge.value || !graph) return
|
||||
const edge = graph.getCellById(selectedEdge.value.id)
|
||||
if (!edge || !edge.isEdge()) return
|
||||
|
||||
if (selectedEdge.value.isDefault) {
|
||||
edge.setData({ ...edge.getData(), conditionExpr: '' })
|
||||
edge.setLabels([])
|
||||
} else {
|
||||
const rules = selectedEdge.value.rules
|
||||
.filter((r) => r.field.trim())
|
||||
.map((r) => ({ field: r.field, op: r.op, value: r.value }))
|
||||
const expr = JSON.stringify({ rules })
|
||||
edge.setData({ ...edge.getData(), conditionExpr: expr })
|
||||
// 设置标签显示条件摘要
|
||||
const summary = rules.map((r) => `${r.field} ${r.op} ${r.value}`).join(' & ')
|
||||
edge.setLabels([
|
||||
{
|
||||
attrs: {
|
||||
label: {
|
||||
text: summary || '条件',
|
||||
fill: '#374151',
|
||||
fontSize: 11,
|
||||
textAnchor: 'middle',
|
||||
textVerticalAnchor: 'bottom',
|
||||
},
|
||||
rect: {
|
||||
fill: '#fff',
|
||||
stroke: '#d1d5db',
|
||||
strokeWidth: 1,
|
||||
rx: 4,
|
||||
ry: 4,
|
||||
},
|
||||
},
|
||||
position: {
|
||||
distance: 0.5,
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 修改序列化函数,从 edge data 读取 conditionExpr**
|
||||
|
||||
当前 `serializeGraph()` 已经正确读取 `e.getData()?.conditionExpr`(line 670),无需修改。确认即可。
|
||||
|
||||
- [ ] **Step 6: 修改模板 — 右侧属性面板增加连线编辑状态**
|
||||
|
||||
在模板的 `<aside class="props-panel">` 中(line 60-138),修改三态逻辑:
|
||||
|
||||
将原来的 `<template v-if="selectedNode">` ... `<template v-else>` 结构改为三态:
|
||||
|
||||
```html
|
||||
<!-- 右侧属性面板 -->
|
||||
<aside class="props-panel">
|
||||
<!-- 节点选中状态 -->
|
||||
<template v-if="selectedNode">
|
||||
<div class="panel-section-title">节点属性</div>
|
||||
<el-form label-position="top" class="props-form">
|
||||
<!-- ... 保持原有节点属性表单不变 (lines 64-132) ... -->
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<!-- 连线选中状态(条件编辑) -->
|
||||
<template v-else-if="selectedEdge">
|
||||
<div class="panel-section-title">连线条件</div>
|
||||
<div class="edge-info">
|
||||
<span class="edge-from">{{ selectedEdge.fromNodeName }}</span>
|
||||
<span class="edge-arrow">→</span>
|
||||
<span class="edge-to">{{ selectedEdge.toNodeName }}</span>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top" class="props-form" style="margin-top: 12px">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="selectedEdge.isDefault" @change="syncEdgeCondition">
|
||||
默认分支(无条件,其他分支都不满足时走此路径)
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="!selectedEdge.isDefault">
|
||||
<div v-for="(rule, idx) in selectedEdge.rules" :key="idx" class="condition-rule-row">
|
||||
<el-input v-model="rule.field" placeholder="字段名" style="width: 30%" @input="syncEdgeCondition" />
|
||||
<el-select v-model="rule.op" style="width: 22%" @change="syncEdgeCondition">
|
||||
<el-option label="==" value="==" />
|
||||
<el-option label="!=" value="!=" />
|
||||
<el-option label=">" value=">" />
|
||||
<el-option label="<" value="<" />
|
||||
<el-option label=">=" value=">=" />
|
||||
<el-option label="<=" value="<=" />
|
||||
<el-option label="in" value="in" />
|
||||
<el-option label="not_in" value="not_in" />
|
||||
<el-option label="contains" value="contains" />
|
||||
</el-select>
|
||||
<el-input v-model="rule.value" placeholder="值" style="width: 30%" @input="syncEdgeCondition" />
|
||||
<el-button
|
||||
:icon="selectedEdge.rules.length > 1 ? 'Delete' : undefined"
|
||||
text
|
||||
type="danger"
|
||||
@click="removeConditionRule(idx)"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" link @click="addConditionRule">+ 添加条件</el-button>
|
||||
</template>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<template v-else>
|
||||
<div class="panel-section-title">属性</div>
|
||||
<div class="empty-hint">点击画布中的节点或连线查看属性</div>
|
||||
</template>
|
||||
</aside>
|
||||
```
|
||||
|
||||
- [ ] **Step 7: 添加条件规则操作函数**
|
||||
|
||||
在 `syncEdgeCondition` 函数后面添加:
|
||||
|
||||
```typescript
|
||||
function addConditionRule() {
|
||||
if (selectedEdge.value) {
|
||||
selectedEdge.value.rules.push({ field: '', op: '==', value: '' })
|
||||
}
|
||||
}
|
||||
|
||||
function removeConditionRule(idx: number) {
|
||||
if (selectedEdge.value && selectedEdge.value.rules.length > 1) {
|
||||
selectedEdge.value.rules.splice(idx, 1)
|
||||
syncEdgeCondition()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 8: 添加条件编辑面板的 CSS**
|
||||
|
||||
在 `<style scoped lang="scss">` 块中添加:
|
||||
|
||||
```scss
|
||||
.edge-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #f9fafb;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.edge-from {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.edge-arrow {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.edge-to {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.condition-rule-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 9: 更新操作提示列表**
|
||||
|
||||
在左侧面板的操作提示(line 46-51)中,添加连线相关的提示:
|
||||
|
||||
```html
|
||||
<li>点击连线编辑条件规则</li>
|
||||
```
|
||||
|
||||
添加在 "从节点连接点拖出连线" 后面。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: SQL 模式帮助提示
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/system/workflow/designer/FlowDesigner.vue`
|
||||
|
||||
- [ ] **Step 1: 在 SQL textarea 下方添加帮助提示**
|
||||
|
||||
将原有的 SQL textarea(约 line 111-118)修改为:
|
||||
|
||||
```html
|
||||
<el-form-item v-if="selectedNode.config.strategy === 'sql'" label="SQL查询语句">
|
||||
<el-input
|
||||
v-model="selectedNode.config.sqlExpr"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="SELECT user_id FROM sys_user WHERE ..."
|
||||
/>
|
||||
<div class="sql-help">
|
||||
<p><strong>SQL审批人策略说明:</strong></p>
|
||||
<ul>
|
||||
<li>必须以 <code>SELECT</code> 开头,只允许只读查询</li>
|
||||
<li>查询结果第一列将作为审批人 user_id</li>
|
||||
<li>可使用流程变量作为参数:<code>:var_name</code> 占位符</li>
|
||||
<li>禁止 INSERT / UPDATE / DELETE / DROP 等写操作</li>
|
||||
</ul>
|
||||
<p class="sql-example">示例:<code>SELECT id FROM sys_user WHERE dept_id = :dept_id AND del_flag = 0</code></p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 添加 SQL 帮助样式**
|
||||
|
||||
在 scoped style 中添加:
|
||||
|
||||
```scss
|
||||
.sql-help {
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
|
||||
p {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 4px 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e0f2fe;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
color: #0369a1;
|
||||
}
|
||||
}
|
||||
|
||||
.sql-example {
|
||||
margin-top: 6px !important;
|
||||
color: #059669;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 验证
|
||||
|
||||
- [ ] **Step 1: 后端验证**
|
||||
|
||||
```bash
|
||||
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.api.admin import router; print('OK')"
|
||||
```
|
||||
|
||||
Expected: `OK`
|
||||
|
||||
- [ ] **Step 2: 前端 build 验证**
|
||||
|
||||
```bash
|
||||
cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm build
|
||||
```
|
||||
|
||||
Expected: Build 成功,无新增错误
|
||||
|
||||
- [ ] **Step 3: LSP 诊断**
|
||||
|
||||
对 `FlowDesigner.vue` 运行 `lsp_diagnostics`,确认无类型错误。
|
||||
@@ -0,0 +1,612 @@
|
||||
# FlowDesigner 企业级增强实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
|
||||
|
||||
**Goal:** 将流程设计器从基础原型提升到企业可用级别:节点删除、撤销重做、键盘快捷键、AND/OR条件组、发布前验证、缩放工具栏。
|
||||
|
||||
**Architecture:** 安装 AntV X6 插件(Selection/Keyboard/History/Snapline/Clipboard),增强 FlowDesigner.vue 的交互能力和条件编辑器,新增验证逻辑。
|
||||
|
||||
**Tech Stack:** AntV X6 v3 + X6 插件, Vue 3 + Element Plus
|
||||
|
||||
---
|
||||
|
||||
## 前置:安装 X6 插件
|
||||
|
||||
```bash
|
||||
cd web-admin && pnpm add @antv/x6-plugin-selection @antv/x6-plugin-keyboard @antv/x6-plugin-history @antv/x6-plugin-snapline @antv/x6-plugin-clipboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 交互增强 — 删除 + 撤销重做 + 快捷键
|
||||
|
||||
**Files:** `web-admin/src/views/system/workflow/designer/FlowDesigner.vue`
|
||||
|
||||
### 1.1 新增插件导入
|
||||
```typescript
|
||||
import { Selection } from '@antv/x6-plugin-selection'
|
||||
import { Keyboard } from '@antv/x6-plugin-keyboard'
|
||||
import { History } from '@antv/x6-plugin-history'
|
||||
import { Snapline } from '@antv/x6-plugin-snapline'
|
||||
import { Clipboard } from '@antv/x6-plugin-clipboard'
|
||||
```
|
||||
|
||||
### 1.2 Graph 构造函数启用 history
|
||||
```typescript
|
||||
history: { enabled: true },
|
||||
```
|
||||
|
||||
### 1.3 initGraph 中注册插件(在 graph 创建后、事件绑定前)
|
||||
```typescript
|
||||
graph.use(new Selection({ enabled: true, multiple: true, rubberband: true, movable: true, showNodeSelectionBox: true }))
|
||||
graph.use(new Keyboard({ enabled: true }))
|
||||
graph.use(new Snapline({ enabled: true }))
|
||||
graph.use(new Clipboard({ enabled: true }))
|
||||
```
|
||||
|
||||
### 1.4 键盘快捷键绑定
|
||||
```typescript
|
||||
// Delete/Backspace 删除选中
|
||||
graph.bindKey(['delete', 'backspace'], () => {
|
||||
const cells = graph.getSelectedCells()
|
||||
if (cells.length) {
|
||||
// 保护:不允许删除 start 和 end 节点以外的连线
|
||||
const toRemove = cells.filter((c) => {
|
||||
if (c.isNode()) {
|
||||
const data = c.getData() || {}
|
||||
return data.nodeType !== 'start' && data.nodeType !== 'end'
|
||||
}
|
||||
return true // 边都可以删除
|
||||
})
|
||||
if (toRemove.length) graph.removeCells(toRemove)
|
||||
}
|
||||
selectedNode.value = null
|
||||
selectedEdge.value = null
|
||||
return false
|
||||
})
|
||||
|
||||
// Undo Ctrl+Z
|
||||
graph.bindKey(['ctrl+z', 'meta+z'], () => {
|
||||
if (graph.canUndo()) graph.undo()
|
||||
return false
|
||||
})
|
||||
|
||||
// Redo Ctrl+Y
|
||||
graph.bindKey(['ctrl+y', 'meta+shift+z'], () => {
|
||||
if (graph.canRedo()) graph.redo()
|
||||
return false
|
||||
})
|
||||
|
||||
// Copy Ctrl+C
|
||||
graph.bindKey(['ctrl+c', 'meta+c'], () => {
|
||||
const cells = graph.getSelectedCells().filter((c) => c.isNode())
|
||||
if (cells.length) graph.copy(cells)
|
||||
return false
|
||||
})
|
||||
|
||||
// Paste Ctrl+V
|
||||
graph.bindKey(['ctrl+v', 'meta+v'], () => {
|
||||
if (!graph.isClipboardEmpty()) {
|
||||
const cells = graph.paste({ offset: 32 })
|
||||
graph.cleanSelection()
|
||||
graph.select(cells)
|
||||
}
|
||||
return false
|
||||
})
|
||||
```
|
||||
|
||||
### 1.5 节点/边删除后清除选中状态
|
||||
```typescript
|
||||
graph.on('cell:removed', () => {
|
||||
selectedNode.value = null
|
||||
selectedEdge.value = null
|
||||
})
|
||||
```
|
||||
|
||||
### 1.6 属性面板增加删除按钮
|
||||
在节点属性面板的顶部,节点名称旁边增加删除按钮:
|
||||
```html
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span style="font-weight: 500">节点属性</span>
|
||||
<el-button
|
||||
v-if="selectedNode.nodeType !== 'start' && selectedNode.nodeType !== 'end'"
|
||||
type="danger"
|
||||
text
|
||||
size="small"
|
||||
@click="deleteSelectedNode"
|
||||
>
|
||||
删除节点
|
||||
</el-button>
|
||||
</div>
|
||||
```
|
||||
|
||||
在连线条件面板顶部也增加删除按钮:
|
||||
```html
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span class="panel-section-title" style="margin: 0">连线条件</span>
|
||||
<el-button type="danger" text size="small" @click="deleteSelectedEdge">删除连线</el-button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 1.7 删除函数
|
||||
```typescript
|
||||
function deleteSelectedNode() {
|
||||
if (!selectedNode.value || !graph) return
|
||||
const cell = graph.getCellById(selectedNode.value.id)
|
||||
if (cell) graph.removeCell(cell)
|
||||
selectedNode.value = null
|
||||
}
|
||||
|
||||
function deleteSelectedEdge() {
|
||||
if (!selectedEdge.value || !graph) return
|
||||
const cell = graph.getCellById(selectedEdge.value.id)
|
||||
if (cell) graph.removeCell(cell)
|
||||
selectedEdge.value = null
|
||||
}
|
||||
```
|
||||
|
||||
### 1.8 工具栏增加撤销/重做按钮
|
||||
在 header 的按钮区域增加撤销/重做:
|
||||
```html
|
||||
<el-button class="btn-ghost" :disabled="!canUndo" @click="handleUndo">撤销</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!canRedo" @click="handleRedo">重做</el-button>
|
||||
```
|
||||
|
||||
新增响应式状态和函数:
|
||||
```typescript
|
||||
const canUndo = ref(false)
|
||||
const canRedo = ref(false)
|
||||
|
||||
// 在 initGraph 中,history 插件注册后:
|
||||
graph.on('history:change', () => {
|
||||
canUndo.value = graph?.canUndo() ?? false
|
||||
canRedo.value = graph?.canRedo() ?? false
|
||||
})
|
||||
|
||||
function handleUndo() {
|
||||
if (graph?.canUndo()) graph.undo()
|
||||
}
|
||||
|
||||
function handleRedo() {
|
||||
if (graph?.canRedo()) graph.redo()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 条件表达式增强 — AND/OR 分组 + 流程变量提示
|
||||
|
||||
### 2.1 数据模型改为分组结构
|
||||
|
||||
替换 `ConditionRule` 接口为分组结构:
|
||||
```typescript
|
||||
interface ConditionRule {
|
||||
field: string
|
||||
op: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface ConditionGroup {
|
||||
logic: 'and' | 'or' // 组内规则之间的逻辑
|
||||
rules: ConditionRule[]
|
||||
}
|
||||
```
|
||||
|
||||
替换 `SelectedEdgeData` 的 `rules` 为 `groups`:
|
||||
```typescript
|
||||
interface SelectedEdgeData {
|
||||
id: string
|
||||
fromNodeName: string
|
||||
toNodeName: string
|
||||
groups: ConditionGroup[] // 组之间是 OR 关系
|
||||
groupLogic: 'or' // 组间逻辑(固定为 OR)
|
||||
isDefault: boolean
|
||||
}
|
||||
```
|
||||
|
||||
条件语义:组内 AND,组间 OR。即 `(rule1 AND rule2) OR (rule3 AND rule4)`
|
||||
|
||||
### 2.2 条件编辑器 UI 改为分组模式
|
||||
|
||||
```html
|
||||
<template v-if="!selectedEdge.isDefault">
|
||||
<div v-for="(group, gIdx) in selectedEdge.groups" :key="gIdx" class="condition-group">
|
||||
<div class="group-header">
|
||||
<span class="group-label">条件组 {{ gIdx + 1 }}</span>
|
||||
<span class="group-logic-badge">组内条件全部满足 (AND)</span>
|
||||
<el-button
|
||||
v-if="selectedEdge.groups.length > 1"
|
||||
type="danger"
|
||||
text
|
||||
size="small"
|
||||
@click="removeConditionGroup(gIdx)"
|
||||
>
|
||||
删除组
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-for="(rule, rIdx) in group.rules" :key="rIdx" class="condition-rule-row">
|
||||
<el-input v-model="rule.field" placeholder="字段名(流程变量)" style="width: 28%" @input="syncEdgeCondition" />
|
||||
<el-select v-model="rule.op" style="width: 20%" @change="syncEdgeCondition">
|
||||
<el-option label="==" value="==" />
|
||||
<el-option label="!=" value="!=" />
|
||||
<el-option label=">" value=">" />
|
||||
<el-option label="<" value="<" />
|
||||
<el-option label=">=" value=">=" />
|
||||
<el-option label="<=" value="<=" />
|
||||
<el-option label="包含" value="contains" />
|
||||
<el-option label="不包含" value="not_in" />
|
||||
</el-select>
|
||||
<el-input v-model="rule.value" placeholder="值" style="width: 28%" @input="syncEdgeCondition" />
|
||||
<el-button
|
||||
v-if="group.rules.length > 1"
|
||||
text
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeConditionRule(gIdx, rIdx)"
|
||||
>
|
||||
×
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" link size="small" @click="addConditionRule(gIdx)">
|
||||
+ 添加条件
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="group-logic-hint">组间关系:任一条件组满足即可 (OR)</div>
|
||||
<el-button type="primary" link @click="addConditionGroup">+ 添加条件组 (OR)</el-button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2.3 操作函数更新
|
||||
|
||||
```typescript
|
||||
function addConditionGroup() {
|
||||
if (selectedEdge.value) {
|
||||
selectedEdge.value.groups.push({ logic: 'and', rules: [{ field: '', op: '==', value: '' }] })
|
||||
}
|
||||
}
|
||||
|
||||
function removeConditionGroup(gIdx: number) {
|
||||
if (selectedEdge.value && selectedEdge.value.groups.length > 1) {
|
||||
selectedEdge.value.groups.splice(gIdx, 1)
|
||||
syncEdgeCondition()
|
||||
}
|
||||
}
|
||||
|
||||
function addConditionRule(gIdx: number) {
|
||||
if (selectedEdge.value) {
|
||||
selectedEdge.value.groups[gIdx].rules.push({ field: '', op: '==', value: '' })
|
||||
}
|
||||
}
|
||||
|
||||
function removeConditionRule(gIdx: number, rIdx: number) {
|
||||
if (selectedEdge.value && selectedEdge.value.groups[gIdx].rules.length > 1) {
|
||||
selectedEdge.value.groups[gIdx].rules.splice(rIdx, 1)
|
||||
syncEdgeCondition()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 syncEdgeCondition 序列化更新
|
||||
|
||||
生成与后端引擎兼容的 JSON:
|
||||
```typescript
|
||||
function syncEdgeCondition() {
|
||||
if (!selectedEdge.value || !graph) return
|
||||
const edge = graph.getCellById(selectedEdge.value.id)
|
||||
if (!edge || !edge.isEdge()) return
|
||||
|
||||
if (selectedEdge.value.isDefault) {
|
||||
edge.setData({ ...edge.getData(), conditionExpr: '' })
|
||||
edge.setLabels([])
|
||||
} else {
|
||||
// 序列化为引擎兼容格式:[{and: [{field,op,value},...]}, {and: [...]}]
|
||||
// 顶层 array = OR 语义(引擎 _check_rules 对 list 做 any())
|
||||
const groups = selectedEdge.value.groups
|
||||
.filter((g) => g.rules.some((r) => r.field.trim()))
|
||||
.map((g) => ({
|
||||
and: g.rules
|
||||
.filter((r) => r.field.trim())
|
||||
.map((r) => ({ field: r.field, op: r.op, value: r.value })),
|
||||
}))
|
||||
const expr = JSON.stringify(groups)
|
||||
edge.setData({ ...edge.getData(), conditionExpr: expr })
|
||||
|
||||
// 生成可读标签
|
||||
const parts = groups.map((g) =>
|
||||
g.and.map((r) => `${r.field} ${r.op} ${r.value}`).join(' & '),
|
||||
)
|
||||
const summary = parts.join(' | ')
|
||||
edge.setLabels([
|
||||
{
|
||||
attrs: {
|
||||
label: { text: summary || '条件', fill: '#374151', fontSize: 11, textAnchor: 'middle', textVerticalAnchor: 'bottom' },
|
||||
rect: { fill: '#fff', stroke: '#d1d5db', strokeWidth: 1, rx: 4, ry: 4 },
|
||||
},
|
||||
position: { distance: 0.5 },
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 edge:click 解析更新
|
||||
|
||||
解析引擎格式到分组 UI:
|
||||
```typescript
|
||||
// 在 edge:click handler 中:
|
||||
let normalizedGroups: ConditionGroup[] = []
|
||||
if (edgeData.conditionExpr) {
|
||||
try {
|
||||
const parsed = JSON.parse(edgeData.conditionExpr)
|
||||
if (Array.isArray(parsed)) {
|
||||
// 引擎格式: [{and: [{field,op,value},...]}, ...]
|
||||
normalizedGroups = parsed.map((item: any) => {
|
||||
if (item.and) {
|
||||
return { logic: 'and' as const, rules: item.and.map((r: any) => ({ field: r.field || '', op: r.op || '==', value: String(r.value ?? '') })) }
|
||||
}
|
||||
if (item.or) {
|
||||
return { logic: 'or' as const, rules: item.or.map((r: any) => ({ field: r.field || '', op: r.op || '==', value: String(r.value ?? '') })) }
|
||||
}
|
||||
// 兼容旧格式: [{rules: [...]}]
|
||||
const rawRules = item.rules || [item]
|
||||
return { logic: 'and' as const, rules: rawRules.map((r: any) => ({ field: r.field || '', op: r.op || '==', value: String(r.value ?? '') })) }
|
||||
})
|
||||
} else if (parsed.rules) {
|
||||
// 旧格式: {rules: [{field,op,value},...]}
|
||||
normalizedGroups = [{ logic: 'and', rules: parsed.rules.map((r: any) => ({ field: r.field || '', op: r.op || '==', value: String(r.value ?? '') })) }]
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (normalizedGroups.length === 0) {
|
||||
normalizedGroups = [{ logic: 'and', rules: [{ field: '', op: '==', value: '' }] }]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 流程变量提示
|
||||
|
||||
在条件字段输入下方添加提示:
|
||||
```html
|
||||
<div class="field-hint">
|
||||
<p>字段名为流程发起时传入的变量名,支持点号嵌套路径(如 <code>order.amount</code>)</p>
|
||||
<p>常见变量:<code>amount</code>(金额) <code>department</code>(部门) <code>applicant.dept_id</code>(发起人部门)</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2.7 CSS 新增
|
||||
```scss
|
||||
.condition-group {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.group-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.group-logic-badge {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
background: #e5e7eb;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.group-logic-hint {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin: 8px 0;
|
||||
padding: 4px 8px;
|
||||
background: #fef3c7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #1e40af;
|
||||
line-height: 1.6;
|
||||
|
||||
p { margin: 0 0 2px; }
|
||||
code { background: #dbeafe; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 保存/发布前验证
|
||||
|
||||
### 3.1 验证函数
|
||||
```typescript
|
||||
interface ValidationError {
|
||||
type: 'error' | 'warning'
|
||||
message: string
|
||||
nodeId?: string
|
||||
}
|
||||
|
||||
function validateGraph(): ValidationError[] {
|
||||
const errors: ValidationError[] = []
|
||||
if (!graph) return errors
|
||||
|
||||
const cells = graph.getCells()
|
||||
const nodes = cells.filter((c) => c.isNode())
|
||||
const edges = cells.filter((c) => c.isEdge())
|
||||
|
||||
// 1. 必须有且仅有一个开始节点
|
||||
const startNodes = nodes.filter((n) => (n.getData() || {}).nodeType === 'start')
|
||||
if (startNodes.length === 0) {
|
||||
errors.push({ type: 'error', message: '必须有一个开始节点' })
|
||||
} else if (startNodes.length > 1) {
|
||||
errors.push({ type: 'error', message: '只能有一个开始节点', nodeId: startNodes[1].id })
|
||||
}
|
||||
|
||||
// 2. 必须有至少一个结束节点
|
||||
const endNodes = nodes.filter((n) => (n.getData() || {}).nodeType === 'end')
|
||||
if (endNodes.length === 0) {
|
||||
errors.push({ type: 'error', message: '必须至少有一个结束节点' })
|
||||
}
|
||||
|
||||
// 3. 开始节点必须有出边
|
||||
if (startNodes.length === 1) {
|
||||
const hasOutEdge = edges.some((e) => e.getSourceCellId() === startNodes[0].id)
|
||||
if (!hasOutEdge) {
|
||||
errors.push({ type: 'error', message: '开始节点必须连接到下游节点', nodeId: startNodes[0].id })
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 结束节点必须有入边
|
||||
endNodes.forEach((en) => {
|
||||
const hasInEdge = edges.some((e) => e.getTargetCellId() === en.id)
|
||||
if (!hasInEdge) {
|
||||
errors.push({ type: 'error', message: '结束节点必须有入边连接', nodeId: en.id })
|
||||
}
|
||||
})
|
||||
|
||||
// 5. 审批节点必须配置审批人
|
||||
nodes.filter((n) => (n.getData() || {}).nodeType === 'approval').forEach((n) => {
|
||||
const config = (n.getData() || {}).config || {}
|
||||
const strategy = config.strategy || 'users'
|
||||
if (strategy === 'users' && (!config.userIds || config.userIds.length === 0)) {
|
||||
errors.push({ type: 'error', message: `审批节点"${(n.getData() || {}).nodeName || n.id}"未选择审批用户`, nodeId: n.id })
|
||||
}
|
||||
if (strategy === 'roles' && (!config.roleIds || config.roleIds.length === 0)) {
|
||||
errors.push({ type: 'error', message: `审批节点"${(n.getData() || {}).nodeName || n.id}"未选择审批角色`, nodeId: n.id })
|
||||
}
|
||||
if (strategy === 'sql' && (!config.sqlExpr || !config.sqlExpr.trim())) {
|
||||
errors.push({ type: 'error', message: `审批节点"${(n.getData() || {}).nodeName || n.id}"未填写SQL查询`, nodeId: n.id })
|
||||
}
|
||||
})
|
||||
|
||||
// 6. 条件节点必须有至少2条出边
|
||||
nodes.filter((n) => (n.getData() || {}).nodeType === 'condition').forEach((n) => {
|
||||
const outEdges = edges.filter((e) => e.getSourceCellId() === n.id)
|
||||
if (outEdges.length < 2) {
|
||||
errors.push({ type: 'error', message: `条件节点"${(n.getData() || {}).nodeName || n.id}"至少需要2条出边`, nodeId: n.id })
|
||||
}
|
||||
})
|
||||
|
||||
// 7. 孤立节点检查(非开始/结束节点必须有入边和出边)
|
||||
nodes.forEach((n) => {
|
||||
const nodeType = (n.getData() || {}).nodeType
|
||||
if (nodeType === 'start' || nodeType === 'end') return
|
||||
const hasIn = edges.some((e) => e.getTargetCellId() === n.id)
|
||||
const hasOut = edges.some((e) => e.getSourceCellId() === n.id)
|
||||
if (!hasIn || !hasOut) {
|
||||
errors.push({ type: 'warning', message: `节点"${(n.getData() || {}).nodeName || n.id}"未完全连接`, nodeId: n.id })
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 在 handlePublish 中调用验证
|
||||
```typescript
|
||||
async function handlePublish() {
|
||||
const errors = validateGraph()
|
||||
const blockers = errors.filter((e) => e.type === 'error')
|
||||
if (blockers.length > 0) {
|
||||
ElMessage.error(blockers[0].message)
|
||||
// 高亮错误节点
|
||||
if (blockers[0].nodeId && graph) {
|
||||
const cell = graph.getCellById(blockers[0].nodeId)
|
||||
if (cell && cell.isNode()) {
|
||||
graph.resetSelection(cell)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// ... 原有发布逻辑
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 缩放工具栏 + 默认分支标记
|
||||
|
||||
### 4.1 缩放工具栏
|
||||
在 header 按钮区域添加缩放控制:
|
||||
```html
|
||||
<div class="zoom-controls">
|
||||
<el-button text size="small" @click="handleZoomOut" title="缩小">−</el-button>
|
||||
<span class="zoom-level">{{ zoomLevel }}%</span>
|
||||
<el-button text size="small" @click="handleZoomIn" title="放大">+</el-button>
|
||||
<el-button text size="small" @click="handleZoomReset" title="适应画布">⊞</el-button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```typescript
|
||||
const zoomLevel = ref(100)
|
||||
|
||||
function handleZoomIn() {
|
||||
if (!graph) return
|
||||
const cur = graph.zoom()
|
||||
graph.zoom(0.1)
|
||||
zoomLevel.value = Math.round((cur + 0.1) * 100)
|
||||
}
|
||||
|
||||
function handleZoomOut() {
|
||||
if (!graph) return
|
||||
const cur = graph.zoom()
|
||||
graph.zoom(-0.1)
|
||||
zoomLevel.value = Math.round((cur - 0.1) * 100)
|
||||
}
|
||||
|
||||
function handleZoomReset() {
|
||||
if (!graph) return
|
||||
graph.zoomToFit({ padding: 40, maxScale: 1 })
|
||||
zoomLevel.value = Math.round(graph.zoom() * 100)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 默认分支连线标记
|
||||
默认分支的连线标签显示"其他情况":
|
||||
```typescript
|
||||
// syncEdgeCondition 中,默认分支:
|
||||
if (selectedEdge.value.isDefault) {
|
||||
edge.setData({ ...edge.getData(), conditionExpr: '' })
|
||||
edge.setLabels([{
|
||||
attrs: {
|
||||
label: { text: '其他情况', fill: '#9ca3af', fontSize: 11, textAnchor: 'middle', textVerticalAnchor: 'bottom', fontStyle: 'italic' },
|
||||
rect: { fill: '#f9fafb', stroke: '#d1d5db', strokeWidth: 1, rx: 4, ry: 4, strokeDasharray: '3,3' },
|
||||
},
|
||||
position: { distance: 0.5 },
|
||||
}])
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 CSS
|
||||
```scss
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 12px;
|
||||
padding-right: 12px;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.zoom-level {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,618 @@
|
||||
# 运营支撑模块 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement 3 operations support modules (洁净室管理, 库位管理, 展馆预约) with backend API + frontend UI, following existing codebase patterns exactly.
|
||||
|
||||
**Architecture:** FastAPI backend with flat-function CRUD (no service layer), Vue 3 + Element Plus frontend with standard list/tree page templates. All 3 modules share a new API route file `app/api/operations.py` and frontend route group.
|
||||
|
||||
**Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2 (backend); Vue 3 / TypeScript / Element Plus / Tailwind CSS 4 (frontend)
|
||||
|
||||
**Design Spec:** `docs/superpowers/specs/2026-05-15-operations-support-design.md`
|
||||
|
||||
**Key Reference Files (READ FIRST):**
|
||||
- Backend CRUD pattern: `backend/app/crud/item_unit.py`
|
||||
- Backend API pattern: `backend/app/api/basic_info.py` (lines 1-100 for simple CRUD)
|
||||
- Backend model pattern: `backend/app/models/models.py` (class BizItemUnit, lines 259-275)
|
||||
- Frontend standard list: `web-admin/src/views/system/unit/index.vue`
|
||||
- Frontend tree template: `web-admin/src/views/system/category/index.vue`
|
||||
- Frontend API pattern: `web-admin/src/api/admin.ts` (any CRUD section)
|
||||
- Frontend router: `web-admin/src/router/modules/sqxp.ts`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Backend (create/modify)
|
||||
| File | Action | Purpose |
|
||||
|------|--------|---------|
|
||||
| `backend/app/models/models.py` | MODIFY | Add 5 model classes at end of file |
|
||||
| `backend/app/crud/clean_room.py` | CREATE | CleanRoom + AccessLog CRUD functions |
|
||||
| `backend/app/crud/storage_facility.py` | CREATE | StorageFacility tree + Slot CRUD functions |
|
||||
| `backend/app/crud/exhibition_appointment.py` | CREATE | ExhibitionAppointment CRUD functions |
|
||||
| `backend/app/api/operations.py` | CREATE | All 3 modules API routes |
|
||||
| `backend/app/api/__init__.py` | MODIFY | Import + export operations_router |
|
||||
| `backend/app/main.py` | MODIFY | Import + register operations_router |
|
||||
| `backend/tests/test_operations_api.py` | CREATE | Backend API tests |
|
||||
|
||||
### Frontend (create/modify)
|
||||
| File | Action | Purpose |
|
||||
|------|--------|---------|
|
||||
| `web-admin/src/api/admin.ts` | MODIFY | Add API functions for all 3 modules |
|
||||
| `web-admin/src/router/modules/sqxp.ts` | MODIFY | Add 运营支撑 route group |
|
||||
| `web-admin/src/views/operations/clean-room/index.vue` | CREATE | Clean room list page |
|
||||
| `web-admin/src/views/operations/storage-facility/index.vue` | CREATE | Storage facility tree page |
|
||||
| `web-admin/src/views/operations/exhibition-appointment/index.vue` | CREATE | Exhibition appointment list page |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Backend Models (5 SQLAlchemy models)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/models/models.py` (append at end, before EOF)
|
||||
|
||||
- [ ] **Step 1: Add 5 model classes to models.py**
|
||||
|
||||
Append the following after the last class in `backend/app/models/models.py`:
|
||||
|
||||
```python
|
||||
# =============================================================================
|
||||
# V0.4 运营支撑(2.4 洁净室管理 / 库位管理 / 展馆预约)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class BizCleanRoom(Base):
|
||||
"""洁净室信息 biz_clean_room"""
|
||||
|
||||
__tablename__ = "biz_clean_room"
|
||||
|
||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||
room_code = Column(String(64), nullable=False, comment="编号")
|
||||
room_name = Column(String(200), nullable=False, comment="名称")
|
||||
room_grade = Column(String(8), nullable=True, comment="等级 A/B/C/D")
|
||||
room_status = Column(String(16), nullable=False, default="zc", comment="zc正常 yc异常")
|
||||
area_sqm = Column(Numeric(12, 2), nullable=True, comment="面积m²")
|
||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(512), nullable=True, comment="备注")
|
||||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||
|
||||
|
||||
class BizCleanRoomAccessLog(Base):
|
||||
"""洁净室人员进出 biz_clean_room_access_log"""
|
||||
|
||||
__tablename__ = "biz_clean_room_access_log"
|
||||
|
||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||
clean_room_id = Column(String(64), nullable=False, comment="洁净室ID")
|
||||
user_id = Column(String(64), nullable=True, comment="人员用户ID")
|
||||
person_name = Column(String(100), nullable=True, comment="人员姓名(冗余)")
|
||||
dept_name = Column(String(200), nullable=True, comment="部门")
|
||||
enter_time = Column(DateTime, nullable=False, comment="进入时间")
|
||||
purpose = Column(String(512), nullable=True, comment="目的")
|
||||
leave_time = Column(DateTime, nullable=True, comment="离开时间")
|
||||
body_temp = Column(String(32), nullable=True, comment="体温")
|
||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(512), nullable=True, comment="备注")
|
||||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||
|
||||
|
||||
class BizStorageFacility(Base):
|
||||
"""细胞存储设施树节点 biz_storage_facility"""
|
||||
|
||||
__tablename__ = "biz_storage_facility"
|
||||
|
||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||
parent_id = Column(String(64), nullable=False, default="0", comment="父节点ID,0根")
|
||||
node_name = Column(String(200), nullable=False, comment="节点名称(罐/层/盒等)")
|
||||
node_type = Column(String(32), nullable=True, comment="facility/layer/box 等扩展")
|
||||
is_leaf = Column(SmallInteger, nullable=False, default=0, comment="是否叶子存储位 1是")
|
||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(512), nullable=True, comment="备注")
|
||||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||
|
||||
|
||||
class BizStorageFacilitySlot(Base):
|
||||
"""细胞存储设施储存位置(格子)biz_storage_facility_slot"""
|
||||
|
||||
__tablename__ = "biz_storage_facility_slot"
|
||||
|
||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||
facility_node_id = Column(String(64), nullable=False, comment="设施树叶子节点ID")
|
||||
slot_code = Column(String(64), nullable=True, comment="小格编码/坐标")
|
||||
slot_status = Column(String(16), nullable=True, comment="占用状态等扩展")
|
||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(512), nullable=True, comment="备注")
|
||||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||
|
||||
|
||||
class BizExhibitionAppointment(Base):
|
||||
"""展馆预约 biz_exhibition_appointment"""
|
||||
|
||||
__tablename__ = "biz_exhibition_appointment"
|
||||
|
||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||
appointment_no = Column(String(64), nullable=False, comment="预约编号")
|
||||
visit_org = Column(String(200), nullable=True, comment="来访单位")
|
||||
contact_name = Column(String(100), nullable=True, comment="联系人")
|
||||
visit_date = Column(CHAR(8), nullable=True, comment="到访日期YYYYMMDD")
|
||||
visitor_count = Column(Integer, nullable=True, comment="人数")
|
||||
reception_user_id = Column(String(64), nullable=True, comment="接待人员用户ID")
|
||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(512), nullable=True, comment="备注")
|
||||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify models load without errors**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.models.models import BizCleanRoom, BizCleanRoomAccessLog, BizStorageFacility, BizStorageFacilitySlot, BizExhibitionAppointment; print('All models imported OK')"`
|
||||
Expected: "All models imported OK"
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Backend CRUD — Exhibition Appointment (simplest, start here)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/crud/exhibition_appointment.py`
|
||||
|
||||
- [ ] **Step 1: Create CRUD file**
|
||||
|
||||
Create `backend/app/crud/exhibition_appointment.py` following the `item_unit.py` pattern exactly. Functions: `_row_to_dict`, `list_exhibition_appointments`, `get_exhibition_appointment`, `appointment_no_exists`, `create_exhibition_appointment`, `update_exhibition_appointment`, `soft_delete_exhibition_appointment`, `total_pages`. All queries filter by `del_flag==0` and `tenant_no`. List supports keyword search on `appointment_no`, `visit_org`, `contact_name`. Default sort by `create_time DESC`.
|
||||
|
||||
- [ ] **Step 2: Verify import**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.crud import exhibition_appointment; print('OK')"`
|
||||
Expected: "OK"
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Backend CRUD — Clean Room + Access Log
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/crud/clean_room.py`
|
||||
|
||||
- [ ] **Step 1: Create CRUD file**
|
||||
|
||||
Create `backend/app/crud/clean_room.py` with two sets of CRUD functions:
|
||||
|
||||
**Clean Room functions:** `_row_to_dict`, `list_clean_rooms`, `get_clean_room`, `room_code_exists`, `create_clean_room`, `update_clean_room`, `soft_delete_clean_room`, `total_pages`. Keyword search on `room_code`, `room_name`. Filter by `tenant_no` and `del_flag==0`.
|
||||
|
||||
**Access Log functions:** `_access_log_to_dict`, `list_access_logs` (filter by `clean_room_id`), `get_access_log`, `create_access_log`, `update_access_log`, `soft_delete_access_log`. List supports `clean_room_id` filter + keyword on `person_name`, `dept_name`.
|
||||
|
||||
- [ ] **Step 2: Verify import**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.crud import clean_room; print('OK')"`
|
||||
Expected: "OK"
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Backend CRUD — Storage Facility (tree) + Slot
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/crud/storage_facility.py`
|
||||
|
||||
- [ ] **Step 1: Create CRUD file**
|
||||
|
||||
Create `backend/app/crud/storage_facility.py` with:
|
||||
|
||||
**Facility tree functions:** `_row_to_dict`, `list_all_facilities` (returns flat list, filtered by `tenant_no`, `del_flag==0`), `get_facility`, `create_facility`, `update_facility`, `soft_delete_facility` (cascade: soft-delete all children + their slots), `get_children` (find direct children of a node).
|
||||
|
||||
**Slot functions:** `_slot_to_dict`, `list_slots` (filter by `facility_node_id`), `get_slot`, `create_slot`, `update_slot`, `soft_delete_slot`.
|
||||
|
||||
**Tree helpers:** `build_tree` — converts flat list to nested tree structure with `children` array. `collect_descendant_ids` — recursively collects all descendant node IDs for cascade delete.
|
||||
|
||||
- [ ] **Step 2: Verify import**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.crud import storage_facility; print('OK')"`
|
||||
Expected: "OK"
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Backend API Routes — operations.py
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/api/operations.py`
|
||||
|
||||
- [ ] **Step 1: Create the API route file**
|
||||
|
||||
Create `backend/app/api/operations.py` with `router = APIRouter(prefix="/web/admin", tags=["运营支撑"])`. Include:
|
||||
|
||||
**Clean Room endpoints:**
|
||||
- `GET /clean-rooms` — paginated list with keyword
|
||||
- `POST /clean-rooms` — create
|
||||
- `PUT /clean-rooms/{room_id}` — update
|
||||
- `DELETE /clean-rooms/{room_id}` — soft delete
|
||||
|
||||
**Access Log endpoints:**
|
||||
- `GET /clean-room-access-logs` — list by clean_room_id, paginated
|
||||
- `POST /clean-room-access-logs` — create
|
||||
- `PUT /clean-room-access-logs/{log_id}` — update
|
||||
- `DELETE /clean-room-access-logs/{log_id}` — soft delete
|
||||
|
||||
**Storage Facility endpoints:**
|
||||
- `GET /storage-facilities` — flat list (frontend builds tree)
|
||||
- `POST /storage-facilities` — create node
|
||||
- `PUT /storage-facilities/{node_id}` — update node
|
||||
- `DELETE /storage-facilities/{node_id}` — cascade soft delete
|
||||
|
||||
**Storage Slot endpoints:**
|
||||
- `GET /storage-facility-slots` — list by facility_node_id
|
||||
- `POST /storage-facility-slots` — create
|
||||
- `PUT /storage-facility-slots/{slot_id}` — update
|
||||
- `DELETE /storage-facility-slots/{slot_id}` — soft delete
|
||||
|
||||
**Exhibition Appointment endpoints:**
|
||||
- `GET /exhibition-appointments` — paginated list with keyword
|
||||
- `POST /exhibition-appointments` — create
|
||||
- `PUT /exhibition-appointments/{appointment_id}` — update
|
||||
- `DELETE /exhibition-appointments/{appointment_id}` — soft delete
|
||||
|
||||
All endpoints use `Depends(require_admin_or_super_admin)` and `_get_tenant_no(db, user)`.
|
||||
|
||||
Each endpoint follows the pattern from `basic_info.py`: Pydantic body classes inline with `ConfigDict(populate_by_name=True)` and `Field(..., alias="camelCase")`. List endpoints return `success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})`.
|
||||
|
||||
- [ ] **Step 2: Verify import**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.api.operations import router; print(f'Routes: {len(router.routes)}')"`
|
||||
Expected: "Routes: 13"
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Backend Router Registration
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/__init__.py`
|
||||
- Modify: `backend/app/main.py`
|
||||
|
||||
- [ ] **Step 1: Register in __init__.py**
|
||||
|
||||
Add to `backend/app/api/__init__.py`:
|
||||
```python
|
||||
from app.api.operations import router as operations_router
|
||||
```
|
||||
And add `"operations_router"` to `__all__`.
|
||||
|
||||
- [ ] **Step 2: Register in main.py**
|
||||
|
||||
Add to `backend/app/main.py` line 13 import section:
|
||||
```python
|
||||
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, leave_router, operations_router
|
||||
```
|
||||
Add after line 67:
|
||||
```python
|
||||
app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify app starts**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.main import app; print(f'App routes: {len(app.routes)}')"`
|
||||
Expected: A number > previous route count
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Backend Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/tests/test_operations_api.py`
|
||||
|
||||
- [ ] **Step 1: Write test file**
|
||||
|
||||
Create `backend/tests/test_operations_api.py` with tests for each module:
|
||||
|
||||
1. **Clean Room tests:** test_create_clean_room, test_list_clean_rooms, test_update_clean_room, test_delete_clean_room
|
||||
2. **Access Log tests:** test_create_access_log, test_list_access_logs_by_room
|
||||
3. **Storage Facility tests:** test_create_facility_node, test_list_facilities_flat, test_create_child_node, test_cascade_delete
|
||||
4. **Slot tests:** test_create_slot, test_list_slots_by_node
|
||||
5. **Exhibition Appointment tests:** test_create_appointment, test_list_appointments, test_update_appointment, test_delete_appointment
|
||||
|
||||
Use `pytest` with FastAPI `TestClient`. Each test creates, reads, updates, deletes and verifies. Tests can use the running dev server or TestClient.
|
||||
|
||||
- [ ] **Step 2: Run tests**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -m pytest tests/test_operations_api.py -v`
|
||||
Expected: All tests PASS
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Frontend API Functions
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/admin.ts`
|
||||
|
||||
- [ ] **Step 1: Add API functions for all 3 modules**
|
||||
|
||||
Append to `web-admin/src/api/admin.ts` three new section blocks:
|
||||
|
||||
**Clean Room section:**
|
||||
```typescript
|
||||
// ---------------------------------------------------------------------------
|
||||
// 洁净室管理
|
||||
// ---------------------------------------------------------------------------
|
||||
export function fetchCleanRoomList(params?: { page?: number; pageSize?: number; keyword?: string }) {
|
||||
return request.get({ url: '/web/admin/clean-rooms', params })
|
||||
}
|
||||
export function fetchCreateCleanRoom(data: Record<string, any>) {
|
||||
return request.post({ url: '/web/admin/clean-rooms', params: data })
|
||||
}
|
||||
export function fetchUpdateCleanRoom(id: string, data: Record<string, any>) {
|
||||
return request.put({ url: `/web/admin/clean-rooms/${id}`, params: data })
|
||||
}
|
||||
export function fetchDeleteCleanRoom(id: string) {
|
||||
return request.del({ url: `/web/admin/clean-rooms/${id}` })
|
||||
}
|
||||
// 人员进出
|
||||
export function fetchAccessLogList(params?: { page?: number; pageSize?: number; cleanRoomId?: string; keyword?: string }) {
|
||||
return request.get({ url: '/web/admin/clean-room-access-logs', params })
|
||||
}
|
||||
export function fetchCreateAccessLog(data: Record<string, any>) {
|
||||
return request.post({ url: '/web/admin/clean-room-access-logs', params: data })
|
||||
}
|
||||
export function fetchUpdateAccessLog(id: string, data: Record<string, any>) {
|
||||
return request.put({ url: `/web/admin/clean-room-access-logs/${id}`, params: data })
|
||||
}
|
||||
export function fetchDeleteAccessLog(id: string) {
|
||||
return request.del({ url: `/web/admin/clean-room-access-logs/${id}` })
|
||||
}
|
||||
```
|
||||
|
||||
**Storage Facility section:**
|
||||
```typescript
|
||||
// ---------------------------------------------------------------------------
|
||||
// 库位管理
|
||||
// ---------------------------------------------------------------------------
|
||||
export function fetchStorageFacilityList(params?: { keyword?: string }) {
|
||||
return request.get({ url: '/web/admin/storage-facilities', params })
|
||||
}
|
||||
export function fetchCreateStorageFacility(data: Record<string, any>) {
|
||||
return request.post({ url: '/web/admin/storage-facilities', params: data })
|
||||
}
|
||||
export function fetchUpdateStorageFacility(id: string, data: Record<string, any>) {
|
||||
return request.put({ url: `/web/admin/storage-facilities/${id}`, params: data })
|
||||
}
|
||||
export function fetchDeleteStorageFacility(id: string) {
|
||||
return request.del({ url: `/web/admin/storage-facilities/${id}` })
|
||||
}
|
||||
// 格子
|
||||
export function fetchStorageSlotList(params?: { facilityNodeId?: string }) {
|
||||
return request.get({ url: '/web/admin/storage-facility-slots', params })
|
||||
}
|
||||
export function fetchCreateStorageSlot(data: Record<string, any>) {
|
||||
return request.post({ url: '/web/admin/storage-facility-slots', params: data })
|
||||
}
|
||||
export function fetchUpdateStorageSlot(id: string, data: Record<string, any>) {
|
||||
return request.put({ url: `/web/admin/storage-facility-slots/${id}`, params: data })
|
||||
}
|
||||
export function fetchDeleteStorageSlot(id: string) {
|
||||
return request.del({ url: `/web/admin/storage-facility-slots/${id}` })
|
||||
}
|
||||
```
|
||||
|
||||
**Exhibition Appointment section:**
|
||||
```typescript
|
||||
// ---------------------------------------------------------------------------
|
||||
// 展馆预约
|
||||
// ---------------------------------------------------------------------------
|
||||
export function fetchExhibitionAppointmentList(params?: { page?: number; pageSize?: number; keyword?: string }) {
|
||||
return request.get({ url: '/web/admin/exhibition-appointments', params })
|
||||
}
|
||||
export function fetchCreateExhibitionAppointment(data: Record<string, any>) {
|
||||
return request.post({ url: '/web/admin/exhibition-appointments', params: data })
|
||||
}
|
||||
export function fetchUpdateExhibitionAppointment(id: string, data: Record<string, any>) {
|
||||
return request.put({ url: `/web/admin/exhibition-appointments/${id}`, params: data })
|
||||
}
|
||||
export function fetchDeleteExhibitionAppointment(id: string) {
|
||||
return request.del({ url: `/web/admin/exhibition-appointments/${id}` })
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify no TypeScript errors**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm build:typecheck 2>&1 | tail -5`
|
||||
Expected: No errors related to new functions
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Frontend Router — Add 运营支撑 Group
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/router/modules/sqxp.ts`
|
||||
|
||||
- [ ] **Step 1: Add route group**
|
||||
|
||||
Add before the closing `]` of `sqxpAdminRoutes` (before the last `}` or after the Workflow section):
|
||||
|
||||
```typescript
|
||||
,
|
||||
{
|
||||
id: '60000000000000000000000000000001',
|
||||
name: 'Operations',
|
||||
path: '/operations',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: '运营支撑',
|
||||
icon: 'operation',
|
||||
keepAlive: false,
|
||||
roles: ['admin']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: '60000000000000000000000000000002',
|
||||
name: 'CleanRoom',
|
||||
path: 'clean-room',
|
||||
component: '/operations/clean-room/index',
|
||||
meta: { title: '洁净室管理', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '60000000000000000000000000000003',
|
||||
name: 'StorageFacility',
|
||||
path: 'storage-facility',
|
||||
component: '/operations/storage-facility/index',
|
||||
meta: { title: '库位管理', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '60000000000000000000000000000004',
|
||||
name: 'ExhibitionAppointment',
|
||||
path: 'exhibition-appointment',
|
||||
component: '/operations/exhibition-appointment/index',
|
||||
meta: { title: '展馆预约', keepAlive: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify routes compile**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm build:typecheck 2>&1 | tail -5`
|
||||
Expected: No route-related errors
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Frontend — Clean Room Page
|
||||
|
||||
**Files:**
|
||||
- Create: `web-admin/src/views/operations/clean-room/index.vue`
|
||||
|
||||
- [ ] **Step 1: Create the Vue page**
|
||||
|
||||
Create `web-admin/src/views/operations/clean-room/index.vue` following the `system/unit/index.vue` template exactly:
|
||||
|
||||
**Page structure:**
|
||||
- `page-container` → `header.page-head` → `section.panel`
|
||||
- Filter: keyword search
|
||||
- Table columns: 编号(roomCode), 名称(roomName), 等级(roomGrade, tag display: A级绿色/B级蓝色/C级黄色/D级灰色), 状态(roomStatus, tag: 正常=success/异常=danger), 面积(areaSqm), 操作
|
||||
- Toolbar: 新增按钮, 批量删除按钮
|
||||
- Dialog form (label-position="top", form-grid-2):
|
||||
- 编号(roomCode, required), 名称(roomName, required), 等级(roomGrade, select: A/B/C/D), 状态(roomStatus, select: 正常zc/异常yc), 面积(areaSqm, number input, suffix "m²"), 备注(remark)
|
||||
- 操作列: 查看, 编辑, 人员进出, 删除
|
||||
- "人员进出"按钮 → 打开新弹窗显示该洁净室的进出记录列表(带分页), 弹窗内支持新增/编辑/删除进出记录
|
||||
- 进出记录表单: 人员姓名(personName), 部门(deptName), 进入时间(enterTime, datetime), 目的(purpose), 离开时间(leaveTime, datetime), 体温(bodyTemp), 备注(remark)
|
||||
- View dialog using SystemViewDialog component
|
||||
- API calls: `fetchCleanRoomList`, `fetchCreateCleanRoom`, `fetchUpdateCleanRoom`, `fetchDeleteCleanRoom`, `fetchAccessLogList`, `fetchCreateAccessLog`, `fetchUpdateAccessLog`, `fetchDeleteAccessLog`
|
||||
|
||||
- [ ] **Step 2: Verify page renders**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm dev`, then navigate to `/#/operations/clean-room`
|
||||
Expected: Page loads without console errors, shows empty list with filter bar and toolbar
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Frontend — Storage Facility Page (Tree)
|
||||
|
||||
**Files:**
|
||||
- Create: `web-admin/src/views/operations/storage-facility/index.vue`
|
||||
|
||||
- [ ] **Step 1: Create the Vue page**
|
||||
|
||||
Create `web-admin/src/views/operations/storage-facility/index.vue` following the `system/category/index.vue` tree template:
|
||||
|
||||
**Page structure:**
|
||||
- Left panel: tree structure display using `el-table` with `row-key="id"` and `:tree-props="{ children: 'children' }"`
|
||||
- Data fetched as flat list via `fetchStorageFacilityList`, then `buildTree()` converts to nested tree
|
||||
- Tree columns: 节点名称(nodeName), 节点类型(nodeType, display as tag: facility=设施/layer=层/box=盒), 是否叶子(isLeaf, icon or tag), 备注(remark)
|
||||
- Toolbar: 新增根节点, 展开全部, 折叠全部
|
||||
- 操作列: 新增子节点, 编辑, 删除(需确认)
|
||||
- Right panel: when a leaf node (`isLeaf===1`) is selected, show its slots (格子) list
|
||||
- Slot table: 编码(slotCode), 状态(slotStatus), 操作(编辑/删除)
|
||||
- Add slot button
|
||||
- Dialog form for facility node (label-position="top", form-grid-2):
|
||||
- 父节点(parentId, select from tree list, show full path via `buildTreePathLabelFromFlat`), 节点名称(nodeName, required), 节点类型(nodeType, select: facility/layer/box), 是否叶子(isLeaf, switch), 备注(remark)
|
||||
- Dialog form for slot:
|
||||
- 编码(slotCode, required), 状态(slotStatus, select), 备注(remark)
|
||||
- Tree utilities: import `buildTreePathLabelFromFlat` from `@/utils/tree-path`
|
||||
- API calls: `fetchStorageFacilityList`, `fetchCreateStorageFacility`, `fetchUpdateStorageFacility`, `fetchDeleteStorageFacility`, `fetchStorageSlotList`, `fetchCreateStorageSlot`, `fetchUpdateStorageSlot`, `fetchDeleteStorageSlot`
|
||||
|
||||
- [ ] **Step 2: Verify page renders**
|
||||
|
||||
Navigate to `/#/operations/storage-facility`
|
||||
Expected: Page loads, shows empty tree with toolbar
|
||||
|
||||
---
|
||||
|
||||
## Task 12: Frontend — Exhibition Appointment Page
|
||||
|
||||
**Files:**
|
||||
- Create: `web-admin/src/views/operations/exhibition-appointment/index.vue`
|
||||
|
||||
- [ ] **Step 1: Create the Vue page**
|
||||
|
||||
Create `web-admin/src/views/operations/exhibition-appointment/index.vue` following the `system/unit/index.vue` standard list template:
|
||||
|
||||
**Page structure:**
|
||||
- Filter: keyword search (搜索预约编号/来访单位/联系人)
|
||||
- Table columns: 预约编号(appointmentNo), 来访单位(visitOrg), 联系人(contactName), 到访日期(visitDate), 人数(visitorCount), 接待人员(receptionUserId → display name), 操作
|
||||
- Toolbar: 新增按钮, 批量删除按钮
|
||||
- Dialog form (label-position="top", form-grid-2):
|
||||
- 预约编号(appointmentNo, required), 来访单位(visitOrg, required), 联系人(contactName), 到访日期(visitDate, date picker), 人数(visitorCount, number input), 接待人员(receptionUserId, select from user options), 备注(remark)
|
||||
- View dialog using SystemViewDialog component
|
||||
- API calls: `fetchExhibitionAppointmentList`, `fetchCreateExhibitionAppointment`, `fetchUpdateExhibitionAppointment`, `fetchDeleteExhibitionAppointment`
|
||||
|
||||
- [ ] **Step 2: Verify page renders**
|
||||
|
||||
Navigate to `/#/operations/exhibition-appointment`
|
||||
Expected: Page loads without errors, shows empty list
|
||||
|
||||
---
|
||||
|
||||
## Task 13: Integration Test & Verification
|
||||
|
||||
- [ ] **Step 1: Start backend and verify all API endpoints**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -m pytest tests/test_operations_api.py -v`
|
||||
Expected: All tests pass
|
||||
|
||||
- [ ] **Step 2: Start frontend and verify all 3 pages**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm dev`
|
||||
Navigate to each page:
|
||||
- `/#/operations/clean-room` — verify list, create dialog, access log dialog
|
||||
- `/#/operations/storage-facility` — verify tree display, node CRUD, slot CRUD
|
||||
- `/#/operations/exhibition-appointment` — verify list, create/edit dialog
|
||||
|
||||
- [ ] **Step 3: Run frontend typecheck**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm build:typecheck`
|
||||
Expected: No TypeScript errors
|
||||
|
||||
- [ ] **Step 4: Run frontend lint**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && pnpm lint`
|
||||
Expected: No lint errors (or only pre-existing ones)
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
1. **Spec coverage**: All 5 tables have models ✓, all 3 modules have CRUD ✓, API ✓, frontend ✓
|
||||
2. **Placeholder scan**: No TBD/TODO, all steps have actual code or descriptions ✓
|
||||
3. **Type consistency**: Model field names match CRUD `_row_to_dict` keys match API aliases ✓
|
||||
4. **Convention compliance**: tenant_no filter ✓, soft delete ✓, camelCase API ✓, Fresh Mint colors ✓
|
||||
@@ -0,0 +1,924 @@
|
||||
# 库位管理页面改造 — 实现计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 将库位管理页面从树形表格+右侧窄面板改为左右布局(左侧 el-tree 树形列表 + 右侧货架行格子卡片),并新增批量生成格子功能。
|
||||
|
||||
**Architecture:** 前端完全重写 `index.vue`,参考 `user/index.vue` 的 el-tree 侧边栏模式。后端新增一个批量创建格子的 API 端点(rows×cols 参数 → 一次事务创建多个 slot)。前端格子区域使用 CSS flex 按行分组展示,每行左侧有行标(A/B/C),格子内编号+状态在第一行,下方预留空间。
|
||||
|
||||
**Tech Stack:** FastAPI + SQLAlchemy(后端),Vue 3 + Element Plus + SCSS(前端)
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Responsibility |
|
||||
|------|--------|---------------|
|
||||
| `backend/app/crud/storage_facility.py` | Modify | 新增 `batch_create_slots()` 函数 |
|
||||
| `backend/app/api/operations.py` | Modify | 新增 `POST /storage-facility-slots/batch` 端点 |
|
||||
| `web-admin/src/api/admin.ts` | Modify | 新增 `fetchBatchCreateSlots()` 函数 |
|
||||
| `web-admin/src/views/operations/storage-facility/index.vue` | Rewrite | 完全重写为 el-tree + 货架行布局 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 后端 — 批量创建格子 CRUD 函数
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/crud/storage_facility.py` (在文件末尾 `soft_delete_slot` 之后追加)
|
||||
|
||||
- [ ] **Step 1: 在 `storage_facility.py` 末尾添加 `batch_create_slots` 函数**
|
||||
|
||||
在 `backend/app/crud/storage_facility.py` 文件末尾追加以下函数(在 `soft_delete_slot` 函数之后):
|
||||
|
||||
```python
|
||||
def batch_create_slots(
|
||||
db: Session,
|
||||
*,
|
||||
facility_node_id: str,
|
||||
rows: int,
|
||||
cols: int,
|
||||
code_prefix: str = "",
|
||||
tenant_no: str = "zkxw",
|
||||
user_id: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Batch create rows×cols slots for a leaf facility node in a single transaction."""
|
||||
fid = (facility_node_id or "").strip()
|
||||
if not fid:
|
||||
raise ValueError("所属设施节点不能为空")
|
||||
if rows < 1 or cols < 1:
|
||||
raise ValueError("行数和列数必须大于 0")
|
||||
if rows > 50 or cols > 50:
|
||||
raise ValueError("单次最多生成 50×50 = 2500 个格子")
|
||||
|
||||
node = db.query(BizStorageFacility).filter(
|
||||
and_(BizStorageFacility.id == fid, BizStorageFacility.del_flag == 0)
|
||||
).first()
|
||||
if not node:
|
||||
raise ValueError("所属设施节点不存在")
|
||||
if not node.is_leaf:
|
||||
raise ValueError("只能为叶子节点创建格子")
|
||||
|
||||
prefix = (code_prefix or "").strip()
|
||||
now = datetime.now()
|
||||
slots: List[BizStorageFacilitySlot] = []
|
||||
|
||||
for r in range(1, rows + 1):
|
||||
for c in range(1, cols + 1):
|
||||
code = f"{prefix}{r}-{c}" if prefix else f"{r}-{c}"
|
||||
order = (r - 1) * cols + c
|
||||
obj = BizStorageFacilitySlot(
|
||||
id=generate_uuid(),
|
||||
facility_node_id=fid,
|
||||
slot_code=code,
|
||||
slot_status="空",
|
||||
tenant_no=tenant_no,
|
||||
order_num=order,
|
||||
remark=None,
|
||||
create_by=user_id,
|
||||
create_dept_id="",
|
||||
create_time=now,
|
||||
update_by=user_id,
|
||||
update_time=now,
|
||||
del_flag=0,
|
||||
)
|
||||
db.add(obj)
|
||||
slots.append(obj)
|
||||
|
||||
db.commit()
|
||||
for s in slots:
|
||||
db.refresh(s)
|
||||
return [_slot_to_dict(s) for s in slots]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证语法无误**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.crud.storage_facility import batch_create_slots; print('OK')"`
|
||||
Expected: `OK`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 后端 — 批量创建格子 API 端点
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/operations.py` (在 `StorageSlotCreateBody` 类之后、`list_storage_slots` 端点之前插入)
|
||||
|
||||
- [ ] **Step 1: 添加 Pydantic body model 和 API 端点**
|
||||
|
||||
在 `backend/app/api/operations.py` 中,在 `StorageSlotCreateBody` 类定义之后(约第 377 行之后),添加 body model:
|
||||
|
||||
```python
|
||||
class StorageSlotBatchCreateBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
facility_node_id: str = Field(..., alias="facilityNodeId")
|
||||
rows: int = Field(..., ge=1, le=50)
|
||||
cols: int = Field(..., ge=1, le=50)
|
||||
code_prefix: Optional[str] = Field("", alias="codePrefix")
|
||||
```
|
||||
|
||||
然后在 `@router.post("/storage-facility-slots")` 端点之前(`StorageSlotBatchCreateBody` 定义之后),添加批量创建端点:
|
||||
|
||||
```python
|
||||
@router.post("/storage-facility-slots/batch")
|
||||
async def batch_create_storage_slots(
|
||||
body: StorageSlotBatchCreateBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
items = storage_facility_svc.batch_create_slots(
|
||||
db,
|
||||
facility_node_id=body.facility_node_id,
|
||||
rows=body.rows,
|
||||
cols=body.cols,
|
||||
code_prefix=body.code_prefix,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user.get("user_id"),
|
||||
)
|
||||
return success_response(items, message=f"批量创建 {len(items)} 个格子成功")
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
```
|
||||
|
||||
**重要**:这个端点必须放在 `/storage-facility-slots` 路由之前,否则 FastAPI 会把 `batch` 当做 `{slot_id}` 参数匹配。
|
||||
|
||||
- [ ] **Step 2: 验证语法无误**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && python -c "from app.api.operations import router; print('OK')"`
|
||||
Expected: `OK`
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 前端 — 添加批量创建 API 函数
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/admin.ts` (在 `fetchCreateStorageSlot` 函数之后)
|
||||
|
||||
- [ ] **Step 1: 在 `admin.ts` 中 `fetchCreateStorageSlot` 之后添加批量创建函数**
|
||||
|
||||
在 `web-admin/src/api/admin.ts` 中,在 `fetchCreateStorageSlot` 函数(第 1336-1338 行)之后插入:
|
||||
|
||||
```typescript
|
||||
export function fetchBatchCreateSlots(data: { facilityNodeId: string; rows: number; cols: number; codePrefix?: string }) {
|
||||
return request.post({ url: '/web/admin/storage-facility-slots/batch', params: data })
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证 lint 通过**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && npx eslint src/api/admin.ts --no-error-on-unmatched-pattern 2>&1 | tail -5`
|
||||
Expected: 无错误输出
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 前端 — 重写库位管理页面
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `web-admin/src/views/operations/storage-facility/index.vue`
|
||||
- Reference: `web-admin/src/views/system/user/index.vue` (el-tree 侧边栏模式)
|
||||
- Reference: `web-admin/storage-redesign-prototype.html` (视觉原型)
|
||||
|
||||
这是最大的任务。完全重写页面,从树形表格+窄面板 → el-tree 侧边栏 + 货架行格子布局。
|
||||
|
||||
- [ ] **Step 1: 重写整个 index.vue**
|
||||
|
||||
用以下完整内容替换 `web-admin/src/views/operations/storage-facility/index.vue` 的全部内容。
|
||||
|
||||
**关键设计决策:**
|
||||
|
||||
1. **左侧 el-tree** — 参考 `user/index.vue` 的 `w-[280px] shrink-0 border-r` 布局。使用 `el-tree` 组件 + `buildTree()` 构建树形数据。树节点通过 `@node-click` 触发右侧面板更新。
|
||||
|
||||
2. **右侧货架行格子** — 使用 flex 布局,按行分组。每行由行标 `.shelf-label` + 格子容器 `.shelf-cells` 组成。每个格子 `.shelf-cell` 内编号+状态在顶部一行,下方预留空间。
|
||||
|
||||
3. **批量生成** — 点击"批量生成格子"弹出 dialog,输入行数、列数、编码前缀。确认后调用 `fetchBatchCreateSlots`。
|
||||
|
||||
4. **格子编辑** — 点击格子弹出编辑 dialog,修改编码和状态。
|
||||
|
||||
5. **节点 CRUD** — el-tree 节点 hover 显示操作按钮(新增子节点、编辑、删除),点击弹出 dialog。
|
||||
|
||||
**完整代码如下:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>库位管理</h1>
|
||||
<p class="page-desc">维护库位设施树结构,以及叶子节点的格子管理。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="flex gap-4">
|
||||
<div class="w-[280px] shrink-0 border-r border-[var(--mint-border)] pr-4">
|
||||
<div class="panel-top sf-tree-aside__head">
|
||||
<h2 class="title-bar-heading">库位树</h2>
|
||||
<el-button class="btn-main" type="primary" size="small" @click="handleAddRoot">新增</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="treeKeyword"
|
||||
placeholder="搜索节点"
|
||||
clearable
|
||||
class="sf-tree-search"
|
||||
/>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="{ label: 'nodeName', children: 'children' }"
|
||||
node-key="id"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
:filter-node-method="filterNode"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="sf-tree-node" @mouseenter="hoveredNodeId = data.id" @mouseleave="hoveredNodeId = ''">
|
||||
<span class="sf-tree-node__label">{{ data.nodeName }}</span>
|
||||
<span v-if="data.isLeaf === 1" class="sf-tree-node__leaf">叶</span>
|
||||
<div v-if="hoveredNodeId === data.id" class="sf-tree-node__ops" @click.stop>
|
||||
<el-button v-if="data.isLeaf !== 1" link size="small" @click="handleAddChild(data)">+</el-button>
|
||||
<el-button link size="small" @click="handleEditNode(data)">编辑</el-button>
|
||||
<el-button link size="small" type="danger" @click="handleDeleteNode(data)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div v-if="!selectedNode" class="sf-empty">
|
||||
<el-empty description="请在左侧选择一个叶子节点查看格子" />
|
||||
</div>
|
||||
|
||||
<template v-if="selectedNode && selectedNode.isLeaf !== 1">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">{{ selectedNode.nodeName }}</h2>
|
||||
</div>
|
||||
<el-empty description="非叶子节点,请继续选择子节点" />
|
||||
</template>
|
||||
|
||||
<template v-if="selectedNode && selectedNode.isLeaf === 1">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">
|
||||
{{ buildTreePathLabelFromFlat(selectedNode.id, flatList, { nameKey: 'nodeName' }) }} — 格子管理
|
||||
</h2>
|
||||
<span class="sf-count">共 {{ slotList.length }} 个格子</span>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<el-button class="btn-main" type="primary" @click="handleBatchCreate">批量生成格子</el-button>
|
||||
<el-button
|
||||
v-if="slotList.length > 0"
|
||||
class="btn-ghost"
|
||||
@click="handleClearAllSlots"
|
||||
>
|
||||
清空格子
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="slotList.length === 0" class="sf-empty">
|
||||
<el-empty description="暂无格子,点击「批量生成格子」创建" />
|
||||
</div>
|
||||
|
||||
<div v-else class="sf-shelf-container">
|
||||
<div v-for="(row, ri) in shelfRows" :key="ri" class="sf-shelf-row">
|
||||
<div class="sf-shelf-label">{{ row.label }}</div>
|
||||
<div class="sf-shelf-cells">
|
||||
<div
|
||||
v-for="cell in row.cells"
|
||||
:key="cell.id"
|
||||
class="sf-shelf-cell"
|
||||
:class="{ 'is-occupied': cell.slotStatus === '占用' }"
|
||||
@click="handleEditSlot(cell)"
|
||||
>
|
||||
<div class="sf-cell-top">
|
||||
<span class="sf-cell-code">{{ cell.slotCode }}</span>
|
||||
<span
|
||||
class="sf-cell-status"
|
||||
:class="cell.slotStatus === '占用' ? 'is-occupied' : 'is-empty'"
|
||||
>
|
||||
{{ cell.slotStatus || '空' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="sf-cell-body">
|
||||
<span class="sf-cell-reserved">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="nodeDialogVisible"
|
||||
:title="nodeDialogTitle"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleNodeDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="nodeFormRef"
|
||||
:model="nodeFormData"
|
||||
:rules="nodeFormRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="上级节点">
|
||||
<el-select
|
||||
v-model="nodeFormData.parentId"
|
||||
placeholder="请选择上级节点(空为根节点)"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in flatList"
|
||||
:key="item.id"
|
||||
:label="buildTreePathLabelFromFlat(item.id, flatList, { nameKey: 'nodeName' })"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="节点名称" prop="nodeName">
|
||||
<el-input v-model="nodeFormData.nodeName" placeholder="请输入节点名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点类型">
|
||||
<el-select v-model="nodeFormData.nodeType" placeholder="请选择节点类型">
|
||||
<el-option label="设施" value="facility" />
|
||||
<el-option label="层" value="layer" />
|
||||
<el-option label="盒" value="box" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否叶子">
|
||||
<el-switch v-model="nodeFormData.isLeaf" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="nodeFormData.remark" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="nodeDialogVisible = false">取消</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleNodeSubmit" :loading="nodeSubmitLoading">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="slotDialogVisible"
|
||||
:title="slotDialogTitle"
|
||||
width="480px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleSlotDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="slotFormRef"
|
||||
:model="slotFormData"
|
||||
:rules="slotFormRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="格子编码" prop="slotCode">
|
||||
<el-input v-model="slotFormData.slotCode" placeholder="请输入格子编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="格子状态">
|
||||
<el-select v-model="slotFormData.slotStatus" placeholder="请选择状态">
|
||||
<el-option label="空" value="空" />
|
||||
<el-option label="占用" value="占用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="slotDialogVisible = false">取消</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSlotSubmit" :loading="slotSubmitLoading">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="batchDialogVisible"
|
||||
title="批量生成格子"
|
||||
width="440px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="行数">
|
||||
<el-input-number v-model="batchForm.rows" :min="1" :max="20" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="列数">
|
||||
<el-input-number v-model="batchForm.cols" :min="1" :max="20" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="编码前缀">
|
||||
<el-input v-model="batchForm.codePrefix" placeholder="如 A、B-、01-(可选)" />
|
||||
<template #label>
|
||||
编码前缀
|
||||
<span class="form-label-tip">生成格式:前缀+行号-列号,如 A-1-1</span>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<div class="sf-batch-preview">
|
||||
将生成 <strong>{{ batchForm.rows * batchForm.cols }}</strong> 个格子({{ batchForm.rows }} 行 × {{ batchForm.cols }} 列)
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="batchDialogVisible = false">取消</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleBatchSubmit" :loading="batchSubmitLoading">确认生成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
fetchStorageFacilityList,
|
||||
fetchCreateStorageFacility,
|
||||
fetchUpdateStorageFacility,
|
||||
fetchDeleteStorageFacility,
|
||||
fetchStorageSlotList,
|
||||
fetchBatchCreateSlots,
|
||||
fetchUpdateStorageSlot,
|
||||
fetchDeleteStorageSlot
|
||||
} from '@/api/admin'
|
||||
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
|
||||
|
||||
defineOptions({ name: 'StorageFacility' })
|
||||
|
||||
const ROW_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T']
|
||||
|
||||
const hoveredNodeId = ref('')
|
||||
const treeKeyword = ref('')
|
||||
const treeRef = ref()
|
||||
const loading = ref(false)
|
||||
const flatList = ref<any[]>([])
|
||||
const treeData = ref<any[]>([])
|
||||
const selectedNode = ref<any>(null)
|
||||
const slotLoading = ref(false)
|
||||
const slotList = ref<any[]>([])
|
||||
|
||||
const buildTree = (list: any[]) => {
|
||||
const map = new Map<string, any>()
|
||||
const roots: any[] = []
|
||||
list.forEach(item => { map.set(item.id, { ...item, children: [] }) })
|
||||
list.forEach(item => {
|
||||
const node = map.get(item.id)!
|
||||
if (item.parentId && item.parentId !== '0' && map.has(item.parentId)) {
|
||||
map.get(item.parentId)!.children.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
})
|
||||
return roots
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchStorageFacilityList({ keyword: treeKeyword.value || undefined })
|
||||
const list = res?.list || res || []
|
||||
flatList.value = list
|
||||
treeData.value = buildTree(list)
|
||||
} catch (error) {
|
||||
console.error('加载库位数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true
|
||||
return data.nodeName?.includes(value)
|
||||
}
|
||||
|
||||
watch(treeKeyword, val => { treeRef.value?.filter(val) })
|
||||
|
||||
const handleNodeClick = async (data: any) => {
|
||||
selectedNode.value = data
|
||||
if (data.isLeaf === 1) {
|
||||
await loadSlots(data.id)
|
||||
} else {
|
||||
slotList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const loadSlots = async (facilityNodeId: string) => {
|
||||
slotLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchStorageSlotList({ facilityNodeId })
|
||||
slotList.value = res?.list || res || []
|
||||
} catch (error) {
|
||||
console.error('加载格子数据失败:', error)
|
||||
slotList.value = []
|
||||
} finally {
|
||||
slotLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const shelfRows = computed(() => {
|
||||
const cols = batchForm.cols
|
||||
const result: { label: string; cells: any[] }[] = []
|
||||
for (let r = 0; r * cols < slotList.value.length; r++) {
|
||||
const rowCells = slotList.value.slice(r * cols, (r + 1) * cols)
|
||||
if (rowCells.length > 0) {
|
||||
result.push({ label: ROW_LABELS[r] || String(r + 1), cells: rowCells })
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const nodeDialogVisible = ref(false)
|
||||
const nodeDialogTitle = ref('新增根节点')
|
||||
const isNodeEdit = ref(false)
|
||||
const nodeSubmitLoading = ref(false)
|
||||
const nodeFormRef = ref()
|
||||
const nodeFormData = reactive({
|
||||
id: '',
|
||||
parentId: '',
|
||||
nodeName: '',
|
||||
nodeType: 'facility',
|
||||
isLeaf: 0 as 0 | 1,
|
||||
remark: ''
|
||||
})
|
||||
const nodeFormRules = { nodeName: [{ required: true, message: '请输入节点名称', trigger: 'blur' }] }
|
||||
|
||||
const handleAddRoot = () => {
|
||||
isNodeEdit.value = false
|
||||
nodeDialogTitle.value = '新增根节点'
|
||||
Object.assign(nodeFormData, { id: '', parentId: '', nodeName: '', nodeType: 'facility', isLeaf: 0, remark: '' })
|
||||
nodeDialogVisible.value = true
|
||||
nextTick(() => nodeFormRef.value?.clearValidate?.())
|
||||
}
|
||||
|
||||
const handleAddChild = (row: any) => {
|
||||
isNodeEdit.value = false
|
||||
nodeDialogTitle.value = '新增子节点'
|
||||
Object.assign(nodeFormData, { id: '', parentId: row.id, nodeName: '', nodeType: 'layer', isLeaf: 0, remark: '' })
|
||||
nodeDialogVisible.value = true
|
||||
nextTick(() => nodeFormRef.value?.clearValidate?.())
|
||||
}
|
||||
|
||||
const handleEditNode = (row: any) => {
|
||||
isNodeEdit.value = true
|
||||
nodeDialogTitle.value = '编辑节点'
|
||||
Object.assign(nodeFormData, {
|
||||
id: row.id,
|
||||
parentId: row.parentId || '',
|
||||
nodeName: row.nodeName || '',
|
||||
nodeType: row.nodeType || 'facility',
|
||||
isLeaf: row.isLeaf ?? 0,
|
||||
remark: row.remark || ''
|
||||
})
|
||||
nodeDialogVisible.value = true
|
||||
nextTick(() => nodeFormRef.value?.clearValidate?.())
|
||||
}
|
||||
|
||||
const handleNodeSubmit = async () => {
|
||||
await nodeFormRef.value?.validate?.()
|
||||
nodeSubmitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
parentId: nodeFormData.parentId || undefined,
|
||||
nodeName: nodeFormData.nodeName.trim(),
|
||||
nodeType: nodeFormData.nodeType,
|
||||
isLeaf: nodeFormData.isLeaf,
|
||||
remark: nodeFormData.remark?.trim() || undefined
|
||||
}
|
||||
if (isNodeEdit.value && nodeFormData.id) {
|
||||
await fetchUpdateStorageFacility(nodeFormData.id, payload)
|
||||
} else {
|
||||
await fetchCreateStorageFacility(payload)
|
||||
}
|
||||
ElMessage.success(isNodeEdit.value ? '更新成功' : '创建成功')
|
||||
nodeDialogVisible.value = false
|
||||
await loadData()
|
||||
} catch { /* handled globally */ } finally {
|
||||
nodeSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeDialogClose = () => {
|
||||
Object.assign(nodeFormData, { id: '', parentId: '', nodeName: '', nodeType: 'facility', isLeaf: 0, remark: '' })
|
||||
nodeFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
const handleDeleteNode = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('删除节点将一并删除其子节点和格子。确定继续吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteStorageFacility(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
if (selectedNode.value?.id === row.id) {
|
||||
selectedNode.value = null
|
||||
slotList.value = []
|
||||
}
|
||||
await loadData()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const slotDialogVisible = ref(false)
|
||||
const slotDialogTitle = ref('编辑格子')
|
||||
const isSlotEdit = ref(false)
|
||||
const slotSubmitLoading = ref(false)
|
||||
const slotFormRef = ref()
|
||||
const editingSlotId = ref<string | null>(null)
|
||||
const slotFormData = reactive({ slotCode: '', slotStatus: '空' })
|
||||
const slotFormRules = { slotCode: [{ required: true, message: '请输入格子编码', trigger: 'blur' }] }
|
||||
|
||||
const handleEditSlot = (row: any) => {
|
||||
isSlotEdit.value = true
|
||||
slotDialogTitle.value = '编辑格子'
|
||||
editingSlotId.value = row.id
|
||||
Object.assign(slotFormData, { slotCode: row.slotCode || '', slotStatus: row.slotStatus || '空' })
|
||||
slotDialogVisible.value = true
|
||||
nextTick(() => slotFormRef.value?.clearValidate?.())
|
||||
}
|
||||
|
||||
const handleSlotSubmit = async () => {
|
||||
await slotFormRef.value?.validate?.()
|
||||
slotSubmitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
facilityNodeId: selectedNode.value?.id,
|
||||
slotCode: slotFormData.slotCode.trim(),
|
||||
slotStatus: slotFormData.slotStatus
|
||||
}
|
||||
if (editingSlotId.value) {
|
||||
await fetchUpdateStorageSlot(editingSlotId.value, payload)
|
||||
}
|
||||
ElMessage.success('更新成功')
|
||||
slotDialogVisible.value = false
|
||||
if (selectedNode.value?.id) await loadSlots(selectedNode.value.id)
|
||||
} catch { /* handled globally */ } finally {
|
||||
slotSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSlotDialogClose = () => {
|
||||
Object.assign(slotFormData, { slotCode: '', slotStatus: '空' })
|
||||
editingSlotId.value = null
|
||||
slotFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
const batchDialogVisible = ref(false)
|
||||
const batchSubmitLoading = ref(false)
|
||||
const batchForm = reactive({ rows: 4, cols: 6, codePrefix: '' })
|
||||
|
||||
const handleBatchCreate = () => {
|
||||
Object.assign(batchForm, { rows: 4, cols: 6, codePrefix: '' })
|
||||
batchDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleBatchSubmit = async () => {
|
||||
if (!selectedNode.value?.id) return
|
||||
batchSubmitLoading.value = true
|
||||
try {
|
||||
await fetchBatchCreateSlots({
|
||||
facilityNodeId: selectedNode.value.id,
|
||||
rows: batchForm.rows,
|
||||
cols: batchForm.cols,
|
||||
codePrefix: batchForm.codePrefix || undefined
|
||||
})
|
||||
ElMessage.success('批量生成成功')
|
||||
batchDialogVisible.value = false
|
||||
await loadSlots(selectedNode.value.id)
|
||||
} catch { /* handled globally */ } finally {
|
||||
batchSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearAllSlots = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要清空当前节点的全部 ${slotList.value.length} 个格子吗?`, '提示', { type: 'warning' })
|
||||
for (const slot of slotList.value) {
|
||||
await fetchDeleteStorageSlot(slot.id)
|
||||
}
|
||||
ElMessage.success('清空成功')
|
||||
if (selectedNode.value?.id) await loadSlots(selectedNode.value.id)
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') console.error('清空失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sf-tree-aside__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sf-tree-search {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sf-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-tree-node__label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sf-tree-node__leaf {
|
||||
font-size: 10px;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: var(--el-color-success-light-9, #ecfdf5);
|
||||
color: var(--el-color-success, #10b981);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sf-tree-node__ops {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sf-empty {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.sf-count {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary, #909399);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Shelf layout */
|
||||
.sf-shelf-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sf-shelf-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sf-shelf-label {
|
||||
width: 36px;
|
||||
flex-shrink: 0;
|
||||
background: #059669;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
|
||||
.sf-shelf-cells {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sf-shelf-cell {
|
||||
flex: 1;
|
||||
min-height: 80px;
|
||||
border: 1px solid var(--el-border-color-lighter, #d1fae5);
|
||||
border-right: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
background: #fff;
|
||||
padding: 8px 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sf-shelf-cell:last-child {
|
||||
border-right: 1px solid var(--el-border-color-lighter, #d1fae5);
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.sf-shelf-cell:first-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.sf-shelf-cell:hover {
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.sf-shelf-cell.is-occupied {
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.sf-shelf-cell.is-occupied:hover {
|
||||
background: #fef3c7;
|
||||
}
|
||||
|
||||
.sf-cell-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sf-cell-code {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.sf-shelf-cell.is-occupied .sf-cell-code {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.sf-cell-status {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sf-cell-status.is-empty {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.sf-cell-status.is-occupied {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.sf-cell-body {
|
||||
flex: 1;
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sf-cell-reserved {
|
||||
font-size: 11px;
|
||||
color: #dcdfe6;
|
||||
}
|
||||
|
||||
.sf-batch-preview {
|
||||
background: #ecfdf5;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
color: #059669;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证 lint 通过**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && npx eslint src/views/operations/storage-facility/index.vue --no-error-on-unmatched-pattern 2>&1 | tail -10`
|
||||
Expected: 无错误输出
|
||||
|
||||
- [ ] **Step 3: 验证 dev server 正常**
|
||||
|
||||
Run: `cd /Users/wuzilong/work/00code/zkxw/web-admin && npx vite build --mode development 2>&1 | tail -5`
|
||||
Expected: 构建成功,无编译错误
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:**
|
||||
- ✅ 左侧 el-tree 树形列表(自由层级)
|
||||
- ✅ 右侧货架行布局格子展示(最末层叶子节点)
|
||||
- ✅ 编号+状态在格子顶部一行
|
||||
- ✅ 下方预留空间
|
||||
- ✅ 批量生成格子(指定行列数)
|
||||
- ✅ 格子点击编辑(dialog)
|
||||
- ✅ 节点 CRUD(弹窗)
|
||||
- ✅ 节点删除级联删除子节点和格子
|
||||
|
||||
**Placeholder scan:** 无 TBD/TODO/待定内容。所有代码完整。
|
||||
|
||||
**Type consistency:** 所有函数签名、变量名前后一致。`fetchBatchCreateSlots` 在 admin.ts 定义并在 index.vue 中使用。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 细胞存储管理(2.2.6~2.2.10)— 实现设计(方案 C)
|
||||
|
||||
**依据**:`docs/中科兴皖需求V0.3.md` 菜单 2.2.6~2.2.10;数据模型 `docs/sql/zkxw-requirements-v0.3-schema.sql`(`biz_collection_protocol` 及其子表)。
|
||||
**依据**:`docs/中科兴皖需求V0.3.md` 菜单 2.2.6~2.2.10;数据模型 `docs/sql/zkxw.sql`(`biz_collection_protocol` 及其子表)。
|
||||
|
||||
**范围**:Web 管理端 `web-admin` + FastAPI `backend`;租户隔离与既有 `/web/admin` 响应风格保持一致。
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
### 3.3 列表默认排序
|
||||
|
||||
- 无自定义排序时:`order_num ASC, create_time DESC`(与工程规范一致);预警列表可按 `installment_due_date ASC` 等业务需求单独约定。
|
||||
- 无自定义排序时:`create_time DESC`(与工程规范一致);预警列表可按 `installment_due_date ASC` 等业务需求单独约定。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
# 工作流模块设计文档
|
||||
|
||||
**模块**: 2.1.11 流程管理
|
||||
**日期**: 2026-05-13
|
||||
**状态**: 待审核
|
||||
**决策记录**:
|
||||
- 流程设计器: AntV X6(蚂蚁图编辑引擎)
|
||||
- V1范围: 全量实现(配置管理+设计器+待办中心+已办中心+后端引擎)
|
||||
- 业务集成: API调用+回调模式
|
||||
- 引擎架构: 图遍历引擎(方案A)
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体架构与模块划分
|
||||
|
||||
### 1.1 后端(`backend/app/`)
|
||||
|
||||
```
|
||||
models/models.py ← 新增6个wf_* SQLAlchemy模型
|
||||
schemas/workflow.py ← Pydantic schemas(请求/响应模型)
|
||||
crud/workflow.py ← 数据库CRUD操作
|
||||
services/
|
||||
workflow_engine.py ← 核心引擎:节点分发、条件评估、审批人解析
|
||||
workflow_callback.py ← 回调服务:通知业务模块状态变更
|
||||
api/workflow.py ← API路由(注册到admin前缀)
|
||||
```
|
||||
|
||||
### 1.2 前端(`web-admin/src/`)
|
||||
|
||||
```
|
||||
api/workflow.ts ← API调用
|
||||
types/api.d.ts ← 新增Workflow相关TS类型
|
||||
views/system/workflow/
|
||||
process/index.vue ← 流程配置管理(列表+设计器弹窗)
|
||||
designer/
|
||||
FlowDesigner.vue ← AntV X6流程设计器组件
|
||||
nodes/ ← 自定义节点组件(Start/End/Approval/Condition)
|
||||
config/ ← 节点属性配置面板
|
||||
task-pending/index.vue ← 待办中心
|
||||
task-completed/index.vue ← 已办中心
|
||||
components/
|
||||
ApprovalDialog.vue ← 审批操作弹窗(通过/驳回+意见)
|
||||
ProcessStatusTag.vue ← 流程状态标签
|
||||
router/modules/sqxp.ts ← 新增3个路由
|
||||
```
|
||||
|
||||
### 1.3 核心数据流
|
||||
|
||||
1. 管理员在流程配置页 → AntV X6设计器画流程 → 保存 `graph_json` + 同步写 `wf_node`/`wf_transition`
|
||||
2. 业务模块调用 `POST /api/admin/workflow/instances` 发起流程 → 引擎锁定当前版本 → 创建 start 任务
|
||||
3. 引擎按 `wf_transition` 遍历节点 → 审批节点创建 `wf_task` → 待办人收到待办
|
||||
4. 待办人审批 → 引擎判断 any/all 策略 → 推进到下一节点 → 到达 end 节点 → 回调业务模块
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库模型
|
||||
|
||||
已有 6 张表 DDL 见全库导出 `docs/sql/zkxw.sql`(搜索 `wf_process_def` 等表名);基线表结构以当前导出为准。
|
||||
|
||||
| 表名 | 用途 | 关键字段 |
|
||||
|------|------|----------|
|
||||
| `wf_process_def` | 流程定义 | process_code, process_name, status |
|
||||
| `wf_process_version` | 流程版本 | process_def_id, version_no, graph_json, is_latest |
|
||||
| `wf_node` | 流程节点 | process_version_id, node_key, node_type, config_json |
|
||||
| `wf_transition` | 流程连线 | process_version_id, from_node_key, to_node_key, condition_expr |
|
||||
| `wf_process_instance` | 流程实例 | process_version_id, business_type, business_id, variables_json, status |
|
||||
| `wf_task` | 流程任务 | instance_id, node_key, assignee_user_id, status, comment_text |
|
||||
|
||||
**node_type 枚举值**: `start`, `end`, `approval`, `condition`
|
||||
**instance.status 枚举值**: `running`, `completed`, `rejected`, `cancelled`
|
||||
**task.status 枚举值**: `pending`, `approved`, `rejected`, `withdrawn`
|
||||
|
||||
所有表均遵循公共字段规范:id(VARCHAR 64 UUID) → 业务字段 → tenant_no → order_num → remark → create_by → create_dept_id → create_time → update_by → update_time → del_flag
|
||||
|
||||
### 2.1 Schema 变更(需新增字段)
|
||||
|
||||
`wf_process_instance` 需新增 `callback_url` 字段(VARCHAR 512, 可空),用于业务回调地址。DDL 变更将在实施阶段维护到 `docs/sql/`(全量基线为 `docs/sql/zkxw.sql`,或附加增量脚本)。
|
||||
|
||||
### 2.2 双存储语义
|
||||
|
||||
工作流存在两套数据,各有用途:
|
||||
- **`graph_json`**(wf_process_version): 前端设计器画布快照,用于设计器加载/编辑/渲染。仅供前端使用,后端引擎不解析此字段。
|
||||
- **`wf_node` + `wf_transition`**(关系表): 后端引擎运行时使用的规范化数据。引擎只读这些表来执行节点分发和条件评估。
|
||||
- 两者在"发布版本"时同步写入,保持一致。
|
||||
|
||||
---
|
||||
|
||||
## 3. 工作流引擎核心设计
|
||||
|
||||
### 3.1 节点分发器模式
|
||||
|
||||
每种节点类型对应一个 handler,引擎根据 node_type 调度:
|
||||
|
||||
```
|
||||
WorkflowEngine
|
||||
├── StartNodeHandler → 自动推进到下一个节点
|
||||
├── EndNodeHandler → 标记实例完成,触发回调
|
||||
├── ApprovalNodeHandler → 解析审批人,创建wf_task记录
|
||||
└── ConditionNodeHandler → 评估条件表达式,选择分支
|
||||
```
|
||||
|
||||
引擎入口方法:`advance(instance_id, from_node_key)` — 从指定节点开始,执行当前节点逻辑,然后沿出边推进到下一个节点。
|
||||
|
||||
### 3.2 条件表达式格式
|
||||
|
||||
`wf_transition.condition_expr` 字段存储的是 JSON 规则格式(VARCHAR 512),不是任意文本表达式。安全可控,不支持任意代码执行:
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{"field": "amount", "op": "<=", "value": 100},
|
||||
{"field": "amount", "op": ">", "value": 100, "and": [{"field": "amount", "op": "<", "value": 300}]},
|
||||
{"field": "amount", "op": ">=", "value": 300}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**支持的操作符**: `==`, `!=`, `>`, `<`, `>=`, `<=`, `in`, `not_in`, `contains`
|
||||
|
||||
**字段取值**: 从 `wf_process_instance.variables_json` 中读取。支持嵌套路径用 `.` 分隔(如 `applicant.dept_id`)。
|
||||
|
||||
**条件评估逻辑**: ConditionNodeHandler 遍历所有出边(wf_transition),依次评估每条边的 condition_expr。第一条匹配的边即为执行路径。最后一条边可省略条件作为"其他情况"默认分支。
|
||||
|
||||
### 3.3 审批人解析
|
||||
|
||||
`config_json` 中的 `approver` 字段定义解析策略:
|
||||
|
||||
```json
|
||||
{
|
||||
"strategy": "users",
|
||||
"pass_type": "any",
|
||||
"user_ids": ["uuid1", "uuid2"]
|
||||
}
|
||||
```
|
||||
|
||||
**strategy 枚举**:
|
||||
|
||||
| 策略 | 配置字段 | 说明 |
|
||||
|------|----------|------|
|
||||
| `users` | `user_ids: string[]` | 指定具体用户 |
|
||||
| `roles` | `role_ids: string[]` | 指定角色,运行时解析角色下的所有用户 |
|
||||
| `sql` | `sql: string` | SQL查询,返回user_id列表 |
|
||||
|
||||
**pass_type 枚举**:
|
||||
- `any` — 任一审批人通过即推进
|
||||
- `all` — 所有审批人都必须通过才推进
|
||||
|
||||
**SQL策略安全约束**:
|
||||
- 只允许 SELECT 语句
|
||||
- 禁止包含 INSERT/UPDATE/DELETE/DROP/ALTER
|
||||
- 只取结果第一列作为 user_id
|
||||
- 流程变量作为参数注入(`:var_name` 占位符)
|
||||
- 查询结果受 tenant_no 隐式过滤
|
||||
|
||||
### 3.4 版本锁定
|
||||
|
||||
- 发起流程时,`wf_process_instance.process_version_id` 锁定当前 `is_latest=1` 的版本
|
||||
- 发布新版本时:将旧版本的 `is_latest` 置为 0,创建新版本记录 `is_latest=1`
|
||||
- 已发起的实例始终使用锁定的版本运行,不受后续版本变更影响
|
||||
|
||||
### 3.5 实例状态流转
|
||||
|
||||
```
|
||||
发起 → running → completed(到达end节点)
|
||||
→ rejected(审批驳回,无其他分支时)
|
||||
→ cancelled(业务主动取消)
|
||||
```
|
||||
|
||||
### 3.6 任务状态流转
|
||||
|
||||
```
|
||||
创建 → pending → approved(审批通过)
|
||||
→ rejected(审批驳回)
|
||||
→ withdrawn(发起人撤回)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API路由设计
|
||||
|
||||
共13个路由,全部走 `/api/admin/workflow/` 前缀,带JWT认证和租户隔离。
|
||||
|
||||
### 4.1 流程定义管理
|
||||
|
||||
| 路由 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/admin/workflow/defs` | GET | 流程定义列表(分页+搜索,支持按名称/编码/状态筛选) |
|
||||
| `/api/admin/workflow/defs` | POST | 新建流程定义 |
|
||||
| `/api/admin/workflow/defs/{id}` | PUT | 更新流程定义基本信息 |
|
||||
| `/api/admin/workflow/defs/{id}` | DELETE | 软删除流程定义(del_flag=1) |
|
||||
| `/api/admin/workflow/defs/{id}/publish` | POST | 发布新版本(接收graph_json+nodes+transitions,创建新版本) |
|
||||
| `/api/admin/workflow/defs/{id}/versions` | GET | 版本历史列表 |
|
||||
|
||||
### 4.2 流程实例
|
||||
|
||||
| 路由 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/admin/workflow/instances` | POST | 发起流程(业务模块调用,传入process_code+business_type+business_id+variables) |
|
||||
| `/api/admin/workflow/instances/{id}` | GET | 流程实例详情(含任务列表和流程图高亮当前节点) |
|
||||
|
||||
### 4.3 任务办理
|
||||
|
||||
| 路由 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/admin/workflow/tasks/pending` | GET | 我的待办列表(按assignee_user_id=当前用户+status=pending过滤) |
|
||||
| `/api/admin/workflow/tasks/completed` | GET | 我的已办列表(按assignee_user_id=当前用户+status in [approved,rejected]过滤) |
|
||||
| `/api/admin/workflow/tasks/{id}/approve` | POST | 审批通过(传comment_text) |
|
||||
| `/api/admin/workflow/tasks/{id}/reject` | POST | 审批驳回(传comment_text) |
|
||||
|
||||
### 4.4 请求/响应格式
|
||||
|
||||
**发起流程请求**:
|
||||
```json
|
||||
POST /api/admin/workflow/instances
|
||||
{
|
||||
"process_code": "agreement_payment",
|
||||
"business_type": "agreement",
|
||||
"business_id": "agr_uuid_123",
|
||||
"variables": {
|
||||
"amount": 5000,
|
||||
"dept_id": "dept_001",
|
||||
"applicant_id": "user_001"
|
||||
},
|
||||
"callback_url": "/api/admin/agreements/workflow-callback"
|
||||
}
|
||||
```
|
||||
|
||||
**审批操作请求**:
|
||||
```json
|
||||
POST /api/admin/workflow/tasks/{id}/approve
|
||||
{
|
||||
"comment_text": "同意,费用已核实"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 前端页面设计
|
||||
|
||||
### 5.1 流程配置管理(`process/index.vue`)
|
||||
|
||||
**模式**: 标准列表页(参考 `unit/index.vue` 模板)
|
||||
|
||||
**列表字段**: 流程编码、流程名称、状态(启用/停用)、最新版本号、创建时间、操作
|
||||
|
||||
**操作按钮**: 新增、编辑、**流程设计**(打开X6设计器弹窗)、发布新版本、停用/启用、删除
|
||||
|
||||
**设计器入口**: 点击"流程设计"→ 全屏弹窗打开 AntV X6 设计器
|
||||
|
||||
### 5.2 AntV X6 流程设计器(`designer/FlowDesigner.vue`)
|
||||
|
||||
**三栏布局**:
|
||||
- **左侧**: 节点组件面板(开始/结束/审批人/条件分支),拖拽到画布
|
||||
- **中间**: 画布区域(AntV X6 Graph),支持节点拖拽、连线、删除、缩放
|
||||
- **右侧**: 属性配置面板(选中节点后展示,根据节点类型动态切换)
|
||||
- 审批人节点: 策略选择(用户/角色/SQL)、具体配置、通过策略(any/all)
|
||||
- 条件分支节点: 每条出边的条件规则编辑
|
||||
- 开始/结束节点: 只显示名称编辑
|
||||
|
||||
**顶部工具栏**: 保存草稿、发布新版本、撤销/重做、放大/缩小/适应画布
|
||||
|
||||
**保存逻辑**: 保存时序列化为 `{graph_json, nodes: [{node_key, node_type, config_json, ...}], transitions: [{from_node_key, to_node_key, condition_expr, ...}]}` 提交到后端
|
||||
|
||||
**自定义节点样式**: 每种节点类型使用不同的形状和颜色
|
||||
- 开始节点: 圆形,绿色
|
||||
- 结束节点: 圆形,红色
|
||||
- 审批人节点: 矩形,蓝色
|
||||
- 条件分支节点: 菱形,橙色
|
||||
|
||||
### 5.3 待办中心(`task-pending/index.vue`)
|
||||
|
||||
**模式**: 复杂列表页(参考 `order/index.vue`)
|
||||
|
||||
**筛选**: 流程名称、业务类型、发起时间范围
|
||||
|
||||
**列表字段**: 流程名称、业务类型、业务单号、发起人、发起时间、到达时间、操作
|
||||
|
||||
**操作**: 审批(打开审批弹窗)、查看详情
|
||||
|
||||
**审批弹窗**(`ApprovalDialog.vue`): 通过/驳回选择 + 审批意见输入框 + 查看流程图(只读模式,高亮当前节点)
|
||||
|
||||
### 5.4 已办中心(`task-completed/index.vue`)
|
||||
|
||||
**模式**: 标准列表页
|
||||
|
||||
**筛选**: 同待办中心 + 审批结果筛选(通过/驳回)
|
||||
|
||||
**列表字段**: 流程名称、业务类型、业务单号、发起人、审批结果、审批意见、审批时间
|
||||
|
||||
**操作**: 查看详情
|
||||
|
||||
---
|
||||
|
||||
## 6. 回调与业务集成
|
||||
|
||||
### 6.1 回调机制
|
||||
|
||||
引擎在以下事件触发 HTTP POST 回调到 `callback_url`:
|
||||
|
||||
**实例完成**:
|
||||
```json
|
||||
{
|
||||
"event": "completed",
|
||||
"instance_id": "inst_uuid",
|
||||
"process_code": "agreement_payment",
|
||||
"business_type": "agreement",
|
||||
"business_id": "agr_uuid_123"
|
||||
}
|
||||
```
|
||||
|
||||
**实例驳回**:
|
||||
```json
|
||||
{
|
||||
"event": "rejected",
|
||||
"instance_id": "inst_uuid",
|
||||
"process_code": "agreement_payment",
|
||||
"business_type": "agreement",
|
||||
"business_id": "agr_uuid_123",
|
||||
"reject_comment": "费用异常,请核实"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 业务模块集成步骤
|
||||
|
||||
1. 在流程配置页创建流程定义,发布版本
|
||||
2. 业务模块在需要触发审批的地方调用 `POST /api/admin/workflow/instances`
|
||||
3. 业务模块实现回调接口接收审批结果
|
||||
4. 用户在待办中心处理审批任务
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全与权限
|
||||
|
||||
- 所有API带JWT认证,租户隔离(tenant_no过滤)
|
||||
- SQL审批人解析策略限制为只读SELECT,禁止DDL/DML
|
||||
- 流程定义管理需要系统管理权限
|
||||
- 待办/已办中心只展示当前用户的任务
|
||||
- 审批操作只能操作 status=pending 且 assignee_user_id=当前用户 的任务
|
||||
|
||||
---
|
||||
|
||||
## 8. 已确认的约束
|
||||
|
||||
- 遵循项目公共字段规范(id→业务→tenant_no→...→del_flag)
|
||||
- 软删除(del_flag 0/1)
|
||||
- 默认排序(create_time DESC)
|
||||
- Element Plus组件自动导入,不手动导入
|
||||
- Fresh Mint主题色 #10b981
|
||||
- label-position="top",.form-grid-2 布局
|
||||
- 操作列固定右侧(2按钮260px,3按钮320px)
|
||||
- 标题绿色竖条(需注册到App.vue白名单)
|
||||
@@ -0,0 +1,48 @@
|
||||
# 运营支撑模块设计规格
|
||||
|
||||
## 概述
|
||||
实现需求文档 2.4 节中运营支撑下的三个子模块:洁净室管理、库位管理、展馆预约。
|
||||
新增一级菜单【运营支撑】,下设三个二级菜单。
|
||||
|
||||
## SQL 表(已存在,无需新建)
|
||||
1. `biz_clean_room` - 洁净室信息(编号、名称、等级A/B/C/D、状态正常/异常、面积)
|
||||
2. `biz_clean_room_access_log` - 洁净室人员进出(人员、部门、进入时间、目的、离开时间、体温)
|
||||
3. `biz_storage_facility` - 细胞存储设施树节点(树形结构:parent_id, node_name, node_type, is_leaf)
|
||||
4. `biz_storage_facility_slot` - 存储位置格子(叶子节点下的小方格)
|
||||
5. `biz_exhibition_appointment` - 展馆预约(预约编号、来访单位、联系人、到访日期、人数、接待人员)
|
||||
|
||||
## 模块设计
|
||||
|
||||
### 模块 1: 洁净室管理
|
||||
- **类型**: 标准列表 + 子表模式
|
||||
- **后端**: Model + CRUD (`clean_room.py`) + API 路由
|
||||
- **前端**: 参照 `system/unit/index.vue`,增加"人员进出"按钮弹窗查看进出记录
|
||||
- **路由**: `/operations/clean-room`
|
||||
- **API**: `/web/admin/clean-rooms` + `/web/admin/clean-room-access-logs`
|
||||
|
||||
### 模块 2: 库位管理
|
||||
- **类型**: 树形结构模式
|
||||
- **后端**: Model + CRUD (`storage_facility.py`) + API 路由(树查询 + 节点 CRUD + 格子 CRUD)
|
||||
- **前端**: 参照 `system/category/index.vue`,左侧树形结构,右侧节点详情+格子列表
|
||||
- **路由**: `/operations/storage-facility`
|
||||
- **API**: `/web/admin/storage-facilities` + `/web/admin/storage-facility-slots`
|
||||
- **工具**: 使用 `tree-path.ts` 构建完整路径
|
||||
|
||||
### 模块 3: 展馆预约
|
||||
- **类型**: 标准列表模式
|
||||
- **后端**: Model + CRUD (`exhibition_appointment.py`) + API 路由
|
||||
- **前端**: 参照 `system/unit/index.vue` 标准列表
|
||||
- **路由**: `/operations/exhibition-appointment`
|
||||
- **API**: `/web/admin/exhibition-appointments`
|
||||
|
||||
## 技术规范
|
||||
- 后端模式:flat 函数式 CRUD(参照 `app/crud/item_unit.py`),不用 service 层
|
||||
- 前端模式:API 函数加入 `admin.ts`,路由加入 `sqxp.ts`
|
||||
- 所有查询带 `tenant_no` 过滤,删除为软删除
|
||||
- Fresh Mint 配色 (`--primary #10b981`)
|
||||
- 树形模块使用 `tree-path.ts` 工具
|
||||
|
||||
## 实施顺序
|
||||
1. 后端:Models → CRUD → API 路由 → 注册
|
||||
2. 前端:API 函数 → 路由 → 页面组件
|
||||
3. 测试:API 验证 + 页面功能验证
|
||||
+52
-5
@@ -67,11 +67,15 @@
|
||||
- 展馆预约[2.4.3、展馆预约]
|
||||
- 物料管理(暂时不开发,后续在完善)
|
||||
- 财务管理(暂时不开发,后续在完善)
|
||||
- 生产管理(暂时不开发,后续在完善)
|
||||
- 耗材管理
|
||||
- 耗材包管理
|
||||
- 工艺库管理
|
||||
- 工艺路线配置
|
||||
- 生产管理
|
||||
- 耗材管理[2.5.1、耗材管理]
|
||||
- 耗材包管理[2.5.2、耗材包管理]
|
||||
- 试剂管理[2.5.3、试剂管理]
|
||||
- SOP文档[2.5.4、SOP文档]
|
||||
- 工艺工序分类[2.5.5、工艺工序分类]
|
||||
- 工艺产出物类型[2.3.6、工艺产出物类型]
|
||||
- 工艺库管理[2.3.7、工艺库管理]
|
||||
- 工艺路线配置[2.3.8、工艺路线配置]
|
||||
- 细胞存储协议单
|
||||
- 中试服务单
|
||||
- 生产工单
|
||||
@@ -84,6 +88,49 @@
|
||||
- 采集协议号管理[2.2.4、采集协议号管理]
|
||||
- 样本编码管理[2.2.5、样本编码管理]
|
||||
|
||||
|
||||
## 2.5、生产管理
|
||||
### 2.5.1、耗材管理
|
||||
- 此模块用于生产耗材管理,支持耗材的新增、修改、删除等功能
|
||||
- 生产耗材表核心字段:耗材编码、耗材名称、规格型号、标准用量、描述
|
||||
|
||||
### 2.5.2、耗材包管理
|
||||
- 此模块将生产耗材组合成一个耗材包,方便日常数据录入
|
||||
- 耗材包主表:编号、名称、使用场景、类型(bzhcb - 标准耗材包、 zdyhcb - 自定义耗材包),状态 (1 - 启用 、 0 - 停用)、备注
|
||||
- 耗材包耗材明细表:耗材包id、耗材编码、耗材名称、耗材规格型号、标准用量、备注
|
||||
|
||||
### 2.5.3、试剂管理
|
||||
- 此模块用于生产试剂信息的管理
|
||||
- 试剂信息表的核心字段:试剂编码、试剂名称、规格、标准用量、单位[下拉数据从 2.1.9、物品单位管理 获取]、描述
|
||||
|
||||
### 2.5.4、SOP文档
|
||||
- 此模块用于管理SOP文档管理
|
||||
- SOP文档表核心字段:SOP编号、SOP文档名称、版本号、SOP文档附件
|
||||
|
||||
### 2.5.5、工艺工序分类
|
||||
- 此模块用于工艺工序分类管理
|
||||
- 工艺工序分类表核心字段:分类编码、分类名称
|
||||
|
||||
### 2.3.6、工艺产出物类型
|
||||
- 此模块用于工艺产出物类型管理
|
||||
- 工艺产出物类型表核心字段:类型编码、类型名称
|
||||
|
||||
### 2.3.7、工艺库管理
|
||||
- 此模块用于定义细胞生产过程中的每个工艺的信息,包括预计使用的试剂、预计使用的耗材、预计产出物等关联信息。工艺信息理解就是一个预设的情况,等后续真正生产时候,有这些预制的值可以起到指导作用,同时也方便用户录入信息
|
||||
- 生产工艺主表 核心字段:工序代码、工序分类[下拉数据来自于 2.5.5、工艺工序分类]、工序名称、描述、预计使用设备名称、预估耗时、关联的SOP文[从 2.5.4、SOP文档 关联选择, 支持多个附件]
|
||||
- 生产工艺试剂关联表 核心字段: 试剂编码、试剂名称、规格、标准用量、单位、是否关键物料 。 (通过关联2.5.3、试剂管理 下拉后,将试剂编码、试剂名称、规格、标准用量、单位都带过来,但是用户可以自己改 标准用量)
|
||||
- 生产工艺耗材关联表:核心字段:耗材编码、耗材名称、耗材规格型号、标准用量、是否必需(1 - 必须 , 0 - 可选)、备注, 可以通过从2.5.2、耗材包管理 关联一次性将耗材批量带过来,然后修改。 也可以从2.5.1、耗材管理 待过来
|
||||
- 生产工艺产出关联表:核心字段:产出类型(下拉数据来源于 2.3.6、工艺产出物类型)、产出物名称、产出物条码。 产出物条码只有在真实生产时候才会有数据
|
||||
|
||||
### 2.3.8、工艺路线配置
|
||||
- 此模块用于将多个生产工艺组织成一个工艺路线图,方便实验室人员在进行细胞生产过程中,快速勾选一个工艺路线图,调整后就可以将生产过程记录下来了
|
||||
- 工艺路线图主表 核心字段:路线编码、产品类型(下拉数据来自于 2.3.1、细胞产品类型管理)、路线名称、备注
|
||||
- 工艺路线图工艺明细表:工序代码、工序分类[下拉数据来自于 2.5.5、工艺工序分类]、工序名称、描述、预计使用设备名称、预估耗时、关联的SOP文[从 2.5.4、SOP文档 关联选择, 支持多个附件]、排序号
|
||||
- 工艺路线图工艺明细表试剂关联表 核心字段: 试剂编码、试剂名称、规格、标准用量、单位、是否关键物料 。 (通过关联2.5.3、试剂管理 下拉后,将试剂编码、试剂名称、规格、标准用量、单位都带过来,但是用户可以自己改 标准用量)
|
||||
- 工艺路线图工艺明细表耗材关联表:核心字段:耗材编码、耗材名称、耗材规格型号、标准用量、是否必需(1 - 必须 , 0 - 可选)、备注, 可以通过从2.5.2、耗材包管理 关联一次性将耗材批量带过来,然后修改。 也可以从2.5.1、耗材管理 待过来
|
||||
- 工艺路线图工艺明细表产出关联表:核心字段:产出类型(下拉数据来源于 2.3.6、工艺产出物类型)、产出物名称、产出物条码。 产出物条码只有在真实生产时候才会有数据
|
||||
- 工艺路线配置这个界面,是可以做成拖拉配置的。界面分三部分、左边是内置的生产工艺的卡片、中间是生产工艺排版的界面,右侧是每个工艺对应的内容(工艺信息、试剂关联信息、耗材关联信息、产出物信息等)
|
||||
|
||||
## 2.4、运营支撑
|
||||
### 2.4.1、洁净室管理
|
||||
- 此模块用于洁净室信息录入 和 人员出入数据录入就行了
|
||||
|
||||
Reference in new issue
Block a user