zhangwencui
2026-10-29 5df6d6cd93ed5655065652a008189d1630fbfeab
change
已添加15个文件
已修改16个文件
8754 ■■■■■ 文件已修改
.build-check-rollback/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20261028_customer_project_upgrade.sql 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20261029_customer_button_role_extra.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20261029_customer_financial_report.sql 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20261029_customer_report_audit.sql 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20261029_disable_unused_menus.sql 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20261029_project_classification.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20261029_purchase_ledger_simple.sql 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/数据库升级说明.md 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerFinancialReport.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/customerPortal/submission.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementLedger.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesLedger.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FileUpload/index.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProjectManagement/ProgressReportDialog.vue 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/permission.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 1072 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalManagement/index.vue 1521 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 1338 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerPortal/myReport/index.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerPortal/mySubmission/index.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerPortal/submissionManagement/index.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 2393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/formDia.vue 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/projectDetail.vue 345 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/reportAudit/index.vue 323 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.build-check-rollback/favicon.ico
.gitignore
@@ -1,6 +1,7 @@
.DS_Store
node_modules/
dist/
.local-backups/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
sql/20261028_customer_project_upgrade.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
-- å®¢æˆ·ç”³æŠ¥ä¸Žé¡¹ç›®ä¸šåŠ¡å‡çº§è„šæœ¬
-- é€‚用库:product-inventory-management-xmgl
-- æ‰§è¡Œå‰å¿…须先做全库备份;本脚本不删除现有表和数据。
-- å»ºè®®åœ¨æµ‹è¯•库验证后再执行生产库。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE IF NOT EXISTS `customer_submission` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `customer_id` bigint NOT NULL COMMENT '客户ID',
  `report_year` int NOT NULL COMMENT '报告年度',
  `report_name` varchar(255) DEFAULT NULL COMMENT '材料名称',
  `material_files` text COMMENT '客户材料附件,逗号分隔',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待审核 1处理中 2已发布 3已退回',
  `result_text` varchar(2000) DEFAULT NULL COMMENT '发布结果说明',
  `result_files` text COMMENT '结果附件,逗号分隔',
  `review_remark` varchar(1000) DEFAULT NULL COMMENT '审核意见',
  `submit_user_id` bigint DEFAULT NULL COMMENT '提交人ID',
  `submit_user_name` varchar(100) DEFAULT NULL COMMENT '提交人名称',
  `publish_user_id` bigint DEFAULT NULL COMMENT '发布人ID',
  `publish_user_name` varchar(100) DEFAULT NULL COMMENT '发布人名称',
  `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
  `tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '删除标记',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_customer_report_year` (`customer_id`,`report_year`,`is_deleted`),
  KEY `idx_submission_status` (`status`),
  KEY `idx_submission_user` (`submit_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户年度申报材料与结果';
DROP PROCEDURE IF EXISTS `add_column_if_missing`;
DELIMITER $$
CREATE PROCEDURE `add_column_if_missing`(
  IN p_table varchar(64), IN p_column varchar(64), IN p_definition varchar(1000)
)
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.COLUMNS
    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_table AND COLUMN_NAME = p_column
  ) THEN
    SET @ddl = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `', p_column, '` ', p_definition);
    PREPARE stmt FROM @ddl;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
  END IF;
END$$
DELIMITER ;
CALL add_column_if_missing('sales_ledger', 'business_approval_status', "tinyint NOT NULL DEFAULT 0 COMMENT '0草稿 1待审批 2已通过 3已退回'");
CALL add_column_if_missing('sales_ledger', 'business_approval_remark', "varchar(1000) DEFAULT NULL COMMENT '审批意见'");
CALL add_column_if_missing('sales_ledger', 'business_approved_by', "bigint DEFAULT NULL COMMENT '审批人ID'");
CALL add_column_if_missing('sales_ledger', 'business_approved_name', "varchar(100) DEFAULT NULL COMMENT '审批人名称'");
CALL add_column_if_missing('sales_ledger', 'business_approved_time', "datetime DEFAULT NULL COMMENT '审批时间'");
CALL add_column_if_missing('sales_ledger', 'business_submitted_time', "datetime DEFAULT NULL COMMENT '提交审批时间'");
CALL add_column_if_missing('sales_ledger', 'project_management_info_id', "bigint DEFAULT NULL COMMENT '审批通过后生成的项目ID'");
DROP PROCEDURE IF EXISTS `add_column_if_missing`;
-- å›ºå®šå…­é˜¶æ®µæ¨¡æ¿ï¼›é‡å¤æ‰§è¡Œä¸ä¼šé‡å¤åˆ›å»ºã€‚
INSERT INTO `project_management_plan` (`name`, `description`, `is_delete`, `create_time`, `update_time`, `create_user_name`)
SELECT '项目申报六阶段', '业务审批通过后自动生成:立项、团队分工、资料汇报、申报、申报结果、验收', 0, NOW(), NOW(), '升级脚本'
WHERE NOT EXISTS (
  SELECT 1 FROM `project_management_plan` WHERE `name` = '项目申报六阶段' AND `is_delete` = 0
);
SET @plan_id = (SELECT `id` FROM `project_management_plan` WHERE `name` = '项目申报六阶段' AND `is_delete` = 0 ORDER BY `id` LIMIT 1);
INSERT INTO `project_management_plan_node`
(`project_management_plan_id`, `sort`, `name`, `estimated_duration`, `work_content`, `is_delete`, `create_time`, `update_time`, `create_user_name`)
SELECT @plan_id, 1, '立项', 1, '建立项目并确认负责人、计划周期和基础资料', 0, NOW(), NOW(), '升级脚本'
WHERE NOT EXISTS (SELECT 1 FROM `project_management_plan_node` WHERE `project_management_plan_id`=@plan_id AND `name`='立项' AND `is_delete`=0);
INSERT INTO `project_management_plan_node` (`project_management_plan_id`,`sort`,`name`,`estimated_duration`,`work_content`,`is_delete`,`create_time`,`update_time`,`create_user_name`)
SELECT @plan_id,2,'团队分工',1,'确定项目成员与工作分工',0,NOW(),NOW(),'升级脚本' WHERE NOT EXISTS (SELECT 1 FROM `project_management_plan_node` WHERE `project_management_plan_id`=@plan_id AND `name`='团队分工' AND `is_delete`=0);
INSERT INTO `project_management_plan_node` (`project_management_plan_id`,`sort`,`name`,`estimated_duration`,`work_content`,`is_delete`,`create_time`,`update_time`,`create_user_name`)
SELECT @plan_id,3,'资料汇报',1,'收集、补充并汇总申报资料',0,NOW(),NOW(),'升级脚本' WHERE NOT EXISTS (SELECT 1 FROM `project_management_plan_node` WHERE `project_management_plan_id`=@plan_id AND `name`='资料汇报' AND `is_delete`=0);
INSERT INTO `project_management_plan_node` (`project_management_plan_id`,`sort`,`name`,`estimated_duration`,`work_content`,`is_delete`,`create_time`,`update_time`,`create_user_name`)
SELECT @plan_id,4,'申报',1,'完成正式申报并登记申报信息',0,NOW(),NOW(),'升级脚本' WHERE NOT EXISTS (SELECT 1 FROM `project_management_plan_node` WHERE `project_management_plan_id`=@plan_id AND `name`='申报' AND `is_delete`=0);
INSERT INTO `project_management_plan_node` (`project_management_plan_id`,`sort`,`name`,`estimated_duration`,`work_content`,`is_delete`,`create_time`,`update_time`,`create_user_name`)
SELECT @plan_id,5,'申报结果',1,'登记申报结果并形成客户发布材料',0,NOW(),NOW(),'升级脚本' WHERE NOT EXISTS (SELECT 1 FROM `project_management_plan_node` WHERE `project_management_plan_id`=@plan_id AND `name`='申报结果' AND `is_delete`=0);
INSERT INTO `project_management_plan_node` (`project_management_plan_id`,`sort`,`name`,`estimated_duration`,`work_content`,`is_delete`,`create_time`,`update_time`,`create_user_name`)
SELECT @plan_id,6,'验收',1,'完成交付、验收和项目归档',0,NOW(),NOW(),'升级脚本' WHERE NOT EXISTS (SELECT 1 FROM `project_management_plan_node` WHERE `project_management_plan_id`=@plan_id AND `name`='验收' AND `is_delete`=0);
-- å®¢æˆ·é—¨æˆ·èœå•。
INSERT INTO `sys_menu` (`menu_name`,`parent_id`,`order_num`,`path`,`component`,`route_name`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`create_by`,`create_time`,`remark`)
SELECT '客户服务',0,7,'customerPortal',NULL,'CustomerPortal',1,0,'M','0','0','customer:portal','#','admin',NOW(),'客户材料与结果门户'
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `path`='customerPortal' AND `parent_id`=0);
SET @portal_menu_id = (SELECT `menu_id` FROM `sys_menu` WHERE `path`='customerPortal' AND `parent_id`=0 ORDER BY `menu_id` LIMIT 1);
INSERT INTO `sys_menu` (`menu_name`,`parent_id`,`order_num`,`path`,`component`,`route_name`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`create_by`,`create_time`,`remark`)
SELECT '我的申报',@portal_menu_id,1,'mySubmission','customerPortal/mySubmission/index','MySubmission',1,0,'C','0','0','customer:submission:mine','upload','admin',NOW(),'客户上传近三年材料并查看结果'
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id`=@portal_menu_id AND `path`='mySubmission');
INSERT INTO `sys_menu` (`menu_name`,`parent_id`,`order_num`,`path`,`component`,`route_name`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`create_by`,`create_time`,`remark`)
SELECT '申报管理',@portal_menu_id,2,'submissionManagement','customerPortal/submissionManagement/index','SubmissionManagement',1,0,'C','0','0','customer:submission:manage','list','admin',NOW(),'内部人员审核并发布客户申报结果'
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id`=@portal_menu_id AND `path`='submissionManagement');
SET @my_submission_menu_id = (SELECT `menu_id` FROM `sys_menu` WHERE `parent_id`=@portal_menu_id AND `path`='mySubmission' ORDER BY `menu_id` LIMIT 1);
-- é”€å”®å°è´¦ä¸­çš„业务审批按钮权限。已有销售台账权限的角色自动获得“提交”,
-- â€œå®¡æ‰¹â€ä¸è‡ªåŠ¨æŽˆäºˆï¼Œç”±ç®¡ç†å‘˜åœ¨çŽ°æœ‰è§’è‰²ç®¡ç†ä¸­åˆ†é…ç»™ç®¡ç†äººå‘˜ã€‚
SET @sales_ledger_menu_id = (SELECT `menu_id` FROM `sys_menu` WHERE `component`='salesManagement/salesLedger/index' ORDER BY `menu_id` LIMIT 1);
INSERT INTO `sys_menu` (`menu_name`,`parent_id`,`order_num`,`path`,`component`,`route_name`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`create_by`,`create_time`,`remark`)
SELECT '提交业务审批',@sales_ledger_menu_id,20,'#',NULL,'',1,0,'F','0','0','business:approval:submit','#','admin',NOW(),'提交业务进入管理审批'
WHERE @sales_ledger_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `perms`='business:approval:submit');
INSERT INTO `sys_menu` (`menu_name`,`parent_id`,`order_num`,`path`,`component`,`route_name`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`create_by`,`create_time`,`remark`)
SELECT '审核业务',@sales_ledger_menu_id,21,'#',NULL,'',1,0,'F','0','0','business:approval:review','#','admin',NOW(),'审批通过后自动生成项目'
WHERE @sales_ledger_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `perms`='business:approval:review');
SET @business_submit_menu_id = (SELECT `menu_id` FROM `sys_menu` WHERE `perms`='business:approval:submit' ORDER BY `menu_id` LIMIT 1);
INSERT IGNORE INTO `sys_role_menu` (`role_id`,`menu_id`)
SELECT `role_id`, @business_submit_menu_id FROM `sys_role_menu`
WHERE `menu_id`=@sales_ledger_menu_id AND @business_submit_menu_id IS NOT NULL;
-- å®¢æˆ·è§’色只授予客户门户和“我的申报”,账号仍由管理员在现有用户管理中创建。
INSERT INTO `sys_role` (`role_name`,`role_key`,`role_sort`,`data_scope`,`menu_check_strictly`,`dept_check_strictly`,`status`,`del_flag`,`create_by`,`create_time`,`remark`)
SELECT '客户','customer',90,'5',1,1,'0','0','admin',NOW(),'外部客户账号;不开放自行注册'
WHERE NOT EXISTS (SELECT 1 FROM `sys_role` WHERE `role_key`='customer' AND `del_flag`='0');
SET @customer_role_id = (SELECT `role_id` FROM `sys_role` WHERE `role_key`='customer' AND `del_flag`='0' ORDER BY `role_id` LIMIT 1);
INSERT IGNORE INTO `sys_role_menu` (`role_id`,`menu_id`) VALUES (@customer_role_id,@portal_menu_id),(@customer_role_id,@my_submission_menu_id);
SET FOREIGN_KEY_CHECKS = 1;
-- æ‰§è¡ŒåŽæ£€æŸ¥
SELECT 'customer_submission' AS item, COUNT(*) AS count_value FROM `customer_submission`
UNION ALL SELECT 'six_stage_nodes', COUNT(*) FROM `project_management_plan_node` WHERE `project_management_plan_id`=@plan_id AND `is_delete`=0
UNION ALL SELECT 'customer_portal_menus', COUNT(*) FROM `sys_menu` WHERE `menu_id`=@portal_menu_id OR `parent_id`=@portal_menu_id;
sql/20261029_customer_button_role_extra.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
-- å®¢æˆ·æ¡£æ¡ˆæŒ‰é’®æƒé™è¡¥å……:公海客户页面同样使用客户新增/修改/删除接口
INSERT IGNORE INTO `sys_role_menu` (`role_id`,`menu_id`)
SELECT rm.role_id, m.menu_id FROM `sys_role_menu` rm
JOIN `sys_menu` m ON m.menu_id BETWEEN 2362 AND 2368
WHERE rm.menu_id = 2290;
sql/20261029_customer_financial_report.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
-- å®¢æˆ·è¿‘3年财务报表 + å®¢æˆ·æ¡£æ¡ˆæŒ‰é’®æƒé™
-- æ‰§è¡Œåº“:product-inventory-management-xmgl
CREATE TABLE IF NOT EXISTS `customer_financial_report` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `customer_id` bigint NOT NULL COMMENT '客户id',
  `report_year` int NOT NULL COMMENT '报表年度',
  `report_name` varchar(255) DEFAULT NULL COMMENT '报表名称',
  `report_files` text COMMENT '报表附件(JSON数组)',
  `upload_user_id` bigint DEFAULT NULL COMMENT '上传人id',
  `upload_user_name` varchar(100) DEFAULT NULL COMMENT '上传人',
  `tenant_id` bigint DEFAULT NULL COMMENT '租户id',
  `dept_id` bigint DEFAULT NULL COMMENT '部门id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_customer_year` (`customer_id`, `report_year`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户近3年财务报表';
-- å®¢æˆ·æ¡£æ¡ˆèœå•挂上查询权限
UPDATE `sys_menu` SET `perms` = 'basic:customer:list' WHERE `menu_id` = 2001;
-- æŒ‰é’®æƒé™ï¼ˆF ç±»åž‹ï¼‰
INSERT IGNORE INTO `sys_menu` (`menu_id`,`menu_name`,`parent_id`,`order_num`,`path`,`component`,`query`,`route_name`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`create_by`,`create_time`,`remark`)
VALUES
 (2362,'客户新增',2001,2,'',NULL,'','',1,0,'F','0','0','basic:customer:add','#','admin',NOW(),'客户档案按钮权限'),
 (2363,'客户修改',2001,3,'',NULL,'','',1,0,'F','0','0','basic:customer:edit','#','admin',NOW(),'客户档案按钮权限'),
 (2364,'客户删除',2001,4,'',NULL,'','',1,0,'F','0','0','basic:customer:remove','#','admin',NOW(),'客户档案按钮权限'),
 (2365,'客户导入',2001,5,'',NULL,'','',1,0,'F','0','0','basic:customer:import','#','admin',NOW(),'客户档案按钮权限'),
 (2366,'客户导出',2001,6,'',NULL,'','',1,0,'F','0','0','basic:customer:export','#','admin',NOW(),'客户档案按钮权限'),
 (2367,'客户详情',2001,7,'',NULL,'','',1,0,'F','0','0','basic:customer:query','#','admin',NOW(),'客户档案按钮权限'),
 (2368,'近3年财务报表上传',2001,8,'',NULL,'','',1,0,'F','0','0','basic:customer:report','#','admin',NOW(),'客户档案按钮权限');
-- å·²æœ‰å®¢æˆ·æ¡£æ¡ˆèœå•的角色自动获得按钮权限
INSERT IGNORE INTO `sys_role_menu` (`role_id`,`menu_id`)
SELECT rm.role_id, m.menu_id FROM `sys_role_menu` rm
JOIN `sys_menu` m ON m.menu_id BETWEEN 2362 AND 2368
WHERE rm.menu_id = 2001;
-- å®¢æˆ·é—¨æˆ·ï¼šæˆ‘的申报 -> è´¢åŠ¡æŠ¥è¡¨ä¸Šä¼ ï¼ˆä»…å®¢æˆ·è§’è‰²å¯è§ï¼‰
UPDATE `sys_menu` SET `menu_name` = '财务报表上传', `path` = 'myReport', `route_name` = 'MyReport',
       `component` = 'customerPortal/myReport/index', `perms` = 'customer:report:mine', `order_num` = 2
WHERE `menu_id` = 2358;
-- å·²ç»‘定的客户账号补授“客户”角色(role_key=customer)
INSERT IGNORE INTO `sys_user_role` (`user_id`,`role_id`)
SELECT DISTINCT cu.user_id, r.role_id FROM `customer_user` cu
JOIN `sys_role` r ON r.role_key = 'customer' AND r.del_flag = '0';
sql/20261029_customer_report_audit.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
-- å®¢æˆ·è¿‘3年财务报表:审批状态 + è¥é”€ç®¡ç†ä¸‹çš„“申报审批”菜单
-- æ‰§è¡Œåº“:product-inventory-management-xmgl
ALTER TABLE `customer_financial_report`
  ADD COLUMN `audit_status` tinyint NOT NULL DEFAULT 0 COMMENT '审批状态:0待审批 1已通过 2已退回' AFTER `report_files`,
  ADD COLUMN `audit_user_id` bigint DEFAULT NULL COMMENT '审批人id' AFTER `audit_status`,
  ADD COLUMN `audit_user_name` varchar(100) DEFAULT NULL COMMENT '审批人' AFTER `audit_user_id`,
  ADD COLUMN `audit_time` datetime DEFAULT NULL COMMENT '审批时间' AFTER `audit_user_name`,
  ADD COLUMN `audit_remark` varchar(500) DEFAULT NULL COMMENT '审批意见/退回原因' AFTER `audit_time`;
-- è¥é”€ç®¡ç†ï¼ˆmenu_id=2010)下新增“申报审批”菜单
INSERT IGNORE INTO `sys_menu` (`menu_id`,`menu_name`,`parent_id`,`order_num`,`path`,`component`,`query`,`route_name`,`is_frame`,`is_cache`,`menu_type`,`visible`,`status`,`perms`,`icon`,`create_by`,`create_time`,`remark`)
VALUES (2369,'申报审批',2010,7,'reportAudit','salesManagement/reportAudit/index','','ReportAudit',1,0,'C','0','0','sales:report:audit','form','admin',NOW(),'客户财务报表申报审批');
-- æ‹¥æœ‰è¥é”€ç®¡ç†èœå•的角色自动获得申报审批菜单
INSERT IGNORE INTO `sys_role_menu` (`role_id`,`menu_id`)
SELECT rm.role_id, 2369 FROM `sys_role_menu` rm WHERE rm.menu_id = 2010;
sql/20261029_disable_unused_menus.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
-- ç²¾ç®€é¡¹ç›®èœå•:仅停用/隐藏,不删除菜单和权限数据。
-- ä¿ç•™ï¼šç³»ç»Ÿç®¡ç†ã€å®¢æˆ·æœåŠ¡ã€è¥é”€ä¸šåŠ¡ã€é‡‡è´­å°è´¦ã€é¡¹ç›®ç®¡ç†ã€è¯·å‡/审批。
SET NAMES utf8mb4;
START TRANSACTION;
-- åœç”¨å½“前项目不使用的一级业务目录。
UPDATE sys_menu
SET visible = '1', status = '1', update_time = NOW(), update_by = 'admin'
WHERE menu_id IN (
  2000, -- åŸºç¡€æ•°æ®
  2291, -- å·¥è‰ºè®¾è®¡
  2029, -- ä»“储
  2033, -- äººåŠ›èµ„æºï¼ˆäººå‘˜ã€éƒ¨é—¨æ”¹ç”¨ç³»ç»Ÿç®¡ç†ï¼‰
  2058, -- ç”Ÿäº§ç®¡æŽ§
  2071, -- è´¢åŠ¡ç®¡ç†
  2101, -- æ¡£æ¡ˆç®¡ç†
  2040, -- è´¨é‡ç®¡ç†
  2042, -- è®¾å¤‡ç®¡ç†
  2064, -- èƒ½è€—管理
  2036, -- ç®¡ç†é©¾é©¶èˆ±
  2190, -- åŠ³ä¿ç®¡ç†
  2067, -- å”®åŽæœåŠ¡
  2250  -- å®‰å…¨ç”Ÿäº§
);
-- è¥é”€ç®¡ç†åªä¿ç•™å®¢æˆ·æ¡£æ¡ˆ(私海)、销售台账及其按钮权限。
UPDATE sys_menu
SET visible = '1', status = '1', update_time = NOW(), update_by = 'admin'
WHERE parent_id = 2010
  AND menu_id NOT IN (2001, 2011)
  AND menu_type <> 'F';
-- é‡‡è´­ç®¡ç†ä¿ç•™ä¾›åº”商档案、采购台账。
UPDATE sys_menu
SET visible = '1', status = '1', update_time = NOW(), update_by = 'admin'
WHERE parent_id = 2015
  AND menu_id NOT IN (2012, 2016)
  AND menu_type <> 'F';
-- ååŒåŠžå…¬ä¿ç•™å®¡æ‰¹ç®¡ç†ã€ååŒå®¡æ‰¹ã€è€ƒå‹¤ç®¡ç†ã€‚
UPDATE sys_menu
SET visible = '1', status = '1', update_time = NOW(), update_by = 'admin'
WHERE parent_id = 2053
  AND menu_id NOT IN (2327, 2326, 2306)
  AND menu_type <> 'F';
-- è€ƒå‹¤ç®¡ç†åªä¿ç•™è¯·å‡ç”³è¯·ï¼›å‡ºå·®ç­‰å®¡æ‰¹ç»Ÿä¸€ä»ŽååŒå®¡æ‰¹è¿›å…¥ã€‚
UPDATE sys_menu
SET visible = '1', status = '1', update_time = NOW(), update_by = 'admin'
WHERE parent_id = 2306
  AND menu_id <> 2320
  AND menu_type <> 'F';
-- æ˜Žç¡®å¯ç”¨éœ€è¦ä¿ç•™çš„一级菜单和核心业务菜单。
UPDATE sys_menu
SET visible = '0', status = '0', update_time = NOW(), update_by = 'admin'
WHERE menu_id IN (
  1,    -- ç³»ç»Ÿç®¡ç†
  2053, -- ååŒåŠžå…¬
  2010, -- è¥é”€ç®¡ç†
  2015, -- é‡‡è´­ç®¡ç†
  2276, -- é¡¹ç›®ç®¡ç†
  2357, -- å®¢æˆ·æœåŠ¡
  100,101,102,103,104,105,108,2287,
  2327,2326,2306,2320,
  2001,2011,
  2012,2016,
  2277,2283,2285,
  2358,2359
);
-- å°†æ‰€æœ‰å·²åœç”¨/隐藏菜单的下级路由和按钮权限同步停用。
-- èœå•树当前层级有限,逐层同步 8 æ¬¡è¶³ä»¥è¦†ç›–全部后代节点。
UPDATE sys_menu child JOIN sys_menu parent ON child.parent_id = parent.menu_id SET child.visible = '1', child.status = '1', child.update_time = NOW(), child.update_by = 'admin' WHERE parent.status = '1' OR parent.visible = '1';
UPDATE sys_menu child JOIN sys_menu parent ON child.parent_id = parent.menu_id SET child.visible = '1', child.status = '1', child.update_time = NOW(), child.update_by = 'admin' WHERE parent.status = '1' OR parent.visible = '1';
UPDATE sys_menu child JOIN sys_menu parent ON child.parent_id = parent.menu_id SET child.visible = '1', child.status = '1', child.update_time = NOW(), child.update_by = 'admin' WHERE parent.status = '1' OR parent.visible = '1';
UPDATE sys_menu child JOIN sys_menu parent ON child.parent_id = parent.menu_id SET child.visible = '1', child.status = '1', child.update_time = NOW(), child.update_by = 'admin' WHERE parent.status = '1' OR parent.visible = '1';
UPDATE sys_menu child JOIN sys_menu parent ON child.parent_id = parent.menu_id SET child.visible = '1', child.status = '1', child.update_time = NOW(), child.update_by = 'admin' WHERE parent.status = '1' OR parent.visible = '1';
UPDATE sys_menu child JOIN sys_menu parent ON child.parent_id = parent.menu_id SET child.visible = '1', child.status = '1', child.update_time = NOW(), child.update_by = 'admin' WHERE parent.status = '1' OR parent.visible = '1';
UPDATE sys_menu child JOIN sys_menu parent ON child.parent_id = parent.menu_id SET child.visible = '1', child.status = '1', child.update_time = NOW(), child.update_by = 'admin' WHERE parent.status = '1' OR parent.visible = '1';
UPDATE sys_menu child JOIN sys_menu parent ON child.parent_id = parent.menu_id SET child.visible = '1', child.status = '1', child.update_time = NOW(), child.update_by = 'admin' WHERE parent.status = '1' OR parent.visible = '1';
COMMIT;
-- æ£€æŸ¥æœ€ç»ˆæ˜¾ç¤ºçš„菜单路由。
SELECT menu_id, menu_name, parent_id, path, component, menu_type
FROM sys_menu
WHERE visible = '0' AND status = '0' AND menu_type IN ('M','C')
ORDER BY parent_id, order_num, menu_id;
-- å¦‚需恢复,可从菜单清理前的全库备份恢复;本脚本没有删除任何记录。
sql/20261029_project_classification.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
-- é¡¹ç›®ç®¡ç†ï¼šæ–°å¢žâ€œé¡¹ç›®åˆ†ç±»â€å­—段(先写死几个选项,后续可切换为字典)
ALTER TABLE project_management_info
    ADD COLUMN project_classification varchar(50) DEFAULT NULL COMMENT '项目分类' AFTER source;
