1. 完成项目管理全流程优化,补充阶段推进、汇报统计及生命周期记录。
2. 完成业务与项目数据关联,支持查看项目进度、详情及相关附件。
3. 完成采购审批流程接入,支持配置审批人员及采购业务审批。
4. 精简用户、角色、部门、菜单和字典,完善操作日志及附件下载。
| | |
| | | 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æ¯å¹´'"); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | CREATE TABLE IF NOT EXISTS `project_management_phase_history` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT, |
| | | `project_id` bigint NOT NULL, |
| | | `phase_code` varchar(30) NOT NULL, |
| | | `phase_name` varchar(50) NOT NULL, |
| | | `action_type` varchar(30) NOT NULL, |
| | | `action_label` varchar(100) NOT NULL, |
| | | `form_data` json DEFAULT NULL, |
| | | `remark` varchar(2000) DEFAULT NULL, |
| | | `attachments` json DEFAULT NULL, |
| | | `operator_name` varchar(100) DEFAULT NULL, |
| | | `action_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (`id`), |
| | | KEY `idx_phase_history_project` (`project_id`,`action_time`), |
| | | KEY `idx_phase_history_phase` (`project_id`,`phase_code`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项ç®é¶æ®µåçåå²'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- å½å项ç®ç³»ç»åºç¡æ°æ®ç²¾ç®èæ¬ |
| | | -- æ§è¡åå¤ä»½ï¼.local-backups/product-inventory-management-xmgl-before-system-cleanup-20261105-145021.sql |
| | | -- ä¸ä¸ªè´¦å·åå§å¯ç ç»ä¸ä¸ºï¼admin123ï¼ç»å½åè¯·åæ¶ä¿®æ¹æ®éç¨æ·å客æ·å¯ç ï¼ |
| | | |
| | | SET NAMES utf8mb4; |
| | | SET FOREIGN_KEY_CHECKS = 0; |
| | | START TRANSACTION; |
| | | |
| | | -- 1. ç¨æ·ãè§è²ãé¨é¨ï¼ä»
ä¿ç管çåãæ®éç¨æ·ã客æ·ä¸ç±»è´¦å·ã |
| | | DELETE FROM sys_user_post; |
| | | DELETE FROM sys_user_role; |
| | | DELETE FROM sys_user_dept; |
| | | DELETE FROM sys_role_menu; |
| | | DELETE FROM sys_role_dept; |
| | | DELETE FROM sys_user WHERE user_id <> 1; |
| | | |
| | | UPDATE sys_user |
| | | SET user_name='admin', nick_name='管çåè´¦å·', user_type='00', |
| | | password='$2a$10$E0Rg.GqdeQd1ba2Z5P87HOsOELqoKwYwKFQGRGop.LEPaom7yzji.', |
| | | status='0', del_flag='0', tenant_id=208, update_by='system-cleanup', update_time=NOW(), |
| | | remark='ç³»ç»ç®¡çå' |
| | | WHERE user_id=1; |
| | | |
| | | INSERT INTO sys_user |
| | | (user_id,user_name,nick_name,user_type,email,phonenumber,sex,password,status,del_flag, |
| | | create_by,create_time,remark,tenant_id,ai_enabled) |
| | | VALUES |
| | | (2,'user','æ®éç¨æ·','00','','','0','$2a$10$E0Rg.GqdeQd1ba2Z5P87HOsOELqoKwYwKFQGRGop.LEPaom7yzji.','0','0', |
| | | 'admin',NOW(),'é¡¹ç®æ¥å¸¸ä½¿ç¨è´¦å·',208,0), |
| | | (3,'customer','客æ·è´¦å·','01','','','0','$2a$10$E0Rg.GqdeQd1ba2Z5P87HOsOELqoKwYwKFQGRGop.LEPaom7yzji.','0','0', |
| | | 'admin',NOW(),'客æ·é¨æ·è´¦å·',208,0); |
| | | |
| | | DELETE FROM sys_role; |
| | | INSERT INTO sys_role |
| | | (role_id,role_name,role_key,role_sort,data_scope,menu_check_strictly,dept_check_strictly,status,del_flag,create_by,create_time,remark) |
| | | VALUES |
| | | (1,'ç³»ç»ç®¡çå','admin',1,'1',1,1,'0','0','admin',NOW(),'æ¥æå
¨é¨ç³»ç»æé'), |
| | | (2,'æ®éç¨æ·','common',2,'2',1,1,'0','0','admin',NOW(),'ä¸å¡ãéè´åé¡¹ç®æ¥å¸¸æä½'), |
| | | (3,'客æ·è§è²','customer',3,'5',1,1,'0','0','admin',NOW(),'ä»
访é®å®¢æ·é¨æ·'); |
| | | |
| | | DELETE FROM sys_dept; |
| | | INSERT INTO sys_dept |
| | | (dept_id,parent_id,ancestors,dept_name,order_num,leader,status,del_flag,create_by,create_time,dept_nick) |
| | | VALUES |
| | | (100,0,'0','å·¥ä½å®¤',1,'管çåè´¦å·','0','0','admin',NOW(),'å·¥ä½å®¤'), |
| | | (101,100,'0,100','综å管çé¨',1,'管çåè´¦å·','0','0','admin',NOW(),'综å'), |
| | | (102,100,'0,100','项ç®é¨',2,'æ®éç¨æ·','0','0','admin',NOW(),'项ç®'); |
| | | |
| | | INSERT INTO sys_user_role(user_id,role_id) VALUES (1,1),(2,2),(3,3); |
| | | INSERT INTO sys_user_dept(user_id,dept_id,create_user,create_time) |
| | | VALUES (1,100,1,NOW()),(2,102,1,NOW()),(3,100,1,NOW()); |
| | | INSERT INTO sys_role_dept(role_id,dept_id) VALUES (1,100),(1,101),(1,102),(2,102),(3,100); |
| | | |
| | | -- å²ä½å¨å½åé¡¹ç®æµç¨ä¸æ²¡æç¬ç«ä¸å¡ä½ç¨ï¼æ¸
空å²ä½å¹¶ä»èåä¸ç§»é¤ã |
| | | DELETE FROM sys_post; |
| | | |
| | | -- 2. èåï¼åªä¿çå½å项ç®å®é
使ç¨çä¸å¡èååç²¾ç®åçç³»ç»ç®¡çã |
| | | INSERT INTO sys_menu |
| | | (menu_id,menu_name,parent_id,order_num,path,component,query,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | VALUES |
| | | (2390,'éè´å®¡æ¹',2053,4,'purchaseApproval','collaborativeApproval/approvalProcess/index5',NULL,'PurchaseApproval',1,0,'C','0','0','approval:purchase:list','clipboard','admin',NOW(),'æå®¡æ¹æµç¨é
ç½®é级审æ¹éè´å') |
| | | ON DUPLICATE KEY UPDATE menu_name=VALUES(menu_name),parent_id=VALUES(parent_id),order_num=VALUES(order_num), |
| | | path=VALUES(path),component=VALUES(component),route_name=VALUES(route_name),visible='0',status='0',perms=VALUES(perms),icon=VALUES(icon); |
| | | |
| | | DELETE FROM sys_menu |
| | | WHERE menu_id NOT IN ( |
| | | 1,100,101,102,103,105,108,500,501, |
| | | 1000,1001,1002,1003,1004,1005,1006, |
| | | 1007,1008,1009,1010,1011, |
| | | 1012,1013,1014,1015, |
| | | 1016,1017,1018,1019, |
| | | 1025,1026,1027,1028,1029, |
| | | 1039,1040,1041,1042,1043,1044,1045, |
| | | 2053,2370,2371,2293,2390, |
| | | 2010,2001,2362,2363,2364,2365,2366,2367,2368, |
| | | 2372,2011,2360,2361, |
| | | 2015,2016, |
| | | 2276,2285,2373,2376,2377,2378,2379,2380,2381,2382,2383,2384,2385,2388, |
| | | 2374,2375,2386,2387, |
| | | 2357,2358,2359 |
| | | ); |
| | | |
| | | UPDATE sys_menu SET visible='0',status='0'; |
| | | UPDATE sys_menu SET order_num=1 WHERE menu_id=2053; |
| | | UPDATE sys_menu SET order_num=2 WHERE menu_id=2010; |
| | | UPDATE sys_menu SET order_num=3 WHERE menu_id=2372; |
| | | UPDATE sys_menu SET order_num=4 WHERE menu_id=2015; |
| | | UPDATE sys_menu SET order_num=5 WHERE menu_id=2276; |
| | | UPDATE sys_menu SET order_num=6 WHERE menu_id=2374; |
| | | UPDATE sys_menu SET order_num=7 WHERE menu_id=1; |
| | | UPDATE sys_menu SET order_num=8 WHERE menu_id=2357; |
| | | UPDATE sys_menu SET menu_name='å®¡æ¹æµç¨é
ç½®' WHERE menu_id=2293; |
| | | UPDATE sys_menu SET menu_name='éè´ç®¡ç' WHERE menu_id=2015; |
| | | UPDATE sys_menu SET menu_name='éè´å°è´¦' WHERE menu_id=2016; |
| | | UPDATE sys_menu SET menu_name='项ç®å表' WHERE menu_id=2285; |
| | | |
| | | -- 管ç忥æå
¨é¨ä¿çèåã |
| | | INSERT INTO sys_role_menu(role_id,menu_id) SELECT 1,menu_id FROM sys_menu; |
| | | |
| | | -- æ®éç¨æ·ï¼è¡æ¿ç³è¯·/审æ¹ã客æ·ãä¸å¡ãéè´ã项ç®ãæ¿çæä»¶ã |
| | | INSERT INTO sys_role_menu(role_id,menu_id) |
| | | SELECT 2,menu_id FROM sys_menu WHERE menu_id IN ( |
| | | 2053,2370,2371,2390, |
| | | 2010,2001,2367,2368, |
| | | 2372,2011,2360,2361, |
| | | 2015,2016, |
| | | 2276,2285,2373,2376,2377,2378,2379,2380,2381,2382,2383,2384,2385,2388, |
| | | 2374,2375,2386,2387 |
| | | ); |
| | | |
| | | -- 客æ·è§è²ï¼åªå¼æ¾å®¢æ·èµæå
¥å£ã |
| | | INSERT INTO sys_role_menu(role_id,menu_id) |
| | | SELECT 3,menu_id FROM sys_menu WHERE menu_id IN (2357,2358,2359); |
| | | |
| | | -- 3. åå
¸ï¼å 餿 å
³å¶é ä¸åå²åå
¸ï¼ä¿çç³»ç»åå
¸å¹¶å¢å å½å项ç®éæ©é¡¹ã |
| | | DELETE FROM sys_dict_data; |
| | | DELETE FROM sys_dict_type; |
| | | |
| | | INSERT INTO sys_dict_type(dict_id,dict_name,dict_type,status,create_by,create_time,remark) VALUES |
| | | (1,'ç¨æ·æ§å«','sys_user_sex','0','admin',NOW(),'ç³»ç»åå
¸'), |
| | | (2,'èåæ¾ç¤ºç¶æ','sys_show_hide','0','admin',NOW(),'ç³»ç»åå
¸'), |
| | | (3,'ç³»ç»å¯ç¨ç¶æ','sys_normal_disable','0','admin',NOW(),'ç³»ç»åå
¸'), |
| | | (4,'æ¯å¦é项','sys_yes_no','0','admin',NOW(),'ç³»ç»åå
¸'), |
| | | (5,'æä½ç±»å','sys_oper_type','0','admin',NOW(),'æ¥å¿åå
¸'), |
| | | (6,'éç¨ç¶æ','sys_common_status','0','admin',NOW(),'ç³»ç»åå
¸'), |
| | | (7,'ç¨ç','tax_rate','0','admin',NOW(),'ä¸å¡éè´éç¨'), |
| | | (8,'é¡¹ç®æ¥æº','project_source','0','admin',NOW(),'项ç®ç®¡ç'), |
| | | (9,'项ç®é¶æ®µ','project_phase','0','admin',NOW(),'项ç®ç®¡ç'), |
| | | (10,'项ç®è·è¿å¨æ','project_follow_cycle','0','admin',NOW(),'项ç®ç®¡ç'), |
| | | (11,'项ç®èµæç±»å','project_material_type','0','admin',NOW(),'项ç®ç®¡ç'), |
| | | (12,'éç¨å®¡æ¹ç¶æ','approval_status','0','admin',NOW(),'éè´ååå审æ¹'), |
| | | (13,'ä¸å¡å®¡æ¹ç¶æ','business_approval_status','0','admin',NOW(),'ä¸å¡ç®¡ç'), |
| | | (14,'è´¢å¡æ¥è¡¨å®¡æ¹ç¶æ','financial_report_status','0','admin',NOW(),'客æ·ç®¡ç'), |
| | | (15,'客æ·ç级','customer_level','0','admin',NOW(),'客æ·ç®¡ç'), |
| | | (16,'æ¿ç级å«','policy_level','0','admin',NOW(),'æ¿åºæ¿çæä»¶'); |
| | | |
| | | INSERT INTO sys_dict_data |
| | | (dict_sort,dict_label,dict_value,dict_type,css_class,list_class,is_default,status,create_by,create_time,remark) |
| | | VALUES |
| | | (1,'ç·','0','sys_user_sex','','','N','0','admin',NOW(),''),(2,'女','1','sys_user_sex','','','N','0','admin',NOW(),''),(3,'æªç¥','2','sys_user_sex','','info','Y','0','admin',NOW(),''), |
| | | (1,'æ¾ç¤º','0','sys_show_hide','','primary','Y','0','admin',NOW(),''),(2,'éè','1','sys_show_hide','','info','N','0','admin',NOW(),''), |
| | | (1,'æ£å¸¸','0','sys_normal_disable','','success','Y','0','admin',NOW(),''),(2,'åç¨','1','sys_normal_disable','','danger','N','0','admin',NOW(),''), |
| | | (1,'æ¯','Y','sys_yes_no','','success','N','0','admin',NOW(),''),(2,'å¦','N','sys_yes_no','','info','Y','0','admin',NOW(),''), |
| | | (0,'å
¶ä»','0','sys_oper_type','','info','N','0','admin',NOW(),''),(1,'æ°å¢','1','sys_oper_type','','success','N','0','admin',NOW(),''),(2,'ä¿®æ¹','2','sys_oper_type','','primary','N','0','admin',NOW(),''),(3,'å é¤','3','sys_oper_type','','danger','N','0','admin',NOW(),''),(5,'导åº','5','sys_oper_type','','warning','N','0','admin',NOW(),''),(6,'导å
¥','6','sys_oper_type','','warning','N','0','admin',NOW(),''), |
| | | (1,'æ£å¸¸','0','sys_common_status','','success','Y','0','admin',NOW(),''),(2,'åç¨','1','sys_common_status','','danger','N','0','admin',NOW(),''), |
| | | (1,'0%','0','tax_rate','','info','N','0','admin',NOW(),''),(2,'1%','1','tax_rate','','','N','0','admin',NOW(),''),(3,'3%','3','tax_rate','','','N','0','admin',NOW(),''),(4,'6%','6','tax_rate','','','N','0','admin',NOW(),''),(5,'9%','9','tax_rate','','','N','0','admin',NOW(),''),(6,'13%','13','tax_rate','','primary','Y','0','admin',NOW(),''), |
| | | (1,'æå¨å建','MANUAL','project_source','','primary','Y','0','admin',NOW(),''),(2,'ä¸å¡çæ','BUSINESS','project_source','','success','N','0','admin',NOW(),''), |
| | | (1,'æªå¼å§','æªå¼å§','project_phase','','info','Y','0','admin',NOW(),''),(2,'ç«é¡¹','ç«é¡¹','project_phase','','warning','N','0','admin',NOW(),''),(3,'å¼å','å¼å','project_phase','','primary','N','0','admin',NOW(),''),(4,'ç³æ¥','ç³æ¥','project_phase','','warning','N','0','admin',NOW(),''),(5,'ç³æ¥ç»æ','ç³æ¥ç»æ','project_phase','','success','N','0','admin',NOW(),''),(6,'éªæ¶','éªæ¶','project_phase','','success','N','0','admin',NOW(),''),(7,'已宿','已宿','project_phase','','success','N','0','admin',NOW(),''), |
| | | (1,'æ¯æ','MONTHLY','project_follow_cycle','','','Y','0','admin',NOW(),''),(2,'æ¯å£åº¦','QUARTERLY','project_follow_cycle','','','N','0','admin',NOW(),''),(3,'æ¯å¹´','YEARLY','project_follow_cycle','','','N','0','admin',NOW(),''), |
| | | (1,'ç«é¡¹èµæ','INITIATION','project_material_type','','','N','0','admin',NOW(),''),(2,'å¼åèµæ','DEVELOPMENT','project_material_type','','','N','0','admin',NOW(),''),(3,'ç³æ¥èµæ','APPLICATION','project_material_type','','','N','0','admin',NOW(),''),(4,'éªæ¶èµæ','ACCEPTANCE','project_material_type','','','N','0','admin',NOW(),''),(5,'å
¶ä»','OTHER','project_material_type','','info','Y','0','admin',NOW(),''), |
| | | (0,'è稿','0','approval_status','','info','Y','0','admin',NOW(),''),(1,'å¾
审æ¹','1','approval_status','','warning','N','0','admin',NOW(),''),(2,'审æ¹ä¸','2','approval_status','','primary','N','0','admin',NOW(),''),(3,'å·²éè¿','3','approval_status','','success','N','0','admin',NOW(),''),(4,'已驳å','4','approval_status','','danger','N','0','admin',NOW(),''), |
| | | (0,'è稿','0','business_approval_status','','info','Y','0','admin',NOW(),''),(1,'å¾
审æ¹','1','business_approval_status','','warning','N','0','admin',NOW(),''),(2,'å·²éè¿','2','business_approval_status','','success','N','0','admin',NOW(),''),(3,'å·²éå','3','business_approval_status','','danger','N','0','admin',NOW(),''), |
| | | (0,'å¾
审æ¹','0','financial_report_status','','warning','Y','0','admin',NOW(),''),(1,'å·²éè¿','1','financial_report_status','','success','N','0','admin',NOW(),''),(2,'å·²éå','2','financial_report_status','','danger','N','0','admin',NOW(),''), |
| | | (1,'éç¹å®¢æ·','A','customer_level','','danger','N','0','admin',NOW(),''),(2,'æ®é客æ·','B','customer_level','','primary','Y','0','admin',NOW(),''),(3,'æ½å¨å®¢æ·','C','customer_level','','info','N','0','admin',NOW(),''), |
| | | (1,'å½å®¶çº§','NATIONAL','policy_level','','danger','N','0','admin',NOW(),''),(2,'ç级','PROVINCE','policy_level','','warning','N','0','admin',NOW(),''),(3,'å¸çº§','CITY','policy_level','','primary','Y','0','admin',NOW(),''),(4,'åºå¿çº§','DISTRICT','policy_level','','info','N','0','admin',NOW(),''); |
| | | |
| | | -- éè´å®¡æ¹é»è®¤é
置管çå为第ä¸çº§å®¡æ¹äººï¼åç»å¯å¨âå®¡æ¹æµç¨é
ç½®âä¸è°æ´ã |
| | | DELETE FROM approve_process_config_node WHERE approve_type=5; |
| | | INSERT INTO approve_process_config_node |
| | | (approve_type,node_order,approver_id,approver_name,timeout_hours,tenant_id,create_user,create_time,delete_flag,dept_id) |
| | | VALUES (5,1,1,'管çåè´¦å·',24,208,1,NOW(),0,100); |
| | | |
| | | -- 4. æ¥å¿ä»å½åé¡¹ç®æ£å¼çæ¬éæ°å¼å§è®°å½ã |
| | | DELETE FROM sys_oper_log; |
| | | DELETE FROM sys_logininfor; |
| | | |
| | | COMMIT; |
| | | SET FOREIGN_KEY_CHECKS = 1; |
| | |
| | | url: `/projectManagement/workflow/${projectId}`, |
| | | method: 'get' |
| | | }) |
| | | export const getProjectLifecycle = projectId => request({ url: `/projectManagement/workflow/lifecycle/${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 saveProjectModulesBatch = data => request({ url: '/projectManagement/workflow/modules/batch', 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' }) |
| | |
| | | :on-change="handleChange" |
| | | :on-remove="handleRemove" |
| | | :on-exceed="handleExceed" |
| | | :on-preview="openFile" |
| | | > |
| | | <el-icon class="upload-drag-icon"><UploadFilled /></el-icon> |
| | | <div class="el-upload__text"> |
| | |
| | | :limit="limit" |
| | | :on-change="handleChange" |
| | | :on-remove="handleRemove" |
| | | :on-preview="handlePreview" |
| | | :on-exceed="handleExceed" |
| | | > |
| | | <div class="upload-trigger"> |
| | |
| | | type="primary" |
| | | size="small" |
| | | class="download-link" |
| | | @click="previewFile(scope.row.previewURL)"> |
| | | é¢è§ |
| | | </el-button> |
| | | <el-button link |
| | | type="primary" |
| | | size="small" |
| | | class="download-link" |
| | | @click="downloadFile(scope.row.downloadURL)"> |
| | | ä¸è½½ |
| | | </el-button> |
| | |
| | | </el-table> |
| | | </div> |
| | | </el-dialog> |
| | | <filePreview ref="filePreviewRef" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ElMessage } from 'element-plus' |
| | | import { ref, computed, getCurrentInstance, onMounted, watch } from "vue"; |
| | | import AttachmentUpload from "@/components/AttachmentUpload/file/index.vue"; |
| | | import { |
| | |
| | | deleteAttachment, |
| | | createAttachment, |
| | | } from "@/api/basicData/storageAttachment.js"; |
| | | import filePreview from '@/components/filePreview/index.vue' |
| | | const filePreviewRef = ref() |
| | | |
| | | const props = defineProps({ |
| | | visible: { |
| | |
| | | const handleClose = () => { |
| | | isShow.value = false; |
| | | }; |
| | | |
| | | // é¢è§æä»¶ |
| | | const previewFile = (url) => { |
| | | if (url) { |
| | | filePreviewRef.value.open(url) |
| | | } else { |
| | | ElMessage.warning('æä»¶å°åæ æï¼æ æ³é¢è§') |
| | | } |
| | | } |
| | | |
| | | const handleUpload = () => { |
| | | uploadDialogVisible.value = true; |
| | |
| | | :deep(.el-table__empty-text) { |
| | | color: #999; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | @click="handleDownload(scope.row)"> |
| | | ä¸è½½ |
| | | </el-button> |
| | | <el-button v-if="showPreview" |
| | | link |
| | | type="primary" |
| | | size="small" |
| | | @click="handlePreview(scope.row)"> |
| | | é¢è§ |
| | | </el-button> |
| | | <el-button v-if="showDeleteButton" |
| | | link |
| | | type="danger" |
| | |
| | | @pagination="paginationSearch" |
| | | @change="handleChange" /> |
| | | </el-dialog> |
| | | <!-- // todo éä»¶é¢è§ç¸å
³ --> |
| | | <filePreview v-if="showPreview" |
| | | ref="filePreviewRef" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, getCurrentInstance } from "vue"; |
| | | import pagination from "@/components/Pagination/index.vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import filePreview from "@/components/filePreview/index.vue"; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | const props = defineProps({ |
| | |
| | | }, |
| | | showPreview: { |
| | | type: Boolean, |
| | | default: true, |
| | | default: false, |
| | | }, |
| | | showUploadButton: { |
| | | type: Boolean, |
| | |
| | | ]); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const filePreviewRef = ref(null); |
| | | const uploadFileList = ref([]); |
| | | |
| | | const dialogVisible = computed({ |
| | |
| | | emit("download", row); |
| | | }; |
| | | |
| | | const handlePreview = row => { |
| | | if (props.previewMethod) { |
| | | props.previewMethod(row); |
| | | } else { |
| | | // é»è®¤é¢è§æ¹æ³ |
| | | if (filePreviewRef.value) { |
| | | filePreviewRef.value.open(row[props.urlField]); |
| | | } |
| | | } |
| | | emit("preview", row); |
| | | }; |
| | | const paginationSearch = page => { |
| | | props.page.current = page.page; |
| | | props.page.size = page.limit; |
| | |
| | | <!-- æä»¶å表 -->
|
| | | <transition-group ref="uploadListRef" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul"> |
| | | <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
|
| | | <el-link :href="getFileUrl(file.url)" :underline="false" target="_blank"> |
| | | <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
|
| | | </el-link>
|
| | | <div class="ele-upload-list__item-content-action">
|
| | | <el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled"> å é¤</el-link>
|
| | | <span class="el-icon-document"> {{ getFileName(file.name) }} </span> |
| | | <div class="ele-upload-list__item-content-action"> |
| | | <el-link :underline="false" @click="downloadFile(file)" type="primary"> ä¸è½½</el-link> |
| | | <el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled"> å é¤</el-link> |
| | | </div>
|
| | | </li>
|
| | | </transition-group>
|
| | |
| | | const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action); // ä¸ä¼ æä»¶æå¡å¨å°å
|
| | | const headers = ref({ Authorization: "Bearer " + getToken() });
|
| | | const fileList = ref([]);
|
| | | const showTip = computed(
|
| | | const showTip = computed( |
| | | () => props.isShowTip && (props.fileType || props.fileSize)
|
| | | );
|
| | | ); |
| | | |
| | | const downloadFile = file => { |
| | | const url = file?.url || ""; |
| | | if (!url) { |
| | | proxy.$modal.msgWarning("æä»¶å°åæ æ"); |
| | | return; |
| | | } |
| | | proxy.$download.resource(url); |
| | | }; |
| | |
|
| | | watch(
|
| | | () => props.modelValue,
|
| | |
| | | <!-- æé® --> |
| | | <div v-else-if="item.dataType == 'action'" |
| | | @click.stop> |
| | | <template v-for="(o, key) in item.operation" |
| | | <template v-for="(o, key) in item.operation.filter(operation => operation.name !== 'é¢è§')" |
| | | :key="key"> |
| | | <el-button v-show="o.type != 'upload'" |
| | | v-if="o.showHide ? o.showHide(scope.row) : true" |
| | |
| | | </template> |
| | | </el-dialog> |
| | | <!-- è¿3å¹´è´¢å¡æ¥è¡¨ä¸ä¼ --> |
| | | <el-dialog title="è¿3å¹´è´¢å¡æ¥è¡¨ä¸ä¼ " |
| | | <el-dialog title="è¿3å¹´è´¢å¡æ¥è¡¨" |
| | | v-model="reportDialogVisible" |
| | | width="900px" |
| | | width="980px" |
| | | @close="closeReportDialog"> |
| | | <div class="report-customer">客æ·ï¼{{ reportCustomer.customerName }}</div> |
| | | <el-form :model="reportForm" |
| | | label-width="90px" |
| | | inline |
| | | class="report-form"> |
| | | <el-form-item label="æ¥è¡¨å¹´åº¦"> |
| | | <el-select v-model="reportForm.reportYear" |
| | | placeholder="è¯·éæ©å¹´åº¦" |
| | | style="width: 150px"> |
| | | <el-option v-for="year in reportYears" |
| | | :key="year" |
| | | :label="year + 'å¹´'" |
| | | :value="year" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥è¡¨åç§°"> |
| | | <el-input v-model="reportForm.reportName" |
| | | placeholder="ä¾å¦ï¼2025å¹´åº¦è´¢å¡æ¥è¡¨" |
| | | style="width: 260px" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <FileUpload v-model="reportForm.reportFiles" |
| | | :limit="5" |
| | | :file-size="20" |
| | | :file-type="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png', 'zip', 'rar']" /> |
| | | <div class="report-actions"> |
| | | <el-button type="primary" |
| | | :loading="reportSaving" |
| | | @click="saveReport">ä¿åä¸ä¼ </el-button> |
| | | <el-button @click="resetReportForm">éç½®</el-button> |
| | | <div class="report-dialog-head"> |
| | | <div> |
| | | <div class="report-dialog-head__label">å½å客æ·</div> |
| | | <div class="report-dialog-head__name">{{ reportCustomer.customerName }}</div> |
| | | </div> |
| | | <el-tag type="primary" effect="light"> |
| | | å·²ä¸ä¼ {{ reportRows.length }} / 3 个年度 |
| | | </el-tag> |
| | | </div> |
| | | <el-table :data="reportRows" |
| | | border |
| | | size="small" |
| | | style="margin-top: 16px"> |
| | | <el-table-column prop="reportYear" |
| | | label="年度" |
| | | width="90" |
| | | align="center" /> |
| | | <el-table-column prop="reportName" |
| | | label="æ¥è¡¨åç§°" |
| | | min-width="180" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="æ¥è¡¨éä»¶" |
| | | min-width="260"> |
| | | <template #default="scope"> |
| | | <FileUpload v-model="scope.row.reportFiles" |
| | | disabled |
| | | :is-show-tip="false" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="uploadUserName" |
| | | label="ä¸ä¼ 人" |
| | | width="110" /> |
| | | <el-table-column prop="updateTime" |
| | | label="ä¸ä¼ æ¶é´" |
| | | width="170" /> |
| | | <el-table-column label="审æ¹ç¶æ" |
| | | width="100" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="reportStatusTag(scope.row.auditStatus)"> |
| | | {{ reportStatusText(scope.row.auditStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" |
| | | width="140" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-button type="primary" |
| | | link |
| | | @click="editReport(scope.row)">ä¿®æ¹</el-button> |
| | | <el-button type="danger" |
| | | link |
| | | @click="deleteReport(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div v-if="reportRows.length === 0" |
| | | class="no-record">ææ è´¢å¡æ¥è¡¨ï¼è¯·æå¹´åº¦ä¸ä¼ è¿3å¹´è´¢å¡æ¥è¡¨</div> |
| | | |
| | | <div class="report-layout"> |
| | | <div class="report-upload-card"> |
| | | <div class="report-card-title">ä¸ä¼ ææ¿æ¢æ¥è¡¨</div> |
| | | <div class="report-card-tip">æ¯ä¸ªå¹´åº¦ä¿åä¸ç»æ¥è¡¨æä»¶ï¼å次ä¿åå°æ´æ°è¯¥å¹´åº¦è®°å½ã</div> |
| | | <el-form :model="reportForm" label-position="top" class="report-form"> |
| | | <el-form-item label="æ¥è¡¨å¹´åº¦"> |
| | | <el-select |
| | | v-model="reportForm.reportYear" |
| | | placeholder="è¯·éæ©å¹´åº¦" |
| | | style="width: 100%" |
| | | @change="selectReportYear" |
| | | > |
| | | <el-option |
| | | v-for="year in reportYears" |
| | | :key="year" |
| | | :label="year + 'å¹´'" |
| | | :value="year" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥è¡¨åç§°"> |
| | | <el-input |
| | | v-model="reportForm.reportName" |
| | | placeholder="ä¾å¦ï¼2026å¹´åº¦è´¢å¡æ¥è¡¨" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥è¡¨éä»¶"> |
| | | <FileUpload |
| | | v-model="reportForm.reportFiles" |
| | | :limit="5" |
| | | :file-size="20" |
| | | :file-type="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png', 'zip', 'rar']" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="report-actions"> |
| | | <el-button type="primary" :loading="reportSaving" @click="saveReport"> |
| | | {{ reportYearRecord(reportForm.reportYear) ? "ä¿åä¿®æ¹" : "ä¿åä¸ä¼ " }} |
| | | </el-button> |
| | | <el-button @click="resetReportForm">æ¸
空</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="report-year-list"> |
| | | <div class="report-card-title">è¿3å¹´ä¸ä¼ æ
åµ</div> |
| | | <div |
| | | v-for="year in reportYears" |
| | | :key="year" |
| | | class="report-year-card" |
| | | :class="{ 'is-empty': !reportYearRecord(year) }" |
| | | > |
| | | <div class="report-year-card__year">{{ year }}å¹´</div> |
| | | <template v-if="reportYearRecord(year)"> |
| | | <div class="report-year-card__body"> |
| | | <div class="report-year-card__name"> |
| | | {{ reportYearRecord(year).reportName || year + "å¹´åº¦è´¢å¡æ¥è¡¨" }} |
| | | </div> |
| | | <div class="report-year-card__meta"> |
| | | ä¸ä¼ 人ï¼{{ reportYearRecord(year).uploadUserName || "-" }} · |
| | | {{ reportYearRecord(year).updateTime || "-" }} |
| | | </div> |
| | | <FileUpload |
| | | v-model="reportYearRecord(year).reportFiles" |
| | | disabled |
| | | :is-show-tip="false" |
| | | /> |
| | | </div> |
| | | <div class="report-year-card__actions"> |
| | | <el-tag :type="reportStatusTag(reportYearRecord(year).auditStatus)"> |
| | | {{ reportStatusText(reportYearRecord(year).auditStatus) }} |
| | | </el-tag> |
| | | <el-button type="primary" link @click="editReport(reportYearRecord(year))"> |
| | | ä¿®æ¹ |
| | | </el-button> |
| | | <el-button type="danger" link @click="deleteReport(reportYearRecord(year))"> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | <template v-else> |
| | | <div class="report-year-card__empty">å°æªä¸ä¼ </div> |
| | | <el-button type="primary" link @click="selectReportYear(year)">ç«å³ä¸ä¼ </el-button> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeReportDialog">å
³é</el-button> |
| | |
| | | const loadReports = customerId => { |
| | | listCustomerFinancialReport({ customerId }).then(res => { |
| | | reportRows.value = res.data || []; |
| | | selectReportYear(reportForm.reportYear); |
| | | }); |
| | | }; |
| | | |
| | | const reportYearRecord = year => |
| | | reportRows.value.find(item => Number(item.reportYear) === Number(year)); |
| | | |
| | | const reportStatusText = status => { |
| | | if (status === 1) return "å·²éè¿"; |
| | |
| | | |
| | | const resetReportForm = () => { |
| | | reportForm.reportYear = nowYear; |
| | | reportForm.reportName = ""; |
| | | reportForm.reportName = `${nowYear}å¹´åº¦è´¢å¡æ¥è¡¨`; |
| | | reportForm.reportFiles = ""; |
| | | }; |
| | | |
| | | const selectReportYear = year => { |
| | | reportForm.reportYear = Number(year); |
| | | const current = reportYearRecord(year); |
| | | reportForm.reportName = current?.reportName || `${year}å¹´åº¦è´¢å¡æ¥è¡¨`; |
| | | reportForm.reportFiles = current?.reportFiles || ""; |
| | | }; |
| | | |
| | | const editReport = row => { |
| | | reportForm.reportYear = row.reportYear; |
| | | reportForm.reportName = row.reportName; |
| | | reportForm.reportFiles = row.reportFiles; |
| | | selectReportYear(row.reportYear); |
| | | }; |
| | | |
| | | const saveReport = () => { |
| | | if (!reportForm.reportName?.trim()) { |
| | | proxy.$modal.msgWarning("请è¾å
¥æ¥è¡¨åç§°"); |
| | | return; |
| | | } |
| | | if (!reportForm.reportFiles) { |
| | | proxy.$modal.msgWarning("请ä¸ä¼ è´¢å¡æ¥è¡¨éä»¶"); |
| | | return; |
| | |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .report-customer { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 12px; |
| | | .report-dialog-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 14px 18px; |
| | | margin-bottom: 16px; |
| | | background: linear-gradient(135deg, #eef5ff, #f8fbff); |
| | | border: 1px solid #dbe9fb; |
| | | border-radius: 10px; |
| | | |
| | | &__label { |
| | | margin-bottom: 3px; |
| | | font-size: 12px; |
| | | color: #8491a5; |
| | | } |
| | | |
| | | &__name { |
| | | font-size: 17px; |
| | | font-weight: 700; |
| | | color: #23334d; |
| | | } |
| | | } |
| | | |
| | | .report-layout { |
| | | display: grid; |
| | | grid-template-columns: 360px minmax(0, 1fr); |
| | | gap: 18px; |
| | | align-items: start; |
| | | } |
| | | |
| | | .report-upload-card, |
| | | .report-year-list { |
| | | padding: 18px; |
| | | background: #fff; |
| | | border: 1px solid #e5eaf1; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | .report-card-title { |
| | | margin-bottom: 6px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | color: #26364d; |
| | | } |
| | | |
| | | .report-card-tip { |
| | | margin-bottom: 14px; |
| | | font-size: 12px; |
| | | line-height: 1.6; |
| | | color: #8a95a6; |
| | | } |
| | | |
| | | .report-form { |
| | | margin-bottom: 4px; |
| | | :deep(.el-form-item) { |
| | | margin-bottom: 14px; |
| | | } |
| | | } |
| | | |
| | | .report-actions { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .report-year-card { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | gap: 14px; |
| | | padding: 14px; |
| | | margin-top: 12px; |
| | | background: #f9fbfe; |
| | | border: 1px solid #dfe7f2; |
| | | border-radius: 9px; |
| | | |
| | | &.is-empty { |
| | | align-items: center; |
| | | color: #98a2b3; |
| | | background: #fafafa; |
| | | border-style: dashed; |
| | | } |
| | | |
| | | &__year { |
| | | flex: 0 0 58px; |
| | | padding: 6px 8px; |
| | | font-weight: 700; |
| | | color: #1254b5; |
| | | text-align: center; |
| | | background: #eaf2ff; |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | &__body { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | &__name { |
| | | margin-bottom: 5px; |
| | | font-weight: 600; |
| | | color: #303b4c; |
| | | } |
| | | |
| | | &__meta { |
| | | margin-bottom: 5px; |
| | | font-size: 12px; |
| | | color: #8a95a6; |
| | | } |
| | | |
| | | &__actions { |
| | | display: flex; |
| | | flex: 0 0 auto; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | &__empty { |
| | | flex: 1; |
| | | font-size: 13px; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 900px) { |
| | | .report-layout { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | { value: "2", label: "请å管ç", icon: "Calendar", color: "#67C23A" }, |
| | | { value: "3", label: "åºå·®ç®¡ç", icon: "Location", color: "#E6A23C" }, |
| | | // { value: '4', label: 'æ¥é管ç', icon: 'Money', color: '#F56C6C' }, |
| | | // { value: '5', label: 'éè´å®¡æ¹', icon: 'ShoppingCart', color: '#909399' }, |
| | | { value: "5", label: "éè´å®¡æ¹", icon: "ShoppingCart", color: "#4F7EF7" }, |
| | | // { value: '6', label: 'æ¥ä»·å®¡æ¹', icon: 'DocumentChecked', color: '#9B59B6' }, |
| | | // { value: '7', label: 'å货审æ¹', icon: 'Van', color: '#1ABC9C' }, |
| | | ]; |
| | |
| | | 2: "请å管ç", |
| | | 3: "åºå·®ç®¡ç", |
| | | // 4: 'æ¥é管ç', |
| | | // 5: 'éè´å®¡æ¹', |
| | | 5: "éè´å®¡æ¹", |
| | | // 6: 'æ¥ä»·å®¡æ¹', |
| | | // 7: 'å货审æ¹', |
| | | }; |
| | |
| | | <el-table-column fixed="right" label="æä½" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="downLoadFile(scope.row)">ä¸è½½</el-button> |
| | | <el-button link type="primary" @click="lookFile(scope.row)">é¢è§</el-button> |
| | | <el-button link type="danger" @click="handleDelete(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | }) |
| | | </script> |
| | | |
| | | <style></style> |
| | | <style></style> |
| | |
| | | { value: "2", label: "请å管ç", icon: "Calendar", color: "#67C23A" }, |
| | | { value: "3", label: "åºå·®ç®¡ç", icon: "Location", color: "#E6A23C" }, |
| | | // { value: '4', label: 'æ¥é管ç', icon: 'Money', color: '#F56C6C' }, |
| | | // { value: '5', label: 'éè´å®¡æ¹', icon: 'ShoppingCart', color: '#909399' }, |
| | | { value: "5", label: "éè´å®¡æ¹", icon: "ShoppingCart", color: "#4F7EF7" }, |
| | | // { value: '6', label: 'æ¥ä»·å®¡æ¹', icon: 'DocumentChecked', color: '#9B59B6' }, |
| | | // { value: '7', label: 'å货审æ¹', icon: 'Van', color: '#1ABC9C' }, |
| | | ]; |
| | |
| | | </el-table-column> |
| | | <el-table-column fixed="right" label="æä½" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="previewFile(scope.row)">é¢è§</el-button> |
| | | <el-button link type="primary" size="small" @click="downloadFile(scope.row)">ä¸è½½</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | .attachment-table { |
| | | border-radius: 4px; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <el-table-column fixed="right" label="æä½" width="100" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">ä¸è½½</el-button> |
| | | <el-button link type="primary" size="small" @click="lookFile(scope.row)">é¢è§</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | }) |
| | | </script> |
| | | |
| | | <style></style> |
| | | <style></style> |
| | |
| | | </el-table-column> |
| | | <el-table-column fixed="right" label="æä½" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="previewFile(scope.row)">é¢è§</el-button> |
| | | <el-button link type="primary" size="small" @click="downloadFile(scope.row)">ä¸è½½</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | {{ file.name || file.originalFilename }} |
| | | </span> |
| | | <div class="file-actions"> |
| | | <el-link v-if="file.previewURL || file.url" |
| | | type="primary" |
| | | :underline="false" |
| | | @click="openFile(file.previewURL || file.url)">é¢è§</el-link> |
| | | <el-divider v-if="(file.previewURL || file.url) && file.downloadURL" |
| | | direction="vertical" /> |
| | | <el-link v-if="file.downloadURL" |
| | | type="primary" |
| | | :underline="false" |
| | |
| | | align-items: center; |
| | | flex-shrink: 0; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <el-table-column fixed="right" label="æä½" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">ä¸è½½</el-button> |
| | | <el-button link type="primary" size="small" @click="lookFile(scope.row)">é¢è§</el-button> |
| | | <el-button link type="danger" size="small" @click="handleDelete(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | }) |
| | | </script> |
| | | |
| | | <style></style> |
| | | <style></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-tab-pane v-if="!singleTab && 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> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="模å设置" name="modules"> |
| | | <el-tab-pane v-if="!singleTab || singleTab === 'modules'" :label="readonly ? '模åä¿¡æ¯' : '模å管ç'" name="modules"> |
| | | <div class="tab-actions"> |
| | | <span class="hint">模åå 项ç®å·¥ä½éå计ï¼{{ totalModuleWeight }}%</span> |
| | | <el-button v-hasPermi="['project:module:edit']" type="primary" @click="openModule()">æ°å¢æ¨¡å</el-button> |
| | | <el-button v-if="!readonly" v-hasPermi="['project:module:edit']" type="primary" @click="openModuleBatch">{{ isStarted ? 'ç®¡çæ§è¡äºº' : 'ç¼è¾æ¨¡å' }}</el-button> |
| | | </div> |
| | | <el-table :data="data.modules" border> |
| | | <el-table-column prop="moduleName" label="模ååç§°" min-width="160" /> |
| | |
| | | </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"> |
| | | <el-tab-pane v-if="isStarted && (!singleTab || singleTab === 'follow')" 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> |
| | | <el-button v-if="!readonly" 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-column label="éä»¶" min-width="190"> |
| | | <template #default="{ row }"> |
| | | <div v-if="row.attachmentList?.length" class="file-cell"><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 class="empty-file">æ éä»¶</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="!readonly" label="æä½" width="140" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:follow:add']" link type="primary" @click="openFollow(row)">ç¼è¾</el-button><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> |
| | | <el-tab-pane v-if="isStarted && (!singleTab || singleTab === 'reports')" label="é¡¹ç®æ±æ¥" name="reports"> |
| | | <div class="report-workbench"> |
| | | <aside class="report-module-nav"> |
| | | <div class="report-nav-title">æ±æ¥èå´</div> |
| | | <button type="button" class="report-nav-item" :class="{ active: reportModuleFilter === 'all' }" @click="selectReportModule('all')"> |
| | | <span class="report-nav-name">å
¨é¨ï¼æ´ä¸ªé¡¹ç®ï¼</span> |
| | | <span class="report-nav-meta">å
± {{ data.reports.length }} æ¡æ±æ¥</span> |
| | | </button> |
| | | <button v-for="module in data.modules" :key="module.id" type="button" class="report-nav-item" :class="{ active: reportModuleFilter === String(module.id) }" @click="selectReportModule(String(module.id))"> |
| | | <span class="report-nav-name">{{ module.moduleName }}</span> |
| | | <span class="report-nav-meta">è¿åº¦ {{ Number(module.currentProgress || 0) }}% · {{ moduleReportCount(module.id) }} æ¡æ±æ¥</span> |
| | | </button> |
| | | </aside> |
| | | <section class="report-content"> |
| | | <div class="tab-actions"> |
| | | <span class="report-stat-summary">{{ reportStatisticsSummary }}</span> |
| | | <el-button v-if="!readonly" v-hasPermi="['project:report:add']" type="primary" :disabled="!eligibleReportModules.length" @click="openReport()">æ°å¢æ±æ¥</el-button> |
| | | </div> |
| | | <div class="report-layout" :class="{ 'report-only': !readonly }"> |
| | | <div v-if="readonly" ref="chartRef" class="chart" /> |
| | | <el-table :data="filteredReports" 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="incrementProgress" label="æ¬æ¬¡è¿åº¦" width="100"><template #default="{ row }">{{ row.incrementProgress ?? 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="æä»¶" min-width="190"> |
| | | <template #default="{ row }"> |
| | | <div v-if="row.attachmentList?.length" class="file-cell"><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 class="empty-file">æ éä»¶</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="!readonly" label="æä½" width="140" align="center"><template #default="{ row }"><el-button v-if="canEditReport(row)" v-hasPermi="['project:report:add']" link type="primary" @click="openReport(row)">ç¼è¾</el-button><el-button v-if="canEditReport(row)" v-hasPermi="['project:report:remove']" link type="danger" @click="removeRecord('report', row)">å é¤</el-button></template></el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="项ç®èµæ" name="materials"> |
| | | <el-tab-pane v-if="!singleTab || singleTab === 'materials'" label="项ç®èµæ" name="materials"> |
| | | <div class="tab-actions"> |
| | | <span class="hint">é¡¹ç®æ´ä½èµæåºï¼ä¸ä¸å¡éä»¶å忬¡æ±æ¥éä»¶åå¼ä¿å</span> |
| | | <el-button v-hasPermi="['project:material:add']" type="primary" @click="openMaterial()">ä¸ä¼ èµæ</el-button> |
| | | <el-button v-if="!readonly" 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-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-column v-if="!readonly" 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> |
| | | |
| | | <template v-if="!singleTab"> |
| | | <el-tab-pane v-for="stage in visibleProcessStages" :key="stage.code" :label="stage.label" :name="stage.code"> |
| | | <el-empty v-if="!stageRecords(stage.code).length" description="ææ é¶æ®µåçè®°å½" /> |
| | | <el-timeline v-else class="phase-record-timeline"> |
| | | <el-timeline-item v-for="record in stageRecords(stage.code)" :key="`${stage.code}-${record.id || record.actionTime}`" :timestamp="formatActionTime(record.actionTime)" placement="top" :type="phaseRecordType(record.actionType)"> |
| | | <el-card shadow="never" class="phase-record-card"> |
| | | <template #header> |
| | | <div class="phase-record-head"><div><strong>{{ record.actionLabel || stage.phaseName }}</strong><el-tag size="small" :type="phaseRecordType(record.actionType)">{{ recordStatusLabel(record) }}</el-tag></div><span>æä½äººï¼{{ record.operatorName || '-' }}</span></div> |
| | | </template> |
| | | <el-descriptions v-if="phaseFieldRows(record).length" :column="2" border> |
| | | <el-descriptions-item v-for="field in phaseFieldRows(record)" :key="field.label" :label="field.label">{{ field.value }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div v-if="record.remark" class="phase-record-remark"><b>åç说æï¼</b>{{ record.remark }}</div> |
| | | <div v-if="record.attachmentList?.length" class="phase-record-files"><b>ç¸å
³éä»¶ï¼</b><el-button v-for="(file,index) in record.attachmentList" :key="file.id || index" link type="primary" @click="download(file)">{{ file.name || file.fileName || `éä»¶${index + 1}` }}</el-button></div> |
| | | </el-card> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </el-tab-pane> |
| | | </template> |
| | | </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> |
| | | <el-dialog v-model="moduleVisible" :title="isStarted ? 'ç®¡çæ¨¡åæ§è¡äºº' : 'ç¼è¾é¡¹ç®æ¨¡å'" width="1100px" top="4vh" destroy-on-close> |
| | | <el-alert :title="isStarted ? '项ç®å¼å§å模åä¿¡æ¯ä¸å¯ä¿®æ¹ï¼ä»
坿°å¢æ§è¡äººï¼å·²ææ¬æ¨¡åå·¥ä½å æ¯çæ§è¡äººä¸è½å é¤ã' : 'æææ¨¡å䏿¬¡æ§ä¿åï¼å·¥ä½éå æ¯å计å¿
须为100%åé¡¹ç®æè½å¼å§ã'" type="info" :closable="false" class="dialog-tip" /> |
| | | <div class="module-total" :class="{ invalid: Number(moduleDraftTotal) !== 100 }">模åå·¥ä½éå计ï¼{{ moduleDraftTotal }}%ï¼å©ä½å¯åé
ï¼{{ moduleDraftRemaining }}%</div> |
| | | <div v-for="(module, moduleIndex) in moduleDrafts" :key="module.localKey" class="module-batch-card"> |
| | | <div class="module-batch-title"> |
| | | <strong>模å {{ moduleIndex + 1 }}</strong> |
| | | <span v-if="module.locked" class="locked-tip">项ç®å·²å¼å§ï¼æ¨¡åä¿¡æ¯ä¸å¯ä¿®æ¹</span> |
| | | <el-button v-if="!isStarted && !module.locked" link type="danger" @click="removeModuleDraft(moduleIndex)">ç§»é¤æ¨¡å</el-button> |
| | | </div> |
| | | <div class="module-grid"> |
| | | <el-form-item label="模ååç§°" required><el-input v-model="module.moduleName" :disabled="module.locked" /></el-form-item> |
| | | <el-form-item label="é¢è®¡å¼å§" required><el-date-picker v-model="module.plannedStartDate" :disabled="module.locked" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item> |
| | | <el-form-item label="å·¥ä½éå æ¯" required><el-input-number v-model="module.projectWeight" :disabled="module.locked" :min="0.01" :max="draftWeightMax(moduleIndex)" :precision="2" /><span class="unit">%</span><div class="percent-hint">å
¶ä»æ¨¡å {{ otherDraftWeight(moduleIndex) }}%ï¼æ¬æ¨¡åæå¤ {{ draftWeightMax(moduleIndex) }}%</div></el-form-item> |
| | | <el-form-item label="æ¨¡åæ¦è¦" required class="summary-item"><el-input v-model="module.summary" :disabled="module.locked" type="textarea" :rows="2" maxlength="1000" show-word-limit /></el-form-item> |
| | | <el-form-item label="æ¨¡åæ§è¡äºº" required class="members-item"> |
| | | <div class="member-editor"> |
| | | <div v-for="(member, memberIndex) in module.members" :key="memberIndex" class="member-row"> |
| | | <el-select v-model="member.userId" :disabled="member.hasReportedShare" filterable placeholder="è¯·éæ©æ§è¡äºº" @change="id => syncMemberName(member, id)"><el-option v-for="user in userOptions" :key="user.value" :label="user.label" :value="user.value" /></el-select> |
| | | <el-tag v-if="member.hasReportedShare" type="warning">å·²å®ææ¨¡åå æ¯ï¼{{ member.completedModuleShare }}%</el-tag> |
| | | <el-button v-else link type="danger" @click="module.members.splice(memberIndex, 1)">å é¤</el-button> |
| | | </div> |
| | | <el-button link type="primary" @click="module.members.push({ userId:null, userName:'', hasReportedShare:false, completedModuleShare:0 })">+ æ·»å æ§è¡äºº</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-form-item> |
| | | </div> |
| | | </div> |
| | | <el-button v-if="!isStarted" type="primary" plain @click="addModuleDraft">+ æ·»å æ¨¡å</el-button> |
| | | <template #footer><el-button @click="moduleVisible = false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submitModuleBatch">ä¿å</el-button></template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="followVisible" title="å¡«å项ç®è·è¿" width="650px" destroy-on-close> |
| | | <el-dialog v-model="followVisible" :title="followForm.id ? 'ç¼è¾é¡¹ç®è·è¿' : 'å¡«å项ç®è·è¿'" 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-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="é¡¹ç®æ¨¡å" prop="moduleId"><el-select v-model="reportForm.moduleId" :disabled="!!reportForm.id" style="width: 100%" @change="changeReportModule"><el-option v-for="item in reportModuleOptions" :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"> |
| | |
| | | <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="æ¬æ¬¡è¿åº¦" prop="incrementProgress"><el-input-number v-model="reportForm.incrementProgress" :min="0.01" :max="reportIncrementMax" :precision="2" /><span class="unit">%</span><span class="current-progress-hint">{{ reportProgressHint }}</span></el-form-item> |
| | | <el-form-item label="æ±æ¥æ¶é´" prop="reportDate"><el-date-picker v-model="reportForm.reportDate" :disabled="!!reportForm.id" type="date" value-format="YYYY-MM-DD" style="width: 100%" /><div v-if="reportForm.id" class="percent-hint">ç¼è¾å·²ææ±æ¥æ¶ä¸å
è®¸è°æ´æ¶é´ï¼ä»¥ä¿è¯åå²è¿åº¦é¡ºåºæ£ç¡®ã</div></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> |
| | | <template #footer><el-button @click="reportVisible = false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submitReport">{{ reportForm.id ? 'ä¿åä¿®æ¹' : 'æäº¤æ±æ¥' }}</el-button></template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="materialVisible" title="ä¸ä¼ 项ç®èµæ" width="650px" destroy-on-close> |
| | |
| | | import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import * as echarts from 'echarts' |
| | | import dayjs from 'dayjs' |
| | | import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue' |
| | | import { userListAll } from '@/api/publicApi' |
| | | import { |
| | | deleteProjectFollowUp, deleteProjectMaterial, deleteProjectModule, deleteProjectProgressReport, |
| | | getProjectWorkflow, saveProjectFollowUp, saveProjectMaterial, saveProjectModule, saveProjectProgressReport |
| | | deleteProjectFollowUp, deleteProjectMaterial, deleteProjectProgressReport, |
| | | getProjectWorkflow, saveProjectFollowUp, saveProjectMaterial, saveProjectModulesBatch, saveProjectProgressReport |
| | | } from '@/api/projectManagement/workflow' |
| | | import useUserStore from '@/store/modules/user' |
| | | |
| | | const props = defineProps({ projectId: [String, Number], projectInfo: { type: Object, default: () => ({}) } }) |
| | | const props = defineProps({ |
| | | projectId: [String, Number], |
| | | projectInfo: { type: Object, default: () => ({}) }, |
| | | readonly: { type: Boolean, default: false }, |
| | | singleTab: { type: String, default: '' } |
| | | }) |
| | | const emit = defineEmits(['project-updated']) |
| | | const { proxy } = getCurrentInstance() |
| | | const userStore = useUserStore() |
| | | const loading = ref(false) |
| | | const saving = ref(false) |
| | | const activeTab = ref('modules') |
| | | const activeTab = ref(props.singleTab || 'modules') |
| | | const reportModuleFilter = ref('all') |
| | | const userOptions = ref([]) |
| | | const data = reactive({ modules: [], followUps: [], reports: [], materials: [], sourceBusiness: {}, memberStatistics: [], followWindow: {} }) |
| | | const data = reactive({ modules: [], followUps: [], reports: [], materials: [], phases: [], phaseHistories: [], sourceBusiness: {}, memberStatistics: [], followWindow: {} }) |
| | | const chartRef = ref() |
| | | let chart |
| | | |
| | | const today = () => new Date().toISOString().slice(0, 10) |
| | | const today = () => dayjs().format('YYYY-MM-DD') |
| | | 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 => |
| | | const isStarted = computed(() => !!props.projectInfo?.currentPhase && props.projectInfo.currentPhase !== 'æªå¼å§') |
| | | const memberReportModules = computed(() => data.modules.filter(module => |
| | | (module.members || []).some(member => String(member.userId) === String(userStore.id)) |
| | | )) |
| | | const eligibleReportModules = computed(() => memberReportModules.value.filter(module => Number(module.currentProgress || 0) < 100)) |
| | | const reportModuleOptions = computed(() => reportForm.id ? memberReportModules.value : eligibleReportModules.value) |
| | | const selectedReportModule = computed(() => data.modules.find(module => String(module.id) === String(reportForm.moduleId))) |
| | | const reportBaseProgress = computed(() => Number(reportForm.id ? reportForm.previousProgress : selectedReportModule.value?.currentProgress || 0)) |
| | | const reportIncrementMax = computed(() => { |
| | | if (!reportForm.id) return Math.max(0.01, Number((100 - reportBaseProgress.value).toFixed(2))) |
| | | const current = Number(selectedReportModule.value?.currentProgress || 0) |
| | | return Math.max(0.01, Number((Number(reportForm.originalIncrementProgress || 0) + 100 - current).toFixed(2))) |
| | | }) |
| | | const reportProgressHint = computed(() => { |
| | | const current = Number(selectedReportModule.value?.currentProgress || 0).toFixed(2) |
| | | if (reportForm.id) return `è¯¥æ¬¡æ±æ¥å为 ${reportBaseProgress.value.toFixed(2)}%ï¼æ¨¡åå½å为 ${current}%` |
| | | return `模åå½åè¿åº¦ ${current}%ï¼æ¬æ¬¡æå¤å¯æ±æ¥ ${reportIncrementMax.value}%` |
| | | }) |
| | | const filteredReports = computed(() => reportModuleFilter.value === 'all' |
| | | ? data.reports |
| | | : data.reports.filter(report => String(report.moduleId) === reportModuleFilter.value)) |
| | | const filteredMemberStatistics = computed(() => { |
| | | if (reportModuleFilter.value === 'all') return data.memberStatistics || [] |
| | | const totals = new Map() |
| | | for (const report of filteredReports.value) { |
| | | const incrementProgress = Number(report.incrementProgress ?? report.progress ?? 0) |
| | | for (const member of report.reporterShareList || []) { |
| | | const userId = String(member.userId) |
| | | const current = totals.get(userId) || { userId: member.userId, userName: member.userName || 'æªå½å', value: 0 } |
| | | current.value += incrementProgress * Number(member.moduleShare || 0) / 100 |
| | | totals.set(userId, current) |
| | | } |
| | | } |
| | | return [...totals.values()] |
| | | .map(item => ({ ...item, value: Number(item.value.toFixed(2)) })) |
| | | .sort((a, b) => b.value - a.value) |
| | | }) |
| | | const reportStatisticsSummary = computed(() => { |
| | | const prefix = reportModuleFilter.value === 'all' ? '项ç®å æ¯ç»è®¡' : '模åå æ¯ç»è®¡' |
| | | if (!filteredMemberStatistics.value.length) return `${prefix}ï¼ææ æ°æ®` |
| | | return `${prefix}ï¼${filteredMemberStatistics.value.map(item => `${item.userName} ${item.value}%`).join('ï¼')}` |
| | | }) |
| | | const processStages = [ |
| | | { code: 'APPLICATION', label: 'ç³æ¥è®°å½', phaseName: 'ç³æ¥', rank: 3 }, |
| | | { code: 'RESULT', label: 'ç³æ¥ç»æ', phaseName: 'ç³æ¥ç»æ', rank: 4 }, |
| | | { code: 'ACCEPTANCE', label: 'éªæ¶è®°å½', phaseName: 'éªæ¶', rank: 5 } |
| | | ] |
| | | const currentPhaseRank = computed(() => ({ æªå¼å§: 0, ç«é¡¹: 1, å¼å: 2, ç³æ¥: 3, ç³æ¥ç»æ: 4, éªæ¶: 5, 已宿: 6 })[props.projectInfo?.currentPhase] || 0) |
| | | const visibleProcessStages = computed(() => processStages.filter(stage => currentPhaseRank.value >= stage.rank)) |
| | | const moduleReporterOptions = computed(() => { |
| | | const members = selectedReportModule.value?.members || [] |
| | | return members.map(member => ({ |
| | |
| | | }) |
| | | |
| | | 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 moduleDrafts = ref([]) |
| | | let draftSequence = 0 |
| | | const moduleDraftTotal = computed(() => moduleDrafts.value.reduce((sum, module) => sum + Number(module.projectWeight || 0), 0).toFixed(2)) |
| | | const moduleDraftRemaining = computed(() => (100 - Number(moduleDraftTotal.value)).toFixed(2)) |
| | | const followVisible = ref(false) |
| | | const followFormRef = ref() |
| | | const followForm = reactive({ followDate: today(), detail: '' }) |
| | | const followForm = reactive({ id: null, 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 reportForm = reactive({ id: null, moduleId: null, reporterShares: [], incrementProgress: 0, originalIncrementProgress: 0, previousProgress: 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) |
| | |
| | | } 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: '' }) |
| | | function newModuleDraft(row = {}) { |
| | | return { |
| | | id: row.id || null, localKey: row.id ? `module-${row.id}` : `new-${++draftSequence}`, |
| | | moduleName: row.moduleName || '', plannedStartDate: row.plannedStartDate || '', summary: row.summary || '', |
| | | projectWeight: Number(row.projectWeight || 0), |
| | | members: (row.members || []).map(member => { |
| | | const reportStats = memberReportedContribution(row, member.userId) |
| | | return { userId: member.userId, userName: member.userName, ...reportStats } |
| | | }), |
| | | locked: isStarted.value |
| | | } |
| | | } |
| | | |
| | | function memberReportedContribution(module, userId) { |
| | | let hasReportedShare = false |
| | | const completedModuleShare = data.reports |
| | | .filter(report => String(report.moduleId) === String(module.id)) |
| | | .reduce((total, report) => { |
| | | const memberShare = (report.reporterShareList || []) |
| | | .filter(member => String(member.userId) === String(userId)) |
| | | .reduce((sum, member) => sum + Number(member.moduleShare || 0), 0) |
| | | if (memberShare > 0) hasReportedShare = true |
| | | const incrementProgress = Number(report.incrementProgress ?? report.progress ?? 0) |
| | | return total + incrementProgress * memberShare / 100 |
| | | }, 0) |
| | | return { hasReportedShare, completedModuleShare: Number(completedModuleShare.toFixed(2)) } |
| | | } |
| | | |
| | | function openModuleBatch() { |
| | | moduleDrafts.value = data.modules.map(newModuleDraft) |
| | | if (!moduleDrafts.value.length) addModuleDraft() |
| | | moduleVisible.value = true |
| | | } |
| | | |
| | | function addModuleDraft() { moduleDrafts.value.push(newModuleDraft({ members: [{ userId: null, userName: '', hasReportedShare: false, completedModuleShare: 0 }] })) } |
| | | function removeModuleDraft(index) { moduleDrafts.value.splice(index, 1) } |
| | | function otherDraftWeight(index) { return moduleDrafts.value.reduce((sum, module, i) => i === index ? sum : sum + Number(module.projectWeight || 0), 0).toFixed(2) } |
| | | function draftWeightMax(index) { return Math.max(0.01, Number((100 - Number(otherDraftWeight(index))).toFixed(2))) } |
| | | |
| | | 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('æ¨¡åæ§è¡äººä¸è½éå¤') |
| | | async function submitModuleBatch() { |
| | | if (!moduleDrafts.value.length) return ElMessage.warning('请è³å°æ·»å ä¸ä¸ªé¡¹ç®æ¨¡å') |
| | | if (moduleDrafts.value.some(module => !module.moduleName?.trim() || !module.plannedStartDate || !module.summary?.trim())) return ElMessage.warning('è¯·å®æ´å¡«åæææ¨¡åçåç§°ãé¢è®¡å¼å§æ¶é´åæ¨¡åæ¦è¦') |
| | | if (Number(moduleDraftTotal.value) !== 100) return ElMessage.warning('æææ¨¡åç项ç®å·¥ä½éå æ¯å计å¿
须为100%') |
| | | for (const module of moduleDrafts.value) { |
| | | if (!module.members.length || module.members.some(member => !member.userId)) return ElMessage.warning(`请为模åâ${module.moduleName}âè³å°éæ©ä¸åæ§è¡äºº`) |
| | | if (new Set(module.members.map(member => String(member.userId))).size !== module.members.length) return ElMessage.warning(`模åâ${module.moduleName}âçæ§è¡äººä¸è½éå¤`) |
| | | } |
| | | saving.value = true |
| | | try { |
| | | await saveProjectModule({ ...moduleForm, projectId: props.projectId }) |
| | | ElMessage.success('模åä¿åæå'); moduleVisible.value = false; await loadData() |
| | | await saveProjectModulesBatch({ projectId: props.projectId, modules: moduleDrafts.value }) |
| | | 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(row) { |
| | | Object.assign(followForm, row |
| | | ? { id: row.id, followDate: row.followDate, detail: row.detail || '' } |
| | | : { id: null, followDate: today(), detail: '' }) |
| | | followFiles.value = row ? [...(row.attachmentList || [])] : [] |
| | | followVisible.value = true |
| | | } |
| | | |
| | | 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() { |
| | |
| | | finally { saving.value = false } |
| | | } |
| | | |
| | | function openReport() { |
| | | if (!eligibleReportModules.value.length) return ElMessage.warning('åªææ¨¡åæ§è¡äººæè½å¡«å该模åçè¿åº¦æ±æ¥') |
| | | function openReport(row) { |
| | | if (row) { |
| | | if (!memberReportModules.value.some(module => String(module.id) === String(row.moduleId))) return ElMessage.warning('åªæè¯¥æ¨¡åæ§è¡äººæè½ç¼è¾è¿åº¦æ±æ¥') |
| | | const incrementProgress = Number(row.incrementProgress ?? row.progress ?? 0) |
| | | Object.assign(reportForm, { |
| | | id: row.id, moduleId: row.moduleId, |
| | | reporterShares: (row.reporterShareList || []).map(member => ({ ...member })), |
| | | incrementProgress, originalIncrementProgress: incrementProgress, |
| | | previousProgress: Number(row.previousProgress || 0), reportDate: row.reportDate, remark: row.remark || '' |
| | | }) |
| | | reportFiles.value = [...(row.attachmentList || [])] |
| | | reportVisible.value = true |
| | | return |
| | | } |
| | | 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: '' }) |
| | | const module = reportModuleFilter.value === 'all' |
| | | ? null |
| | | : eligibleReportModules.value.find(item => String(item.id) === reportModuleFilter.value) |
| | | Object.assign(reportForm, { |
| | | id: null, |
| | | moduleId: module?.id ?? null, |
| | | reporterShares: module && userId ? [{ userId, userName, moduleShare: 100 }] : [], |
| | | incrementProgress: 0, |
| | | originalIncrementProgress: 0, |
| | | previousProgress: Number(module?.currentProgress || 0), |
| | | reportDate: today(), |
| | | remark: '' |
| | | }) |
| | | reportFiles.value = []; reportVisible.value = true |
| | | } |
| | | |
| | |
| | | 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.incrementProgress = 0 |
| | | reportForm.previousProgress = Number(module?.currentProgress || 0) |
| | | reportForm.reporterShares = userId ? [{ userId, userName, moduleShare: 100 }] : [] |
| | | } |
| | | |
| | | function canEditReport(row) { |
| | | return memberReportModules.value.some(module => String(module.id) === String(row.moduleId)) |
| | | } |
| | | |
| | | function reporterShareLimit(index) { |
| | |
| | | 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%') |
| | | if (Number(reportForm.incrementProgress || 0) <= 0) return ElMessage.warning('æ¬æ¬¡è¿åº¦å¿
须大äº0') |
| | | saving.value = true |
| | | try { await saveProjectProgressReport({ ...reportForm, projectId: props.projectId, attachments: normalizeFiles(reportFiles.value) }); ElMessage.success('æ±æ¥å·²æäº¤'); reportVisible.value = false; await loadData() } |
| | | try { |
| | | await saveProjectProgressReport({ ...reportForm, projectId: props.projectId, attachments: normalizeFiles(reportFiles.value) }) |
| | | ElMessage.success(reportForm.id ? 'æ±æ¥ä¿®æ¹å·²ä¿å' : 'æ±æ¥å·²æäº¤') |
| | | reportVisible.value = false |
| | | await loadData() |
| | | emit('project-updated') |
| | | } |
| | | finally { saving.value = false } |
| | | } |
| | | |
| | |
| | | await ElMessageBox.confirm('确认å é¤è¿æ¡è®°å½åï¼', 'æç¤º', { type: 'warning' }) |
| | | const api = { follow: deleteProjectFollowUp, report: deleteProjectProgressReport, material: deleteProjectMaterial }[type] |
| | | await api(row.id); ElMessage.success('å 餿å'); await loadData() |
| | | if (type === 'report') emit('project-updated') |
| | | } |
| | | |
| | | 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 || [] |
| | | const source = filteredMemberStatistics.value |
| | | const isAll = reportModuleFilter.value === 'all' |
| | | chart.setOption({ |
| | | title: { text: 'æå已宿工ä½éæ¯é', left: 'center', textStyle: { fontSize: 15 } }, |
| | | title: { text: isAll ? 'æå项ç®å æ¯' : 'æå模åå æ¯', 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 selectReportModule(moduleId) { reportModuleFilter.value = moduleId; nextTick(renderChart) } |
| | | function moduleReportCount(moduleId) { return data.reports.filter(report => String(report.moduleId) === String(moduleId)).length } |
| | | function stageRecords(phaseCode) { |
| | | const histories = (data.phaseHistories || []).filter(record => record.phaseCode === phaseCode) |
| | | if (histories.length) return histories |
| | | const snapshot = (data.phases || []).find(record => record.phaseCode === phaseCode) |
| | | if (!snapshot || snapshot.status === 'NOT_STARTED') return [] |
| | | return [{ ...snapshot, actionLabel: snapshot.phaseName, actionType: snapshot.status === 'COMPLETED' ? 'PASS' : 'ENTER' }] |
| | | } |
| | | function formatActionTime(time) { return time ? String(time).replace('T', ' ') : 'æ¶é´æªè®°å½' } |
| | | function phaseRecordType(type) { return ['RECTIFY', 'SUPPLEMENT'].includes(type) ? 'warning' : type === 'REJECTED' ? 'danger' : type === 'PASS' ? 'success' : 'primary' } |
| | | function recordStatusLabel(record) { return record.actionType === 'RECTIFY' ? 'éè¦æ´æ¹' : record.actionType === 'SUPPLEMENT' ? 'éè¦è¡¥å
' : record.actionType === 'REJECTED' ? 'æªéè¿' : record.actionType === 'PASS' ? 'å·²éè¿' : 'å·²åç' } |
| | | function phaseFieldRows(record) { |
| | | const form = record.formData || {} |
| | | const labels = { |
| | | applicationDate: 'ç³æ¥æ¥æ', applicationType: 'ç³æ¥ç±»å', applicationName: 'ç³æ¥åç§°', expectedFeedbackDate: 'é¢è®¡å馿¥æ', description: 'ç³æ¥è¯´æ', |
| | | resultDate: 'ç»ææ¥æ', resultStatus: 'ç³æ¥ç»æ', resultDescription: 'ç»æè¯´æ', acceptancePlanDate: '计åéªæ¶æ¥æ', |
| | | acceptor: 'éªæ¶äºº/客æ·', acceptanceStandard: 'éªæ¶æ å', acceptanceDate: 'éªæ¶æ¥æ', acceptanceResult: 'éªæ¶ç»æ', acceptanceDescription: 'éªæ¶è¯´æ' |
| | | } |
| | | const enumLabels = { PASS: 'éè¿', SUPPLEMENT: 'éè¦è¡¥å
', REJECTED: 'æªéè¿', RECTIFY: 'éè¦æ´æ¹' } |
| | | return Object.entries(labels).filter(([key]) => form[key] !== null && form[key] !== undefined && form[key] !== '').map(([key, label]) => ({ label, value: enumLabels[form[key]] || form[key] })) |
| | | } |
| | | function resizeChart() { chart?.resize() } |
| | | onMounted(async () => { await Promise.all([loadUsers(), loadData()]); window.addEventListener('resize', resizeChart) }) |
| | | onBeforeUnmount(() => { window.removeEventListener('resize', resizeChart); chart?.dispose() }) |
| | |
| | | .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; } |
| | | .report-stat-summary { color: #606266; font-size: 13px; line-height: 1.7; } |
| | | .report-workbench { display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 16px; align-items: start; } |
| | | .report-module-nav { padding: 12px; border: 1px solid #e4e7ed; border-radius: 10px; background: #f8fafc; } |
| | | .report-nav-title { padding: 4px 10px 10px; color: #303133; font-size: 14px; font-weight: 600; } |
| | | .report-nav-item { width: 100%; margin: 0 0 8px; padding: 11px 12px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: #303133; text-align: left; cursor: pointer; transition: all .2s; } |
| | | .report-nav-item:last-child { margin-bottom: 0; } |
| | | .report-nav-item:hover { border-color: #b8cdf8; background: #eef4ff; } |
| | | .report-nav-item.active { border-color: #8fb2f5; background: #e8f1ff; color: #0b46b5; box-shadow: 0 2px 7px rgba(11, 70, 181, .08); } |
| | | .report-nav-name, .report-nav-meta { display: block; } |
| | | .report-nav-name { overflow: hidden; font-size: 13px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } |
| | | .report-nav-meta { margin-top: 5px; color: #909399; font-size: 12px; line-height: 1.35; } |
| | | .report-nav-item.active .report-nav-meta { color: #5e7fbf; } |
| | | .report-content { min-width: 0; } |
| | | .member-tag { margin: 2px 6px 2px 0; } |
| | | .report-layout { display: grid; grid-template-columns: minmax(300px, 34%) 1fr; gap: 14px; } |
| | | .report-layout.report-only { grid-template-columns: 1fr; } |
| | | .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; } |
| | | .member-row { display: grid; grid-template-columns: minmax(260px, 1fr) auto 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; } |
| | | .current-progress-hint { margin-left: 14px; color: #909399; font-size: 12px; } |
| | | .phase-record-timeline { padding: 14px 12px 0 8px; }.phase-record-card { border-color: #dce6f5; }.phase-record-head { display:flex;align-items:center;justify-content:space-between;gap:16px; }.phase-record-head>div { display:flex;align-items:center;gap:10px; }.phase-record-head>span { color:#909399;font-size:12px; }.phase-record-remark,.phase-record-files { margin-top:14px;color:#606266;font-size:13px;line-height:1.7; }.phase-record-files { display:flex;align-items:center;flex-wrap:wrap;gap:4px; } |
| | | .file-cell { display:flex;flex-direction:column;align-items:flex-start;gap:2px; }.file-cell .el-button { max-width:100%;margin-left:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }.empty-file { color:#c0c4cc;font-size:12px; } |
| | | .module-total { margin: 0 0 14px; font-weight: 600; color: #067647; } |
| | | .module-total.invalid { color: #d97706; } |
| | | .module-batch-card { margin-bottom: 14px; padding: 16px 16px 4px; border: 1px solid #dcdfe6; border-radius: 8px; background: #fafcff; } |
| | | .module-batch-title { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; } |
| | | .module-batch-title .el-button { margin-left: auto; } |
| | | .locked-tip { color: #e6a23c; font-size: 12px; } |
| | | .module-grid { display: grid; grid-template-columns: 1.2fr 1fr 1.25fr; column-gap: 18px; } |
| | | .summary-item, .members-item { grid-column: 1 / -1; } |
| | | @media (max-width: 1200px) { .report-layout { grid-template-columns: 1fr; } } |
| | | @media (max-width: 900px) { .report-workbench { grid-template-columns: 1fr; } .report-module-nav { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px; } .report-nav-title { grid-column: 1 / -1; } .report-nav-item { margin: 0; } } |
| | | </style> |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" :title="dialogTitle" :width="startProject ? '1100px' : '900px'" destroy-on-close> |
| | | <el-dialog v-model="visible" :title="dialogTitle" width="900px" destroy-on-close> |
| | | <el-alert v-if="isBusinessProject" title="该项ç®ç±ä¸å¡å®¡æ¹çæï¼ä¿ååå¯å¨é¡¹ç®è¯¦æ
æ¥çæ¥æºä¸å¡åå
¶éä»¶ã" type="info" :closable="false" class="tip" /> |
| | | <el-alert v-if="startProject" title="å¼å§é¡¹ç®ä¹åï¼è¯·å
éè¿å表ä¸çâæ¨¡å管çâææææ¨¡åçå·¥ä½éå æ¯åé
å°100%ã" 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-row :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-row :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-row :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-row :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="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 class="module-actions"><span>å½å模åå·¥ä½éå计ï¼{{ startModuleWeight }}%</span><el-button type="primary" plain @click="addStartModule">+ æ·»å æ¨¡å</el-button></div> |
| | | </template> |
| | | <el-form-item label="项ç®å¤æ³¨"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="2000" show-word-limit /></el-form-item> |
| | | <el-form-item label="项ç®èµæ"><AttachmentUploadFile v-model:file-list="files" :limit="20" /></el-form-item> |
| | | </el-form> |
| | | <template #footer><el-button @click="visible=false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submit">{{ personnelOnly ? 'ä¿åæ§è¡äºº' : startProject ? 'ä¿åå¹¶å¼å§é¡¹ç®' : 'ä¿å' }}</el-button></template> |
| | | <template #footer><el-button @click="visible=false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submit">{{ startProject ? 'ä¿åå¹¶å¼å§é¡¹ç®' : 'ä¿å' }}</el-button></template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | |
| | | import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue' |
| | | import { addProject, getProject, updateProject } from '@/api/projectManagement/project' |
| | | import { listCustomer } from '@/api/basicData/customer' |
| | | import { deleteProjectModule, getProjectWorkflow, listProjectCategories, saveProjectModule, startProjectWorkflow, updateProjectExecutors } from '@/api/projectManagement/workflow' |
| | | import { listProjectCategories, startProjectWorkflow } from '@/api/projectManagement/workflow' |
| | | import { userListAll } from '@/api/publicApi' |
| | | |
| | | const emit = defineEmits(['completed']) |
| | | const visible = ref(false) |
| | | const saving = ref(false) |
| | | const startProject = ref(false) |
| | | const personnelOnly = ref(false) |
| | | const operationType = ref('add') |
| | | const formRef = 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 form = reactive({ id:null,no:'',title:'',clientId:null,clientName:'',source:'æå¨å建',sourceBusinessId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],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 dialogTitle = computed(() => startProject.value ? 'å¡«åç«é¡¹ä¿¡æ¯å¹¶å¼å§é¡¹ç®' : operationType.value === 'add' ? 'æå¨å建项ç®' : 'ç¼è¾é¡¹ç®åºæ¬æ
åµ') |
| | | |
| | | 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 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 reset() { Object.assign(form,{ id:null,no:'',title:'',clientId:null,clientName:'',source:'æå¨å建',sourceBusinessId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'æªå¼å§',remark:'' }); files.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 |
| | | } |
| | | |
| | | async function openDialog(options={}) { |
| | | reset(); operationType.value=options.operationType||'add'; startProject.value=!!options.startProject; personnelOnly.value=!!options.personnelOnly; await loadOptions() |
| | | reset(); operationType.value=options.operationType||'add'; startProject.value=!!options.startProject; 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()} |
| | | 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||[] }); files.value=(info.attachmentList||[]).map(f=>({...f})) } |
| | | visible.value=true |
| | | } |
| | | |
| | | 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 |
| | | } |
| | | 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 { |
| | | 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) |
| | | } |
| | | ElMessage.success(startProject.value?'项ç®å·²å¼å§':'ä¿åæå') |
| | |
| | | defineExpose({ openDialog }) |
| | | </script> |
| | | |
| | | <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> |
| | | <style scoped>.tip{margin-bottom:18px}</style> |
| | |
| | | <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="currentPhase" label="å½åé¶æ®µ" width="110"><template #default="{row}"><el-tag class="phase-tag" :class="phaseTagClass(row.currentPhase)">{{ row.currentPhase || 'æªå¼å§' }}</el-tag></template></el-table-column> |
| | | <el-table-column prop="projectProgress" label="项ç®è¿åº¦" width="150" align="center"><template #default="{row}"><el-progress :percentage="Number(row.projectProgress || 0)" /></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"> |
| | | <el-table-column prop="actualStartTime" label="å®é
å¼å§" width="120"><template #default="{row}">{{ row.actualStartTime || '-' }}</template></el-table-column> |
| | | <el-table-column prop="actualEndTime" label="å®é
ç»æ" width="120"><template #default="{row}">{{ row.actualEndTime || '-' }}</template></el-table-column> |
| | | <el-table-column label="æä½" width="520" 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 v-if="row.currentPhase==='æªå¼å§'" link type="primary" @click="maintain(row, 'modules')">模å管ç</el-button> |
| | | <el-dropdown v-if="row.currentPhase!=='æªå¼å§'" class="operation-dropdown" @command="tab => maintain(row, tab)"> |
| | | <el-button link type="primary">项ç®ç»´æ¤<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item command="modules">模å管ç</el-dropdown-item> |
| | | <el-dropdown-item command="follow">项ç®è·è¿</el-dropdown-item> |
| | | <el-dropdown-item command="reports">é¡¹ç®æ±æ¥</el-dropdown-item> |
| | | <el-dropdown-item command="materials">项ç®èµæ</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | <el-button link type="primary" @click="detail(row)">详æ
</el-button> |
| | | <el-button link type="danger" @click="remove(row)">å é¤</el-button> |
| | | <el-button v-if="canAdvanceStage(row)" link type="success" @click="advanceStage(row)">{{ nextStageAction(row) }}</el-button> |
| | | <el-button link type="primary" @click="openLifecycle(row)">项ç®çå½å¨æ</el-button> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" link type="danger" @click="remove(row)">å é¤</el-button> |
| | | </template> |
| | | </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" /> |
| | | <el-dialog v-model="maintenanceVisible" :title="`${selectedProject.title || ''} - ${maintenanceTitle}`" width="92%" top="5vh" destroy-on-close> |
| | | <ProjectExecutionPanel v-if="maintenanceVisible" :project-id="selectedProject.id" :project-info="selectedProject" :single-tab="maintenanceTab" :readonly="maintenanceReadonly" @project-updated="refreshProjectList" /> |
| | | </el-dialog> |
| | | <el-drawer v-model="lifecycleVisible" :title="`${lifecycleProject.title || ''} - 项ç®çå½å¨æ`" size="520px" destroy-on-close> |
| | | <div v-loading="lifecycleLoading" class="lifecycle-drawer"> |
| | | <div class="lifecycle-summary"> |
| | | <div><span>å½åé¶æ®µ</span><strong>{{ lifecycleProject.currentPhase || 'æªå¼å§' }}</strong></div> |
| | | <div><span>计å卿</span><strong>{{ lifecycleProject.planStartTime || '-' }} è³ {{ lifecycleProject.planEndTime || '-' }}</strong></div> |
| | | <div><span>å®é
卿</span><strong>{{ lifecycleProject.actualStartTime || '-' }} è³ {{ lifecycleProject.actualEndTime || '-' }}</strong></div> |
| | | </div> |
| | | <el-empty v-if="!lifecycleLoading && !lifecycleNodes.length" description="ææ çå½å¨æè®°å½" /> |
| | | <el-timeline v-else class="project-timeline"> |
| | | <el-timeline-item v-for="node in lifecycleNodes" :key="node.code" :timestamp="formatLifecycleTime(node.time)" placement="top" :type="lifecycleNodeType(node.status,node.eventType)" :hollow="node.status === 'PENDING'"> |
| | | <div class="lifecycle-node" :class="node.status?.toLowerCase()"> |
| | | <div class="lifecycle-node-head"><strong>{{ node.name }}</strong><el-tag size="small" :type="lifecycleTagType(node.status,node.eventType)">{{ node.statusLabel || lifecycleStatusLabel(node.status) }}</el-tag></div> |
| | | <p>{{ node.description || 'ææ è¯´æ' }}</p> |
| | | </div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </div> |
| | | </el-drawer> |
| | | <el-dialog v-model="stageVisible" :title="stageDialogTitle" width="680px" destroy-on-close> |
| | | <el-alert :title="`å½å项ç®ï¼${stageProject.title || '-'}ï¼å½åé¶æ®µï¼${stageProject.currentPhase || '-'}`" type="info" :closable="false" class="stage-alert" /> |
| | | <el-form label-width="110px"> |
| | | <template v-if="stageProject.currentPhase==='å¼å'"> |
| | | <el-form-item label="ç³æ¥æ¥æ" required><el-date-picker v-model="stageForm.applicationDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item> |
| | | <el-form-item label="ç³æ¥ç±»å" required><el-select v-model="stageForm.applicationType" style="width:100%"><el-option v-for="item in ['客æ·äº¤ä»','å¹³å°åå¸','软件è使','å
é¨åå¸','å
¶ä»']" :key="item" :label="item" :value="item" /></el-select></el-form-item> |
| | | <el-form-item label="ç³æ¥åç§°" required><el-input v-model="stageForm.applicationName" maxlength="200" /></el-form-item> |
| | | <el-form-item label="é¢è®¡å馿¥æ"><el-date-picker v-model="stageForm.expectedFeedbackDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item> |
| | | <el-form-item label="ç³æ¥è¯´æ" required><el-input v-model="stageForm.description" type="textarea" :rows="4" maxlength="2000" show-word-limit /></el-form-item> |
| | | </template> |
| | | <template v-else-if="stageProject.currentPhase==='ç³æ¥'"> |
| | | <el-form-item label="ç»ææ¥æ" required><el-date-picker v-model="stageForm.resultDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item> |
| | | <el-form-item label="ç³æ¥ç»æ" required><el-radio-group v-model="stageForm.resultStatus"><el-radio value="PASS">éè¿</el-radio><el-radio value="SUPPLEMENT">éè¦è¡¥å
</el-radio><el-radio value="REJECTED">æªéè¿</el-radio></el-radio-group></el-form-item> |
| | | <el-form-item label="ç»æè¯´æ" required><el-input v-model="stageForm.resultDescription" type="textarea" :rows="4" maxlength="2000" show-word-limit /></el-form-item> |
| | | <el-alert v-if="stageForm.resultStatus!=='PASS'" title="ä¿åå项ç®ä»åçå¨ç³æ¥é¶æ®µï¼å¯ç»§ç»è¡¥å
æéæ°ç»è®°ç»æã" type="warning" :closable="false" /> |
| | | </template> |
| | | <template v-else-if="stageProject.currentPhase==='ç³æ¥ç»æ'"> |
| | | <el-form-item label="计åéªæ¶æ¥æ" required><el-date-picker v-model="stageForm.acceptancePlanDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item> |
| | | <el-form-item label="éªæ¶äºº/客æ·" required><el-input v-model="stageForm.acceptor" maxlength="100" /></el-form-item> |
| | | <el-form-item label="éªæ¶æ å" required><el-input v-model="stageForm.acceptanceStandard" type="textarea" :rows="4" maxlength="2000" show-word-limit /></el-form-item> |
| | | </template> |
| | | <template v-else-if="stageProject.currentPhase==='éªæ¶'"> |
| | | <el-form-item label="éªæ¶æ¥æ" required><el-date-picker v-model="stageForm.acceptanceDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item> |
| | | <el-form-item label="éªæ¶äºº/客æ·" required><el-input v-model="stageForm.acceptor" maxlength="100" /></el-form-item> |
| | | <el-form-item label="éªæ¶ç»æ" required><el-radio-group v-model="stageForm.acceptanceResult"><el-radio value="PASS">éè¿</el-radio><el-radio value="RECTIFY">éè¦æ´æ¹</el-radio></el-radio-group></el-form-item> |
| | | <el-form-item label="éªæ¶è¯´æ" required><el-input v-model="stageForm.acceptanceDescription" type="textarea" :rows="4" maxlength="2000" show-word-limit /></el-form-item> |
| | | <el-alert v-if="stageForm.acceptanceResult==='RECTIFY'" title="ä¿åå项ç®ä»åçå¨éªæ¶é¶æ®µï¼æ´æ¹å®æåå¯å次填åéªæ¶ç»æã" type="warning" :closable="false" /> |
| | | </template> |
| | | <el-form-item label="ç¸å
³éä»¶"><AttachmentUploadFile v-model:file-list="stageFiles" :limit="20" /></el-form-item> |
| | | </el-form> |
| | | <template #footer><el-button @click="stageVisible=false">åæ¶</el-button><el-button type="primary" :loading="stageSaving" @click="submitStage">{{ stageSubmitText }}</el-button></template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagement"> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { listProject, delProject } from '@/api/projectManagement/project' |
| | | import { getProjectLifecycle, saveProjectPhase, transitionProjectStage } from '@/api/projectManagement/workflow' |
| | | import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue' |
| | | import FormDia from './components/formDia.vue' |
| | | import ProjectExecutionPanel from './components/ProjectExecutionPanel.vue' |
| | | |
| | | const router=useRouter(); const loading=ref(false); const rows=ref([]); const total=ref(0); const formRef=ref() |
| | | const maintenanceVisible=ref(false); const maintenanceTab=ref('modules'); const selectedProject=ref({}) |
| | | const lifecycleVisible=ref(false); const lifecycleLoading=ref(false); const lifecycleProject=ref({}); const lifecycleNodes=ref([]) |
| | | const stageVisible=ref(false); const stageSaving=ref(false); const stageProject=ref({}); const stageFiles=ref([]) |
| | | const stageForm=reactive({applicationDate:'',applicationType:'客æ·äº¤ä»',applicationName:'',expectedFeedbackDate:'',description:'',resultDate:'',resultStatus:'PASS',resultDescription:'',acceptancePlanDate:'',acceptor:'',acceptanceStandard:'',acceptanceDate:'',acceptanceResult:'PASS',acceptanceDescription:''}) |
| | | 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})} |
| | | const maintenanceNames={modules:'模å管ç',follow:'项ç®è·è¿',reports:'é¡¹ç®æ±æ¥',materials:'项ç®èµæ'} |
| | | const nextStageMap={å¼å:'ç³æ¥',ç³æ¥:'ç³æ¥ç»æ',ç³æ¥ç»æ:'éªæ¶',éªæ¶:'已宿'} |
| | | const maintenanceReadonly=computed(()=>['ç³æ¥','ç³æ¥ç»æ','éªæ¶','已宿'].includes(selectedProject.value.currentPhase)) |
| | | const maintenanceTitle=computed(()=>`${maintenanceNames[maintenanceTab.value]||'项ç®ç»´æ¤'}${maintenanceReadonly.value?'ï¼åªè¯»ï¼':''}`) |
| | | function maintain(row,tab){selectedProject.value={...row};maintenanceTab.value=tab;maintenanceVisible.value=true} |
| | | async function refreshProjectList(){ |
| | | await load() |
| | | const latest=rows.value.find(row=>String(row.id)===String(selectedProject.value.id)) |
| | | if(latest) selectedProject.value={...latest} |
| | | } |
| | | function detail(row){router.push(`/projectManagement/Management/detail/${row.id}`)} |
| | | function canAdvanceStage(row){ |
| | | if(!nextStageMap[row.currentPhase]) return false |
| | | return row.currentPhase!=='å¼å'||Number(row.projectProgress||0)>=100 |
| | | } |
| | | function phaseTagClass(phase){ |
| | | return ({æªå¼å§:'phase-not-started',ç«é¡¹:'phase-initiation',å¼å:'phase-development',ç³æ¥:'phase-application',ç³æ¥ç»æ:'phase-result',éªæ¶:'phase-acceptance',已宿:'phase-completed'})[phase]||'phase-not-started' |
| | | } |
| | | function nextStageAction(row){return ({å¼å:'æäº¤ç³æ¥',ç³æ¥:'ç»è®°ç³æ¥ç»æ',ç³æ¥ç»æ:'è¿å
¥éªæ¶',éªæ¶:'å¡«åéªæ¶ç»æ'})[row.currentPhase]||'é¶æ®µåç'} |
| | | const stageDialogTitle=computed(()=>nextStageAction(stageProject.value)) |
| | | const stageSubmitText=computed(()=>stageProject.value.currentPhase==='éªæ¶'&&stageForm.acceptanceResult==='PASS'?'éªæ¶éè¿å¹¶å®æé¡¹ç®':'ä¿å') |
| | | function advanceStage(row){ |
| | | const today=new Date().toISOString().slice(0,10) |
| | | stageProject.value={...row} |
| | | Object.assign(stageForm,{applicationDate:today,applicationType:'客æ·äº¤ä»',applicationName:row.title||'',expectedFeedbackDate:'',description:'',resultDate:today,resultStatus:'PASS',resultDescription:'',acceptancePlanDate:today,acceptor:row.clientName||'',acceptanceStandard:'',acceptanceDate:today,acceptanceResult:'PASS',acceptanceDescription:''}) |
| | | stageFiles.value=[] |
| | | stageVisible.value=true |
| | | } |
| | | function normalizedStageFiles(){return stageFiles.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)} |
| | | function stageRemark(phase){ |
| | | if(phase==='å¼å') return `${stageForm.applicationType}ï¼${stageForm.applicationName}` |
| | | if(phase==='ç³æ¥') return `ç³æ¥ç»æï¼${({PASS:'éè¿',SUPPLEMENT:'éè¦è¡¥å
',REJECTED:'æªéè¿'})[stageForm.resultStatus]}ï¼${stageForm.resultDescription}` |
| | | if(phase==='ç³æ¥ç»æ') return `计åéªæ¶ï¼${stageForm.acceptancePlanDate}ï¼éªæ¶äººï¼${stageForm.acceptor}` |
| | | return `éªæ¶ç»æï¼${stageForm.acceptanceResult==='PASS'?'éè¿':'éè¦æ´æ¹'}ï¼${stageForm.acceptanceDescription}` |
| | | } |
| | | function validateStageForm(phase){ |
| | | if(phase==='å¼å'&&(!stageForm.applicationDate||!stageForm.applicationType||!stageForm.applicationName.trim()||!stageForm.description.trim())) return 'è¯·å®æ´å¡«åç³æ¥æ¥æãç±»åãåç§°å说æ' |
| | | if(phase==='ç³æ¥'&&(!stageForm.resultDate||!stageForm.resultStatus||!stageForm.resultDescription.trim())) return 'è¯·å®æ´å¡«åç³æ¥ç»ææ¥æãç»æå说æ' |
| | | if(phase==='ç³æ¥ç»æ'&&(!stageForm.acceptancePlanDate||!stageForm.acceptor.trim()||!stageForm.acceptanceStandard.trim())) return 'è¯·å®æ´å¡«åéªæ¶æ¥æãéªæ¶äººåéªæ¶æ å' |
| | | if(phase==='éªæ¶'&&(!stageForm.acceptanceDate||!stageForm.acceptor.trim()||!stageForm.acceptanceResult||!stageForm.acceptanceDescription.trim())) return 'è¯·å®æ´å¡«åéªæ¶æ¥æãéªæ¶äººãç»æå说æ' |
| | | } |
| | | async function submitStage(){ |
| | | const phase=stageProject.value.currentPhase |
| | | const validationMessage=validateStageForm(phase) |
| | | if(validationMessage) return ElMessage.warning(validationMessage) |
| | | const common={projectId:stageProject.value.id,formData:{...stageForm},remark:stageRemark(phase),attachments:normalizedStageFiles()} |
| | | stageSaving.value=true |
| | | try{ |
| | | if(phase==='å¼å') await transitionProjectStage({...common,targetStage:'ç³æ¥'}) |
| | | else if(phase==='ç³æ¥'&&stageForm.resultStatus==='PASS') await transitionProjectStage({...common,targetStage:'ç³æ¥ç»æ'}) |
| | | else if(phase==='ç³æ¥') await saveProjectPhase({...common,phaseCode:'APPLICATION',status:'IN_PROGRESS'}) |
| | | else if(phase==='ç³æ¥ç»æ') await transitionProjectStage({...common,targetStage:'éªæ¶'}) |
| | | else if(phase==='éªæ¶'&&stageForm.acceptanceResult==='PASS') await transitionProjectStage({...common,targetStage:'已宿'}) |
| | | else if(phase==='éªæ¶') await saveProjectPhase({...common,phaseCode:'ACCEPTANCE',status:'IN_PROGRESS'}) |
| | | ElMessage.success(phase==='éªæ¶'&&stageForm.acceptanceResult==='PASS'?'项ç®å·²éªæ¶å®æ':'é¶æ®µä¿¡æ¯å·²ä¿å') |
| | | stageVisible.value=false |
| | | await load() |
| | | }finally{stageSaving.value=false} |
| | | } |
| | | async function openLifecycle(row){ |
| | | lifecycleProject.value={...row};lifecycleNodes.value=[];lifecycleVisible.value=true;lifecycleLoading.value=true |
| | | try{const res=await getProjectLifecycle(row.id);const data=res?.data||{};lifecycleProject.value={...row,...(data.project||{})};lifecycleNodes.value=data.nodes||[]} |
| | | catch{ElMessage.error('è·å项ç®çå½å¨æå¤±è´¥')}finally{lifecycleLoading.value=false} |
| | | } |
| | | function formatLifecycleTime(time){return time?String(time).replace('T',' '):'æ¶é´å¾
è®°å½'} |
| | | function lifecycleNodeType(status,eventType){return ['RECTIFY','SUPPLEMENT'].includes(eventType)?'warning':eventType==='REJECTED'?'danger':status==='COMPLETED'?'success':status==='CURRENT'?'primary':'info'} |
| | | function lifecycleTagType(status,eventType){return lifecycleNodeType(status,eventType)} |
| | | function lifecycleStatusLabel(status){return status==='COMPLETED'?'已宿':status==='CURRENT'?'è¿è¡ä¸':'å¾
è¿è¡'} |
| | | 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>.app-container{padding:20px}.search-card{margin-bottom:16px}.actions{margin-bottom:14px;text-align:right}</style> |
| | | <style scoped> |
| | | .app-container{padding:20px}.search-card{margin-bottom:16px}.actions{margin-bottom:14px;text-align:right}.operation-dropdown{margin:0 10px} |
| | | .stage-alert{margin-bottom:18px} |
| | | .lifecycle-drawer{min-height:300px;padding:0 6px 24px}.lifecycle-summary{display:grid;gap:10px;margin-bottom:26px;padding:16px;border:1px solid #dbe7fb;border-radius:10px;background:linear-gradient(135deg,#f4f8ff,#fff)} |
| | | .lifecycle-summary div{display:flex;align-items:center;justify-content:space-between;gap:14px}.lifecycle-summary span{color:#909399;font-size:13px}.lifecycle-summary strong{color:#303133;font-size:13px;text-align:right} |
| | | .project-timeline{padding:4px 4px 0 8px}.lifecycle-node{padding:13px 14px;border:1px solid #e4e7ed;border-radius:9px;background:#fff}.lifecycle-node.current{border-color:#9bbaf2;background:#f3f7ff}.lifecycle-node.completed{border-color:#b9dfca;background:#f4fbf7}.lifecycle-node-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.lifecycle-node-head strong{font-size:15px}.lifecycle-node p{margin:9px 0 0;color:#606266;font-size:13px;line-height:1.6} |
| | | .phase-tag.phase-not-started{color:#667085;background:#f2f4f7;border-color:#d0d5dd}.phase-tag.phase-initiation{color:#087f8c;background:#e8f8fa;border-color:#9bd8de}.phase-tag.phase-development{color:#1769e0;background:#eaf3ff;border-color:#aac9f9}.phase-tag.phase-application{color:#c65d00;background:#fff3e6;border-color:#ffc58f}.phase-tag.phase-result{color:#6941c6;background:#f4ebff;border-color:#d6bbfb}.phase-tag.phase-acceptance{color:#087a65;background:#e8f7f2;border-color:#9ad7c8}.phase-tag.phase-completed{color:#21834a;background:#eaf8ef;border-color:#a9ddb9} |
| | | </style> |
| | |
| | | <div class="header-row"> |
| | | <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-tag class="phase-tag" :class="phaseTagClass(info.currentPhase)" size="large">{{ info.currentPhase || 'æªå¼å§' }}</el-tag> |
| | | <el-button @click="router.back()">è¿å</el-button> |
| | | </div> |
| | | </div> |
| | |
| | | <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.actualStartTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®é
ç»æ">{{ info.actualEndTime || '-' }}</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-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" /> |
| | | <el-card shadow="never" class="content-card"><ProjectExecutionPanel v-if="info.id" ref="executionRef" :project-id="info.id" :project-info="info" readonly /></el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagementDetail"> |
| | | import { computed, getCurrentInstance, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { ElMessage } from 'element-plus' |
| | | 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 loading = ref(false) |
| | | const info = ref({}) |
| | | const attachments = ref([]) |
| | | 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] || '')) |
| | | function phaseTagClass(phase) { |
| | | return ({ æªå¼å§: 'phase-not-started', ç«é¡¹: 'phase-initiation', å¼å: 'phase-development', ç³æ¥: 'phase-application', ç³æ¥ç»æ: 'phase-result', éªæ¶: 'phase-acceptance', 已宿: 'phase-completed' })[phase] || 'phase-not-started' |
| | | } |
| | | |
| | | async function loadDetail() { |
| | | loading.value = true |
| | |
| | | await executionRef.value?.refresh?.() |
| | | } catch { ElMessage.error('è·å项ç®è¯¦æ
失败') } finally { loading.value = false } |
| | | } |
| | | 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> |
| | | .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; } |
| | | .phase-tag.phase-not-started{color:#667085;background:#f2f4f7;border-color:#d0d5dd}.phase-tag.phase-initiation{color:#087f8c;background:#e8f8fa;border-color:#9bd8de}.phase-tag.phase-development{color:#1769e0;background:#eaf3ff;border-color:#aac9f9}.phase-tag.phase-application{color:#c65d00;background:#fff3e6;border-color:#ffc58f}.phase-tag.phase-result{color:#6941c6;background:#f4ebff;border-color:#d6bbfb}.phase-tag.phase-acceptance{color:#087a65;background:#e8f7f2;border-color:#9ad7c8}.phase-tag.phase-completed{color:#21834a;background:#eaf8ef;border-color:#a9ddb9} |
| | | </style> |
| | |
| | | <el-table-column fixed="right" label="æä½" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">ä¸è½½</el-button> |
| | | <el-button link type="primary" size="small" @click="lookFile(scope.row)">é¢è§</el-button> |
| | | <el-button link type="danger" size="small" @click="handleDelete(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | }) |
| | | </script> |
| | | |
| | | <style></style> |
| | | <style></style> |
| | |
| | | <el-table-column fixed="right" label="æä½" width="100" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="downLoadFile(scope.row)">ä¸è½½</el-button> |
| | | <el-button link type="primary" @click="lookFile(scope.row)">é¢è§</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | }) |
| | | </script> |
| | | |
| | | <style></style> |
| | | <style></style> |
| | |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
³è项ç®" width="290" align="left"> |
| | | <template #default="scope"> |
| | | <div |
| | | v-if="scope.row.projectManagementInfoId" |
| | | class="business-project-cell" |
| | | @click="openProjectProgress(scope.row)" |
| | | > |
| | | <div class="business-project-cell__head"> |
| | | <span class="business-project-cell__name"> |
| | | {{ scope.row.linkedProjectName || scope.row.projectName || "å
³è项ç®" }} |
| | | </span> |
| | | <el-tag |
| | | size="small" |
| | | effect="light" |
| | | :type="projectPhaseTagType(scope.row.projectCurrentPhase)" |
| | | > |
| | | {{ scope.row.projectCurrentPhase || "æªå¼å§" }} |
| | | </el-tag> |
| | | </div> |
| | | <el-progress |
| | | :percentage="Number(scope.row.projectProgress || 0)" |
| | | :stroke-width="7" |
| | | /> |
| | | <div class="business-project-cell__meta"> |
| | | 模å {{ scope.row.projectCompletedModuleCount || 0 }}/{{ scope.row.projectModuleCount || 0 }}ï¼ |
| | | è´è´£äººï¼{{ scope.row.projectLeaderName || "æªè®¾ç½®" }} |
| | | </div> |
| | | </div> |
| | | <el-tag v-else type="info" effect="plain">ææªçæé¡¹ç®</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column fixed="right" label="æä½" width="500" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="openForm('view', scope.row)" |
| | |
| | | link |
| | | type="primary" |
| | | @click="openProjectProgress(scope.row)" |
| | | >æ¥ç项ç®è¿åº¦ |
| | | >é¡¹ç®æ¦åµ |
| | | </el-button> |
| | | <el-button |
| | | v-if="![1, 2].includes(scope.row.businessApprovalStatus)" |
| | |
| | | </template> |
| | | </el-table> |
| | | </el-dialog> |
| | | |
| | | <el-drawer |
| | | v-model="projectOverviewVisible" |
| | | title="å
³è项ç®è¿åº¦ä¸æ
åµ" |
| | | size="720px" |
| | | destroy-on-close |
| | | > |
| | | <div v-loading="projectOverviewLoading" class="project-overview"> |
| | | <template v-if="projectOverviewRow"> |
| | | <div class="project-overview__hero"> |
| | | <div> |
| | | <div class="project-overview__title"> |
| | | {{ projectOverviewRow.linkedProjectName || projectOverviewRow.projectName }} |
| | | </div> |
| | | <div class="project-overview__no"> |
| | | {{ projectOverviewRow.projectNo || "ææ é¡¹ç®ç¼å·" }} |
| | | </div> |
| | | </div> |
| | | <el-tag |
| | | size="large" |
| | | :type="projectPhaseTagType(projectOverviewRow.projectCurrentPhase)" |
| | | > |
| | | {{ projectOverviewRow.projectCurrentPhase || "æªå¼å§" }} |
| | | </el-tag> |
| | | </div> |
| | | |
| | | <div class="project-overview__progress-card"> |
| | | <div class="project-overview__progress-title"> |
| | | <span>é¡¹ç®æ»è¿åº¦</span> |
| | | <strong>{{ Number(projectOverviewRow.projectProgress || 0) }}%</strong> |
| | | </div> |
| | | <el-progress |
| | | :percentage="Number(projectOverviewRow.projectProgress || 0)" |
| | | :stroke-width="12" |
| | | :show-text="false" |
| | | /> |
| | | </div> |
| | | |
| | | <el-descriptions :column="2" border class="project-overview__descriptions"> |
| | | <el-descriptions-item label="项ç®è´è´£äºº"> |
| | | {{ projectOverviewRow.projectLeaderName || "æªè®¾ç½®" }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="模å宿"> |
| | | {{ projectOverviewRow.projectCompletedModuleCount || 0 }} / |
| | | {{ projectOverviewRow.projectModuleCount || 0 }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="计å卿"> |
| | | {{ formatProjectDate(projectOverviewRow.projectPlanStartTime) }} è³ |
| | | {{ formatProjectDate(projectOverviewRow.projectPlanEndTime) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å®é
卿"> |
| | | {{ formatProjectDate(projectOverviewRow.projectActualStartTime) }} è³ |
| | | {{ formatProjectDate(projectOverviewRow.projectActualEndTime) }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div class="project-overview__section-title">项ç®èç¹</div> |
| | | <div class="project-overview__nodes"> |
| | | <div |
| | | v-for="node in projectCoreLifecycleNodes" |
| | | :key="node.code" |
| | | class="project-overview__node" |
| | | :class="'is-' + String(node.status || 'PENDING').toLowerCase()" |
| | | > |
| | | <span class="project-overview__node-dot"></span> |
| | | <span class="project-overview__node-name">{{ node.name }}</span> |
| | | <span class="project-overview__node-time"> |
| | | {{ node.time ? formatProjectDate(node.time) : "å¾
è¿è¡" }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="project-overview__section-title">模åè¿åº¦</div> |
| | | <el-table :data="projectOverview.modules || []" border size="small"> |
| | | <el-table-column prop="moduleName" label="模å" min-width="150" /> |
| | | <el-table-column prop="projectWeight" label="项ç®å æ¯" width="100" align="center"> |
| | | <template #default="scope">{{ Number(scope.row.projectWeight || 0) }}%</template> |
| | | </el-table-column> |
| | | <el-table-column label="å½åè¿åº¦" min-width="170"> |
| | | <template #default="scope"> |
| | | <el-progress :percentage="Number(scope.row.currentProgress || 0)" :stroke-width="8" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="reportCount" label="æ±æ¥æ¬¡æ°" width="90" align="center" /> |
| | | </el-table> |
| | | |
| | | <div class="project-overview__section-title">æè¿å¨æ</div> |
| | | <el-timeline v-if="projectRecentActivities.length"> |
| | | <el-timeline-item |
| | | v-for="item in projectRecentActivities" |
| | | :key="item.key" |
| | | :timestamp="item.time" |
| | | placement="top" |
| | | > |
| | | <strong>{{ item.title }}</strong> |
| | | <div class="project-overview__activity-detail">{{ item.detail }}</div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="ææ è·è¿ææ±æ¥è®°å½" :image-size="70" /> |
| | | |
| | | <div class="project-overview__footer"> |
| | | <el-button type="primary" @click="openProjectDetail">è¿å
¥é¡¹ç®è¯¦æ
</el-button> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </el-drawer> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | productionPlanCombine, |
| | | productionPlanListPage, |
| | | } from "@/api/productionPlan/productionPlan.js"; |
| | | import { |
| | | getProjectWorkflow, |
| | | getProjectLifecycle, |
| | | } from "@/api/projectManagement/workflow.js"; |
| | | |
| | | const FileList = defineAsyncComponent(() => |
| | | import("@/components/Dialog/FileList.vue") |
| | |
| | | const businessActionId = ref(null); |
| | | const fileList = ref([]); |
| | | const deliveryFileList = ref([]); |
| | | const projectOverviewVisible = ref(false); |
| | | const projectOverviewLoading = ref(false); |
| | | const projectOverviewRow = ref(null); |
| | | const projectOverview = ref({ modules: [], followUps: [], reports: [] }); |
| | | const projectLifecycle = ref({ nodes: [] }); |
| | | const projectRecentActivities = computed(() => { |
| | | const follows = (projectOverview.value.followUps || []).map((item) => ({ |
| | | key: "follow-" + item.id, |
| | | time: item.followDate || item.createTime || "", |
| | | title: "项ç®è·è¿", |
| | | detail: item.detail || "已填å项ç®è·è¿", |
| | | })); |
| | | const reports = (projectOverview.value.reports || []).map((item) => ({ |
| | | key: "report-" + item.id, |
| | | time: item.reportDate || item.createTime || "", |
| | | title: (item.moduleName || "é¡¹ç®æ¨¡å") + "è¿åº¦æ±æ¥", |
| | | detail: |
| | | (item.reporterNames || "") + |
| | | " æ±æ¥è³ " + |
| | | Number(item.progress || 0) + |
| | | "%" + |
| | | (item.remark ? "ï¼" + item.remark : ""), |
| | | })); |
| | | return [...follows, ...reports] |
| | | .sort((a, b) => String(b.time).localeCompare(String(a.time))) |
| | | .slice(0, 6); |
| | | }); |
| | | const projectCoreLifecycleNodes = computed(() => |
| | | (projectLifecycle.value.nodes || []).filter((item) => !item.eventType) |
| | | ); |
| | | |
| | | // ç¨æ·ä¿¡æ¯è¡¨åå¼¹æ¡æ°æ® |
| | | const operationType = ref(""); |
| | |
| | | fileDialogVisible.value = true; |
| | | }; |
| | | |
| | | // æ¥ç项ç®è¿åº¦ï¼è·³è½¬é¡¹ç®è¯¦æ
ï¼å¯æ¥çé¶æ®µè¿å±ä¸èµææä»¶ï¼ |
| | | const openProjectProgress = (row) => { |
| | | const projectPhaseTagType = (phase) => { |
| | | const types = { |
| | | æªå¼å§: "info", |
| | | ç«é¡¹: "warning", |
| | | å¼å: "primary", |
| | | ç³æ¥: "warning", |
| | | ç³æ¥ç»æ: "success", |
| | | éªæ¶: "success", |
| | | 已宿: "success", |
| | | }; |
| | | return types[phase] || "info"; |
| | | }; |
| | | |
| | | const formatProjectDate = (value) => { |
| | | if (!value) return "æªè®°å½"; |
| | | const date = dayjs(value); |
| | | return date.isValid() ? date.format("YYYY-MM-DD") : value; |
| | | }; |
| | | |
| | | // å¨ä¸å¡ç®¡çå
æ¥çå
³è项ç®çé¶æ®µãè¿åº¦ã模ååæè¿å¨æã |
| | | const openProjectProgress = async (row) => { |
| | | const projectId = row.projectManagementInfoId || row.projectId; |
| | | if (!projectId) { |
| | | proxy.$modal.msgWarning("该ä¸å¡ææªå
³è项ç®"); |
| | | return; |
| | | } |
| | | router.push(`/projectManagement/Management/detail/${projectId}`); |
| | | projectOverviewRow.value = row; |
| | | projectOverview.value = { modules: [], followUps: [], reports: [] }; |
| | | projectLifecycle.value = { nodes: [] }; |
| | | projectOverviewVisible.value = true; |
| | | projectOverviewLoading.value = true; |
| | | try { |
| | | const [workflowRes, lifecycleRes] = await Promise.all([ |
| | | getProjectWorkflow(projectId), |
| | | getProjectLifecycle(projectId), |
| | | ]); |
| | | projectOverview.value = workflowRes.data || {}; |
| | | projectLifecycle.value = lifecycleRes.data || {}; |
| | | } finally { |
| | | projectOverviewLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const openProjectDetail = () => { |
| | | const projectId = |
| | | projectOverviewRow.value?.projectManagementInfoId || |
| | | projectOverviewRow.value?.projectId; |
| | | if (!projectId) return; |
| | | projectOverviewVisible.value = false; |
| | | router.push("/projectManagement/Management/detail/" + projectId); |
| | | }; |
| | | |
| | | // æå¼åè´§å¼¹æ¡ |
| | |
| | | } |
| | | } |
| | | |
| | | .business-project-cell { |
| | | cursor: pointer; |
| | | padding: 3px 0; |
| | | |
| | | &__head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | &__name { |
| | | overflow: hidden; |
| | | font-weight: 600; |
| | | color: #1f3b68; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | &__meta { |
| | | margin-top: 2px; |
| | | font-size: 12px; |
| | | color: #7a8699; |
| | | } |
| | | |
| | | :deep(.el-progress__text) { |
| | | min-width: 38px; |
| | | font-size: 12px !important; |
| | | } |
| | | } |
| | | |
| | | .project-overview { |
| | | padding: 0 4px 24px; |
| | | |
| | | &__hero, |
| | | &__progress-title, |
| | | &__footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | &__hero { |
| | | padding: 18px 20px; |
| | | margin-bottom: 14px; |
| | | background: linear-gradient(135deg, #eef5ff, #f7faff); |
| | | border: 1px solid #dce9fb; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | &__title { |
| | | margin-bottom: 5px; |
| | | font-size: 20px; |
| | | font-weight: 700; |
| | | color: #1f2d3d; |
| | | } |
| | | |
| | | &__no, |
| | | &__activity-detail { |
| | | font-size: 13px; |
| | | color: #7a8699; |
| | | } |
| | | |
| | | &__progress-card { |
| | | padding: 16px 20px; |
| | | margin-bottom: 14px; |
| | | background: #fff; |
| | | border: 1px solid #e5eaf1; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | &__progress-title { |
| | | margin-bottom: 10px; |
| | | |
| | | strong { |
| | | font-size: 22px; |
| | | color: var(--el-color-primary); |
| | | } |
| | | } |
| | | |
| | | &__descriptions { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | &__section-title { |
| | | padding-left: 10px; |
| | | margin: 22px 0 12px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | border-left: 4px solid var(--el-color-primary); |
| | | } |
| | | |
| | | &__nodes { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 8px; |
| | | } |
| | | |
| | | &__node { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | min-width: 0; |
| | | padding: 9px 10px; |
| | | color: #8a94a6; |
| | | background: #f7f8fa; |
| | | border: 1px solid #e8ebf0; |
| | | border-radius: 8px; |
| | | |
| | | &.is-current { |
| | | color: #1d5ec7; |
| | | background: #edf5ff; |
| | | border-color: #a9ccff; |
| | | } |
| | | |
| | | &.is-completed { |
| | | color: #168464; |
| | | background: #effaf6; |
| | | border-color: #a9dfce; |
| | | } |
| | | } |
| | | |
| | | &__node-dot { |
| | | flex: 0 0 auto; |
| | | width: 7px; |
| | | height: 7px; |
| | | background: currentColor; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | &__node-name { |
| | | overflow: hidden; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | &__node-time { |
| | | margin-left: auto; |
| | | font-size: 11px; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | &__activity-detail { |
| | | margin-top: 5px; |
| | | line-height: 1.6; |
| | | } |
| | | |
| | | &__footer { |
| | | justify-content: flex-end; |
| | | padding-top: 18px; |
| | | } |
| | | } |
| | | |
| | | @media print { |
| | | .app-container { |
| | | display: none; |