| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- 项ç®ç®¡çç³»ç»æ°æµç¨åçº§èæ¬ |
| | | -- éç¨åºï¼product-inventory-management-xmgl |
| | | -- 说æï¼æ§è¡åå
åå
¨åºå¤ä»½ãæ¬èæ¬ä¸å é¤åä¸å¡æ°æ®ï¼å¯é夿§è¡ã |
| | | |
| | | SET NAMES utf8mb4; |
| | | SET FOREIGN_KEY_CHECKS = 0; |
| | | |
| | | 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('project_management_info','project_leader_id',"bigint DEFAULT NULL COMMENT '项ç®è´è´£äººID'"); |
| | | CALL add_column_if_missing('project_management_info','project_leader_name',"varchar(100) DEFAULT NULL COMMENT '项ç®è´è´£äºº'"); |
| | | CALL add_column_if_missing('project_management_info','project_manager_ids',"json DEFAULT NULL COMMENT '项ç®ç»çIDæ°ç»'"); |
| | | CALL add_column_if_missing('project_management_info','project_manager_names',"json DEFAULT NULL COMMENT '项ç®ç»çåç§°æ°ç»'"); |
| | | CALL add_column_if_missing('project_management_info','project_executor_ids',"json DEFAULT NULL COMMENT 'é¡¹ç®æ§è¡äººIDæ°ç»'"); |
| | | CALL add_column_if_missing('project_management_info','project_executor_names',"json DEFAULT NULL COMMENT 'é¡¹ç®æ§è¡äººåç§°æ°ç»'"); |
| | | |
| | | -- æ°ççâç«é¡¹âæ¯å¼å§åçä¿¡æ¯å夿´»å¨ï¼å¼å§åç»ä¸è¿å
¥âå¼åâé¶æ®µã |
| | | UPDATE project_management_info SET current_phase = 'å¼å' WHERE current_phase = 'ç«é¡¹' AND is_delete = 0; |
| | | CALL add_column_if_missing('project_management_info','follow_cycle',"varchar(20) DEFAULT 'MONTH' COMMENT 'MONTHæ¯æ QUARTERæ¯å£åº¦ YEARæ¯å¹´'"); |
| | | CALL add_column_if_missing('project_management_info','follow_detail',"varchar(1000) DEFAULT NULL COMMENT 'è·è¿è¦æ±è¯´æ'"); |
| | | CALL add_column_if_missing('project_management_info','current_phase',"varchar(30) DEFAULT 'æªå¼å§' COMMENT 'æªå¼å§/ç«é¡¹/å¼å/ç³æ¥/ç³æ¥ç»æ/éªæ¶'"); |
| | | CALL add_column_if_missing('project_management_info','source_business_id',"bigint DEFAULT NULL COMMENT 'æ¥æºä¸å¡ID'"); |
| | | ALTER TABLE `project_management_info` MODIFY COLUMN `project_management_plan_id` bigint DEFAULT NULL COMMENT 'æ§è®¡å模æ¿IDï¼æ°æµç¨ä¸åå¿
å¡«ï¼'; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `project_management_category` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `parent_id` bigint NOT NULL DEFAULT 0, |
| | | `category_name` varchar(100) NOT NULL, |
| | | `sort` int NOT NULL DEFAULT 0, |
| | | `remark` varchar(500) DEFAULT NULL, |
| | | `is_deleted` tinyint NOT NULL DEFAULT 0, |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), KEY `idx_project_category_parent` (`parent_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项ç®å¤çº§åç±»'; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `project_management_module` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `project_id` bigint NOT NULL, |
| | | `module_name` varchar(200) NOT NULL, |
| | | `planned_start_date` date DEFAULT NULL, |
| | | `summary` varchar(1000) DEFAULT NULL, |
| | | `project_weight` decimal(7,2) NOT NULL DEFAULT 0 COMMENT '模åå 项ç®å·¥ä½éç¾åæ¯', |
| | | `current_progress` decimal(7,2) NOT NULL DEFAULT 0 COMMENT 'æ¨¡åææ°è¿åº¦ç¾åæ¯', |
| | | `is_deleted` tinyint NOT NULL DEFAULT 0, |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), KEY `idx_project_module_project` (`project_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='é¡¹ç®æ¨¡å'; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `project_management_module_member` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `module_id` bigint NOT NULL, |
| | | `user_id` bigint NOT NULL, |
| | | `user_name` varchar(100) NOT NULL, |
| | | `module_share` decimal(7,2) NOT NULL DEFAULT 0 COMMENT 'å
¼å®¹æ§æ°æ®ï¼æ°æµç¨ä¸å¨æ¨¡å设置ä¸å¡«å', |
| | | `is_deleted` tinyint NOT NULL DEFAULT 0, |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), KEY `idx_module_member_module` (`module_id`), KEY `idx_module_member_user` (`user_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='é¡¹ç®æ¨¡åæ§è¡äºº'; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `project_management_follow_up` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `project_id` bigint NOT NULL, |
| | | `follow_date` date NOT NULL, |
| | | `detail` varchar(2000) NOT NULL, |
| | | `attachments` json DEFAULT NULL, |
| | | `create_user_id` bigint DEFAULT NULL, |
| | | `create_user_name` varchar(100) DEFAULT NULL, |
| | | `is_deleted` tinyint NOT NULL DEFAULT 0, |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), KEY `idx_project_follow_project` (`project_id`,`follow_date`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项ç®å¨æè·è¿'; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `project_management_progress_report` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `project_id` bigint NOT NULL, |
| | | `module_id` bigint NOT NULL, |
| | | `reporter_ids` json NOT NULL, |
| | | `reporter_names` json NOT NULL, |
| | | `reporter_shares` json DEFAULT NULL COMMENT 'æ¬æ¬¡æ±æ¥äººååå·¥ä½éæ¯é', |
| | | `progress` decimal(7,2) NOT NULL, |
| | | `report_date` date NOT NULL, |
| | | `remark` varchar(2000) DEFAULT NULL, |
| | | `attachments` json DEFAULT NULL, |
| | | `is_deleted` tinyint NOT NULL DEFAULT 0, |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), KEY `idx_progress_report_project` (`project_id`,`report_date`), KEY `idx_progress_report_module` (`module_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='é¡¹ç®æ¨¡åè¿åº¦æ±æ¥'; |
| | | |
| | | CALL add_column_if_missing('project_management_progress_report','reporter_shares',"json DEFAULT NULL COMMENT 'æ¬æ¬¡æ±æ¥äººååå·¥ä½éæ¯é'"); |
| | | DROP PROCEDURE IF EXISTS `add_column_if_missing`; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `project_management_material` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `project_id` bigint NOT NULL, |
| | | `material_name` varchar(200) NOT NULL, |
| | | `material_type` varchar(50) DEFAULT NULL, |
| | | `remark` varchar(1000) DEFAULT NULL, |
| | | `attachments` json DEFAULT NULL, |
| | | `create_user_id` bigint DEFAULT NULL, |
| | | `create_user_name` varchar(100) DEFAULT NULL, |
| | | `is_deleted` tinyint NOT NULL DEFAULT 0, |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), KEY `idx_project_material_project` (`project_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='é¡¹ç®æ´ä½èµæåº'; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `project_management_phase` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `project_id` bigint NOT NULL, |
| | | `phase_code` varchar(30) NOT NULL, |
| | | `phase_name` varchar(50) NOT NULL, |
| | | `phase_order` int NOT NULL, |
| | | `status` varchar(20) NOT NULL DEFAULT 'NOT_STARTED' COMMENT 'NOT_STARTED/IN_PROGRESS/COMPLETED', |
| | | `form_data` json DEFAULT NULL COMMENT 'é¶æ®µå¯¹åºä¸å¡ä¿¡æ¯', |
| | | `remark` varchar(2000) DEFAULT NULL, |
| | | `attachments` json DEFAULT NULL, |
| | | `completed_time` datetime DEFAULT NULL, |
| | | `update_user_name` varchar(100) DEFAULT NULL, |
| | | `is_deleted` tinyint NOT NULL DEFAULT 0, |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_project_phase` (`project_id`,`phase_code`), |
| | | KEY `idx_project_phase_order` (`project_id`,`phase_order`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项ç®ç«é¡¹ãå¼åãç³æ¥ãç³æ¥ç»æãéªæ¶æµç¨'; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `government_policy_file` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `policy_name` varchar(255) NOT NULL, |
| | | `publish_date` date DEFAULT NULL, |
| | | `policy_url` varchar(1000) DEFAULT NULL, |
| | | `attachments` json DEFAULT NULL, |
| | | `maintainer_name` varchar(100) DEFAULT NULL, |
| | | `remark` varchar(1000) DEFAULT NULL, |
| | | `is_deleted` tinyint NOT NULL DEFAULT 0, |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), KEY `idx_policy_publish_date` (`publish_date`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='æ¿åºæ¿çæä»¶'; |
| | | |
| | | -- äºé¶æ®µæ¨¡æ¿ï¼ç«é¡¹ â å¼å â ç³æ¥ â ç³æ¥ç»æ â éªæ¶ã |
| | | 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 @five_stage_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 @five_stage_plan_id,1,'ç«é¡¹',1,'å®å项ç®åºæ¬æ
åµãåç±»ãè´è´£äººãç»çã卿忍¡å',0,NOW(),NOW(),'åçº§èæ¬' |
| | | WHERE NOT EXISTS (SELECT 1 FROM project_management_plan_node WHERE project_management_plan_id=@five_stage_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 @five_stage_plan_id,2,'å¼å',1,'å¼å±é¡¹ç®è·è¿ãè¿åº¦æ±æ¥ãç»è®¡åèµæç»´æ¤',0,NOW(),NOW(),'åçº§èæ¬' |
| | | WHERE NOT EXISTS (SELECT 1 FROM project_management_plan_node WHERE project_management_plan_id=@five_stage_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 @five_stage_plan_id,3,'ç³æ¥',1,'å¡«å项ç®ç³æ¥ä¿¡æ¯å¹¶ä¸ä¼ ç³æ¥æä»¶',0,NOW(),NOW(),'åçº§èæ¬' |
| | | WHERE NOT EXISTS (SELECT 1 FROM project_management_plan_node WHERE project_management_plan_id=@five_stage_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 @five_stage_plan_id,4,'ç³æ¥ç»æ',1,'ç»è®°ç³æ¥ç»æå¹¶ä¸ä¼ ç»ææä»¶',0,NOW(),NOW(),'åçº§èæ¬' |
| | | WHERE NOT EXISTS (SELECT 1 FROM project_management_plan_node WHERE project_management_plan_id=@five_stage_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 @five_stage_plan_id,5,'éªæ¶',1,'å¡«åéªæ¶ä¿¡æ¯ãä¸ä¼ éªæ¶æä»¶å¹¶å½æ¡£',0,NOW(),NOW(),'åçº§èæ¬' |
| | | WHERE NOT EXISTS (SELECT 1 FROM project_management_plan_node WHERE project_management_plan_id=@five_stage_plan_id AND name='éªæ¶' AND is_delete=0); |
| | | |
| | | -- å·²ææå·¥é¡¹ç®è¥å°æªæå®å¨æåé¶æ®µï¼è¡¥é»è®¤å¼ï¼ä¸è¦çå·²æå
容ã |
| | | UPDATE project_management_info SET follow_cycle='MONTH' WHERE follow_cycle IS NULL OR follow_cycle=''; |
| | | UPDATE project_management_info SET current_phase='æªå¼å§' WHERE current_phase IS NULL OR current_phase=''; |
| | | UPDATE project_management_info SET project_leader_id=manager_id,project_leader_name=manager_name |
| | | WHERE project_leader_id IS NULL AND manager_id IS NOT NULL; |
| | | |
| | | -- ------------------------------ |
| | | -- èåè·¯ç±éç» |
| | | -- ------------------------------ |
| | | -- è¡æ¿ç®¡çï¼å¤ç¨åâåååå
¬âæ ¹èåã |
| | | UPDATE sys_menu SET menu_name='è¡æ¿ç®¡ç',path='administrationManagement',route_name='AdministrationManagement',order_num=1, |
| | | icon='time-range',visible='0',status='0',remark='请åãåºå·®åç»ä¸å®¡æ¹å
¥å£' |
| | | WHERE menu_id=2053; |
| | | |
| | | -- ç»§ç»ä½¿ç¨é¡¹ç®åæç审æ¹ç®¡çä¸åå审æ¹é¡µé¢ï¼ä¸å¯ç¨å¦ä¸å¥å®¡æ¹æµç¨é
置页é¢ã |
| | | UPDATE sys_menu SET menu_name='审æ¹ç®¡ç',parent_id=2053,order_num=3,path='approvalManagement', |
| | | component='collaborativeApproval/approvalManagement/index',route_name='ApprovalManagement', |
| | | perms='',icon='cascader',visible='0',status='0',app_component=NULL, |
| | | remark='åæå®¡æ¹ç±»åå审æ¹èç¹ç®¡ç' WHERE menu_id=2293; |
| | | UPDATE sys_menu SET menu_name='åå审æ¹',parent_id=2053,order_num=4,path='approvalProcess', |
| | | component='collaborativeApproval/approvalProcess/index',route_name='ApprovalProcess', |
| | | perms='',icon='skill',visible='1',status='1',app_component='/pages/indexItem?label=åå审æ¹', |
| | | remark='åè½ä¸è¯·åãåºå·®å
¥å£éå¤ï¼èååç¨ä½ä¿ç页é¢åå岿°æ®' WHERE menu_id=2054; |
| | | |
| | | 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 '请å管ç',2053,1,'leave','officeProcessAutomation/AttendManage/leave-apply/index','LeaveManagement',1,0,'C','0','0','approval:leave:list','form','admin',NOW(),'å工请åç³è¯·ä¸éæ°æäº¤' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2053 AND path='leave'); |
| | | 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 'åºå·®ç®¡ç',2053,2,'businessTrip','officeProcessAutomation/AttendManage/out-apply/index','BusinessTripManagement',1,0,'C','0','0','approval:trip:list','documentation','admin',NOW(),'åå·¥åºå·®ç³è¯·ä¸éæ°æäº¤' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2053 AND path='businessTrip'); |
| | | UPDATE sys_menu SET component='collaborativeApproval/approvalProcess/index2', |
| | | remark='使ç¨åæåå审æ¹çè¯·åæµç¨ï¼ç±»å2ï¼' WHERE parent_id=2053 AND path='leave'; |
| | | UPDATE sys_menu SET component='collaborativeApproval/approvalProcess/index3', |
| | | remark='使ç¨åæåå审æ¹çåºå·®æµç¨ï¼ç±»å3ï¼' WHERE parent_id=2053 AND path='businessTrip'; |
| | | |
| | | -- 客æ·ç®¡çï¼å¤ç¨åâè¥é管çâæ ¹èåã |
| | | UPDATE sys_menu SET menu_name='客æ·ç®¡ç',path='customerManagement',route_name='CustomerManagement',order_num=2, |
| | | icon='peoples',visible='0',status='0',remark='客æ·ä¿¡æ¯ä¸è¿ä¸å¹´è´¢å¡æ¥è¡¨' WHERE menu_id=2010; |
| | | UPDATE sys_menu SET menu_name='客æ·ä¿¡æ¯ç»´æ¤',parent_id=2010,order_num=1,path='customerFile', |
| | | component='basicData/customerFile/index',route_name='CustomerFile',visible='0',status='0' WHERE menu_id=2001; |
| | | UPDATE sys_menu SET menu_name='è¿ä¸å¹´è´¢å¡æ¥è¡¨',parent_id=2010,order_num=2,path='financialReport', |
| | | component='salesManagement/reportAudit/index',route_name='CustomerFinancialReport', |
| | | perms='sales:report:audit',visible='1',status='1',remark='å·²åå¹¶å°å®¢æ·ä¿¡æ¯ç»´æ¤ï¼ä¸ååç¬æ¾ç¤º' WHERE menu_id=2369; |
| | | |
| | | -- ä¸å¡ç®¡çæ ¹èåï¼å¹¶è¿ç§»åéå®å°è´¦é¡µé¢ã |
| | | 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,3,'businessManagement',NULL,'BusinessManagement',1,0,'M','0','0','','documentation','admin',NOW(),'å
³è客æ·çä¸å¡ç»è®°ãéä»¶ãè¿åº¦ä¸å®¡æ¹' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=0 AND path='businessManagement'); |
| | | SET @business_root=(SELECT menu_id FROM sys_menu WHERE parent_id=0 AND path='businessManagement' ORDER BY menu_id LIMIT 1); |
| | | UPDATE sys_menu SET menu_name='ä¸å¡ç»è®°',parent_id=@business_root,order_num=1,path='businessLedger', |
| | | component='salesManagement/salesLedger/index',route_name='BusinessLedger',visible='0',status='0',remark='ä¸å¡ç»è®°ãéä»¶ãè¿åº¦ä¸å®¡æ¹' |
| | | WHERE menu_id=2011; |
| | | |
| | | UPDATE sys_menu SET order_num=4,visible='0',status='0' WHERE menu_id=2276; |
| | | UPDATE sys_menu SET menu_name='项ç®å表',order_num=1,path='projectList',route_name='ProjectList',visible='0',status='0' WHERE menu_id=2285; |
| | | 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 '项ç®åç±»',2276,2,'projectCategory','projectManagement/projectCategory/index','ProjectCategory',1,0,'C','0','0','project:category:list','tree','admin',NOW(),'å¤å±æ 形项ç®å类维æ¤' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2276 AND path='projectCategory'); |
| | | |
| | | UPDATE sys_menu SET order_num=5,visible='0',status='0' WHERE menu_id=2015; |
| | | UPDATE sys_menu SET menu_name='éè´ä¿¡æ¯',order_num=1,route_name='ProcurementLedger',visible='0',status='0' WHERE menu_id=2016; |
| | | |
| | | -- æ¿åºæ¿çæä»¶ã |
| | | 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,6,'governmentPolicy',NULL,'GovernmentPolicy',1,0,'M','0','0','','documentation','admin',NOW(),'æ¿ç便®èµæåº' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=0 AND path='governmentPolicy'); |
| | | SET @policy_root=(SELECT menu_id FROM sys_menu WHERE parent_id=0 AND path='governmentPolicy' 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 'æ¿çæä»¶ç»´æ¤',@policy_root,1,'policyFile','governmentPolicy/policyFile/index','PolicyFile',1,0,'C','0','0','policy:file:list','documentation','admin',NOW(),'æ¿çåç§°ãå叿¶é´ã龿¥ãæä»¶åç»´æ¤äºº' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=@policy_root AND path='policyFile'); |
| | | |
| | | -- 客æ·è´¦å·å
¥å£ä¿çï¼ä½åªåé
ç»å®¢æ·è§è²ï¼ä¸æä¾èªè¡æ³¨åã |
| | | UPDATE sys_menu SET menu_name='客æ·èµæ',order_num=90,visible='0',status='0' WHERE menu_id=2357; |
| | | UPDATE sys_menu SET menu_name='è´¢å¡æ¥è¡¨ä¸ä¼ ',parent_id=2357,order_num=1,visible='0',status='0' WHERE menu_id=2358; |
| | | UPDATE sys_menu SET status='1',visible='1' WHERE menu_id=2359; |
| | | |
| | | -- é¡¹ç®æé®æéã |
| | | SET @project_list_menu=2285; |
| | | 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 '模åç»´æ¤',@project_list_menu,21,'#',NULL,'',1,0,'F','0','0','project:module:edit','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:module:edit'); |
| | | 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 '模åå é¤',@project_list_menu,22,'#',NULL,'',1,0,'F','0','0','project:module:remove','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:module:remove'); |
| | | 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 'æ°å¢è·è¿',@project_list_menu,23,'#',NULL,'',1,0,'F','0','0','project:follow:add','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:follow:add'); |
| | | 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 'å é¤è·è¿',@project_list_menu,24,'#',NULL,'',1,0,'F','0','0','project:follow:remove','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:follow:remove'); |
| | | 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 'è¿åº¦æ±æ¥',@project_list_menu,25,'#',NULL,'',1,0,'F','0','0','project:report:add','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:report:add'); |
| | | 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 '项ç®èµæç»´æ¤',@project_list_menu,26,'#',NULL,'',1,0,'F','0','0','project:material:add','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:material:add'); |
| | | 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 'å é¤è¿åº¦æ±æ¥',@project_list_menu,27,'#',NULL,'',1,0,'F','0','0','project:report:remove','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:report:remove'); |
| | | 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 'å é¤é¡¹ç®èµæ',@project_list_menu,28,'#',NULL,'',1,0,'F','0','0','project:material:remove','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:material:remove'); |
| | | 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 'ç»´æ¤é¡¹ç®é¶æ®µ',@project_list_menu,29,'#',NULL,'',1,0,'F','0','0','project:phase:edit','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:phase:edit'); |
| | | |
| | | SET @project_category_menu=(SELECT menu_id FROM sys_menu WHERE parent_id=2276 AND path='projectCategory' 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 'ç»´æ¤é¡¹ç®åç±»',@project_category_menu,1,'#',NULL,'',1,0,'F','0','0','project:category:edit','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:category:edit'); |
| | | 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 'å é¤é¡¹ç®åç±»',@project_category_menu,2,'#',NULL,'',1,0,'F','0','0','project:category:remove','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:category:remove'); |
| | | |
| | | SET @policy_file_menu=(SELECT menu_id FROM sys_menu WHERE parent_id=@policy_root AND path='policyFile' 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 'ç»´æ¤æ¿çæä»¶',@policy_file_menu,1,'#',NULL,'',1,0,'F','0','0','policy:file:edit','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='policy:file:edit'); |
| | | 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 'å 餿¿çæä»¶',@policy_file_menu,2,'#',NULL,'',1,0,'F','0','0','policy:file:remove','#','admin',NOW(),'' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='policy:file:remove'); |
| | | |
| | | -- è¶
级管çåè§è²è¡¥é½æ°å¢èåï¼å
¶ä»è§è²ä»å¨âè§è²ç®¡çâ䏿éé
ç½®ã |
| | | INSERT IGNORE INTO sys_role_menu(role_id,menu_id) |
| | | SELECT 1,menu_id FROM sys_menu WHERE status='0' AND ( |
| | | menu_id IN (2053,2010,2015,2276,2357,@business_root,@policy_root) |
| | | OR parent_id IN (2053,2010,2015,2276,2357,@business_root,@policy_root,2285) |
| | | ); |
| | | |
| | | SET FOREIGN_KEY_CHECKS = 1; |
| | | |
| | | SELECT 'five_stage_nodes' AS item,COUNT(*) AS count_value FROM project_management_plan_node |
| | | WHERE project_management_plan_id=@five_stage_plan_id AND is_delete=0 |
| | | UNION ALL SELECT 'project_workflow_tables',COUNT(*) FROM information_schema.TABLES |
| | | WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME IN ( |
| | | 'project_management_category','project_management_module','project_management_module_member', |
| | | 'project_management_follow_up','project_management_progress_report','project_management_material', |
| | | 'project_management_phase','government_policy_file' |
| | | ) |
| | | UNION ALL SELECT 'enabled_root_menus',COUNT(*) FROM sys_menu WHERE parent_id=0 AND status='0'; |
| | |
| | | 6. 管çåå¨ç°æâç¨æ·ç®¡çâå建客æ·è´¦å·ï¼èµäºâ客æ·âè§è²ï¼åå¨ç°æâå®¢æ·æ¡£æ¡âä¸æè¯¥ç¨æ·ç»å®å°å¯¹åºå®¢æ·ãå
é¨åå·¥èåæéä»å¨ç°æè§è²ç®¡çä¸é
ç½®ã |
| | | |
| | | åæ»æ¹å¼ï¼åæ¢åºç¨ï¼å建ä¸ä¸ªç©ºåºå导å
¥ä¸è¿°å
¨éå¤ä»½ï¼ç¡®è®¤æ 误ååæ¢è¿æ¥ãä¸è¦å¨ææ°ä¸å¡æ°æ®äº§çåç´æ¥è¦çç产åºã |
| | | |
| | | ## 2026-11-03 项ç®ä¸å¡æµç¨å级 |
| | | |
| | | 1. æ¬æ¬¡å级åå
¨éå¤ä»½ï¼`D:\newcode\product-inventory-management\.local-backups\product-inventory-management-xmgl-before-workflow-upgrade-20261103.sql`ï¼çº¦ 83.6 MBï¼ã |
| | | 2. å¨ `product-inventory-management-xmgl` æ°æ®åºæ§è¡ `20261103_project_workflow_upgrade.sql`ã |
| | | 3. èæ¬ä¼è°æ´å¨æèåä¸ºè¡æ¿ç®¡çã客æ·ç®¡çãä¸å¡ç®¡çã项ç®ç®¡çãéè´ç®¡çãæ¿åºæ¿çæä»¶åç³»ç»ç®¡çï¼å¹¶ä¿ç客æ·èµæå
¥å£ï¼è¡æ¿ç®¡ç使ç¨åæâ审æ¹ç®¡çâç»´æ¤å®¡æ¹äººï¼è¯·åååºå·®ä½¿ç¨å审æ¹ä½ç³»ï¼éå¤çâåå审æ¹âæ±æ»å
¥å£é»è®¤åç¨ã |
| | | 4. èæ¬æ°å¢é¡¹ç®åç±»ãæ¨¡å/æ§è¡äººãè·è¿ãè¿åº¦æ±æ¥ã项ç®èµæãäºé¶æ®µæµç¨ãæ¿çæä»¶ç 8 å¼ ä¸å¡è¡¨ï¼å¹¶ä¸ºé¡¹ç®è¡¥å
è´è´£äººã管çåãè·è¿å¨æãå½åé¶æ®µåæ¥æºä¸å¡å段ã |
| | | 5. æ°é¡¹ç®ä¸åä½¿ç¨æ§è®¡å模æ¿é¶æ®µï¼ç¬ç«æï¼ç«é¡¹ãå¼åãç³æ¥ãç³æ¥ç»æãéªæ¶é¡ºåºç»´æ¤å¯¹åºä¿¡æ¯åæä»¶ï¼æ§é¶æ®µæ°æ®åªä¿çå
¼å®¹ï¼ä¸å¨æ°é¡µé¢ä½¿ç¨ã |
| | | 6. èæ¬æ«å°¾æ ¡éªç»æåºä¸ºï¼`project_workflow_tables = 8`ã`enabled_root_menus = 8`ã |
| | | |
| | | å¦éåæ»æ¬æ¬¡å级ï¼è¯·åæ¢åå端åï¼æä¸è¿° `before-workflow-upgrade` å¤ä»½å¯¼å
¥æ°çç©ºæ°æ®åºï¼æ ¸å¯¹æ°æ®åååæ¢è¿æ¥ï¼ä¸è¦ç´æ¥è¦çä»å¨ä½¿ç¨çæ°æ®åºã |
| | |
| | | }) |
| | | } |
| | | |
| | | export function delProject(ids) { |
| | | export function delProject(id) { |
| | | return request({ |
| | | url: '/projectManagement/info/remove', |
| | | method: 'delete', |
| | | data: ids |
| | | url: `/projectManagement/info/delete/${id}`, |
| | | method: 'post' |
| | | }) |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from '@/utils/request' |
| | | |
| | | export const getProjectWorkflow = projectId => request({ |
| | | url: `/projectManagement/workflow/${projectId}`, |
| | | method: 'get' |
| | | }) |
| | | export const startProjectWorkflow = projectId => request({ url: `/projectManagement/workflow/start/${projectId}`, method: 'post' }) |
| | | export const updateProjectExecutors = data => request({ url: '/projectManagement/workflow/project-executors', method: 'post', data }) |
| | | export const transitionProjectStage = data => request({ url: '/projectManagement/workflow/transition', method: 'post', data }) |
| | | |
| | | export const saveProjectModule = data => request({ url: '/projectManagement/workflow/module', method: 'post', data }) |
| | | export const deleteProjectModule = id => request({ url: `/projectManagement/workflow/module/${id}`, method: 'delete' }) |
| | | export const saveProjectFollowUp = data => request({ url: '/projectManagement/workflow/follow-up', method: 'post', data }) |
| | | export const deleteProjectFollowUp = id => request({ url: `/projectManagement/workflow/follow-up/${id}`, method: 'delete' }) |
| | | export const saveProjectProgressReport = data => request({ url: '/projectManagement/workflow/report', method: 'post', data }) |
| | | export const deleteProjectProgressReport = id => request({ url: `/projectManagement/workflow/report/${id}`, method: 'delete' }) |
| | | export const saveProjectMaterial = data => request({ url: '/projectManagement/workflow/material', method: 'post', data }) |
| | | export const deleteProjectMaterial = id => request({ url: `/projectManagement/workflow/material/${id}`, method: 'delete' }) |
| | | export const saveProjectPhase = data => request({ url: '/projectManagement/workflow/phase', method: 'post', data }) |
| | | |
| | | export const listProjectCategories = () => request({ url: '/projectManagement/workflow/category/list', method: 'get' }) |
| | | export const saveProjectCategory = data => request({ url: '/projectManagement/workflow/category', method: 'post', data }) |
| | | export const deleteProjectCategory = id => request({ url: `/projectManagement/workflow/category/${id}`, method: 'delete' }) |
| | | |
| | | export const listGovernmentPolicies = params => request({ url: '/projectManagement/workflow/policy/list', method: 'get', params }) |
| | | export const saveGovernmentPolicy = data => request({ url: '/projectManagement/workflow/policy', method: 'post', data }) |
| | | export const deleteGovernmentPolicy = id => request({ url: `/projectManagement/workflow/policy/${id}`, method: 'delete' }) |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ --> |
| | | <div class="type-tabs"> |
| | | <div v-if="!props.approveType" class="type-tabs"> |
| | | <div v-for="type in approveTypes" |
| | | :key="type.value" |
| | | class="type-tab" |
| | |
| | | |
| | | const userStore = useUserStore(); |
| | | const route = useRoute(); |
| | | const props = defineProps({ |
| | | approveType: { |
| | | type: Number, |
| | | default: null, |
| | | }, |
| | | }); |
| | | |
| | | // å½åéä¸çæ ç¾é¡µï¼é»è®¤ä¸ºè¯·å管ç |
| | | const activeTab = ref("2"); |
| | | const activeTab = ref(String(props.approveType || 2)); |
| | | |
| | | // åç±»åæ°éç»è®¡ |
| | | const typeCounts = ref({}); |
| | |
| | | if (approveType) { |
| | | // 设置æ ç¾é¡µï¼approveType å¯¹åº activeTab ç nameï¼ |
| | | activeTab.value = String(approveType); |
| | | } else if (props.approveType) { |
| | | activeTab.value = String(props.approveType); |
| | | } |
| | | |
| | | if (approveId) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-card shadow="never"> |
| | | <div class="toolbar"> |
| | | <el-input v-model="keyword" clearable placeholder="请è¾å
¥æ¿çåç§°" style="width: 280px" @keyup.enter="loadData" /> |
| | | <el-button type="primary" @click="loadData">æ¥è¯¢</el-button> |
| | | <el-button @click="keyword = ''; loadData()">éç½®</el-button> |
| | | <div class="toolbar-spacer" /> |
| | | <el-button v-hasPermi="['policy:file:edit']" type="primary" @click="openDialog()">æ°å¢æ¿çæä»¶</el-button> |
| | | </div> |
| | | |
| | | <el-table v-loading="loading" :data="rows" border> |
| | | <el-table-column prop="policyName" label="æ¿çåç§°" min-width="240" show-overflow-tooltip /> |
| | | <el-table-column prop="publishDate" label="å叿¶é´" width="130" /> |
| | | <el-table-column label="åæé¾æ¥" min-width="200"> |
| | | <template #default="{ row }"> |
| | | <el-link v-if="row.policyUrl" :href="row.policyUrl" target="_blank" type="primary">æå¼é¾æ¥</el-link> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä»¶" min-width="220"> |
| | | <template #default="{ row }"> |
| | | <div v-if="row.attachmentList?.length" class="file-list"> |
| | | <el-button v-for="(file, index) in row.attachmentList" :key="file.id || index" link type="primary" @click="download(file)"> |
| | | {{ file.name || file.fileName || `éä»¶${index + 1}` }} |
| | | </el-button> |
| | | </div> |
| | | <span v-else>-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="maintainerName" label="ç»´æ¤äºº" width="130" /> |
| | | <el-table-column prop="remark" label="夿³¨" min-width="180" show-overflow-tooltip /> |
| | | <el-table-column label="æä½" width="150" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button v-hasPermi="['policy:file:edit']" link type="primary" @click="openDialog(row)">ä¿®æ¹</el-button> |
| | | <el-button v-hasPermi="['policy:file:remove']" link type="danger" @click="removeRow(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-dialog v-model="dialogVisible" :title="form.id ? 'ä¿®æ¹æ¿çæä»¶' : 'æ°å¢æ¿çæä»¶'" width="680px" destroy-on-close> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="90px"> |
| | | <el-form-item label="æ¿çåç§°" prop="policyName"> |
| | | <el-input v-model="form.policyName" maxlength="255" show-word-limit /> |
| | | </el-form-item> |
| | | <el-form-item label="å叿¶é´" prop="publishDate"> |
| | | <el-date-picker v-model="form.publishDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="åæé¾æ¥"> |
| | | <el-input v-model="form.policyUrl" placeholder="https://" /> |
| | | </el-form-item> |
| | | <el-form-item label="æä»¶"> |
| | | <AttachmentUploadFile v-model:file-list="fileList" :limit="10" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç»´æ¤äºº"> |
| | | <el-input v-model="form.maintainerName" placeholder="ä¸å¡«ååå½åç»å½äºº" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨"> |
| | | <el-input v-model="form.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="dialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" :loading="saving" @click="submit">ä¿å</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="PolicyFile"> |
| | | import { getCurrentInstance, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue' |
| | | import { deleteGovernmentPolicy, listGovernmentPolicies, saveGovernmentPolicy } from '@/api/projectManagement/workflow' |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const loading = ref(false) |
| | | const saving = ref(false) |
| | | const keyword = ref('') |
| | | const rows = ref([]) |
| | | const dialogVisible = ref(false) |
| | | const formRef = ref() |
| | | const fileList = ref([]) |
| | | const form = reactive({ id: null, policyName: '', publishDate: '', policyUrl: '', maintainerName: '', remark: '' }) |
| | | const rules = { |
| | | policyName: [{ required: true, message: '请è¾å
¥æ¿çåç§°', trigger: 'blur' }], |
| | | publishDate: [{ required: true, message: 'è¯·éæ©å叿¶é´', trigger: 'change' }] |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | | const res = await listGovernmentPolicies({ keyword: keyword.value }) |
| | | rows.value = Array.isArray(res?.data) ? res.data : [] |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | function openDialog(row = null) { |
| | | Object.assign(form, { |
| | | id: row?.id || null, |
| | | policyName: row?.policyName || '', |
| | | publishDate: row?.publishDate || '', |
| | | policyUrl: row?.policyUrl || '', |
| | | maintainerName: row?.maintainerName || '', |
| | | remark: row?.remark || '' |
| | | }) |
| | | fileList.value = (row?.attachmentList || []).map(file => ({ ...file })) |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | function normalizeFiles() { |
| | | return fileList.value.map(file => ({ |
| | | id: file.id ?? file.fileId ?? file.rawData?.id, |
| | | name: file.name ?? file.fileName ?? file.rawData?.originalFilename, |
| | | url: file.url ?? file.fileUrl ?? file.rawData?.url |
| | | })).filter(file => file.id || file.url) |
| | | } |
| | | |
| | | async function submit() { |
| | | await formRef.value?.validate() |
| | | saving.value = true |
| | | try { |
| | | await saveGovernmentPolicy({ ...form, attachments: normalizeFiles() }) |
| | | ElMessage.success('ä¿åæå') |
| | | dialogVisible.value = false |
| | | await loadData() |
| | | } finally { |
| | | saving.value = false |
| | | } |
| | | } |
| | | |
| | | async function removeRow(row) { |
| | | await ElMessageBox.confirm(`确认å é¤â${row.policyName}âåï¼`, 'æç¤º', { type: 'warning' }) |
| | | await deleteGovernmentPolicy(row.id) |
| | | ElMessage.success('å 餿å') |
| | | await loadData() |
| | | } |
| | | |
| | | function download(file) { |
| | | if (!file?.url) return ElMessage.warning('éä»¶ææ ä¸è½½å°å') |
| | | proxy.$download.byUrl(file.url, file.name || file.fileName) |
| | | } |
| | | |
| | | onMounted(loadData) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; } |
| | | .toolbar-spacer { flex: 1; } |
| | | .file-list { display: flex; flex-wrap: wrap; gap: 4px 8px; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div v-loading="loading" class="execution-panel"> |
| | | <el-tabs v-model="activeTab" @tab-change="handleTabChange"> |
| | | <el-tab-pane v-if="data.sourceBusiness?.id" label="æ¥æºä¸å¡" name="business"> |
| | | <el-descriptions :column="3" border> |
| | | <el-descriptions-item label="ä¸å¡ç¼å·">{{ data.sourceBusiness.id }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®åç§°">{{ data.sourceBusiness.projectName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ data.sourceBusiness.customerName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡å">{{ data.sourceBusiness.salesman || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å½å
¥æ¥æ">{{ data.sourceBusiness.entryDate || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="审æ¹ç¶æ">{{ ({ 0:'è稿',1:'å¾
审æ¹',2:'å·²éè¿',3:'å·²éå' })[data.sourceBusiness.approvalStatus] || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡å¤æ³¨" :span="3">{{ data.sourceBusiness.remarks || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡éä»¶" :span="3"> |
| | | <template v-if="data.sourceBusiness.attachmentList?.length"> |
| | | <el-button v-for="(file,index) in data.sourceBusiness.attachmentList" :key="index" link type="primary" @click="download(file)">{{ file.name || file.fileName || `éä»¶${index+1}` }}</el-button> |
| | | </template> |
| | | <span v-else>{{ data.sourceBusiness.attachmentMaterials || 'æ ' }}</span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="模å设置" name="modules"> |
| | | <div class="tab-actions"> |
| | | <span class="hint">模åå 项ç®å·¥ä½éå计ï¼{{ totalModuleWeight }}%</span> |
| | | <el-button v-hasPermi="['project:module:edit']" type="primary" @click="openModule()">æ°å¢æ¨¡å</el-button> |
| | | </div> |
| | | <el-table :data="data.modules" border> |
| | | <el-table-column prop="moduleName" label="模ååç§°" min-width="160" /> |
| | | <el-table-column prop="plannedStartDate" label="é¢è®¡å¼å§" width="120" /> |
| | | <el-table-column prop="summary" label="æ¨¡åæ¦è¦" min-width="220" show-overflow-tooltip /> |
| | | <el-table-column prop="projectWeight" label="项ç®å·¥ä½éå æ¯" width="150" align="center"> |
| | | <template #default="{ row }">{{ row.projectWeight }}%</template> |
| | | </el-table-column> |
| | | <el-table-column prop="currentProgress" label="å½åè¿åº¦" width="120" align="center"> |
| | | <template #default="{ row }"><el-progress :percentage="Number(row.currentProgress || 0)" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¨¡åæ§è¡äºº" min-width="230"> |
| | | <template #default="{ row }"> |
| | | <el-tag v-for="member in row.members || []" :key="member.id" class="member-tag"> |
| | | {{ member.userName }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="140" align="center" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button v-hasPermi="['project:module:edit']" link type="primary" @click="openModule(row)">ä¿®æ¹</el-button> |
| | | <el-button v-hasPermi="['project:module:remove']" link type="danger" @click="removeModule(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="项ç®è·è¿" name="follow"> |
| | | <div class="tab-actions"> |
| | | <div class="hint"> |
| | | è·è¿å¨æï¼{{ followCycleLabel }}ï¼ |
| | | <span v-if="data.followWindow?.dueDate">æ¬æ¬¡åºè·è¿æ¥ {{ data.followWindow.dueDate }}ï¼å¯å¡«å {{ data.followWindow.windowStart }} è³ {{ data.followWindow.windowEnd }}</span> |
| | | <span v-else>馿¬¡è·è¿å¯ç´æ¥å¡«å</span> |
| | | </div> |
| | | <el-button v-hasPermi="['project:follow:add']" type="primary" @click="openFollow()">å¡«åè·è¿</el-button> |
| | | </div> |
| | | <el-table :data="data.followUps" border> |
| | | <el-table-column prop="followDate" label="è·è¿æ¥æ" width="130" /> |
| | | <el-table-column prop="createUserName" label="项ç®è´è´£äºº" width="140" /> |
| | | <el-table-column prop="detail" label="è·è¿è¯¦æ
" min-width="300" show-overflow-tooltip /> |
| | | <el-table-column label="éä»¶" width="100" align="center"><template #default="{ row }">{{ row.attachmentList?.length || 0 }}</template></el-table-column> |
| | | <el-table-column label="æä½" width="130" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:follow:remove']" link type="danger" @click="removeRecord('follow', row)">å é¤</el-button></template></el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="è¿åº¦æ±æ¥ä¸ç»è®¡" name="reports"> |
| | | <div class="tab-actions"> |
| | | <span class="hint">æå宿æ¯éæ ¹æ®æ¨¡åå·¥ä½éãæ¯æ¬¡æ°å¢è¿åº¦åè¯¥æ¬¡æ±æ¥äººåæ¯é计ç®</span> |
| | | <el-button v-hasPermi="['project:report:add']" type="primary" :disabled="!eligibleReportModules.length" @click="openReport()">æ°å¢æ±æ¥</el-button> |
| | | </div> |
| | | <div class="report-layout"> |
| | | <div ref="chartRef" class="chart" /> |
| | | <el-table :data="data.reports" border class="report-table"> |
| | | <el-table-column prop="moduleName" label="é¡¹ç®æ¨¡å" min-width="130" /> |
| | | <el-table-column label="æ±æ¥äººåæ¯é" min-width="210"><template #default="{ row }"><el-tag v-for="member in row.reporterShareList || []" :key="member.userId" class="member-tag">{{ member.userName }} {{ member.moduleShare }}%</el-tag></template></el-table-column> |
| | | <el-table-column prop="progress" label="模åè¿åº¦" width="100"><template #default="{ row }">{{ row.progress }}%</template></el-table-column> |
| | | <el-table-column prop="reportDate" label="æ¶é´" width="120" /> |
| | | <el-table-column prop="remark" label="夿³¨" min-width="180" show-overflow-tooltip /> |
| | | <el-table-column label="æä»¶" width="80" align="center"><template #default="{ row }">{{ row.attachmentList?.length || 0 }}</template></el-table-column> |
| | | <el-table-column label="æä½" width="90" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:report:remove']" link type="danger" @click="removeRecord('report', row)">å é¤</el-button></template></el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="项ç®èµæ" name="materials"> |
| | | <div class="tab-actions"> |
| | | <span class="hint">é¡¹ç®æ´ä½èµæåºï¼ä¸ä¸å¡éä»¶å忬¡æ±æ¥éä»¶åå¼ä¿å</span> |
| | | <el-button v-hasPermi="['project:material:add']" type="primary" @click="openMaterial()">ä¸ä¼ èµæ</el-button> |
| | | </div> |
| | | <el-table :data="data.materials" border> |
| | | <el-table-column prop="materialName" label="èµæåç§°" min-width="220" /> |
| | | <el-table-column prop="materialType" label="èµæç±»å" width="130" /> |
| | | <el-table-column prop="createUserName" label="ç»´æ¤äºº" width="130" /> |
| | | <el-table-column prop="createTime" label="ä¸ä¼ æ¶é´" width="180" /> |
| | | <el-table-column prop="remark" label="夿³¨" min-width="200" show-overflow-tooltip /> |
| | | <el-table-column label="æä»¶" min-width="200"> |
| | | <template #default="{ row }"> |
| | | <el-button v-for="(file, index) in row.attachmentList || []" :key="file.id || index" link type="primary" @click="download(file)"> |
| | | {{ file.name || `éä»¶${index + 1}` }} |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="90" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:material:remove']" link type="danger" @click="removeRecord('material', row)">å é¤</el-button></template></el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <el-dialog v-model="moduleVisible" :title="moduleForm.id ? 'ä¿®æ¹æ¨¡å' : 'æ°å¢æ¨¡å'" width="760px" destroy-on-close> |
| | | <el-form ref="moduleFormRef" :model="moduleForm" :rules="moduleRules" label-width="130px"> |
| | | <el-alert v-if="moduleForm.locked" title="该模åå·²æè¿åº¦æ±æ¥ï¼åºç¡ä¿¡æ¯å模åå æ¯å·²éå®ï¼ä»
å¯è°æ´æ§è¡äººã" type="warning" :closable="false" class="dialog-tip" /> |
| | | <el-form-item label="模ååç§°" prop="moduleName"><el-input v-model="moduleForm.moduleName" :disabled="moduleForm.locked" /></el-form-item> |
| | | <el-form-item label="é¢è®¡å¼å§æ¶é´" prop="plannedStartDate"><el-date-picker v-model="moduleForm.plannedStartDate" :disabled="moduleForm.locked" type="date" value-format="YYYY-MM-DD" style="width: 100%" /></el-form-item> |
| | | <el-form-item label="æ¨¡åæ¦è¦" prop="summary"><el-input v-model="moduleForm.summary" :disabled="moduleForm.locked" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item> |
| | | <el-form-item label="项ç®å·¥ä½éå æ¯" prop="projectWeight"><el-input-number v-model="moduleForm.projectWeight" :disabled="moduleForm.locked" :min="0" :max="moduleWeightMax" :precision="2" /><span class="unit">%</span><div class="percent-hint">å
¶ä»æ¨¡åå计 {{ otherModuleWeight }}%ï¼å½å模åå¯è°èå´ 0ï½{{ moduleWeightMax }}%ï¼ä¿ååæ»è®¡ {{ moduleWeightAfterChange }}%</div></el-form-item> |
| | | <el-form-item label="æ¨¡åæ§è¡äºº" required> |
| | | <div class="member-editor"> |
| | | <div v-for="(member, index) in moduleForm.members" :key="index" class="member-row"> |
| | | <el-select v-model="member.userId" filterable placeholder="è¯·éæ©æ§è¡äºº" @change="id => syncMemberName(member, id)"> |
| | | <el-option v-for="user in projectExecutorOptions" :key="user.value" :label="user.label" :value="user.value" /> |
| | | </el-select> |
| | | <el-button link type="danger" @click="moduleForm.members.splice(index, 1)">å é¤</el-button> |
| | | </div> |
| | | <el-button link type="primary" @click="moduleForm.members.push({ userId: null, userName: '' })">+ æ·»å æ§è¡äºº</el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer><el-button @click="moduleVisible = false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submitModule">ä¿å</el-button></template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="followVisible" title="å¡«å项ç®è·è¿" width="650px" destroy-on-close> |
| | | <el-form ref="followFormRef" :model="followForm" :rules="followRules" label-width="90px"> |
| | | <el-form-item label="è·è¿æ¥æ" prop="followDate"><el-date-picker v-model="followForm.followDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" /></el-form-item> |
| | | <el-form-item label="è·è¿è¯¦æ
" prop="detail"><el-input v-model="followForm.detail" type="textarea" :rows="5" maxlength="2000" show-word-limit /></el-form-item> |
| | | <el-form-item label="éä»¶"><AttachmentUploadFile v-model:file-list="followFiles" :limit="10" /></el-form-item> |
| | | </el-form> |
| | | <template #footer><el-button @click="followVisible = false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submitFollow">æäº¤</el-button></template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="reportVisible" title="è¿åº¦æ±æ¥" width="680px" destroy-on-close> |
| | | <el-form ref="reportFormRef" :model="reportForm" :rules="reportRules" label-width="100px"> |
| | | <el-form-item label="æå±é¡¹ç®"><el-input :model-value="projectInfo?.title" disabled /></el-form-item> |
| | | <el-form-item label="é¡¹ç®æ¨¡å" prop="moduleId"><el-select v-model="reportForm.moduleId" style="width: 100%" @change="changeReportModule"><el-option v-for="item in eligibleReportModules" :key="item.id" :label="item.moduleName" :value="item.id" /></el-select><div class="percent-hint">ä»
å±ç¤ºå½åç»å½äººå䏿§è¡ç模åã</div></el-form-item> |
| | | <el-form-item label="æ±æ¥äººåæ¯é" required> |
| | | <div class="member-editor"> |
| | | <div v-for="(member,index) in reportForm.reporterShares" :key="index" class="reporter-row"> |
| | | <el-select v-model="member.userId" filterable placeholder="è¯·éæ©äººå" @change="id => syncMemberName(member,id)"><el-option v-for="user in moduleReporterOptions" :key="user.value" :label="user.label" :value="user.value" /></el-select> |
| | | <el-input-number v-model="member.moduleShare" :min="0" :max="reporterShareLimit(index)" :precision="2" /> |
| | | <span>%</span> |
| | | <el-button link type="danger" @click="reportForm.reporterShares.splice(index,1)">å é¤</el-button> |
| | | </div> |
| | | <el-button link type="primary" @click="reportForm.reporterShares.push({userId:null,userName:'',moduleShare:0})">+ æ·»å 人å</el-button> |
| | | <div class="share-total">æ¬æ¬¡äººåæ¯éå计ï¼{{ reportShareTotal }}%ï¼å©ä½å¯åé
ï¼{{ reportShareRemaining }}%ãæ¯äººçä¸éä¼éå
¶ä»äººåå æ¯èªå¨è°æ´ã</div> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="模åè¿åº¦" prop="progress"><el-input-number v-model="reportForm.progress" :min="0" :max="100" :precision="2" /><span class="unit">%</span></el-form-item> |
| | | <el-form-item label="æ±æ¥æ¶é´" prop="reportDate"><el-date-picker v-model="reportForm.reportDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" /></el-form-item> |
| | | <el-form-item label="夿³¨"><el-input v-model="reportForm.remark" type="textarea" :rows="3" maxlength="2000" show-word-limit /></el-form-item> |
| | | <el-form-item label="æä»¶"><AttachmentUploadFile v-model:file-list="reportFiles" :limit="10" /></el-form-item> |
| | | </el-form> |
| | | <template #footer><el-button @click="reportVisible = false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submitReport">æäº¤æ±æ¥</el-button></template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="materialVisible" title="ä¸ä¼ 项ç®èµæ" width="650px" destroy-on-close> |
| | | <el-form ref="materialFormRef" :model="materialForm" :rules="materialRules" label-width="90px"> |
| | | <el-form-item label="èµæåç§°" prop="materialName"><el-input v-model="materialForm.materialName" /></el-form-item> |
| | | <el-form-item label="èµæç±»å"><el-select v-model="materialForm.materialType" allow-create filterable clearable style="width: 100%"><el-option v-for="type in ['ç«é¡¹èµæ','å¼åèµæ','ç³æ¥èµæ','ç»æèµæ','éªæ¶èµæ','å
¶ä»']" :key="type" :label="type" :value="type" /></el-select></el-form-item> |
| | | <el-form-item label="æä»¶" required><AttachmentUploadFile v-model:file-list="materialFiles" :limit="20" /></el-form-item> |
| | | <el-form-item label="夿³¨"><el-input v-model="materialForm.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item> |
| | | </el-form> |
| | | <template #footer><el-button @click="materialVisible = false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submitMaterial">ä¿å</el-button></template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import * as echarts from 'echarts' |
| | | import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue' |
| | | import { userListAll } from '@/api/publicApi' |
| | | import { |
| | | deleteProjectFollowUp, deleteProjectMaterial, deleteProjectModule, deleteProjectProgressReport, |
| | | getProjectWorkflow, saveProjectFollowUp, saveProjectMaterial, saveProjectModule, saveProjectProgressReport |
| | | } from '@/api/projectManagement/workflow' |
| | | import useUserStore from '@/store/modules/user' |
| | | |
| | | const props = defineProps({ projectId: [String, Number], projectInfo: { type: Object, default: () => ({}) } }) |
| | | const { proxy } = getCurrentInstance() |
| | | const userStore = useUserStore() |
| | | const loading = ref(false) |
| | | const saving = ref(false) |
| | | const activeTab = ref('modules') |
| | | const userOptions = ref([]) |
| | | const data = reactive({ modules: [], followUps: [], reports: [], materials: [], sourceBusiness: {}, memberStatistics: [], followWindow: {} }) |
| | | const chartRef = ref() |
| | | let chart |
| | | |
| | | const today = () => new Date().toISOString().slice(0, 10) |
| | | const totalModuleWeight = computed(() => data.modules.reduce((sum, row) => sum + Number(row.projectWeight || 0), 0).toFixed(2)) |
| | | const reportShareTotal = computed(() => reportForm.reporterShares.reduce((sum, row) => sum + Number(row.moduleShare || 0), 0).toFixed(2)) |
| | | const reportShareRemaining = computed(() => Math.max(0, 100 - Number(reportShareTotal.value)).toFixed(2)) |
| | | const followCycleLabel = computed(() => ({ MONTH: 'æ¯æ', QUARTER: 'æ¯å£åº¦', YEAR: 'æ¯å¹´' }[props.projectInfo?.followCycle] || props.projectInfo?.followCycle || 'æ¯æ')) |
| | | const projectExecutorOptions = computed(() => { |
| | | const ids = new Set((props.projectInfo?.projectExecutorIds || []).map(String)) |
| | | return userOptions.value.filter(user => ids.has(String(user.value))) |
| | | }) |
| | | const eligibleReportModules = computed(() => data.modules.filter(module => |
| | | (module.members || []).some(member => String(member.userId) === String(userStore.id)) |
| | | )) |
| | | const selectedReportModule = computed(() => data.modules.find(module => String(module.id) === String(reportForm.moduleId))) |
| | | const moduleReporterOptions = computed(() => { |
| | | const members = selectedReportModule.value?.members || [] |
| | | return members.map(member => ({ |
| | | value: member.userId, |
| | | label: member.userName || userOptions.value.find(user => String(user.value) === String(member.userId))?.label || String(member.userId) |
| | | })) |
| | | }) |
| | | |
| | | const moduleVisible = ref(false) |
| | | const moduleFormRef = ref() |
| | | const moduleForm = reactive({ id: null, moduleName: '', plannedStartDate: '', summary: '', projectWeight: 0, members: [], locked: false }) |
| | | const otherModuleWeight = computed(() => Number(data.modules.reduce((sum, row) => |
| | | String(row.id) === String(moduleForm.id) ? sum : sum + Number(row.projectWeight || 0), 0).toFixed(2))) |
| | | const moduleWeightMax = computed(() => Math.max(0, Number((100 - otherModuleWeight.value).toFixed(2)))) |
| | | const moduleWeightAfterChange = computed(() => Number((otherModuleWeight.value + Number(moduleForm.projectWeight || 0)).toFixed(2))) |
| | | const moduleRules = { moduleName: [{ required: true, message: '请è¾å
¥æ¨¡ååç§°', trigger: 'blur' }], plannedStartDate:[{required:true,message:'è¯·éæ©é¢è®¡å¼å§æ¶é´',trigger:'change'}],summary:[{required:true,message:'请è¾å
¥æ¨¡åæ¦è¦',trigger:'blur'}], projectWeight: [{ required: true, message: '请è¾å
¥å·¥ä½éå æ¯', trigger: 'change' }] } |
| | | const followVisible = ref(false) |
| | | const followFormRef = ref() |
| | | const followForm = reactive({ followDate: today(), detail: '' }) |
| | | const followFiles = ref([]) |
| | | const followRules = { followDate: [{ required: true, message: 'è¯·éæ©è·è¿æ¥æ', trigger: 'change' }], detail: [{ required: true, message: '请è¾å
¥è·è¿è¯¦æ
', trigger: 'blur' }] } |
| | | const reportVisible = ref(false) |
| | | const reportFormRef = ref() |
| | | const reportForm = reactive({ moduleId: null, reporterShares: [], progress: 0, reportDate: today(), remark: '' }) |
| | | const reportFiles = ref([]) |
| | | const reportRules = { moduleId: [{ required: true, message: 'è¯·éæ©é¡¹ç®æ¨¡å', trigger: 'change' }], reportDate: [{ required: true, message: 'è¯·éæ©æ±æ¥æ¶é´', trigger: 'change' }] } |
| | | const materialVisible = ref(false) |
| | | const materialFormRef = ref() |
| | | const materialForm = reactive({ materialName: '', materialType: '', remark: '' }) |
| | | const materialFiles = ref([]) |
| | | const materialRules = { materialName: [{ required: true, message: '请è¾å
¥èµæåç§°', trigger: 'blur' }] } |
| | | async function loadUsers() { |
| | | const res = await userListAll() |
| | | const list = res?.data || res?.rows || res || [] |
| | | userOptions.value = (Array.isArray(list) ? list : []).map(user => ({ label: user.nickName || user.userName || user.name, value: user.userId || user.id })) |
| | | } |
| | | |
| | | async function loadData() { |
| | | if (!props.projectId) return |
| | | loading.value = true |
| | | try { |
| | | const res = await getProjectWorkflow(props.projectId) |
| | | Object.assign(data, res?.data || {}) |
| | | await nextTick() |
| | | if (activeTab.value === 'reports') renderChart() |
| | | } finally { loading.value = false } |
| | | } |
| | | |
| | | function openModule(row = null) { |
| | | Object.assign(moduleForm, { |
| | | id: row?.id || null, moduleName: row?.moduleName || '', plannedStartDate: row?.plannedStartDate || '', |
| | | summary: row?.summary || '', projectWeight: Number(row?.projectWeight || 0), |
| | | members: (row?.members || []).map(member => ({ userId: member.userId, userName: member.userName })), |
| | | locked: Number(row?.reportCount || 0) > 0 |
| | | }) |
| | | if (!moduleForm.members.length) moduleForm.members.push({ userId: null, userName: '' }) |
| | | moduleVisible.value = true |
| | | } |
| | | |
| | | function syncMemberName(member, id) { member.userName = userOptions.value.find(user => String(user.value) === String(id))?.label || '' } |
| | | |
| | | async function submitModule() { |
| | | await moduleFormRef.value?.validate() |
| | | if (!moduleForm.members.length || moduleForm.members.some(member => !member.userId)) return ElMessage.warning('请è³å°éæ©ä¸åæ¨¡åæ§è¡äºº') |
| | | if (new Set(moduleForm.members.map(member => String(member.userId))).size !== moduleForm.members.length) return ElMessage.warning('æ¨¡åæ§è¡äººä¸è½éå¤') |
| | | saving.value = true |
| | | try { |
| | | await saveProjectModule({ ...moduleForm, projectId: props.projectId }) |
| | | ElMessage.success('模åä¿åæå'); moduleVisible.value = false; await loadData() |
| | | } finally { saving.value = false } |
| | | } |
| | | |
| | | async function removeModule(row) { |
| | | await ElMessageBox.confirm(`确认å 餿¨¡åâ${row.moduleName}âåï¼`, 'æç¤º', { type: 'warning' }) |
| | | await deleteProjectModule(row.id); ElMessage.success('å 餿å'); await loadData() |
| | | } |
| | | |
| | | function openFollow() { Object.assign(followForm, { followDate: data.followWindow?.dueDate || today(), detail: '' }); followFiles.value = []; followVisible.value = true } |
| | | function normalizeFiles(list) { return list.map(file => ({ id: file.id ?? file.fileId ?? file.rawData?.id, name: file.name ?? file.fileName ?? file.rawData?.originalFilename, url: file.url ?? file.fileUrl ?? file.rawData?.url })).filter(file => file.id || file.url) } |
| | | |
| | | async function submitFollow() { |
| | | await followFormRef.value?.validate(); saving.value = true |
| | | try { await saveProjectFollowUp({ ...followForm, projectId: props.projectId, attachments: normalizeFiles(followFiles.value) }); ElMessage.success('è·è¿å·²ä¿å'); followVisible.value = false; await loadData() } |
| | | finally { saving.value = false } |
| | | } |
| | | |
| | | function openReport() { |
| | | if (!eligibleReportModules.value.length) return ElMessage.warning('åªææ¨¡åæ§è¡äººæè½å¡«å该模åçè¿åº¦æ±æ¥') |
| | | const userId = userStore.id |
| | | const userName = userOptions.value.find(user => String(user.value) === String(userId))?.label || userStore.name || '' |
| | | const module = eligibleReportModules.value[0] |
| | | Object.assign(reportForm, { moduleId: module.id, reporterShares: userId ? [{ userId, userName, moduleShare: 100 }] : [], progress: Number(module.currentProgress || 0), reportDate: today(), remark: '' }) |
| | | reportFiles.value = []; reportVisible.value = true |
| | | } |
| | | |
| | | function changeReportModule(moduleId) { |
| | | const module = data.modules.find(item => String(item.id) === String(moduleId)) |
| | | const userId = userStore.id |
| | | const userName = module?.members?.find(member => String(member.userId) === String(userId))?.userName || userStore.name || '' |
| | | reportForm.progress = Number(module?.currentProgress || 0) |
| | | reportForm.reporterShares = userId ? [{ userId, userName, moduleShare: 100 }] : [] |
| | | } |
| | | |
| | | function reporterShareLimit(index) { |
| | | const usedByOthers = reportForm.reporterShares.reduce((sum, member, memberIndex) => |
| | | memberIndex === index ? sum : sum + Number(member.moduleShare || 0), 0) |
| | | return Math.max(0, Number((100 - usedByOthers).toFixed(2))) |
| | | } |
| | | |
| | | async function submitReport() { |
| | | await reportFormRef.value?.validate() |
| | | if (!reportForm.reporterShares.length || reportForm.reporterShares.some(member => !member.userId)) return ElMessage.warning('请è³å°éæ©ä¸åæ±æ¥äººå') |
| | | if (new Set(reportForm.reporterShares.map(member => String(member.userId))).size !== reportForm.reporterShares.length) return ElMessage.warning('æ±æ¥äººåä¸è½éå¤') |
| | | if (Number(reportShareTotal.value) !== 100) return ElMessage.warning('æ¬æ¬¡æ±æ¥äººåæ¯éå计å¿
须为100%') |
| | | saving.value = true |
| | | try { await saveProjectProgressReport({ ...reportForm, projectId: props.projectId, attachments: normalizeFiles(reportFiles.value) }); ElMessage.success('æ±æ¥å·²æäº¤'); reportVisible.value = false; await loadData() } |
| | | finally { saving.value = false } |
| | | } |
| | | |
| | | function openMaterial() { Object.assign(materialForm, { materialName: '', materialType: '', remark: '' }); materialFiles.value = []; materialVisible.value = true } |
| | | async function submitMaterial() { |
| | | await materialFormRef.value?.validate() |
| | | if (!materialFiles.value.length) return ElMessage.warning('请ä¸ä¼ è³å°ä¸ä¸ªæä»¶') |
| | | saving.value = true |
| | | try { await saveProjectMaterial({ ...materialForm, projectId: props.projectId, attachments: normalizeFiles(materialFiles.value) }); ElMessage.success('èµæå·²ä¿å'); materialVisible.value = false; await loadData() } |
| | | finally { saving.value = false } |
| | | } |
| | | |
| | | async function removeRecord(type, row) { |
| | | await ElMessageBox.confirm('确认å é¤è¿æ¡è®°å½åï¼', 'æç¤º', { type: 'warning' }) |
| | | const api = { follow: deleteProjectFollowUp, report: deleteProjectProgressReport, material: deleteProjectMaterial }[type] |
| | | await api(row.id); ElMessage.success('å 餿å'); await loadData() |
| | | } |
| | | |
| | | function download(file) { if (!file?.url) return ElMessage.warning('éä»¶ææ ä¸è½½å°å'); proxy.$download.byUrl(file.url, file.name || file.fileName) } |
| | | |
| | | function renderChart() { |
| | | if (!chartRef.value) return |
| | | chart ||= echarts.init(chartRef.value) |
| | | const source = data.memberStatistics || [] |
| | | chart.setOption({ |
| | | title: { text: 'æå已宿工ä½éæ¯é', left: 'center', textStyle: { fontSize: 15 } }, |
| | | tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, |
| | | legend: { bottom: 0 }, |
| | | series: [{ type: 'pie', radius: ['38%', '68%'], center: ['50%', '48%'], data: source.map(item => ({ name: item.userName || 'æªå½å', value: Number(item.value || 0) })), label: { formatter: '{b}\n{c}' } }] |
| | | }) |
| | | } |
| | | |
| | | function handleTabChange(name) { if (name === 'reports') nextTick(renderChart) } |
| | | function resizeChart() { chart?.resize() } |
| | | onMounted(async () => { await Promise.all([loadUsers(), loadData()]); window.addEventListener('resize', resizeChart) }) |
| | | onBeforeUnmount(() => { window.removeEventListener('resize', resizeChart); chart?.dispose() }) |
| | | defineExpose({ refresh: loadData }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .execution-panel { min-height: 260px; } |
| | | .tab-actions { display: flex; align-items: center; justify-content: space-between; margin: 4px 0 14px; } |
| | | .hint { color: #606266; font-size: 13px; } |
| | | .member-tag { margin: 2px 6px 2px 0; } |
| | | .report-layout { display: grid; grid-template-columns: minmax(300px, 34%) 1fr; gap: 14px; } |
| | | .chart { height: 390px; border: 1px solid #ebeef5; border-radius: 6px; } |
| | | .report-table { min-width: 0; } |
| | | .member-editor { width: 100%; } |
| | | .member-row { display: grid; grid-template-columns: 1fr 52px; align-items: center; gap: 10px; margin-bottom: 10px; } |
| | | .reporter-row { display: grid; grid-template-columns: 1fr 150px 24px 52px; align-items: center; gap: 10px; margin-bottom: 10px; } |
| | | .share-total { margin-top: 8px; color: #606266; } |
| | | .percent-hint { width: 100%; margin-top: 5px; color: #909399; font-size: 12px; line-height: 1.5; } |
| | | .dialog-tip { margin-bottom: 16px; } |
| | | .unit { margin-left: 8px; color: #606266; } |
| | | @media (max-width: 1200px) { .report-layout { grid-template-columns: 1fr; } } |
| | | </style> |
| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | width="95%" |
| | | top="5vh" |
| | | destroy-on-close |
| | | @close="closeDialog" |
| | | > |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-position="top" |
| | | label-width="120px" |
| | | :disabled="isView" |
| | | > |
| | | <div class="section"> |
| | | <div class="section-header" @click="toggleSection('base')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>åºç¡èµæ</span> |
| | | </div> |
| | | <el-icon class="toggle-icon"> |
| | | <ArrowDown v-if="sectionCollapsed.base" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.base" class="section-body"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="åæ®ç¼å·" prop="billNo"> |
| | | <el-input v-model="form.billNo" placeholder="ç³»ç»çæ" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="项ç®åç§°" prop="projectName"> |
| | | <el-input v-model="form.projectName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="客æ·åç§°" prop="customerName"> |
| | | <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-dialog v-model="visible" :title="dialogTitle" :width="startProject ? '1100px' : '900px'" destroy-on-close> |
| | | <el-alert v-if="isBusinessProject" title="该项ç®ç±ä¸å¡å®¡æ¹çæï¼ä¿ååå¯å¨é¡¹ç®è¯¦æ
æ¥çæ¥æºä¸å¡åå
¶éä»¶ã" type="info" :closable="false" class="tip" /> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> |
| | | <el-row v-if="!personnelOnly" :gutter="20"> |
| | | <el-col :span="12"><el-form-item label="项ç®åç§°" prop="title"><el-input v-model="form.title" maxlength="200" /></el-form-item></el-col> |
| | | <el-col :span="12"><el-form-item label="客æ·åç§°" prop="clientName"><el-select v-model="form.clientName" filterable clearable style="width:100%" @change="syncCustomer"><el-option v-for="item in customers" :key="item.id" :label="item.customerName" :value="item.customerName" /></el-select></el-form-item></el-col> |
| | | </el-row> |
| | | <el-row v-if="!personnelOnly" :gutter="20"> |
| | | <el-col :span="12"><el-form-item label="项ç®åç±»" prop="projectClassification"><el-tree-select v-model="form.projectClassification" :data="categoryTree" :props="{ label:'categoryName',value:'categoryName',children:'children' }" check-strictly clearable :filterable="false" placeholder="è¯·éæ©é¡¹ç®åç±»" style="width:100%" /></el-form-item></el-col> |
| | | <el-col :span="12"><el-form-item label="é¡¹ç®æ¥æº"><el-input v-model="form.source" disabled /></el-form-item></el-col> |
| | | </el-row> |
| | | <el-row v-if="!personnelOnly" :gutter="20"> |
| | | <el-col :span="12"><el-form-item label="é¢è®¡å¼å§æ¶é´" prop="planStartTime"><el-date-picker v-model="form.planStartTime" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col> |
| | | <el-col :span="12"><el-form-item label="é¢è®¡ç»ææ¶é´" prop="planEndTime"><el-date-picker v-model="form.planEndTime" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col> |
| | | </el-row> |
| | | <el-row v-if="!personnelOnly" :gutter="20"> |
| | | <el-col :span="12"><el-form-item label="项ç®è´è´£äºº" prop="projectLeaderId"><el-select v-model="form.projectLeaderId" filterable style="width:100%" @change="syncLeader"><el-option v-for="u in users" :key="u.value" :label="u.label" :value="u.value" /></el-select></el-form-item></el-col> |
| | | <el-col :span="12"><el-form-item label="项ç®ç»ç" prop="projectManagerIds"><el-select v-model="form.projectManagerIds" multiple filterable style="width:100%"><el-option v-for="u in users" :key="u.value" :label="u.label" :value="u.value" /></el-select></el-form-item></el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="24"><el-form-item label="é¡¹ç®æ§è¡äºº" prop="projectExecutorIds"><el-select v-model="form.projectExecutorIds" multiple filterable style="width:100%"><el-option v-for="u in users" :key="u.value" :label="u.label" :value="u.value" /></el-select><div class="field-hint">项ç®å¼å§åä»å¯æ°å¢ï¼å·²æé¡¹ç®æ±æ¥å·¥ä½å æ¯çæ§è¡äººä¸è½å é¤ã</div></el-form-item></el-col> |
| | | </el-row> |
| | | <el-row v-if="!personnelOnly" :gutter="20"> |
| | | <el-col :span="8"><el-form-item label="è·è¿å¨æ" prop="followCycle"><el-select v-model="form.followCycle" style="width:100%"><el-option label="æ¯æ" value="MONTH" /><el-option label="æ¯å£åº¦" value="QUARTER" /><el-option label="æ¯å¹´" value="YEAR" /></el-select></el-form-item></el-col> |
| | | <el-col :span="16"><el-form-item label="è·è¿è¦æ±" prop="followDetail"><el-input v-model="form.followDetail" maxlength="1000" /></el-form-item></el-col> |
| | | </el-row> |
| | | <el-form-item v-if="!personnelOnly" label="项ç®å¤æ³¨"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="2000" show-word-limit /></el-form-item> |
| | | <el-form-item v-if="!personnelOnly" label="项ç®èµæ"><AttachmentUploadFile v-model:file-list="files" :limit="20" /></el-form-item> |
| | | <template v-if="startProject && !personnelOnly"> |
| | | <el-divider content-position="left">é¡¹ç®æ¨¡åç»æï¼å¼å§é¡¹ç®åå¿
å¡«ï¼</el-divider> |
| | | <el-alert title="模åå·¥ä½éå æ¯å计å¿
须为100%ï¼è¿éåªéæ©æ§è¡äººï¼äººåæ¯é卿¯æ¬¡è¿åº¦æ±æ¥æ¶å¡«åã" type="info" :closable="false" class="tip" /> |
| | | <div v-for="(module,index) in startModules" :key="module.localKey" class="module-card"> |
| | | <div class="module-card__title"><span>模å {{ index + 1 }}</span><el-button link type="danger" @click="removeStartModule(index)">å é¤</el-button></div> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="8"><el-form-item label="模ååç§°" required><el-input v-model="module.moduleName" maxlength="200" /></el-form-item></el-col> |
| | | <el-col :span="8"><el-form-item label="é¢è®¡å¼å§" required><el-date-picker v-model="module.plannedStartDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col> |
| | | <el-col :span="8"><el-form-item label="项ç®å·¥ä½é" required><el-input-number v-model="module.projectWeight" :min="0" :max="moduleWeightLimit(index)" :precision="2" /><span class="unit">%</span><div class="field-hint">å
¶ä»æ¨¡å {{ otherStartModuleWeight(index) }}%ï¼å½åæå¤ {{ moduleWeightLimit(index) }}%</div></el-form-item></el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="项ç®åç±»" prop="projectClassification"> |
| | | <el-select |
| | | v-model="form.projectClassification" |
| | | placeholder="è¯·éæ©" |
| | | 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-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 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="8"> |
| | | <el-form-item label="å忥æ"> |
| | | <el-input :model-value="form.orderDate" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="10" > |
| | | <el-col :span="24"> |
| | | <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 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 /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"><el-form-item label="æ¨¡åæ§è¡äºº" required><el-select v-model="module.memberIds" multiple filterable style="width:100%"><el-option v-for="u in projectExecutorOptions" :key="u.value" :label="u.label" :value="u.value" /></el-select></el-form-item></el-col> |
| | | <el-col :span="12"><el-form-item label="æ¨¡åæ¦è¦" required><el-input v-model="module.summary" type="textarea" :rows="2" maxlength="1000" /></el-form-item></el-col> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="false" class="section"> |
| | | <div class="section-header" @click="toggleSection('team')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>项ç®å¢é</span> |
| | | </div> |
| | | <div class="section-actions" @click.stop> |
| | | <el-button v-if="!isView" type="primary" :icon="Plus" @click="addTeamRow">æ°å¢è¡</el-button> |
| | | <el-icon class="toggle-icon" @click="toggleSection('team')"> |
| | | <ArrowDown v-if="sectionCollapsed.team" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.team" class="section-body"> |
| | | <PIMTable |
| | | :column="teamColumns" |
| | | :tableData="form.teamList" |
| | | :tableLoading="false" |
| | | :isSelection="false" |
| | | :isShowPagination="false" |
| | | height="220" |
| | | > |
| | | <template #memberId="{ row }"> |
| | | <el-select v-model="row.memberId" placeholder="è¯·éæ©" filterable clearable style="width: 100%" :disabled="isView"> |
| | | <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #roleId="{ row }"> |
| | | <el-select v-model="row.roleId" placeholder="è¯·éæ©" clearable style="width: 100%" :disabled="isView"> |
| | | <el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #enterDate="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.enterDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #leaveDate="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.leaveDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #phone="{ row }"> |
| | | <el-input v-model="row.phone" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #teamRemark="{ row }"> |
| | | <el-input v-model="row.remark" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #teamAction="{ row, index }"> |
| | | <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeTeamRow(index)">å é¤</el-button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- <div class="section"> |
| | | <div class="section-header" @click="toggleSection('phase')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>项ç®é¶æ®µ</span> |
| | | </div> |
| | | <div class="section-actions" @click.stop> |
| | | <el-button v-if="!isView" type="primary" :icon="Plus" @click="addPhaseRow">æ°å¢è¡</el-button> |
| | | <el-icon class="toggle-icon" @click="toggleSection('phase')"> |
| | | <ArrowDown v-if="sectionCollapsed.phase" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.phase" class="section-body"> |
| | | <PIMTable |
| | | :column="phaseColumns" |
| | | :tableData="form.phaseList" |
| | | :tableLoading="false" |
| | | :isSelection="false" |
| | | :isShowPagination="false" |
| | | height="240" |
| | | > |
| | | <template #phaseName="{ row }"> |
| | | <el-input v-model="row.phaseName" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #phaseDesc="{ row }"> |
| | | <el-input v-model="row.description" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #ownerId="{ row }"> |
| | | <el-select v-model="row.ownerId" placeholder="è¯·éæ©" filterable clearable style="width: 100%" :disabled="isView"> |
| | | <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #planDays="{ row }"> |
| | | <el-input-number v-model="row.planDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" /> |
| | | </template> |
| | | <template #planStart="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.planStartDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #planEnd="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.planEndDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #progress="{ row }"> |
| | | <el-input-number v-model="row.progress" :min="0" :max="100" controls-position="right" style="width: 100%" :disabled="isView" /> |
| | | </template> |
| | | <template #actualStart="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.actualStartDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #actualEnd="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.actualEndDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #overdueDays="{ row }"> |
| | | <el-input-number v-model="row.overdueDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" /> |
| | | </template> |
| | | <template #completion="{ row }"> |
| | | <el-input v-model="row.completionRemark" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #phaseAction="{ row, index }"> |
| | | <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removePhaseRow(index)">å é¤</el-button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> --> |
| | | |
| | | <div v-if="false" class="section"> |
| | | <div class="section-header" @click="toggleSection('address')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>æ¶è´§å°å</span> |
| | | </div> |
| | | <div class="section-actions" @click.stop> |
| | | <el-button v-if="!isView" type="primary" :icon="Plus" @click="addAddressRow">æ°å¢è¡</el-button> |
| | | <el-icon class="toggle-icon" @click="toggleSection('address')"> |
| | | <ArrowDown v-if="sectionCollapsed.address" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.address" class="section-body"> |
| | | <PIMTable |
| | | :column="addressColumns" |
| | | :tableData="form.addressList" |
| | | :tableLoading="false" |
| | | :isSelection="false" |
| | | :isShowPagination="false" |
| | | height="200" |
| | | > |
| | | <template #receiver="{ row }"> |
| | | <el-input v-model="row.receiver" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #receiverPhone="{ row }"> |
| | | <el-input v-model="row.phone" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #receiverAddress="{ row }"> |
| | | <el-input v-model="row.address" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #addressAction="{ row, index }"> |
| | | <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeAddressRow(index)">å é¤</el-button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="false" class="section"> |
| | | <div class="section-header" @click="toggleSection('contact')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>è系信æ¯</span> |
| | | </div> |
| | | <el-icon class="toggle-icon"> |
| | | <ArrowDown v-if="sectionCollapsed.contact" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.contact" class="section-body"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="è系人å§å" prop="contactName"> |
| | | <el-input v-model="form.contactName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="æ§å«" prop="contactGender"> |
| | | <el-select v-model="form.contactGender" placeholder="è¯·éæ©" clearable style="width: 100%"> |
| | | <el-option label="ç·" value="1" /> |
| | | <el-option label="女" value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="çæ¥" prop="contactBirthday"> |
| | | <el-date-picker |
| | | v-model="form.contactBirthday" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="é®ç®±" prop="contactEmail"> |
| | | <el-input v-model="form.contactEmail" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="é¨é¨" prop="contactDept"> |
| | | <el-input v-model="form.contactDept" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="èå¡" prop="contactJob"> |
| | | <el-input v-model="form.contactJob" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="ææºå·ç " prop="contactMobile"> |
| | | <el-input v-model="form.contactMobile" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="微信å·ç " prop="contactWechat"> |
| | | <el-input v-model="form.contactWechat" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="QQ" prop="contactQq"> |
| | | <el-input v-model="form.contactQq" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="ä¼ä¸å¾®ä¿¡" prop="contactWorkWechat"> |
| | | <el-input v-model="form.contactWorkWechat" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å°å" prop="contactAddress"> |
| | | <el-input v-model="form.contactAddress" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="夿³¨" prop="contactRemark"> |
| | | <el-input v-model="form.contactRemark" type="textarea" :rows="2" placeholder="请è¾å
¥" maxlength="200" show-word-limit /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | <div class="module-actions"><span>å½å模åå·¥ä½éå计ï¼{{ startModuleWeight }}%</span><el-button type="primary" plain @click="addStartModule">+ æ·»å æ¨¡å</el-button></div> |
| | | </template> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button v-if="!isView" type="primary" @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDialog">{{ isView ? 'å
³é' : 'åæ¶' }}</el-button> |
| | | </div> |
| | | </template> |
| | | <template #footer><el-button @click="visible=false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submit">{{ personnelOnly ? 'ä¿åæ§è¡äºº' : startProject ? 'ä¿åå¹¶å¼å§é¡¹ç®' : 'ä¿å' }}</el-button></template> |
| | | </el-dialog> |
| | | |
| | | <FormDialog |
| | | v-model="productFormVisible" |
| | | :title="productOperationType === 'add' ? 'æ°å¢äº§å' : 'ç¼è¾äº§å'" |
| | | :width="'40%'" |
| | | :operation-type="productOperationType" |
| | | @close="closeProductDia" |
| | | @confirm="submitProduct" |
| | | @cancel="closeProductDia" |
| | | > |
| | | <el-form ref="productFormRef" :model="productForm" label-width="140px" label-position="top" :rules="productRules"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="产å大类ï¼" prop="productCategoryId"> |
| | | <el-tree-select |
| | | v-model="productForm.productCategoryId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | check-strictly |
| | | :data="productCategoryOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | @change="getModels" |
| | | /> |
| | | </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="è¯·éæ©" clearable filterable @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="calculateFromTaxRate"> |
| | | <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" |
| | | :step="0.01" |
| | | :min="0" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | @change="calculateFromUnitPrice" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ°éï¼" prop="quantity"> |
| | | <el-input-number |
| | | v-model="productForm.quantity" |
| | | :step="0.1" |
| | | :min="0" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | @change="calculateFromQuantity" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å«ç¨æ»ä»·(å
)ï¼" prop="taxInclusiveTotalPrice"> |
| | | <el-input v-model="productForm.taxInclusiveTotalPrice" placeholder="请è¾å
¥" clearable @change="calculateFromTotalPrice" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¸å«ç¨æ»ä»·(å
)ï¼" prop="taxExclusiveTotalPrice"> |
| | | <el-input v-model="productForm.taxExclusiveTotalPrice" placeholder="请è¾å
¥" clearable @change="calculateFromExclusiveTotalPrice" /> |
| | | </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-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagementFormDia"> |
| | | import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue' |
| | | import { ArrowDown, ArrowUp, Delete, Plus } from '@element-plus/icons-vue' |
| | | <script setup> |
| | | import { computed, reactive, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | 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' |
| | | import { findRoleListPage } from '@/api/projectManagement/role' |
| | | import { userListAll } from '@/api/publicApi' |
| | | import { addProject, getProject, updateProject } from '@/api/projectManagement/project' |
| | | import { modelList, productTreeList } from '@/api/basicData/product' |
| | | import { delProduct as delSalesProduct } from '@/api/salesManagement/salesLedger' |
| | | import { listCustomer } from '@/api/basicData/customer' |
| | | import { deleteProjectModule, getProjectWorkflow, listProjectCategories, saveProjectModule, startProjectWorkflow, updateProjectExecutors } from '@/api/projectManagement/workflow' |
| | | import { userListAll } from '@/api/publicApi' |
| | | |
| | | const emit = defineEmits(['completed']) |
| | | const { proxy } = getCurrentInstance() |
| | | const { bill_status, project_management, plan_status, tax_rate } = proxy.useDict('bill_status', 'project_management', 'plan_status', 'tax_rate') |
| | | |
| | | const dialogVisible = ref(false) |
| | | const visible = ref(false) |
| | | const saving = ref(false) |
| | | const startProject = ref(false) |
| | | const personnelOnly = ref(false) |
| | | const operationType = ref('add') |
| | | const formRef = ref() |
| | | const fileList = ref([]) |
| | | const customerOptions = ref([]) |
| | | const customers = ref([]) |
| | | const users = ref([]) |
| | | const categoryTree = ref([]) |
| | | const files = ref([]) |
| | | const startModules = ref([]) |
| | | const originalModuleIds = ref([]) |
| | | const form = reactive({ id:null,no:'',title:'',clientId:null,clientName:'',source:'æå¨å建',sourceBusinessId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],projectExecutorIds:[],projectExecutorNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'æªå¼å§',remark:'' }) |
| | | const rules = { |
| | | title:[{ required:true,message:'请è¾å
¥é¡¹ç®åç§°',trigger:'blur' }], clientName:[{ required:true,message:'è¯·éæ©å®¢æ·',trigger:'change' }], |
| | | projectClassification:[{ required:true,message:'è¯·éæ©é¡¹ç®åç±»',trigger:'change' }], planStartTime:[{ required:true,message:'è¯·éæ©é¢è®¡å¼å§æ¶é´',trigger:'change' }], |
| | | planEndTime:[{ required:true,message:'è¯·éæ©é¢è®¡ç»ææ¶é´',trigger:'change' }], projectLeaderId:[{ required:true,message:'è¯·éæ©é¡¹ç®è´è´£äºº',trigger:'change' }], |
| | | projectManagerIds:[{ required:true,type:'array',min:1,message:'è¯·éæ©è³å°ä¸å项ç®ç»ç',trigger:'change' }], followCycle:[{ required:true,message:'è¯·éæ©è·è¿å¨æ',trigger:'change' }], |
| | | projectExecutorIds:[{ required:true,type:'array',min:1,message:'è¯·éæ©è³å°ä¸åé¡¹ç®æ§è¡äºº',trigger:'change' }], |
| | | followDetail:[{ required:true,message:'请è¾å
¥è·è¿è¦æ±',trigger:'blur' }] |
| | | } |
| | | const isBusinessProject = computed(() => !!form.sourceBusinessId || form.source === 'ä¸å¡çæ' || form.source === 'ä¸å¡å®¡æ¹') |
| | | const startModuleWeight = computed(() => startModules.value.reduce((sum,module)=>sum+Number(module.projectWeight||0),0).toFixed(2)) |
| | | const projectExecutorOptions = computed(() => users.value.filter(user=>form.projectExecutorIds.some(id=>String(id)===String(user.value)))) |
| | | const dialogTitle = computed(() => personnelOnly.value ? 'è°æ´é¡¹ç®æ§è¡äºº' : startProject.value ? 'å¡«åç«é¡¹ä¿¡æ¯å¹¶å¼å§é¡¹ç®' : operationType.value === 'add' ? 'æå¨å建项ç®' : 'ç¼è¾é¡¹ç®åºæ¬æ
åµ') |
| | | |
| | | const projectTypeOptions = ref([]) |
| | | // 项ç®åç±»ï¼å
åæ»å 个ï¼åç»å¯åæ¢ä¸ºåå
¸ |
| | | const projectClassificationOptions = ['æ°åå转å', 'æºè½å¶é ', 'ç³»ç»éæ', 'å
¶ä»'] |
| | | const roleOptions = ref([]) |
| | | const userOptions = ref([]) |
| | | const productData = ref([]) |
| | | const productSelectedRows = ref([]) |
| | | const productCategoryOptions = ref([]) |
| | | const modelOptions = ref([]) |
| | | const productFormVisible = ref(false) |
| | | const productOperationType = ref('add') |
| | | const productFormRef = ref() |
| | | const productIndex = ref(0) |
| | | const isCalculating = ref(false) |
| | | |
| | | const productFormData = reactive({ |
| | | productForm: { |
| | | productCategoryId: undefined, |
| | | productCategory: '', |
| | | productModelId: undefined, |
| | | specificationModel: '', |
| | | unit: '', |
| | | quantity: '', |
| | | taxInclusiveUnitPrice: '', |
| | | taxRate: '', |
| | | taxInclusiveTotalPrice: '', |
| | | taxExclusiveTotalPrice: '', |
| | | invoiceType: '' |
| | | }, |
| | | productRules: { |
| | | productCategoryId: [{ 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' }], |
| | | taxInclusiveTotalPrice: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | taxExclusiveTotalPrice: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | invoiceType: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }] |
| | | } |
| | | }) |
| | | |
| | | const { productForm, productRules } = toRefs(productFormData) |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | clientId: undefined, |
| | | parentProjectId: undefined, |
| | | projectManagementPlanId: undefined, |
| | | managerId: undefined, |
| | | salesmanId: undefined, |
| | | salesmanName: '', |
| | | actualStartDate: '', |
| | | actualEndDate: '', |
| | | departmentId: undefined, |
| | | departmentName: '', |
| | | orderDate: '', |
| | | billNo: '', |
| | | projectName: '', |
| | | customerName: '', |
| | | parentProjectName: '', |
| | | setupDate: todayStr(), |
| | | projectSource: 'æå¨æ·»å ', |
| | | projectClassification: '', |
| | | creatorName: '', |
| | | billStatus: '', |
| | | projectStage: '', |
| | | estimatedDays: 0, |
| | | planStartDate: '', |
| | | planEndDate: '', |
| | | projectManagementPlanId: undefined, |
| | | projectAmount: 0, |
| | | auditStatus: '', |
| | | remark: '', |
| | | attachmentIds: [], |
| | | teamList: [], |
| | | phaseList: [], |
| | | addressList: [], |
| | | contactName: '', |
| | | contactGender: '', |
| | | contactBirthday: '', |
| | | contactEmail: '', |
| | | contactDept: '', |
| | | contactJob: '', |
| | | contactMobile: '', |
| | | contactWechat: '', |
| | | contactQq: '', |
| | | contactWorkWechat: '', |
| | | contactAddress: '', |
| | | contactRemark: '' |
| | | }, |
| | | rules: { |
| | | projectName: [{ required: true, message: '请è¾å
¥é¡¹ç®åç§°', trigger: 'blur' }] |
| | | } |
| | | }) |
| | | |
| | | const { form, rules } = toRefs(data) |
| | | |
| | | const sectionCollapsed = reactive({ |
| | | base: false, |
| | | product: false, |
| | | team: false, |
| | | phase: false, |
| | | address: false, |
| | | contact: false |
| | | }) |
| | | |
| | | 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 'ç¼è¾é¡¹ç®' |
| | | return '项ç®è¯¦æ
' |
| | | }) |
| | | |
| | | const teamColumns = [ |
| | | { label: 'å§å', prop: 'memberId', align: 'center', width: 180, dataType: 'slot', slot: 'memberId' }, |
| | | { label: '项ç®ç»è§è²', prop: 'roleId', align: 'center', width: 160, dataType: 'slot', slot: 'roleId' }, |
| | | { label: 'è¿å
¥æ¥æ', prop: 'enterDate', align: 'center', width: 160, dataType: 'slot', slot: 'enterDate' }, |
| | | { label: 'ç¦»å¼æ¥æ', prop: 'leaveDate', align: 'center', width: 160, dataType: 'slot', slot: 'leaveDate' }, |
| | | { label: 'èç³»æ¹å¼', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'phone' }, |
| | | { label: '夿³¨', prop: 'remark', align: 'center', dataType: 'slot', slot: 'teamRemark' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'teamAction', fixed: 'right' } |
| | | ] |
| | | |
| | | const phaseColumns = [ |
| | | { label: 'é¶æ®µåç§°', prop: 'phaseName', align: 'center', width: 160, dataType: 'slot', slot: 'phaseName' }, |
| | | { label: 'æè¿°', prop: 'description', align: 'center', width: 200, dataType: 'slot', slot: 'phaseDesc' }, |
| | | { label: 'è´è´£äºº', prop: 'ownerId', align: 'center', width: 160, dataType: 'slot', slot: 'ownerId' }, |
| | | { label: 'é¢è®¡å·¥æ(天)', prop: 'planDays', align: 'center', width: 140, dataType: 'slot', slot: 'planDays' }, |
| | | { label: '计åå¼å§æ¥æ', prop: 'planStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'planStart' }, |
| | | { label: '计åç»ææ¥æ', prop: 'planEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'planEnd' }, |
| | | { label: 'è¿åº¦(%)', prop: 'progress', align: 'center', width: 120, dataType: 'slot', slot: 'progress' }, |
| | | { label: 'å®é
å¼å§æ¥æ', prop: 'actualStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualStart' }, |
| | | { label: 'å®é
ç»ææ¥æ', prop: 'actualEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualEnd' }, |
| | | { label: '龿天æ°', prop: 'overdueDays', align: 'center', width: 120, dataType: 'slot', slot: 'overdueDays' }, |
| | | { label: '宿æ
åµ', prop: 'completionRemark', align: 'center', width: 200, dataType: 'slot', slot: 'completion' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'phaseAction', fixed: 'right' } |
| | | ] |
| | | |
| | | const addressColumns = [ |
| | | { label: 'æ¶è´§äºº', prop: 'receiver', align: 'center', width: 180, dataType: 'slot', slot: 'receiver' }, |
| | | { label: 'èç³»æ¹å¼', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'receiverPhone' }, |
| | | { label: 'æ¶è´§å°å', prop: 'address', align: 'center', dataType: 'slot', slot: 'receiverAddress' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'addressAction', fixed: 'right' } |
| | | ] |
| | | |
| | | function toggleSection(key) { |
| | | sectionCollapsed[key] = !sectionCollapsed[key] |
| | | function buildTree(list) { const map=new Map((list||[]).map(x=>[String(x.id),{...x,children:[]}])) ; const roots=[]; map.forEach(node=>{ const parent=map.get(String(node.parentId)); if(parent&&node.parentId) parent.children.push(node); else roots.push(node) }); return roots } |
| | | async function loadOptions() { |
| | | const [customerRes,userRes,categoryRes] = await Promise.all([listCustomer({ current:-1,size:-1,type:0 }),userListAll(),listProjectCategories()]) |
| | | const customerData=customerRes?.rows||customerRes?.data?.records||customerRes?.data||[]; customers.value=Array.isArray(customerData)?customerData:[] |
| | | const userData=userRes?.data||userRes?.rows||userRes||[]; users.value=(Array.isArray(userData)?userData:[]).map(u=>({ label:u.nickName||u.userName||u.name,value:u.userId||u.id })) |
| | | categoryTree.value=buildTree(categoryRes?.data||[]) |
| | | } |
| | | function reset() { Object.assign(form,{ id:null,no:'',title:'',clientId:null,clientName:'',source:'æå¨å建',sourceBusinessId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],projectExecutorIds:[],projectExecutorNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'æªå¼å§',remark:'' }); files.value=[];startModules.value=[];originalModuleIds.value=[] } |
| | | function syncCustomer(name) { const item=customers.value.find(x=>x.customerName===name); form.clientId=item?.id||item?.customerId||null } |
| | | function syncLeader(id) { form.projectLeaderName=users.value.find(u=>String(u.value)===String(id))?.label||'' } |
| | | function normalizeFiles(list) { return (list||[]).map(f=>({ id:f.id??f.fileId??f.rawData?.id,name:f.name??f.fileName??f.rawData?.originalFilename,url:f.url??f.fileUrl??f.rawData?.url })).filter(f=>f.id||f.url) } |
| | | function newModule(module={}) { return { localKey:module.id||`${Date.now()}-${Math.random()}`,id:module.id||null,moduleName:module.moduleName||'',plannedStartDate:module.plannedStartDate||'',summary:module.summary||'',projectWeight:Number(module.projectWeight||0),memberIds:(module.members||[]).map(member=>member.userId) } } |
| | | function addStartModule(){startModules.value.push(newModule())} |
| | | function removeStartModule(index){startModules.value.splice(index,1)} |
| | | function otherStartModuleWeight(index){return startModules.value.reduce((sum,module,i)=>sum+(i===index?0:Number(module.projectWeight||0)),0).toFixed(2)} |
| | | function moduleWeightLimit(index){return Math.max(0,100-Number(otherStartModuleWeight(index)))} |
| | | function validateStartModules(){ |
| | | if(!startModules.value.length){ElMessage.warning('项ç®å¼å§å请è³å°æ·»å ä¸ä¸ªé¡¹ç®æ¨¡å');return false} |
| | | if(startModules.value.some(module=>!module.moduleName||!module.plannedStartDate||!module.summary||!module.memberIds.length)){ElMessage.warning('è¯·å®æ´å¡«åæ¯ä¸ªæ¨¡åçåç§°ãé¢è®¡å¼å§æ¶é´ãæ¨¡åæ¦è¦åæ§è¡äºº');return false} |
| | | if(Number(startModuleWeight.value)!==100){ElMessage.warning('æææ¨¡åç项ç®å·¥ä½éå æ¯å计å¿
须为100%');return false} |
| | | return true |
| | | } |
| | | |
| | | function todayStr() { |
| | | const d = new Date() |
| | | const pad = n => String(n).padStart(2, '0') |
| | | return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` |
| | | async function openDialog(options={}) { |
| | | reset(); operationType.value=options.operationType||'add'; startProject.value=!!options.startProject; personnelOnly.value=!!options.personnelOnly; await loadOptions() |
| | | const id=options.row?.id |
| | | if(id) { const res=await getProject(id); const detail=res?.data?.data??res?.data??res; const info=detail?.info||{}; Object.assign(form,info,{ projectManagerIds:info.projectManagerIds||[],projectManagerNames:info.projectManagerNames||[],projectExecutorIds:info.projectExecutorIds||[],projectExecutorNames:info.projectExecutorNames||[] }); files.value=(info.attachmentList||[]).map(f=>({...f})) } |
| | | if(startProject.value&&id){const workflow=await getProjectWorkflow(id);const modules=workflow?.data?.modules||[];startModules.value=modules.map(newModule);originalModuleIds.value=modules.map(module=>module.id);if(!startModules.value.length)addStartModule()} |
| | | visible.value=true |
| | | } |
| | | |
| | | function resetFormData() { |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | | clientId: undefined, |
| | | parentProjectId: undefined, |
| | | projectManagementPlanId: undefined, |
| | | managerId: undefined, |
| | | salesmanId: undefined, |
| | | salesmanName: '', |
| | | actualStartDate: '', |
| | | actualEndDate: '', |
| | | departmentId: undefined, |
| | | departmentName: '', |
| | | orderDate: '', |
| | | billNo: '', |
| | | projectName: '', |
| | | customerName: '', |
| | | parentProjectName: '', |
| | | setupDate: todayStr(), |
| | | projectSource: 'æå¨æ·»å ', |
| | | projectClassification: '', |
| | | creatorName: '', |
| | | billStatus: '', |
| | | projectStage: '', |
| | | estimatedDays: 0, |
| | | planStartDate: '', |
| | | planEndDate: '', |
| | | projectManagementPlanId: undefined, |
| | | projectAmount: 0, |
| | | auditStatus: '', |
| | | remark: '', |
| | | attachmentIds: [], |
| | | teamList: [], |
| | | phaseList: [], |
| | | addressList: [], |
| | | contactName: '', |
| | | contactGender: '', |
| | | contactBirthday: '', |
| | | contactEmail: '', |
| | | contactDept: '', |
| | | contactJob: '', |
| | | contactMobile: '', |
| | | contactWechat: '', |
| | | contactQq: '', |
| | | contactWorkWechat: '', |
| | | contactAddress: '', |
| | | contactRemark: '' |
| | | }) |
| | | fileList.value = [] |
| | | productData.value = [] |
| | | } |
| | | |
| | | function formattedNumber(row, column, cellValue) { |
| | | const val = Number(cellValue ?? 0) |
| | | return Number.isFinite(val) ? val.toFixed(2) : '0.00' |
| | | } |
| | | |
| | | function summarizeProductTable(param) { |
| | | return proxy.summarizeTable(param, ['taxInclusiveTotalPrice', 'taxExclusiveTotalPrice']) |
| | | } |
| | | |
| | | function productSelected(selection) { |
| | | productSelectedRows.value = selection |
| | | } |
| | | |
| | | function convertIdToValue(data) { |
| | | return (Array.isArray(data) ? data : []).map(item => { |
| | | const { id, children, ...rest } = item |
| | | const newItem = { |
| | | ...rest, |
| | | value: id |
| | | } |
| | | if (children && children.length > 0) { |
| | | newItem.children = convertIdToValue(children) |
| | | } |
| | | return newItem |
| | | }) |
| | | } |
| | | |
| | | function findNodeById(nodes, productId) { |
| | | for (let i = 0; i < (nodes || []).length; i++) { |
| | | if (nodes[i].value === productId) { |
| | | return nodes[i].label |
| | | } |
| | | if (nodes[i].children && nodes[i].children.length > 0) { |
| | | const foundNode = findNodeById(nodes[i].children, productId) |
| | | if (foundNode) return foundNode |
| | | } |
| | | } |
| | | return null |
| | | } |
| | | |
| | | function findNodeIdByLabel(nodes, label) { |
| | | if (!label) return null |
| | | for (let i = 0; i < (nodes || []).length; i++) { |
| | | const node = nodes[i] |
| | | if (node.label === label) return node.value |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeIdByLabel(node.children, label) |
| | | if (found !== null && found !== undefined) return found |
| | | } |
| | | } |
| | | return null |
| | | } |
| | | |
| | | function getProductOptions() { |
| | | return productTreeList().then(res => { |
| | | const list = res?.data || res |
| | | productCategoryOptions.value = convertIdToValue(list) |
| | | return productCategoryOptions.value |
| | | }) |
| | | } |
| | | |
| | | function getModels(value) { |
| | | const categoryLabel = findNodeById(productCategoryOptions.value, value) |
| | | productForm.value.productCategory = categoryLabel || '' |
| | | modelList({ id: value }).then(res => { |
| | | modelOptions.value = res?.data || res || [] |
| | | }) |
| | | } |
| | | |
| | | function 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 = '' |
| | | productForm.value.unit = '' |
| | | } |
| | | } |
| | | |
| | | async function openProductForm(type, row, index) { |
| | | productOperationType.value = type |
| | | productIndex.value = index || 0 |
| | | productForm.value = {} |
| | | proxy.resetForm('productFormRef') |
| | | |
| | | if (!productCategoryOptions.value || productCategoryOptions.value.length === 0) { |
| | | await getProductOptions() |
| | | } |
| | | |
| | | if (type === 'edit' && row) { |
| | | productForm.value = { ...row } |
| | | try { |
| | | const categoryId = findNodeIdByLabel(productCategoryOptions.value, productForm.value.productCategory) |
| | | if (categoryId) { |
| | | productForm.value.productCategoryId = categoryId |
| | | const models = await modelList({ id: categoryId }) |
| | | modelOptions.value = models?.data || models || [] |
| | | const currentModel = (modelOptions.value || []).find(m => m.model === productForm.value.specificationModel) |
| | | if (currentModel) { |
| | | productForm.value.productModelId = currentModel.id |
| | | } |
| | | } |
| | | } catch {} |
| | | } else { |
| | | productForm.value = { |
| | | productCategoryId: undefined, |
| | | productCategory: '', |
| | | productModelId: undefined, |
| | | specificationModel: '', |
| | | unit: '', |
| | | quantity: '', |
| | | taxInclusiveUnitPrice: '', |
| | | taxRate: '', |
| | | taxInclusiveTotalPrice: '', |
| | | taxExclusiveTotalPrice: '', |
| | | invoiceType: '' |
| | | } |
| | | } |
| | | |
| | | productFormVisible.value = true |
| | | } |
| | | |
| | | function closeProductDia() { |
| | | proxy.resetForm('productFormRef') |
| | | productFormVisible.value = false |
| | | } |
| | | |
| | | function submitProduct() { |
| | | productFormRef.value?.validate?.(valid => { |
| | | if (!valid) return |
| | | const payload = { ...productForm.value } |
| | | if (productOperationType.value === 'add') { |
| | | productData.value.push(payload) |
| | | } else { |
| | | productData.value[productIndex.value] = payload |
| | | } |
| | | closeProductDia() |
| | | }) |
| | | } |
| | | |
| | | function deleteProduct() { |
| | | if (!productSelectedRows.value || productSelectedRows.value.length === 0) { |
| | | proxy.$modal?.msgWarning?.('è¯·éæ©æ°æ®') |
| | | async function submit() { |
| | | form.projectExecutorNames=form.projectExecutorIds.map(id=>users.value.find(u=>String(u.value)===String(id))?.label||'').filter(Boolean) |
| | | if(personnelOnly.value){ |
| | | if(!form.projectExecutorIds.length)return ElMessage.warning('请è³å°ä¿çä¸åé¡¹ç®æ§è¡äºº') |
| | | saving.value=true |
| | | try{await updateProjectExecutors({projectId:form.id,executors:form.projectExecutorIds.map(id=>({userId:id,userName:users.value.find(user=>String(user.value)===String(id))?.label||''}))});ElMessage.success('é¡¹ç®æ§è¡äººå·²æ´æ°');visible.value=false;emit('completed',{id:form.id})}finally{saving.value=false} |
| | | return |
| | | } |
| | | const selectedIds = productSelectedRows.value.map(r => r?.id).filter(Boolean) |
| | | if (operationType.value !== 'add' && selectedIds.length > 0) { |
| | | delSalesProduct(selectedIds) |
| | | .then(() => { |
| | | proxy.$modal?.msgSuccess?.('å 餿å') |
| | | productData.value = productData.value.filter(row => !selectedIds.includes(row?.id)) |
| | | productSelectedRows.value = [] |
| | | }) |
| | | .catch(() => {}) |
| | | return |
| | | } |
| | | |
| | | productData.value = productData.value.filter(row => !productSelectedRows.value.includes(row)) |
| | | productSelectedRows.value = [] |
| | | } |
| | | |
| | | function calculateFromTotalPrice() { |
| | | if (isCalculating.value) return |
| | | const totalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice) |
| | | const quantity = parseFloat(productForm.value.quantity) |
| | | if (!totalPrice || !quantity || quantity <= 0) return |
| | | isCalculating.value = true |
| | | productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2) |
| | | if (productForm.value.taxRate) { |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(totalPrice, productForm.value.taxRate) |
| | | } |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | function calculateFromExclusiveTotalPrice() { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal?.msgWarning?.('请å
éæ©ç¨ç') |
| | | return |
| | | } |
| | | if (isCalculating.value) return |
| | | const exclusiveTotalPrice = parseFloat(productForm.value.taxExclusiveTotalPrice) |
| | | const quantity = parseFloat(productForm.value.quantity) |
| | | const taxRate = parseFloat(productForm.value.taxRate) |
| | | if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) return |
| | | isCalculating.value = true |
| | | const taxRateDecimal = taxRate / 100 |
| | | const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal) |
| | | productForm.value.taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2) |
| | | productForm.value.taxInclusiveUnitPrice = (inclusiveTotalPrice / quantity).toFixed(2) |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | function calculateFromQuantity() { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal?.msgWarning?.('请å
éæ©ç¨ç') |
| | | return |
| | | } |
| | | if (isCalculating.value) return |
| | | const quantity = parseFloat(productForm.value.quantity) |
| | | const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice) |
| | | if (!quantity || quantity <= 0 || !unitPrice) return |
| | | isCalculating.value = true |
| | | productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2) |
| | | if (productForm.value.taxRate) { |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice( |
| | | productForm.value.taxInclusiveTotalPrice, |
| | | productForm.value.taxRate |
| | | ) |
| | | } |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | function calculateFromUnitPrice() { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal?.msgWarning?.('请å
éæ©ç¨ç') |
| | | return |
| | | } |
| | | if (isCalculating.value) return |
| | | const quantity = parseFloat(productForm.value.quantity) |
| | | const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice) |
| | | if (!quantity || quantity <= 0 || !unitPrice) return |
| | | isCalculating.value = true |
| | | productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2) |
| | | if (productForm.value.taxRate) { |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice( |
| | | productForm.value.taxInclusiveTotalPrice, |
| | | productForm.value.taxRate |
| | | ) |
| | | } |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | function calculateFromTaxRate() { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal?.msgWarning?.('请å
éæ©ç¨ç') |
| | | return |
| | | } |
| | | if (isCalculating.value) return |
| | | const inclusiveTotalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice) |
| | | const taxRate = parseFloat(productForm.value.taxRate) |
| | | if (!inclusiveTotalPrice || !taxRate) return |
| | | isCalculating.value = true |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(inclusiveTotalPrice, taxRate) |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | async function loadProjectTypeOptions() { |
| | | await formRef.value?.validate() |
| | | if(form.planEndTime < form.planStartTime) return ElMessage.warning('é¢è®¡ç»ææ¶é´ä¸è½æ©äºé¢è®¡å¼å§æ¶é´') |
| | | if(startProject.value&&!validateStartModules()) return |
| | | form.projectManagerNames=form.projectManagerIds.map(id=>users.value.find(u=>String(u.value)===String(id))?.label||'').filter(Boolean) |
| | | const info={ ...form, currentPhase:startProject.value?'æªå¼å§':form.currentPhase, establishTime:form.establishTime||new Date().toISOString().slice(0,10), managerId:form.projectLeaderId,managerName:form.projectLeaderName,attachmentIds:normalizeFiles(files.value).map(f=>f.id).filter(Boolean),projectManagementPlanId:null } |
| | | saving.value=true |
| | | try { |
| | | 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 |
| | | await (form.id?updateProject:addProject)({ info,shippingAddress:null,contractInfo:null,salesLedgerProductList:null }) |
| | | if(startProject.value){ |
| | | for(const module of startModules.value){ |
| | | const members=module.memberIds.map(id=>({userId:id,userName:users.value.find(user=>String(user.value)===String(id))?.label||''})) |
| | | const res=await saveProjectModule({...module,projectId:form.id,members}) |
| | | module.id=res?.data||module.id |
| | | } |
| | | const retainedIds=new Set(startModules.value.map(module=>String(module.id))) |
| | | for(const id of originalModuleIds.value.filter(id=>!retainedIds.has(String(id)))) await deleteProjectModule(id) |
| | | await startProjectWorkflow(form.id) |
| | | } |
| | | } catch { |
| | | projectTypeOptions.value = [] |
| | | } |
| | | ElMessage.success(startProject.value?'项ç®å·²å¼å§':'ä¿åæå') |
| | | visible.value=false |
| | | emit('completed',{ id:form.id,startProject:startProject.value }) |
| | | } finally { saving.value=false } |
| | | } |
| | | |
| | | async function loadRoleOptions() { |
| | | 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 = [] |
| | | } |
| | | } |
| | | |
| | | async function loadUserOptions() { |
| | | 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 = [] |
| | | } |
| | | } |
| | | |
| | | 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() { |
| | | form.value.teamList.push({ |
| | | memberId: undefined, |
| | | roleId: undefined, |
| | | enterDate: '', |
| | | leaveDate: '', |
| | | phone: '', |
| | | remark: '' |
| | | }) |
| | | } |
| | | |
| | | function removeTeamRow(index) { |
| | | if (index > -1) form.value.teamList.splice(index, 1) |
| | | } |
| | | |
| | | function addPhaseRow() { |
| | | form.value.phaseList.push({ |
| | | phaseName: '', |
| | | description: '', |
| | | ownerId: undefined, |
| | | planDays: 0, |
| | | planStartDate: '', |
| | | planEndDate: '', |
| | | progress: 0, |
| | | actualStartDate: '', |
| | | actualEndDate: '', |
| | | overdueDays: 0, |
| | | completionRemark: '' |
| | | }) |
| | | } |
| | | |
| | | function removePhaseRow(index) { |
| | | if (index > -1) form.value.phaseList.splice(index, 1) |
| | | } |
| | | |
| | | function addAddressRow() { |
| | | form.value.addressList.push({ |
| | | receiver: '', |
| | | phone: '', |
| | | address: '' |
| | | }) |
| | | } |
| | | |
| | | function removeAddressRow(index) { |
| | | if (index > -1) form.value.addressList.splice(index, 1) |
| | | } |
| | | |
| | | async function openDialog(payload = {}) { |
| | | operationType.value = payload.operationType || 'add' |
| | | resetFormData() |
| | | await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), loadCustomerOptions(), getProductOptions()]) |
| | | if (payload.row?.id) { |
| | | try { |
| | | const res = await getProject(payload.row.id) |
| | | const detail = res?.data?.data ?? res?.data ?? res |
| | | const info = detail?.info || {} |
| | | const shippingAddress = detail?.shippingAddress || {} |
| | | const contractInfo = detail?.contractInfo || {} |
| | | |
| | | const normalizeId = v => { |
| | | if (v === undefined || v === null || v === '') return undefined |
| | | const n = Number(v) |
| | | return Number.isNaN(n) ? v : n |
| | | } |
| | | |
| | | const normalizeDictValue = v => { |
| | | if (v === undefined || v === null || v === '') return '' |
| | | return String(v) |
| | | } |
| | | |
| | | const computeEstimatedDays = (start, end) => { |
| | | 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)) return 0 |
| | | if (endTime < startTime) return 0 |
| | | return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1 |
| | | } |
| | | |
| | | Object.assign(form.value, { |
| | | id: info.id, |
| | | billNo: info.no ?? '', |
| | | projectManagementPlanId: info.projectManagementPlanId ?? '', |
| | | 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 : [], |
| | | teamList: Array.isArray(info.teamList) ? info.teamList.map(t => ({ |
| | | memberId: normalizeId(t.userId), |
| | | roleId: normalizeId(t.userRoleId), |
| | | enterDate: t.joinTime, |
| | | leaveDate: t.departTime, |
| | | phone: t.contact, |
| | | remark: t.remark |
| | | })) : [], |
| | | addressList: shippingAddress?.address |
| | | ? [{ |
| | | receiver: shippingAddress.consignee, |
| | | phone: shippingAddress.contract, |
| | | address: shippingAddress.address |
| | | }] |
| | | : [], |
| | | contactName: contractInfo.name ?? '', |
| | | contactGender: contractInfo.sex === 'ç·' ? '1' : contractInfo.sex === '女' ? '2' : '', |
| | | contactBirthday: contractInfo.birthday ?? '', |
| | | contactDept: contractInfo.department ?? '', |
| | | contactJob: contractInfo.job ?? '', |
| | | contactMobile: contractInfo.phoneNumber ?? '', |
| | | contactEmail: contractInfo.email ?? '', |
| | | contactQq: contractInfo.qq ?? '', |
| | | contactWechat: contractInfo.wx ?? '', |
| | | contactWorkWechat: contractInfo.lineaFissa ?? '', |
| | | contactAddress: contractInfo.origineEtnica ?? '', |
| | | contactRemark: contractInfo.rappresentanteLegale ?? '' |
| | | }) |
| | | |
| | | fileList.value = Array.isArray(info.attachmentList) |
| | | ? info.attachmentList.map(a => ({ |
| | | id: a.id ?? a.fileId, |
| | | name: a.fileName ?? a.name, |
| | | url: a.url ?? a.fileUrl ?? a.path |
| | | })) |
| | | : [] |
| | | |
| | | const rawPhaseList = |
| | | detail?.phaseList || |
| | | detail?.projectPhaseList || |
| | | detail?.projectStageList || |
| | | info?.phaseList || |
| | | info?.projectPhaseList || |
| | | [] |
| | | form.value.phaseList = Array.isArray(rawPhaseList) |
| | | ? rawPhaseList.map(p => ({ |
| | | phaseName: p.phaseName ?? p.name ?? p.title ?? '', |
| | | description: p.description ?? p.workContent ?? p.desc ?? '', |
| | | ownerId: normalizeId(p.ownerId ?? p.leaderId ?? p.userId), |
| | | planDays: Number(p.planDays ?? p.estimatedDuration ?? p.estimatedDays) || 0, |
| | | planStartDate: p.planStartDate ?? p.planStartTime ?? p.startDate ?? '', |
| | | planEndDate: p.planEndDate ?? p.planEndTime ?? p.endDate ?? '', |
| | | progress: Number(p.progress ?? p.schedule) || 0, |
| | | actualStartDate: p.actualStartDate ?? p.actualStartTime ?? '', |
| | | actualEndDate: p.actualEndDate ?? p.actualEndTime ?? '', |
| | | overdueDays: Number(p.overdueDays ?? p.overDays) || 0, |
| | | completionRemark: p.completionRemark ?? p.remark ?? '' |
| | | })) |
| | | : [] |
| | | |
| | | productData.value = detail?.salesLedgerProductList || detail?.productData || [] |
| | | } catch {} |
| | | } |
| | | if (form.value.teamList.length === 0 && !isView.value) addTeamRow() |
| | | if (form.value.phaseList.length === 0 && !isView.value) addPhaseRow() |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | function downloadAttachment(att) { |
| | | if (att) { |
| | | try { |
| | | proxy.$download.byUrl(att.url, att.originalFilename); |
| | | return |
| | | } catch (e) {} |
| | | } |
| | | ElMessage.warning('éä»¶ææ ä¸è½½å°å') |
| | | } |
| | | function closeDialog() { |
| | | dialogVisible.value = false |
| | | } |
| | | |
| | | async function submitForm() { |
| | | if (isView.value) { |
| | | closeDialog() |
| | | return |
| | | } |
| | | await formRef.value?.validate?.() |
| | | const info = { |
| | | id: form.value.id ?? null, |
| | | no: form.value.billNo, |
| | | title: form.value.projectName, |
| | | clientId: form.value.clientId ?? null, |
| | | clientName: form.value.customerName, |
| | | projectManagementInfoParentId: form.value.parentProjectId ?? null, |
| | | projectManagementPlanId: form.value.projectManagementPlanId ?? null, |
| | | 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, |
| | | salesmanName: form.value.salesmanName ?? '', |
| | | planStartTime: form.value.planStartDate, |
| | | planEndTime: form.value.planEndDate, |
| | | actualStartTime: form.value.actualStartDate, |
| | | actualEndTime: form.value.actualEndDate, |
| | | status: form.value.billStatus === '' || form.value.billStatus === undefined || form.value.billStatus === null ? null : Number(form.value.billStatus), |
| | | departmentId: form.value.departmentId ?? null, |
| | | departmentName: form.value.departmentName ?? '', |
| | | orderDate: form.value.orderDate, |
| | | orderAmount: form.value.projectAmount, |
| | | 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: (fileList.value || []) |
| | | .map(f => (f?.id ?? f?.rawData?.id)) |
| | | .filter(v => v !== undefined && v !== null && v !== '') |
| | | .map(String) |
| | | } |
| | | |
| | | const payload = { |
| | | info, |
| | | shippingAddress: null, |
| | | contractInfo: null, |
| | | salesLedgerProductList: null |
| | | } |
| | | |
| | | const req = operationType.value === 'edit' ? updateProject : addProject |
| | | const res = await req(payload) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('ä¿åæå') |
| | | closeDialog() |
| | | emit('completed') |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'ä¿å失败') |
| | | } |
| | | |
| | | defineExpose({ openDialog }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .section { |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 8px; |
| | | margin-bottom: 14px; |
| | | background: #fff; |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 14px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .section-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .section-bar { |
| | | width: 3px; |
| | | height: 14px; |
| | | background: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .section-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .toggle-icon { |
| | | color: #909399; |
| | | } |
| | | |
| | | .section-body { |
| | | padding: 0 14px 14px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | display: flex; |
| | | justify-content: center; |
| | | gap: 12px; |
| | | } |
| | | .attachment-upload{ |
| | | |
| | | } |
| | | </style> |
| | | <style scoped>.tip{margin-bottom:18px}.module-card{padding:14px 14px 2px;margin-bottom:12px;border:1px solid #dcdfe6;border-radius:6px}.module-card__title,.module-actions{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.module-card__title{font-weight:600}.module-actions{color:#606266}.unit{margin-left:6px;color:#606266}.field-hint{width:100%;margin-top:4px;color:#909399;font-size:12px;line-height:18px}</style> |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <SearchPanel |
| | | v-model="queryParams" |
| | | :schema="searchSchema" |
| | | @search="handleQuery" |
| | | @reset="resetQuery" |
| | | > |
| | | <template #billStatus="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©åæ®ç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in bill_status" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #auditStatus="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©è®¡åç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in project_management" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #projectStage="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©å®¡æ ¸ç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in plan_status" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | </SearchPanel> |
| | | |
| | | <div class="table-container"> |
| | | <div class="table-actions"> |
| | | <el-button style="background-color: #002FA7; color: #fff" @click="handleAdd">æ°å¢</el-button> |
| | | <!-- <el-dropdown split-button type="default" @command="handleGenerateBill" style="margin-left: 10px;"> |
| | | çæåæ® |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item command="1">çæåæ®1</el-dropdown-item> |
| | | <el-dropdown-item command="2">çæåæ®2</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | <el-card shadow="never" class="search-card"> |
| | | <el-form :inline="true" :model="query"> |
| | | <el-form-item label="项ç®åç§°/ç¼å·"><el-input v-model="query.keyword" clearable /></el-form-item> |
| | | <el-form-item label="客æ·åç§°"><el-input v-model="query.customerName" clearable /></el-form-item> |
| | | <el-form-item label="é¡¹ç®æ¥æº"><el-select v-model="query.source" clearable style="width:150px"><el-option label="æå¨å建" value="æå¨å建" /><el-option label="ä¸å¡çæ" value="ä¸å¡çæ" /></el-select></el-form-item> |
| | | <el-form-item><el-button type="primary" @click="search">æ¥è¯¢</el-button><el-button @click="reset">éç½®</el-button></el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | <el-card shadow="never"> |
| | | <div class="actions"><el-button type="primary" @click="add">æå¨å建项ç®</el-button></div> |
| | | <el-table :data="rows" v-loading="loading" border> |
| | | <el-table-column prop="no" label="项ç®ç¼å·" width="170" /> |
| | | <el-table-column prop="title" label="项ç®åç§°" min-width="180" /> |
| | | <el-table-column prop="clientName" label="客æ·åç§°" min-width="160" /> |
| | | <el-table-column prop="source" label="é¡¹ç®æ¥æº" width="110" /> |
| | | <el-table-column prop="projectClassification" label="项ç®åç±»" width="140" /> |
| | | <el-table-column prop="projectLeaderName" label="项ç®è´è´£äºº" width="120" /> |
| | | <el-table-column prop="currentPhase" label="å½åé¶æ®µ" width="110"><template #default="{row}"><el-tag :type="row.currentPhase==='已宿'?'success':row.currentPhase==='æªå¼å§'?'info':'warning'">{{ row.currentPhase || 'æªå¼å§' }}</el-tag></template></el-table-column> |
| | | <el-table-column prop="planStartTime" label="é¢è®¡å¼å§" width="120" /> |
| | | <el-table-column prop="planEndTime" label="é¢è®¡ç»æ" width="120" /> |
| | | <el-table-column label="æä½" width="270" fixed="right" align="center"> |
| | | <template #default="{row}"> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" link type="success" @click="start(row)">å¼å§é¡¹ç®</el-button> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" link type="primary" @click="edit(row)">ç¼è¾</el-button> |
| | | <el-button v-else link type="primary" @click="adjustExecutors(row)">è°æ´æ§è¡äºº</el-button> |
| | | <el-button link type="primary" @click="detail(row)">详æ
</el-button> |
| | | <el-button link type="danger" @click="remove(row)">å é¤</el-button> |
| | | </template> |
| | | </el-dropdown> --> |
| | | <el-button :loading="submitLoading" @click="handleSubmit">æäº¤</el-button> |
| | | <el-button :loading="auditLoading" @click="handleAudit">å®¡æ ¸</el-button> |
| | | <!-- <el-button :loading="reverseAuditLoading" @click="handleReverseAudit">åå®¡æ ¸</el-button> --> |
| | | <el-button :loading="deleteLoading" @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | |
| | | <PIMTable |
| | | :column="columns" |
| | | :tableData="tableData" |
| | | :page="pagination" |
| | | :tableLoading="loading" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="handlePagination" |
| | | > |
| | | <template #auditStatus="{ row }"> |
| | | <dict-tag :options="project_management" :value="row.auditStatus" /> |
| | | </template> |
| | | <template #projectStage="{ row }"> |
| | | <dict-tag :options="plan_status" :value="row.projectStage" /> |
| | | </template> |
| | | <template #action="{ row }"> |
| | | <el-button link type="primary" @click="handleEdit(row)">ç¼è¾</el-button> |
| | | <el-button link type="primary" :loading="progressBtnLoadingId===row.id" @click="handleProgressReport(row)">è¿åº¦æ±æ¥</el-button> |
| | | <el-button link type="primary" @click="handleDetail(row)">详æ
</el-button> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <FormDia ref="formDiaRef" @completed="getList" /> |
| | | <ProgressReportDialog |
| | | v-model="progressReportVisible" |
| | | :project-id="progressProjectId" |
| | | :project-info="progressProjectInfo" |
| | | :plan-nodes="progressPlanNodes" |
| | | :default-plan-node-id="progressDefaultPlanNodeId" |
| | | @submitted="handleProgressSubmitted" |
| | | /> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination v-show="total>0" :total="total" v-model:page="query.pageNum" v-model:limit="query.pageSize" @pagination="load" /> |
| | | </el-card> |
| | | <FormDia ref="formRef" @completed="completed" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagement"> |
| | | import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue' |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import SearchPanel from '@/components/SearchPanel/index.vue' |
| | | import PIMTable from '@/components/PIMTable/PIMTable.vue' |
| | | import FormDia from './components/formDia.vue' |
| | | import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue' |
| | | import { |
| | | listProject, |
| | | delProject, |
| | | submitProject, |
| | | auditProject, |
| | | reverseAuditProject, |
| | | getProject, |
| | | saveStage |
| | | } from '@/api/projectManagement/project' |
| | | import { listPlan } from '@/api/projectManagement/projectType' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import useUserStore from '@/store/modules/user' |
| | | import { listProject, delProject } from '@/api/projectManagement/project' |
| | | import FormDia from './components/formDia.vue' |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status') |
| | | const router = useRouter() |
| | | const userStore = useUserStore() |
| | | |
| | | const loading = ref(false) |
| | | const ids = ref([]) |
| | | const tableData = ref([]) |
| | | const formDiaRef = ref() |
| | | const progressReportVisible = ref(false) |
| | | const progressProjectId = ref(undefined) |
| | | const progressProjectInfo = ref({}) |
| | | const progressPlanNodes = ref([]) |
| | | const progressDefaultPlanNodeId = ref(undefined) |
| | | const progressBtnLoadingId = ref(null) |
| | | const submitLoading = ref(false) |
| | | const auditLoading = ref(false) |
| | | const reverseAuditLoading = ref(false) |
| | | const deleteLoading = ref(false) |
| | | |
| | | const data = reactive({ |
| | | queryParams: { |
| | | projectNameOrCode: undefined, |
| | | customerName: undefined, |
| | | billStatus: undefined, |
| | | projectStage: undefined, |
| | | auditStatus: undefined, |
| | | salesperson: undefined, |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | }, |
| | | pagination: { |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | layout: 'total, sizes, prev, pager, next, jumper' |
| | | } |
| | | }) |
| | | |
| | | const { queryParams, pagination } = toRefs(data) |
| | | |
| | | const searchSchema = [ |
| | | { prop: 'projectNameOrCode', label: '项ç®åç§°/ç¼å·', type: 'input', placeholder: '请è¾å
¥é¡¹ç®åç§°/ç¼å·' }, |
| | | { prop: 'customerName', label: '客æ·åç§°', type: 'input', placeholder: '请è¾å
¥å®¢æ·åç§°' }, |
| | | { prop: 'billStatus', label: 'åæ®ç¶æ', slot: 'billStatus' }, |
| | | { prop: 'projectStage', label: '计åç¶æ', slot: 'projectStage' }, |
| | | { prop: 'auditStatus', label: 'å®¡æ ¸ç¶æ', slot: 'auditStatus' }, |
| | | { prop: 'salesperson', label: 'ä¸å¡äººå', type: 'input', placeholder: '请è¾å
¥ä¸å¡äººå' } |
| | | ] |
| | | |
| | | const columns = [ |
| | | { label: 'åæ®ç¼å·', prop: 'billNo', align: 'center', width: '150' }, |
| | | { label: '项ç®åç§°', prop: 'projectName', align: 'center' }, |
| | | { label: 'å®¡æ ¸ç¶æ', prop: 'auditStatus', align: 'center', dataType: 'slot', slot: 'auditStatus' }, |
| | | { label: '客æ·åç§°', prop: 'customerName', align: 'center' }, |
| | | { label: 'ç«é¡¹æ¥æ', prop: 'setupDate', align: 'center', width: '120' }, |
| | | { label: 'é¡¹ç®æ¥æº', prop: 'projectSource', align: 'center' }, |
| | | { label: '项ç®åç±»', prop: 'projectClassification', align: 'center' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: '250', dataType: 'slot', slot: 'action', fixed: 'right' } |
| | | ] |
| | | |
| | | function getList() { |
| | | loading.value = true |
| | | const params = { |
| | | noOrName: queryParams.value.projectNameOrCode, |
| | | clientName: queryParams.value.customerName, |
| | | salesmanName: queryParams.value.salesperson, |
| | | reviewStatus: queryParams.value.auditStatus, |
| | | stage: queryParams.value.projectStage, |
| | | current: queryParams.value.pageNum, |
| | | size: queryParams.value.pageSize |
| | | } |
| | | listProject(params) |
| | | .then(response => { |
| | | const records = response?.data?.records || response?.rows || response?.records || [] |
| | | const billFilter = queryParams.value.billStatus |
| | | const filtered = billFilter === undefined || billFilter === null || billFilter === '' |
| | | ? records |
| | | : records.filter(r => String(r.billStatus ?? r.status) === String(billFilter)) |
| | | tableData.value = filtered.map(r => ({ |
| | | id: r.id, |
| | | billNo: r.no ?? r.billNo, |
| | | projectName: r.title ?? r.projectName, |
| | | billStatus: r.billStatus ?? r.status, |
| | | auditStatus: r.reviewStatus ?? r.auditStatus, |
| | | projectStage: r.stage ?? r.projectStage, |
| | | customerName: r.clientName ?? r.customerName, |
| | | parentProject: r.parentTitle ?? r.parentName ?? r.parentProject, |
| | | setupDate: r.establishTime ?? r.setupDate, |
| | | projectType: r.planName ?? r.projectType, |
| | | projectSource: r.source ?? r.projectSource, |
| | | projectClassification: r.projectClassification, |
| | | raw: r |
| | | })) |
| | | pagination.value.total = response?.total || response?.data?.total || 0 |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false |
| | | }) |
| | | } |
| | | |
| | | function handleQuery() { |
| | | queryParams.value.pageNum = 1 |
| | | pagination.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | function resetQuery() { |
| | | queryParams.value = { |
| | | projectNameOrCode: undefined, |
| | | customerName: undefined, |
| | | billStatus: undefined, |
| | | projectStage: undefined, |
| | | auditStatus: undefined, |
| | | salesperson: undefined, |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | } |
| | | handleQuery() |
| | | } |
| | | |
| | | function handleSelectionChange(selection) { |
| | | ids.value = selection.map(item => item.id) |
| | | } |
| | | |
| | | function handlePagination({ page, limit }) { |
| | | queryParams.value.pageNum = page |
| | | queryParams.value.pageSize = limit |
| | | pagination.value.current = page |
| | | pagination.value.size = limit |
| | | getList() |
| | | } |
| | | |
| | | function handleAdd() { |
| | | formDiaRef.value?.openDialog({ operationType: 'add' }) |
| | | } |
| | | |
| | | async function handleDelete() { |
| | | const delIds = ids.value |
| | | if (delIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦å é¤çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å 餿鿰æ®é¡¹?', 'è¦å', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | deleteLoading.value = true |
| | | await delProject(delIds) |
| | | getList() |
| | | ElMessage.success('å 餿å') |
| | | } catch {} finally { |
| | | deleteLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | | const submitIds = ids.value |
| | | if (submitIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦æäº¤çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤æäº¤æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | submitLoading.value = true |
| | | await Promise.all(submitIds.map(id => submitProject({ id }))) |
| | | getList() |
| | | ElMessage.success('æäº¤æå') |
| | | } catch {} finally { |
| | | submitLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleAudit() { |
| | | const auditIds = ids.value |
| | | if (auditIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦å®¡æ ¸çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | auditLoading.value = true |
| | | await Promise.all(auditIds.map(id => auditProject({ id }))) |
| | | getList() |
| | | ElMessage.success('å®¡æ ¸æå') |
| | | } catch {} finally { |
| | | auditLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleReverseAudit() { |
| | | const reverseAuditIds = ids.value |
| | | if (reverseAuditIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦åå®¡æ ¸çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤åå®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | reverseAuditLoading.value = true |
| | | await Promise.all(reverseAuditIds.map(id => reverseAuditProject({ id }))) |
| | | getList() |
| | | ElMessage.success('åå®¡æ ¸æå') |
| | | } catch {} finally { |
| | | reverseAuditLoading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleGenerateBill(command) { |
| | | ElMessage.info(`çæåæ®: ${command}`) |
| | | } |
| | | |
| | | function computeDefaultPlanNodeId(stageVal, nodes) { |
| | | const list = Array.isArray(nodes) ? nodes : [] |
| | | if (list.length === 0) return undefined |
| | | const direct = list.find(n => String(n.id) === String(stageVal)) |
| | | if (direct?.id) return direct.id |
| | | const sorted = [...list].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 handleProgressReport(row) { |
| | | if (!row?.id) return |
| | | try { |
| | | progressBtnLoadingId.value = row.id |
| | | const res = await getProject(row.id) |
| | | const detail = res?.data?.data ?? res?.data ?? res |
| | | const info = detail?.info || {} |
| | | progressProjectId.value = info.id |
| | | progressProjectInfo.value = info |
| | | |
| | | const planId = info.projectManagementPlanId |
| | | if (planId) { |
| | | const planRes = await listPlan({ current: 1, size: 999 }) |
| | | const records = planRes?.data?.records || planRes?.records || planRes?.rows || [] |
| | | const plan = (records || []).find(p => String(p.id) === String(planId)) || {} |
| | | progressPlanNodes.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : [] |
| | | } else { |
| | | progressPlanNodes.value = [] |
| | | } |
| | | |
| | | progressDefaultPlanNodeId.value = computeDefaultPlanNodeId(info.stage, progressPlanNodes.value) |
| | | progressReportVisible.value = true |
| | | } catch (e) { |
| | | ElMessage.error('è·å项ç®è¯¦æ
失败') |
| | | } finally { |
| | | progressBtnLoadingId.value = null |
| | | } |
| | | } |
| | | |
| | | async function handleProgressSubmitted(payload) { |
| | | try { |
| | | const nodes = Array.isArray(progressPlanNodes.value) ? progressPlanNodes.value : [] |
| | | const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {} |
| | | const description = payload.remark |
| | | ? `${payload.reportDate || ''} ${payload.remark}`.trim() |
| | | : `${payload.reportDate || ''} è¿åº¦æ±æ¥`.trim() |
| | | const req = { |
| | | id: null, |
| | | projectManagementPlanNodeId: payload.planNodeId, |
| | | projectManagementInfoId: progressProjectId.value, |
| | | description, |
| | | actualLeaderId: userStore.id || progressProjectInfo.value?.managerId, |
| | | actualLeaderName: userStore.nickName || progressProjectInfo.value?.managerName, |
| | | estimatedDuration: Number(node.estimatedDuration ?? 0) || 0, |
| | | planStartTime: payload.planStartTime || progressProjectInfo.value?.planStartTime, |
| | | planEndTime: payload.planEndTime || progressProjectInfo.value?.planEndTime, |
| | | actualStartTime: payload.actualStartTime || null, |
| | | actualEndTime: payload.actualEndTime || null, |
| | | progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0, |
| | | storageBlobDTOs: Array.isArray(payload.storageBlobDTOs) ? payload.storageBlobDTOs : [] |
| | | } |
| | | const res = await saveStage(req) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('æäº¤æå') |
| | | getList() |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'æäº¤å¤±è´¥') |
| | | } catch (e) { |
| | | ElMessage.error('æäº¤å¤±è´¥') |
| | | } |
| | | } |
| | | |
| | | function handleDetail(row) { |
| | | if (!row?.id) return |
| | | router.push(`/projectManagement/Management/detail/${row.id}`) |
| | | } |
| | | |
| | | function handleEdit(row) { |
| | | formDiaRef.value?.openDialog({ operationType: 'edit', row }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | const router=useRouter(); const loading=ref(false); const rows=ref([]); const total=ref(0); const formRef=ref() |
| | | const query=reactive({ keyword:'',customerName:'',source:'',pageNum:1,pageSize:10 }) |
| | | async function load(){ loading.value=true; try{ const res=await listProject({ noOrName:query.keyword,clientName:query.customerName,current:query.pageNum,size:query.pageSize }); const data=res?.data?.records||res?.rows||[]; rows.value=(data||[]).filter(r=>!query.source||r.source===query.source); total.value=res?.data?.total||res?.total||0 }finally{loading.value=false} } |
| | | function search(){query.pageNum=1;load()} function reset(){Object.assign(query,{keyword:'',customerName:'',source:'',pageNum:1,pageSize:10});load()} |
| | | function add(){formRef.value?.openDialog({operationType:'add'})} function edit(row){formRef.value?.openDialog({operationType:'edit',row})} function start(row){formRef.value?.openDialog({operationType:'edit',row,startProject:true})} |
| | | function adjustExecutors(row){formRef.value?.openDialog({operationType:'edit',row,personnelOnly:true})} |
| | | function detail(row){router.push(`/projectManagement/Management/detail/${row.id}`)} |
| | | function completed(result){ load(); if(result?.startProject&&result?.id) router.push(`/projectManagement/Management/detail/${result.id}`) } |
| | | async function remove(row){ await ElMessageBox.confirm(`确认å é¤é¡¹ç®â${row.title}âåï¼`,'æç¤º',{type:'warning'}); await delProject(row.id); ElMessage.success('å 餿å');load() } |
| | | onMounted(load) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | .table-container { |
| | | background-color: #fff; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | | } |
| | | .table-actions { |
| | | text-align: right; |
| | | margin-bottom: 10px; |
| | | } |
| | | </style> |
| | | <style scoped>.app-container{padding:20px}.search-card{margin-bottom:16px}.actions{margin-bottom:14px;text-align:right}</style> |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-card class="header-card" shadow="never"> |
| | | <div class="app-container" v-loading="loading"> |
| | | <el-card shadow="never" class="header-card"> |
| | | <div class="header-row"> |
| | | <div class="header-title">项ç®è¯¦æ
</div> |
| | | <div class="header-actions"> |
| | | <el-button style="color: white;background: #002FA7;" @click="openProgressReport">è¿åº¦æ±æ¥</el-button> |
| | | <el-button @click="goBack">è¿å</el-button> |
| | | </div> |
| | | </div> |
| | | <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 class="title">{{ info.title || '项ç®è¯¦æ
' }}</div><div class="subtitle">{{ info.no || '-' }} · {{ info.source || 'æå¨å建' }}</div></div> |
| | | <div> |
| | | <el-tag :type="phaseTagType" size="large">{{ info.currentPhase || 'æªå¼å§' }}</el-tag> |
| | | <el-button v-if="info.currentPhase === 'æªå¼å§'" type="primary" @click="openStart">å¼å§é¡¹ç®</el-button> |
| | | <el-button v-if="info.currentPhase === 'æªå¼å§'" @click="openEdit">ç¼è¾ç«é¡¹ä¿¡æ¯</el-button> |
| | | <el-button v-else @click="adjustExecutors">è°æ´é¡¹ç®æ§è¡äºº</el-button> |
| | | <el-button v-if="nextStage" type="success" @click="advanceStage">è¿å
¥{{ nextStage }}</el-button> |
| | | <el-button @click="router.back()">è¿å</el-button> |
| | | </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="åæ®ç¼å·">{{ 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="项ç®åç±»">{{ info.projectClassification || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¶é¡¹ç®">{{ parentProjectLabel }}</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="å½åé¶æ®µ">{{ currentStageName }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸ç¶æ"> |
| | | <dict-tag :options="project_management" :value="String(info.reviewStatus ?? '')" /> |
| | | </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"> |
| | | <div class="attachment-title">éä»¶</div> |
| | | <div class="attachment-list"> |
| | | <div v-for="(att, idx) in attachments" :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-tab-pane> |
| | | |
| | | <el-tab-pane label="项ç®é¶æ®µ" name="stage"> |
| | | <el-table :data="stageNodeRows" border style="margin-top: 14px;"> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="é¶æ®µ" prop="stageName" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="æè¿°" prop="description" min-width="220" show-overflow-tooltip /> |
| | | <el-table-column label="å®é
è´è´£äºº" prop="actualLeaderName" width="140" show-overflow-tooltip /> |
| | | <el-table-column label="è¿åº¦(%)" prop="progress" width="110" /> |
| | | <el-table-column label="计åå¼å§" prop="planStartTime" width="120" /> |
| | | <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="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"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="danger" size="small" @click="handleDeleteStage(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | <el-card shadow="never" class="content-card"> |
| | | <el-descriptions title="项ç®åºæ¬æ
åµ" :column="4" border> |
| | | <el-descriptions-item label="项ç®åç§°">{{ info.title || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ info.clientName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¡¹ç®æ¥æº">{{ info.source || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®åç±»">{{ info.projectClassification || '-' }}</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.projectLeaderName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®ç»ç">{{ (info.projectManagerNames || []).join('ã') || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¡¹ç®æ§è¡äºº" :span="2">{{ (info.projectExecutorNames || []).join('ã') || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="è·è¿å¨æ">{{ followCycleLabel }}</el-descriptions-item> |
| | | <el-descriptions-item label="è·è¿è¦æ±" :span="3">{{ info.followDetail || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®å¤æ³¨" :span="4">{{ info.remark || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div v-if="attachments.length" class="attachment-block"> |
| | | <div class="section-title">项ç®éä»¶</div> |
| | | <el-button v-for="(file,index) in attachments" :key="file.id || index" link type="primary" @click="download(file)">{{ file.name || file.fileName || `éä»¶${index + 1}` }}</el-button> |
| | | </div> |
| | | </el-card> |
| | | <el-card shadow="never" class="content-card"><ProjectExecutionPanel v-if="info.id" ref="executionRef" :project-id="info.id" :project-info="info" /></el-card> |
| | | <FormDia ref="formDiaRef" @completed="loadDetail" /> |
| | | </div> |
| | | |
| | | <ProgressReportDialog |
| | | v-model="progressReportVisible" |
| | | :project-id="info.id" |
| | | :project-info="info" |
| | | :plan-nodes="planNodeRows" |
| | | :stage-records="stageNodeRows" |
| | | :default-plan-node-id="defaultPlanNodeId" |
| | | @submitted="handleProgressSubmitted" |
| | | /> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagementDetail"> |
| | | import { computed, getCurrentInstance, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { Document } from '@element-plus/icons-vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { getProject, saveStage, listStage, deleteStage, updateProject } from '@/api/projectManagement/project' |
| | | import { listPlan } from '@/api/projectManagement/projectType' |
| | | import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue' |
| | | import useUserStore from '@/store/modules/user' |
| | | import { getProject } from '@/api/projectManagement/project' |
| | | import { transitionProjectStage } from '@/api/projectManagement/workflow' |
| | | import ProjectExecutionPanel from './components/ProjectExecutionPanel.vue' |
| | | import FormDia from './components/formDia.vue' |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const { project_management } = proxy.useDict('project_management') |
| | | |
| | | const loading = ref(false) |
| | | const activeTab = ref('base') |
| | | const progressReportVisible = ref(false) |
| | | const userStore = useUserStore() |
| | | |
| | | const info = ref({}) |
| | | const attachments = ref([]) |
| | | const projectTypeMap = ref(new Map()) |
| | | const planNodeRows = ref([]) |
| | | const stageNodeRows = ref([]) |
| | | |
| | | const isFromContract = computed(() => info.value?.source === 'ä¸å¡å®¡æ¹') |
| | | |
| | | const parentProjectLabel = computed(() => { |
| | | return ( |
| | | info.value?.parentTitle || |
| | | info.value?.projectManagementInfoParentName || |
| | | info.value?.projectManagementInfoParentId || |
| | | '-' |
| | | ) |
| | | }) |
| | | |
| | | // åé¶æ®µå½åè¿åº¦ï¼åä¸é¶æ®µå¤æ¬¡æ±æ¥åæå¤§å¼ï¼ |
| | | 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 |
| | | }) |
| | | |
| | | // é¶æ®µè¿åº¦æ¡æ°æ®ï¼ä¼å
使ç¨é¡¹ç®ä¸åæ¥ç 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 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() { |
| | | router.back() |
| | | } |
| | | |
| | | function openProgressReport() { |
| | | progressReportVisible.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 || 'å¢éä¿å失败') |
| | | } |
| | | |
| | | 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 |
| | | ? `${payload.reportDate || ''} ${payload.remark}`.trim() |
| | | : `${payload.reportDate || ''} è¿åº¦æ±æ¥`.trim() |
| | | const req = { |
| | | id: null, |
| | | projectManagementPlanNodeId: payload.planNodeId, |
| | | projectManagementInfoId: info.value?.id, |
| | | description, |
| | | actualLeaderId: userStore.id || info.value?.managerId, |
| | | 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.completionProgress ?? payload.totalProgress ?? 0) || 0, |
| | | storageBlobDTOs: Array.isArray(payload.storageBlobDTOs) ? payload.storageBlobDTOs : [] |
| | | } |
| | | 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(e?.message || 'æäº¤å¤±è´¥') |
| | | } |
| | | } |
| | | |
| | | function downloadAttachment(row) { |
| | | if (row?.url) { |
| | | try { |
| | | proxy.$download.byUrl(row.url, row.originalFilename) |
| | | return |
| | | } catch (e) {} |
| | | } |
| | | ElMessage.warning('éä»¶ææ ä¸è½½å°å') |
| | | } |
| | | |
| | | async function loadProjectTypeMap() { |
| | | try { |
| | | const res = await listPlan({ current: 1, size: 999 }) |
| | | const records = res?.data?.records || res?.records || res?.rows || [] |
| | | projectTypeMap.value = new Map((records || []).map(r => [Number(r.id), r])) |
| | | } catch { |
| | | projectTypeMap.value = new Map() |
| | | } |
| | | } |
| | | |
| | | function getPlanNodeName(planNodeId) { |
| | | const list = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const node = list.find(n => String(n.id) === String(planNodeId)) |
| | | return node?.name || node?.workContent || String(planNodeId ?? '') |
| | | } |
| | | |
| | | async function loadStageList() { |
| | | const projectId = info.value?.id |
| | | if (!projectId) { |
| | | stageNodeRows.value = [] |
| | | return |
| | | } |
| | | try { |
| | | const res = await listStage(projectId) |
| | | const data = res?.data?.data ?? res?.data ?? res |
| | | const list = data?.records || data?.rows || data?.list || data || [] |
| | | const records = Array.isArray(list) ? list : [] |
| | | stageNodeRows.value = records.map(r => { |
| | | const attachmentList = Array.isArray(r.attachmentList) ? r.attachmentList : [] |
| | | const blobList = Array.isArray(r.storageBlobVOs) ? r.storageBlobVOs : [] |
| | | return { |
| | | ...r, |
| | | stageName: getPlanNodeName(r.projectManagementPlanNodeId), |
| | | reportDate: r.createTime ? String(r.createTime).slice(0, 10) : '', |
| | | attachmentCount: attachmentList.length || blobList.length || 0 |
| | | } |
| | | }) |
| | | } catch { |
| | | stageNodeRows.value = [] |
| | | } |
| | | } |
| | | |
| | | async function handleDeleteStage(row) { |
| | | const stageId = row?.id |
| | | if (!stageId) return |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å é¤è¯¥é¡¹ç®é¶æ®µï¼', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | const res = await deleteStage(stageId) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('å 餿å') |
| | | await loadStageList() |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'å é¤å¤±è´¥') |
| | | } catch {} |
| | | } |
| | | |
| | | function syncProjectPlan() { |
| | | const id = info.value?.projectManagementPlanId |
| | | if (id === undefined || id === null || id === '') { |
| | | planNodeRows.value = [] |
| | | return |
| | | } |
| | | const plan = projectTypeMap.value.get(Number(id)) || {} |
| | | planNodeRows.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : [] |
| | | } |
| | | const formDiaRef = ref() |
| | | const executionRef = ref() |
| | | const followCycleLabel = computed(() => ({ MONTH: 'æ¯æ', QUARTER: 'æ¯å£åº¦', YEAR: 'æ¯å¹´' }[info.value.followCycle] || '-')) |
| | | const phaseTagType = computed(() => info.value.currentPhase === '已宿' ? 'success' : info.value.currentPhase === 'æªå¼å§' ? 'info' : 'warning') |
| | | const nextStage = computed(() => ({ 'ç«é¡¹': 'ç³æ¥', 'å¼å': 'ç³æ¥', 'ç³æ¥': 'ç³æ¥ç»æ', 'ç³æ¥ç»æ': 'éªæ¶', 'éªæ¶': '已宿' }[info.value.currentPhase] || '')) |
| | | |
| | | async function loadDetail() { |
| | | const id = route.params?.id |
| | | if (!id) return |
| | | loading.value = true |
| | | try { |
| | | const res = await getProject(id) |
| | | const res = await getProject(route.params.id) |
| | | const detail = res?.data?.data ?? res?.data ?? res |
| | | info.value = detail?.info || {} |
| | | attachments.value = Array.isArray(detail?.info?.attachmentList) ? detail.info.attachmentList : [] |
| | | syncProjectPlan() |
| | | await loadStageList() |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | attachments.value = info.value.attachmentList || [] |
| | | await executionRef.value?.refresh?.() |
| | | } catch { ElMessage.error('è·å项ç®è¯¦æ
失败') } finally { loading.value = false } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await loadProjectTypeMap() |
| | | function openEdit() { formDiaRef.value?.openDialog({ operationType: 'edit', row: { id: info.value.id }, startProject: false }) } |
| | | function openStart() { formDiaRef.value?.openDialog({ operationType: 'edit', row: { id: info.value.id }, startProject: true }) } |
| | | function adjustExecutors() { formDiaRef.value?.openDialog({ operationType: 'edit', row: { id: info.value.id }, personnelOnly: true }) } |
| | | async function advanceStage() { |
| | | await ElMessageBox.confirm(`确认å°é¡¹ç®æ¨è¿å°â${nextStage.value}âåï¼`, '项ç®é¶æ®µæ¨è¿', { type: 'warning' }) |
| | | await transitionProjectStage({ projectId: info.value.id, targetStage: nextStage.value }) |
| | | ElMessage.success(`å·²è¿å
¥${nextStage.value}é¶æ®µ`) |
| | | await loadDetail() |
| | | }) |
| | | } |
| | | function download(file) { if (!file?.url) return ElMessage.warning('éä»¶ææ ä¸è½½å°å'); proxy.$download.byUrl(file.url, file.name || file.fileName) } |
| | | onMounted(loadDetail) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .header-card { |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .header-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .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 { |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .attachment-block { |
| | | margin-top: 14px; |
| | | } |
| | | |
| | | .attachment-title { |
| | | font-weight: 600; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .attachment-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .attachment-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .attachment-name { |
| | | max-width: 520px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | <style scoped> |
| | | .app-container { padding: 20px; }.header-card,.content-card { margin-bottom: 16px; }.header-row { display:flex;align-items:center;justify-content:space-between;gap:16px; }.title { font-size:22px;font-weight:600;color:#1f2d3d; }.subtitle { margin-top:6px;color:#909399; }.header-row>div:last-child { display:flex;align-items:center;gap:10px; }.attachment-block { margin-top:18px; }.section-title { margin-bottom:10px;font-weight:600; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-card shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <div> |
| | | <div class="title">项ç®åç±»</div> |
| | | <div class="subtitle">ç»´æ¤å¤å±æ å½¢åç±»ï¼é¡¹ç®å¼å§æ¶éçº§éæ©</div> |
| | | </div> |
| | | <el-button v-hasPermi="['project:category:edit']" type="primary" @click="openDialog()">æ°å¢ä¸çº§åç±»</el-button> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="treeRows" |
| | | row-key="id" |
| | | border |
| | | default-expand-all |
| | | :tree-props="{ children: 'children' }" |
| | | > |
| | | <el-table-column prop="categoryName" label="åç±»åç§°" min-width="260" /> |
| | | <el-table-column prop="sort" label="æåº" width="100" align="center" /> |
| | | <el-table-column prop="remark" label="夿³¨" min-width="260" show-overflow-tooltip /> |
| | | <el-table-column label="æä½" width="250" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button v-hasPermi="['project:category:edit']" link type="primary" @click="openDialog(row, true)">æ°å¢ä¸çº§</el-button> |
| | | <el-button v-hasPermi="['project:category:edit']" link type="primary" @click="openDialog(row)">ä¿®æ¹</el-button> |
| | | <el-button v-hasPermi="['project:category:remove']" link type="danger" @click="removeRow(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-dialog v-model="dialogVisible" :title="form.id ? 'ä¿®æ¹é¡¹ç®åç±»' : 'æ°å¢é¡¹ç®åç±»'" width="520px" destroy-on-close> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="90px"> |
| | | <el-form-item label="ä¸çº§åç±»"> |
| | | <el-tree-select |
| | | v-model="form.parentId" |
| | | :data="parentOptions" |
| | | node-key="id" |
| | | :props="{ label: 'categoryName', children: 'children' }" |
| | | check-strictly |
| | | clearable |
| | | placeholder="ä¸é表示ä¸çº§åç±»" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="åç±»åç§°" prop="categoryName"> |
| | | <el-input v-model="form.categoryName" maxlength="100" show-word-limit /> |
| | | </el-form-item> |
| | | <el-form-item label="æåº"> |
| | | <el-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨"> |
| | | <el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="dialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" :loading="saving" @click="submit">ä¿å</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="ProjectCategory"> |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { deleteProjectCategory, listProjectCategories, saveProjectCategory } from '@/api/projectManagement/workflow' |
| | | |
| | | const loading = ref(false) |
| | | const saving = ref(false) |
| | | const dialogVisible = ref(false) |
| | | const formRef = ref() |
| | | const rows = ref([]) |
| | | const form = reactive({ id: null, parentId: null, categoryName: '', sort: 0, remark: '' }) |
| | | const rules = { categoryName: [{ required: true, message: '请è¾å
¥åç±»åç§°', trigger: 'blur' }] } |
| | | |
| | | function buildTree(list, excludedId = null) { |
| | | const source = list.filter(item => String(item.id) !== String(excludedId ?? '')) |
| | | const map = new Map(source.map(item => [String(item.id), { ...item, children: [] }])) |
| | | const roots = [] |
| | | map.forEach(item => { |
| | | const parent = map.get(String(item.parentId || 0)) |
| | | if (parent) parent.children.push(item) |
| | | else roots.push(item) |
| | | }) |
| | | const sortRows = list => list |
| | | .sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0)) |
| | | .map(item => ({ ...item, children: sortRows(item.children || []) })) |
| | | return sortRows(roots) |
| | | } |
| | | |
| | | const treeRows = computed(() => buildTree(rows.value)) |
| | | const parentOptions = computed(() => buildTree(rows.value, form.id)) |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | | const res = await listProjectCategories() |
| | | rows.value = Array.isArray(res?.data) ? res.data : [] |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | function openDialog(row = null, asChild = false) { |
| | | Object.assign(form, { |
| | | id: asChild ? null : row?.id ?? null, |
| | | parentId: asChild ? row?.id : row?.parentId || null, |
| | | categoryName: asChild ? '' : row?.categoryName || '', |
| | | sort: asChild ? 0 : Number(row?.sort || 0), |
| | | remark: asChild ? '' : row?.remark || '' |
| | | }) |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | async function submit() { |
| | | await formRef.value?.validate() |
| | | saving.value = true |
| | | try { |
| | | await saveProjectCategory({ ...form, parentId: form.parentId || 0 }) |
| | | ElMessage.success('ä¿åæå') |
| | | dialogVisible.value = false |
| | | await loadData() |
| | | } finally { |
| | | saving.value = false |
| | | } |
| | | } |
| | | |
| | | async function removeRow(row) { |
| | | await ElMessageBox.confirm(`确认å é¤åç±»â${row.categoryName}âåï¼`, 'æç¤º', { type: 'warning' }) |
| | | await deleteProjectCategory(row.id) |
| | | ElMessage.success('å 餿å') |
| | | await loadData() |
| | | } |
| | | |
| | | onMounted(loadData) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .card-header { display: flex; align-items: center; justify-content: space-between; } |
| | | .title { font-size: 17px; font-weight: 600; color: #303133; } |
| | | .subtitle { margin-top: 5px; font-size: 13px; color: #909399; } |
| | | </style> |
| | |
| | | @change="handleQuery" |
| | | /> |
| | | </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" |
| | |
| | | </el-table-column> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column |
| | | label="éå®ååå·" |
| | | prop="salesContractNo" |
| | | width="180" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="客æ·åç§°" |
| | | prop="customerName" |
| | | width="300" |
| | |
| | | prop="projectName" |
| | | width="180" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="仿¬¾æ¹å¼" |
| | | prop="paymentMethod" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="ååéé¢(å
)" |
| | | prop="contractAmount" |
| | | width="220" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" |
| | | /> |
| | | <el-table-column |
| | | label="å½å
¥äºº" |
| | |
| | | > |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ååç±»åï¼" prop="contractType"> |
| | | <el-select |
| | | v-model="form.contractType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | :disabled="operationType === 'view' || operationType === 'edit'" |
| | | > |
| | | <el-option label="æ®ééå®åå" :value="1" /> |
| | | <el-option label="æ¡æ¶åå" :value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="éå®ååå·ï¼" prop="salesContractNo"> |
| | | <div |
| | | style=" |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | width: 100%; |
| | | " |
| | | > |
| | | <el-checkbox |
| | | v-model="form.autoGenerateContractNo" |
| | | v-if="operationType === 'add'" |
| | | >èªå¨çæ |
| | | </el-checkbox> |
| | | <el-input |
| | | v-model="form.salesContractNo" |
| | | :placeholder=" |
| | | form.autoGenerateContractNo ? 'ä¿ååèªå¨çæ' : '请è¾å
¥' |
| | | " |
| | | clearable |
| | | :disabled=" |
| | | form.autoGenerateContractNo || operationType === 'view' |
| | | " |
| | | /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¸å¡åï¼" prop="salesman"> |
| | | <el-select |
| | | v-model="form.salesman" |
| | |
| | | v-model="form.projectName" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <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 |
| | | :disabled="operationType === 'view'" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="仿¬¾æ¹å¼"> |
| | | <el-input |
| | | v-model="form.paymentMethod" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="operationType === 'view'" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </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> |
| | |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | executionDate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | |
| | | // 主表åè®¡æ¹æ³ |
| | | const summarizeMainTable = (param) => { |
| | | return proxy.summarizeTable(param, [ |
| | | "contractAmount", |
| | | "taxInclusiveTotalPrice", |
| | | "taxExclusiveTotalPrice", |
| | | ]); |
| | |
| | | const { VITE_APP_ENV } = env; |
| | | const baseUrl = |
| | | env.VITE_APP_ENV === "development" |
| | | ? "http://127.0.0.1:7005" |
| | | ? (process.env.VITE_DEV_API_TARGET || "http://127.0.0.1:7006") |
| | | : env.VITE_BASE_API; |
| | | const javaUrl = |
| | | env.VITE_APP_ENV === "development" |