sql/20261029_purchase_ledger_simple.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
-- é‡‡è´­å°è´¦ï¼ˆç®€ï¼‰è¡¨ï¼šä»…保留基础字段,不与合同/供应商/产品/入库等模块关联
CREATE TABLE IF NOT EXISTS purchase_ledger_simple (
    id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    payment_date DATE DEFAULT NULL COMMENT '支出日期',
    purchase_date DATE DEFAULT NULL COMMENT '采购日期',
    amount DECIMAL(18, 2) DEFAULT NULL COMMENT '金额',
    purchase_goods VARCHAR(255) DEFAULT NULL COMMENT '采购物品',
    invoice_number VARCHAR(100) DEFAULT NULL COMMENT '发票号码',
    payment_method VARCHAR(50) DEFAULT NULL COMMENT '付款方式',
    remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
    recorder_id BIGINT DEFAULT NULL COMMENT '录入人id',
    recorder_name VARCHAR(64) DEFAULT NULL COMMENT '录入人',
    entry_date DATE DEFAULT NULL COMMENT '录入日期',
    create_user BIGINT DEFAULT NULL COMMENT '创建用户',
    dept_id BIGINT DEFAULT NULL COMMENT '部门id',
    create_time DATETIME DEFAULT NULL COMMENT '创建时间',
    update_time DATETIME DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '采购台账(简)';
sql/Êý¾Ý¿âÉý¼¶ËµÃ÷.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
# æ•°æ®åº“升级说明
1. å·²åœ¨å¼€å‘前完成数据库全量备份:`D:\newcode\product-inventory-management\.local-backups\product-inventory-management-xmgl-20261028-131516.sql`(约 83.5 MB)。
2. æ­£å¼æ‰§è¡Œå‰ï¼Œå»ºè®®å†ç”Ÿæˆä¸€ä»½å½“时点备份,并确认备份文件大小不是 0。
3. ä½¿ç”¨æ•°æ®åº“管理工具选择 `product-inventory-management-xmgl`,执行 `20261028_customer_project_upgrade.sql`。
4. è„šæœ¬åªæ–°å¢žä¸šåŠ¡è¡¨ã€é”€å”®å°è´¦å­—æ®µã€å…­é˜¶æ®µæ¨¡æ¿ã€å®¢æˆ·è§’è‰²åŠèœå•ï¼Œä¸åˆ é™¤åŽŸæ•°æ®ï¼›åŒä¸€æ•°æ®åº“å¯é‡å¤æ‰§è¡Œã€‚
5. æ‰§è¡Œå®ŒæˆåŽï¼Œæœ«å°¾æ£€æŸ¥ç»“果应为:六阶段节点数量 `6`,客户服务菜单数量 `3`。
6. ç®¡ç†å‘˜åœ¨çŽ°æœ‰â€œç”¨æˆ·ç®¡ç†â€åˆ›å»ºå®¢æˆ·è´¦å·ï¼Œèµ‹äºˆâ€œå®¢æˆ·â€è§’è‰²ï¼›å†åœ¨çŽ°æœ‰â€œå®¢æˆ·æ¡£æ¡ˆâ€ä¸­æŠŠè¯¥ç”¨æˆ·ç»‘å®šåˆ°å¯¹åº”å®¢æˆ·ã€‚å†…éƒ¨å‘˜å·¥èœå•æƒé™ä»åœ¨çŽ°æœ‰è§’è‰²ç®¡ç†ä¸­é…ç½®ã€‚
回滚方式:停止应用,创建一个空库后导入上述全量备份,确认无误再切换连接。不要在有新业务数据产生后直接覆盖生产库。
src/api/basicData/customerFinancialReport.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
import request from '@/utils/request'
// åŽå°ï¼šæŒ‰å®¢æˆ·æŸ¥è¯¢è¿‘3年财务报表
export function listCustomerFinancialReport(params) {
  return request({
    url: '/basic/customerFinancialReport/list',
    method: 'get',
    params
  })
}
// å®¢æˆ·è´¦å·ï¼šæŸ¥è¯¢è‡ªå·±çš„è¿‘3年财务报表
export function listMyFinancialReport() {
  return request({
    url: '/basic/customerFinancialReport/mine',
    method: 'get'
  })
}
// ä¸Šä¼ /修改财务报表
export function saveCustomerFinancialReport(data) {
  return request({
    url: '/basic/customerFinancialReport/save',
    method: 'post',
    data
  })
}
// åˆ é™¤è´¢åŠ¡æŠ¥è¡¨ï¼ˆä»…åŽå°äººå‘˜ï¼‰
export function delCustomerFinancialReport(id) {
  return request({
    url: '/basic/customerFinancialReport/' + id,
    method: 'delete'
  })
}
// ç”³æŠ¥å®¡æ‰¹ï¼šæŸ¥è¯¢æ‰€æœ‰å®¢æˆ·æäº¤æƒ…况
export function auditListCustomerFinancialReport(params) {
  return request({
    url: '/basic/customerFinancialReport/auditList',
    method: 'get',
    params
  })
}
// ç”³æŠ¥å®¡æ‰¹ï¼šé€šè¿‡/退回
export function auditCustomerFinancialReport(data) {
  return request({
    url: '/basic/customerFinancialReport/audit',
    method: 'post',
    data
  })
}
src/api/customerPortal/submission.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
import request from "@/utils/request";
export function listMySubmissions(params) {
  return request({ url: "/customer/submission/mine", method: "get", params });
}
export function submitCustomerMaterial(data) {
  return request({ url: "/customer/submission", method: "post", data });
}
export function listSubmissionManage(params) {
  return request({ url: "/customer/submission/manage/list", method: "get", params });
}
export function reviewSubmission(data) {
  return request({ url: "/customer/submission/review", method: "put", data });
}
src/api/procurementManagement/procurementLedger.js
@@ -140,3 +140,35 @@
    data: query,
  });
}
// ===== é‡‡è´­å°è´¦ï¼ˆç®€ï¼‰ï¼šç‹¬ç«‹ç®€å•台账,不与合同/供应商/产品/入库关联 =====
export function simpleLedgerList(params) {
  return request({
    url: "/purchase/simpleLedger/list",
    method: "get",
    params,
  });
}
export function simpleLedgerGet(id) {
  return request({
    url: `/purchase/simpleLedger/${id}`,
    method: "get",
  });
}
export function simpleLedgerSave(data) {
  return request({
    url: "/purchase/simpleLedger/save",
    method: "post",
    data,
  });
}
export function simpleLedgerDelete(ids) {
  return request({
    url: "/purchase/simpleLedger/delete",
    method: "delete",
    data: ids,
  });
}
src/api/salesManagement/salesLedger.js
@@ -101,6 +101,23 @@
  });
}
// ä¸šåŠ¡æäº¤ç®¡ç†å®¡æ‰¹
export function submitBusinessApproval(id) {
  return request({
    url: `/business/approval/submit/${id}`,
    method: "post",
  });
}
// ç®¡ç†äººå‘˜å®¡æ ¸ä¸šåŠ¡ï¼›é€šè¿‡åŽåŽç«¯è‡ªåŠ¨ç”Ÿæˆå…­é˜¶æ®µé¡¹ç›®
export function reviewBusinessApproval(data) {
  return request({
    url: "/business/approval/review",
    method: "put",
    data,
  });
}
// æ ¹æ®é”€å”®åˆåŒå·æŸ¥äº§å“ä¿¡æ¯
export function getProductInfoByContractNo(query) {
  return request({
src/components/FileUpload/index.vue
@@ -1,6 +1,6 @@
<template>
  <div class="upload-file">
    <el-upload multiple :action="uploadFileUrl" :before-upload="handleBeforeUpload" :file-list="fileList" :data="data"
    <el-upload multiple :action="uploadFileUrl" name="files" :before-upload="handleBeforeUpload" :file-list="fileList" :data="data"
      :limit="limit" :on-error="handleUploadError" :on-exceed="handleExceed" :on-success="handleUploadSuccess"
      :show-file-list="false" :headers="headers" class="upload-file-uploader" ref="fileUpload" v-if="!disabled">
      <!-- ä¸Šä¼ æŒ‰é’® -->
@@ -18,9 +18,9 @@
      çš„æ–‡ä»¶
    </div>
    <!-- æ–‡ä»¶åˆ—表 -->
    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
    <transition-group ref="uploadListRef" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
        <el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
        <el-link :href="getFileUrl(file.url)" :underline="false" target="_blank">
          <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
        </el-link>
        <div class="ele-upload-list__item-content-action">
@@ -81,7 +81,8 @@
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const uploadList = ref([]);
const uploadListRef = ref();
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action); // ä¸Šä¼ æ–‡ä»¶æœåŠ¡å™¨åœ°å€
const headers = ref({ Authorization: "Bearer " + getToken() });
@@ -95,8 +96,20 @@
  (val) => {
    if (val) {
      let temp = 1;
      // é¦–先将值转为数组
      const list = Array.isArray(val) ? val : props.modelValue.split(",");
      let list = [];
      if (Array.isArray(val)) {
        list = val;
      } else if (typeof val === "string" && val.trim().startsWith("[")) {
        // åŽç«¯è¿”回/前端保存的 JSON æ•°ç»„格式:[{ id, name, url }]
        try {
          const parsed = JSON.parse(val);
          list = Array.isArray(parsed) ? parsed : [];
        } catch (e) {
          list = val.split(",");
        }
      } else {
        list = String(val).split(",");
      }
      // ç„¶åŽå°†æ•°ç»„转为对象数组
      fileList.value = list.map((item) => {
        if (typeof item === "string") {
@@ -158,13 +171,21 @@
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
function handleUploadSuccess(res, file) {
  if (res.code === 200) {
    uploadList.value.push({ name: res.fileName, url: res.fileName });
  // åŽç«¯ /common/upload è¿”回 { code, msg, data: [ { id, name, url, downloadURL ... } ] }
  const list = Array.isArray(res?.data) ? res.data : [];
  if (res?.code === 200 && list.length > 0) {
    list.forEach((item) => {
      uploadList.value.push({
        id: item.id ?? item.storageAttachmentId,
        name: item.name || item.originalFilename || item.fileName || file.name,
        url: item.url || item.downloadURL || item.previewURL || item.previewUrl || "",
      });
    });
    uploadedSuccessfully();
  } else {
    number.value--;
    proxy.$modal.closeLoading();
    proxy.$modal.msgError(res.msg);
    proxy.$modal.msgError(res?.msg || "上传文件失败");
    proxy.$refs.fileUpload.handleRemove(file);
    uploadedSuccessfully();
  }
@@ -190,33 +211,35 @@
}
// èŽ·å–æ–‡ä»¶åç§°
function getFileName(name) {
function getFileName(name) {
  // å¦‚果是url那么取最后的名字 å¦‚果不是直接返回
  if (name.lastIndexOf("/") > -1) {
    return name.slice(name.lastIndexOf("/") + 1);
  } else {
    return name;
  }
}
}
function getFileUrl(url) {
  if (!url) return "";
  return /^https?:\/\//i.test(url) ? url : `${baseUrl}${url}`;
}
// å¯¹è±¡è½¬æˆæŒ‡å®šå­—符串分隔
function listToString(list, separator) {
  let strs = "";
  separator = separator || ",";
  for (let i in list) {
    if (list[i].url) {
      strs += list[i].url + separator;
    }
  }
  return strs != "" ? strs.substr(0, strs.length - 1) : "";
// å¯¹è±¡è½¬æˆ JSON æ•°ç»„字符串,保存文件 id、名称与地址
function listToString(list) {
  const files = (list || [])
    .filter((item) => item && item.url)
    .map((item) => ({ id: item.id, name: item.name, url: item.url }));
  return files.length > 0 ? JSON.stringify(files) : "";
}
// åˆå§‹åŒ–拖拽排序
onMounted(() => {
  if (props.drag) {
    nextTick(() => {
      const element = document.querySelector(".upload-file-list");
      Sortable.create(element, {
onMounted(() => {
  if (props.drag && !props.disabled) {
    nextTick(() => {
      const element = uploadListRef.value?.$el || uploadListRef.value;
      if (!element) return;
      Sortable.create(element, {
        ghostClass: "file-upload-darg",
        onEnd: (evt) => {
          const movedItem = fileList.value.splice(evt.oldIndex, 1)[0];
src/components/ProjectManagement/ProgressReportDialog.vue
@@ -1,10 +1,11 @@
<template>
  <el-dialog v-model="visible" title="进度汇报" width="900px" top="8vh" append-to-body destroy-on-close @close="handleClose">
  <el-dialog v-model="visible" title="进度汇报" width="960px" top="6vh" append-to-body destroy-on-close @close="handleClose">
    <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px">
      <el-form-item label="项目阶段" prop="planNodeId">
        <el-select v-model="form.planNodeId" placeholder="请选择" clearable style="width: 100%">
        <el-select v-model="form.planNodeId" placeholder="请选择" style="width: 100%" @change="handleStageChange">
          <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
        </el-select>
        <div v-if="stageTip" class="stage-tip">{{ stageTip }}</div>
      </el-form-item>
      <el-row :gutter="20">
        <el-col :span="8">
@@ -63,8 +64,8 @@
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="上次进度(%)" prop="lastProgress">
            <el-input-number v-model="form.lastProgress" :min="0" :max="100" controls-position="right" style="width: 100%" />
          <el-form-item label="上次进度(%)">
            <el-input-number v-model="form.lastProgress" :min="0" :max="100" controls-position="right" style="width: 100%" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="8">
@@ -115,6 +116,70 @@
        </el-col>
      </el-row>
      <div v-if="isTeamStage" class="team-block">
        <div class="team-header">
          <span class="team-title">项目团队</span>
          <el-button type="primary" size="small" @click="addTeamRow">新增成员</el-button>
        </div>
        <el-table :data="teamRows" border size="small">
          <el-table-column label="姓名" width="150">
            <template #default="{ row }">
              <el-select v-model="row.userId" placeholder="请选择" filterable clearable style="width: 100%">
                <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column label="项目组角色" width="150">
            <template #default="{ row }">
              <el-select v-model="row.userRoleId" placeholder="请选择" filterable clearable style="width: 100%">
                <el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column label="进入日期" width="150">
            <template #default="{ row }">
              <el-date-picker
                v-model="row.joinTime"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择"
                style="width: 100%"
                clearable
              />
            </template>
          </el-table-column>
          <el-table-column label="离开日期" width="150">
            <template #default="{ row }">
              <el-date-picker
                v-model="row.departTime"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择"
                style="width: 100%"
                clearable
              />
            </template>
          </el-table-column>
          <el-table-column label="联系方式" min-width="150">
            <template #default="{ row }">
              <el-input v-model="row.contact" placeholder="请输入" clearable />
            </template>
          </el-table-column>
          <el-table-column label="备注" min-width="140">
            <template #default="{ row }">
              <el-input v-model="row.remark" placeholder="请输入" clearable />
            </template>
          </el-table-column>
          <el-table-column label="操作" width="70" align="center">
            <template #default="{ $index }">
              <el-button link type="danger" size="small" @click="removeTeamRow($index)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <el-form-item label="附件" prop="attachmentIds">
        <FileUpload v-model:file-list="form.storageBlobDTOs" />
      </el-form-item>
@@ -130,16 +195,18 @@
</template>
<script setup name="ProgressReportDialog">
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { getToken } from '@/utils/auth'
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { computed, ref, watch } from 'vue'
import { ElMessageBox } from 'element-plus'
import FileUpload from '@/components/AttachmentUpload/file/index.vue'
import { findRoleListPage } from '@/api/projectManagement/role'
import { userListAll } from '@/api/publicApi'
const props = defineProps({
  modelValue: { type: Boolean, default: false },
  projectId: { type: [Number, String], default: undefined },
  projectInfo: { type: Object, default: () => ({}) },
  planNodes: { type: Array, default: () => [] },
  stageRecords: { type: Array, default: () => [] },
  defaultPlanNodeId: { type: [Number, String], default: undefined }
})
@@ -151,6 +218,9 @@
})
const formRef = ref()
const userOptions = ref([])
const roleOptions = ref([])
const teamRows = ref([])
const form = ref({
  planNodeId: undefined,
@@ -176,40 +246,176 @@
  completionProgress: [{ required: true, message: '请输入', trigger: 'change' }]
}
const stageOptions = computed(() => {
const sortedNodes = computed(() => {
  const list = Array.isArray(props.planNodes) ? props.planNodes : []
  const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
  return sorted
  return [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
})
const stageOptions = computed(() =>
  sortedNodes.value
    .map(n => ({
      label: n.name || n.workContent || n.title || String(n.id ?? ''),
      value: n.id
    }))
    .filter(i => i.value !== undefined && i.value !== null && i.value !== '')
)
const stageNameOf = nodeId => {
  const node = sortedNodes.value.find(n => String(n.id) === String(nodeId))
  return node?.name || node?.workContent || ''
}
// å„阶段最近一条汇报记录(记录按阶段、时间升序返回)
const latestProgressMap = computed(() => {
  const map = {}
  const records = Array.isArray(props.stageRecords) ? props.stageRecords : []
  records.forEach(r => {
    const key = String(r.projectManagementPlanNodeId ?? '')
    map[key] = Number(r.progress) || 0
  })
  return map
})
// å½“前应汇报的下一阶段:第一个未完成(进度 < 100)的阶段
const expectedNodeId = computed(() => {
  const nodes = sortedNodes.value
  if (nodes.length === 0) return undefined
  const next = nodes.find(n => (latestProgressMap.value[String(n.id)] ?? 0) < 100)
  return (next || nodes[nodes.length - 1])?.id
})
const isTeamStage = computed(() => stageNameOf(form.value.planNodeId).includes('团队'))
const stageTip = computed(() => {
  if (!form.value.planNodeId) return ''
  const progress = latestProgressMap.value[String(form.value.planNodeId)] ?? 0
  if (progress >= 100) return '该阶段已完成,仍可继续补充汇报'
  if (expectedNodeId.value && String(form.value.planNodeId) !== String(expectedNodeId.value)) {
    return '当前选择的不是应汇报的下一阶段'
  }
  return ''
})
function todayStr() {
  const d = new Date()
  const pad = n => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
function latestRecordOf(nodeId) {
  const records = Array.isArray(props.stageRecords) ? props.stageRecords : []
  const matched = records.filter(r => String(r.projectManagementPlanNodeId) === String(nodeId))
  return matched.length ? matched[matched.length - 1] : null
}
function applyStageBase(nodeId) {
  const record = latestRecordOf(nodeId)
  form.value.lastProgress = Number(record?.progress ?? 0) || 0
  form.value.completionProgress = 0
  form.value.totalProgress = Number(record?.progress ?? 0) || 0
  form.value.planStartTime = record?.planStartTime || props.projectInfo?.planStartTime || ''
  form.value.planEndTime = record?.planEndTime || props.projectInfo?.planEndTime || ''
  form.value.actualStartTime = record?.actualStartTime || props.projectInfo?.actualStartTime || ''
  form.value.actualEndTime = record?.actualEndTime || ''
}
async function handleStageChange(val) {
  applyStageBase(val)
  if (!val) return
  const expected = expectedNodeId.value
  if (expected && String(val) !== String(expected)) {
    try {
      await ElMessageBox.confirm(
        `当前应汇报的“${stageNameOf(expected)}”阶段还未完成,选择其他阶段将跳过中间阶段,是否继续?`,
        '提示',
        { confirmButtonText: 'ç»§ç»­', cancelButtonText: '返回', type: 'warning' }
      )
    } catch {
      form.value.planNodeId = expected
      applyStageBase(expected)
    }
  }
}
function resetFromProject() {
  const info = props.projectInfo || {}
  form.value = {
    planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value,
    planStartTime: info.planStartTime || '',
    planEndTime: info.planEndTime || '',
    actualStartTime: info.actualStartTime || '',
    actualEndTime: info.actualEndTime || '',
    reportDate: '',
    lastProgress: Number(info.lastProgress ?? 0) || 0,
    planNodeId: props.defaultPlanNodeId ?? expectedNodeId.value ?? stageOptions.value[0]?.value,
    planStartTime: '',
    planEndTime: '',
    actualStartTime: '',
    actualEndTime: '',
    reportDate: todayStr(),
    lastProgress: 0,
    completionProgress: 0,
    totalProgress: Number(info.totalProgress ?? info.progress ?? 0) || 0,
    totalProgress: 0,
    managerName: info.managerName || '',
    departmentName: info.departmentName || '',
    remark: '',
    storageBlobDTOs: []
  }
  applyStageBase(form.value.planNodeId)
  form.value.reportDate = todayStr()
  loadTeamRows()
}
function loadTeamRows() {
  const list = Array.isArray(props.projectInfo?.teamList) ? props.projectInfo.teamList : []
  teamRows.value = list.map(t => ({
    userId: t.userId,
    userRoleId: t.userRoleId,
    joinTime: t.joinTime || '',
    departTime: t.departTime || '',
    contact: t.contact || '',
    remark: t.remark || ''
  }))
  if (teamRows.value.length === 0) addTeamRow()
}
function addTeamRow() {
  teamRows.value.push({ userId: undefined, userRoleId: undefined, joinTime: '', departTime: '', contact: '', remark: '' })
}
function removeTeamRow(index) {
  teamRows.value.splice(index, 1)
  if (teamRows.value.length === 0) addTeamRow()
}
async function loadOptions() {
  try {
    const res = await userListAll()
    const list = res?.data || res?.rows || res || []
    userOptions.value = (Array.isArray(list) ? list : []).map(u => ({
      label: u.nickName || u.userName || u.username || u.name,
      value: u.userId || u.id
    }))
  } catch {
    userOptions.value = []
  }
  try {
    const res = await findRoleListPage({ pageNum: 1, pageSize: 999 })
    const records = res?.data?.records || res?.rows || res?.records || []
    roleOptions.value = records.map(item => ({ label: item.roleName || item.name, value: item.id }))
  } catch {
    roleOptions.value = []
  }
}
watch(
  () => props.modelValue,
  v => {
    if (v) resetFromProject()
    if (v) {
      loadOptions()
      resetFromProject()
    }
  }
)
// ç´¯è®¡è¿›åº¦è·Ÿéšæœ¬æ¬¡å®Œæˆè¿›åº¦
watch(
  () => form.value.completionProgress,
  v => {
    form.value.totalProgress = Number(v) || 0
  }
)
@@ -220,15 +426,39 @@
function markDone() {
  form.value.completionProgress = 100
  form.value.totalProgress = 100
  if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || ''
  if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || todayStr()
}
function buildTeamList() {
  const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label || ''
  return teamRows.value
    .filter(r => r.userId !== undefined && r.userId !== null && r.userId !== '')
    .map(r => ({
      userId: Number(r.userId),
      userName: findLabel(userOptions.value, r.userId),
      userRoleId: r.userRoleId ?? '',
      userRoleName: findLabel(roleOptions.value, r.userRoleId),
      joinTime: r.joinTime || null,
      departTime: r.departTime || null,
      contact: r.contact || '',
      remark: r.remark || ''
    }))
}
async function submit() {
  await formRef.value?.validate?.()
  emit('submitted', {
  try {
    await formRef.value?.validate?.()
  } catch {
    return
  }
  const payload = {
    projectId: props.projectId,
    ...form.value
  })
  }
  if (isTeamStage.value) {
    payload.teamList = buildTeamList()
  }
  emit('submitted', payload)
  visible.value = false
}
</script>
@@ -239,4 +469,28 @@
  justify-content: flex-end;
  gap: 10px;
}
.stage-tip {
  margin-top: 6px;
  font-size: 12px;
  color: #e6a23c;
  line-height: 1.4;
}
.team-block {
  margin-bottom: 18px;
}
.team-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}
.team-title {
  font-weight: 600;
  color: #303133;
  font-size: 14px;
}
</style>
src/permission.js
@@ -11,7 +11,7 @@
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/register', '/callbacklccpn','/device-info']
const whiteList = ['/login', '/register', '/callbacklccpn','/device-info']
const isWhiteList = (path) => {
  return whiteList.some(pattern => isPathMatch(pattern, path))
src/router/index.js
@@ -72,31 +72,31 @@
    ],
  },
  // ç³»ç»Ÿæž¶æž„图
  {
    path: "/system-architecture",
    component: Layout,
    redirect: "/system-architecture/index",
    children: [
      {
        path: "index",
        component: () => import("@/views/systemArchitecture/index.vue"),
        name: "SystemArchitecture",
        meta: { title: "系统架构图", icon: "tree" },
      },
    ],
  },
  {
    path: "/ai-industrial-brain",
    component: Layout,
    children: [
      {
        path: "index",
        component: () => import("@/views/aiIndustrialBrain/index.vue"),
        name: "AiIndustrialBrain",
        meta: { title: "AI工业大脑", icon: "skill" },
      },
    ],
  },
  // {
  //   path: "/system-architecture",
  //   component: Layout,
  //   redirect: "/system-architecture/index",
  //   children: [
  //     {
  //       path: "index",
  //       component: () => import("@/views/systemArchitecture/index.vue"),
  //       name: "SystemArchitecture",
  //       meta: { title: "系统架构图", icon: "tree" },
  //     },
  //   ],
  // },
  // {
  //   path: "/ai-industrial-brain",
  //   component: Layout,
  //   children: [
  //     {
  //       path: "index",
  //       component: () => import("@/views/aiIndustrialBrain/index.vue"),
  //       name: "AiIndustrialBrain",
  //       meta: { title: "AI工业大脑", icon: "skill" },
  //     },
  //   ],
  // },
  // æ•™å­¦æ¼”示模块
  {
    path: "/productionManagement/teachingDemo",
src/views/basicData/customerFile/index.vue
@@ -26,17 +26,18 @@
      </div>
      <div>
        <el-button type="primary"
                   v-hasPermi="['basic:customer:add']"
                   @click="openForm('add')">新增客户</el-button>
        <el-button type="primary"
                   plain
                   @click="back">流入公海</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button v-hasPermi="['basic:customer:export']"
                   @click="handleOut">导出</el-button>
        <el-button type="info"
                   plain
                   icon="Upload"
                   v-hasPermi="['basic:customer:import']"
                   @click="handleImport">导入</el-button>
        <el-button type="danger"
                   plain
                   v-hasPermi="['basic:customer:remove']"
                   @click="handleDelete">删除</el-button>
      </div>
    </div>
@@ -132,6 +133,23 @@
                           value="三大油" />
                <el-option label="外销"
                           value="外销" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="绑定登录账号:"
                          prop="bindUserId">
              <el-select v-model="form.bindUserId"
                         placeholder="请选择该客户的登录账号"
                         clearable
                         filterable
                         style="width: 100%">
                <el-option v-for="item in userList"
                           :key="item.userId"
                           :label="item.nickName + '(' + item.userName + ')'"
                           :value="item.userId" />
              </el-select>
            </el-form-item>
          </el-col>
@@ -244,127 +262,96 @@
        </div>
      </template>
    </el-dialog>
    <!-- å›žè®¿æé†’对话框 -->
    <el-dialog title="回访提醒"
               v-model="reminderDialogVisible"
               width="500px"
               @close="closeReminderDialog">
      <el-form :model="reminderForm"
               label-width="100px"
               :rules="reminderRules"
               ref="reminderFormRef">
        <el-form-item label="客户名称:">
          <el-input v-model="reminderForm.customerName"
                    disabled />
    <!-- è¿‘3年财务报表上传 -->
    <el-dialog title="近3年财务报表上传"
               v-model="reportDialogVisible"
               width="900px"
               @close="closeReportDialog">
      <div class="report-customer">客户:{{ reportCustomer.customerName }}</div>
      <el-form :model="reportForm"
               label-width="90px"
               inline
               class="report-form">
        <el-form-item label="报表年度">
          <el-select v-model="reportForm.reportYear"
                     placeholder="请选择年度"
                     style="width: 150px">
            <el-option v-for="year in reportYears"
                       :key="year"
                       :label="year + 'å¹´'"
                       :value="year" />
          </el-select>
        </el-form-item>
        <el-form-item label="提醒开关:">
          <el-switch v-model="reminderForm.reminderSwitch" />
        </el-form-item>
        <el-form-item label="提醒内容:"
                      prop="reminderContent">
          <el-input v-model="reminderForm.reminderContent"
                    type="textarea"
                    :maxlength="100"
                    show-word-limit
                    placeholder="请输入提醒内容" />
        </el-form-item>
        <el-form-item label="提醒时间:"
                      prop="reminderTime">
          <el-date-picker v-model="reminderForm.reminderTime"
                          type="datetime"
                          value-format="YYYY-MM-DD HH:mm:ss"
                          format="YYYY-MM-DD HH:mm:ss"
                          placeholder="请选择提醒时间"
                          style="width: 100%" />
        <el-form-item label="报表名称">
          <el-input v-model="reportForm.reportName"
                    placeholder="例如:2025年度财务报表"
                    style="width: 260px"
                    clearable />
        </el-form-item>
      </el-form>
      <FileUpload v-model="reportForm.reportFiles"
                  :limit="5"
                  :file-size="20"
                  :file-type="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png', 'zip', 'rar']" />
      <div class="report-actions">
        <el-button type="primary"
                   :loading="reportSaving"
                   @click="saveReport">保存上传</el-button>
        <el-button @click="resetReportForm">重置</el-button>
      </div>
      <el-table :data="reportRows"
                border
                size="small"
                style="margin-top: 16px">
        <el-table-column prop="reportYear"
                         label="年度"
                         width="90"
                         align="center" />
        <el-table-column prop="reportName"
                         label="报表名称"
                         min-width="180"
                         show-overflow-tooltip />
        <el-table-column label="报表附件"
                         min-width="260">
          <template #default="scope">
            <FileUpload v-model="scope.row.reportFiles"
                        disabled
                        :is-show-tip="false" />
          </template>
        </el-table-column>
        <el-table-column prop="uploadUserName"
                         label="上传人"
                         width="110" />
        <el-table-column prop="updateTime"
                         label="上传时间"
                         width="170" />
        <el-table-column label="审批状态"
                         width="100"
                         align="center">
          <template #default="scope">
            <el-tag :type="reportStatusTag(scope.row.auditStatus)">
              {{ reportStatusText(scope.row.auditStatus) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作"
                         width="140"
                         align="center">
          <template #default="scope">
            <el-button type="primary"
                       link
                       @click="editReport(scope.row)">修改</el-button>
            <el-button type="danger"
                       link
                       @click="deleteReport(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div v-if="reportRows.length === 0"
           class="no-record">暂无财务报表,请按年度上传近3年财务报表</div>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitReminderForm">确认</el-button>
          <el-button @click="closeReminderDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ·»åŠ /修改洽谈进度对话框 -->
    <el-dialog :title="negotiationForm.editIndex !== undefined ? '修改进度' : '添加进度'"
               v-model="negotiationDialogVisible"
               width="600px"
               @close="closeNegotiationDialog">
      <el-form :model="negotiationForm"
               label-width="100px"
               :rules="negotiationRules"
               ref="negotiationFormRef">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="跟进方式:"
                          prop="followUpMethod">
              <el-select v-model="negotiationForm.followUpMethod"
                         placeholder="请选择"
                         style="width: 100%">
                <el-option label="电话"
                           value="电话" />
                <el-option label="邮件"
                           value="邮件" />
                <el-option label="上门"
                           value="上门" />
                <el-option label="微信"
                           value="微信" />
                <el-option label="其他"
                           value="其他" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="跟进程度:"
                          prop="followUpLevel">
              <el-select v-model="negotiationForm.followUpLevel"
                         placeholder="请选择"
                         style="width: 100%">
                <el-option label="潜在客户"
                           value="潜在客户" />
                <el-option label="初次拜访"
                           value="初次拜访" />
                <el-option label="多次拜访"
                           value="多次拜访" />
                <el-option label="意向客户"
                           value="意向客户" />
                <el-option label="已签约客户"
                           value="已签约客户" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="跟进时间:"
                          prop="followUpTime">
              <el-date-picker v-model="negotiationForm.followUpTime"
                              type="datetime"
                              value-format="YYYY-MM-DD HH:mm:ss"
                              format="YYYY-MM-DD HH:mm:ss"
                              placeholder="请选择"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="跟进人:">
              <el-input v-model="negotiationForm.followerUserName"
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="内容:"
                      prop="content">
          <el-input v-model="negotiationForm.content"
                    type="textarea"
                    :rows="4"
                    placeholder="请输入" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitNegotiationForm">确认</el-button>
          <el-button @click="closeNegotiationDialog">取消</el-button>
          <el-button @click="closeReportDialog">关闭</el-button>
        </div>
      </template>
    </el-dialog>
@@ -373,7 +360,6 @@
               v-model="detailDialogVisible"
               width="1000px"
               @close="closeDetailDialog">
      <!-- å®¢æˆ·åŸºæœ¬ä¿¡æ¯ -->
      <div class="detail-section">
        <h3 class="section-title">客户基本信息</h3>
        <div class="info-display">
@@ -450,10 +436,18 @@
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">绑定登录账号:</span>
                <span class="info-value">{{ detailForm.bindUserName || '-' }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">维护人:</span>
                <span class="info-value">{{ detailForm.maintainer }}</span>
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">维护时间:</span>
@@ -463,149 +457,41 @@
          </el-row>
        </div>
      </div>
      <!-- æ´½è°ˆè¿›åº¦è®°å½• -->
      <div class="detail-section">
        <div class="section-header">
          <h3 class="section-title">洽谈进度记录</h3>
          <el-button type="primary"
                     size="small"
                     @click="openNegotiationDialog(detailForm)">
            æ·»åŠ è¿›åº¦
          </el-button>
        </div>
        <el-table :data="negotiationRecords"
        <h3 class="section-title">近3年财务报表</h3>
        <el-table :data="detailReports"
                  border
                  size="small"
                  style="width: 100%">
          <el-table-column prop="followUpTime"
                           label="跟进时间"
                           width="160" />
          <el-table-column prop="followUpMethod"
                           label="跟进方式"
                           width="100" />
          <el-table-column prop="followUpLevel"
                           label="跟进程度" />
          <el-table-column prop="followerUserName"
                           label="跟进人"
                           width="100" />
          <el-table-column prop="content"
                           label="内容"
          <el-table-column prop="reportYear"
                           label="年度"
                           width="90"
                           align="center" />
          <el-table-column prop="reportName"
                           label="报表名称"
                           min-width="180"
                           show-overflow-tooltip />
          <el-table-column label="附件"
                           width="100"
                           align="center">
            <template #default="{ row }">
              <el-button type="info"
                         link
                         @click="openAttachmentDialog(row)">
                <el-icon>
                  <Paperclip />
                </el-icon>
                é™„ä»¶
                <!-- {{ row.fileList && row.fileList.length > 0 ? row.fileList.length : '上传' }} -->
              </el-button>
          <el-table-column label="报表附件"
                           min-width="260">
            <template #default="scope">
              <FileUpload v-model="scope.row.reportFiles"
                          disabled
                          :is-show-tip="false" />
            </template>
          </el-table-column>
          <el-table-column label="操作"
                           width="150"
                           align="center">
            <template #default="{ row, $index }">
              <el-button type="primary"
                         link
                         @click="editNegotiationRecord(row, $index)">
                ä¿®æ”¹
              </el-button>
              <el-button type="danger"
                         link
                         @click="deleteNegotiationRecord(row, $index)">
                åˆ é™¤
              </el-button>
            </template>
          </el-table-column>
          <el-table-column prop="uploadUserName"
                           label="上传人"
                           width="110" />
          <el-table-column prop="updateTime"
                           label="上传时间"
                           width="170" />
        </el-table>
        <div v-if="negotiationRecords.length === 0"
             class="no-records">
          æš‚无洽谈进度记录
        </div>
        <div v-if="detailReports.length === 0"
             class="no-record">暂无财务报表</div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDetailDialog">关闭</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é™„件上传弹窗 -->
    <el-dialog title="附件管理"
               v-model="attachmentDialogVisible"
               width="600px"
               @close="closeAttachmentDialog">
      <div class="attachment-section">
        <div class="upload-area">
          <el-upload ref="attachmentUploadRef"
                     :action="getAttachmentUploadUrl()"
                     :headers="attachmentUploadHeaders"
                     :file-list="currentAttachmentList"
                     :on-success="handleAttachmentSuccess"
                     :on-error="handleAttachmentError"
                     :on-remove="handleAttachmentRemove"
                     :before-upload="beforeAttachmentUpload"
                     multiple
                     :limit="10"
                     name="files">
            <el-button type="primary">
              <el-icon>
                <Upload />
              </el-icon>
              ä¸Šä¼ é™„ä»¶
            </el-button>
            <template #tip>
              <div class="el-upload__tip">
                æ”¯æŒä¸Šä¼ å›¾ç‰‡ã€æ–‡æ¡£ç­‰æ–‡ä»¶ï¼Œå•个文件不超过50MB
              </div>
            </template>
          </el-upload>
        </div>
        <div v-if="currentAttachmentList.length > 0"
             class="attachment-list">
          <h4>已上传附件:</h4>
          <el-table :data="currentAttachmentList"
                    border
                    size="small">
            <el-table-column prop="name"
                             label="文件名"
                             show-overflow-tooltip />
            <el-table-column prop="size"
                             label="大小"
                             width="100">
              <template #default="{ row }">
                {{ formatFileSize(row.size) }}
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             width="120"
                             align="center">
              <template #default="{ row, $index }">
                <el-button type="primary"
                           link
                           @click="downloadAttachment(row)">
                  ä¸‹è½½
                </el-button>
                <el-button type="danger"
                           link
                           @click="deleteAttachment(row, $index)">
                  åˆ é™¤
                </el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <div v-else
             class="no-attachment">
          æš‚无附件
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeAttachmentDialog">关闭</el-button>
        </div>
      </template>
    </el-dialog>
@@ -614,68 +500,24 @@
<script setup>
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
  import { Search, Close } from "@element-plus/icons-vue";
  import { listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer } from "@/api/basicData/customer.js";
  import {
    addCustomerFollow,
    updateCustomerFollow,
    delCustomerFollow,
    addReturnVisit,
    getReturnVisit,
  } from "@/api/basicData/customerFile.js";
  import {listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer, backCustomer} from "@/api/basicData/customer.js";
    listCustomerFinancialReport,
    saveCustomerFinancialReport,
    delCustomerFinancialReport,
  } from "@/api/basicData/customerFinancialReport.js";
  import { ElMessageBox } from "element-plus";
  import { userListNoPage } from "@/api/system/user.js";
  import useUserStore from "@/store/modules/user";
  import { getToken } from "@/utils/auth.js";
  import FileUpload from "@/components/FileUpload/index.vue";
  const { proxy } = getCurrentInstance();
  const userStore = useUserStore();
  // å›žè®¿æé†’相关
  const reminderDialogVisible = ref(false);
  const reminderFormRef = ref();
  const currentCustomerId = ref();
  const reminderForm = reactive({
    customerName: "",
    reminderSwitch: false,
    reminderContent: "",
    reminderTime: "",
  });
  const reminderRules = {
    reminderContent: [
      { required: true, message: "请输入提醒内容", trigger: "blur" },
    ],
    reminderTime: [
      { required: true, message: "请选择提醒时间", trigger: "change" },
    ],
  };
  // æ´½è°ˆè¿›åº¦ç›¸å…³
  const negotiationDialogVisible = ref(false);
  const negotiationFormRef = ref();
  const negotiationForm = reactive({
    customerName: "",
        customerId: "",
    followUpMethod: "",
    followUpLevel: "",
    followUpTime: "",
    followerUserName: "",
    content: "",
  });
  const negotiationRules = {
    followUpMethod: [
      { required: true, message: "请选择跟进方式", trigger: "change" },
    ],
    followUpLevel: [
      { required: true, message: "请选择跟进程度", trigger: "change" },
    ],
    followUpTime: [
      { required: true, message: "请选择跟进时间", trigger: "change" },
    ],
    content: [{ required: true, message: "请输入内容", trigger: "blur" }],
  };
  // è¯¦æƒ…相关
  const detailDialogVisible = ref(false);
  const detailReports = ref([]);
  const detailForm = reactive({
    customerName: "",
    customerType: "",
@@ -687,26 +529,20 @@
    bankCode: "",
    contactPerson: "",
    contactPhone: "",
    bindUserId: "",
    bindUserName: "",
    maintainer: "",
    maintenanceTime: "",
  });
  const negotiationRecords = ref([]);
  // é™„件相关
  const attachmentDialogVisible = ref(false);
  const attachmentUploadRef = ref();
  const currentAttachmentList = ref([]);
  const currentFollowRecord = ref({});
  const attachmentUploadHeaders = { Authorization: "Bearer " + getToken() };
  // åŠ¨æ€æž„å»ºä¸Šä¼ URL
  const getAttachmentUploadUrl = () => {
    const baseUrl =
      import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload";
    return currentFollowRecord.value.id
      ? `${baseUrl}/${currentFollowRecord.value.id}`
      : baseUrl;
  };
  // è¿‘3年财务报表相关
  const reportDialogVisible = ref(false);
  const reportSaving = ref(false);
  const reportCustomer = reactive({ id: "", customerName: "" });
  const reportRows = ref([]);
  const nowYear = new Date().getFullYear();
  const reportYears = [nowYear, nowYear - 1, nowYear - 2];
  const reportForm = reactive({ reportYear: nowYear, reportName: "", reportFiles: "" });
  const tableColumn = ref([
    {
@@ -726,7 +562,7 @@
    },
    {
      label: "地址及联系方式",
      prop: "companyAddress",
      prop: "addressPhone",
      width: 250,
    },
    {
@@ -739,97 +575,36 @@
      width: 150,
    },
    {
      label: "跟进进度",
      prop: "followUpLevel",
      width: 120,
    },
    {
      label: "跟进时间",
      prop: "followUpTime",
      width: 120,
    },
    {
      label: "银行基本户",
      prop: "basicBankAccount",
      width: 220,
    },
    {
      label: "银行账号",
      prop: "bankAccount",
      width: 220,
    },
    {
      label: "开户行号",
      prop: "bankCode",
      width: 220,
    },
    {
      label: "维护人",
      prop: "maintainer",
    },
    {
      label: "客户来源",
      prop: "type",
      dataType: "tag",
      width: 100,
      formatData: value => {
        if (value === 1 || value === "1") {
          return "公海";
        }
        return "私海";
      },
      formatType: value => {
        if (value === 1 || value === "1") {
          return "warning";
        }
        return "success";
      },
    },
    {
      label: "维护时间",
      prop: "maintenanceTime",
      width: 100,
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 290,
      width: 240,
      operation: [
        {
          name: "编辑",
          type: "text",
          showHide: row => !isCustomerEditLocked(row),
          showHide: () => proxy.$auth.hasPermi("basic:customer:edit"),
          clickFun: row => {
            if (isCustomerEditLocked(row)) {
              proxy.$modal.msgWarning("已被销售模块引用的私海客户不能编辑");
              return;
            }
            openForm("edit", row);
          },
        },
                {
                    name: "添加洽谈进度",
                    type: "text",
                    clickFun: row => {
                        openNegotiationDialog(row);
                    },
                },
        {
          name: "回访提醒",
          name: "近3年财务报表",
          type: "text",
          showHide: () => proxy.$auth.hasPermi("basic:customer:report"),
          clickFun: row => {
            openReminderDialog(row);
            openReportDialog(row);
          },
        },
                {
                    name: "详情",
                    type: "text",
                    clickFun: row => {
                        openDetailDialog(row);
                    },
                },
        {
          name: "详情",
          type: "text",
          showHide: () => proxy.$auth.hasPermi("basic:customer:query"),
          clickFun: row => {
            openDetailDialog(row);
          },
        },
      ],
    },
  ]);
@@ -842,13 +617,11 @@
    size: 100,
    total: 0,
  });
  const total = ref(0);
  // ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
  const operationType = ref("");
  const dialogFormVisible = ref(false);
  const formYYs = ref({
    // å…¶ä»–字段...
    contactList: [
      {
        contactPerson: "",
@@ -869,6 +642,7 @@
      companyPhone: "",
      contactPerson: "",
      contactPhone: "",
      bindUserId: "",
      maintainer: "",
      maintenanceTime: "",
      basicBankAccount: "",
@@ -884,8 +658,6 @@
      ],
      companyAddress: [{ required: true, message: "请输入", trigger: "blur" }],
      companyPhone: [{ required: true, message: "请输入", trigger: "blur" }],
      // contactPerson: [{ required: true, message: "请输入", trigger: "blur" }],
      // contactPhone: [{ required: true, message: "请输入", trigger: "blur" }],
      maintainer: [{ required: false, message: "请选择", trigger: "change" }],
      maintenanceTime: [
        { required: false, message: "请选择", trigger: "change" },
@@ -912,7 +684,6 @@
    },
    // æ–‡ä»¶ä¸Šä¼ å‰çš„回调
    beforeUpload: file => {
      console.log("文件即将上传", file);
      // å¯ä»¥åœ¨æ­¤å¤„做文件类型或大小校验
      const isValid =
        file.type ===
@@ -930,7 +701,6 @@
    },
    // æ–‡ä»¶ä¸Šä¼ æˆåŠŸæ—¶çš„å›žè°ƒ
    onSuccess: (response, file, fileList) => {
      console.log("上传成功", response, file, fileList);
      upload.isUploading = false;
      if (response.code === 200) {
        proxy.$modal.msgSuccess("文件上传成功");
@@ -945,12 +715,11 @@
    },
    // æ–‡ä»¶ä¸Šä¼ å¤±è´¥æ—¶çš„回调
    onError: (error, file, fileList) => {
      console.error("上传失败", error, file, fileList);
      upload.isUploading = false;
      proxy.$modal.msgError("文件上传失败");
    },
    // æ–‡ä»¶ä¸Šä¼ è¿›åº¦å›žè°ƒ
    onProgress: (event, file, fileList) => {
    onProgress: event => {
      console.log("上传中...", event.percent);
    },
  });
@@ -984,6 +753,8 @@
      tableLoading.value = false;
      tableData.value = res.data.records;
      page.total = res.data.total;
    }).catch(() => {
      tableLoading.value = false;
    });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
@@ -1006,10 +777,6 @@
  }
  // æ‰“开弹框
  const openForm = (type, row) => {
    if (type === "edit" && isCustomerEditLocked(row)) {
      proxy.$modal.msgWarning("已被销售模块引用的私海客户不能编辑");
      return;
    }
    operationType.value = type;
    form.value = {};
    form.value.maintainer = userStore.nickName;
@@ -1027,23 +794,21 @@
    if (type === "edit") {
      getCustomer(row.id).then(res => {
        form.value = { ...res.data };
        formYYs.value.contactList = res.data.contactPerson
        formYYs.value.contactList = (res.data.contactPerson || "")
          .split(",")
          .filter(item => item)
          .map((item, index) => {
            return {
              contactPerson: item,
              contactPhone: res.data.contactPhone.split(",")[index],
              contactPhone: (res.data.contactPhone || "").split(",")[index],
            };
          });
        if (formYYs.value.contactList.length === 0) {
          formYYs.value.contactList = [{ contactPerson: "", contactPhone: "" }];
        }
      });
    }
    dialogFormVisible.value = true;
  };
  const isCustomerEditLocked = row => {
    const salesReferenced = Number(row?.salesReferenceFlag || 0) === 1;
    const customerType = Number(row?.type);
    const usageStatus = Number(row?.usageStatus || 0);
    return salesReferenced && (customerType === 0 || (customerType === 1 && usageStatus === 1));
  };
  // æäº¤è¡¨å•
  const submitForm = () => {
@@ -1068,7 +833,7 @@
    form.value.contactPhone = formYYs.value.contactList
      .map(item => item.contactPhone)
      .join(",");
    addCustomer(form.value).then(res => {
    addCustomer(form.value).then(() => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
      getList();
@@ -1082,7 +847,7 @@
    form.value.contactPhone = formYYs.value.contactList
      .map(item => item.contactPhone)
      .join(",");
    updateCustomer(form.value).then(res => {
    updateCustomer(form.value).then(() => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
      getList();
@@ -1101,7 +866,7 @@
      type: "warning",
    })
      .then(() => {
        proxy.download("/basic/customer/export", {type: 0}, "客户档案.xlsx");
        proxy.download("/basic/customer/export", { type: 0 }, "客户档案.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -1144,182 +909,98 @@
      });
  };
  const back = () => {
    if (selectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
  // æ‰“开近3年财务报表弹窗
  const openReportDialog = row => {
    reportCustomer.id = row.id;
    reportCustomer.customerName = row.customerName;
    resetReportForm();
    loadReports(row.id);
    reportDialogVisible.value = true;
  };
  const loadReports = customerId => {
    listCustomerFinancialReport({ customerId }).then(res => {
      reportRows.value = res.data || [];
    });
  };
  const reportStatusText = status => {
    if (status === 1) return "已通过";
    if (status === 2) return "已退回";
    return "待审批";
  };
  const reportStatusTag = status => {
    if (status === 1) return "success";
    if (status === 2) return "danger";
    return "warning";
  };
  const resetReportForm = () => {
    reportForm.reportYear = nowYear;
    reportForm.reportName = "";
    reportForm.reportFiles = "";
  };
  const editReport = row => {
    reportForm.reportYear = row.reportYear;
    reportForm.reportName = row.reportName;
    reportForm.reportFiles = row.reportFiles;
  };
  const saveReport = () => {
    if (!reportForm.reportFiles) {
      proxy.$modal.msgWarning("请上传财务报表附件");
      return;
    }
    const ids = selectedRows.value.map(item => item.id);
    ElMessageBox.confirm("选中的客户将流入公海,是否确认?", "流入公海提示", {
    reportSaving.value = true;
    saveCustomerFinancialReport({
      customerId: reportCustomer.id,
      reportYear: reportForm.reportYear,
      reportName: reportForm.reportName,
      reportFiles: reportForm.reportFiles,
    }).then(() => {
      proxy.$modal.msgSuccess("保存成功");
      resetReportForm();
      loadReports(reportCustomer.id);
    }).finally(() => {
      reportSaving.value = false;
    });
  };
  const deleteReport = row => {
    ElMessageBox.confirm("确定要删除该年度财务报表吗?", "删除提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        tableLoading.value = true;
        return Promise.all(ids.map(id => backCustomer(id)))
          .then(() => {
            proxy.$modal.msgSuccess("流入公海成功");
            selectedRows.value = [];
            getList();
          })
          .finally(() => {
            tableLoading.value = false;
          });
        delCustomerFinancialReport(row.id).then(() => {
          proxy.$modal.msgSuccess("删除成功");
          loadReports(reportCustomer.id);
        });
      })
      .catch(error => {
        if (error === "cancel" || error === "close") {
          proxy.$modal.msg("已取消");
        }
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // æ‰“开回访提醒弹窗
  const openReminderDialog = row => {
    currentCustomerId.value = row.id;
    reminderForm.customerName = row.customerName;
    reminderForm.reminderSwitch = false;
    reminderForm.reminderContent = "";
    reminderForm.reminderTime = "";
    // å°è¯•获取已有的回访提醒
    getReturnVisit(row.id)
      .then(res => {
        if (res.code === 200 && res.data) {
          reminderForm.reminderSwitch = res.data.isEnabled === 1;
          reminderForm.reminderContent = res.data.content;
          reminderForm.reminderTime = res.data.reminderTime;
          reminderForm.id = res.data.id;
        }
      })
      .catch(error => {
        console.error("获取回访提醒失败:", error);
      });
    reminderDialogVisible.value = true;
  };
  // å…³é—­å›žè®¿æé†’弹窗
  const closeReminderDialog = () => {
    proxy.resetForm("reminderFormRef");
    reminderDialogVisible.value = false;
  };
  const submitvalue = ref({});
  // æäº¤å›žè®¿æé†’
  const submitReminderForm = () => {
    console.log("提交回访提醒数据:", userStore.id, userStore);
    proxy.$refs.reminderFormRef.validate(valid => {
      if (valid) {
        if (reminderForm.id) {
          submitvalue.value = {
            id: reminderForm.id,
                        customerId: currentCustomerId.value,
            isEnabled: reminderForm.reminderSwitch ? 1 : 0,
            content: reminderForm.reminderContent,
            reminderTime: reminderForm.reminderTime,
            remindUserId: userStore.id,
          };
        } else {
          submitvalue.value = {
            customerId: currentCustomerId.value,
            isEnabled: reminderForm.reminderSwitch ? 1 : 0,
            content: reminderForm.reminderContent,
            reminderTime: reminderForm.reminderTime,
            remindUserId: userStore.id,
          };
        }
        // è°ƒç”¨æŽ¥å£
        addReturnVisit(submitvalue.value)
          .then(res => {
            if (res.code === 200) {
              proxy.$modal.msgSuccess("回访提醒设置成功");
              closeReminderDialog();
            } else {
              proxy.$modal.msgError(res.msg || "设置失败");
            }
          })
          .catch(error => {
            console.error("设置回访提醒失败:", error);
            proxy.$modal.msgError("设置失败");
          });
      }
    });
  };
  // æ‰“开洽谈进度弹窗
  const openNegotiationDialog = row => {
    negotiationForm.customerName = row.customerName;
    negotiationForm.customerId = row.id;
    negotiationForm.followUpMethod = "";
    negotiationForm.followUpLevel = "";
    negotiationForm.followUpTime = "";
    negotiationForm.followerUserName = userStore.nickName; // é»˜è®¤å½“前登录人
    negotiationForm.content = "";
    negotiationDialogVisible.value = true;
  };
  // å…³é—­æ´½è°ˆè¿›åº¦å¼¹çª—
  const closeNegotiationDialog = () => {
    proxy.resetForm("negotiationFormRef");
    // æ¸…除编辑状态
    delete negotiationForm.editIndex;
    delete negotiationForm.id;
    negotiationDialogVisible.value = false;
  };
  // æäº¤æ´½è°ˆè¿›åº¦
  const submitNegotiationForm = () => {
    proxy.$refs.negotiationFormRef.validate(valid => {
      if (valid) {
        // åˆ¤æ–­æ˜¯æ–°å¢žè¿˜æ˜¯ä¿®æ”¹
        const isEdit = negotiationForm.editIndex !== undefined;
        if (isEdit) {
          // ä¿®æ”¹æ“ä½œ
          updateCustomerFollow(negotiationForm).then(res => {
            // æ›´æ–°æœ¬åœ°æ•°æ®
            getCustomer(negotiationForm.customerId).then(res => {
              // æ›´æ–°æœ¬åœ°æ•°æ®
              negotiationRecords.value = res.data.followUpList || [];
            });
          });
          proxy.$modal.msgSuccess("修改成功");
          closeNegotiationDialog();
        } else {
          // æ–°å¢žæ“ä½œ
          console.log("提交洽谈进度数据:", negotiationForm);
          addCustomerFollow(negotiationForm).then(res => {
            // æ·»åŠ æˆåŠŸåŽæ›´æ–°è¯¦æƒ…é¡µé¢çš„è¿›åº¦è®°å½•
            const newRecord = {
              followUpTime: negotiationForm.followUpTime,
              followUpMethod: negotiationForm.followUpMethod,
              followUpLevel: negotiationForm.followUpLevel,
              followerUserName: negotiationForm.followerUserName,
              content: negotiationForm.content,
            };
            negotiationRecords.value.unshift(newRecord);
            proxy.$modal.msgSuccess("提交成功");
            closeNegotiationDialog();
            getList();
          });
        }
      }
    });
  const closeReportDialog = () => {
    reportDialogVisible.value = false;
    reportRows.value = [];
    resetReportForm();
  };
  // æ‰“开详情弹窗
  const openDetailDialog = row => {
    getCustomer(row.id).then(res => {
      // å¡«å……客户基本信息
      Object.assign(detailForm, res.data);
      // èŽ·å–æ´½è°ˆè¿›åº¦è®°å½•
      negotiationRecords.value = res.data.followUpList || [];
      const bindUser = userList.value.find(item => item.userId === res.data.bindUserId);
      detailForm.bindUserName = bindUser ? bindUser.nickName : "";
      detailReports.value = [];
      detailDialogVisible.value = true;
      listCustomerFinancialReport({ customerId: row.id }).then(reportRes => {
        detailReports.value = reportRes.data || [];
      });
    });
  };
@@ -1328,201 +1009,19 @@
    detailDialogVisible.value = false;
  };
  // ä¿®æ”¹æ´½è°ˆè®°å½•
  const editNegotiationRecord = (row, index) => {
    // å°†å½“前记录数据填充到表单
    Object.assign(negotiationForm, {
      customerName: row.customerName,
            customerId: row.customerId,
      followUpMethod: row.followUpMethod,
      followUpLevel: row.followUpLevel,
      followUpTime: row.followUpTime,
      followerUserName: row.followerUserName,
      content: row.content,
      id: row.id, // è®°å½•ID用于更新
      editIndex: index, // è®°å½•索引用于本地更新
    });
    negotiationDialogVisible.value = true;
  };
  // åˆ é™¤æ´½è°ˆè®°å½•
  const deleteNegotiationRecord = (row, index) => {
    ElMessageBox.confirm("确定要删除这条洽谈记录吗?", "删除提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // è¿™é‡Œå¯ä»¥è°ƒç”¨åˆ é™¤æŽ¥å£
        // å®žé™…项目中需要根据后端接口进行调整
        // ç¤ºä¾‹ï¼šdeleteCustomerFollow(row.id).then(() => {
        //   negotiationRecords.value.splice(index, 1);
        //   proxy.$modal.msgSuccess("删除成功");
        // });
        delCustomerFollow(row.id).then(() => {
          // åˆ é™¤æˆåŠŸåŽæ›´æ–°æœ¬åœ°æ•°æ®
          getCustomer(row.customerId).then(res => {
            // æ›´æ–°æœ¬åœ°æ•°æ®
            negotiationRecords.value = res.data.followUpList || [];
          });
          proxy.$modal.msgSuccess("删除成功");
        });
        // æœ¬åœ°åˆ é™¤ï¼ˆæ¨¡æ‹Ÿï¼‰
        negotiationRecords.value.splice(index, 1);
        proxy.$modal.msgSuccess("删除成功");
      })
      .catch(() => {
        proxy.$modal.msg("已取消删除");
      });
  };
  // æ‰“开附件弹窗
  const openAttachmentDialog = row => {
    currentFollowRecord.value = row;
    // è½¬æ¢ä¸ºç¬¦åˆElement Plus fileList格式的数组
    currentAttachmentList.value = (row.fileList || []).map((file, index) => ({
      name: file.fileName,
      url: file.fileUrl,
      size: file.fileSize,
      id: file.id,
      uid: file.id || index,
      status: "success",
    }));
    attachmentDialogVisible.value = true;
  };
  // å…³é—­é™„件弹窗
  const closeAttachmentDialog = () => {
    attachmentDialogVisible.value = false;
    currentFollowRecord.value = {};
    currentAttachmentList.value = [];
  };
  // é™„件上传成功
  const handleAttachmentSuccess = (response, file, fileList) => {
    if (response.code === 200) {
      proxy.$modal.msgSuccess("上传成功");
      // æ›´æ–°å½“前记录的附件列表
      currentAttachmentList.value = fileList.map(item => ({
        name: item.name,
        size: item.size,
        url: item.response?.data?.url || item.url,
        id: item.response?.data?.id,
        uid: item.uid,
        status: "success",
      }));
      // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
      if (currentFollowRecord.value) {
        currentFollowRecord.value.files = [...currentAttachmentList.value];
      }
    } else {
      proxy.$modal.msgError(response.msg || "上传失败");
    }
  };
  // é™„件上传失败
  const handleAttachmentError = (error, file, fileList) => {
    console.error("上传失败:", error);
    proxy.$modal.msgError("上传失败");
  };
  // é™„件移除
  const handleAttachmentRemove = (file, fileList) => {
    currentAttachmentList.value = fileList;
    // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
    if (currentFollowRecord.value) {
      currentFollowRecord.value.files = [...fileList];
    }
  };
  // é™„件上传前校验
  const beforeAttachmentUpload = file => {
    const maxSize = 50 * 1024 * 1024; // 50MB
    if (file.size > maxSize) {
      proxy.$modal.msgError("文件大小不能超过50MB");
      return false;
    }
    return true;
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (size < 1024) {
      return size + " B";
    } else if (size < 1024 * 1024) {
      return (size / 1024).toFixed(2) + " KB";
    } else {
      return (size / (1024 * 1024)).toFixed(2) + " MB";
    }
  };
  // ä¸‹è½½é™„ä»¶
  const downloadAttachment = row => {
    if (row.url) {
      // proxy.download(row.url, {}, row.name);
            proxy.$download.byUrl(row.url, row.originalFilename);
    } else {
      proxy.$modal.msgError("下载链接不存在");
    }
  };
  // åˆ é™¤é™„ä»¶
  const deleteAttachment = (row, index) => {
    ElMessageBox.confirm("确定要删除这个附件吗?", "删除提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // è°ƒç”¨åŽç«¯æŽ¥å£åˆ é™¤é™„ä»¶
        const deleteUrl =
          import.meta.env.VITE_APP_BASE_API +
          "/basic/customer-follow/file/" +
          row.id;
        fetch(deleteUrl, {
          method: "DELETE",
          headers: {
            Authorization: "Bearer " + getToken(),
            "Content-Type": "application/json",
          },
        })
          .then(response => response.json())
          .then(res => {
            if (res.code === 200) {
              // åˆ é™¤æˆåŠŸåŽæ›´æ–°æœ¬åœ°æ–‡ä»¶åˆ—è¡¨
              currentAttachmentList.value.splice(index, 1);
              // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
              if (currentFollowRecord.value) {
                currentFollowRecord.value.files = [
                  ...currentAttachmentList.value,
                ];
              }
              proxy.$modal.msgSuccess("删除成功");
            } else {
              proxy.$modal.msgError(res.msg || "删除失败");
            }
          })
          .catch(error => {
            console.error("删除附件失败:", error);
            proxy.$modal.msgError("删除失败");
          });
      })
      .catch(() => {
        proxy.$modal.msg("已取消删除");
      });
  };
  // èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º YYYY-MM-DD
  function getCurrentDate() {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, "0"); // æœˆä»½ä»Ž0开始
    const month = String(today.getMonth() + 1).padStart(2, "0");
    const day = String(today.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
  }
  onMounted(() => {
    userListNoPage().then(res => {
      userList.value = res.data;
    });
    getList();
  });
</script>
@@ -1533,13 +1032,6 @@
    padding: 15px;
    background-color: #f9f9f9;
    border-radius: 4px;
  }
  .section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
  }
  .section-title {
@@ -1574,40 +1066,24 @@
    word-break: break-word;
  }
  .no-records {
  .no-record {
    text-align: center;
    padding: 30px;
    padding: 20px;
    color: #999;
    font-size: 14px;
  }
  .attachment-section {
    .upload-area {
      margin-bottom: 20px;
      padding: 20px;
      background-color: #f9f9f9;
      border-radius: 4px;
      border: 1px dashed #d9d9d9;
  .report-customer {
    font-size: 14px;
    color: #606266;
    margin-bottom: 12px;
  }
      .el-upload__tip {
        margin-top: 10px;
        color: #909399;
      }
    }
  .report-form {
    margin-bottom: 4px;
  }
    .attachment-list {
      h4 {
        margin: 0 0 10px 0;
        font-size: 14px;
        color: #606266;
      }
    }
    .no-attachment {
      text-align: center;
      padding: 40px;
      color: #909399;
      font-size: 14px;
    }
  .report-actions {
    margin-top: 10px;
  }
</style>
src/views/collaborativeApproval/approvalManagement/index.vue
@@ -3,879 +3,940 @@
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <div class="header-title">
        <el-icon class="title-icon"><Setting /></el-icon>
        <el-icon class="title-icon">
          <Setting />
        </el-icon>
        <span>审批流程配置</span>
      </div>
      <div class="header-desc">为不同审批类型配置审批流程和审批人</div>
    </div>
    <!-- å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 -->
    <div class="type-tabs">
      <div
        v-for="type in approveTypes"
        :key="type.value"
        class="type-tab"
        :class="{ active: activeTab === type.value }"
        @click="activeTab = type.value; handleTabChange()"
      >
        <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }">
      <div v-for="type in approveTypes"
           :key="type.value"
           class="type-tab"
           :class="{ active: activeTab === type.value }"
           @click="activeTab = type.value; handleTabChange()">
        <el-icon :size="14"
                 :style="{ color: activeTab === type.value ? type.color : '#909399' }">
          <component :is="type.icon" />
        </el-icon>
        <span class="tab-name">{{ type.label }}</span>
      </div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
    <el-card class="config-card" shadow="hover" v-loading="loading">
    <el-card class="config-card"
             shadow="hover"
             v-loading="loading">
      <template #header>
        <div class="card-header">
          <div class="header-left">
            <div class="type-icon" :style="{ backgroundColor: getTypeColor(currentApproveType) }">
              <el-icon :size="20" color="#fff"><component :is="getTypeIcon(currentApproveType)" /></el-icon>
            <div class="type-icon"
                 :style="{ backgroundColor: getTypeColor(currentApproveType) }">
              <el-icon :size="20"
                       color="#fff">
                <component :is="getTypeIcon(currentApproveType)" />
              </el-icon>
            </div>
            <div class="header-info">
              <span class="type-name">{{ currentApproveTypeName }}</span>
              <el-tag :type="approverList.length > 0 ? 'success' : 'warning'" size="small" effect="light">
              <el-tag :type="approverList.length > 0 ? 'success' : 'warning'"
                      size="small"
                      effect="light">
                {{ approverList.length > 0 ? `已配置 ${approverList.length} ä¸ªå®¡æ‰¹äºº` : '未配置审批人' }}
              </el-tag>
            </div>
          </div>
          <div class="header-actions" v-if="approverList.length > 0">
            <el-button @click="handleReset" size="default">
              <el-icon><RefreshLeft /></el-icon>
          <div class="header-actions"
               v-if="approverList.length > 0">
            <el-button @click="handleReset"
                       size="default">
              <el-icon>
                <RefreshLeft />
              </el-icon>
              é‡ç½®
            </el-button>
            <el-button type="primary" @click="handleSave" :loading="saveLoading" size="default">
              <el-icon><Check /></el-icon>
            <el-button type="primary"
                       @click="handleSave"
                       :loading="saveLoading"
                       size="default">
              <el-icon>
                <Check />
              </el-icon>
              ä¿å­˜é…ç½®
            </el-button>
          </div>
        </div>
      </template>
      <!-- å®¡æ‰¹æµç¨‹å±•示 -->
      <div class="flow-wrapper" v-if="approverList.length > 0">
      <div class="flow-wrapper"
           v-if="approverList.length > 0">
        <div class="flow-container">
          <div
            v-for="(item, index) in approverList"
            :key="index"
            class="flow-item"
          >
          <div v-for="(item, index) in approverList"
               :key="index"
               class="flow-item">
            <!-- å®¡æ‰¹èŠ‚ç‚¹å¡ç‰‡ -->
            <div class="node-card" :class="{ 'empty': !item.approverId }">
            <div class="node-card"
                 :class="{ 'empty': !item.approverId }">
              <!-- é¡¶éƒ¨åºå·å’Œçº§åˆ« -->
              <div class="node-badge">{{ index + 1 }}</div>
              <!-- å¤´åƒåŒºåŸŸ -->
              <div class="node-avatar-section">
                <div
                  class="node-avatar"
                  :class="{ 'has-user': item.approverId }"
                  :style="item.approverId ? { backgroundColor: getAvatarColor(item.approverName) } : {}"
                >
                <div class="node-avatar"
                     :class="{ 'has-user': item.approverId }"
                     :style="item.approverId ? { backgroundColor: getAvatarColor(item.approverName) } : {}">
                  <span v-if="item.approverId">{{ item.approverName.charAt(0) }}</span>
                  <el-icon v-else :size="24"><User /></el-icon>
                  <el-icon v-else
                           :size="24">
                    <User />
                  </el-icon>
                </div>
                <div class="node-level">{{ getLevelText(index) }}</div>
              </div>
              <!-- é€‰æ‹©åŒºåŸŸ -->
              <div class="node-select-section">
                <el-select
                  v-model="item.approverId"
                  placeholder="选择审批人"
                  filterable
                  size="default"
                  @change="(val) => handleApproverChange(val, item)"
                >
                  <el-option
                    v-for="user in userList"
                    :key="user.userId"
                    :label="user.nickName"
                    :value="user.userId"
                  />
                <el-select v-model="item.approverId"
                           placeholder="选择审批人"
                           filterable
                           size="default"
                           @change="(val) => handleApproverChange(val, item)">
                  <el-option v-for="user in userList"
                             :key="user.userId"
                             :label="user.nickName"
                             :value="user.userId" />
                </el-select>
              </div>
              <!-- æ“ä½œæŒ‰é’® -->
              <div class="node-actions">
                <el-button
                  type="primary"
                  circle
                  :disabled="index === 0"
                  @click="moveLeft(index)"
                  size="small"
                  class="action-btn"
                  title="前移"
                >
                  <el-icon><ArrowLeft /></el-icon>
                <el-button type="primary"
                           circle
                           :disabled="index === 0"
                           @click="moveLeft(index)"
                           size="small"
                           class="action-btn"
                           title="前移">
                  <el-icon>
                    <ArrowLeft />
                  </el-icon>
                </el-button>
                <el-button
                  type="primary"
                  circle
                  :disabled="index === approverList.length - 1"
                  @click="moveRight(index)"
                  size="small"
                  class="action-btn"
                  title="后移"
                >
                  <el-icon><ArrowRight /></el-icon>
                <el-button type="primary"
                           circle
                           :disabled="index === approverList.length - 1"
                           @click="moveRight(index)"
                           size="small"
                           class="action-btn"
                           title="后移">
                  <el-icon>
                    <ArrowRight />
                  </el-icon>
                </el-button>
                <el-button
                  type="danger"
                  circle
                  @click="handleDelete(index)"
                  size="small"
                  class="action-btn"
                >
                  <el-icon><Delete /></el-icon>
                <el-button type="danger"
                           circle
                           @click="handleDelete(index)"
                           size="small"
                           class="action-btn">
                  <el-icon>
                    <Delete />
                  </el-icon>
                </el-button>
              </div>
            </div>
            <!-- è¿žæŽ¥ç®­å¤´ -->
            <div class="arrow-connector" v-if="index < approverList.length - 1">
            <div class="arrow-connector"
                 v-if="index < approverList.length - 1">
              <div class="arrow-line"></div>
              <el-icon class="arrow-icon"><ArrowRight /></el-icon>
              <el-icon class="arrow-icon">
                <ArrowRight />
              </el-icon>
            </div>
          </div>
          <!-- æ–°å¢žèŠ‚ç‚¹æŒ‰é’® - æ”¾åœ¨æµç¨‹æœ€åŽ -->
          <div class="add-node-item">
            <div class="arrow-connector" v-if="approverList.length > 0">
            <div class="arrow-connector"
                 v-if="approverList.length > 0">
              <div class="arrow-line"></div>
              <el-icon class="arrow-icon"><ArrowRight /></el-icon>
              <el-icon class="arrow-icon">
                <ArrowRight />
              </el-icon>
            </div>
            <div class="add-node-card" @click="handleAdd">
            <div class="add-node-card"
                 @click="handleAdd">
              <div class="add-icon-wrapper">
                <el-icon :size="28"><Plus /></el-icon>
                <el-icon :size="28">
                  <Plus />
                </el-icon>
              </div>
              <span class="add-text">新增审批人</span>
            </div>
          </div>
        </div>
      </div>
      <!-- ç©ºçŠ¶æ€ -->
      <div class="empty-state" v-else>
      <div class="empty-state"
           v-else>
        <div class="empty-content">
          <div class="empty-icon-wrapper">
            <el-icon :size="48" color="#c0c4cc"><User /></el-icon>
            <el-icon :size="48"
                     color="#c0c4cc">
              <User />
            </el-icon>
          </div>
          <div class="empty-text">暂无审批人配置</div>
          <div class="empty-subtext">点击下方按钮添加第一个审批人</div>
          <el-button type="primary" size="large" @click="handleAdd" class="empty-add-btn">
            <el-icon><Plus /></el-icon>
          <el-button type="primary"
                     size="large"
                     @click="handleAdd"
                     class="empty-add-btn">
            <el-icon>
              <Plus />
            </el-icon>
            æ–°å¢žå®¡æ‰¹äºº
          </el-button>
        </div>
      </div>
    </el-card>
    <!-- åº•部提示 -->
    <div class="bottom-tips">
      <el-icon><InfoFilled /></el-icon>
      <el-icon>
        <InfoFilled />
      </el-icon>
      <span>提示:每个流程至少配置一个审批人,审批按顺序流转,可通过箭头调整顺序</span>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
  Plus, ArrowLeft, Delete, Check, RefreshLeft, Setting,
  Suitcase, Calendar, Location, Money, ShoppingCart, DocumentChecked,
  Van, ArrowRight, User, InfoFilled
} from '@element-plus/icons-vue';
import { getApproveProcessConfigNodeList, addApproveProcessConfigNode } from '@/api/collaborativeApproval/approvalManagement';
import { userListNoPage } from '@/api/system/user';
  import { ref, computed, onMounted } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import {
    Plus,
    ArrowLeft,
    Delete,
    Check,
    RefreshLeft,
    Setting,
    Suitcase,
    Calendar,
    Location,
    Money,
    ShoppingCart,
    DocumentChecked,
    Van,
    ArrowRight,
    User,
    InfoFilled,
  } from "@element-plus/icons-vue";
  import {
    getApproveProcessConfigNodeList,
    addApproveProcessConfigNode,
  } from "@/api/collaborativeApproval/approvalManagement";
  import { userListNoPage } from "@/api/system/user";
// å½“前选中的标签页
const activeTab = ref('1');
  // å½“前选中的标签页
  const activeTab = ref("2");
// å®¡æ‰¹ç±»åž‹é…ç½®æ•°ç»„
const approveTypes = [
  { value: '1', label: '公出管理', icon: 'Suitcase', color: '#409EFF' },
  { value: '2', label: '请假管理', icon: 'Calendar', color: '#67C23A' },
  { value: '3', label: '出差管理', icon: 'Location', color: '#E6A23C' },
  { value: '4', label: '报销管理', icon: 'Money', color: '#F56C6C' },
  { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
  { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
  { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
];
  // å®¡æ‰¹ç±»åž‹é…ç½®æ•°ç»„
  const approveTypes = [
    // { value: '1', label: '公出管理', icon: 'Suitcase', color: '#409EFF' },
    { value: "2", label: "请假管理", icon: "Calendar", color: "#67C23A" },
    { value: "3", label: "出差管理", icon: "Location", color: "#E6A23C" },
    // { value: '4', label: '报销管理', icon: 'Money', color: '#F56C6C' },
    // { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
    // { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
    // { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
  ];
// å®¡æ‰¹ç±»åž‹åç§°æ˜ å°„
const approveTypeNameMap = {
  1: '公出管理',
  2: '请假管理',
  3: '出差管理',
  4: '报销管理',
  5: '采购审批',
  6: '报价审批',
  7: '发货审批',
};
  // å®¡æ‰¹ç±»åž‹åç§°æ˜ å°„
  const approveTypeNameMap = {
    // 1: '公出管理',
    2: "请假管理",
    3: "出差管理",
    // 4: '报销管理',
    // 5: '采购审批',
    // 6: '报价审批',
    // 7: '发货审批',
  };
// å®¡æ‰¹ç±»åž‹å›¾æ ‡æ˜ å°„
const typeIconMap = {
  1: 'Suitcase',
  2: 'Calendar',
  3: 'Location',
  4: 'Money',
  5: 'ShoppingCart',
  6: 'DocumentChecked',
  7: 'Van',
};
  // å®¡æ‰¹ç±»åž‹å›¾æ ‡æ˜ å°„
  const typeIconMap = {
    1: "Suitcase",
    2: "Calendar",
    3: "Location",
    4: "Money",
    5: "ShoppingCart",
    6: "DocumentChecked",
    7: "Van",
  };
// å®¡æ‰¹ç±»åž‹é¢œè‰²æ˜ å°„
const typeColorMap = {
  1: '#409EFF',
  2: '#67C23A',
  3: '#E6A23C',
  4: '#F56C6C',
  5: '#909399',
  6: '#9B59B6',
  7: '#1ABC9C',
};
  // å®¡æ‰¹ç±»åž‹é¢œè‰²æ˜ å°„
  const typeColorMap = {
    1: "#409EFF",
    2: "#67C23A",
    3: "#E6A23C",
    4: "#F56C6C",
    5: "#909399",
    6: "#9B59B6",
    7: "#1ABC9C",
  };
// å¤´åƒé¢œè‰²æ± 
const avatarColors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#9B59B6', '#1ABC9C', '#FF6B6B', '#4ECDC4'];
  // å¤´åƒé¢œè‰²æ± 
  const avatarColors = [
    "#409EFF",
    "#67C23A",
    "#E6A23C",
    "#F56C6C",
    "#9B59B6",
    "#1ABC9C",
    "#FF6B6B",
    "#4ECDC4",
  ];
// å½“前审批类型名称
const currentApproveTypeName = computed(() => {
  return approveTypeNameMap[activeTab.value] || '未知类型';
});
  // å½“前审批类型名称
  const currentApproveTypeName = computed(() => {
    return approveTypeNameMap[activeTab.value] || "未知类型";
  });
// å½“前审批类型
const currentApproveType = computed(() => {
  return Number(activeTab.value);
});
  // å½“前审批类型
  const currentApproveType = computed(() => {
    return Number(activeTab.value);
  });
// èŽ·å–ç±»åž‹å›¾æ ‡
const getTypeIcon = (type) => typeIconMap[type] || 'Setting';
  // èŽ·å–ç±»åž‹å›¾æ ‡
  const getTypeIcon = type => typeIconMap[type] || "Setting";
// èŽ·å–ç±»åž‹é¢œè‰²
const getTypeColor = (type) => typeColorMap[type] || '#409EFF';
  // èŽ·å–ç±»åž‹é¢œè‰²
  const getTypeColor = type => typeColorMap[type] || "#409EFF";
// èŽ·å–å¤´åƒé¢œè‰²
const getAvatarColor = (name) => {
  if (!name) return '#C0C4CC';
  let hash = 0;
  for (let i = 0; i < name.length; i++) {
    hash = name.charCodeAt(i) + ((hash << 5) - hash);
  }
  return avatarColors[Math.abs(hash) % avatarColors.length];
};
  // èŽ·å–å¤´åƒé¢œè‰²
  const getAvatarColor = name => {
    if (!name) return "#C0C4CC";
    let hash = 0;
    for (let i = 0; i < name.length; i++) {
      hash = name.charCodeAt(i) + ((hash << 5) - hash);
    }
    return avatarColors[Math.abs(hash) % avatarColors.length];
  };
// èŽ·å–çº§åˆ«æ–‡æœ¬
const getLevelText = (index) => {
  const texts = ['第一级', '第二级', '第三级', '第四级', '第五级', '第六级', '第七级', '第八级'];
  return texts[index] || `第${index + 1}级`;
};
  // èŽ·å–çº§åˆ«æ–‡æœ¬
  const getLevelText = index => {
    const texts = [
      "第一级",
      "第二级",
      "第三级",
      "第四级",
      "第五级",
      "第六级",
      "第七级",
      "第八级",
    ];
    return texts[index] || `第${index + 1}级`;
  };
// å®¡æ‰¹äººåˆ—表(真实接口)
const userList = ref([]);
  // å®¡æ‰¹äººåˆ—表(真实接口)
  const userList = ref([]);
// å®¡æ‰¹äººåˆ—表
const approverList = ref([]);
  // å®¡æ‰¹äººåˆ—表
  const approverList = ref([]);
// åŽŸå§‹æ•°æ®ï¼Œç”¨äºŽé‡ç½®
const originalList = ref([]);
  // åŽŸå§‹æ•°æ®ï¼Œç”¨äºŽé‡ç½®
  const originalList = ref([]);
// åŠ è½½çŠ¶æ€
const loading = ref(false);
const saveLoading = ref(false);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  const saveLoading = ref(false);
// æ ‡ç­¾é¡µåˆ‡æ¢å¤„理
const handleTabChange = () => {
  loadData();
};
  // æ ‡ç­¾é¡µåˆ‡æ¢å¤„理
  const handleTabChange = () => {
    loadData();
  };
// åŠ è½½å®¡æ‰¹é…ç½®æ•°æ®ï¼ˆæ¨¡æ‹Ÿï¼‰
const loadData = async () => {
  loading.value = true;
  try {
    const res = await getApproveProcessConfigNodeList(currentApproveType.value);
    const source = Array.isArray(res?.data)
      ? res.data
      : Array.isArray(res?.rows)
  // åŠ è½½å®¡æ‰¹é…ç½®æ•°æ®ï¼ˆæ¨¡æ‹Ÿï¼‰
  const loadData = async () => {
    loading.value = true;
    try {
      const res = await getApproveProcessConfigNodeList(currentApproveType.value);
      const source = Array.isArray(res?.data)
        ? res.data
        : Array.isArray(res?.rows)
        ? res.rows
        : Array.isArray(res?.data?.records)
          ? res.data.records
          : [];
    const data = source.map((item, index) => ({
      ...item,
      sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1,
    }));
    approverList.value = data.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
    originalList.value = JSON.parse(JSON.stringify(approverList.value));
  } catch (error) {
    approverList.value = [];
    originalList.value = [];
    ElMessage.error('加载审批配置失败');
  } finally {
    loading.value = false;
  }
};
        ? res.data.records
        : [];
      const data = source.map((item, index) => ({
        ...item,
        sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1,
      }));
      approverList.value = data.sort(
        (a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)
      );
      originalList.value = JSON.parse(JSON.stringify(approverList.value));
    } catch (error) {
      approverList.value = [];
      originalList.value = [];
      ElMessage.error("加载审批配置失败");
    } finally {
      loading.value = false;
    }
  };
const loadUserList = async () => {
  try {
    const res = await userListNoPage();
    userList.value = Array.isArray(res?.data) ? res.data : [];
  } catch (error) {
    userList.value = [];
    ElMessage.error('加载人员列表失败');
  }
};
  const loadUserList = async () => {
    try {
      const res = await userListNoPage();
      userList.value = Array.isArray(res?.data) ? res.data : [];
    } catch (error) {
      userList.value = [];
      ElMessage.error("加载人员列表失败");
    }
  };
// å®¡æ‰¹äººé€‰æ‹©å˜åŒ–
const handleApproverChange = (userId, row) => {
  const user = userList.value.find((u) => u.userId === userId);
  if (user) {
    row.approverName = user.nickName;
  }
};
  // å®¡æ‰¹äººé€‰æ‹©å˜åŒ–
  const handleApproverChange = (userId, row) => {
    const user = userList.value.find(u => u.userId === userId);
    if (user) {
      row.approverName = user.nickName;
    }
  };
// æ–°å¢žå®¡æ‰¹äºº
const handleAdd = () => {
  const newOrder = approverList.value.length + 1;
  approverList.value.push({
    id: null,
    approveType: currentApproveType.value,
    approverId: null,
    approverName: '',
    sortOrder: newOrder,
  });
};
// åˆ é™¤å®¡æ‰¹äºº
const handleDelete = (index) => {
  ElMessageBox.confirm('确定删除该审批人吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      approverList.value.splice(index, 1);
      approverList.value.forEach((item, idx) => {
        item.sortOrder = idx + 1;
      });
      ElMessage.success('删除成功');
    })
    .catch(() => {});
};
// å‰ç§»
const moveLeft = (index) => {
  if (index === 0) return;
  const temp = approverList.value[index];
  approverList.value[index] = approverList.value[index - 1];
  approverList.value[index - 1] = temp;
  approverList.value[index].sortOrder = index + 1;
  approverList.value[index - 1].sortOrder = index;
};
// åŽç§»
const moveRight = (index) => {
  if (index === approverList.value.length - 1) return;
  const temp = approverList.value[index];
  approverList.value[index] = approverList.value[index + 1];
  approverList.value[index + 1] = temp;
  approverList.value[index].sortOrder = index + 1;
  approverList.value[index + 1].sortOrder = index + 2;
};
// ä¿å­˜é…ç½®
const handleSave = async () => {
  if (approverList.value.length === 0) {
    ElMessage.warning('请至少配置一个审批人');
    return;
  }
  const hasEmptyApprover = approverList.value.some((item) => !item.approverId);
  if (hasEmptyApprover) {
    ElMessage.warning('请选择所有审批人');
    return;
  }
  const approverIds = approverList.value.map((item) => item.approverId);
  const uniqueIds = [...new Set(approverIds)];
  if (uniqueIds.length !== approverIds.length) {
    ElMessage.warning('审批人不能重复');
    return;
  }
  saveLoading.value = true;
  try {
    const payload = approverList.value.map((item, index) => ({
  // æ–°å¢žå®¡æ‰¹äºº
  const handleAdd = () => {
    const newOrder = approverList.value.length + 1;
    approverList.value.push({
      id: null,
      approveType: currentApproveType.value,
      nodeOrder: index + 1,
      approverId: item.approverId,
      approverName: item.approverName,
    }));
    await addApproveProcessConfigNode(payload);
    ElMessage.success('保存成功');
    await loadData();
  } catch (error) {
    ElMessage.error('保存失败');
  } finally {
    saveLoading.value = false;
  }
};
      approverId: null,
      approverName: "",
      sortOrder: newOrder,
    });
  };
// é‡ç½®
const handleReset = () => {
  if (originalList.value.length === 0) {
    approverList.value = [];
    return;
  }
  ElMessageBox.confirm('确定要重置当前配置吗?未保存的更改将丢失。', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      approverList.value = JSON.parse(JSON.stringify(originalList.value));
      ElMessage.success('已重置');
  // åˆ é™¤å®¡æ‰¹äºº
  const handleDelete = index => {
    ElMessageBox.confirm("确定删除该审批人吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
    .catch(() => {});
};
      .then(() => {
        approverList.value.splice(index, 1);
        approverList.value.forEach((item, idx) => {
          item.sortOrder = idx + 1;
        });
        ElMessage.success("删除成功");
      })
      .catch(() => {});
  };
onMounted(async () => {
  await loadUserList();
  await loadData();
});
  // å‰ç§»
  const moveLeft = index => {
    if (index === 0) return;
    const temp = approverList.value[index];
    approverList.value[index] = approverList.value[index - 1];
    approverList.value[index - 1] = temp;
    approverList.value[index].sortOrder = index + 1;
    approverList.value[index - 1].sortOrder = index;
  };
  // åŽç§»
  const moveRight = index => {
    if (index === approverList.value.length - 1) return;
    const temp = approverList.value[index];
    approverList.value[index] = approverList.value[index + 1];
    approverList.value[index + 1] = temp;
    approverList.value[index].sortOrder = index + 1;
    approverList.value[index + 1].sortOrder = index + 2;
  };
  // ä¿å­˜é…ç½®
  const handleSave = async () => {
    if (approverList.value.length === 0) {
      ElMessage.warning("请至少配置一个审批人");
      return;
    }
    const hasEmptyApprover = approverList.value.some(item => !item.approverId);
    if (hasEmptyApprover) {
      ElMessage.warning("请选择所有审批人");
      return;
    }
    const approverIds = approverList.value.map(item => item.approverId);
    const uniqueIds = [...new Set(approverIds)];
    if (uniqueIds.length !== approverIds.length) {
      ElMessage.warning("审批人不能重复");
      return;
    }
    saveLoading.value = true;
    try {
      const payload = approverList.value.map((item, index) => ({
        approveType: currentApproveType.value,
        nodeOrder: index + 1,
        approverId: item.approverId,
        approverName: item.approverName,
      }));
      await addApproveProcessConfigNode(payload);
      ElMessage.success("保存成功");
      await loadData();
    } catch (error) {
      ElMessage.error("保存失败");
    } finally {
      saveLoading.value = false;
    }
  };
  // é‡ç½®
  const handleReset = () => {
    if (originalList.value.length === 0) {
      approverList.value = [];
      return;
    }
    ElMessageBox.confirm("确定要重置当前配置吗?未保存的更改将丢失。", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        approverList.value = JSON.parse(JSON.stringify(originalList.value));
        ElMessage.success("已重置");
      })
      .catch(() => {});
  };
  onMounted(async () => {
    await loadUserList();
    await loadData();
  });
</script>
<style scoped>
.page-header {
  margin-bottom: 20px;
}
  .page-header {
    margin-bottom: 20px;
  }
.header-title {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 20px;
  font-weight: 600;
  color: var(--el-text-color-primary, #303133);
  margin-bottom: 6px;
}
  .header-title {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 20px;
    font-weight: 600;
    color: var(--el-text-color-primary, #303133);
    margin-bottom: 6px;
  }
.title-icon {
  font-size: 24px;
  color: var(--el-color-primary, #409EFF);
}
  .title-icon {
    font-size: 24px;
    color: var(--el-color-primary, #409eff);
  }
.header-desc {
  font-size: 13px;
  color: var(--el-text-color-secondary, #909399);
  margin-left: 34px;
}
  .header-desc {
    font-size: 13px;
    color: var(--el-text-color-secondary, #909399);
    margin-left: 34px;
  }
/* å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 */
.type-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 16px;
  padding: 4px;
  background: var(--el-fill-color-light, #f5f7fa);
  border-radius: 8px;
  overflow-x: auto;
}
.type-tab {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s ease;
  white-space: nowrap;
  font-size: 13px;
  color: var(--el-text-color-regular, #606266);
}
.type-tab:hover {
  background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1));
  color: var(--el-color-primary, #409EFF);
}
.type-tab.active {
  background: var(--el-bg-color, #fff);
  color: var(--el-color-primary, #409EFF);
  font-weight: 600;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.tab-name {
  font-size: 13px;
}
.tab-count {
  min-width: 16px;
  height: 16px;
  padding: 0 5px;
  background: var(--el-color-success, #67C23A);
  color: #fff;
  border-radius: 8px;
  font-size: 11px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}
.config-card {
  margin-bottom: 16px;
  border-radius: 12px;
}
:deep(.el-card__header) {
  padding: 16px 20px;
  border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.header-left {
  display: flex;
  align-items: center;
  gap: 14px;
}
.type-icon {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-info {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.type-name {
  font-size: 16px;
  font-weight: 600;
  color: var(--el-text-color-primary, #303133);
}
.header-actions {
  display: flex;
  gap: 10px;
}
.flow-wrapper {
  overflow-x: auto;
  padding: 8px 4px;
}
.flow-container {
  display: flex;
  align-items: center;
  gap: 0;
  min-width: min-content;
}
.flow-item {
  display: flex;
  align-items: center;
}
.node-card {
  width: 200px;
  background: var(--el-bg-color, #fff);
  border: 2px solid var(--el-border-color, #e4e7ed);
  border-radius: 12px;
  padding: 16px;
  position: relative;
  transition: all 0.3s ease;
  flex-shrink: 0;
}
.node-card:hover {
  border-color: var(--el-color-primary, #409EFF);
  box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15);
  transform: translateY(-2px);
}
.node-card.empty {
  border-style: dashed;
  border-color: var(--el-border-color, #c0c4cc);
  background: var(--el-fill-color-light, #fafbfc);
}
.node-card.empty:hover {
  border-color: var(--el-color-primary, #409EFF);
  background: var(--el-fill-color-light, #f5f7fa);
}
.node-badge {
  position: absolute;
  top: -10px;
  left: 16px;
  width: 24px;
  height: 24px;
  background: var(--el-color-primary, #409EFF);
  color: #fff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 700;
  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
}
.node-avatar-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 12px;
  margin-top: 4px;
}
.node-avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--el-fill-color, #f0f2f5);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 8px;
  color: var(--el-text-color-placeholder, #c0c4cc);
  transition: all 0.3s ease;
}
.node-avatar.has-user {
  color: #fff;
  font-size: 22px;
  font-weight: 600;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.node-level {
  font-size: 12px;
  color: var(--el-text-color-secondary, #909399);
  font-weight: 500;
}
.node-select-section {
  margin-bottom: 12px;
}
.node-select-section :deep(.el-select) {
  width: 100%;
}
.node-actions {
  display: flex;
  justify-content: center;
  gap: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--el-border-color-light, #ebeef5);
}
.action-btn {
  transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
  transform: scale(1.1);
}
.arrow-connector {
  display: flex;
  align-items: center;
  width: 50px;
  position: relative;
}
.arrow-line {
  flex: 1;
  height: 2px;
  background: var(--el-border-color, #c0c4cc);
}
.arrow-icon {
  color: var(--el-text-color-placeholder, #c0c4cc);
  font-size: 14px;
  margin-left: -2px;
}
/* æ–°å¢žèŠ‚ç‚¹æ ·å¼ */
.add-node-item {
  display: flex;
  align-items: center;
}
.add-node-card {
  width: 140px;
  height: 200px;
  border: 2px dashed var(--el-border-color, #c0c4cc);
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  cursor: pointer;
  transition: all 0.3s ease;
  background: var(--el-fill-color-light, #fafbfc);
  flex-shrink: 0;
  margin-left: 0;
}
.add-node-card:hover {
  border-color: var(--el-color-primary, #409EFF);
  background: var(--el-color-primary-light-9, #f0f7ff);
  transform: translateY(-2px);
  box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15);
}
.add-icon-wrapper {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--el-color-primary, #409EFF);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
  transition: all 0.3s ease;
}
.add-node-card:hover .add-icon-wrapper {
  transform: scale(1.1);
  box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
}
.add-text {
  font-size: 14px;
  color: var(--el-text-color-regular, #606266);
  font-weight: 500;
}
.add-node-card:hover .add-text {
  color: var(--el-color-primary, #409EFF);
}
/* ç©ºçŠ¶æ€ */
.empty-state {
  padding: 50px 20px;
}
.empty-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
.empty-icon-wrapper {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background: var(--el-fill-color-light, #f5f7fa);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 8px;
}
.empty-text {
  font-size: 16px;
  font-weight: 600;
  color: var(--el-text-color-regular, #606266);
}
.empty-subtext {
  font-size: 13px;
  color: var(--el-text-color-secondary, #909399);
}
.empty-add-btn {
  margin-top: 8px;
  padding: 12px 28px;
}
/* åº•部提示 */
.bottom-tips {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 20px;
  background: var(--el-fill-color-light, #f5f7fa);
  border-radius: 8px;
  color: var(--el-text-color-regular, #606266);
  font-size: 13px;
}
.bottom-tips .el-icon {
  color: var(--el-color-primary, #409EFF);
  font-size: 16px;
}
@media (max-width: 768px) {
  /* å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 */
  .type-tabs {
    padding: 3px;
    display: flex;
    gap: 4px;
    margin-bottom: 16px;
    padding: 4px;
    background: var(--el-fill-color-light, #f5f7fa);
    border-radius: 8px;
    overflow-x: auto;
  }
  .type-tab {
    padding: 6px 10px;
    font-size: 12px;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 8px 14px;
    border-radius: 6px;
    cursor: pointer;
    transition: all 0.2s ease;
    white-space: nowrap;
    font-size: 13px;
    color: var(--el-text-color-regular, #606266);
  }
  .type-tab:hover {
    background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1));
    color: var(--el-color-primary, #409eff);
  }
  .type-tab.active {
    background: var(--el-bg-color, #fff);
    color: var(--el-color-primary, #409eff);
    font-weight: 600;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .tab-name {
    font-size: 12px;
    font-size: 13px;
  }
  .tab-count {
    min-width: 16px;
    height: 16px;
    padding: 0 5px;
    background: var(--el-color-success, #67c23a);
    color: #fff;
    border-radius: 8px;
    font-size: 11px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .config-card {
    margin-bottom: 16px;
    border-radius: 12px;
  }
  :deep(.el-card__header) {
    padding: 16px 20px;
    border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
  }
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .header-left {
    display: flex;
    align-items: center;
    gap: 14px;
  }
  .type-icon {
    width: 44px;
    height: 44px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }
  .header-info {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .type-name {
    font-size: 16px;
    font-weight: 600;
    color: var(--el-text-color-primary, #303133);
  }
  .header-actions {
    display: flex;
    gap: 10px;
  }
  .flow-wrapper {
    overflow-x: auto;
    padding: 8px 4px;
  }
  .flow-container {
    flex-wrap: wrap;
    display: flex;
    align-items: center;
    gap: 0;
    min-width: min-content;
  }
  .flow-item {
    display: flex;
    align-items: center;
  }
  .node-card {
    width: 200px;
    background: var(--el-bg-color, #fff);
    border: 2px solid var(--el-border-color, #e4e7ed);
    border-radius: 12px;
    padding: 16px;
    position: relative;
    transition: all 0.3s ease;
    flex-shrink: 0;
  }
  .node-card:hover {
    border-color: var(--el-color-primary, #409eff);
    box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15);
    transform: translateY(-2px);
  }
  .node-card.empty {
    border-style: dashed;
    border-color: var(--el-border-color, #c0c4cc);
    background: var(--el-fill-color-light, #fafbfc);
  }
  .node-card.empty:hover {
    border-color: var(--el-color-primary, #409eff);
    background: var(--el-fill-color-light, #f5f7fa);
  }
  .node-badge {
    position: absolute;
    top: -10px;
    left: 16px;
    width: 24px;
    height: 24px;
    background: var(--el-color-primary, #409eff);
    color: #fff;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 700;
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
  }
  .node-avatar-section {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-bottom: 12px;
    margin-top: 4px;
  }
  .node-avatar {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--el-fill-color, #f0f2f5);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 8px;
    color: var(--el-text-color-placeholder, #c0c4cc);
    transition: all 0.3s ease;
  }
  .node-avatar.has-user {
    color: #fff;
    font-size: 22px;
    font-weight: 600;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }
  .node-level {
    font-size: 12px;
    color: var(--el-text-color-secondary, #909399);
    font-weight: 500;
  }
  .node-select-section {
    margin-bottom: 12px;
  }
  .node-select-section :deep(.el-select) {
    width: 100%;
  }
  .node-actions {
    display: flex;
    justify-content: center;
    gap: 8px;
    padding-top: 12px;
    border-top: 1px solid var(--el-border-color-light, #ebeef5);
  }
  .action-btn {
    transition: all 0.2s;
  }
  .action-btn:hover:not(:disabled) {
    transform: scale(1.1);
  }
  .arrow-connector {
    width: 100%;
    height: 30px;
    flex-direction: row;
    justify-content: center;
    display: flex;
    align-items: center;
    width: 50px;
    position: relative;
  }
  .arrow-line {
    width: 2px;
    height: 30px;
    flex: 1;
    height: 2px;
    background: var(--el-border-color, #c0c4cc);
  }
  .arrow-icon {
    right: auto;
    top: auto;
    bottom: -5px;
    transform: rotate(90deg);
    color: var(--el-text-color-placeholder, #c0c4cc);
    font-size: 14px;
    margin-left: -2px;
  }
  /* æ–°å¢žèŠ‚ç‚¹æ ·å¼ */
  .add-node-item {
    width: 100%;
    justify-content: center;
    margin-top: 10px;
    display: flex;
    align-items: center;
  }
  .add-node-item .arrow-connector {
    display: none;
  .add-node-card {
    width: 140px;
    height: 200px;
    border: 2px dashed var(--el-border-color, #c0c4cc);
    border-radius: 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    cursor: pointer;
    transition: all 0.3s ease;
    background: var(--el-fill-color-light, #fafbfc);
    flex-shrink: 0;
    margin-left: 0;
  }
}
  .add-node-card:hover {
    border-color: var(--el-color-primary, #409eff);
    background: var(--el-color-primary-light-9, #f0f7ff);
    transform: translateY(-2px);
    box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15);
  }
  .add-icon-wrapper {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: var(--el-color-primary, #409eff);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
    transition: all 0.3s ease;
  }
  .add-node-card:hover .add-icon-wrapper {
    transform: scale(1.1);
    box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
  }
  .add-text {
    font-size: 14px;
    color: var(--el-text-color-regular, #606266);
    font-weight: 500;
  }
  .add-node-card:hover .add-text {
    color: var(--el-color-primary, #409eff);
  }
  /* ç©ºçŠ¶æ€ */
  .empty-state {
    padding: 50px 20px;
  }
  .empty-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
  }
  .empty-icon-wrapper {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    background: var(--el-fill-color-light, #f5f7fa);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 8px;
  }
  .empty-text {
    font-size: 16px;
    font-weight: 600;
    color: var(--el-text-color-regular, #606266);
  }
  .empty-subtext {
    font-size: 13px;
    color: var(--el-text-color-secondary, #909399);
  }
  .empty-add-btn {
    margin-top: 8px;
    padding: 12px 28px;
  }
  /* åº•部提示 */
  .bottom-tips {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 20px;
    background: var(--el-fill-color-light, #f5f7fa);
    border-radius: 8px;
    color: var(--el-text-color-regular, #606266);
    font-size: 13px;
  }
  .bottom-tips .el-icon {
    color: var(--el-color-primary, #409eff);
    font-size: 16px;
  }
  @media (max-width: 768px) {
    .type-tabs {
      padding: 3px;
    }
    .type-tab {
      padding: 6px 10px;
      font-size: 12px;
    }
    .tab-name {
      font-size: 12px;
    }
    .flow-container {
      flex-wrap: wrap;
      justify-content: center;
    }
    .arrow-connector {
      width: 100%;
      height: 30px;
      flex-direction: row;
      justify-content: center;
    }
    .arrow-line {
      width: 2px;
      height: 30px;
    }
    .arrow-icon {
      right: auto;
      top: auto;
      bottom: -5px;
      transform: rotate(90deg);
    }
    .add-node-item {
      width: 100%;
      justify-content: center;
      margin-top: 10px;
    }
    .add-node-item .arrow-connector {
      display: none;
    }
  }
</style>
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -2,130 +2,154 @@
  <div class="app-container">
    <!-- å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 -->
    <div class="type-tabs">
      <div
        v-for="type in approveTypes"
        :key="type.value"
        class="type-tab"
        :class="{ active: activeTab === type.value }"
        @click="activeTab = type.value; handleTabChange()"
      >
        <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }">
      <div v-for="type in approveTypes"
           :key="type.value"
           class="type-tab"
           :class="{ active: activeTab === type.value }"
           @click="activeTab = type.value; handleTabChange()">
        <el-icon :size="14"
                 :style="{ color: activeTab === type.value ? type.color : '#909399' }">
          <component :is="type.icon" />
        </el-icon>
        <span class="tab-name">{{ type.label }}</span>
      </div>
    </div>
    <!-- æœç´¢å’Œæ“ä½œåŒºåŸŸ -->
    <el-card class="search-card" shadow="never">
    <el-card class="search-card"
             shadow="never">
      <div class="search-content">
        <div class="search-filters">
          <div class="filter-item">
            <span class="filter-label">流程编号</span>
            <el-input
              v-model="searchForm.approveId"
              placeholder="请输入流程编号"
              clearable
              :prefix-icon="Search"
              @keyup.enter="handleQuery"
              class="search-input"
            />
            <el-input v-model="searchForm.approveId"
                      placeholder="请输入流程编号"
                      clearable
                      :prefix-icon="Search"
                      @keyup.enter="handleQuery"
                      class="search-input" />
          </div>
          <div class="filter-item">
            <span class="filter-label">审批状态</span>
            <el-select
              v-model="searchForm.approveStatus"
              clearable
              @change="handleQuery"
              placeholder="请选择状态"
              class="search-select"
            >
              <el-option label="待审核" :value="0">
                <el-tag size="small" type="warning">待审核</el-tag>
            <el-select v-model="searchForm.approveStatus"
                       clearable
                       @change="handleQuery"
                       placeholder="请选择状态"
                       class="search-select">
              <el-option label="待审核"
                         :value="0">
                <el-tag size="small"
                        type="warning">待审核</el-tag>
              </el-option>
              <el-option label="审核中" :value="1">
                <el-tag size="small" type="primary">审核中</el-tag>
              <el-option label="审核中"
                         :value="1">
                <el-tag size="small"
                        type="primary">审核中</el-tag>
              </el-option>
              <el-option label="审核完成" :value="2">
                <el-tag size="small" type="success">审核完成</el-tag>
              <el-option label="审核完成"
                         :value="2">
                <el-tag size="small"
                        type="success">审核完成</el-tag>
              </el-option>
              <el-option label="审核未通过" :value="3">
                <el-tag size="small" type="danger">审核未通过</el-tag>
              <el-option label="审核未通过"
                         :value="3">
                <el-tag size="small"
                        type="danger">审核未通过</el-tag>
              </el-option>
              <el-option label="已重新提交" :value="4">
                <el-tag size="small" type="info">已重新提交</el-tag>
              <el-option label="已重新提交"
                         :value="4">
                <el-tag size="small"
                        type="info">已重新提交</el-tag>
              </el-option>
            </el-select>
          </div>
          <el-button type="primary" @click="handleQuery" class="search-btn">
            <el-icon><Search /></el-icon>
          <el-button type="primary"
                     @click="handleQuery"
                     class="search-btn">
            <el-icon>
              <Search />
            </el-icon>
            æœç´¢
          </el-button>
          <el-button @click="resetQuery" class="reset-btn">
            <el-icon><RefreshRight /></el-icon>
          <el-button @click="resetQuery"
                     class="reset-btn">
            <el-icon>
              <RefreshRight />
            </el-icon>
            é‡ç½®
          </el-button>
        </div>
        <div class="search-actions">
          <el-button
            type="primary"
            @click="openForm('add')"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
            class="action-btn primary"
          >
            <el-icon><Plus /></el-icon>
          <el-button type="primary"
                     @click="openForm('add')"
                     v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
                     class="action-btn primary">
            <el-icon>
              <Plus />
            </el-icon>
            æ–°å¢ž
          </el-button>
          <el-button @click="handleOut" class="action-btn">
            <el-icon><Download /></el-icon>
          <el-button @click="handleOut"
                     class="action-btn">
            <el-icon>
              <Download />
            </el-icon>
            å¯¼å‡º
          </el-button>
          <el-button
            type="danger"
            plain
            @click="handleDelete"
            v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
            class="action-btn danger"
          >
            <el-icon><Delete /></el-icon>
          <el-button type="danger"
                     plain
                     @click="handleDelete"
                     v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
                     class="action-btn danger">
            <el-icon>
              <Delete />
            </el-icon>
            åˆ é™¤
          </el-button>
        </div>
      </div>
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card class="table-card" shadow="never" v-loading="tableLoading">
    <el-card class="table-card"
             shadow="never"
             v-loading="tableLoading">
      <template #header>
        <div class="table-header">
          <div class="table-title">
            <div class="type-tag" :style="{ backgroundColor: currentTypeInfo.color }">
              <el-icon color="#fff" :size="16"><component :is="currentTypeInfo.icon" /></el-icon>
            <div class="type-tag"
                 :style="{ backgroundColor: currentTypeInfo.color }">
              <el-icon color="#fff"
                       :size="16">
                <component :is="currentTypeInfo.icon" />
              </el-icon>
            </div>
            <span>{{ currentTypeInfo.label }}列表</span>
            <el-tag type="info" size="small" effect="plain" class="count-tag">
            <el-tag type="info"
                    size="small"
                    effect="plain"
                    class="count-tag">
              å…± {{ page.total }} æ¡
            </el-tag>
          </div>
        </div>
      </template>
      <PIMTable
        rowKey="id"
        :column="tableColumnCopy"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
        class="custom-table"
      ></PIMTable>
      <PIMTable rowKey="id"
                :column="tableColumnCopy"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
                :total="page.total"
                class="custom-table"></PIMTable>
    </el-card>
    <!-- å¼¹çª—组件 -->
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia>
    <info-form-dia ref="infoFormDia"
                   @close="handleQuery"
                   :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia"
                  @close="handleQuery"
                  :approveType="currentApproveType"></approval-dia>
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              record-type="approve_process"
@@ -134,362 +158,392 @@
</template>
<script setup>
import { Search, Plus, Delete, Download, RefreshRight, DocumentChecked } from "@element-plus/icons-vue";
import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance, defineAsyncComponent} from "vue";
import {ElMessageBox} from "element-plus";
import { useRoute } from 'vue-router';
import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
  import {
    Search,
    Plus,
    Delete,
    Download,
    RefreshRight,
    DocumentChecked,
  } from "@element-plus/icons-vue";
  import {
    onMounted,
    ref,
    computed,
    reactive,
    toRefs,
    nextTick,
    getCurrentInstance,
    defineAsyncComponent,
  } from "vue";
  import { ElMessageBox } from "element-plus";
  import { useRoute } from "vue-router";
  import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
  import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
  import {
    approveProcessDelete,
    approveProcessListPage,
  } from "@/api/collaborativeApproval/approvalProcess.js";
  import useUserStore from "@/store/modules/user";
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
const userStore = useUserStore();
const route = useRoute();
  const userStore = useUserStore();
  const route = useRoute();
// å½“前选中的标签页,默认为公出管理
const activeTab = ref('1');
  // å½“前选中的标签页,默认为请假管理
  const activeTab = ref("2");
// å„类型数量统计
const typeCounts = ref({});
  // å„类型数量统计
  const typeCounts = ref({});
// å®¡æ‰¹ç±»åž‹é…ç½®
const approveTypes = [
  { value: '1', label: '公出管理', icon: 'Suitcase', color: '#409EFF' },
  { value: '2', label: '请假管理', icon: 'Calendar', color: '#67C23A' },
  { value: '3', label: '出差管理', icon: 'Location', color: '#E6A23C' },
  { value: '4', label: '报销管理', icon: 'Money', color: '#F56C6C' },
  { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
  { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
  { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
];
  // å®¡æ‰¹ç±»åž‹é…ç½®
  const approveTypes = [
    // { value: '1', label: '公出管理', icon: 'Suitcase', color: '#409EFF' },
    { value: "2", label: "请假管理", icon: "Calendar", color: "#67C23A" },
    { value: "3", label: "出差管理", icon: "Location", color: "#E6A23C" },
    // { value: '4', label: '报销管理', icon: 'Money', color: '#F56C6C' },
    // { value: '5', label: '采购审批', icon: 'ShoppingCart', color: '#909399' },
    // { value: '6', label: '报价审批', icon: 'DocumentChecked', color: '#9B59B6' },
    // { value: '7', label: '发货审批', icon: 'Van', color: '#1ABC9C' },
  ];
// å½“前审批类型信息
const currentTypeInfo = computed(() => {
  return approveTypes.find(t => t.value === activeTab.value) || approveTypes[0];
});
  // å½“前审批类型信息
  const currentTypeInfo = computed(() => {
    return approveTypes.find(t => t.value === activeTab.value) || approveTypes[0];
  });
// èŽ·å–ç±»åž‹æ•°é‡
const getTypeCount = (value) => {
  return typeCounts.value[value] || 0;
};
  // èŽ·å–ç±»åž‹æ•°é‡
  const getTypeCount = value => {
    return typeCounts.value[value] || 0;
  };
// å½“前审批类型,根据选中的标签页计算
const currentApproveType = computed(() => {
  return Number(activeTab.value);
});
  // å½“前审批类型,根据选中的标签页计算
  const currentApproveType = computed(() => {
    return Number(activeTab.value);
  });
// æ ‡ç­¾é¡µåˆ‡æ¢å¤„理
const handleTabChange = () => {
  // åˆ‡æ¢æ ‡ç­¾é¡µæ—¶é‡ç½®æœç´¢æ¡ä»¶å’Œåˆ†é¡µï¼Œå¹¶é‡æ–°åŠ è½½æ•°æ®
  searchForm.value.approveId = '';
  searchForm.value.approveStatus = '';
  page.current = 1;
  getList();
};
  // æ ‡ç­¾é¡µåˆ‡æ¢å¤„理
  const handleTabChange = () => {
    // åˆ‡æ¢æ ‡ç­¾é¡µæ—¶é‡ç½®æœç´¢æ¡ä»¶å’Œåˆ†é¡µï¼Œå¹¶é‡æ–°åŠ è½½æ•°æ®
    searchForm.value.approveId = "";
    searchForm.value.approveStatus = "";
    page.current = 1;
    getList();
  };
  const data = reactive({
    searchForm: {
      approveId: "",
      approveStatus: "",
    },
  });
  const { searchForm } = toRefs(data);
const data = reactive({
  searchForm: {
    approveId: "",
    approveStatus: "",
  },
});
const { searchForm } = toRefs(data);
  // é‡ç½®æœç´¢
  const resetQuery = () => {
    searchForm.value.approveId = "";
    searchForm.value.approveStatus = "";
    handleQuery();
  };
// é‡ç½®æœç´¢
const resetQuery = () => {
  searchForm.value.approveId = '';
  searchForm.value.approveStatus = '';
  handleQuery();
};
  // åŠ¨æ€è¡¨æ ¼åˆ—é…ç½®ï¼Œæ ¹æ®å®¡æ‰¹ç±»åž‹ç”Ÿæˆåˆ—
  const tableColumnCopy = computed(() => {
    const isLeaveType = currentApproveType.value === 2; // è¯·å‡ç®¡ç†
    const isBusinessTripType = currentApproveType.value === 3; // å‡ºå·®ç®¡ç†
    const isReimburseType = currentApproveType.value === 4; // æŠ¥é”€ç®¡ç†
    const isQuotationType = currentApproveType.value === 6; // æŠ¥ä»·å®¡æ‰¹
    const isPurchaseType = currentApproveType.value === 5; // é‡‡è´­å®¡æ‰¹
// åŠ¨æ€è¡¨æ ¼åˆ—é…ç½®ï¼Œæ ¹æ®å®¡æ‰¹ç±»åž‹ç”Ÿæˆåˆ—
const tableColumnCopy = computed(() => {
  const isLeaveType = currentApproveType.value === 2; // è¯·å‡ç®¡ç†
  const isBusinessTripType = currentApproveType.value === 3; // å‡ºå·®ç®¡ç†
  const isReimburseType = currentApproveType.value === 4; // æŠ¥é”€ç®¡ç†
  const isQuotationType = currentApproveType.value === 6; // æŠ¥ä»·å®¡æ‰¹
  const isPurchaseType = currentApproveType.value === 5; // é‡‡è´­å®¡æ‰¹
  // åŸºç¡€åˆ—配置
  const baseColumns = [
    {
      label: "审批状态",
      prop: "approveStatus",
      dataType: "tag",
      width: 100,
      formatData: (params) => {
        if (params == 0) {
          return "待审核";
        } else if (params == 1) {
          return "审核中";
        } else if (params == 2) {
          return "审核完成";
        } else if (params == 4) {
          return "已重新提交";
        } else {
          return '不通过';
        }
    // åŸºç¡€åˆ—配置
    const baseColumns = [
      {
        label: "审批状态",
        prop: "approveStatus",
        dataType: "tag",
        width: 100,
        formatData: params => {
          if (params == 0) {
            return "待审核";
          } else if (params == 1) {
            return "审核中";
          } else if (params == 2) {
            return "审核完成";
          } else if (params == 4) {
            return "已重新提交";
          } else {
            return "不通过";
          }
        },
        formatType: params => {
          if (params == 0) {
            return "warning";
          } else if (params == 1) {
            return "primary";
          } else if (params == 2) {
            return "success";
          } else if (params == 4) {
            return "info";
          } else {
            return "danger";
          }
        },
      },
      formatType: (params) => {
        if (params == 0) {
          return "warning";
        } else if (params == 1) {
          return "primary";
        } else if (params == 2) {
          return "success";
        } else if (params == 4) {
          return "info";
        } else {
          return 'danger';
        }
      {
        label: "流程编号",
        prop: "approveId",
        width: 170,
      },
    },
    {
      label: "流程编号",
      prop: "approveId",
      width: 170
    },
    {
      label: "申请部门",
      prop: "approveDeptName",
      width: 220
    },
    {
      label: isQuotationType ? "报价单号" : isPurchaseType ? "采购合同号" : "审批事由",
      prop: "approveReason",
    },
    {
      label: "申请人",
      prop: "approveUserName",
      width: 120
      {
        label: "申请部门",
        prop: "approveDeptName",
        width: 220,
      },
      {
        label: isQuotationType
          ? "报价单号"
          : isPurchaseType
          ? "采购合同号"
          : "审批事由",
        prop: "approveReason",
      },
      {
        label: "申请人",
        prop: "approveUserName",
        width: 120,
      },
    ];
    // é‡‘额列(仅报销管理显示)
    if (isReimburseType) {
      baseColumns.push({
        label: "金额(元)",
        prop: "price",
        width: 120,
      });
    }
  ];
  // é‡‘额列(仅报销管理显示)
  if (isReimburseType) {
    // è¯·å‡ç®¡ç†ï¼šå¼€å§‹æ—¥æœŸ / ç»“束日期
    if (isLeaveType) {
      baseColumns.push(
        { label: "开始日期", prop: "startDate", width: 120 },
        { label: "结束日期", prop: "endDate", width: 120 }
      );
    }
    // å‡ºå·®ç®¡ç†ï¼šå¼€å§‹æ—¶é—´ / ç»“束时间(不含秒)
    if (isBusinessTripType) {
      baseColumns.push(
        {
          label: "开始时间",
          prop: "startDateTime",
          width: 180,
          formatData: val => (val ? val.substring(0, 16) : ""),
        },
        {
          label: "结束时间",
          prop: "endDateTime",
          width: 180,
          formatData: val => (val ? val.substring(0, 16) : ""),
        }
      );
    }
    // å½“前审批人列
    baseColumns.push({
      label: "金额(元)",
      prop: "price",
      width: 120
      label: "当前审批人",
      prop: "approveUserCurrentName",
      width: 120,
    });
  }
  // è¯·å‡ç®¡ç†ï¼šå¼€å§‹æ—¥æœŸ / ç»“束日期
  if (isLeaveType) {
    baseColumns.push(
      { label: "开始日期", prop: "startDate", width: 120 },
      { label: "结束日期", prop: "endDate", width: 120 }
    );
  }
  // å‡ºå·®ç®¡ç†ï¼šå¼€å§‹æ—¶é—´ / ç»“束时间(不含秒)
  if (isBusinessTripType) {
    baseColumns.push(
    // ç”³è¯·æ—¶é—´ - æ‰€æœ‰ç±»åž‹éƒ½æ˜¾ç¤º
    baseColumns.push({
      label: "申请时间",
      prop: "approveTime",
      width: 180,
    });
    // å®¡æ‰¹æ—¶é—´ - æ‰€æœ‰ç±»åž‹éƒ½æ˜¾ç¤º
    baseColumns.push({
      label: "审批时间",
      prop: "approveOverTime",
      width: 180,
    });
    // æ“ä½œåˆ—
    const actionOperations = [
      {
        label: "开始时间",
        prop: "startDateTime",
        width: 180,
        formatData: (val) => val ? val.substring(0, 16) : ''
        name: "编辑",
        type: "text",
        clickFun: row => {
          openForm("edit", row);
        },
        disabled: row =>
          currentApproveType.value === 5 ||
          currentApproveType.value === 6 ||
          currentApproveType.value === 7 ||
          row.approveStatus == 2 ||
          row.approveStatus == 1 ||
          row.approveStatus == 4,
      },
      {
        label: "结束时间",
        prop: "endDateTime",
        width: 180,
        formatData: (val) => val ? val.substring(0, 16) : ''
      }
    );
  }
  // å½“前审批人列
  baseColumns.push({
    label: "当前审批人",
    prop: "approveUserCurrentName",
    width: 120
  });
        name: "审核",
        type: "text",
        clickFun: row => {
          openApprovalDia("approval", row);
        },
        disabled: row =>
          row.approveUserCurrentId == null ||
          row.approveStatus == 2 ||
          row.approveStatus == 3 ||
          row.approveStatus == 4 ||
          row.approveUserCurrentId !== userStore.id,
      },
      {
        name: "详情",
        type: "text",
        clickFun: row => {
          openApprovalDia("view", row);
        },
      },
    ];
  // ç”³è¯·æ—¶é—´ - æ‰€æœ‰ç±»åž‹éƒ½æ˜¾ç¤º
  baseColumns.push({
    label: "申请时间",
    prop: "approveTime",
    width: 180,
  });
    // æŠ¥ä»·å®¡æ‰¹ï¼ˆç±»åž‹ 6)不展示"附件"操作
    if (!isQuotationType) {
      actionOperations.push({
        name: "附件",
        type: "text",
        clickFun: row => {
          openFilesFormDia(row);
        },
      });
    }
  // å®¡æ‰¹æ—¶é—´ - æ‰€æœ‰ç±»åž‹éƒ½æ˜¾ç¤º
  baseColumns.push({
    label: "审批时间",
    prop: "approveOverTime",
    width: 180,
  });
  // æ“ä½œåˆ—
  const actionOperations = [
    {
      name: "编辑",
      type: "text",
      clickFun: (row) => {
        openForm("edit", row);
      },
      disabled: (row) =>
        currentApproveType.value === 5 ||
        currentApproveType.value === 6 ||
        currentApproveType.value === 7 ||
        row.approveStatus == 2 ||
        row.approveStatus == 1 ||
        row.approveStatus == 4
    },
    {
      name: "审核",
      type: "text",
      clickFun: (row) => {
        openApprovalDia("approval", row);
      },
      disabled: (row) =>
        row.approveUserCurrentId == null ||
        row.approveStatus == 2 ||
        row.approveStatus == 3 ||
        row.approveStatus == 4 ||
        row.approveUserCurrentId !== userStore.id
    },
    {
      name: "详情",
      type: "text",
      clickFun: (row) => {
        openApprovalDia("view", row);
      },
    },
  ];
  // æŠ¥ä»·å®¡æ‰¹ï¼ˆç±»åž‹ 6)不展示"附件"操作
  if (!isQuotationType) {
    actionOperations.push({
      name: "附件",
      type: "text",
      clickFun: (row) => {
        openFilesFormDia(row);
      },
    baseColumns.push({
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 230,
      operation: actionOperations,
    });
  }
  baseColumns.push({
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 230,
    operation: actionOperations,
    return baseColumns;
  });
  return baseColumns;
});
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
  total: 0
});
const infoFormDia = ref()
const approvalDia = ref()
const { proxy } = getCurrentInstance()
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const infoFormDia = ref();
  const approvalDia = ref();
  const { proxy } = getCurrentInstance();
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1;
  getList();
};
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
// æ‰“开附件弹窗
const recordId =ref(0)
const fileDialogVisible = ref(false)
  // æ‰“开附件弹窗
  const recordId = ref(0);
  const fileDialogVisible = ref(false);
// æ‰“开附件弹框
const openFilesFormDia = async (row) => {
  recordId.value = row.id
  fileDialogVisible.value = true
}
  // æ‰“开附件弹框
  const openFilesFormDia = async row => {
    recordId.value = row.id;
    fileDialogVisible.value = true;
  };
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  approveProcessListPage({...page, ...searchForm.value, approveType: currentApproveType.value}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
    // æ›´æ–°å½“前类型数量
    typeCounts.value[activeTab.value] = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
// å¯¼å‡º
const handleOut = () => {
  const type = currentApproveType.value
  const urlMap = {
    0: "/approveProcess/exportZero",
    1: "/approveProcess/exportOne",
    2: "/approveProcess/exportTwo",
    3: "/approveProcess/exportThree",
    4: "/approveProcess/exportFour",
    5: "/approveProcess/exportFive",
    6: "/approveProcess/exportSix",
    7: "/approveProcess/exportSeven",
  }
  const url = urlMap[type] || urlMap[0]
  const nameMap = {
    0: "协同审批管理表",
    1: "公出管理审批表",
    2: "请假管理审批表",
    3: "出差管理审批表",
    4: "报销管理审批表",
    5: "采购申请审批表",
    6: "报价审批表",
    7: "发货审批表",
  }
  const fileName = nameMap[type] || nameMap[0]
  proxy.download(url, {}, `${fileName}.xlsx`)
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    approveProcessListPage({
      ...page,
      ...searchForm.value,
      approveType: currentApproveType.value,
    })
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
        // æ›´æ–°å½“前类型数量
        typeCounts.value[activeTab.value] = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
      });
  };
  // å¯¼å‡º
  const handleOut = () => {
    const type = currentApproveType.value;
    const urlMap = {
      0: "/approveProcess/exportZero",
      1: "/approveProcess/exportOne",
      2: "/approveProcess/exportTwo",
      3: "/approveProcess/exportThree",
      4: "/approveProcess/exportFour",
      5: "/approveProcess/exportFive",
      6: "/approveProcess/exportSix",
      7: "/approveProcess/exportSeven",
    };
    const url = urlMap[type] || urlMap[0];
    const nameMap = {
      0: "协同审批管理表",
      1: "公出管理审批表",
      2: "请假管理审批表",
      3: "出差管理审批表",
      4: "报销管理审批表",
      5: "采购申请审批表",
      6: "报价审批表",
      7: "发货审批表",
    };
    const fileName = nameMap[type] || nameMap[0];
    proxy.download(url, {}, `${fileName}.xlsx`);
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
// æ‰“开新增、编辑弹框
const openForm = (type, row) => {
  nextTick(() => {
    infoFormDia.value?.openDialog(type, row)
  })
};
// æ‰“开新增检验弹框
const openApprovalDia = (type, row) => {
  nextTick(() => {
    approvalDia.value?.openDialog(type, row)
  })
};
  // æ‰“开新增、编辑弹框
  const openForm = (type, row) => {
    nextTick(() => {
      infoFormDia.value?.openDialog(type, row);
    });
  };
  // æ‰“开新增检验弹框
  const openApprovalDia = (type, row) => {
    nextTick(() => {
      approvalDia.value?.openDialog(type, row);
    });
  };
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.approveId);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
  // åˆ é™¤
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map(item => item.approveId);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        approveProcessDelete(ids).then((res) => {
        approveProcessDelete(ids).then(res => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
@@ -497,278 +551,278 @@
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
onMounted(() => {
  // æ ¹æ®URL参数设置标签页和查询条件
  const approveType = route.query.approveType;
  const approveId = route.query.approveId;
  if (approveType) {
    // è®¾ç½®æ ‡ç­¾é¡µï¼ˆapproveType å¯¹åº” activeTab çš„ name)
    activeTab.value = String(approveType);
  }
  if (approveId) {
    // è®¾ç½®æµç¨‹ç¼–号查询条件
    searchForm.value.approveId = String(approveId);
  }
  // æŸ¥è¯¢åˆ—表
  getList();
});
  };
  onMounted(() => {
    // æ ¹æ®URL参数设置标签页和查询条件
    const approveType = route.query.approveType;
    const approveId = route.query.approveId;
    if (approveType) {
      // è®¾ç½®æ ‡ç­¾é¡µï¼ˆapproveType å¯¹åº” activeTab çš„ name)
      activeTab.value = String(approveType);
    }
    if (approveId) {
      // è®¾ç½®æµç¨‹ç¼–号查询条件
      searchForm.value.approveId = String(approveId);
    }
    // æŸ¥è¯¢åˆ—表
    getList();
  });
</script>
<style scoped>
.page-header {
  margin-bottom: 20px;
}
  .page-header {
    margin-bottom: 20px;
  }
.header-title {
  display: flex;
  align-items: center;
  gap: 12px;
}
  .header-title {
    display: flex;
    align-items: center;
    gap: 12px;
  }
.title-icon {
  font-size: 28px;
  color: var(--el-color-primary, #409EFF);
}
  .title-icon {
    font-size: 28px;
    color: var(--el-color-primary, #409eff);
  }
.header-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.main-title {
  font-size: 20px;
  font-weight: 600;
  color: var(--el-text-color-primary, #303133);
}
.sub-title {
  font-size: 13px;
  color: var(--el-text-color-secondary, #909399);
}
/* å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 */
.type-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 16px;
  padding: 4px;
  background: var(--el-fill-color-light, #f5f7fa);
  border-radius: 8px;
  overflow-x: auto;
}
.type-tab {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s ease;
  white-space: nowrap;
  font-size: 13px;
  color: var(--el-text-color-regular, #606266);
}
.type-tab:hover {
  background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1));
  color: var(--el-color-primary, #409EFF);
}
.type-tab.active {
  background: var(--el-bg-color, #fff);
  color: var(--el-color-primary, #409EFF);
  font-weight: 600;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.tab-name {
  font-size: 13px;
}
.tab-count {
  min-width: 16px;
  height: 16px;
  padding: 0 5px;
  background: var(--el-color-success, #67C23A);
  color: #fff;
  border-radius: 8px;
  font-size: 11px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* æœç´¢å¡ç‰‡ */
.search-card {
  margin-bottom: 16px;
  border-radius: 12px;
}
:deep(.el-card__body) {
  padding: 20px;
}
.search-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 16px;
}
.search-filters {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 8px;
}
.filter-label {
  font-size: 14px;
  color: var(--el-text-color-regular, #606266);
  font-weight: 500;
  white-space: nowrap;
}
.search-input,
.search-select {
  width: 200px;
}
.search-btn {
  display: flex;
  align-items: center;
  gap: 4px;
}
.reset-btn {
  display: flex;
  align-items: center;
  gap: 4px;
}
.search-actions {
  display: flex;
  gap: 10px;
}
.action-btn {
  display: flex;
  align-items: center;
  gap: 4px;
}
.action-btn.primary {
  background: var(--el-color-primary, #409EFF);
  border: none;
}
.action-btn.danger {
  transition: all 0.3s;
}
.action-btn.danger:hover {
  background: #f56c6c;
  color: #fff;
}
/* è¡¨æ ¼å¡ç‰‡ */
.table-card {
  border-radius: 12px;
}
:deep(.el-card__header) {
  padding: 16px 20px;
  border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
}
.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.table-title {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 16px;
  font-weight: 600;
  color: var(--el-text-color-primary, #303133);
}
.type-tag {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.count-tag {
  margin-left: 8px;
}
.custom-table {
  margin-top: 8px;
}
/* å“åº”式 */
@media (max-width: 1200px) {
  .search-content {
  .header-text {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
  }
  .search-filters {
    justify-content: flex-start;
  .main-title {
    font-size: 20px;
    font-weight: 600;
    color: var(--el-text-color-primary, #303133);
  }
  .search-actions {
    justify-content: flex-end;
  .sub-title {
    font-size: 13px;
    color: var(--el-text-color-secondary, #909399);
  }
}
@media (max-width: 768px) {
  /* å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 */
  .type-tabs {
    padding: 3px;
    display: flex;
    gap: 4px;
    margin-bottom: 16px;
    padding: 4px;
    background: var(--el-fill-color-light, #f5f7fa);
    border-radius: 8px;
    overflow-x: auto;
  }
  .type-tab {
    padding: 6px 10px;
    font-size: 12px;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 8px 14px;
    border-radius: 6px;
    cursor: pointer;
    transition: all 0.2s ease;
    white-space: nowrap;
    font-size: 13px;
    color: var(--el-text-color-regular, #606266);
  }
  .type-tab:hover {
    background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1));
    color: var(--el-color-primary, #409eff);
  }
  .type-tab.active {
    background: var(--el-bg-color, #fff);
    color: var(--el-color-primary, #409eff);
    font-weight: 600;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
  .tab-name {
    font-size: 12px;
    font-size: 13px;
  }
  .tab-count {
    min-width: 16px;
    height: 16px;
    padding: 0 5px;
    background: var(--el-color-success, #67c23a);
    color: #fff;
    border-radius: 8px;
    font-size: 11px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* æœç´¢å¡ç‰‡ */
  .search-card {
    margin-bottom: 16px;
    border-radius: 12px;
  }
  :deep(.el-card__body) {
    padding: 20px;
  }
  .search-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 16px;
  }
  .search-filters {
    flex-direction: column;
    align-items: stretch;
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
  }
  .filter-item {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .filter-label {
    font-size: 14px;
    color: var(--el-text-color-regular, #606266);
    font-weight: 500;
    white-space: nowrap;
  }
  .search-input,
  .search-select {
    width: 100%;
    width: 200px;
  }
}
  .search-btn {
    display: flex;
    align-items: center;
    gap: 4px;
  }
  .reset-btn {
    display: flex;
    align-items: center;
    gap: 4px;
  }
  .search-actions {
    display: flex;
    gap: 10px;
  }
  .action-btn {
    display: flex;
    align-items: center;
    gap: 4px;
  }
  .action-btn.primary {
    background: var(--el-color-primary, #409eff);
    border: none;
  }
  .action-btn.danger {
    transition: all 0.3s;
  }
  .action-btn.danger:hover {
    background: #f56c6c;
    color: #fff;
  }
  /* è¡¨æ ¼å¡ç‰‡ */
  .table-card {
    border-radius: 12px;
  }
  :deep(.el-card__header) {
    padding: 16px 20px;
    border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
  }
  .table-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .table-title {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 16px;
    font-weight: 600;
    color: var(--el-text-color-primary, #303133);
  }
  .type-tag {
    width: 32px;
    height: 32px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .count-tag {
    margin-left: 8px;
  }
  .custom-table {
    margin-top: 8px;
  }
  /* å“åº”式 */
  @media (max-width: 1200px) {
    .search-content {
      flex-direction: column;
      align-items: stretch;
    }
    .search-filters {
      justify-content: flex-start;
    }
    .search-actions {
      justify-content: flex-end;
    }
  }
  @media (max-width: 768px) {
    .type-tabs {
      padding: 3px;
    }
    .type-tab {
      padding: 6px 10px;
      font-size: 12px;
    }
    .tab-name {
      font-size: 12px;
    }
    .search-filters {
      flex-direction: column;
      align-items: stretch;
    }
    .filter-item {
      width: 100%;
    }
    .search-input,
    .search-select {
      width: 100%;
    }
  }
</style>
src/views/customerPortal/myReport/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,166 @@
<template>
  <div class="app-container">
    <el-alert title="请上传最近三个年度的财务报表,上传后可随时重新上传覆盖。提交后可在本页实时查看审批结果,被退回时请按退回原因修改后重新提交。"
              type="info"
              show-icon
              :closable="false" />
    <div class="year-cards">
      <el-card v-for="item in yearRows"
               :key="item.reportYear"
               shadow="never"
               class="year-card">
        <div class="year-header">
          <span class="year-title">{{ item.reportYear }} å¹´åº¦</span>
          <span v-if="item.id">
            <el-tag type="success"
                    size="small"
                    v-if="item.auditStatus === 1">已通过</el-tag>
            <el-tag type="danger"
                    size="small"
                    v-else-if="item.auditStatus === 2">已退回</el-tag>
            <el-tag type="warning"
                    size="small"
                    v-else>待审批</el-tag>
          </span>
          <el-tag v-else
                  type="info"
                  size="small">未上传</el-tag>
        </div>
        <el-alert v-if="item.auditStatus === 2 && item.auditRemark"
                  :title="'退回原因:' + item.auditRemark"
                  type="error"
                  show-icon
                  :closable="false"
                  class="reject-tip" />
        <div class="year-body">
          <div class="year-row">
            <span class="year-label">报表名称</span>
            <el-input v-model="item.reportName"
                      :placeholder="'例如:' + item.reportYear + '年度财务报表'"
                      clearable />
          </div>
          <div class="year-row">
            <span class="year-label">报表附件</span>
            <FileUpload v-model="item.reportFiles"
                        :limit="5"
                        :file-size="20"
                        :file-type="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png', 'zip', 'rar']" />
          </div>
          <div class="year-footer">
            <span v-if="item.updateTime"
                  class="year-tip">最近上传:{{ item.updateTime }}</span>
            <el-button type="primary"
                       :loading="savingYear === item.reportYear"
                       @click="save(item)">保存上传</el-button>
          </div>
        </div>
      </el-card>
    </div>
  </div>
</template>
<script setup>
  import { onMounted, ref, getCurrentInstance } from "vue";
  import FileUpload from "@/components/FileUpload/index.vue";
  import { listMyFinancialReport, saveCustomerFinancialReport } from "@/api/basicData/customerFinancialReport";
  const { proxy } = getCurrentInstance();
  const nowYear = new Date().getFullYear();
  const yearRows = ref([]);
  const savingYear = ref(0);
  // æœ€è¿‘三年,未上传的年度留空待上传
  const buildYears = rows => {
    const map = new Map(rows.map(item => [item.reportYear, item]));
    return [nowYear, nowYear - 1, nowYear - 2].map(year => {
      const row = map.get(year);
      return row
        ? { ...row }
        : { reportYear: year, reportName: "", reportFiles: "" };
    });
  };
  const load = () => {
    listMyFinancialReport().then(res => {
      yearRows.value = buildYears(res.data || []);
    });
  };
  const save = item => {
    if (!item.reportFiles) {
      proxy.$modal.msgWarning("请上传财务报表附件");
      return;
    }
    savingYear.value = item.reportYear;
    saveCustomerFinancialReport({
      reportYear: item.reportYear,
      reportName: item.reportName,
      reportFiles: item.reportFiles,
    }).then(() => {
      proxy.$modal.msgSuccess("上传成功");
      load();
    }).finally(() => {
      savingYear.value = 0;
    });
  };
  onMounted(load);
</script>
<style scoped>
  .year-cards {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    margin-top: 16px;
  }
  .year-card {
    flex: 1 1 320px;
    min-width: 320px;
  }
  .reject-tip {
    margin-bottom: 12px;
  }
  .year-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
  }
  .year-title {
    font-size: 16px;
    font-weight: 600;
  }
  .year-row {
    display: flex;
    align-items: flex-start;
    margin-bottom: 12px;
  }
  .year-label {
    width: 80px;
    flex: 0 0 80px;
    color: #606266;
    line-height: 32px;
  }
  .year-row > :not(.year-label) {
    flex: 1;
  }
  .year-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .year-tip {
    color: #909399;
    font-size: 12px;
  }
</style>
src/views/customerPortal/mySubmission/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
<template>
  <div class="app-container">
    <el-alert title="请按年度提交最近三年的财务报告;已发布的申报结果可直接查看和下载。" type="info" show-icon :closable="false" />
    <el-form :inline="true" :model="query" class="query-form">
      <el-form-item label="年度"><el-select v-model="query.reportYear" clearable style="width: 140px"><el-option v-for="year in years" :key="year" :label="year" :value="year" /></el-select></el-form-item>
      <el-form-item label="状态"><el-select v-model="query.status" clearable style="width: 140px"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
      <el-form-item><el-button type="primary" @click="load">查询</el-button><el-button type="success" @click="openSubmit">提交材料</el-button></el-form-item>
    </el-form>
    <el-table v-loading="loading" :data="rows" border>
      <el-table-column prop="reportYear" label="报告年度" width="110" align="center" />
      <el-table-column prop="reportName" label="材料名称" min-width="180" />
      <el-table-column label="材料附件" min-width="200"><template #default="scope"><FileUpload v-model="scope.row.materialFiles" disabled /></template></el-table-column>
      <el-table-column label="状态" width="110" align="center"><template #default="scope"><el-tag :type="statusType(scope.row.status)">{{ statusText(scope.row.status) }}</el-tag></template></el-table-column>
      <el-table-column prop="reviewRemark" label="审核意见" min-width="180" show-overflow-tooltip />
      <el-table-column prop="resultText" label="发布结果" min-width="220" show-overflow-tooltip />
      <el-table-column label="结果附件" min-width="200"><template #default="scope"><FileUpload v-if="scope.row.resultFiles" v-model="scope.row.resultFiles" disabled /><span v-else>-</span></template></el-table-column>
      <el-table-column prop="publishTime" label="发布时间" width="170" />
      <el-table-column label="操作" width="110" fixed="right"><template #default="scope"><el-button v-if="[0, 3].includes(scope.row.status)" link type="primary" @click="openSubmit(scope.row)">{{ scope.row.status === 3 ? "重新提交" : "修改" }}</el-button><span v-else>-</span></template></el-table-column>
    </el-table>
    <pagination v-show="total > 0" :total="total" v-model:page="query.current" v-model:limit="query.size" @pagination="load" />
    <el-dialog v-model="visible" title="提交年度财务材料" width="620px" append-to-body destroy-on-close :close-on-click-modal="false" :before-close="beforeClose" @closed="resetDialog">
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="报告年度" prop="reportYear"><el-select v-model="form.reportYear" style="width:100%"><el-option v-for="year in years" :key="year" :label="year" :value="year" /></el-select></el-form-item>
        <el-form-item label="材料名称" prop="reportName"><el-input v-model="form.reportName" placeholder="例如:2025年度审计报告" /></el-form-item>
        <el-form-item label="材料附件" prop="materialFiles"><FileUpload v-model="form.materialFiles" :limit="8" :file-size="20" /></el-form-item>
      </el-form>
      <template #footer><el-button :disabled="saving" @click="closeDialog">取消</el-button><el-button type="primary" :loading="saving" @click="save">确认提交</el-button></template>
    </el-dialog>
  </div>
</template>
<script setup>
import FileUpload from "@/components/FileUpload/index.vue";
import { listMySubmissions, submitCustomerMaterial } from "@/api/customerPortal/submission";
const { proxy } = getCurrentInstance();
const nowYear = new Date().getFullYear();
const years = [nowYear, nowYear - 1, nowYear - 2];
const statusOptions = [{ label: "待审核", value: 0 }, { label: "处理中", value: 1 }, { label: "已发布", value: 2 }, { label: "已退回", value: 3 }];
const query = reactive({ current: 1, size: 10, reportYear: undefined, status: undefined });
const rows = ref([]); const total = ref(0); const loading = ref(false); const visible = ref(false); const saving = ref(false); const formRef = ref();
const form = reactive({ reportYear: nowYear, reportName: "", materialFiles: "" });
const initialForm = ref("");
const rules = { reportYear: [{ required: true, message: "请选择报告年度", trigger: "change" }], reportName: [{ required: true, message: "请输入材料名称", trigger: "blur" }], materialFiles: [{ required: true, message: "请上传材料附件", trigger: "change" }] };
const statusText = (value) => statusOptions.find(item => item.value === value)?.label || "未知";
const statusType = (value) => ({ 0: "warning", 1: "primary", 2: "success", 3: "danger" }[value] || "info");
async function load() { loading.value = true; try { const res = await listMySubmissions(query); rows.value = res.data?.records || []; total.value = Number(res.data?.total || 0); } finally { loading.value = false; } }
function formSnapshot() { return JSON.stringify({ reportYear: form.reportYear, reportName: form.reportName, materialFiles: form.materialFiles }); }
function openSubmit(row) { Object.assign(form, { reportYear: row?.reportYear || nowYear, reportName: row?.reportName || "", materialFiles: row?.materialFiles || "" }); initialForm.value = formSnapshot(); visible.value = true; nextTick(() => formRef.value?.clearValidate()); }
function resetDialog() { formRef.value?.clearValidate(); Object.assign(form, { reportYear: nowYear, reportName: "", materialFiles: "" }); initialForm.value = ""; }
function beforeClose(done) { if (saving.value) return; if (formSnapshot() === initialForm.value) return done(); proxy.$modal.confirm("当前填写内容尚未提交,确定关闭吗?").then(done).catch(() => {}); }
function closeDialog() { beforeClose(() => { visible.value = false; }); }
async function save() { await formRef.value.validate(); if (!form.materialFiles) return proxy.$modal.msgError("请上传材料附件"); saving.value = true; try { await submitCustomerMaterial({ ...form }); proxy.$modal.msgSuccess("材料提交成功"); initialForm.value = formSnapshot(); visible.value = false; await load(); } finally { saving.value = false; } }
onMounted(load);
</script>
<style scoped>.query-form { margin-top: 18px; }</style>
src/views/customerPortal/submissionManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
<template>
  <div class="app-container">
    <el-form :inline="true" :model="query">
      <el-form-item label="客户"><el-select v-model="query.customerId" filterable clearable style="width:240px"><el-option v-for="item in customers" :key="item.id" :label="item.customerName" :value="item.id" /></el-select></el-form-item>
      <el-form-item label="年度"><el-input-number v-model="query.reportYear" :controls="false" /></el-form-item>
      <el-form-item label="状态"><el-select v-model="query.status" clearable style="width:140px"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
      <el-form-item><el-button type="primary" @click="handleQuery">查询</el-button><el-button @click="resetQuery">重置</el-button></el-form-item>
    </el-form>
    <el-table v-loading="loading" :data="rows" border>
      <el-table-column prop="customerName" label="客户名称" min-width="200" />
      <el-table-column prop="reportYear" label="年度" width="90" align="center" />
      <el-table-column prop="reportName" label="材料名称" min-width="170" />
      <el-table-column label="材料附件" min-width="210"><template #default="scope"><FileUpload v-model="scope.row.materialFiles" disabled /></template></el-table-column>
      <el-table-column label="状态" width="100"><template #default="scope"><el-tag :type="statusType(scope.row.status)">{{ statusText(scope.row.status) }}</el-tag></template></el-table-column>
      <el-table-column prop="submitUserName" label="提交人" width="110" />
      <el-table-column prop="createTime" label="提交时间" width="170" />
      <el-table-column prop="reviewRemark" label="审核意见" min-width="160" show-overflow-tooltip />
      <el-table-column prop="resultText" label="发布结果" min-width="180" show-overflow-tooltip />
      <el-table-column label="结果附件" min-width="180"><template #default="scope"><FileUpload v-if="scope.row.resultFiles" v-model="scope.row.resultFiles" disabled /><span v-else>-</span></template></el-table-column>
      <el-table-column label="操作" width="160" fixed="right"><template #default="scope"><el-button v-if="scope.row.status === 0" link type="primary" @click="openReview(scope.row, 1)">受理</el-button><el-button v-if="scope.row.status === 1" link type="success" @click="openReview(scope.row, 2)">发布</el-button><el-button v-if="[0, 1].includes(scope.row.status)" link type="danger" @click="openReview(scope.row, 3)">退回</el-button><span v-if="[2, 3].includes(scope.row.status)">-</span></template></el-table-column>
    </el-table>
    <pagination v-show="total > 0" :total="total" v-model:page="query.current" v-model:limit="query.size" @pagination="load" />
    <el-dialog v-model="visible" :title="dialogTitle" width="620px" append-to-body destroy-on-close :close-on-click-modal="false" @closed="resetReviewForm">
      <el-alert :title="`${currentRow.customerName || ''} Â· ${currentRow.reportYear || ''}年度 Â· ${currentRow.reportName || ''}`" type="info" :closable="false" class="review-summary" />
      <el-form ref="reviewFormRef" :model="form" label-width="100px">
        <el-form-item label="结果说明" v-if="form.status === 2"><el-input v-model="form.resultText" type="textarea" :rows="4" /></el-form-item>
        <el-form-item label="结果附件" v-if="form.status === 2"><FileUpload v-model="form.resultFiles" :limit="8" :file-size="20" /></el-form-item>
        <el-form-item label="审核意见"><el-input v-model="form.reviewRemark" type="textarea" :rows="3" /></el-form-item>
      </el-form>
      <template #footer><el-button :disabled="saving" @click="visible=false">取消</el-button><el-button type="primary" :loading="saving" @click="saveReview">确定</el-button></template>
    </el-dialog>
  </div>
</template>
<script setup>
import FileUpload from "@/components/FileUpload/index.vue";
import { listSubmissionManage, reviewSubmission } from "@/api/customerPortal/submission";
import { listCustomer } from "@/api/basicData/customer";
const { proxy } = getCurrentInstance();
const statusOptions = [{ label: "待审核", value: 0 }, { label: "处理中", value: 1 }, { label: "已发布", value: 2 }, { label: "已退回", value: 3 }];
const query = reactive({ current: 1, size: 10, customerId: undefined, reportYear: undefined, status: undefined });
const rows = ref([]); const customers = ref([]); const total = ref(0); const loading = ref(false); const visible = ref(false); const saving = ref(false);
const form = reactive({ id: null, status: 1, resultText: "", resultFiles: "", reviewRemark: "" });
const currentRow = ref({}); const reviewFormRef = ref();
const dialogTitle = computed(() => form.status === 2 ? "发布申报结果" : form.status === 3 ? "退回材料" : "受理材料");
const statusText = (value) => statusOptions.find(item => item.value === value)?.label || "未知";
const statusType = (value) => ({ 0: "warning", 1: "primary", 2: "success", 3: "danger" }[value] || "info");
async function load() { loading.value = true; try { const res = await listSubmissionManage(query); rows.value = res.data?.records || []; total.value = Number(res.data?.total || 0); } finally { loading.value = false; } }
function handleQuery() { query.current = 1; load(); }
function resetQuery() { Object.assign(query, { current: 1, size: query.size, customerId: undefined, reportYear: undefined, status: undefined }); load(); }
function openReview(row, status) { currentRow.value = row; Object.assign(form, { id: row.id, status, resultText: status === 2 ? row.resultText || "" : "", resultFiles: status === 2 ? row.resultFiles || "" : "", reviewRemark: "" }); visible.value = true; }
function resetReviewForm() { currentRow.value = {}; Object.assign(form, { id: null, status: 1, resultText: "", resultFiles: "", reviewRemark: "" }); reviewFormRef.value?.clearValidate(); }
async function saveReview() { if (form.status === 2 && !form.resultText.trim() && !form.resultFiles) return proxy.$modal.msgError("请填写结果说明或上传结果附件"); if (form.status === 3 && !form.reviewRemark.trim()) return proxy.$modal.msgError("请输入退回原因"); saving.value = true; try { await reviewSubmission({ ...form }); proxy.$modal.msgSuccess(form.status === 1 ? "材料已受理" : form.status === 2 ? "结果发布成功" : "材料已退回"); visible.value = false; await load(); } finally { saving.value = false; } }
onMounted(async () => { const res = await listCustomer({ current: -1, size: -1 }); customers.value = res.data?.records || []; await load(); });
</script>
<style scoped>.review-summary { margin-bottom: 18px; }</style>
src/views/procurementManagement/procurementLedger/index.vue
@@ -1,2066 +1,427 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <el-form :model="searchForm"
                 :inline="true">
          <el-form-item label="供应商名称:">
            <el-input v-model="searchForm.supplierName"
                      placeholder="请输入"
                      clearable
                      prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="采购合同号:">
            <el-input v-model="searchForm.purchaseContractNumber"
                      style="width: 240px"
                      placeholder="请输入"
                      @change="handleQuery"
                      clearable
                      :prefix-icon="Search" />
          </el-form-item>
          <el-form-item label="销售合同号:">
            <el-input v-model="searchForm.salesContractNo"
                      placeholder="请输入"
                      clearable
                      prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="项目名称:">
            <el-input v-model="searchForm.projectName"
                      placeholder="请输入"
                      clearable
                      prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="录入日期:">
            <el-date-picker v-model="searchForm.entryDate"
                            value-format="YYYY-MM-DD"
                            format="YYYY-MM-DD"
                            type="daterange"
                            placeholder="请选择"
                            clearable
                            @change="changeDaterange" />
          </el-form-item>
          <el-form-item label="入库状态:">
            <el-select v-model="searchForm.stockInStatus"
                       placeholder="请选择"
                       clearable
                       style="width: 240px"
                       @change="handleQuery">
              <el-option label="待入库"
                         value="待入库" />
              <el-option label="入库中"
                         value="入库中" />
              <el-option label="完全入库"
                         value="完全入库" />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary"
                       @click="handleQuery"> æœç´¢
            </el-button>
          </el-form-item>
        </el-form>
      </div>
      <el-form :model="query" :inline="true">
        <el-form-item label="支出日期:">
          <el-date-picker
            v-model="dateRange"
            type="daterange"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            clearable
            @change="handleDateChange"
          />
        </el-form-item>
        <el-form-item label="付款方式:">
          <el-select
            v-model="query.paymentMethod"
            placeholder="请选择"
            clearable
            style="width: 180px"
            @change="handleQuery"
          >
            <el-option v-for="item in paymentMethodOptions" :key="item" :label="item" :value="item" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">搜索</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="table_list">
      <div style="display: flex;justify-content: flex-end;margin-bottom: 20px;">
<!--        <el-button type="success"-->
<!--                   plain-->
<!--                   @click="handleBatchGenerate">批量生成数据</el-button>-->
        <el-button type="primary"
                   @click="openForm('add')">新增台账</el-button>
        <el-button type="primary"
                   plain
                   @click="handleImport">导入</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除
        </el-button>
      <div class="toolbar">
        <el-button type="primary" @click="openForm('add')">新增</el-button>
        <el-button @click="handleExport">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">批量删除</el-button>
      </div>
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                @selection-change="handleSelectionChange"
                :expand-row-keys="expandedRowKeys"
                :row-key="(row) => row.id"
                show-summary
                :summary-method="summarizeMainTable"
                @expand-change="expandChange"
                height="calc(100vh - 21.5em)">
        <el-table-column align="center"
                         type="selection"
                         width="55" />
        <el-table-column type="expand">
          <template #default="props">
            <el-table :data="props.row.children"
                      border
                      show-summary
                      :summary-method="summarizeChildrenTable">
              <el-table-column align="center"
                               label="序号"
                               type="index"
                               width="60" />
              <el-table-column label="产品大类"
                               prop="productCategory" />
              <el-table-column label="规格型号"
                               prop="specificationModel" />
              <el-table-column label="单位"
                               prop="unit" />
              <el-table-column label="入库审核状态"
                               prop="stockInApprovalStatus"
                               width="120">
                <template #default="scope">
                  <el-tag :type="getStockInApprovalStatusType(scope.row.stockInApprovalStatus)"
                          size="small">
                    {{ scope.row.stockInApprovalStatus || '--' }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="数量"
                               prop="quantity" />
              <el-table-column label="销售发货数量"
                               prop="shippedQuantity" />
              <el-table-column label="可用数量"
                               prop="availableQuality" />
              <el-table-column label="退货数量"
                               prop="returnQuantity" />
              <el-table-column label="税率(%)"
                               prop="taxRate" />
              <el-table-column label="含税单价(元)"
                               prop="taxInclusiveUnitPrice"
                               :formatter="formattedNumber" />
              <el-table-column label="含税总价(元)"
                               prop="taxInclusiveTotalPrice"
                               :formatter="formattedNumber" />
              <el-table-column label="不含税总价(元)"
                               prop="taxExclusiveTotalPrice"
                               :formatter="formattedNumber" />
            </el-table>
          </template>
      <el-table
        v-loading="loading"
        :data="tableData"
        border
        height="calc(100vh - 21.5em)"
        @selection-change="handleSelectionChange"
      >
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="序号" type="index" width="60" align="center" />
        <el-table-column label="采购日期" prop="purchaseDate" width="120" align="center" />
        <el-table-column label="金额" prop="amount" min-width="120" align="right">
          <template #default="{ row }">{{ formatAmount(row.amount) }}</template>
        </el-table-column>
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="采购合同号"
                         prop="purchaseContractNumber"
                         width="160"
                         show-overflow-tooltip />
        <el-table-column label="销售合同号"
                         prop="salesContractNo"
                         width="160"
                         show-overflow-tooltip />
        <el-table-column label="供应商名称"
                         prop="supplierName"
                         width="160"
                         show-overflow-tooltip />
        <el-table-column label="项目名称"
                         prop="projectName"
                         width="320"
                         show-overflow-tooltip />
        <el-table-column label="审批状态"
                         prop="approvalStatus"
                         width="100"
                         show-overflow-tooltip>
          <template #default="scope">
            <el-tag :type="getApprovalStatusType(scope.row.approvalStatus)"
                    size="small">
              {{ approvalStatusText[scope.row.approvalStatus] || '未知状态' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="入库状态"
                         prop="stockInStatus"
                         width="100"
                         show-overflow-tooltip>
          <template #default="scope">
            <el-tag :type="getStockInStatusType(scope.row.stockInStatus)"
                    size="small">
              {{ scope.row.stockInStatus || '--' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="签订日期"
                         prop="executionDate"
                         width="100"
                         show-overflow-tooltip />
        <el-table-column label="付款方式"
                         width="100"
                         prop="paymentMethod"
                         show-overflow-tooltip />
        <el-table-column label="合同金额(元)"
                         prop="contractAmount"
                         width="200"
                         show-overflow-tooltip
                         :formatter="formattedNumber" />
        <el-table-column label="实际合同金额(元)"
                         prop="netContractAmount"
                         width="200"
                         show-overflow-tooltip
                         :formatter="(_, __, cellValue) => formatMoney(cellValue)" />
        <el-table-column label="录入人"
                         prop="recorderName"
                         width="120"
                         show-overflow-tooltip />
        <el-table-column label="录入日期"
                         prop="entryDate"
                         width="100"
                         show-overflow-tooltip />
        <el-table-column label="备注"
                         prop="remarks"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="120"
                         align="center">
          <template #default="scope">
            <el-button link
                       type="primary"
                       @click="openForm('edit', scope.row)"
                       :disabled="scope.row.approvalStatus === 3">编辑
            </el-button>
            <el-button link
                       type="primary"
                       @click="openFileDialog(scope.row)">附件</el-button>
        <el-table-column label="采购物品" prop="purchaseGoods" min-width="180" show-overflow-tooltip />
        <el-table-column label="发票号码" prop="invoiceNumber" min-width="150" show-overflow-tooltip />
        <el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
        <el-table-column label="录入人" prop="recorderName" width="120" align="center" />
        <el-table-column label="录入日期" prop="entryDate" width="120" align="center" />
        <el-table-column label="操作" width="140" fixed="right" align="center">
          <template #default="{ row }">
            <el-button link type="primary" @click="openForm('edit', row)">编辑</el-button>
            <el-button link type="primary" @click="openFileDialog(row)">附件</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0"
                  :total="total"
                  layout="total, sizes, prev, pager, next, jumper"
                  :page="page.current"
                  :limit="page.size"
                  @pagination="paginationChange" />
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="query.current"
        :limit="query.size"
        @pagination="paginationChange"
      />
    </div>
    <FormDialog v-model="dialogFormVisible"
                :title="operationType === 'add' ? '新增采购台账页面' : '编辑采购台账页面'"
                :width="'70%'"
                :operation-type="operationType"
                @close="closeDia"
                @confirm="submitForm"
                @cancel="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="采购合同号:"
                          prop="purchaseContractNumber">
              <el-input v-model="form.purchaseContractNumber"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="销售合同号:"
                          prop="salesLedgerId">
              <el-select v-model="form.salesLedgerId"
                         placeholder="请选择"
                         filterable
                         clearable
                         @change="salesLedgerChange">
                <el-option v-for="item in salesContractList"
                           :key="item.id"
                           :label="item.salesContractNo"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="供应商名称:"
                          prop="supplierId">
              <el-select v-model="form.supplierId"
                         placeholder="请选择"
                         filterable
                         clearable>
                <el-option v-for="item in supplierList"
                           :key="item.id"
                           :label="item.supplierName"
                           :value="item.id">{{ item.supplierName + '---' + item.supplierType }}
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="项目名称"
                          prop="projectName">
              <el-input v-model="form.projectName"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="付款方式">
              <el-input v-model="form.paymentMethod"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="签订日期:"
                          prop="executionDate">
              <el-date-picker style="width: 100%"
                              v-model="form.executionDate"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              type="date"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="录入人:"
                          prop="recorderId">
              <el-select v-model="form.recorderId"
                         placeholder="请选择"
                         clearable
                         filterable>
                <el-option v-for="item in userList"
                           :key="item.userId"
                           :label="item.nickName"
                           :value="item.userId" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="录入日期:"
                          prop="entryDate">
              <el-date-picker style="width: 100%"
                              v-model="form.entryDate"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              type="date"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-form-item label="产品信息:"
                        prop="entryDate">
            <el-button type="primary"
                       @click="openProductForm('add')">添加
            </el-button>
            <el-button plain
                       type="danger"
                       @click="deleteProduct">删除
            </el-button>
          </el-form-item>
          <div class="select-button-group"
               style="width: 500px; margin: 20px 0;"
               v-if="operationType === 'add'">
            <el-select filterable
                       allow-create
                       :reserve-keyword="true"
                       :default-first-option="false"
                       clearable
                       v-model="templateName"
                       :input-value="filterInputValue"
                       @filter-change="onTemplateFilterChange"
                       @change="onTemplateChange"
                       @focus="getTemplateList"
                       style="width: 500px;"
                       placeholder="请选择模版或者输入新的模版名称后选择"
                       class="no-arrow-select">
              <el-option v-for="item in templateList"
                         :key="item.id || item.value"
                         :label="item.templateName"
                         :value="item.templateName">
                <div style="display: flex; justify-content: space-between; align-items: center;">
                  <span>{{ item.templateName }}</span>
                  <el-icon v-if="item.id"
                           class="delete-icon"
                           @click.stop="handleDeleteTemplate(item)"
                           style="cursor: pointer; color: #f56c6c; font-size: 14px; margin-left: 8px;">
                    <Delete />
                  </el-icon>
                </div>
              </el-option>
            </el-select>
            <!-- æŒ‰é’®ï¼šä¸Ž Select é«˜åº¦åŒ¹é…ï¼ŒåŽ»æŽ‰å·¦ä¾§è¾¹æ¡†ï¼Œæ— ç¼è¡”æŽ¥ -->
            <el-button size="small"
                       style="height: 32px;margin-left: 8px;"
                       @click="handleButtonClick"
                       :disabled="!templateName || templateName.trim() === '' || (!currentTemplateId && isTemplateNameDuplicate)">
              ä¿å­˜
            </el-button>
          </div>
        </el-row>
        <el-table :data="productData"
                  border
                  @selection-change="productSelected"
                  show-summary
                  :summary-method="summarizeProTable">
          <el-table-column align="center"
                           type="selection"
                           width="55" />
          <el-table-column align="center"
                           label="序号"
                           type="index"
                           width="60" />
          <el-table-column label="产品大类"
                           prop="productCategory" />
          <el-table-column label="规格型号"
                           prop="specificationModel" />
          <el-table-column label="单位"
                           prop="unit"
                           width="70" />
          <el-table-column label="数量"
                           prop="quantity"
                           width="70" />
          <el-table-column label="库存预警数量"
                           prop="warnNum"
                           width="120"
                           show-overflow-tooltip />
          <el-table-column label="税率(%)"
                           prop="taxRate"
                           width="80" />
          <el-table-column label="含税单价(元)"
                           prop="taxInclusiveUnitPrice"
                           :formatter="formattedNumber"
                           width="150" />
          <el-table-column label="含税总价(元)"
                           prop="taxInclusiveTotalPrice"
                           :formatter="formattedNumber"
                           width="150" />
          <el-table-column label="不含税总价(元)"
                           prop="taxExclusiveTotalPrice"
                           :formatter="formattedNumber"
                           width="150" />
          <el-table-column label="是否质检"
                           prop="isChecked"
                           width="150">
            <template #default="scope">
              <el-tag :type="scope.row.isChecked ? 'success' : 'info'">
                {{ scope.row.isChecked ? '是' : '否' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="入库审核状态"
                           prop="stockInApprovalStatus"
                           width="120">
            <template #default="scope">
              <el-tag :type="getStockInApprovalStatusType(scope.row.stockInApprovalStatus)"
                      size="small">
                {{ scope.row.stockInApprovalStatus || '--' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column fixed="right"
                           label="操作"
                           min-width="60"
                           align="center">
            <template #default="scope">
              <el-button link
                         type="primary"
                         @click="openProductForm('edit', scope.row, scope.$index)"
                         :disabled="scope.row.stockInApprovalStatus === '完全入库'">编辑
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注·:"
                          prop="remarks">
              <el-input v-model="form.remarks"
                        placeholder="请输入"
                        clearable
                        type="textarea"
                        :rows="2" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件材料:"
                          prop="purchaseLedgerFiles">
              <FileUpload v-model:file-list="fileList" />
            </el-form-item>
          </el-col>
        </el-row>
    <el-dialog
      v-model="formVisible"
      :title="operationType === 'add' ? '新增采购台账' : '编辑采购台账'"
      width="640px"
      destroy-on-close
      append-to-body
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="采购日期:" prop="purchaseDate">
          <el-date-picker
            v-model="form.purchaseDate"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="请选择"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="支出日期:" prop="paymentDate">
          <el-date-picker
            v-model="form.paymentDate"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="请选择"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="付款方式:" prop="paymentMethod">
          <el-select
            v-model="form.paymentMethod"
            placeholder="请选择"
            clearable
            filterable
            allow-create
            default-first-option
            style="width: 100%"
          >
            <el-option v-for="item in paymentMethodOptions" :key="item" :label="item" :value="item" />
          </el-select>
        </el-form-item>
        <el-form-item label="金额(元):" prop="amount">
          <el-input-number
            v-model="form.amount"
            :min="0"
            :precision="2"
            controls-position="right"
            placeholder="请输入"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="采购物品:" prop="purchaseGoods">
          <el-input v-model="form.purchaseGoods" placeholder="请输入" clearable />
        </el-form-item>
        <el-form-item label="发票号码:" prop="invoiceNumber">
          <el-input v-model="form.invoiceNumber" placeholder="请输入" clearable />
        </el-form-item>
        <el-form-item label="备注:" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入" maxlength="500" show-word-limit />
        </el-form-item>
        <el-form-item label="附件:" prop="attachments">
          <AttachmentUploadFile v-model:file-list="formFiles" :limit="10" />
        </el-form-item>
      </el-form>
    </FormDialog>
    <!-- å¯¼å…¥å¼¹çª— -->
    <FormDialog v-model="importUpload.open"
                :title="importUpload.title"
                :width="'600px'"
                @close="importUpload.open = false"
                @confirm="submitImportFile"
                @cancel="importUpload.open = false">
      <el-upload ref="importUploadRef"
                 :limit="1"
                 accept=".xlsx,.xls"
                 :action="importUpload.url"
                 :headers="importUpload.headers"
                 :before-upload="importUpload.beforeUpload"
                 :on-success="importUpload.onSuccess"
                 :on-error="importUpload.onError"
                 :on-progress="importUpload.onProgress"
                 :on-change="importUpload.onChange"
                 :auto-upload="false"
                 drag>
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">
          å°†æ–‡ä»¶æ‹–到此处,或<em>点击上传</em>
        </div>
        <template #tip>
          <div class="el-upload__tip">
            ä»…支持 xls/xlsx,大小不超过 10MB。
            <el-button link
                       type="primary"
                       @click="downloadTemplate">下载导入模板</el-button>
          </div>
        </template>
      </el-upload>
    </FormDialog>
    <FormDialog v-model="productFormVisible"
                :title="productOperationType === 'add' ? '新增产品' : '编辑产品'"
                :width="'40%'"
                :operation-type="productOperationType"
                @close="closeProductDia"
                @confirm="submitProduct"
                @cancel="closeProductDia">
      <el-form :model="productForm"
               label-width="140px"
               label-position="top"
               :rules="productRules"
               ref="productFormRef">
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="产品大类:"
                          prop="productId">
              <el-tree-select v-model="productForm.productId"
                              placeholder="请选择"
                              clearable
                              filterable
                              check-strictly
                              @change="getModels"
                              :data="productOptions"
                              :render-after-expand="false"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="规格型号:"
                          prop="productModelId">
              <el-select v-model="productForm.productModelId"
                         placeholder="请选择"
                         filterable
                         clearable
                         @change="getProductModel">
                <el-option v-for="item in modelOptions"
                           :key="item.id"
                           :label="item.model"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="单位:"
                          prop="unit">
              <el-input v-model="productForm.unit"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="税率(%):"
                          prop="taxRate">
              <el-select v-model="productForm.taxRate"
                         placeholder="请选择"
                         clearable
                         @change="mathNum">
                <el-option v-for="dict in tax_rate"
                           :key="dict.value"
                           :label="dict.label"
                           :value="dict.value" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="含税单价(元):"
                          prop="taxInclusiveUnitPrice">
              <el-input-number v-model="productForm.taxInclusiveUnitPrice"
                               :precision="2"
                               :step="0.1"
                               :min="0"
                               clearable
                               style="width: 100%"
                               @change="mathNum" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="数量:"
                          prop="quantity">
              <el-input-number :step="0.1"
                               clearable
                               :precision="3"
                               :min="0"
                               style="width: 100%"
                               v-model="productForm.quantity"
                               placeholder="请输入"
                               @change="mathNum" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="含税总价(元):"
                          prop="taxInclusiveTotalPrice">
              <el-input-number v-model="productForm.taxInclusiveTotalPrice"
                               :precision="2"
                               :step="0.1"
                               :min="0"
                               clearable
                               style="width: 100%"
                               @change="reverseMathNum('taxInclusiveTotalPrice')" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="不含税总价(元):"
                          prop="taxExclusiveTotalPrice">
              <el-input-number v-model="productForm.taxExclusiveTotalPrice"
                               :precision="2"
                               :step="0.1"
                               :min="0"
                               clearable
                               style="width: 100%"
                               @change="reverseMathNum('taxExclusiveTotalPrice')" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="发票类型:"
                          prop="invoiceType">
              <el-select v-model="productForm.invoiceType"
                         placeholder="请选择"
                         clearable>
                <el-option label="增普票"
                           value="增普票" />
                <el-option label="增专票"
                           value="增专票" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="库存预警数量:"
                          prop="warnNum">
              <el-input-number v-model="productForm.warnNum"
                               :precision="2"
                               :step="0.1"
                               :min="0"
                               clearable
                               style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="是否质检:"
                          prop="isChecked">
              <el-radio-group v-model="productForm.isChecked">
                <el-radio label="是"
                          :value="true" />
                <el-radio label="否"
                          :value="false" />
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </FormDialog>
    <FileList v-if="fileListDialogVisible"
              v-model:visible="fileListDialogVisible"
              record-type="purchase_ledger"
              :record-id="recordId" />
      <template #footer>
        <el-button @click="formVisible = false">取消</el-button>
        <el-button type="primary" :loading="saving" @click="submit">确定</el-button>
      </template>
    </el-dialog>
    <FileList
      v-if="fileDialogVisible"
      v-model:visible="fileDialogVisible"
      record-type="purchase_ledger_simple"
      :record-id="currentRowId"
    />
  </div>
</template>
<script setup>
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import FileListDialog from "@/components/Dialog/FileListDialog.vue";
  import { getToken } from "@/utils/auth";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import {
    ref,
    onMounted,
    reactive,
    toRefs,
    getCurrentInstance,
    nextTick,
  } from "vue";
  import { Search, Delete } from "@element-plus/icons-vue";
  import { ElMessageBox, ElMessage } from "element-plus";
  import { userListNoPage } from "@/api/system/user.js";
  import {
    addOrUpdateSalesLedgerProduct,
    delProduct,
    delLedgerFile,
    getProductInfoByContractNo,
  } from "@/api/salesManagement/salesLedger.js";
  import {
    addOrEditPurchase,
    addPurchaseTemplate,
    updatePurchaseTemplate,
    createPurchaseNo,
    delPurchase,
    getSalesNo,
    purchaseListPage,
    productList,
    getPurchaseById,
    getOptions,
    getPurchaseTemplateList,
    delPurchaseTemplate,
    batchGeneratePurchaseInboundSteps,
  } from "@/api/procurementManagement/procurementLedger.js";
  import useFormData from "@/hooks/useFormData.js";
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
<script setup name="ProcurementLedger">
import { getCurrentInstance, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import pagination from '@/components/PIMTable/Pagination.vue'
import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue'
import FileList from '@/components/Dialog/FileList.vue'
import { simpleLedgerList, simpleLedgerGet, simpleLedgerSave, simpleLedgerDelete } from '@/api/procurementManagement/procurementLedger.js'
import { attachmentList, createAttachment } from '@/api/basicData/storageAttachment.js'
import useUserStore from '@/store/modules/user'
  const { proxy } = getCurrentInstance();
  const { tax_rate } = proxy.useDict("tax_rate");
  const tableData = ref([]);
  const productData = ref([]);
  const selectedRows = ref([]);
  const productSelectedRows = ref([]);
  const modelOptions = ref([]);
  const userList = ref([]);
  const productOptions = ref([]);
  const salesContractList = ref([]);
  const supplierList = ref([]);
  const tableLoading = ref(false);
  const recordId = ref();
  const fileListDialogVisible = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
  });
  const total = ref(0);
  const fileList = ref([]);
  import useUserStore from "@/store/modules/user";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import dayjs from "dayjs";
  import FileUpload from "@/components/AttachmentUpload/file/index.vue";
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
  const userStore = useUserStore();
const RECORD_TYPE = 'purchase_ledger_simple'
const paymentMethodOptions = ['现金', '银行转账', '承兑汇票', '其他']
  // è®¢å•审批状态显示文本
  const approvalStatusText = {
    1: "待审核",
    2: "审批中",
    3: "审批通过",
    4: "审批失败",
  };
const loading = ref(false)
const saving = ref(false)
const total = ref(0)
const tableData = ref([])
const selectedRows = ref([])
const dateRange = ref([])
  // èŽ·å–å®¡æ‰¹çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getApprovalStatusType = status => {
    const typeMap = {
      1: "info", // å¾…审核 - ç°è‰²
      2: "warning", // å®¡æ‰¹ä¸­ - æ©™è‰²
      3: "success", // å®¡æ‰¹é€šè¿‡ - ç»¿è‰²
      4: "danger", // å®¡æ‰¹å¤±è´¥ - çº¢è‰²
    };
    return typeMap[status] || "";
  };
const query = reactive({
  current: 1,
  size: 10,
  paymentDateStart: undefined,
  paymentDateEnd: undefined,
  paymentMethod: undefined
})
  // èŽ·å–å…¥åº“çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStockInStatusType = status => {
    const typeMap = {
      å¾…入库: "info", // å¾…入库 - ç°è‰²
      å…¥åº“中: "warning", // å…¥åº“中 - æ©™è‰²
      å®Œå…¨å…¥åº“: "success", // å®Œå…¨å…¥åº“ - ç»¿è‰²
    };
    return typeMap[status] || "";
  };
const formVisible = ref(false)
const operationType = ref('add')
const formRef = ref()
const formFiles = ref([])
const fileDialogVisible = ref(false)
const currentRowId = ref(0)
  // èŽ·å–å…¥åº“å®¡æ ¸çŠ¶æ€æ ‡ç­¾ç±»åž‹
  const getStockInApprovalStatusType = status => {
    const typeMap = {
      å¾…入库: "info", // å¾…入库 - ç°è‰²
      å…¥åº“中: "warning", // å…¥åº“中 - æ©™è‰²
      å®Œå…¨å…¥åº“: "success", // å®Œå…¨å…¥åº“ - ç»¿è‰²
    };
    return typeMap[status] || "";
  };
const form = ref({
  id: undefined,
  purchaseDate: '',
  paymentDate: '',
  paymentMethod: '',
  amount: undefined,
  purchaseGoods: '',
  invoiceNumber: '',
  remark: ''
})
  const templateName = ref("");
  const filterInputValue = ref("");
  const templateList = ref([]);
  const isTemplateNameDuplicate = ref(false); // æ ‡è®°æ¨¡æ¿åç§°æ˜¯å¦é‡å¤
  // å½“前选中的模板ID(用于区分新增模板还是更新模板)
  const currentTemplateId = ref(null);
const rules = {
  purchaseDate: [{ required: true, message: '请选择采购日期', trigger: 'change' }],
  amount: [{ required: true, message: '请输入金额', trigger: 'blur' }],
  purchaseGoods: [{ required: true, message: '请输入采购物品', trigger: 'blur' }]
}
  // æ£€æŸ¥æ¨¡æ¿åç§°æ˜¯å¦é‡å¤
  const checkTemplateNameDuplicate = name => {
    if (!name || name.trim() === "") {
      isTemplateNameDuplicate.value = false;
      return false;
    }
    const isDuplicate = templateList.value.some(
      item => item.templateName === name.trim()
    );
    isTemplateNameDuplicate.value = isDuplicate;
    return isDuplicate;
  };
function todayStr() {
  const d = new Date()
  const pad = n => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
  // é˜²æŠ–定时器
  let duplicateCheckTimer = null;
  const onTemplateFilterChange = val => {
    filterInputValue.value = val ?? "";
    // æ¸…除之前的定时器
    if (duplicateCheckTimer) {
      clearTimeout(duplicateCheckTimer);
    }
    // å®žæ—¶æ£€æŸ¥æ¨¡æ¿åç§°æ˜¯å¦é‡å¤ï¼ˆé˜²æŠ–处理,避免频繁提示)
    if (val && val.trim()) {
      duplicateCheckTimer = setTimeout(() => {
        const isDuplicate = checkTemplateNameDuplicate(val);
        if (isDuplicate) {
          ElMessage({
            message: "模板名称已存在,请更换模板名称",
            type: "warning",
            duration: 2000,
          });
        }
      }, 300); // 300ms é˜²æŠ–
    } else {
      isTemplateNameDuplicate.value = false;
    }
  };
function formatAmount(val) {
  const num = Number(val)
  return Number.isFinite(num) ? num.toFixed(2) : '-'
}
  // allow-create æ—¶ï¼Œè¾“入不存在的内容会作为 string å€¼è¿”回;这里同步回输入框以确保文字不丢
  const onTemplateChange = async val => {
    if (typeof val === "string") {
      filterInputValue.value = val;
      // é€‰æ‹©æˆ–输入时检查重复
      checkTemplateNameDuplicate(val);
    }
function getList() {
  loading.value = true
  simpleLedgerList({ ...query })
    .then(res => {
      const data = res?.data || {}
      tableData.value = data.records || []
      total.value = Number(data.total) || 0
    })
    .catch(() => {
      tableData.value = []
      total.value = 0
    })
    .finally(() => {
      loading.value = false
    })
}
    // è¿‡æ»¤æ•°æ®ï¼ŒæŸ¥æ‰¾åŒ¹é…çš„æ¨¡æ¿
    const matchedTemplate = templateList.value.find(
      item => item.templateName === val
    );
function handleQuery() {
  query.current = 1
  getList()
}
    if (matchedTemplate?.id) {
      // è®°å½•当前选中的模板ID,后续保存时进行更新操作
      currentTemplateId.value = matchedTemplate.id;
      // é€‰ä¸­å·²æœ‰æ¨¡æ¿æ—¶ï¼Œä¸åº”视为“模板名称重复导致不可保存”
      isTemplateNameDuplicate.value = false;
      // å¦‚果找到模板,只赋值供应商、项目名称、付款方式和产品信息
      if (matchedTemplate.supplierId) {
        form.value.supplierId = matchedTemplate.supplierId;
      }
      if (matchedTemplate.supplierName) {
        form.value.supplierName = matchedTemplate.supplierName;
      }
      if (matchedTemplate.projectName) {
        form.value.projectName = matchedTemplate.projectName;
      }
      if (matchedTemplate.paymentMethod) {
        form.value.paymentMethod = matchedTemplate.paymentMethod;
      }
      // æ¨¡æ¿æ•°æ®ä¸­çš„产品字段是 productList,需要转换为 productData
      productData.value =
        matchedTemplate.productList || matchedTemplate.productData || [];
    } else {
      // æœªåŒ¹é…åˆ°å·²æœ‰æ¨¡æ¿ï¼Œè§†ä¸ºæ–°æ¨¡æ¿
      currentTemplateId.value = null;
      // å¦‚果没有找到模板,重置表单(保持当前表单状态)
      const currentFormData = { ...form.value };
      const currentProductData = [...productData.value];
function handleDateChange(val) {
  query.paymentDateStart = val?.[0]
  query.paymentDateEnd = val?.[1]
  handleQuery()
}
      // å¦‚果对话框未打开,先打开
      if (!dialogFormVisible.value) {
        operationType.value = "add";
        dialogFormVisible.value = true;
      }
function resetQuery() {
  dateRange.value = []
  query.paymentDateStart = undefined
  query.paymentDateEnd = undefined
  query.paymentMethod = undefined
  query.current = 1
  getList()
}
      // ç­‰å¾…下一个 tick åŽæ¢å¤æ•°æ®
      await nextTick();
      form.value = {
        ...form.value,
        ...currentFormData,
      };
      productData.value = currentProductData;
    }
  };
function paginationChange(obj) {
  query.current = obj.page
  query.size = obj.limit
  getList()
}
  // ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
  const operationType = ref("");
  const dialogFormVisible = ref(false);
  const data = reactive({
    searchForm: {
      supplierName: "", // ä¾›åº”商名称
      purchaseContractNumber: "", // é‡‡è´­åˆåŒç¼–号
      salesContractNo: "", // é”€å”®åˆåŒç¼–号
      projectName: "", // é¡¹ç›®åç§°
      entryDate: null, // å½•入日期
      entryDateStart: undefined,
      entryDateEnd: undefined,
function handleSelectionChange(rows) {
  selectedRows.value = rows
}
function handleExport() {
  proxy.download(
    'purchase/simpleLedger/export',
    {
      paymentDateStart: query.paymentDateStart,
      paymentDateEnd: query.paymentDateEnd,
      paymentMethod: query.paymentMethod
    },
    form: {
      purchaseContractNumber: "",
      salesLedgerId: "",
      projectName: "",
      recorderId: "",
      entryDate: "",
      productData: [],
      supplierName: "",
      supplierId: "",
      paymentMethod: "",
      executionDate: "",
      isChecked: false,
    },
    rules: {
      purchaseContractNumber: [
        { required: false, message: "请输入", trigger: "blur" },
      ],
      projectName: [
        { required: true, message: "请输入项目名称", trigger: "blur" },
      ],
      supplierId: [{ required: true, message: "请输入", trigger: "blur" }],
      entryDate: [{ required: true, message: "请选择", trigger: "change" }],
      executionDate: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { form, rules } = toRefs(data);
  const { form: searchForm } = useFormData({
    ...data.searchForm,
    // è®¾ç½®å½•入日期范围为当天
    entryDate: [
      dayjs().startOf("day").format("YYYY-MM-DD"),
      dayjs().endOf("day").format("YYYY-MM-DD"),
    ],
    entryDateStart: dayjs().startOf("day").format("YYYY-MM-DD"),
    entryDateEnd: dayjs().endOf("day").format("YYYY-MM-DD"),
  });
    '采购台账.xlsx'
  )
}
  // äº§å“è¡¨å•弹框数据
  const productFormVisible = ref(false);
  const productOperationType = ref("");
  const productOperationIndex = ref("");
  const currentId = ref("");
  const productFormData = reactive({
    productForm: {
      productId: "",
      productCategory: "",
      productModelId: "",
      specificationModel: "",
      unit: "",
      quantity: "",
      taxInclusiveUnitPrice: "",
      taxRate: "",
      taxInclusiveTotalPrice: "",
      taxExclusiveTotalPrice: "",
      invoiceType: "",
      warnNum: "",
      isChecked: false,
    },
    productRules: {
      productId: [{ required: true, message: "请选择", trigger: "change" }],
      productModelId: [{ required: true, message: "请选择", trigger: "change" }],
      unit: [{ required: true, message: "请输入", trigger: "blur" }],
      quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      taxInclusiveUnitPrice: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      taxRate: [{ required: true, message: "请选择", trigger: "change" }],
      warnNum: [{ required: true, message: "请选择", trigger: "change" }],
      taxInclusiveTotalPrice: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      taxExclusiveTotalPrice: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      invoiceType: [{ required: true, message: "请选择", trigger: "change" }],
      isChecked: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { productForm, productRules } = toRefs(productFormData);
  const upload = reactive({
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
    // è®¾ç½®ä¸Šä¼ çš„请求头部
    headers: { Authorization: "Bearer " + getToken() },
  });
  // å¯¼å…¥ç›¸å…³
  const importUploadRef = ref(null);
  const importUpload = reactive({
    title: "导入采购台账",
    open: false,
    url: import.meta.env.VITE_APP_BASE_API + "/purchase/ledger/import",
    headers: { Authorization: "Bearer " + getToken() },
    isUploading: false,
    beforeUpload: file => {
      const isExcel = file.name.endsWith(".xlsx") || file.name.endsWith(".xls");
      const isLt10M = file.size / 1024 / 1024 < 10;
      if (!isExcel) {
        proxy.$modal.msgError("上传文件只能是 xlsx/xls æ ¼å¼!");
        return false;
      }
      if (!isLt10M) {
        proxy.$modal.msgError("上传文件大小不能超过 10MB!");
        return false;
      }
      return true;
    },
    onChange: (file, fileList) => {
      // noop
    },
    onProgress: (event, file, fileList) => {
      // noop
    },
    onSuccess: (response, file, fileList) => {
      importUpload.isUploading = false;
      if (response?.code === 200) {
        proxy.$modal.msgSuccess("导入成功");
        importUpload.open = false;
        if (importUploadRef.value) {
          importUploadRef.value.clearFiles?.();
        }
        getList();
function handleDelete() {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning('请选择要删除的数据')
    return
  }
  const ids = selectedRows.value.map(r => r.id)
  ElMessageBox.confirm(`确认删除选中的 ${ids.length} æ¡æ•°æ®å—?`, '删除确认', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  })
    .then(() => simpleLedgerDelete(ids))
    .then(res => {
      if (res?.code === 200) {
        ElMessage.success('删除成功')
        getList()
      } else {
        proxy.$modal.msgError(response?.msg || "导入失败");
        ElMessage.error(res?.msg || '删除失败')
      }
    },
    onError: () => {
      importUpload.isUploading = false;
      proxy.$modal.msgError("导入失败,请重试");
    },
  });
    })
    .catch(() => {})
}
  const handleImport = () => {
    importUpload.title = "导入采购台账";
    importUpload.open = true;
    importUpload.isUploading = false;
    if (importUploadRef.value) {
      importUploadRef.value.clearFiles?.();
async function loadAttachments(recordId) {
  try {
    const res = await attachmentList({ recordType: RECORD_TYPE, recordId })
    const list = res?.data || []
    return (Array.isArray(list) ? list : []).map(item => ({
      id: item.id,
      name: item.name || item.originalFilename || '附件',
      url: item.url || item.downloadURL || item.previewURL || ''
    }))
  } catch {
    return []
  }
}
async function openForm(type, row) {
  operationType.value = type
  formRef.value?.resetFields?.()
  formFiles.value = []
  form.value = {
    id: undefined,
    purchaseDate: '',
    paymentDate: '',
    paymentMethod: '',
    amount: undefined,
    purchaseGoods: '',
    invoiceNumber: '',
    remark: ''
  }
  if (type === 'edit' && row?.id) {
    const detail = (await simpleLedgerGet(row.id))?.data || row
    form.value = {
      id: detail.id,
      purchaseDate: detail.purchaseDate || '',
      paymentDate: detail.paymentDate || '',
      paymentMethod: detail.paymentMethod || '',
      amount: detail.amount === null || detail.amount === undefined ? undefined : Number(detail.amount),
      purchaseGoods: detail.purchaseGoods || '',
      invoiceNumber: detail.invoiceNumber || '',
      remark: detail.remark || ''
    }
  };
    formFiles.value = await loadAttachments(detail.id)
  } else {
    form.value.purchaseDate = todayStr()
  }
  formVisible.value = true
}
  // ä¸‹è½½å¯¼å…¥æ¨¡æ¿ï¼ˆå¦‚后端路径不同,可在此处调整)
  const downloadTemplate = () => {
    proxy.download(
      "/purchase/ledger/exportTemplate",
      {},
      "采购台账导入模板.xlsx"
    );
  };
  const submitImportFile = () => {
    importUpload.isUploading = true;
    proxy.$refs["importUploadRef"]?.submit?.();
  };
  const changeDaterange = value => {
    if (value) {
      searchForm.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
      searchForm.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
    } else {
      searchForm.entryDateStart = undefined;
      searchForm.entryDateEnd = undefined;
async function submit() {
  try {
    await formRef.value?.validate?.()
  } catch {
    return
  }
  saving.value = true
  try {
    const payload = { ...form.value }
    if (!payload.id) {
      payload.recorderId = userStore.id
      payload.recorderName = userStore.nickName || userStore.name
      payload.entryDate = todayStr()
    }
    handleQuery();
  };
  const formattedNumber = (row, column, cellValue) => {
    return parseFloat(cellValue).toFixed(2);
  };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  // ä¿å­˜æ¨¡æ¿
  const handleButtonClick = async () => {
    // æ£€æŸ¥æ¨¡æ¿åç§°æ˜¯å¦ä¸ºç©º
    if (!templateName.value || templateName.value.trim() === "") {
      ElMessage({
        message: "请输入模板名称",
        type: "warning",
      });
      return;
    const res = await simpleLedgerSave(payload)
    if (res?.code !== 200) {
      ElMessage.error(res?.msg || '保存失败')
      return
    }
    // å¦‚果是“新增模板”(没有选中已有模板),才需要做重名校验;
    // è‹¥æ˜¯é€‰ä¸­å·²æœ‰æ¨¡æ¿åŽä¿®æ”¹ï¼Œåˆ™å…è®¸ä½¿ç”¨åŽŸåç§°ï¼ˆè§†ä¸ºæ›´æ–°ï¼‰
    if (!currentTemplateId.value) {
      const isDuplicate = checkTemplateNameDuplicate(templateName.value);
      if (isDuplicate) {
        ElMessage({
          message: "模板名称已存在,请更换模板名称",
          type: "warning",
        });
        return;
      }
    }
    // æ£€æŸ¥ä¾›åº”商是否选择
    if (!form.value.supplierId) {
      ElMessage({
        message: "请先选择供应商",
        type: "warning",
      });
      return;
    }
    // æ£€æŸ¥æ˜¯å¦æœ‰äº§å“æ•°æ®
    if (!productData.value || productData.value.length === 0) {
      ElMessage({
        message: "请先添加产品信息",
        type: "warning",
      });
      return;
    }
    try {
      let params = {
        productData: proxy.HaveJson(productData.value),
        supplierId: form.value.supplierId,
        paymentMethod: form.value.paymentMethod,
        recorderId: form.value.recorderId,
        projectName: form.value.projectName,
        templateName: templateName.value.trim(),
      };
      console.log(
        "template params ===>",
        params,
        "currentTemplateId:",
        currentTemplateId.value
      );
      // å¦‚æžœ currentTemplateId æœ‰å€¼ï¼Œè¯´æ˜Žå½“前是“编辑已有模板” â†’ è°ƒç”¨æ›´æ–°æŽ¥å£
      // å¦åˆ™ä¸ºâ€œæ–°å»ºæ¨¡æ¿â€ â†’ è°ƒç”¨æ–°å¢žæŽ¥å£
      let res;
      if (currentTemplateId.value) {
        res = await updatePurchaseTemplate({
          id: currentTemplateId.value,
          ...params,
        });
      } else {
        res = await addPurchaseTemplate(params);
      }
      if (res && res.code === 200) {
        ElMessage({
          message: currentTemplateId.value ? "模板更新成功" : "模板保存成功",
          type: "success",
        });
        // ä¿å­˜æˆåŠŸåŽé‡æ–°èŽ·å–æ¨¡æ¿åˆ—è¡¨
        await getTemplateList();
        // æ¸…空模板名称输入
        templateName.value = "";
        filterInputValue.value = "";
        isTemplateNameDuplicate.value = false;
        // ä¿å­˜/更新完成后,重置当前模板ID
        currentTemplateId.value = null;
      } else {
        ElMessage({
          message: res?.msg || "模板保存失败",
          type: "error",
        });
      }
    } catch (error) {
      console.error("保存模板失败:", error);
      ElMessage({
        message: "模板保存失败,请稍后重试",
        type: "error",
      });
    }
  };
  // å­è¡¨åˆè®¡æ–¹æ³•
  const summarizeChildrenTable = param => {
    return proxy.summarizeTable(
      param,
      [
        "taxInclusiveUnitPrice",
        "taxInclusiveTotalPrice",
        "taxExclusiveTotalPrice",
        "ticketsNum",
        "ticketsAmount",
        "futureTickets",
        "futureTicketsAmount",
      ],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      }
    );
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    const { entryDate, ...rest } = searchForm;
    purchaseListPage({ ...rest, ...page })
      .then(res => {
        tableLoading.value = false;
        // tableData.value = res.data.records;
        tableData.value = res.data.records.map(record => ({
          ...record,
        }));
        // åˆå§‹åŒ–子数据数组
        tableData.value.forEach(item => {
          item.children = [];
        });
        total.value = res.data.total;
        expandedRowKeys.value = [];
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  const productSelected = selectedRows => {
    productSelectedRows.value = selectedRows;
  };
  const expandedRowKeys = ref([]);
  // å±•开行
  const expandChange = async (row, expandedRows) => {
    if (expandedRows.length > 0) {
      expandedRowKeys.value = [];
    const recordId = res.data || form.value.id
    const blobs = formFiles.value
      .map(f => ({
        id: f.id ?? f.rawData?.id,
        application: 'file',
        name: f.name,
        url: f.url,
        downloadURL: f.url
      }))
      .filter(f => f.id)
    if (blobs.length > 0) {
      try {
        const res = await productList({ salesLedgerId: row.id, type: 2 });
        const index = tableData.value.findIndex(item => item.id === row.id);
        if (index > -1) {
          tableData.value[index].children = res.data || [];
          expandedRowKeys.value.push(row.id);
        }
      } catch (error) {
        console.error("加载产品列表失败:", error);
        proxy.$modal.msgError("加载产品列表失败");
        // å±•开失败时,移除展开状态
        const index = expandedRows.findIndex(item => item.id === row.id);
        if (index > -1) {
          expandedRows.splice(index, 1);
        }
      }
    } else {
      expandedRowKeys.value = [];
    }
  };
  // ä¸»è¡¨åˆè®¡æ–¹æ³•
  const summarizeMainTable = param => {
    return proxy.summarizeTable(param, ["contractAmount", "netContractAmount"]);
  };
  const formatMoney = value => {
    if (value === null || value === undefined || value === "") {
      return "--";
    }
    const num = Number(value);
    return Number.isFinite(num) ? num.toFixed(2) : "--";
  };
  // å­è¡¨åˆè®¡æ–¹æ³•
  const summarizeProTable = param => {
    return proxy.summarizeTable(param, [
      "taxInclusiveUnitPrice",
      "taxInclusiveTotalPrice",
      "taxExclusiveTotalPrice",
    ]);
  };
  // æ‰“开弹框
  const openForm = async (type, row) => {
    // ç¼–辑时检查入库状态,完全入库时不能编辑
    if (type === "edit" && row) {
      if (row.stockInStatus === "完全入库") {
        proxy.$modal.msgWarning("完全入库状态的记录不能编辑");
        return;
      }
    }
    await getTemplateList();
    await userListNoPage().then(res => {
      userList.value = res.data;
    });
    operationType.value = type;
    form.value = {};
    productData.value = [];
    fileList.value = [];
    templateName.value = "";
    filterInputValue.value = "";
    isTemplateNameDuplicate.value = false;
    try {
      // å¹¶è¡ŒåŠ è½½åŸºç¡€æ•°æ®
      const [salesRes, supplierRes] = await Promise.all([
        getSalesNo(),
        getOptions(),
      ]);
      salesContractList.value = salesRes || [];
      // ä¾›åº”商过滤出isWhite=0 çš„æ•°æ®
      supplierList.value = (supplierRes.data || []).filter(
        item => item.isWhite === 0
      );
      form.value.entryDate = getCurrentDate();
      if (type === "edit" && row?.id) {
        // ç¼–辑时加载数据
        currentId.value = row.id;
        try {
          const purchaseRes = await getPurchaseById({ id: row.id, type: 2 });
          form.value = { ...purchaseRes, stockInStatus: row.stockInStatus };
          fileList.value = purchaseRes.storageBlobVOS || [];
          // ä½¿ç”¨ productList æŽ¥å£èŽ·å–äº§å“åˆ—è¡¨ï¼Œä»¥èŽ·å–å…¥åº“å®¡æ ¸çŠ¶æ€
          const productRes = await productList({
            salesLedgerId: row.id,
            type: 2,
          });
          productData.value = productRes.data || [];
        } catch (error) {
          console.error("加载采购台账数据失败:", error);
          proxy.$modal.msgError("加载数据失败");
          return;
        }
      }
      if (form.value.salesLedgerId == -1) {
        form.value.salesLedgerId = null;
      }
      console.log(form.value, "form.value===========");
      dialogFormVisible.value = true;
    } catch (error) {
      console.error("打开表单失败:", error);
      proxy.$modal.msgError("加载基础数据失败");
    }
  };
  // ä¸Šä¼ å‰æ ¡æ£€
  function handleBeforeUpload(file) {
    // æ ¡æ£€æ–‡ä»¶å¤§å°
    if (file.size > 1024 * 1024 * 10) {
      proxy.$modal.msgError("上传文件大小不能超过10MB!");
      return false;
    }
    proxy.$modal.loading("正在上传文件,请稍候...");
    return true;
  }
  // ä¸Šä¼ å¤±è´¥
  function handleUploadError(err) {
    proxy.$modal.msgError("上传文件失败");
    proxy.$modal.closeLoading();
  }
  // ä¸Šä¼ æˆåŠŸå›žè°ƒ
  function handleUploadSuccess(res, file, uploadFiles) {
    proxy.$modal.closeLoading();
    if (res.code === 200) {
      file.tempId = res.data.tempId;
      proxy.$modal.msgSuccess("上传成功");
    } else {
      proxy.$modal.msgError(res.msg);
      proxy.$refs.fileUpload.handleRemove(file);
    }
  }
  // ç§»é™¤æ–‡ä»¶
  async function handleRemove(file) {
    if (!file?.id) {
      return;
    }
    console.log("handleRemove", file.id);
    if (file.size > 1024 * 1024 * 10) {
      // ä»…前端清理,不调用删除接口和提示
      return;
    }
    if (operationType.value === "edit" && file.id) {
      try {
        await delLedgerFile([file.id]);
        proxy.$modal.msgSuccess("删除成功");
      } catch (error) {
        console.error("删除文件失败:", error);
        proxy.$modal.msgError("删除文件失败");
      }
    }
  }
  // æäº¤è¡¨å•
  const submitForm = () => {
    proxy.$refs["formRef"].validate(async valid => {
      if (valid) {
        if (productData.value.length > 0) {
          // æ–°å¢žæ—¶ï¼Œéœ€è¦ä»Žæ¯ä¸ªäº§å“å¯¹è±¡ä¸­åˆ é™¤ id å­—段
          let processedProductData = productData.value;
          if (operationType.value === "add") {
            processedProductData = productData.value.map(product => {
              const { id, ...rest } = product;
              return rest;
            });
          }
          form.value.productData = proxy.HaveJson(processedProductData);
        } else {
          proxy.$modal.msgWarning("请添加产品信息");
          return;
        }
        form.value.storageBlobDTOS = fileList.value;
        form.value.type = 2;
        // å¦‚æžœsalesLedgerId为空,则不传递salesContractNo
        if (!form.value.salesLedgerId) {
          form.value.salesContractNo = "";
        }
        // æ–°å¢žæ—¶ä¸ä¼ é€’id
        const submitData = { ...form.value };
        if (operationType.value === "add") {
          delete submitData.id;
        }
        // å¦‚果采购合同号为空,则根据录入日期自动生成
        if (!submitData.purchaseContractNumber) {
          try {
            const purchaseNoRes = await createPurchaseNo(submitData.entryDate);
            if (purchaseNoRes?.data) {
              submitData.purchaseContractNumber = purchaseNoRes.data;
            }
          } catch (error) {
            console.error("生成采购合同号失败:", error);
            proxy.$modal.msgWarning("生成采购合同号失败");
            return;
          }
        }
        addOrEditPurchase(submitData).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
          getList();
        });
      }
    });
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
  };
  // æ‰“开产品弹框
  const openProductForm = async (type, row, index) => {
    // ç¼–辑时检查产品入库审核状态,完全入库时不能编辑
    if (type === "edit" && row && row.stockInApprovalStatus === "完全入库") {
      proxy.$modal.msgWarning("完全入库状态的产品不能编辑");
      return;
    }
    productOperationType.value = type;
    productOperationIndex.value = index;
    productForm.value = {};
    proxy.resetForm("productFormRef");
    productFormVisible.value = true;
    // å…ˆèŽ·å–äº§å“é€‰é¡¹ï¼Œç¡®ä¿æ•°æ®åŠ è½½å®Œæˆ
    await getProductOptions();
    // ç­‰å¾… DOM æ›´æ–°
    await nextTick();
    if (type === "add") {
      productForm.value.isChecked = false;
    }
    if (type === "edit") {
      // å¤åˆ¶è¡Œæ•°æ®
      productForm.value = { ...row };
      // å¦‚果是从模板加载的数据,可能没有 productId å’Œ productModelId
      // éœ€è¦æ ¹æ® productCategory å’Œ specificationModel æ¥æŸ¥æ‰¾å¯¹åº”çš„ ID
      if (!productForm.value.productId && productForm.value.productCategory) {
        // æ ¹æ® productCategory æŸ¥æ‰¾ productId
        const findProductIdByCategory = (nodes, categoryName) => {
          for (let i = 0; i < nodes.length; i++) {
            if (nodes[i].label === categoryName) {
              return nodes[i].value;
            }
            if (nodes[i].children && nodes[i].children.length > 0) {
              const found = findProductIdByCategory(
                nodes[i].children,
                categoryName
              );
              if (found) return found;
            }
          }
          return null;
        };
        const productId = findProductIdByCategory(
          productOptions.value,
          productForm.value.productCategory
        );
        if (productId) {
          productForm.value.productId = productId;
          // èŽ·å–åž‹å·åˆ—è¡¨å¹¶ç­‰å¾…å®Œæˆ
          const modelRes = await modelList({ id: productId });
          modelOptions.value = modelRes;
          // ç­‰å¾… DOM æ›´æ–°
          await nextTick();
          // æ ¹æ® specificationModel æŸ¥æ‰¾ productModelId
          if (
            productForm.value.specificationModel &&
            modelOptions.value.length > 0
          ) {
            const modelItem = modelOptions.value.find(
              item => item.model === productForm.value.specificationModel
            );
            if (modelItem) {
              productForm.value.productModelId = modelItem.id;
              // è®¾ç½®è§„格型号和单位
              getProductModel(modelItem.id);
            }
          }
        }
      } else if (productForm.value.productId) {
        // å¦‚果有 productId,正常加载型号列表
        await getModels(productForm.value.productId);
        // ç­‰å¾… DOM æ›´æ–°
        await nextTick();
        if (productForm.value.productModelId) {
          getProductModel(productForm.value.productModelId);
        }
      }
      // æœ€åŽå†ç­‰å¾…一次 DOM æ›´æ–°ï¼Œç¡®ä¿æ‰€æœ‰æ•°æ®éƒ½å·²è®¾ç½®
      await nextTick();
    }
  };
  const getProductOptions = () => {
    return productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
      return res;
    });
  };
  const getModels = value => {
    if (value) {
      productForm.value.productCategory =
        findNodeById(productOptions.value, value) || "";
      return modelList({ id: value }).then(res => {
        modelOptions.value = res;
        return res;
      });
    } else {
      productForm.value.productCategory = "";
      modelOptions.value = [];
      return Promise.resolve([]);
    }
  };
  const getProductModel = value => {
    const index = modelOptions.value.findIndex(item => item.id === value);
    if (index !== -1) {
      productForm.value.specificationModel = modelOptions.value[index].model;
      productForm.value.unit = modelOptions.value[index].unit;
    } else {
      productForm.value.specificationModel = null;
      productForm.value.unit = null;
    }
  };
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹çš„label
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œç›´æŽ¥è¿”å›žï¼ˆå·²ç»æ˜¯label字符串)
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  // æäº¤äº§å“è¡¨å•
  const submitProduct = () => {
    proxy.$refs["productFormRef"].validate(valid => {
      if (valid) {
        if (operationType.value === "edit") {
          submitProductEdit();
        } else {
          if (productOperationType.value === "add") {
            productData.value.push({ ...productForm.value });
            console.log("productData.value---", productData.value);
          } else {
            productData.value[productOperationIndex.value] = {
              ...productForm.value,
            };
          }
          closeProductDia();
        }
      }
    });
  };
  const submitProductEdit = () => {
    productForm.value.salesLedgerId = currentId.value;
    productForm.value.type = 2;
    addOrUpdateSalesLedgerProduct(productForm.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
      closeProductDia();
      // ä½¿ç”¨ productList æŽ¥å£åˆ·æ–°äº§å“åˆ—表,以获取入库审核状态
      productList({ salesLedgerId: currentId.value, type: 2 }).then(res => {
        productData.value = res.data || [];
      });
    });
  };
  // åˆ é™¤äº§å“
  const deleteProduct = () => {
    if (productData.value.length <= 1) {
      proxy.$modal.msgWarning("采购台账至少需要保留一条产品数据");
      return;
    }
    if (productSelectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    // æ£€æŸ¥é€‰ä¸­çš„产品中是否有完全入库的
    const hasFullyStocked = productSelectedRows.value.some(
      row => row.stockInApprovalStatus === "完全入库"
    );
    if (hasFullyStocked) {
      proxy.$modal.msgWarning("选中的产品中包含完全入库的产品,无法删除");
      return;
    }
    if (operationType.value === "add") {
      productData.value = productData.value.filter(
        item => !productSelectedRows.value.includes(item)
      );
      productSelectedRows.value = [];
    } else {
      let ids = [];
      if (productSelectedRows.value.length > 0) {
        ids = productSelectedRows.value.map(item => item.id);
      }
      ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          delProduct(ids).then(res => {
            proxy.$modal.msgSuccess("删除成功");
            closeProductDia();
            // ä½¿ç”¨ productList æŽ¥å£åˆ·æ–°äº§å“åˆ—表,以获取入库审核状态
            productList({ salesLedgerId: currentId.value, type: 2 }).then(res => {
              productData.value = res.data || [];
            });
          });
        await createAttachment({
          application: 'file',
          recordType: RECORD_TYPE,
          recordId,
          storageBlobDTOs: blobs
        })
        .catch(() => {
          proxy.$modal.msg("已取消");
        });
      } catch {
        ElMessage.error('附件保存失败,请稍后重试')
      }
    }
  };
  // å…³é—­äº§å“å¼¹æ¡†
  const closeProductDia = () => {
    proxy.resetForm("productFormRef");
    productFormVisible.value = false;
  };
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download("/purchase/ledger/export", {}, "采购台账.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // åˆ é™¤
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value
        .filter(item => item.salesLedgerId === null)
        .map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        delPurchase(ids).then(res => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const handleBatchGenerate = async () => {
    if (selectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    const ids = selectedRows.value.map((item) => item.id);
    ElMessageBox.confirm("确认批量生成数据?", "批量生成", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "info",
    })
      .then(() => {
        proxy.$modal.loading("正在批量生成数据,请稍候...");
        batchGeneratePurchaseInboundSteps({ ids })
          .then((res) => {
            proxy.$modal.msgSuccess("批量生成成功");
            getList();
          })
          .catch(() => {
            proxy.$modal.msgError("批量生成失败");
          })
          .finally(() => {
            proxy.$modal.closeLoading();
          });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º YYYY-MM-DD
  function getCurrentDate() {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, "0"); // æœˆä»½ä»Ž0开始
    const day = String(today.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
    ElMessage.success('保存成功')
    formVisible.value = false
    getList()
  } finally {
    saving.value = false
  }
}
  const mathNum = () => {
    if (!productForm.value.taxRate) {
      proxy.$modal.msgWarning("请先选择税率");
      return;
    }
    if (!productForm.value.taxInclusiveUnitPrice) {
      return;
    }
    if (!productForm.value.quantity) {
      return;
    }
    // å«ç¨Žæ€»ä»·è®¡ç®—
    productForm.value.taxInclusiveTotalPrice =
      proxy.calculateTaxIncludeTotalPrice(
        productForm.value.taxInclusiveUnitPrice,
        productForm.value.quantity
      );
    if (productForm.value.taxRate) {
      // ä¸å«ç¨Žæ€»ä»·è®¡ç®—
      productForm.value.taxExclusiveTotalPrice =
        proxy.calculateTaxExclusiveTotalPrice(
          productForm.value.taxInclusiveTotalPrice,
          productForm.value.taxRate
        );
    }
  };
  const reverseMathNum = field => {
    if (!productForm.value.taxRate) {
      proxy.$modal.msgWarning("请先选择税率");
      return;
    }
    const taxRate = Number(productForm.value.taxRate);
    if (!taxRate) return;
function openFileDialog(row) {
  currentRowId.value = row.id
  fileDialogVisible.value = true
}
    // ç¡®ä¿è¾“入值不为负数
    if (
      field === "taxInclusiveTotalPrice" ||
      field === "taxExclusiveTotalPrice"
    ) {
      const value = Number(productForm.value[field]);
      if (value < 0) {
        productForm.value[field] = "0";
        proxy.$modal.msgWarning("值不能小于0");
        return;
      }
    }
    if (field === "taxInclusiveTotalPrice") {
      // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œæ•°é‡ï¼Œåç®—含税单价
      if (productForm.value.quantity) {
        productForm.value.taxInclusiveUnitPrice = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.quantity)
        ).toFixed(2);
        // ç¡®ä¿ç»“果不为负数
        if (Number(productForm.value.taxInclusiveUnitPrice) < 0) {
          productForm.value.taxInclusiveUnitPrice = "0";
        }
      }
      // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œå«ç¨Žå•价,反算数量
      else if (productForm.value.taxInclusiveUnitPrice) {
        productForm.value.quantity = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.taxInclusiveUnitPrice)
        ).toFixed(3);
        // ç¡®ä¿ç»“果不为负数
        if (Number(productForm.value.quantity) < 0) {
          productForm.value.quantity = "0";
        }
      }
      // åç®—不含税总价
      productForm.value.taxExclusiveTotalPrice = (
        Number(productForm.value.taxInclusiveTotalPrice) /
        (1 + taxRate / 100)
      ).toFixed(2);
      // ç¡®ä¿ç»“果不为负数
      if (Number(productForm.value.taxExclusiveTotalPrice) < 0) {
        productForm.value.taxExclusiveTotalPrice = "0";
      }
    } else if (field === "taxExclusiveTotalPrice") {
      // åç®—含税总价
      productForm.value.taxInclusiveTotalPrice = (
        Number(productForm.value.taxExclusiveTotalPrice) *
        (1 + taxRate / 100)
      ).toFixed(2);
      // ç¡®ä¿ç»“果不为负数
      if (Number(productForm.value.taxInclusiveTotalPrice) < 0) {
        productForm.value.taxInclusiveTotalPrice = "0";
      }
      // å·²çŸ¥æ•°é‡ï¼Œåç®—含税单价
      if (productForm.value.quantity) {
        productForm.value.taxInclusiveUnitPrice = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.quantity)
        ).toFixed(2);
        // ç¡®ä¿ç»“果不为负数
        if (Number(productForm.value.taxInclusiveUnitPrice) < 0) {
          productForm.value.taxInclusiveUnitPrice = "0";
        }
      }
      // å·²çŸ¥å«ç¨Žå•价,反算数量
      else if (productForm.value.taxInclusiveUnitPrice) {
        productForm.value.quantity = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.taxInclusiveUnitPrice)
        ).toFixed(3);
        // ç¡®ä¿ç»“果不为负数
        if (Number(productForm.value.quantity) < 0) {
          productForm.value.quantity = "0";
        }
      }
    }
  };
  // é”€å”®åˆåŒé€‰æ‹©æ”¹å˜æ–¹æ³•
  const salesLedgerChange = async row => {
    console.log("row", row);
    var index = salesContractList.value.findIndex(item => item.id == row);
    console.log("index", index);
    if (index > -1) {
      await querygProductInfoByContractNo();
    }
  };
  const querygProductInfoByContractNo = async () => {
    const { code, data } = await getProductInfoByContractNo({
      contractNo: form.value.salesLedgerId,
    });
    if (code == 200) {
      productData.value = data || [];
    }
  };
  // èŽ·å–æ¨¡æ¿ä¿¡æ¯
  const getTemplateList = async () => {
    let res = await getPurchaseTemplateList();
    if (res && res.code === 200 && Array.isArray(res.data)) {
      templateList.value = res.data;
    }
  };
  // æ‰“开附件弹框
  const openFileDialog = async row => {
    recordId.value = row.id;
    fileListDialogVisible.value = true;
  };
  // åˆ é™¤æ¨¡æ¿
  const handleDeleteTemplate = async item => {
    if (!item.id) {
      proxy.$modal.msgWarning("无法删除该模板");
      return;
    }
    try {
      await ElMessageBox.confirm(
        `确定要删除模板"${item.templateName}"吗?`,
        "删除确认",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      );
      const res = await delPurchaseTemplate([item.id]);
      if (res && res.code === 200) {
        ElMessage({
          message: "删除成功",
          type: "success",
        });
        // å¦‚果删除的是当前选中的模板,清空选择
        if (templateName.value === item.templateName) {
          templateName.value = "";
          filterInputValue.value = "";
        }
        // é‡æ–°èŽ·å–æ¨¡æ¿åˆ—è¡¨
        await getTemplateList();
      } else {
        ElMessage({
          message: res?.msg || "删除失败",
          type: "error",
        });
      }
    } catch (error) {
      if (error !== "cancel") {
        console.error("删除模板失败:", error);
        ElMessage({
          message: "删除失败,请稍后重试",
          type: "error",
        });
      }
    }
  };
  onMounted(() => {
    getList();
    getTemplateList();
  });
onMounted(() => {
  getList()
})
</script>
<style scoped lang="scss">
  .invalid-row {
    opacity: 0.6;
    background-color: #f5f7fa;
  }
<style scoped>
.search_form {
  margin-bottom: 12px;
}
  .el-row {
    justify-content: space-between;
    align-items: center;
  }
.toolbar {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-bottom: 18px;
}
  .no-arrow-select {
    --el-select-suffix-icon-color: transparent; /* éšè—é»˜è®¤ä¸‹æ‹‰ç®­å¤´ */
  }
  .select-button-group {
    display: flex;
    align-items: center;
  }
  // å®¡æ‰¹äººèŠ‚ç‚¹å®¹å™¨æ ·å¼
  .approver-nodes-container {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    padding: 16px;
    background-color: #f8f9fa;
    border-radius: 4px;
    border: 1px solid #e4e7ed;
  }
  .approver-node-item {
    flex: 0 0 calc(33.333% - 12px);
    min-width: 200px;
    padding: 12px;
    background-color: #fff;
    border-radius: 4px;
    border: 1px solid #dcdfe6;
    transition: all 0.3s;
    &:hover {
      border-color: #409eff;
      box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
    }
  }
  .approver-node-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 8px;
  }
  .approver-node-label {
    font-size: 13px;
    font-weight: 500;
    color: #606266;
  }
  @media (max-width: 1200px) {
    .approver-node-item {
      flex: 0 0 calc(50% - 8px);
    }
  }
  @media (max-width: 768px) {
    .approver-node-item {
      flex: 0 0 100%;
    }
  }
  // åˆ é™¤å›¾æ ‡æ ·å¼
  .delete-icon {
    transition: all 0.3s;
    &:hover {
      color: #f56c6c !important;
      transform: scale(1.2);
    }
  }
:deep(.el-form-item) {
  margin-bottom: 12px;
}
</style>
src/views/projectManagement/Management/components/formDia.vue
@@ -40,123 +40,90 @@
            </el-col>
            <el-col :span="6">
              <el-form-item label="客户名称" prop="customerName">
                <el-input v-model="form.customerName" placeholder="请输入" clearable />
                <el-select
                  v-model="form.customerName"
                  placeholder="请选择客户"
                  clearable
                  filterable
                  :disabled="isFromContract"
                  style="width: 100%"
                  @change="handleCustomerChange"
                >
                  <el-option
                    v-for="item in customerOptions"
                    :key="item.id ?? item.customerName"
                    :label="item.customerName"
                    :value="item.customerName"
                  />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="6">
              <el-form-item label="立项日期" prop="setupDate">
                <el-date-picker
                  v-model="form.setupDate"
                  type="date"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
              <el-form-item label="项目分类" prop="projectClassification">
                <el-select
                  v-model="form.projectClassification"
                  placeholder="请选择"
                  style="width: 100%"
                  clearable
                />
                  filterable
                  allow-create
                  default-first-option
                  style="width: 100%"
                >
                  <el-option
                    v-for="c in projectClassificationOptions"
                    :key="c"
                    :label="c"
                    :value="c"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="项目来源" prop="projectSource">
                <el-input v-model="form.projectSource" placeholder="请输入" clearable />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="立项人" prop="creatorName">
                <el-input v-model="form.creatorName" placeholder="请输入" clearable />
                <el-select
                  v-model="form.projectSource"
                  placeholder="请选择"
                  clearable
                  filterable
                  allow-create
                  default-first-option
                  style="width: 100%"
                >
                  <el-option label="手动添加" value="手动添加" />
                  <el-option label="业务审批" value="业务审批" />
                  <el-option label="其他" value="其他" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="6">
              <el-form-item label="预计工期(天)" prop="estimatedDays">
                <el-input-number v-model="form.estimatedDays" :min="0" controls-position="right" style="width: 100%" />
          <el-row v-if="isFromContract" :gutter="20">
            <el-col :span="8">
              <el-form-item label="合同金额(元)">
                <el-input :model-value="form.projectAmount" disabled />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="计划开始日期" prop="planStartDate">
                <el-date-picker
                  v-model="form.planStartDate"
                  type="date"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  placeholder="请选择"
                  style="width: 100%"
                  clearable
                />
            <el-col :span="8">
              <el-form-item label="合同日期">
                <el-input :model-value="form.orderDate" disabled />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="计划完成日期" prop="planEndDate">
                <el-date-picker
                  v-model="form.planEndDate"
                  type="date"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  placeholder="请选择"
                  style="width: 100%"
                  clearable
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="6">
              <el-form-item label="项目类型" prop="projectManagementPlanId">
                <el-select v-model="form.projectManagementPlanId" placeholder="请选择" clearable style="width: 100%">
                  <el-option v-for="opt in projectTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="项目金额" prop="projectAmount">
                <el-input-number v-model="form.projectAmount" :min="0" controls-position="right" style="width: 100%" />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="审核状态" prop="auditStatus">
                <el-select v-model="form.auditStatus" placeholder="请选择" clearable style="width: 100%">
                  <el-option v-for="d in project_management" :key="d.value" :label="d.label" :value="d.value" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="10" >
              <el-col :span="24">
                <el-upload
                  v-model:file-list="fileList"
                  :action="upload.url"
                  :headers="upload.headers"
                  multiple
                  :disabled="isView"
                  :before-upload="beforeUpload"
                  :on-success="handleUploadSuccess"
                  :on-error="handleUploadError"
                  name="files"
                  :on-remove="handleRemove"
                >
                  <el-button type="primary" :disabled="isView">上传文件</el-button>
                </el-upload>
                <div v-if="existingAttachments.length > 0" class="attachment-list">
                  <div
                    v-for="(att, idx) in existingAttachments"
                    :key="att.id || att.url || idx"
                    class="attachment-item"
                  >
                    <el-icon><Document /></el-icon>
                    <span class="attachment-name">{{ att.name || att.fileName || att.url || '附件' }}</span>
                    <el-button link type="primary" size="small" @click="downloadAttachment(att)">下载</el-button>
                  </div>
                </div>
                <el-form-item label="附件" prop="attachmentIds">
                  <AttachmentUploadFile
                    v-model:file-list="fileList"
                    :disabled="isView"
                    :limit="10"
                  />
                </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
          <el-row v-if="false" :gutter="20">
            <el-col :span="24">
              <el-form-item label="备注" prop="remark">
                <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入" maxlength="100" show-word-limit />
@@ -166,49 +133,7 @@
        </div>
      </div>
      <div class="section">
        <div class="section-header" @click="toggleSection('product')">
          <div class="section-title">
            <span class="section-bar" />
            <span>产品信息</span>
          </div>
          <div class="section-actions" @click.stop>
            <el-button v-if="!isView" type="primary" @click="openProductForm('add')">添加</el-button>
            <el-button v-if="!isView" plain type="danger" @click="deleteProduct">删除</el-button>
            <el-icon class="toggle-icon" @click="toggleSection('product')">
              <ArrowDown v-if="sectionCollapsed.product" />
              <ArrowUp v-else />
            </el-icon>
          </div>
        </div>
        <div v-show="!sectionCollapsed.product" class="section-body">
          <el-table
            :data="productData"
            border
            show-summary
            :summary-method="summarizeProductTable"
            @selection-change="productSelected"
          >
            <el-table-column v-if="!isView" align="center" type="selection" width="55" />
            <el-table-column align="center" label="序号" type="index" width="60" />
            <el-table-column label="产品大类" prop="productCategory" />
            <el-table-column label="规格型号" prop="specificationModel" />
            <el-table-column label="单位" prop="unit" />
            <el-table-column label="数量" prop="quantity" />
            <el-table-column label="税率(%)" prop="taxRate" />
            <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
            <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
            <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
            <el-table-column v-if="!isView" fixed="right" label="操作" min-width="60" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row, scope.$index)">编辑</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
      <div class="section">
      <div v-if="false" class="section">
        <div class="section-header" @click="toggleSection('team')">
          <div class="section-title">
            <span class="section-bar" />
@@ -381,7 +306,7 @@
        </div>
      </div> -->
      <div class="section">
      <div v-if="false" class="section">
        <div class="section-header" @click="toggleSection('address')">
          <div class="section-title">
            <span class="section-bar" />
@@ -421,7 +346,7 @@
        </div>
      </div>
      <div class="section">
      <div v-if="false" class="section">
        <div class="section-header" @click="toggleSection('contact')">
          <div class="section-title">
            <span class="section-bar" />
@@ -639,9 +564,10 @@
<script setup name="ProjectManagementFormDia">
import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue'
import { ArrowDown, ArrowUp, Delete, Plus, Document } from '@element-plus/icons-vue'
import { ArrowDown, ArrowUp, Delete, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getToken } from '@/utils/auth'
import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue'
import { listCustomer } from '@/api/basicData/customer.js'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import { listPlan } from '@/api/projectManagement/projectType'
@@ -659,13 +585,11 @@
const operationType = ref('add')
const formRef = ref()
const fileList = ref([])
const existingAttachments = ref([])
const upload = reactive({
  url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
  headers: { Authorization: 'Bearer ' + getToken() }
})
const customerOptions = ref([])
const projectTypeOptions = ref([])
// é¡¹ç›®åˆ†ç±»ï¼šå…ˆå†™æ­»å‡ ä¸ªï¼ŒåŽç»­å¯åˆ‡æ¢ä¸ºå­—å…¸
const projectClassificationOptions = ['数字化转型', '智能制造', '系统集成', '其他']
const roleOptions = ref([])
const userOptions = ref([])
const productData = ref([])
@@ -725,8 +649,9 @@
    projectName: '',
    customerName: '',
    parentProjectName: '',
    setupDate: '',
    projectSource: '',
    setupDate: todayStr(),
    projectSource: '手动添加',
    projectClassification: '',
    creatorName: '',
    billStatus: '',
    projectStage: '',
@@ -771,6 +696,8 @@
})
const isView = computed(() => operationType.value === 'view')
// åˆåŒç”Ÿæˆçš„项目:来源为业务审批,合同信息只读展示
const isFromContract = computed(() => operationType.value !== 'add' && form.value.projectSource === '业务审批')
const dialogTitle = computed(() => {
  if (operationType.value === 'add') return '新增项目'
  if (operationType.value === 'edit') return '编辑项目'
@@ -813,6 +740,12 @@
  sectionCollapsed[key] = !sectionCollapsed[key]
}
function todayStr() {
  const d = new Date()
  const pad = n => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
function resetFormData() {
  Object.assign(form.value, {
    id: undefined,
@@ -831,8 +764,9 @@
    projectName: '',
    customerName: '',
    parentProjectName: '',
    setupDate: '',
    projectSource: '',
    setupDate: todayStr(),
    projectSource: '手动添加',
    projectClassification: '',
    creatorName: '',
    billStatus: '',
    projectStage: '',
@@ -1116,6 +1050,11 @@
    const res = await listPlan({ current: 1, size: 999 })
    const records = res?.data?.records || res?.records || res?.rows || []
    projectTypeOptions.value = records.map(item => ({ label: item.name, value: item.id }))
    // æœªæŒ‡å®šè®¡åˆ’时,默认使用“项目申报六阶段”计划
    if (!form.value.projectManagementPlanId) {
      const defaultPlan = records.find(item => item.name === '项目申报六阶段') || records[0]
      if (defaultPlan) form.value.projectManagementPlanId = defaultPlan.id
    }
  } catch {
    projectTypeOptions.value = []
  }
@@ -1142,6 +1081,21 @@
  } catch {
    userOptions.value = []
  }
}
async function loadCustomerOptions() {
  try {
    const res = await listCustomer({ current: -1, size: -1, type: 0 })
    const records = res?.data?.records || res?.data || res?.records || []
    customerOptions.value = Array.isArray(records) ? records : []
  } catch {
    customerOptions.value = []
  }
}
function handleCustomerChange(customerName) {
  const customer = customerOptions.value.find(item => item.customerName === customerName)
  form.value.clientId = customer?.id ?? null
}
function addTeamRow() {
@@ -1191,41 +1145,10 @@
  if (index > -1) form.value.addressList.splice(index, 1)
}
function beforeUpload() {
  if (isView.value) return false
  proxy.$modal?.loading?.('正在上传文件,请稍候...')
  return true
}
function handleUploadError() {
  proxy.$modal?.closeLoading?.()
  ElMessage.error('上传文件失败')
}
function handleUploadSuccess(res, file) {
  console.log(res, file)
  proxy.$modal?.closeLoading?.()
  if (res?.code !== 200) {
    ElMessage.error(res?.msg || '上传失败')
    return
  }
  const attachmentId = res?.data?.[0]?.id ?? ""
  if (!attachmentId) return
  form.value.attachmentIds.push(attachmentId)
  console.log(form.value.attachmentIds)
  ElMessage.success('上传成功')
}
function handleRemove(file) {
  const attachmentId = file?.attachmentId
  if (!attachmentId) return
  form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
}
async function openDialog(payload = {}) {
  operationType.value = payload.operationType || 'add'
  resetFormData()
  await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), getProductOptions()])
  await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), loadCustomerOptions(), getProductOptions()])
  if (payload.row?.id) {
    try {
      const res = await getProject(payload.row.id)
@@ -1261,15 +1184,18 @@
        estimatedDays: Number(info.estimatedDays) || computeEstimatedDays(info.planStartTime, info.planEndTime) || 0,
        projectName: info.title ?? '',
        customerName: info.clientName ?? '',
        clientId: info.clientId ?? undefined,
        parentProjectName: info.projectManagementInfoParentName ?? '',
        setupDate: info.establishTime ?? '',
        projectSource: info.source ?? '',
        projectClassification: info.projectClassification ?? '',
        creatorName: info.managerName ?? '',
        billStatus: normalizeDictValue(info.status),
        projectStage: normalizeDictValue(info.stage ?? info.projectStage),
        planStartDate: info.planStartTime ?? '',
        planEndDate: info.planEndTime ?? '',
        projectAmount: info.orderAmount ?? 0,
        orderDate: info.orderDate ?? '',
        auditStatus: normalizeDictValue(info.reviewStatus),
        remark: info.remark ?? '',
        attachmentIds: Array.isArray(info.attachmentIds) ? info.attachmentIds : [],
@@ -1302,7 +1228,7 @@
        contactRemark: contractInfo.rappresentanteLegale ?? ''
      })
      existingAttachments.value = Array.isArray(info.attachmentList)
      fileList.value = Array.isArray(info.attachmentList)
        ? info.attachmentList.map(a => ({
            id: a.id ?? a.fileId,
            name: a.fileName ?? a.name,
@@ -1360,45 +1286,6 @@
    return
  }
  await formRef.value?.validate?.()
  if (!productData.value || productData.value.length === 0) {
    proxy.$modal?.msgWarning?.('请添加产品信息')
    return
  }
  const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label
  const teamList = (form.value.teamList || []).map(t => ({
    userId: t.memberId,
    userName: findLabel(userOptions.value, t.memberId),
    userRoleId: t.roleId,
    userRoleName: findLabel(roleOptions.value, t.roleId),
    joinTime: t.enterDate,
    departTime: t.leaveDate,
    contact: t.phone,
    remark: t.remark
  }))
  const shippingRow = (form.value.addressList || [])[0] || {}
  const shippingAddress = {
    id: undefined,
    consignee: shippingRow.receiver,
    contract: shippingRow.phone,
    address: shippingRow.address
  }
  const contractInfo = {
    id: undefined,
    name: form.value.contactName,
    sex: form.value.contactGender === '1' ? '男' : form.value.contactGender === '2' ? '女' : '',
    birthday: form.value.contactBirthday,
    department: form.value.contactDept,
    job: form.value.contactJob,
    phoneNumber: form.value.contactMobile,
    email: form.value.contactEmail,
    qq: form.value.contactQq,
    lineaFissa: form.value.contactWorkWechat,
    wx: form.value.contactWechat,
    origineEtnica: form.value.contactAddress,
    rappresentanteLegale: form.value.contactRemark
  }
  const info = {
    id: form.value.id ?? null,
    no: form.value.billNo,
@@ -1407,8 +1294,9 @@
    clientName: form.value.customerName,
    projectManagementInfoParentId: form.value.parentProjectId ?? null,
    projectManagementPlanId: form.value.projectManagementPlanId ?? null,
    establishTime: form.value.setupDate,
    establishTime: form.value.setupDate || todayStr(),
    source: form.value.projectSource,
    projectClassification: form.value.projectClassification,
    managerId: form.value.managerId ?? null,
    managerName: form.value.creatorName,
    salesmanId: form.value.salesmanId ?? null,
@@ -1425,15 +1313,17 @@
    reviewStatus: form.value.auditStatus === '' || form.value.auditStatus === undefined || form.value.auditStatus === null ? null : Number(form.value.auditStatus),
    stage: form.value.projectStage === '' || form.value.projectStage === undefined || form.value.projectStage === null ? null : Number(form.value.projectStage),
    remark: form.value.remark,
    attachmentIds: Array.isArray(form.value.attachmentIds) ? form.value.attachmentIds : [],
    teamList
    attachmentIds: (fileList.value || [])
      .map(f => (f?.id ?? f?.rawData?.id))
      .filter(v => v !== undefined && v !== null && v !== '')
      .map(String)
  }
  const payload = {
    info,
    shippingAddress,
    contractInfo,
    salesLedgerProductList: productData.value
    shippingAddress: null,
    contractInfo: null,
    salesLedgerProductList: null
  }
  const req = operationType.value === 'edit' ? updateProject : addProject
src/views/projectManagement/Management/index.vue
@@ -187,7 +187,7 @@
        setupDate: r.establishTime ?? r.setupDate,
        projectType: r.planName ?? r.projectType,
        projectSource: r.source ?? r.projectSource,
        projectClassification: r.departmentName ?? r.projectClassification,
        projectClassification: r.projectClassification,
        raw: r
      }))
      pagination.value.total = response?.total || response?.data?.total || 0
src/views/projectManagement/Management/projectDetail.vue
@@ -5,48 +5,41 @@
        <div class="header-title">项目详情</div>
        <div class="header-actions">
          <el-button style="color: white;background: #002FA7;" @click="openProgressReport">进度汇报</el-button>
          <!-- <el-button type="danger" @click="openDiscussProgress">洽谈进度</el-button> -->
          <el-button @click="goBack">返回</el-button>
        </div>
      </div>
      <el-steps v-if="steps.length > 0" :active="activeStep" align-center finish-status="success">
        <el-step v-for="(s, idx) in steps" :key="idx" :title="s" />
      </el-steps>
      <div v-if="stageSteps.length > 0" class="progress-block">
        <el-steps :active="activeStep" align-center finish-status="success">
          <el-step v-for="(s, idx) in stageSteps" :key="idx" :title="s.name" />
        </el-steps>
        <div class="progress-percent">
          <span class="progress-label">整体进度</span>
          <el-progress :percentage="overallProgress" :stroke-width="12" style="flex: 1" />
        </div>
      </div>
    </el-card>
    <el-card class="content-card" shadow="never" v-loading="loading">
      <el-tabs v-model="activeTab">
        <el-tab-pane label="基础资料" name="base">
          <el-descriptions :column="4" border>
            <el-descriptions-item label="项目ID">{{ info.id ?? '-' }}</el-descriptions-item>
            <el-descriptions-item label="单据编号">{{ info.no || '-' }}</el-descriptions-item>
            <el-descriptions-item label="项目名称">{{ info.title || '-' }}</el-descriptions-item>
            <el-descriptions-item label="客户名称">{{ info.clientName || '-' }}</el-descriptions-item>
            <el-descriptions-item label="客户ID">{{ info.clientId ?? '-' }}</el-descriptions-item>
            <el-descriptions-item label="项目分类">{{ info.projectClassification || '-' }}</el-descriptions-item>
            <el-descriptions-item label="父项目">{{ parentProjectLabel }}</el-descriptions-item>
            <el-descriptions-item label="项目类型">{{ projectTypeLabel }}</el-descriptions-item>
            <el-descriptions-item label="立项日期">{{ info.establishTime || '-' }}</el-descriptions-item>
            <el-descriptions-item label="项目来源">{{ info.source || '-' }}</el-descriptions-item>
            <el-descriptions-item label="立项人">{{ info.managerName || '-' }}</el-descriptions-item>
            <el-descriptions-item label="业务员">{{ info.salesmanName || '-' }}</el-descriptions-item>
            <el-descriptions-item label="部门">{{ info.departmentName || '-' }}</el-descriptions-item>
            <el-descriptions-item label="单据状态">
              <dict-tag :options="bill_status" :value="String(info.status ?? '')" />
            </el-descriptions-item>
            <el-descriptions-item label="当前阶段">{{ currentStageName }}</el-descriptions-item>
            <el-descriptions-item label="审核状态">
              <dict-tag :options="project_management" :value="String(info.reviewStatus ?? '')" />
            </el-descriptions-item>
            <el-descriptions-item label="计划状态">
              <dict-tag :options="plan_status" :value="String(info.stage ?? '')" />
            </el-descriptions-item>
            <el-descriptions-item label="预计工期(天)">{{ estimatedDays }}</el-descriptions-item>
            <el-descriptions-item label="计划开始日期">{{ info.planStartTime || '-' }}</el-descriptions-item>
            <el-descriptions-item label="计划完成日期">{{ info.planEndTime || '-' }}</el-descriptions-item>
            <el-descriptions-item label="实际开始日期">{{ info.actualStartTime || '-' }}</el-descriptions-item>
            <el-descriptions-item label="实际完成日期">{{ info.actualEndTime || '-' }}</el-descriptions-item>
            <el-descriptions-item label="订单日期">{{ info.orderDate || '-' }}</el-descriptions-item>
            <el-descriptions-item label="项目金额">{{ info.orderAmount ?? '-' }}</el-descriptions-item>
            <el-descriptions-item label="备注" :span="4">{{ info.remark || '-' }}</el-descriptions-item>
          </el-descriptions>
          <el-descriptions v-if="isFromContract" :column="3" border style="margin-top: 14px;">
            <el-descriptions-item label="合同客户">{{ info.clientName || '-' }}</el-descriptions-item>
            <el-descriptions-item label="合同金额(元)">{{ info.orderAmount ?? '-' }}</el-descriptions-item>
            <el-descriptions-item label="合同日期">{{ info.orderDate || '-' }}</el-descriptions-item>
          </el-descriptions>
          <div class="attachment-block" v-if="attachments.length > 0">
@@ -59,81 +52,6 @@
              </div>
            </div>
          </div>
          <el-divider content-position="left">产品信息</el-divider>
          <el-table :data="productRows" border show-summary :summary-method="summarizeProductTable">
            <el-table-column align="center" label="序号" type="index" width="60" />
            <el-table-column label="产品大类" prop="productCategory" show-overflow-tooltip />
            <el-table-column label="规格型号" prop="specificationModel" show-overflow-tooltip />
            <el-table-column label="单位" prop="unit" width="90" />
            <el-table-column label="数量" prop="quantity" width="90" />
            <el-table-column label="税率(%)" prop="taxRate" width="90" />
            <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
            <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
            <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
            <el-table-column label="发票类型" prop="invoiceType" width="110" />
          </el-table>
          <el-divider content-position="left">项目团队</el-divider>
          <el-table :data="teamRows" border>
            <el-table-column align="center" label="序号" type="index" width="60" />
            <el-table-column label="姓名" prop="userName" show-overflow-tooltip />
            <el-table-column label="项目组角色" prop="userRoleName" show-overflow-tooltip />
            <el-table-column label="进入日期" prop="joinTime" width="140" />
            <el-table-column label="离开日期" prop="departTime" width="140" />
            <el-table-column label="联系方式" prop="contact" show-overflow-tooltip />
            <el-table-column label="备注" prop="remark" show-overflow-tooltip />
          </el-table>
          <el-divider content-position="left">收货地址</el-divider>
          <el-descriptions :column="3" border>
            <el-descriptions-item label="收货人">{{ shippingAddress.consignee || '-' }}</el-descriptions-item>
            <el-descriptions-item label="联系方式">{{ shippingAddress.contract || '-' }}</el-descriptions-item>
            <el-descriptions-item label="地址">{{ shippingAddress.address || '-' }}</el-descriptions-item>
          </el-descriptions>
          <el-divider content-position="left">联系信息</el-divider>
          <el-descriptions :column="4" border>
            <el-descriptions-item label="联系人">{{ contractInfo.name || '-' }}</el-descriptions-item>
            <el-descriptions-item label="性别">{{ contractInfo.sex || '-' }}</el-descriptions-item>
            <el-descriptions-item label="生日">{{ contractInfo.birthday || '-' }}</el-descriptions-item>
            <el-descriptions-item label="部门">{{ contractInfo.department || '-' }}</el-descriptions-item>
            <el-descriptions-item label="职务">{{ contractInfo.job || '-' }}</el-descriptions-item>
            <el-descriptions-item label="手机号">{{ contractInfo.phoneNumber || '-' }}</el-descriptions-item>
            <el-descriptions-item label="邮箱">{{ contractInfo.email || '-' }}</el-descriptions-item>
            <el-descriptions-item label="QQ">{{ contractInfo.qq || '-' }}</el-descriptions-item>
            <el-descriptions-item label="固定电话">{{ contractInfo.lineaFissa || '-' }}</el-descriptions-item>
            <el-descriptions-item label="微信">{{ contractInfo.wx || '-' }}</el-descriptions-item>
            <el-descriptions-item label="籍贯">{{ contractInfo.origineEtnica || '-' }}</el-descriptions-item>
            <el-descriptions-item label="法人代表">{{ contractInfo.rappresentanteLegale || '-' }}</el-descriptions-item>
          </el-descriptions>
        </el-tab-pane>
        <el-tab-pane label="项目类型" name="plan">
          <el-descriptions :column="4" border>
            <el-descriptions-item label="类型名称">{{ projectPlan.name || '-' }}</el-descriptions-item>
            <el-descriptions-item label="备注" :span="3">{{ projectPlan.description || '-' }}</el-descriptions-item>
          </el-descriptions>
          <div class="attachment-block" v-if="planAttachments.length > 0">
            <div class="attachment-title">类型附件</div>
            <div class="attachment-list">
              <div v-for="(att, idx) in planAttachments" :key="att.id || att.url || idx" class="attachment-item">
                <el-icon><Document /></el-icon>
                <span class="attachment-name">{{ att.name || att.fileName || att.url || '附件' }}</span>
                <el-button link type="primary" size="small" @click="downloadAttachment(att)">下载</el-button>
              </div>
            </div>
          </div>
          <el-table :data="planNodeRows" border style="margin-top: 14px;">
            <el-table-column align="center" label="步骤" type="index" width="80" />
            <el-table-column label="阶段名称" prop="name" min-width="160" show-overflow-tooltip />
            <el-table-column label="负责人" prop="leaderName" width="140" show-overflow-tooltip />
            <el-table-column label="预计工期(天)" prop="estimatedDuration" width="140" />
            <el-table-column label="工时单价" prop="hourlyRate" width="120" />
            <el-table-column label="作业内容" prop="workContent" min-width="180" show-overflow-tooltip />
          </el-table>
        </el-tab-pane>
        <el-tab-pane label="项目阶段" name="stage">
@@ -147,13 +65,13 @@
            <el-table-column label="计划结束" prop="planEndTime" width="120" />
            <el-table-column label="实际开始" prop="actualStartTime" width="120" />
            <el-table-column label="实际结束" prop="actualEndTime" width="120" />
            <el-table-column label="预计工期(天)" prop="estimatedDuration" width="130" />
            <el-table-column label="本次进度日期" prop="reportDate" width="130" />
            <el-table-column label="附件" width="90" align="center">
              <template #default="{ row }">
                <span>{{ row.attachmentCount }}</span>
              </template>
            </el-table-column>
            <el-table-column v-if="false" label="操作" width="100" align="center" fixed="right" >
            <el-table-column v-if="false" label="操作" width="100" align="center" fixed="right">
              <template #default="{ row }">
                <el-button link type="danger" size="small" @click="handleDeleteStage(row)">删除</el-button>
              </template>
@@ -169,15 +87,9 @@
    :project-id="info.id"
    :project-info="info"
    :plan-nodes="planNodeRows"
    :stage-records="stageNodeRows"
    :default-plan-node-id="defaultPlanNodeId"
    @submitted="handleProgressSubmitted"
  />
  <DiscussProgressDialog
    v-model="discussProgressVisible"
    :project-id="info.id"
    :plan-nodes="planNodeRows"
    :default-plan-node-id="defaultPlanNodeId"
    @submitted="handleDiscussSubmitted"
  />
</template>
@@ -186,54 +98,28 @@
import { useRoute, useRouter } from 'vue-router'
import { Document } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getProject, saveStage, listStage, deleteStage } from '@/api/projectManagement/project'
import { getProject, saveStage, listStage, deleteStage, updateProject } from '@/api/projectManagement/project'
import { listPlan } from '@/api/projectManagement/projectType'
import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue'
import DiscussProgressDialog from '@/components/ProjectManagement/DiscussProgressDialog.vue'
import useUserStore from '@/store/modules/user'
const { proxy } = getCurrentInstance()
const route = useRoute()
const router = useRouter()
const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
const { project_management } = proxy.useDict('project_management')
const loading = ref(false)
const activeTab = ref('base')
const progressReportVisible = ref(false)
const discussProgressVisible = ref(false)
const userStore = useUserStore()
const info = ref({})
const shippingAddress = ref({})
const contractInfo = ref({})
const productRows = ref([])
const teamRows = ref([])
const attachments = ref([])
const projectTypeMap = ref(new Map())
const projectPlan = ref({})
const planNodeRows = ref([])
const planAttachments = ref([])
const stageNodeRows = ref([])
const estimatedDays = computed(() => {
  const raw = info.value?.estimatedDays
  const n = Number(raw)
  if (Number.isFinite(n) && n > 0) return n
  const start = info.value?.planStartTime
  const end = info.value?.planEndTime
  if (!start || !end) return 0
  const startTime = new Date(`${start}T00:00:00`).getTime()
  const endTime = new Date(`${end}T00:00:00`).getTime()
  if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime < startTime) return 0
  return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1
})
const projectTypeLabel = computed(() => {
  const id = info.value?.projectManagementPlanId
  if (id === undefined || id === null || id === '') return '-'
  const p = projectTypeMap.value.get(Number(id))
  return p?.name || String(id)
})
const isFromContract = computed(() => info.value?.source === '业务审批')
const parentProjectLabel = computed(() => {
  return (
@@ -244,32 +130,62 @@
  )
})
const planStageEnum = computed(() => {
  const list = Array.isArray(plan_status) ? plan_status : []
  return list.map(i => ({ value: String(i.value), label: i.label }))
// å„阶段当前进度(同一阶段多次汇报取最大值)
const stageProgressMap = computed(() => {
  const map = {}
  stageNodeRows.value.forEach(r => {
    const key = String(r.projectManagementPlanNodeId ?? '')
    if (!key) return
    map[key] = Math.max(map[key] || 0, Number(r.progress) || 0)
  })
  return map
})
const steps = computed(() => {
  const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
  const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
  const labels = sorted.map(i => i.name || i.workContent).filter(Boolean)
  return labels.length > 0 ? labels : planStageEnum.value.map(i => i.label)
// é˜¶æ®µè¿›åº¦æ¡æ•°æ®ï¼šä¼˜å…ˆä½¿ç”¨é¡¹ç›®ä¸ŠåŒæ­¥çš„ planStage
const stageSteps = computed(() => {
  const planStages = Array.isArray(info.value?.planStage) ? info.value.planStage : []
  if (planStages.length > 0) {
    return [...planStages]
      .sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
      .map(i => ({
        id: i.id,
        name: i.name || String(i.id ?? ''),
        status: Number(i.planStageEnum) || 1,
        progress: stageProgressMap.value[String(i.id)] ?? 0
      }))
  }
  const nodes = [...(planNodeRows.value || [])].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
  return nodes.map(n => {
    const progress = stageProgressMap.value[String(n.id)] ?? 0
    return {
      id: n.id,
      name: n.name || n.workContent || String(n.id ?? ''),
      status: progress >= 100 ? 3 : progress > 0 ? 2 : 1,
      progress
    }
  })
})
const activeStep = computed(() => {
  const statusOrStage = info.value?.stage ?? info.value?.status
  const enumList = planStageEnum.value
  // ä¼˜å…ˆä½¿ç”¨ planStageEnum çš„ value åŒ¹é…
  const found = enumList.find(i => i.value === String(statusOrStage))
  const label = found?.label
  // åœ¨é¡¹ç›®ç±»åž‹èŠ‚ç‚¹ä¸­æŸ¥æ‰¾å¯¹åº” label çš„下标
  const nodeLabels = steps.value
  const idxByLabel = label ? nodeLabels.findIndex(l => String(l) === String(label)) : -1
  if (idxByLabel >= 0) return idxByLabel + 1
  // å›žé€€ï¼šå¦‚æžœ statusOrStage æ˜¯æ•°å­—索引
  const n = Number(statusOrStage)
  if (Number.isFinite(n) && n > 0) return Math.min(n, nodeLabels.length)
  return 0
  const idx = stageSteps.value.findIndex(s => s.status !== 3)
  return idx === -1 ? stageSteps.value.length : idx
})
const currentStageName = computed(() => {
  const current = stageSteps.value.find(s => s.status === 2) || stageSteps.value[activeStep.value]
  return current?.name || '-'
})
const overallProgress = computed(() => {
  const list = stageSteps.value
  if (list.length === 0) return 0
  const sum = list.reduce((acc, s) => acc + (Number(s.progress) || 0), 0)
  return Math.round(sum / list.length)
})
const defaultPlanNodeId = computed(() => {
  const next = stageSteps.value.find(s => s.status !== 3)
  return next?.id ?? stageSteps.value[stageSteps.value.length - 1]?.id
})
function goBack() {
@@ -280,27 +196,30 @@
  progressReportVisible.value = true
}
function openDiscussProgress() {
  discussProgressVisible.value = true
async function saveTeamIfNeeded(teamList) {
  if (!Array.isArray(teamList)) return
  const attachmentIds = attachments.value
    .map(a => a.id ?? a.fileId)
    .filter(v => v !== undefined && v !== null && v !== '')
    .map(String)
  const res = await updateProject({
    info: {
      id: info.value?.id,
      no: info.value?.no,
      title: info.value?.title,
      projectManagementPlanId: info.value?.projectManagementPlanId,
      teamList,
      attachmentIds
    }
  })
  if (res?.code !== 200) throw new Error(res?.msg || '团队保存失败')
}
const defaultPlanNodeId = computed(() => {
  const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
  if (nodes.length === 0) return undefined
  const stageVal = info.value?.stage
  const direct = nodes.find(n => String(n.id) === String(stageVal))
  if (direct?.id) return direct.id
  const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
  const idx = Number(stageVal)
  if (Number.isFinite(idx)) {
    const byIndex = sorted[idx - 1] || sorted[idx] || sorted[0]
    if (byIndex?.id) return byIndex.id
  }
  return sorted[0]?.id
})
async function handleProgressSubmitted(payload) {
  try {
    if (Array.isArray(payload.teamList)) {
      await saveTeamIfNeeded(payload.teamList)
    }
    const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
    const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {}
    const description = payload.remark
@@ -312,13 +231,13 @@
      projectManagementInfoId: info.value?.id,
      description,
      actualLeaderId: userStore.id || info.value?.managerId,
      actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName,
      actualLeaderName: userStore.nickName || info.value?.managerName || '',
      estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
      planStartTime: payload.planStartTime || info.value?.planStartTime,
      planEndTime: payload.planEndTime || info.value?.planEndTime,
      actualStartTime: payload.actualStartTime || null,
      actualEndTime: payload.actualEndTime || null,
      progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0,
      progress: Number(payload.completionProgress ?? payload.totalProgress ?? 0) || 0,
      storageBlobDTOs: Array.isArray(payload.storageBlobDTOs) ? payload.storageBlobDTOs : []
    }
    const res = await saveStage(req)
@@ -329,45 +248,14 @@
    }
    ElMessage.error(res?.msg || '提交失败')
  } catch (e) {
    ElMessage.error('提交失败')
  }
}
async function handleDiscussSubmitted(payload) {
  try {
    const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
    const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {}
    const req = {
      id: null,
      projectManagementPlanNodeId: payload.planNodeId,
      projectManagementInfoId: info.value?.id,
      description: payload.remark,
      actualLeaderId: userStore.id || info.value?.managerId,
      actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName,
      estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
      planStartTime: info.value?.planStartTime,
      planEndTime: info.value?.planEndTime,
      actualStartTime: info.value?.actualStartTime || null,
      actualEndTime: info.value?.actualEndTime || null,
      progress: Number(info.value?.progress ?? 0) || 0,
      attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : []
    }
    const res = await saveStage(req)
    if (res?.code === 200) {
      ElMessage.success('提交成功')
      await Promise.all([loadDetail(), loadStageList()])
      return
    }
    ElMessage.error(res?.msg || '提交失败')
  } catch (e) {
    ElMessage.error('提交失败')
    ElMessage.error(e?.message || '提交失败')
  }
}
function downloadAttachment(row) {
  if (row?.url) {
    try {
            proxy.$download.byUrl(row.url, row.originalFilename);
      proxy.$download.byUrl(row.url, row.originalFilename)
      return
    } catch (e) {}
  }
@@ -403,11 +291,12 @@
    const records = Array.isArray(list) ? list : []
    stageNodeRows.value = records.map(r => {
      const attachmentList = Array.isArray(r.attachmentList) ? r.attachmentList : []
      const attachmentIds = Array.isArray(r.attachmentIds) ? r.attachmentIds : []
      const blobList = Array.isArray(r.storageBlobVOs) ? r.storageBlobVOs : []
      return {
        ...r,
        stageName: getPlanNodeName(r.projectManagementPlanNodeId),
        attachmentCount: attachmentList.length || attachmentIds.length || 0
        reportDate: r.createTime ? String(r.createTime).slice(0, 10) : '',
        attachmentCount: attachmentList.length || blobList.length || 0
      }
    })
  } catch {
@@ -437,15 +326,11 @@
function syncProjectPlan() {
  const id = info.value?.projectManagementPlanId
  if (id === undefined || id === null || id === '') {
    projectPlan.value = {}
    planNodeRows.value = []
    planAttachments.value = []
    return
  }
  const plan = projectTypeMap.value.get(Number(id)) || {}
  projectPlan.value = plan || {}
  planNodeRows.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : []
  planAttachments.value = Array.isArray(plan?.attachmentList) ? plan.attachmentList : []
}
async function loadDetail() {
@@ -456,10 +341,6 @@
    const res = await getProject(id)
    const detail = res?.data?.data ?? res?.data ?? res
    info.value = detail?.info || {}
    shippingAddress.value = detail?.shippingAddress || {}
    contractInfo.value = detail?.contractInfo || {}
    productRows.value = Array.isArray(detail?.salesLedgerProductList) ? detail.salesLedgerProductList : []
    teamRows.value = Array.isArray(detail?.info?.teamList) ? detail.info.teamList : []
    attachments.value = Array.isArray(detail?.info?.attachmentList) ? detail.info.attachmentList : []
    syncProjectPlan()
    await loadStageList()
@@ -475,12 +356,6 @@
</script>
<style scoped lang="scss">
.section-bar {
  width: 3px;
  height: 14px;
  background: #002FA7;
  border-radius: 2px;
}
.header-card {
  margin-bottom: 14px;
}
@@ -495,6 +370,23 @@
.header-title {
  font-weight: 600;
  font-size: 16px;
}
.progress-block {
  padding-top: 6px;
}
.progress-percent {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
}
.progress-label {
  font-size: 13px;
  color: #606266;
  white-space: nowrap;
}
.content-card {
@@ -529,4 +421,3 @@
  white-space: nowrap;
}
</style>
src/views/salesManagement/reportAudit/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,323 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">客户名称:</span>
        <el-input v-model="searchForm.customerName"
                  style="width: 220px; margin-right: 10px"
                  placeholder="请输入"
                  clearable
                  @keyup.enter="handleQuery" />
        <span class="search_title">报表年度:</span>
        <el-select v-model="searchForm.reportYear"
                   style="width: 150px; margin-right: 10px"
                   placeholder="全部"
                   clearable
                   @change="handleQuery">
          <el-option v-for="year in yearOptions"
                     :key="year"
                     :label="year + 'å¹´'"
                     :value="year" />
        </el-select>
        <span class="search_title">审批状态:</span>
        <el-select v-model="searchForm.auditStatus"
                   style="width: 150px"
                   placeholder="全部"
                   clearable
                   @change="handleQuery">
          <el-option label="待审批"
                     :value="0" />
          <el-option label="已通过"
                     :value="1" />
          <el-option label="已退回"
                     :value="2" />
        </el-select>
        <el-button type="primary"
                   style="margin-left: 10px"
                   @click="handleQuery">搜索</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </div>
    </div>
    <el-table v-loading="loading"
              :data="pagedData"
              border
              size="small"
              style="margin-top: 10px">
      <el-table-column prop="customerName"
                       label="客户名称"
                       min-width="180"
                       show-overflow-tooltip />
      <el-table-column prop="reportYear"
                       label="年度"
                       width="80"
                       align="center" />
      <el-table-column prop="reportName"
                       label="报表名称"
                       min-width="160"
                       show-overflow-tooltip />
      <el-table-column label="报表附件"
                       min-width="240">
        <template #default="scope">
          <FileUpload v-model="scope.row.reportFiles"
                      disabled
                      :is-show-tip="false" />
        </template>
      </el-table-column>
      <el-table-column prop="uploadUserName"
                       label="提交人"
                       width="100" />
      <el-table-column prop="updateTime"
                       label="提交时间"
                       width="160" />
      <el-table-column label="审批状态"
                       width="100"
                       align="center">
        <template #default="scope">
          <el-tag :type="statusTagType(scope.row.auditStatus)">
            {{ statusText(scope.row.auditStatus) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="auditUserName"
                       label="审批人"
                       width="100" />
      <el-table-column prop="auditRemark"
                       label="审批意见"
                       min-width="160"
                       show-overflow-tooltip />
      <el-table-column label="操作"
                       width="130"
                       align="center"
                       fixed="right">
        <template #default="scope">
          <el-button type="primary"
                     link
                     v-hasPermi="['sales:report:audit']"
                     @click="openAuditDialog(scope.row)">审批</el-button>
        </template>
      </el-table-column>
    </el-table>
    <div v-if="total > 0"
         class="pagination-wrap">
      <el-pagination v-model:current-page="page.current"
                     v-model:page-size="page.size"
                     :page-sizes="[10, 20, 50]"
                     :total="total"
                     layout="total, sizes, prev, pager, next"
                     @size-change="handleSizeChange"
                     @current-change="handleCurrentChange" />
    </div>
    <div v-else-if="!loading"
         class="no-record">暂无客户申报数据</div>
    <!-- å®¡æ‰¹å¼¹çª— -->
    <el-dialog v-model="auditDialogVisible"
               title="申报审批"
               width="760px"
               @close="closeAuditDialog">
      <el-descriptions :column="2"
                       border
                       size="small">
        <el-descriptions-item label="客户名称">{{ auditForm.customerName }}</el-descriptions-item>
        <el-descriptions-item label="报表年度">{{ auditForm.reportYear }}å¹´</el-descriptions-item>
        <el-descriptions-item label="报表名称">{{ auditForm.reportName }}</el-descriptions-item>
        <el-descriptions-item label="提交人">{{ auditForm.uploadUserName }}</el-descriptions-item>
        <el-descriptions-item label="提交时间">{{ auditForm.updateTime }}</el-descriptions-item>
        <el-descriptions-item label="当前状态">{{ statusText(auditForm.auditStatus) }}</el-descriptions-item>
      </el-descriptions>
      <div class="audit-files">
        <div class="audit-files-title">报表附件</div>
        <FileUpload v-model="auditForm.reportFiles"
                    disabled
                    :is-show-tip="false" />
      </div>
      <el-form :model="auditForm"
               label-width="90px"
               style="margin-top: 12px">
        <el-form-item label="审批结果">
          <el-radio-group v-model="auditForm.auditStatus">
            <el-radio :value="1">通过</el-radio>
            <el-radio :value="2">退回</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="审批意见">
          <el-input v-model="auditForm.auditRemark"
                    type="textarea"
                    :rows="3"
                    maxlength="500"
                    show-word-limit
                    placeholder="退回时请填写退回原因" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeAuditDialog">取 æ¶ˆ</el-button>
          <el-button type="primary"
                     :loading="auditSaving"
                     @click="submitAudit">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { computed, getCurrentInstance, onMounted, reactive, ref } from "vue";
  import FileUpload from "@/components/FileUpload/index.vue";
  import {
    auditListCustomerFinancialReport,
    auditCustomerFinancialReport,
  } from "@/api/basicData/customerFinancialReport";
  const { proxy } = getCurrentInstance();
  const nowYear = new Date().getFullYear();
  const yearOptions = [nowYear, nowYear - 1, nowYear - 2];
  const searchForm = reactive({ customerName: "", reportYear: "", auditStatus: "" });
  const listData = ref([]);
  const loading = ref(false);
  const total = ref(0);
  const page = reactive({ current: 1, size: 10 });
  const pagedData = computed(() => {
    const start = (page.current - 1) * page.size;
    return listData.value.slice(start, start + page.size);
  });
  const statusText = status => {
    if (status === 1) return "已通过";
    if (status === 2) return "已退回";
    return "待审批";
  };
  const statusTagType = status => {
    if (status === 1) return "success";
    if (status === 2) return "danger";
    return "warning";
  };
  const load = () => {
    loading.value = true;
    auditListCustomerFinancialReport({
      customerName: searchForm.customerName,
      reportYear: searchForm.reportYear,
      auditStatus: searchForm.auditStatus,
    }).then(res => {
      listData.value = res.data || [];
      total.value = listData.value.length;
      if (page.current > 1 && pagedData.value.length === 0) {
        page.current = 1;
      }
    }).finally(() => {
      loading.value = false;
    });
  };
  const handleQuery = () => {
    page.current = 1;
    load();
  };
  const resetQuery = () => {
    searchForm.customerName = "";
    searchForm.reportYear = "";
    searchForm.auditStatus = "";
    handleQuery();
  };
  const handleSizeChange = size => {
    page.size = size;
    page.current = 1;
  };
  const handleCurrentChange = current => {
    page.current = current;
  };
  const auditDialogVisible = ref(false);
  const auditSaving = ref(false);
  const auditForm = reactive({
    id: "",
    customerName: "",
    reportYear: "",
    reportName: "",
    reportFiles: "",
    uploadUserName: "",
    updateTime: "",
    auditStatus: 1,
    auditRemark: "",
  });
  const openAuditDialog = row => {
    Object.keys(auditForm).forEach(key => {
      auditForm[key] = row[key] !== undefined && row[key] !== null ? row[key] : auditForm[key];
    });
    auditForm.id = row.id;
    auditForm.customerName = row.customerName;
    auditForm.reportYear = row.reportYear;
    auditForm.reportName = row.reportName;
    auditForm.reportFiles = row.reportFiles;
    auditForm.uploadUserName = row.uploadUserName;
    auditForm.updateTime = row.updateTime;
    auditForm.auditStatus = 1;
    auditForm.auditRemark = "";
    auditDialogVisible.value = true;
  };
  const closeAuditDialog = () => {
    auditDialogVisible.value = false;
  };
  const submitAudit = () => {
    if (!auditForm.auditStatus) {
      proxy.$modal.msgWarning("请选择审批结果");
      return;
    }
    if (auditForm.auditStatus === 2 && !auditForm.auditRemark) {
      proxy.$modal.msgWarning("退回时请填写退回原因");
      return;
    }
    auditSaving.value = true;
    auditCustomerFinancialReport({
      id: auditForm.id,
      auditStatus: auditForm.auditStatus,
      auditRemark: auditForm.auditRemark,
    }).then(() => {
      proxy.$modal.msgSuccess("审批完成");
      auditDialogVisible.value = false;
      load();
    }).finally(() => {
      auditSaving.value = false;
    });
  };
  onMounted(load);
</script>
<style scoped>
  .search_form {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }
  .search_title {
    color: #606266;
  }
  .pagination-wrap {
    display: flex;
    justify-content: flex-end;
    margin-top: 12px;
  }
  .no-record {
    color: #909399;
    text-align: center;
    padding: 24px 0;
  }
  .audit-files {
    margin-top: 12px;
  }
  .audit-files-title {
    margin-bottom: 6px;
    color: #606266;
  }
</style>
src/views/salesManagement/salesLedger/index.vue
@@ -80,7 +80,7 @@
          width="55"
          fixed="left"
        />
        <el-table-column type="expand" width="60" fixed="left">
        <el-table-column v-if="false" type="expand" width="60" fixed="left">
          <template #default="props">
            <el-table
              :data="props.row.children"
@@ -225,43 +225,19 @@
          :formatter="formattedNumber"
        />
        <el-table-column
          label="实际合同金额(元)"
          prop="netContractAmount"
          width="220"
          show-overflow-tooltip
          :formatter="formattedNumber"
        />
        <el-table-column
          label="录入人"
          prop="entryPersonName"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          label="录入日期"
          prop="entryDate"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          label="签订日期"
          prop="executionDate"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          label="交付日期"
          prop="deliveryDate"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          label="备注"
          prop="remarks"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column fixed="right" label="操作" width="320" align="center">
        <el-table-column label="业务审批" width="120" align="center">
          <template #default="scope">
            <el-tag :type="businessStatusType(scope.row.businessApprovalStatus)">
              {{ businessStatusText(scope.row.businessApprovalStatus) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column fixed="right" label="操作" width="500" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="openForm('view', scope.row)"
              >详情
@@ -294,6 +270,40 @@
            <el-button link type="primary" @click="openFileDialog(scope.row)"
              >附件
            </el-button>
            <el-button
              v-if="scope.row.projectManagementInfoId"
              link
              type="primary"
              @click="openProjectProgress(scope.row)"
              >查看项目进度
            </el-button>
            <el-button
              v-if="![1, 2].includes(scope.row.businessApprovalStatus)"
              v-hasPermi="['business:approval:submit']"
              link
              type="warning"
              :loading="businessActionId === scope.row.id"
              @click="submitBusiness(scope.row)"
              >提交审批
            </el-button>
            <el-button
              v-if="scope.row.businessApprovalStatus === 1"
              v-hasPermi="['business:approval:review']"
              link
              type="success"
              :loading="businessActionId === scope.row.id"
              @click="reviewBusiness(scope.row, true)"
              >通过
            </el-button>
            <el-button
              v-if="scope.row.businessApprovalStatus === 1"
              v-hasPermi="['business:approval:review']"
              link
              type="danger"
              :loading="businessActionId === scope.row.id"
              @click="reviewBusiness(scope.row, false)"
              >退回
            </el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -310,10 +320,10 @@
      v-model="dialogFormVisible"
      :title="
        operationType === 'add'
          ? '新增销售台账页面'
          ? '新增业务台账'
          : operationType === 'edit'
          ? '编辑销售台账页面'
          : '销售台账详情'
          ? '编辑业务台账'
          : '业务台账详情'
      "
      :width="'70%'"
      :operation-type="operationType"
@@ -328,14 +338,6 @@
        :rules="rules"
        ref="formRef"
      >
        <!-- æŠ¥ä»·å•导入入口:放在表单顶部,选择后反显客户/业务员等 -->
        <el-row v-if="operationType === 'add'" style="margin-bottom: 10px">
          <el-col :span="24" style="text-align: right">
            <el-button type="primary" plain @click="openQuotationDialog">
              ä»Žé”€å”®æŠ¥ä»·å¯¼å…¥
            </el-button>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合同类型:" prop="contractType">
@@ -461,6 +463,20 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合同金额(元):" prop="contractAmount">
              <el-input-number
                v-model="form.contractAmount"
                :min="0"
                :precision="2"
                controls-position="right"
                style="width: 100%"
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="录入人:" prop="entryPerson">
              <el-select
                v-model="form.entryPerson"
@@ -512,89 +528,6 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-form-item label="产品信息:" prop="entryDate">
            <el-button
              v-if="operationType !== 'view'"
              type="primary"
              @click="openProductForm('add')"
              >添加
            </el-button>
            <el-button
              v-if="operationType !== 'view'"
              plain
              type="danger"
              @click="deleteProduct"
              >删除
            </el-button>
          </el-form-item>
        </el-row>
        <el-table
          :data="productData"
          border
          @selection-change="productSelected"
          show-summary
          :summary-method="summarizeMainTable"
        >
          <el-table-column
            align="center"
            type="selection"
            width="55"
            v-if="operationType !== 'view'"
            :selectable="(row) => !isProductShipped(row)"
          />
          <el-table-column
            align="center"
            label="序号"
            type="index"
            width="60"
          />
          <el-table-column label="产品大类" prop="productCategory" />
          <el-table-column label="规格型号" prop="specificationModel" />
          <el-table-column label="单位" prop="unit" />
          <el-table-column label="数量" prop="quantity" />
          <el-table-column label="税率(%)" prop="taxRate" />
          <el-table-column
            label="含税单价(元)"
            prop="taxInclusiveUnitPrice"
            :formatter="formattedNumber"
          />
          <el-table-column
            label="含税总价(元)"
            prop="taxInclusiveTotalPrice"
            :formatter="formattedNumber"
          />
          <el-table-column
            label="不含税总价(元)"
            prop="taxExclusiveTotalPrice"
            :formatter="formattedNumber"
          />
          <el-table-column label="是否生产" prop="isProduction" width="150">
            <template #default="scope">
              <el-tag :type="scope.row.isProduction ? 'success' : 'info'">
                {{ scope.row.isProduction ? "是" : "否" }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column
            fixed="right"
            label="操作"
            min-width="60"
            align="center"
            v-if="operationType !== 'view'"
          >
            <template #default="scope">
              <el-button
                link
                type="primary"
                size="small"
                :disabled="isProductShipped(scope.row)"
                @click="openProductForm('edit', scope.row, scope.$index)"
                >编辑
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注:" prop="remarks">
@@ -1501,6 +1434,8 @@
  createFrameworkOrder,
  listFrameworkOrders,
  frameworkContractShippedQuantity,
  submitBusinessApproval,
  reviewBusinessApproval,
} from "@/api/salesManagement/salesLedger.js";
import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
import { modelList, productTreeList } from "@/api/basicData/product.js";
@@ -1539,6 +1474,7 @@
  size: 100,
});
const total = ref(0);
const businessActionId = ref(null);
const fileList = ref([]);
const deliveryFileList = ref([]);
@@ -1857,6 +1793,55 @@
    .catch(() => {
      tableLoading.value = false;
    });
};
const businessStatusText = (status) =>
  ({ 0: "草稿", 1: "待审批", 2: "已通过", 3: "已退回" }[status ?? 0] || "草稿");
const businessStatusType = (status) =>
  ({ 0: "info", 1: "warning", 2: "success", 3: "danger" }[status ?? 0] || "info");
const submitBusiness = async (row) => {
  try {
    await ElMessageBox.confirm(
      `确定提交“${row.projectName || row.salesContractNo}”进行管理审批吗?`,
      "提交业务审批",
      { type: "warning", confirmButtonText: "确认提交", cancelButtonText: "取消" }
    );
    businessActionId.value = row.id;
    await submitBusinessApproval(row.id);
    proxy.$modal.msgSuccess("已提交审批");
    await getList();
  } catch (error) {
    if (error !== "cancel" && error !== "close") throw error;
  } finally {
    businessActionId.value = null;
  }
};
const reviewBusiness = async (row, approved) => {
  try {
    let remark = "";
    if (!approved) {
      const result = await ElMessageBox.prompt("请输入退回原因", "退回业务", {
        inputValidator: (value) => Boolean(value?.trim()) || "请输入退回原因",
        confirmButtonText: "确认退回",
        cancelButtonText: "取消",
        type: "warning",
      });
      remark = result.value.trim();
    } else {
      await ElMessageBox.confirm(
        "审批通过将自动创建项目,是否继续?",
        "通过业务审批",
        { type: "success", confirmButtonText: "通过并建项", cancelButtonText: "取消" }
      );
    }
    businessActionId.value = row.id;
    await reviewBusinessApproval({ salesLedgerId: row.id, approved, remark });
    proxy.$modal.msgSuccess(approved ? "审批通过,项目已自动建立" : "已退回");
    await getList();
  } catch (error) {
    if (error !== "cancel" && error !== "close") throw error;
  } finally {
    businessActionId.value = null;
  }
};
// èŽ·å–äº§å“å¤§ç±»tree数据
const getProductOptions = () => {
@@ -2228,23 +2213,9 @@
const submitForm = () => {
  proxy.$refs["formRef"].validate((valid) => {
    if (valid) {
      console.log("productData.value--", productData.value);
      if (productData.value !== null && productData.value.length > 0) {
        form.value.productData = proxy.HaveJson(productData.value);
      } else {
        proxy.$modal.msgWarning("请添加产品信息");
        return;
      }
      form.value.productData = proxy.HaveJson(productData.value || []);
      form.value.storageBlobDTOs = fileList;
      form.value.type = 1;
      if (isFrameworkContract.value) {
        form.value.productData = form.value.productData.map((item) => ({
          ...item,
          quantity: null,
          deliveryDate: null,
          isProduction: false,
        }));
      }
      if (form.value.autoGenerateContractNo) {
        form.value.salesContractNo = "";
      }
@@ -3171,6 +3142,16 @@
  fileDialogVisible.value = true;
};
// æŸ¥çœ‹é¡¹ç›®è¿›åº¦ï¼ˆè·³è½¬é¡¹ç›®è¯¦æƒ…,可查看阶段进展与资料文件)
const openProjectProgress = (row) => {
  const projectId = row.projectManagementInfoId || row.projectId;
  if (!projectId) {
    proxy.$modal.msgWarning("该业务暂未关联项目");
    return;
  }
  router.push(`/projectManagement/Management/detail/${projectId}`);
};
// æ‰“开发货弹框
const openDeliveryForm = async (row) => {
  // å®¡æ‰¹ä¸­æ—¶æç¤ºç”¨æˆ·å·²æœ‰å¾…审批的发货记录
vite.config.js
@@ -8,7 +8,7 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
      env.VITE_APP_ENV === "development"
          ? "http://36.213.90.123:9015"
          ? "http://127.0.0.1:7005"
          : env.VITE_BASE_API;
  const javaUrl =
      env.VITE_APP_ENV === "development"
@@ -53,7 +53,7 @@
    server: {
      port: 80,
      host: true,
      open: true,
      open: false,
      proxy: {
        // https://cn.vitejs.dev/config/#server-proxy
        "/dev-api": {