feat(modules): 新增财务、会员管理和课题管理模块
- 添加财务模块字典定义,包括报销单状态和费用类型
- 添加会员管理模块字典定义,包括入会申请状态、会员类型和等级
- 添加课题管理模块字典定义,包括课题指南类别、状态和申报书状态
- 实现报销单功能的数据结构和表单配置
- 实现课题申报书功能的数据结构和表单配置
- 实现课题指南功能的数据结构和表单配置
- 实现入会申请功能的数据结构和表单配置
- 添加课题申报书附件用于E2E测试
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 课é¢ç³æ¥ä¹¦éä»¶ï¼E2E æµè¯ï¼ |
| | | |
| | | 课é¢åç§°ï¼é¢åæºè½å·¥åçæ°ååªçå
³é®ææ¯ç ç©¶ |
| | | ç³æ¥äººï¼å¼ ä¼ |
| | | |
| | | æ¬æä»¶ç¨äºç«¯å°ç«¯éªè¯éä»¶ä¸ä¼ ä¸ç»å®æµç¨ã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | - generic [active] [ref=f34e1]: |
| | | - banner [ref=f34e2]: |
| | | - generic [ref=f34e3]: MOM |
| | | - heading "项ç®ä¸å¡å
¨æ¯" [level=1] [ref=f34e5] |
| | | - textbox "æç´¢ä¸å¡ï¼éå®è®¢å / å·¥å / IQC / åºå / 忬¾â¦" [ref=f34e7] |
| | | - navigation [ref=f34e8]: |
| | | - generic [ref=f34e9]: æ»è§ |
| | | - generic [ref=f34e10] [cursor=pointer]: é¡¹ç®æ¦è§ |
| | | - generic [ref=f34e12] [cursor=pointer]: ä¸å¡å
¨æ¯å¾ |
| | | - generic [ref=f34e14]: æ ¸å¿ä¸å¡æµç¨ |
| | | - generic [ref=f34e15] [cursor=pointer]: é宿µç¨ |
| | | - generic [ref=f34e17] [cursor=pointer]: éè´æµç¨ |
| | | - generic [ref=f34e19] [cursor=pointer]: ç产æµç¨ |
| | | - generic [ref=f34e21] [cursor=pointer]: è´¨éæµç¨ |
| | | - generic [ref=f34e23] [cursor=pointer]: ä»å¨æµç¨ |
| | | - generic [ref=f34e25] [cursor=pointer]: å®åæµç¨ |
| | | - generic [ref=f34e27] [cursor=pointer]: è´¢å¡æ¥éæµç¨ |
| | | - generic [ref=f34e29] [cursor=pointer]: ä¼åå
¥ä¼æµç¨ |
| | | - generic [ref=f34e31] [cursor=pointer]: 课é¢ç®¡çæµç¨ |
| | | - generic [ref=f34e33]: ç»æä¸æ°æ® |
| | | - generic [ref=f34e34] [cursor=pointer]: ç³»ç»å
³ç³» |
| | | - generic [ref=f34e36] [cursor=pointer]: æ°æ®æµ |
| | | - generic [ref=f34e38] [cursor=pointer]: ä¸å¡å¯¹è±¡ |
| | | - generic [ref=f34e40] [cursor=pointer]: è§è²æé |
| | | - generic [ref=f34e42]: æ©å± |
| | | - generic [ref=f34e43] [cursor=pointer]: AI ä¸å¡ |
| | | - generic [ref=f34e45] [cursor=pointer]: å¾
确认äºé¡¹ |
| | | - main [ref=f34e47] |
| | | - contentinfo [ref=f34e48] |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | - generic [active] [ref=f34e1]: |
| | | - banner [ref=f34e2]: |
| | | - generic [ref=f34e3]: MOM |
| | | - generic [ref=f34e4]: |
| | | - heading "MOM 工忰ååå¹³å°ï¼mom-pro2ï¼ Â· ä¸å¡å
¨æ¯" [level=1] [ref=f34e49] |
| | | - generic [ref=f34e50]: æ··åç³»ç» | MOM å¶é è¿è¥ç®¡çç³»ç» |
| | | - textbox "æç´¢ä¸å¡ï¼éå®è®¢å / å·¥å / IQC / åºå / 忬¾â¦" [ref=f34e7] |
| | | - navigation [ref=f34e8]: |
| | | - generic [ref=f34e9]: æ»è§ |
| | | - generic [ref=f34e10] [cursor=pointer]: é¡¹ç®æ¦è§ |
| | | - generic [ref=f34e12] [cursor=pointer]: ä¸å¡å
¨æ¯å¾ |
| | | - generic [ref=f34e14]: æ ¸å¿ä¸å¡æµç¨ |
| | | - generic [ref=f34e15] [cursor=pointer]: é宿µç¨ |
| | | - generic [ref=f34e17] [cursor=pointer]: éè´æµç¨ |
| | | - generic [ref=f34e19] [cursor=pointer]: ç产æµç¨ |
| | | - generic [ref=f34e21] [cursor=pointer]: è´¨éæµç¨ |
| | | - generic [ref=f34e23] [cursor=pointer]: ä»å¨æµç¨ |
| | | - generic [ref=f34e25] [cursor=pointer]: å®åæµç¨ |
| | | - generic [ref=f34e27] [cursor=pointer]: è´¢å¡æ¥éæµç¨ |
| | | - generic [ref=f34e29] [cursor=pointer]: ä¼åå
¥ä¼æµç¨ |
| | | - generic [ref=f34e31] [cursor=pointer]: 课é¢ç®¡çæµç¨ |
| | | - generic [ref=f34e33]: ç»æä¸æ°æ® |
| | | - generic [ref=f34e34] [cursor=pointer]: ç³»ç»å
³ç³» |
| | | - generic [ref=f34e36] [cursor=pointer]: æ°æ®æµ |
| | | - generic [ref=f34e38] [cursor=pointer]: ä¸å¡å¯¹è±¡ |
| | | - generic [ref=f34e40] [cursor=pointer]: è§è²æé |
| | | - generic [ref=f34e42]: æ©å± |
| | | - generic [ref=f34e43] [cursor=pointer]: AI ä¸å¡ |
| | | - generic [ref=f34e45] [cursor=pointer]: å¾
确认äºé¡¹ |
| | | - main [ref=f34e47]: |
| | | - generic [ref=f34e184]: |
| | | - heading "课é¢ç®¡çæµç¨" [level=2] [ref=f34e185] |
| | | - paragraph [ref=f34e186]: ç¬ç«ç§ç 模åï¼yudao-module-researchï¼ï¼å¦ä¼åå¸è¯¾é¢æåï¼éç¹/ä¸è¬åç±»ï¼â ä¼åå¨çº¿æäº¤è¯¾é¢ç³æ¥ä¹¦ â æäº¤ BPM ä¸çº§å®¡æ¹ï¼ç§ä¹¦å¤å½¢å¼å®¡æ¥ â ä¸å®¶è¯å®¡ â å¦ä¼ç«é¡¹å®¡æ¹ï¼ï¼ä¸å®¶è¯åä»¥ç»æåæç»å表记å½å¹¶ç±å端èªå¨æ±æ»å æååãæå䏿¨èç»è®º â 审æ¹éè¿çæç«é¡¹ç¼å·å¹¶åå¸ç³»ç»å
¬å â ææ¹æ¬¡ç»è®°ç»è´¹æ¨ä»è®°å½ï¼å«åè¯éä»¶ï¼ãé¢çº§äºæåè¡¥ç»é¡¹éªæ¶ä¸ææå½æ¡£ã |
| | | - generic [ref=f34e187]: |
| | | - generic [ref=f34e188]: |
| | | - generic [ref=f34e189]: 课é¢ç®¡çæµç¨ · mermaid/14-research-flow.mmd |
| | | - generic [ref=f34e190]: |
| | | - button "ï¼" [ref=f34e191] [cursor=pointer] |
| | | - button "ï¼" [ref=f34e192] [cursor=pointer] |
| | | - button "éç½®" [ref=f34e193] [cursor=pointer] |
| | | - button "å
¨å±" [ref=f34e194] [cursor=pointer] |
| | | - document [ref=f34e198]: |
| | | - generic [ref=f34e200]: |
| | | - generic [ref=f34e201]: |
| | | - paragraph [ref=f34e208]: ç«é¡¹å®¡æ¹ BPM research_topic_approve |
| | | - paragraph [ref=f34e215]: å¨çº¿ç³æ¥ |
| | | - paragraph [ref=f34e222]: è¯¾é¢æååå¸ |
| | | - generic [ref=f34e253]: |
| | | - paragraph [ref=f34e259]: 已产çç³æ¥ååç¦æ¢å é¤ |
| | | - paragraph [ref=f34e265]: publish åå¸ |
| | | - paragraph [ref=f34e271]: close æªæ¢ |
| | | - paragraph [ref=f34e277]: æåå·²å叿å¯ä½ä¸ºç³æ¥åæ |
| | | - paragraph [ref=f34e283]: delete å é¤Â·ä»
è稿 0 æ¾è¡Â·è¿å¸¦æ¸
çéä»¶ |
| | | - paragraph [ref=f34e289]: submit éå®¡æ¹æµç¨ processDefinitionKey |
| | | - paragraph [ref=f34e295]: "submit åéæ¡æ ¡éª: â æåå¿
须已åå¸ â¡ å½åæ¶é´å¿
é¡»å¨ç³æ¥èµ·æ¢æå
" |
| | | - paragraph [ref=f34e301]: ä»
审æ¹ä¸å¯å¢å æ¹ä¸å®¶è¯å |
| | | - paragraph [ref=f34e307]: å端éè¡éç® compositeScore = éé¢0.3 + æ¹æ¡0.3 + å¢é0.2 + ææ0.2 |
| | | - paragraph [ref=f34e313]: "åå主å: reviewAvgScore=å
¨é¨ä¸å®¶åå · reviewPass=ååè¾¾æ ä¸ å¤æ°ç»è®ºä¸ºæ¨èç«é¡¹" |
| | | - paragraph [ref=f34e319]: ç§ä¹¦å¤æ±æ»ç³æ¥ä¹¦ä¸è¯å®¡ç»æ |
| | | - paragraph [ref=f34e325]: éè¿ |
| | | - paragraph [ref=f34e331]: éè¿ |
| | | - paragraph [ref=f34e337]: BPM 审æ¹éè¿ |
| | | - paragraph [ref=f34e343]: BPM 审æ¹é©³å |
| | | - paragraph [ref=f34e349]: BPM 审æ¹åæ¶ |
| | | - paragraph [ref=f34e355]: 驳å |
| | | - paragraph [ref=f34e361]: ä¿®æ¹åéæ° submit |
| | | - paragraph [ref=f34e367]: publishNotice åå¸ç«é¡¹éç¥ |
| | | - paragraph [ref=f34e373]: ç»è®°æ¨æ¬¾è®°å½ research_fund_record |
| | | - paragraph [ref=f34e379]: "é颿 ¡éª: å·²ç»è®°éé¢ + æ¬æ¬¡éé¢ ä¸å¾è¶
åºç³æ¥åç³è¯·ç»è´¹Â·è¶
åºæ¶æ¥åºæ¬æ¬¡ å·²ç»è®° å计 é¢ç®å项æ°å¼" |
| | | - paragraph [ref=f34e385]: ä»
å¾
æ¨ä» 0 坿¹å¯å |
| | | - paragraph [ref=f34e391]: ç¶ææ¹ä¸ºå·²æ¨ä»æ¶å¿
须已æåè¯éä»¶ |
| | | - generic [ref=f34e392]: |
| | | - paragraph [ref=f34e399]: æ°å»ºæå·éç±»å« 1éç¹è¯¾é¢ / 2ä¸è¬è¯¾é¢ |
| | | - paragraph [ref=f34e406]: å¡«æååç§° ç³æ¥èµ·æ¢æ¶é´ æåæ£æ éä»¶ |
| | | - paragraph [ref=f34e413]: æåç¼å· GL + yyyyMMdd + 6使µæ°´(RedisÂ·æ¯æ¥éç½®) |
| | | - paragraph [ref=f34e420]: è稿 0·ä»
èç¨¿å¯æ¹å¯å |
| | | - paragraph [ref=f34e427]: æç¤ºè¯¥æåä¸å·²æ N å¼ ç³æ¥å·éå
å¤çç³æ¥å |
| | | - paragraph [ref=f34e434]: å·²åå¸ 20·åå publishTime |
| | | - paragraph [ref=f34e441]: å·²æªæ¢ 30 |
| | | - paragraph [ref=f34e448]: éæåÂ·è¦æ±å·²åå¸ ä¸å¨ç³æ¥æå
|
| | | - paragraph [ref=f34e455]: éä¼å ums_member èªå¨å¸¦åº å§å å·¥ä½åä½ èç§° çµè¯ é®ç®± |
| | | - paragraph [ref=f34e462]: 填课é¢åç§° 课é¢ç»æå ç³è¯·ç»è´¹ ç³æ¥ä¹¦æ£æ ç³æ¥éä»¶ |
| | | - paragraph [ref=f34e469]: ç³æ¥ç¼å· SB + yyyyMMdd + 6使µæ°´Â·ç±»å«åä½èªæåä¸ä¿¡ä»»åç«¯ä¼ å¼ |
| | | - paragraph [ref=f34e476]: è稿 0 |
| | | - paragraph [ref=f34e483]: 审æ¹ä¸ 15·åå processInstanceId |
| | | - paragraph [ref=f34e490]: ä»»ä¸ä¸æ»¡è¶³å³æç»æäº¤Â·æç¤ºå¸¦æååä¸å®é
ç¶ææèµ·æ¢æ¶é´ |
| | | - paragraph [ref=f34e497]: ä¸å®¶è¯å®¡æç» research_topic_review·ä¸ä½ä¸å®¶ä¸è¡ |
| | | - paragraph [ref=f34e504]: æå æåéåºéæå次 ranking |
| | | - paragraph [ref=f34e511]: ç³æ¥åè¯å®¡æ±æ»Â·å 餿å䏿¡æç»æ¶æ¸
ç©ºæ±æ» |
| | | - paragraph [ref=f34e518]: ä¸èç¹é¡ºåºÂ·å®¡æ¹æ¹æ³ä¸å®¡æ¹äººå¨æµç¨å®ä¹å
é
ç½® |
| | | - paragraph [ref=f34e525]: Activity_secretariat_review ç§ä¹¦å¤å½¢å¼å®¡æ¥ |
| | | - paragraph [ref=f34e532]: Activity_expert_review ä¸å®¶è¯å®¡ |
| | | - paragraph [ref=f34e539]: Activity_council_approve å¦ä¼ç«é¡¹å®¡æ¹ |
| | | - paragraph [ref=f34e546]: å·²ç«é¡¹ 20·çå¬å¨çæ ç«é¡¹ç¼å· LX + yyyyMMdd + 6使µæ°´ ä¸ approveTime·已æç¼å·åå¹çè·³è¿ |
| | | - paragraph [ref=f34e553]: 已驳å 35 |
| | | - paragraph [ref=f34e560]: system_notice å
¬å·æ é¢ãå
³äºå
¬å¸ç«é¡¹è¯¾é¢çéç¥ãÂ·æ£æå«è¯¾é¢å ç«é¡¹ç¼å· ç±»å« ç³æ¥äºº åä½ èµå©éé¢Â·åå noticeId·éå¤åå¸è¢«æ¦æª |
| | | - paragraph [ref=f34e567]: å¡«æ¹æ¬¡åç§° æ¨ä»éé¢ æ¨ä»æ¥æ æ¶æ¬¾åä½ æ¨ä»ç¶æ åè¯éä»¶ |
| | | - paragraph [ref=f34e574]: å¾
æ¨ä» 0 |
| | | - paragraph [ref=f34e581]: å·²æ¨ä» 20 |
| | | - paragraph [ref=f34e588]: éä»¶ recordType = research_guideline |
| | | - paragraph [ref=f34e595]: éä»¶ recordType = research_application |
| | | - paragraph [ref=f34e602]: éä»¶ recordType = research_fund_record |
| | | - paragraph [ref=f34e609]: "审æ¹ä¸å¿çå: æµç¨å®ä¹è¡¨åç±»å=èªå®ä¹è¡¨å·åå»ºè·¯å¾ /research/application/index · æ¥çè·¯å¾ /research/application/detail/approve/index.vue·æ businessKey åæ¾ç³æ¥å" |
| | | - paragraph [ref=f34e616]: "审æ¹äººåå¼: ä¸èç¹å¨æµç¨å®ä¹å
é
å¥½å®¡æ¹æ¹æ³(便¬¡å®¡æ¹)ä¸å®¡æ¹äººçç¥Â·é¨ç½²åç±ç®¡çåå¨ BPM æµç¨è®¾è®¡å¨æå¦ä¼å®é
ç»ç»è°æ´Â·æäº¤æ¶å端åªè®©éæµç¨ä¸é审æ¹äºº" |
| | | - paragraph [ref=f34e623]: "ä¸æè¾¹ç: ç»è´¹æ¨ä»åªå¨æ¬æ¨¡åè½æ¨æ¬¾è®°å½Â·ä¸è´¢å¡æ¨¡åæ æ°æ®åæ¥(ä¸å跨模å对æ¥)" |
| | | - paragraph [ref=f34e630]: "äºæèå´(æ¬å¾æªè¦ç): ç»é¡¹éªæ¶ ç»é¡¹è¯ä¹¦ ææå½æ¡£ç¥è¯åº é纳æ
åµä¸ç¤¾ä¼æçæ æ³¨" |
| | | - paragraph [ref=f34e637]: ç¬ç«æ¨¡å yudao-module-research·对 ums_member 为软å
³è(æ å¤é®)·ä»
å¤ç¨ BPM 审æ¹ãsystem å
¬åä¸éä»¶ä¸å¿ |
| | | - heading "æ¥éª¤ä¸è§è²" [level=3] [ref=f34e646] |
| | | - table [ref=f34e647]: |
| | | - rowgroup [ref=f34e648]: |
| | | - row [ref=f34e649]: |
| | | - columnheader "é¶æ®µ" [ref=f34e650] |
| | | - columnheader "å¨ä½ï¼å«çå®ç¶ææµè½¬ï¼" [ref=f34e651] |
| | | - columnheader "è§è²" [ref=f34e652] |
| | | - row [ref=f34e653]: |
| | | - cell "å叿å" [ref=f34e654] |
| | | - 'cell "æ°å»ºæåéç±»å« 1éç¹è¯¾é¢ / 2ä¸è¬è¯¾é¢ï¼å¡«ç³æ¥èµ·æ¢æ¶é´ä¸æåæ£æï¼ç¼å· GL+yyyyMMdd+6使µæ°´ï¼Redis æ¯æ¥éç½®ï¼ï¼status: 0è稿/20å·²åå¸/30å·²æªæ¢ãä»
èç¨¿å¯æ¹å¯å ï¼åå¸åå³å»ç»ï¼å·²äº§çç³æ¥åçæåç¦æ¢å é¤ï¼æç¤ºè¯¥æåä¸å·²æ N å¼ ç³æ¥åï¼" [ref=f34e655]' |
| | | - cell "å¦ä¼ç§ä¹¦å¤ã课é¢ç®¡çå" [ref=f34e656] |
| | | - row [ref=f34e657]: |
| | | - cell "å¨çº¿ç³æ¥" [ref=f34e658] |
| | | - 'cell "ä»å·²åå¸ä¸å¨ç³æ¥æå
çæåæ°å»ºç³æ¥ä¹¦ï¼éä¼åï¼ums_memberï¼èªå¨å¸¦åºå§å/å·¥ä½åä½/èç§°/èç³»çµè¯/é®ç®±ï¼å¯æ¹ï¼å¡«è¯¾é¢åç§°ã课é¢ç»æåãç³è¯·ç»è´¹ãç³æ¥ä¹¦æ£æï¼ç¼å· SB+yyyyMMdd+6使µæ°´ï¼ç±»å«åä½èªæåï¼ä¸ä¿¡ä»»åç«¯ä¼ å¼ï¼status: 0è稿" [ref=f34e659]' |
| | | - cell "课é¢ç³æ¥äººï¼ä¼åï¼ã课é¢ç®¡çå" [ref=f34e660] |
| | | - row [ref=f34e661]: |
| | | - cell "æäº¤å®¡æ¹" [ref=f34e662] |
| | | - cell "submit ä¼ id ä¸ processDefinitionKeyï¼query åæ°ï¼â status=15 审æ¹ä¸å¹¶åå processInstanceIdãæäº¤åéæ¡æ ¡éªï¼â æåå¿
须已åå¸ â¡ å½åæ¶é´å¿
é¡»å¨ç³æ¥èµ·æ¢æå
ââä»»ä¸ä¸æ»¡è¶³å³æç»ï¼æç¤ºå¸¦æååä¸å®é
ç¶ææèµ·æ¢æ¶é´ï¼ä»
è稿ä¸å·²é©³å坿¹å¯éæï¼ä»
è稿å¯å ï¼è¿å¸¦æ¸
éä»¶ï¼" [ref=f34e663] |
| | | - cell "课é¢ç³æ¥äºº" [ref=f34e664] |
| | | - row [ref=f34e665]: |
| | | - cell "ä¸å®¶è¯å®¡" [ref=f34e666] |
| | | - cell "ç³æ¥åå¤äºå®¡æ¹ä¸(15)æ¶å½å
¥ä¸å®¶è¯åæç»ï¼ä¸ä½ä¸å®¶ä¸è¡ï¼å§å/åä½/èç§° + éé¢ä»·å¼0.3/ç ç©¶æ¹æ¡0.3/å¢éåºç¡0.2/颿ææ0.2 å项åå¼ + ç»è®º + æè§ï¼ï¼åç«¯æ¯æ¬¡å¢å æ¹åéç® compositeScore å æå计ãæå æåéåºéæ rankingï¼å¹¶åå主å reviewAvgScoreï¼å
¨é¨ä¸å®¶ååï¼ä¸ reviewPassï¼ååè¾¾æ ä¸å¤æ°ç»è®ºä¸ºæ¨èç«é¡¹ï¼ï¼æç»å
¨é¨å 餿¶æ±æ»å¼æ¸
ç©ºä¸ºæªæ±æ»ï¼å项åå¼åå¨ 0~100 ä¹å¤æ¶æç¤ºæåºå
·ä½ç»´åº¦ä¸å®é
å¼" [ref=f34e667] |
| | | - cell "è¯å®¡ä¸å®¶ãå¦ä¼ç§ä¹¦å¤ã课é¢ç®¡çå" [ref=f34e668] |
| | | - row [ref=f34e669]: |
| | | - cell "ç«é¡¹å®¡æ¹" [ref=f34e670] |
| | | - cell "BPM æµç¨ research_topic_approve 顺åºèµ°ä¸èç¹ï¼ç§ä¹¦å¤å½¢å¼å®¡æ¥ â ä¸å®¶è¯å®¡ â å¦ä¼ç«é¡¹å®¡æ¹ãå®¡æ¹æ¹æ³ä¸å®¡æ¹äººå¨æµç¨å®ä¹å
é
ç½®ï¼é¨ç½²åç±ç®¡çåå¨ BPM æµç¨è®¾è®¡å¨æå¦ä¼å®é
ç»ç»è°æ´ï¼å®¡æ¹ä¸å¿å
çèªå®ä¹è¡¨ååªè¯»åæ¾ç³æ¥å + è¯å®¡æç» + æ¨ä»è®°å½ãéè¿â20 å·²ç«é¡¹ã驳åâ35 已驳åãåæ¶âéå 0 è稿" [ref=f34e671] |
| | | - cell "å¦ä¼ç§ä¹¦å¤ï¼å½¢å¼å®¡æ¥ï¼ãè¯å®¡ä¸å®¶ï¼è¯å®¡èç¹ï¼ãå¦ä¼çäºä¼ï¼ç«é¡¹å®¡æ¹ï¼" [ref=f34e672] |
| | | - row [ref=f34e673]: |
| | | - cell "ç«é¡¹ä¸éç¥" [ref=f34e674] |
| | | - cell "çå¬å¨ ResearchApplicationStatusListener å
åå审æ¹ç¶æï¼å®¡æ¹éè¿ååè° approveApplicationï¼çæç«é¡¹ç¼å· LX+yyyyMMdd+6使µæ°´ ä¸ approveTimeï¼å·²æç¼å·åå¹çè·³è¿ï¼ãç¹ãåå¸ç«é¡¹éç¥ãçæ system_notice å
¬åï¼æ é¢ãå
³äºå
¬å¸ç«é¡¹è¯¾é¢çéç¥ãï¼æ£æå«è¯¾é¢å/ç«é¡¹ç¼å·/ç±»å«/ç³æ¥äºº/åä½/èµå©éé¢ï¼ï¼åå noticeId é²éå¤ââéå¤ç¹å»æå·²å叿¦æªå¹¶åç¥ä¸æ¬¡å叿¶é´" [ref=f34e675] |
| | | - cell "课é¢ç®¡çåãå¦ä¼ç§ä¹¦å¤" [ref=f34e676] |
| | | - row [ref=f34e677]: |
| | | - cell "ç»è´¹æ¨ä»" [ref=f34e678] |
| | | - 'cell "ä»
ãå·²ç«é¡¹(20)ãçç³æ¥åå¯ç»è®°æ¨æ¬¾è®°å½ï¼æ¹æ¬¡åç§°/æ¨ä»éé¢/æ¨ä»æ¥æ/æ¶æ¬¾åä½/ç¶æ/夿³¨/åè¯éä»¶ï¼ï¼é颿 ¡éªï¼å·²ç»è®°éé¢ + æ¬æ¬¡éé¢ ä¸å¾è¶
åºè¯¥åç³è¯·ç»è´¹ï¼è¶
åºæ¶æç¤ºæ¥åºæ¬æ¬¡ãå·²ç»è®°ãå计ãé¢ç®å项æ°å¼ï¼status: 0å¾
æ¨ä»/20å·²æ¨ä»ï¼åè¯æç»èµ° system éä»¶ä¸å¿ recordType=research_fund_recordï¼ç¶ææ¹ä¸ºå·²æ¨ä»æ¶åè¯å¿
å¡«ï¼ä»
å¾
æ¨ä»ç¶æå¯æ¹å¯å ï¼ç¼è¾æ¶ä¼ææ¬è¡éé¢å åä½é¢æ¾ç¤º" [ref=f34e679]' |
| | | - cell "å¦ä¼è´¢å¡ã课é¢ç®¡çå" [ref=f34e680] |
| | | - contentinfo [ref=f34e48]: åºäº dev_business 忝 87bf4a99 æºç ï¼6 ç»å¹¶è¡æºç 走读ï¼MES ç产质é / MES ä»å¨ / ERP / CRM+SRM / å®å+HRM / AI+BI+MDM+BPM+åºåº§ï¼ï¼2026-09-28 å¢éè¡¥å
yudao-module-finance ç¬ç«è´¢å¡æ¥é模åï¼æ¥éå + æç» + å票éä»¶ + BPM å®¡æ¹ + 彿¡£ï¼ä¸ yudao-module-ums ç¬ç«ä¼åç®¡çæ¨¡åï¼å
¥ä¼ç³è¯· + BPM ä¸¤çº§å®¡æ¹ + 审æ¹éè¿èªå¨å»ºä¼åä¸ç»å½è´¦å· + ä¼åå表/é讯å½ï¼ï¼2026-09-28 å¢éï¼BPM æµç¨å®ä¹ ums_membership_approveï¼å
¥ä¼å®¡æ¹ï¼å·²å¨ mom-jssm é¨ç½²å¹¶å¹¶å
¥ config_export_all é¶æ®µä¸ï¼åç±» ums_membership_approveï¼ç§ä¹¦å¤å审 â çäºä¼å®¡è®®ï¼ä¸¤èç¹å®¡æ¹äººå为ãè§è²ãçç¥ãè§è²=è¶
级管çå(1) å
åºï¼è¡¨åç±»å=èªå®ä¹è¡¨åï¼æ¥çè·¯å¾ /ums/application/detail/approve/index.vueï¼ï¼åæ¶ä¿®å¤ mom-jssm ç± CREATE TABLE ... LIKE å
é导è´ç 47 个æå ruoyi-vue-pro ç act_*/flw_* è·¨åºå¤é®ï¼é建为æå mom-jssm èªèº«ï¼å¤å¿è¡é¢æ£ä¸º 0ï¼ï¼2026-09-28 å¢éï¼æ°å¢ yudao-module-research ç¬ç«ç§ç è¯¾é¢æ¨¡å䏿ï¼è¯¾é¢æååå¸ + å¨çº¿ç³æ¥ + ä¸å®¶è¯å®¡æç» + BPM ä¸çº§ç«é¡¹å®¡æ¹ + ç«é¡¹éç¥å
¬å + ç»è´¹æ¨ä»è®°å½ï¼4 å¼ research_* 表 / 5 个åå
¸ / bpm_category id=8002 / æµç¨ research_topic_approve / èå 1075510~1075553ï¼ï¼å¹¶å¹¶å
¥ config_export_all_20260928 é¶æ®µä¸ï¼ç³æ¥äººå ums_memberï¼è½¯å
³èï¼ãç«é¡¹éç¥å¤ç¨ system_notice å
¬åãæ¨æ¬¾åè¯èµ° system éä»¶ä¸å¿ recordType=research_fund_record · çæäº 2026-09-28 · ç»´æ¤è¯·èµ°æè½ project-business-analysis |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace FinanceReimbursementApi { |
| | | /** æ¥éæç»è¡ */ |
| | | export interface ReimbursementItem { |
| | | id?: number; |
| | | reimbursementId?: number; |
| | | expenseType: number; // è´¹ç¨ç±»åï¼åå
¸ finance_expense_type |
| | | invoiceNo?: string; // å票å·ç |
| | | invoiceAmount: number; // å票éé¢ |
| | | invoiceDate?: string; // åç¥¨æ¥æ yyyy-MM-dd |
| | | drawer?: string; // å¼ç¥¨æ¹ |
| | | description?: string; // è´¹ç¨è¯´æ |
| | | } |
| | | |
| | | /** æ¥éåä¿¡æ¯ */ |
| | | export interface Reimbursement { |
| | | id?: number; |
| | | no?: string; // æ¥éåå·ï¼åç«¯çæ |
| | | title: string; // æ¥éæ é¢ |
| | | totalAmount: number; // æ¥éæ»éé¢ |
| | | status?: number; // ç¶æï¼åå
¸ finance_reimbursement_status |
| | | applicantId?: number; // ç³è¯·äºº ID |
| | | applicantName?: string; // ç³è¯·äººå§å |
| | | applyDate: string; // ç³è¯·æ¥æ yyyy-MM-dd |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ ID |
| | | archiveTime?: string; // 彿¡£æ¶é´ |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 ID |
| | | creatorName?: string; // å建人å§å |
| | | createTime?: string; // å建æ¶é´ |
| | | updateTime?: string; // æ´æ°æ¶é´ |
| | | items?: ReimbursementItem[]; // æ¥éæç» |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; // å票éä»¶ |
| | | hasAttachment?: boolean; // æ¯å¦æå票éä»¶ |
| | | blobIds?: number[]; // æäº¤æ¶çéä»¶ ID å表ï¼å
¨éï¼ |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨å®ä¹ */ |
| | | export interface ProcessDefinition { |
| | | id: string; |
| | | key: string; |
| | | name: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥éåå页 */ |
| | | export function getReimbursementPage(params: PageParam) { |
| | | return requestClient.get<PageResult<FinanceReimbursementApi.Reimbursement>>( |
| | | '/finance/reimbursement/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥éå详æ
*/ |
| | | export function getReimbursement(id: number) { |
| | | return requestClient.get<FinanceReimbursementApi.Reimbursement>( |
| | | `/finance/reimbursement/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢æ¥éå */ |
| | | export function createReimbursement( |
| | | data: FinanceReimbursementApi.Reimbursement, |
| | | ) { |
| | | return requestClient.post('/finance/reimbursement/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¥éåï¼ä»
è稿ã已驳å坿¹ï¼ */ |
| | | export function updateReimbursement( |
| | | data: FinanceReimbursementApi.Reimbursement, |
| | | ) { |
| | | return requestClient.put('/finance/reimbursement/update', data); |
| | | } |
| | | |
| | | /** å 餿¥éåï¼ä»
è稿å¯å ï¼ */ |
| | | export function deleteReimbursement(id: number) { |
| | | return requestClient.delete('/finance/reimbursement/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æäº¤æ¥éåå®¡æ¹ */ |
| | | export function submitReimbursement(id: number, processDefinitionKey: string) { |
| | | return requestClient.post('/finance/reimbursement/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** 彿¡£æ¥éåï¼ä»
å·²éè¿å¯å½æ¡£ï¼ */ |
| | | export function archiveReimbursement(id: number) { |
| | | return requestClient.post('/finance/reimbursement/archive', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åæ¥éåå®¡æ¹æµç¨å表 */ |
| | | export function getReimbursementApproveProcessList() { |
| | | return requestClient.get<FinanceReimbursementApi.ProcessDefinition[]>( |
| | | '/finance/reimbursement/approve-process-list', |
| | | ); |
| | | } |
| | | |
| | | /** å¯¼åºæ¥éå Excel */ |
| | | export function exportReimbursement(params: any) { |
| | | return requestClient.download('/finance/reimbursement/export-excel', { |
| | | params, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ResearchApplicationApi { |
| | | /** 课é¢ç³æ¥ä¹¦ */ |
| | | export interface Application { |
| | | id?: number; |
| | | no?: string; // ç³æ¥ç¼å·ï¼å端çæï¼SB åç¼ï¼ |
| | | guidelineId?: number; // æå±è¯¾é¢æå ID |
| | | guidelineTitle?: string; // æå±è¯¾é¢æååç§° |
| | | title: string; // 课é¢åç§° |
| | | category?: number; // 课é¢ç±»å«ï¼åç«¯èªæååä½ï¼å端åªè¯»ï¼ |
| | | memberId?: number; // ç³æ¥ä¼å ID |
| | | applicantName: string; // ç³æ¥äººå§å/åä½åç§° |
| | | applicantUserId?: number; // ç³æ¥ç»å½ç¨æ· ID |
| | | workUnit?: string; // å·¥ä½åä½ |
| | | jobTitle?: string; // èç§° |
| | | contactPhone?: string; // èç³»çµè¯ |
| | | contactEmail?: string; // èç³»é®ç®± |
| | | teamMembers?: string; // 课é¢ç»æåï¼æ¯è¡ä¸æ¡ï¼å§å|åä½|èç§°ï¼ |
| | | budgetAmount?: number; // ç³è¯·ç»è´¹ï¼å
ï¼ |
| | | planContent?: string; // ç³æ¥ä¹¦æ£æ |
| | | status?: number; // ç¶æï¼åå
¸ research_application_status |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ ID |
| | | reviewAvgScore?: number; // ä¸å®¶è¯å®¡å æååï¼åç«¯æ±æ»ï¼ |
| | | reviewPass?: number; // è¯å®¡ç»è®ºï¼0-æªéè¿ 1-æ¨èç«é¡¹ï¼åç«¯æ±æ»ï¼ |
| | | researchNo?: string; // ç«é¡¹ç¼å·ï¼å®¡æ¹éè¿åçæ |
| | | approveTime?: string; // ç«é¡¹æ¶é´ |
| | | noticeId?: number; // å·²åå¸çç«é¡¹éç¥å
Œ ID |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 ID |
| | | creatorName?: string; // å建人å§å |
| | | createTime?: string; // å建æ¶é´ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; // éä»¶å表 |
| | | hasAttachment?: boolean; // æ¯å¦æéä»¶ |
| | | blobIds?: number[]; // æäº¤æ¶çéä»¶ ID å表ï¼å
¨éï¼ |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨å®ä¹ */ |
| | | export interface ProcessDefinition { |
| | | id: string; |
| | | key: string; |
| | | name: string; |
| | | } |
| | | |
| | | /** ä¸å®¶è¯å®¡æç» */ |
| | | export interface TopicReview { |
| | | id?: number; |
| | | applicationId?: number; // 课é¢ç³æ¥ä¹¦ ID |
| | | evaluatorName: string; // ä¸å®¶å§å |
| | | evaluatorUnit?: string; // ä¸å®¶æå¨åä½ |
| | | evaluatorTitle?: string; // ä¸å®¶èç§° |
| | | topicScore?: number; // éé¢ä»·å¼ä¸æä¹å¾åï¼æé 0.3ï¼ |
| | | planScore?: number; // ç ç©¶æ¹æ¡å¯è¡æ§å¾åï¼æé 0.3ï¼ |
| | | teamScore?: number; // å¢éä¸ç ç©¶åºç¡å¾åï¼æé 0.2ï¼ |
| | | resultScore?: number; // 颿ææä¸æçå¾åï¼æé 0.2ï¼ |
| | | compositeScore?: number; // å æå计å¾åï¼å端计ç®ï¼ |
| | | ranking?: number; // æ¬ç³æ¥åå
è¯åæåï¼å端计ç®ï¼ |
| | | conclusion?: number; // è¯å®¡ç»è®ºï¼åå
¸ research_review_conclusion |
| | | opinion?: string; // è¯å®¡æè§ |
| | | reviewTime?: string; // è¯å®¡æ¶é´ |
| | | creatorName?: string; // å½å
¥äººå§å |
| | | createTime?: string; // å建æ¶é´ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è¯¾é¢ç³æ¥ä¹¦å页 */ |
| | | export function getApplicationPage(params: PageParam) { |
| | | return requestClient.get<PageResult<ResearchApplicationApi.Application>>( |
| | | '/research/application/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è¯¾é¢ç³æ¥ä¹¦è¯¦æ
*/ |
| | | export function getApplication(id: number) { |
| | | return requestClient.get<ResearchApplicationApi.Application>( |
| | | `/research/application/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è¯¾é¢ç³æ¥ä¹¦ï¼èç¨¿ï¼ */ |
| | | export function createApplication(data: ResearchApplicationApi.Application) { |
| | | return requestClient.post('/research/application/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹è¯¾é¢ç³æ¥ä¹¦ï¼ä»
è稿ã已驳å坿¹ï¼ */ |
| | | export function updateApplication(data: ResearchApplicationApi.Application) { |
| | | return requestClient.put('/research/application/update', data); |
| | | } |
| | | |
| | | /** å é¤è¯¾é¢ç³æ¥ä¹¦ï¼ä»
è稿å¯å ï¼ */ |
| | | export function deleteApplication(id: number) { |
| | | return requestClient.delete('/research/application/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æäº¤è¯¾é¢ç³æ¥ä¹¦å®¡æ¹ */ |
| | | export function submitApplication(id: number, processDefinitionKey: string) { |
| | | return requestClient.post('/research/application/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·å课é¢ç«é¡¹å®¡æ¹æµç¨å表 */ |
| | | export function getApplicationApproveProcessList() { |
| | | return requestClient.get<ResearchApplicationApi.ProcessDefinition[]>( |
| | | '/research/application/approve-process-list', |
| | | ); |
| | | } |
| | | |
| | | /** åå¸ç«é¡¹éç¥ */ |
| | | export function publishApplicationNotice(id: number) { |
| | | return requestClient.post('/research/application/publish-notice', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥è¯¢æç³æ¥ä¹¦çä¸å®¶è¯å®¡æç»å表 */ |
| | | export function getTopicReviewList(applicationId: number) { |
| | | return requestClient.get<ResearchApplicationApi.TopicReview[]>( |
| | | '/research/topic-review/list', |
| | | { params: { applicationId } }, |
| | | ); |
| | | } |
| | | |
| | | /** å½å
¥ä¸å®¶è¯å®¡æç» */ |
| | | export function createTopicReview(data: ResearchApplicationApi.TopicReview) { |
| | | return requestClient.post('/research/topic-review/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹ä¸å®¶è¯å®¡æç» */ |
| | | export function updateTopicReview(data: ResearchApplicationApi.TopicReview) { |
| | | return requestClient.put('/research/topic-review/update', data); |
| | | } |
| | | |
| | | /** å é¤ä¸å®¶è¯å®¡æç» */ |
| | | export function deleteTopicReview(id: number) { |
| | | return requestClient.delete('/research/topic-review/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ResearchFundRecordApi { |
| | | /** ç»è´¹æ¨ä»è®°å½ */ |
| | | export interface FundRecord { |
| | | id?: number; |
| | | applicationId: number; // å
³è课é¢ç³æ¥ä¹¦ ID |
| | | batchName?: string; // æ¨ä»æ¹æ¬¡ï¼å¦ãç¬¬ä¸æã |
| | | amount: number; // æ¨ä»éé¢ï¼å
ï¼ |
| | | payDate?: string; // æ¨ä»æ¥æ |
| | | payee?: string; // æ¶æ¬¾åä½/è´¦æ·å |
| | | status?: number; // æ¨ä»ç¶æï¼åå
¸ research_fund_status |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 ID |
| | | creatorName?: string; // å建人å§å |
| | | createTime?: string; // å建æ¶é´ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; // åè¯éä»¶å表 |
| | | hasAttachment?: boolean; // æ¯å¦æåè¯éä»¶ |
| | | blobIds?: number[]; // æäº¤æ¶çåè¯éä»¶ ID å表ï¼å
¨éï¼ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æè¯¾é¢ç³æ¥ä¹¦çç»è´¹æ¨ä»è®°å½å表 */ |
| | | export function getFundRecordList(applicationId: number) { |
| | | return requestClient.get<ResearchFundRecordApi.FundRecord[]>( |
| | | '/research/fund-record/list', |
| | | { params: { applicationId } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢ç»è´¹æ¨ä»è®°å½ */ |
| | | export function createFundRecord(data: ResearchFundRecordApi.FundRecord) { |
| | | return requestClient.post('/research/fund-record/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹ç»è´¹æ¨ä»è®°å½ï¼ä»
å¾
æ¨ä»å¯æ¹ï¼ */ |
| | | export function updateFundRecord(data: ResearchFundRecordApi.FundRecord) { |
| | | return requestClient.put('/research/fund-record/update', data); |
| | | } |
| | | |
| | | /** å é¤ç»è´¹æ¨ä»è®°å½ï¼ä»
å¾
æ¨ä»å¯å ï¼ */ |
| | | export function deleteFundRecord(id: number) { |
| | | return requestClient.delete('/research/fund-record/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace ResearchGuidelineApi { |
| | | /** è¯¾é¢æå */ |
| | | export interface Guideline { |
| | | id?: number; |
| | | no?: string; // æåç¼å·ï¼å端çæï¼GL åç¼ï¼ |
| | | title: string; // æååç§° |
| | | category: number; // 课é¢ç±»å«ï¼åå
¸ research_guideline_category |
| | | applyStartTime?: string; // ç³æ¥å¼å§æ¶é´ |
| | | applyEndTime?: string; // ç³æ¥æªæ¢æ¶é´ |
| | | content?: string; // æåæ£æ |
| | | publishStatus?: number; // åå¸ç¶æï¼åå
¸ research_guideline_status |
| | | publishTime?: string; // å叿¶é´ |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 ID |
| | | creatorName?: string; // å建人å§å |
| | | createTime?: string; // å建æ¶é´ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; // éä»¶å表 |
| | | hasAttachment?: boolean; // æ¯å¦æéä»¶ |
| | | blobIds?: number[]; // æäº¤æ¶çéä»¶ ID å表ï¼å
¨éï¼ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢è¯¾é¢æåå页 */ |
| | | export function getGuidelinePage(params: PageParam) { |
| | | return requestClient.get<PageResult<ResearchGuidelineApi.Guideline>>( |
| | | '/research/guideline/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢è¯¾é¢æå详æ
*/ |
| | | export function getGuideline(id: number) { |
| | | return requestClient.get<ResearchGuidelineApi.Guideline>( |
| | | `/research/guideline/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å·²åå¸çè¯¾é¢æåå表ï¼ä¾ç³æ¥éæåï¼ */ |
| | | export function getPublishedGuidelineList() { |
| | | return requestClient.get<ResearchGuidelineApi.Guideline[]>( |
| | | '/research/guideline/published-list', |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è¯¾é¢æåï¼èç¨¿ï¼ */ |
| | | export function createGuideline(data: ResearchGuidelineApi.Guideline) { |
| | | return requestClient.post('/research/guideline/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹è¯¾é¢æåï¼ä»
èç¨¿å¯æ¹ï¼ */ |
| | | export function updateGuideline(data: ResearchGuidelineApi.Guideline) { |
| | | return requestClient.put('/research/guideline/update', data); |
| | | } |
| | | |
| | | /** å é¤è¯¾é¢æåï¼ä»
èç¨¿ä¸æ ç³æ¥ä¹¦å¯å ï¼ */ |
| | | export function deleteGuideline(id: number) { |
| | | return requestClient.delete('/research/guideline/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** åå¸è¯¾é¢æå */ |
| | | export function publishGuideline(id: number) { |
| | | return requestClient.put('/research/guideline/publish', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æªæ¢è¯¾é¢æå */ |
| | | export function closeGuideline(id: number) { |
| | | return requestClient.put('/research/guideline/close', null, { |
| | | params: { id }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace UmsApplicationApi { |
| | | /** å
¥ä¼ç³è¯· */ |
| | | export interface Application { |
| | | id?: number; |
| | | no?: string; // ç³è¯·ç¼å·ï¼åç«¯çæ |
| | | memberType: number; // ä¼åç±»åï¼åå
¸ ums_member_type |
| | | applicantName: string; // ç³è¯·äººå§åï¼ä¸ªäººï¼æåä½åç§°ï¼åä½ï¼ |
| | | contactPhone?: string; // èç³»çµè¯ |
| | | contactEmail?: string; // é®ç®± |
| | | legalPerson?: string; // æ³å®ä»£è¡¨äººï¼åä½ä¸æï¼ |
| | | organizationCode?: string; // ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ï¼åä½ä¸æï¼ |
| | | researchDirection?: string; // ç ç©¶æ¹å |
| | | workUnit?: string; // å·¥ä½åä½ï¼ä¸ªäººä¸æï¼ |
| | | title?: string; // èç§°ï¼ä¸ªäººä¸æï¼ |
| | | memberGrade?: string; // ç³è¯·ççº§ï¼æ memberType åä¸ååå
¸ï¼ |
| | | regionCode?: string; // å°åºç¼ç |
| | | industry?: string; // è¡ä¸ï¼åå
¸ ums_industry |
| | | status?: number; // ç¶æï¼åå
¸ ums_application_status |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ ID |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 ID |
| | | creatorName?: string; // å建人å§å |
| | | createTime?: string; // å建æ¶é´ |
| | | updateTime?: string; // æ´æ°æ¶é´ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; // éä»¶å表 |
| | | hasAttachment?: boolean; // æ¯å¦æéä»¶ |
| | | blobIds?: number[]; // æäº¤æ¶çéä»¶ ID å表ï¼å
¨éï¼ |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨å®ä¹ */ |
| | | export interface ProcessDefinition { |
| | | id: string; |
| | | key: string; |
| | | name: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å
¥ä¼ç³è¯·å页 */ |
| | | export function getApplicationPage(params: PageParam) { |
| | | return requestClient.get<PageResult<UmsApplicationApi.Application>>( |
| | | '/ums/application/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å
¥ä¼ç³è¯·è¯¦æ
*/ |
| | | export function getApplication(id: number) { |
| | | return requestClient.get<UmsApplicationApi.Application>( |
| | | `/ums/application/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢å
¥ä¼ç³è¯· */ |
| | | export function createApplication(data: UmsApplicationApi.Application) { |
| | | return requestClient.post('/ums/application/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹å
¥ä¼ç³è¯·ï¼ä»
è稿ã已驳å坿¹ï¼ */ |
| | | export function updateApplication(data: UmsApplicationApi.Application) { |
| | | return requestClient.put('/ums/application/update', data); |
| | | } |
| | | |
| | | /** å é¤å
¥ä¼ç³è¯·ï¼ä»
è稿å¯å ï¼ */ |
| | | export function deleteApplication(id: number) { |
| | | return requestClient.delete('/ums/application/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | /** æäº¤å
¥ä¼ç³è¯·å®¡æ¹ */ |
| | | export function submitApplication(id: number, processDefinitionKey: string) { |
| | | return requestClient.post('/ums/application/submit', null, { |
| | | params: { id, processDefinitionKey }, |
| | | }); |
| | | } |
| | | |
| | | /** è·åå
¥ä¼ç³è¯·å®¡æ¹æµç¨å表 */ |
| | | export function getApplicationApproveProcessList() { |
| | | return requestClient.get<UmsApplicationApi.ProcessDefinition[]>( |
| | | '/ums/application/approve-process-list', |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace UmsMemberApi { |
| | | /** ä¼å */ |
| | | export interface Member { |
| | | id?: number; |
| | | no?: string; // ä¼åç¼å·ï¼åç«¯çæ |
| | | userId?: number; // å
³èç³»ç»ç¨æ· ID |
| | | username?: string; // å
³èç¨æ·åï¼æµç§°ï¼ |
| | | applicationId?: number; // æ¥æºç³è¯·å ID |
| | | memberType: number; // ä¼åç±»åï¼åå
¸ ums_member_type |
| | | memberGrade: string; // ä¼åççº§ï¼æ memberType åä¸ååå
¸ï¼ |
| | | name: string; // å§åï¼ä¸ªäººï¼æåä½åç§°ï¼åä½ï¼ |
| | | contactUser?: string; // è系人ï¼åä½ç¨ï¼ |
| | | contactPhone?: string; // èç³»çµè¯ |
| | | contactEmail?: string; // é®ç®± |
| | | legalPerson?: string; // æ³å®ä»£è¡¨äººï¼åä½ä¸æï¼ |
| | | organizationCode?: string; // ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç |
| | | researchDirection?: string; // ç ç©¶æ¹å |
| | | workUnit?: string; // å·¥ä½åä½ï¼ä¸ªäººä¸æï¼ |
| | | title?: string; // èç§°ï¼ä¸ªäººä¸æï¼ |
| | | unitIntro?: string; // åä½ç®ä»ï¼åä½ä¸æï¼ |
| | | regionCode?: string; // å°åºç¼ç |
| | | industry?: string; // è¡ä¸ï¼åå
¸ ums_industry |
| | | joinDate?: string; // å
¥ä¼æ¥æ |
| | | expireDate?: string; // æææè³ |
| | | status?: number; // ç¶æï¼åå
¸ ums_member_status |
| | | remark?: string; // 夿³¨ |
| | | createTime?: string; // å建æ¶é´ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; // éä»¶å表 |
| | | hasAttachment?: boolean; // æ¯å¦æéä»¶ |
| | | blobIds?: number[]; // æäº¤æ¶çéä»¶ ID å表ï¼å
¨éï¼ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢ä¼åé讯å½å页 */ |
| | | export function getMemberPage(params: PageParam) { |
| | | return requestClient.get<PageResult<UmsMemberApi.Member>>('/ums/member/page', { |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** æ¥è¯¢ä¼å详æ
*/ |
| | | export function getMember(id: number) { |
| | | return requestClient.get<UmsMemberApi.Member>(`/ums/member/get?id=${id}`); |
| | | } |
| | | |
| | | /** éè¿ç¨æ· ID æ¥è¯¢ä¼å */ |
| | | export function getMemberByUserId(userId: number) { |
| | | return requestClient.get<UmsMemberApi.Member>( |
| | | `/ums/member/get-by-user-id?userId=${userId}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢ä¼å */ |
| | | export function createMember(data: UmsMemberApi.Member) { |
| | | return requestClient.post('/ums/member/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹ä¼å */ |
| | | export function updateMember(data: UmsMemberApi.Member) { |
| | | return requestClient.put('/ums/member/update', data); |
| | | } |
| | | |
| | | /** å é¤ä¼å */ |
| | | export function deleteMember(id: number) { |
| | | return requestClient.delete('/ums/member/delete', { |
| | | params: { id }, |
| | | }); |
| | | } |
| | |
| | | ERP_STOCK_RECORD_BIZ_TYPE: 'erp_stock_record_biz_type', // åºåæç»çä¸å¡ç±»å |
| | | } as const; |
| | | |
| | | /** ========== FINANCE - è´¢å¡æ¨¡å ========== */ |
| | | const FINANCE_DICT = { |
| | | FINANCE_REIMBURSEMENT_STATUS: 'finance_reimbursement_status', // æ¥éåç¶æ |
| | | FINANCE_EXPENSE_TYPE: 'finance_expense_type', // è´¹ç¨ç±»å |
| | | } as const; |
| | | |
| | | /** ========== UMS - ä¼åç®¡çæ¨¡å ========== */ |
| | | const UMS_DICT = { |
| | | UMS_APPLICATION_STATUS: 'ums_application_status', // å
¥ä¼ç³è¯·ç¶æ |
| | | UMS_MEMBER_TYPE: 'ums_member_type', // ä¼åç±»å |
| | | UMS_MEMBER_STATUS: 'ums_member_status', // ä¼åç¶æ |
| | | UMS_PERSONAL_GRADE: 'ums_personal_grade', // 个人ä¼åç级 |
| | | UMS_UNIT_GRADE: 'ums_unit_grade', // åä½ä¼åç级 |
| | | UMS_INDUSTRY: 'ums_industry', // ä¼åè¡ä¸ |
| | | } as const; |
| | | |
| | | /** ========== RESEARCH - 课é¢ç®¡ç模å ========== */ |
| | | const RESEARCH_DICT = { |
| | | RESEARCH_GUIDELINE_CATEGORY: 'research_guideline_category', // è¯¾é¢æåç±»å«ï¼éç¹/ä¸è¬ï¼ |
| | | RESEARCH_GUIDELINE_STATUS: 'research_guideline_status', // è¯¾é¢æååå¸ç¶æ |
| | | RESEARCH_APPLICATION_STATUS: 'research_application_status', // 课é¢ç³æ¥ä¹¦ç¶æ |
| | | RESEARCH_REVIEW_CONCLUSION: 'research_review_conclusion', // ä¸å®¶è¯å®¡ç»è®º |
| | | RESEARCH_FUND_STATUS: 'research_fund_status', // ç»è´¹æ¨ä»ç¶æ |
| | | } as const; |
| | | |
| | | /** ========== AI - 人工æºè½æ¨¡å ========== */ |
| | | const AI_DICT = { |
| | | AI_GENERATE_MODE: 'ai_generate_mode', // AI çææ¨¡å¼ |
| | |
| | | ...BPM_DICT, |
| | | ...CRM_DICT, |
| | | ...ERP_DICT, |
| | | ...FINANCE_DICT, |
| | | ...HRM_DICT, |
| | | ...INFRA_DICT, |
| | | ...IM_DICT, |
| | | ...IOT_DICT, |
| | | ...MES_DICT, |
| | | ...MDM_DICT, |
| | | ...UMS_DICT, |
| | | ...RESEARCH_DICT, |
| | | ...WMS_DICT, |
| | | ...MEMBER_DICT, |
| | | ...MP_DICT, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { FinanceReimbursementApi } from '#/api/finance/reimbursement'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { erpPriceInputFormatter } from '@vben/utils'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** æ¥éåç¶æï¼å¯¹åºåå
¸ finance_reimbursement_statusï¼ */ |
| | | export const ReimbursementStatus = { |
| | | DRAFT: 0, // è稿 |
| | | APPROVING: 15, // 审æ¹ä¸ |
| | | APPROVED: 20, // å·²éè¿ |
| | | REJECTED: 35, // 已驳å |
| | | ARCHIVED: 40, // 已彿¡£ |
| | | } as const; |
| | | |
| | | /** 表åçé
置项 */ |
| | | export function useFormSchema(formType: FormType): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æ¥éåå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ç空åç±ç³»ç»èªå¨çæ', |
| | | maxlength: 36, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æ¥éæ é¢', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¥éæ é¢', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'applyDate', |
| | | label: 'ç³è¯·æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç³è¯·æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'totalAmount', |
| | | label: 'æ¥éæ»éé¢', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | placeholder: 'ç±æç»é颿±æ»ï¼å¯æå¨ä¿®æ¹', |
| | | precision: 2, |
| | | formatter: erpPriceInputFormatter, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'å票éä»¶', |
| | | component: 'FileUpload', |
| | | // 注æï¼valueKey 为 idï¼æäº¤æ¶ formApi 伿æä»¶å¯¹è±¡æå为 blobId æ°ç» |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 20, |
| | | multiple: true, |
| | | showDescription: formType !== 'detail', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'items', |
| | | label: 'æ¥éæç»', |
| | | component: 'Input', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ¥éæç»è¡¨æ ¼åï¼æç»éä¸»è¡¨ä¸æ¬¡æ§æäº¤ï¼å端å¯å¢å è¡ï¼ */ |
| | | export function useItemColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'seq', title: 'åºå·', width: 55, fixed: 'left' }, |
| | | { |
| | | field: 'expenseType', |
| | | title: 'è´¹ç¨ç±»å', |
| | | minWidth: 120, |
| | | slots: { default: 'expenseType' }, |
| | | }, |
| | | { |
| | | field: 'invoiceNo', |
| | | title: 'å票å·ç ', |
| | | minWidth: 160, |
| | | slots: { default: 'invoiceNo' }, |
| | | }, |
| | | { |
| | | field: 'invoiceAmount', |
| | | title: 'å票éé¢', |
| | | minWidth: 130, |
| | | slots: { default: 'invoiceAmount' }, |
| | | }, |
| | | { |
| | | field: 'invoiceDate', |
| | | title: 'åç¥¨æ¥æ', |
| | | minWidth: 140, |
| | | slots: { default: 'invoiceDate' }, |
| | | }, |
| | | { |
| | | field: 'drawer', |
| | | title: 'å¼ç¥¨æ¹', |
| | | minWidth: 180, |
| | | slots: { default: 'drawer' }, |
| | | }, |
| | | { |
| | | field: 'description', |
| | | title: 'è´¹ç¨è¯´æ', |
| | | minWidth: 180, |
| | | slots: { default: 'description' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 80, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * æç»è¡æå½±ä¸ºå¯æäº¤å¯¹è±¡ï¼å段ç½ååï¼ä¸**ä¸å¾ä¸å¸¦ id / reimbursementId**ã |
| | | * å端ä¿åæç»æ¯ãå
å åå¢ãå
¨éè¦çï¼è½¯å æ§è¡ï¼ï¼è¥åä¼ å id ä¼ä¸å·²è½¯å è¡ä¸»é®å²çªã |
| | | * åæ¾ä¸æäº¤ä¸¤æ¡è·¯å¾é½å¿
é¡»ç»è¿æ¤å½æ°ã |
| | | */ |
| | | export function toSubmitItems( |
| | | items?: FinanceReimbursementApi.ReimbursementItem[], |
| | | ): FinanceReimbursementApi.ReimbursementItem[] { |
| | | return (items ?? []).map((item) => ({ |
| | | expenseType: item.expenseType, |
| | | invoiceNo: item.invoiceNo, |
| | | invoiceAmount: item.invoiceAmount, |
| | | invoiceDate: item.invoiceDate, |
| | | drawer: item.drawer, |
| | | description: item.description, |
| | | })); |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æ¥éåå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¥éåå·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æ¥éæ é¢', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¥éæ é¢', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.FINANCE_REIMBURSEMENT_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'applicantId', |
| | | label: 'ç³è¯·äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç³è¯·äºº', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'applyDate', |
| | | label: 'ç³è¯·æ¥æ', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | // å端 LocalDate[] åªæ¥å yyyy-MM-ddï¼ä¸è½å¸¦æ¶åç§ |
| | | valueFormat: 'YYYY-MM-DD', |
| | | allowClear: true, |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | type: 'checkbox', |
| | | width: 50, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'no', |
| | | title: 'æ¥éåå·', |
| | | width: 175, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'title', |
| | | title: 'æ¥éæ é¢', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'totalAmount', |
| | | title: 'æ¥éæ»éé¢', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 95, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.FINANCE_REIMBURSEMENT_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'applicantName', |
| | | title: 'ç³è¯·äºº', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'applyDate', |
| | | title: 'ç³è¯·æ¥æ', |
| | | width: 115, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: 'æ¯å¦ä¸ä¼ å票', |
| | | minWidth: 110, |
| | | slots: { default: 'hasAttachment' }, |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | | title: 'å建人', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 300, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { FinanceReimbursementApi } from '#/api/finance/reimbursement'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, nextTick, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getReimbursement } from '#/api/finance/reimbursement'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | defineOptions({ name: 'FinanceReimbursementApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<FinanceReimbursementApi.Reimbursement>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** æ¥éæç»ï¼å®¡æ¹è¯¦æ
åªè¯»åï¼ */ |
| | | function useReimbursementItemColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'seq', title: 'åºå·', width: 55 }, |
| | | { |
| | | field: 'expenseType', |
| | | title: 'è´¹ç¨ç±»å', |
| | | minWidth: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.FINANCE_EXPENSE_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'invoiceNo', |
| | | title: 'å票å·ç ', |
| | | minWidth: 160, |
| | | formatter: ({ cellValue }) => textRender(cellValue), |
| | | }, |
| | | { |
| | | field: 'invoiceAmount', |
| | | title: 'å票éé¢', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-', |
| | | }, |
| | | { |
| | | field: 'invoiceDate', |
| | | title: 'åç¥¨æ¥æ', |
| | | minWidth: 120, |
| | | formatter: ({ cellValue }) => textRender(cellValue), |
| | | }, |
| | | { |
| | | field: 'drawer', |
| | | title: 'å¼ç¥¨æ¹', |
| | | minWidth: 160, |
| | | formatter: ({ cellValue }) => textRender(cellValue), |
| | | }, |
| | | { |
| | | field: 'description', |
| | | title: 'è´¹ç¨è¯´æ', |
| | | minWidth: 160, |
| | | formatter: ({ cellValue }) => textRender(cellValue), |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useReimbursementItemColumns(), |
| | | data: [], |
| | | minHeight: 200, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'æ¥éåå·', field: 'no', render: textRender }, |
| | | { label: 'æ¥éæ é¢', field: 'title', render: textRender }, |
| | | { |
| | | label: 'æ¥éæ»éé¢ï¼å
ï¼', |
| | | field: 'totalAmount', |
| | | render: (val) => erpPriceInputFormatter(val) || '-', |
| | | }, |
| | | { |
| | | label: 'ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { |
| | | type: DICT_TYPE.FINANCE_REIMBURSEMENT_STATUS, |
| | | value: val, |
| | | }), |
| | | }, |
| | | { label: 'ç³è¯·äºº', field: 'applicantName', render: textRender }, |
| | | { |
| | | label: 'ç³è¯·æ¥æ', |
| | | field: 'applyDate', |
| | | render: (val) => textRender(val), |
| | | }, |
| | | { |
| | | label: '彿¡£æ¶é´', |
| | | field: 'archiveTime', |
| | | render: (val) => (val ? (formatDateTime(val) as string) : '-'), |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getReimbursement(Number(props.id)); |
| | | if (formData.value?.items?.length) { |
| | | await nextTick(); |
| | | await gridApi.grid.reloadData(formData.value.items); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- æ¥éååºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">æ¥éååºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | <!-- æ¥éæç» --> |
| | | <div |
| | | v-if="formData?.items?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">æ¥éæç»</span> |
| | | </div> |
| | | <Grid /> |
| | | </div> |
| | | <!-- å票éä»¶ --> |
| | | <div |
| | | v-if="formData?.attachmentList?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">å票éä»¶</span> |
| | | </div> |
| | | <div class="flex flex-wrap gap-3"> |
| | | <a |
| | | v-for="file in formData.attachmentList" |
| | | :key="file.id" |
| | | :href="file.previewURL || file.url" |
| | | target="_blank" |
| | | class="text-blue-500 hover:underline" |
| | | > |
| | | {{ file.name || `å票-${file.id}` }} |
| | | </a> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { FinanceReimbursementApi } from '#/api/finance/reimbursement'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | archiveReimbursement, |
| | | deleteReimbursement, |
| | | exportReimbursement, |
| | | getReimbursementApproveProcessList, |
| | | getReimbursementPage, |
| | | } from '#/api/finance/reimbursement'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { |
| | | ReimbursementStatus, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ProcessSelectModal from './modules/process-select-modal.vue'; |
| | | |
| | | /** è´¢å¡æ¥éåå表 */ |
| | | defineOptions({ name: 'FinanceReimbursement' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ProcessModal, processModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessSelectModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 导åºè¡¨æ ¼ */ |
| | | async function handleExport() { |
| | | const data = await exportReimbursement(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'æ¥éå.xls', source: data }); |
| | | } |
| | | |
| | | /** æ°å¢æ¥éå */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾æ¥éåï¼ä»
è稿ã已驳åå¯ç¼è¾ï¼ */ |
| | | function handleEdit(row: FinanceReimbursementApi.Reimbursement) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çæ¥éå详æ
*/ |
| | | function handleDetail(row: FinanceReimbursementApi.Reimbursement) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å 餿¥éåï¼ä»
è稿å¯å ï¼ */ |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting'), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await Promise.all(ids.map((id) => deleteReimbursement(id))); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨åè¡¨ï¼æµç¨ key ä¸åæ» */ |
| | | async function handleSubmit(row: FinanceReimbursementApi.Reimbursement) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const processList = await getReimbursementApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | | message.warning('ææ å¯ç¨å®¡æ¹æµç¨ï¼è¯·å
å° BPM æµç¨è®¾è®¡å¨å叿¥éå®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** 彿¡£æ¥éå */ |
| | | async function handleArchive(row: FinanceReimbursementApi.Reimbursement) { |
| | | const hideLoading = message.loading({ |
| | | content: '彿¡£ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await archiveReimbursement(row.id!); |
| | | message.success('彿¡£æå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¯¦æ
*/ |
| | | function handleProcessDetail(row: FinanceReimbursementApi.Reimbursement) { |
| | | if (!row.processInstanceId) { |
| | | message.warning('该æ¥éåå°æªæäº¤å®¡æ¹'); |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ |
| | | records, |
| | | }: { |
| | | records: FinanceReimbursementApi.Reimbursement[]; |
| | | }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getReimbursementPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<FinanceReimbursementApi.Reimbursement>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ProcessModal @success="handleRefresh" /> |
| | | <Grid table-title="æ¥éåå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥éå']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['finance:reimbursement:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['finance:reimbursement:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['finance:reimbursement:delete'], |
| | | popConfirm: { |
| | | title: `æ¯å¦å 餿é䏿°æ®ï¼ï¼ä»
èç¨¿ç¶æå¯å é¤ï¼`, |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'default'"> |
| | | {{ row.hasAttachment ? 'æ' : 'æ ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['finance:reimbursement:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['finance:reimbursement:update'], |
| | | ifShow: () => |
| | | row.status === ReimbursementStatus.DRAFT || |
| | | row.status === ReimbursementStatus.REJECTED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['finance:reimbursement:update-status'], |
| | | ifShow: () => |
| | | row.status === ReimbursementStatus.DRAFT || |
| | | row.status === ReimbursementStatus.REJECTED, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['finance:reimbursement:archive'], |
| | | ifShow: () => row.status === ReimbursementStatus.APPROVED, |
| | | popConfirm: { |
| | | title: `ç¡®è®¤å½æ¡£æ¥éå ${row.no} åï¼å½æ¡£åä¸å¯åä¿®æ¹`, |
| | | confirm: handleArchive.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['finance:reimbursement:query'], |
| | | ifShow: () => !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['finance:reimbursement:delete'], |
| | | ifShow: () => row.status === ReimbursementStatus.DRAFT, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { FinanceReimbursementApi } from '#/api/finance/reimbursement'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createReimbursement, |
| | | getReimbursement, |
| | | updateReimbursement, |
| | | } from '#/api/finance/reimbursement'; |
| | | |
| | | import { toSubmitItems, useFormSchema } from '../data'; |
| | | import ItemForm from './item-form.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formData = ref<FinanceReimbursementApi.Reimbursement>({ |
| | | title: '', |
| | | totalAmount: 0, |
| | | applyDate: '', |
| | | remark: '', |
| | | items: [], |
| | | }); |
| | | |
| | | const formType = ref<FormType>('create'); // 表åç±»åï¼'create' | 'edit' | 'detail' |
| | | const itemFormRef = ref<InstanceType<typeof ItemForm>>(); |
| | | |
| | | /** |
| | | * æç»è¡¨æ ¼çåæ¾æ°æ®æºï¼åªå¨æå¼å¼¹çªæ¶èµå¼ï¼ç¼è¾åæ¾ / æ°å¢ç½®ç©ºï¼ã |
| | | * ç¨æ·å¨è¡¨æ ¼éçå¢å æ¹åªéè¿ emit ååæµå°è¡¨åå¼ï¼ç»ä¸ååå°è¿éï¼ |
| | | * å¦å prop ååä¼è®©è¡¨æ ¼åå¤éè½½ï¼ä¸ä¸åç»ä»¶ watcher å½¢ææ»å¾ªç¯ï¼ã |
| | | */ |
| | | const initialItems = ref<FinanceReimbursementApi.ReimbursementItem[]>([]); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return $t('ui.actionTitle.create', ['æ¥éå']); |
| | | } else if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['æ¥éå']); |
| | | } else { |
| | | return 'æ¥éå详æ
'; |
| | | } |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useFormSchema(formType.value), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** 忥æç»è¡å°è¡¨åå¼ï¼ä¿è¯éä¸»è¡¨ä¸æ¬¡æ§æäº¤ */ |
| | | function handleUpdateItems(items: FinanceReimbursementApi.ReimbursementItem[]) { |
| | | // 第äºä¸ªåæ°ä¸º falseï¼è·³è¿åå¹¶ç®æ³ï¼å¦åæ°ç»ç±»åçå¼ä¼è¢«è¿æ»¤æ |
| | | formApi.setValues({ items }, false); |
| | | } |
| | | |
| | | /** æç»éé¢ååæ¶å填主表æ¥éæ»éé¢ï¼ç¨æ·ä»å¯æå¨ä¿®æ¹ï¼ */ |
| | | function handleUpdateTotalAmount(totalAmount: number) { |
| | | formApi.setValues({ totalAmount }, false); |
| | | } |
| | | |
| | | /** åå»ºææ´æ°æ¥éå */ |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | const itemFormInstance = Array.isArray(itemFormRef.value) |
| | | ? itemFormRef.value[0] |
| | | : itemFormRef.value; |
| | | try { |
| | | itemFormInstance?.validate(); |
| | | } catch (error: any) { |
| | | message.error(error.message || 'æ¥éæç»éªè¯å¤±è´¥'); |
| | | return; |
| | | } |
| | | |
| | | modalApi.lock(); |
| | | const data = |
| | | (await formApi.getValues()) as FinanceReimbursementApi.Reimbursement; |
| | | // å票é件为å
¨éæ¿æ¢è¯ä¹ï¼æ¸
空å
¨é¨å票æ¶å¿
é¡»æ¾å¼ä¼ 空æ°ç»ï¼ |
| | | // ä¼ null å端ä¼è§ä¸ºãä¸å¨éä»¶ãï¼å¯¼è´å 餿ä½ä¸çæ |
| | | data.blobIds = data.blobIds ?? []; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createReimbursement(data) |
| | | : updateReimbursement(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = {} as FinanceReimbursementApi.Reimbursement; |
| | | initialItems.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | formApi.updateSchema(useFormSchema(formType.value)); |
| | | if (!data || !data.id) { |
| | | initialItems.value = []; |
| | | // æ°å»ºæ¶é»è®¤ç³è¯·æ¥æä¸ºä»å¤© |
| | | await formApi.setValues( |
| | | { applyDate: dayjs().format('YYYY-MM-DD') }, |
| | | false, |
| | | ); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getReimbursement(data.id); |
| | | // æç»è¡¨æ ¼çè¡ key ç¨æ°æ®åº idï¼æ
åæ¾æ°æ®æºä¿çå id |
| | | initialItems.value = formData.value.items ?? []; |
| | | // å·²æå票转æä¸ä¼ ç»ä»¶å¯è¯å«çæä»¶å¯¹è±¡ï¼æäº¤æ¶æ valueKey=id æå为 blobId |
| | | const blobIds = (formData.value.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues( |
| | | { |
| | | ...formData.value, |
| | | // 表åéç items åªç¨äºæäº¤ï¼å¿
é¡»å¥æè¡ idï¼å端ãå
å åå¢ãä¼ä¸è½¯å è¡ä¸»é®å²çªï¼ |
| | | items: toSubmitItems(formData.value.items), |
| | | blobIds, |
| | | }, |
| | | false, |
| | | ); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="getTitle" |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3"> |
| | | <template #items> |
| | | <ItemForm |
| | | ref="itemFormRef" |
| | | :items="initialItems" |
| | | :disabled="formType === 'detail'" |
| | | class="w-full" |
| | | @update:items="handleUpdateItems" |
| | | @update:total-amount="handleUpdateTotalAmount" |
| | | /> |
| | | </template> |
| | | </Form> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { FinanceReimbursementApi } from '#/api/finance/reimbursement'; |
| | | |
| | | import type { DefaultOptionType } from 'ant-design-vue/es/select'; |
| | | |
| | | import { nextTick, watch } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { erpPriceInputFormatter } from '@vben/utils'; |
| | | |
| | | import { Button, DatePicker, Input, InputNumber, Select } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { toSubmitItems, useItemColumns } from '../data'; |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | items?: FinanceReimbursementApi.ReimbursementItem[]; |
| | | disabled?: boolean; |
| | | }>(), |
| | | { |
| | | items: () => [], |
| | | disabled: false, |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits(['update:items', 'update:totalAmount']); |
| | | |
| | | const expenseTypeOptions = getDictOptions( |
| | | DICT_TYPE.FINANCE_EXPENSE_TYPE, |
| | | 'number', |
| | | ) as DefaultOptionType[]; |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useItemColumns(), |
| | | data: [], |
| | | minHeight: 250, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | } as VxeTableGridOptions<FinanceReimbursementApi.ReimbursementItem>, |
| | | }); |
| | | |
| | | /** è¾å
¥æ¶ä¸è¡¥é¶ï¼å¤±ç¦å仿é颿 ¼å¼ä¿ç两ä½å°æ° */ |
| | | const amountFormatter = ( |
| | | value: number | string | undefined, |
| | | info: { input: string; userTyping: boolean }, |
| | | ) => { |
| | | return info.userTyping ? info.input : erpPriceInputFormatter(value); |
| | | }; |
| | | |
| | | /** 读åå½åå
¨é¨æç»è¡ï¼æäº¤å½¢æï¼ä¸å«è¡ idï¼ */ |
| | | function collectItems(): FinanceReimbursementApi.ReimbursementItem[] { |
| | | const rows = |
| | | (gridApi.grid.getData() as FinanceReimbursementApi.ReimbursementItem[]) ?? |
| | | []; |
| | | const removed = |
| | | (gridApi.grid.getRemoveRecords() as FinanceReimbursementApi.ReimbursementItem[]) ?? |
| | | []; |
| | | const inserted = |
| | | (gridApi.grid.getInsertRecords() as FinanceReimbursementApi.ReimbursementItem[]) ?? |
| | | []; |
| | | const isRemoved = (row: FinanceReimbursementApi.ReimbursementItem) => |
| | | removed.some( |
| | | (item) => item === row || (row.id != null && item.id === row.id), |
| | | ); |
| | | return toSubmitItems([...rows.filter((row) => !isRemoved(row)), ...inserted]); |
| | | } |
| | | |
| | | /** éæ°æ±æ»æç»å¹¶åæ¥ç»ç¶ç»ä»¶ */ |
| | | function recalc() { |
| | | const rows = collectItems(); |
| | | const totalAmount = rows.reduce( |
| | | (sum, row) => sum + (Number(row.invoiceAmount) || 0), |
| | | 0, |
| | | ); |
| | | emit('update:items', rows); |
| | | emit('update:totalAmount', Number(totalAmount.toFixed(2))); |
| | | } |
| | | |
| | | /** æ·»å æç»è¡ */ |
| | | async function handleAdd() { |
| | | await gridApi.grid.insertAt( |
| | | {} as unknown as FinanceReimbursementApi.ReimbursementItem, |
| | | -1, |
| | | ); |
| | | recalc(); |
| | | } |
| | | |
| | | /** å 餿ç»è¡ */ |
| | | async function handleDelete(row: FinanceReimbursementApi.ReimbursementItem) { |
| | | await gridApi.grid.remove(row); |
| | | recalc(); |
| | | } |
| | | |
| | | /** æ ¡éªæç»ï¼è³å° 1 è¡ï¼ä¸æ¯è¡è´¹ç¨ç±»åä¸å票éé¢å¿
å¡« */ |
| | | function validate() { |
| | | const rows = collectItems(); |
| | | if (rows.length === 0) { |
| | | throw new Error('请è³å°æ·»å 䏿¡æ¥éæç»'); |
| | | } |
| | | for (let i = 0; i < rows.length; i++) { |
| | | const row = rows[i]; |
| | | if (row?.expenseType === undefined || row?.expenseType === null) { |
| | | throw new Error(`第 ${i + 1} è¡ï¼è¯·éæ©è´¹ç¨ç±»å`); |
| | | } |
| | | if (!row?.invoiceAmount || Number(row.invoiceAmount) <= 0) { |
| | | throw new Error(`第 ${i + 1} è¡ï¼å票éé¢å¿
é¡»å¤§äº 0`); |
| | | } |
| | | } |
| | | } |
| | | |
| | | defineExpose({ validate }); |
| | | |
| | | /** |
| | | * åæ¾æç»ï¼ååï¼ç¶ â è¡¨æ ¼ï¼ã |
| | | * æ¤ watcher å
**ç¦æ¢**åä¼ emit('update:items')ï¼ç¶ç»ä»¶ä¼ææ°æ°ç»åå :itemsï¼ |
| | | * äºæ¯æ¬ watcher è¢«åæ¬¡è§¦åï¼ææ prop â emit æ»å¾ªç¯ï¼æ°å¢/ç¼è¾æ¶é¡µé¢ä¼ç´æ¥å¡æ»ã |
| | | * æ»éé¢åçï¼åæ¾æ¶ä¸åå¡«ï¼é¿å
è¦çç¨æ·æå·¥è°æ´è¿çæ¥éæ»éé¢ã |
| | | */ |
| | | watch( |
| | | () => props.items, |
| | | async (items) => { |
| | | if (!items) { |
| | | return; |
| | | } |
| | | await nextTick(); |
| | | await gridApi.grid.loadData([...items]); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Grid class="w-full"> |
| | | <template #expenseType="{ row }"> |
| | | <Select |
| | | v-model:value="row.expenseType" |
| | | :disabled="disabled" |
| | | :options="expenseTypeOptions" |
| | | placeholder="è¯·éæ©è´¹ç¨ç±»å" |
| | | allow-clear |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #invoiceNo="{ row }"> |
| | | <Input |
| | | v-model:value="row.invoiceNo" |
| | | :disabled="disabled" |
| | | placeholder="请è¾å
¥å票å·ç " |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #invoiceAmount="{ row }"> |
| | | <InputNumber |
| | | v-model:value="row.invoiceAmount" |
| | | :disabled="disabled" |
| | | :precision="2" |
| | | :formatter="amountFormatter" |
| | | class="!w-full" |
| | | placeholder="请è¾å
¥å票éé¢" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #invoiceDate="{ row }"> |
| | | <DatePicker |
| | | v-model:value="row.invoiceDate" |
| | | :disabled="disabled" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©åç¥¨æ¥æ" |
| | | class="w-full" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #drawer="{ row }"> |
| | | <Input |
| | | v-model:value="row.drawer" |
| | | :disabled="disabled" |
| | | placeholder="请è¾å
¥å¼ç¥¨æ¹" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #description="{ row }"> |
| | | <Input |
| | | v-model:value="row.description" |
| | | :disabled="disabled" |
| | | placeholder="请è¾å
¥è´¹ç¨è¯´æ" |
| | | @change="() => recalc()" |
| | | /> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.delete'), |
| | | danger: true, |
| | | type: 'link', |
| | | icon: ACTION_ICON.DELETE, |
| | | disabled, |
| | | onClick: handleDelete.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | <div v-if="!disabled" class="-mt-4 flex justify-center"> |
| | | <Button type="primary" ghost @click="handleAdd"> æ·»å æ¥éæç» </Button> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FinanceReimbursementApi } from '#/api/finance/reimbursement'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Select } from 'ant-design-vue'; |
| | | |
| | | import { submitReimbursement } from '#/api/finance/reimbursement'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const reimbursementId = ref<number>(); |
| | | const processList = ref<FinanceReimbursementApi.ProcessDefinition[]>([]); |
| | | const selectedProcessKey = ref<string>(''); |
| | | |
| | | /** æµç¨ key åªè½æ¥èªåç«¯å®æ¶å表ï¼ç¦æ¢åç«¯åæ» */ |
| | | const processOptions = computed(() => |
| | | processList.value.map((item) => ({ label: item.name, value: item.key })), |
| | | ); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await submitReimbursement( |
| | | reimbursementId.value!, |
| | | selectedProcessKey.value, |
| | | ); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æäº¤æå'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | reimbursementId.value = undefined; |
| | | selectedProcessKey.value = ''; |
| | | processList.value = []; |
| | | return; |
| | | } |
| | | const data = |
| | | modalApi.getData<{ |
| | | id: number; |
| | | processList: FinanceReimbursementApi.ProcessDefinition[]; |
| | | }>(); |
| | | reimbursementId.value = data.id; |
| | | processList.value = data.processList || []; |
| | | // é»è®¤éæ©ç¬¬ä¸ä¸ªæµç¨ |
| | | if (processList.value.length > 0) { |
| | | selectedProcessKey.value = processList.value[0]!.key; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="éæ©å®¡æ¹æµç¨" class="w-1/3"> |
| | | <div class="mb-4"> |
| | | <span class="mr-2">å®¡æ¹æµç¨ï¼</span> |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | :options="processOptions" |
| | | style="width: 200px" |
| | | placeholder="è¯·éæ©å®¡æ¹æµç¨" |
| | | /> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | export { default as ResearchMemberSelect } from './member-select.vue'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { UmsMemberApi } from '#/api/ums/member'; |
| | | |
| | | import { nextTick, ref } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | |
| | | import { message, Modal } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getMemberPage } from '#/api/ums/member'; |
| | | |
| | | defineOptions({ name: 'ResearchMemberSelectDialog' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | selected: [rows: UmsMemberApi.Member[]]; |
| | | }>(); |
| | | |
| | | const open = ref(false); |
| | | const selectedRows = ref<UmsMemberApi.Member[]>([]); |
| | | const preSelectedId = ref<number>(); |
| | | |
| | | function getTableRows() { |
| | | return gridApi.grid.getTableData().fullData as UmsMemberApi.Member[]; |
| | | } |
| | | |
| | | async function applyPreSelection() { |
| | | if (!preSelectedId.value) { |
| | | return; |
| | | } |
| | | const row = getTableRows().find((item) => item.id === preSelectedId.value); |
| | | if (!row) { |
| | | return; |
| | | } |
| | | await gridApi.grid.setRadioRow(row); |
| | | selectedRows.value = [row]; |
| | | } |
| | | |
| | | async function handleCellDblclick({ row }: { row: UmsMemberApi.Member }) { |
| | | selectedRows.value = [row]; |
| | | await gridApi.grid.setRadioRow(row); |
| | | handleConfirm(); |
| | | } |
| | | |
| | | const gridColumns = [ |
| | | { field: 'no', title: 'ä¼åç¼å·', width: 180 }, |
| | | { |
| | | field: 'name', |
| | | title: 'å§å / åä½åç§°', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'memberType', |
| | | title: 'ä¼åç±»å', |
| | | width: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.UMS_MEMBER_TYPE }, |
| | | }, |
| | | }, |
| | | { field: 'contactUser', title: 'è系人', width: 110 }, |
| | | { field: 'contactPhone', title: 'èç³»çµè¯', width: 140 }, |
| | | { field: 'workUnit', title: 'å·¥ä½åä½', minWidth: 180 }, |
| | | { field: 'title', title: 'èç§°', width: 110 }, |
| | | ]; |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: [ |
| | | { |
| | | fieldName: 'name', |
| | | label: 'å§å / åä½åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥å§åæåä½åç§°', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactPhone', |
| | | label: 'èç³»çµè¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥èç³»çµè¯', |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | gridOptions: { |
| | | columns: [{ type: 'radio', width: 50 }, ...gridColumns], |
| | | height: 440, |
| | | keepSource: true, |
| | | radioConfig: { |
| | | highlight: true, |
| | | trigger: 'row', |
| | | }, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getMemberPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<UmsMemberApi.Member>, |
| | | gridEvents: { |
| | | cellDblclick: handleCellDblclick, |
| | | radioChange: ({ row }: { row: UmsMemberApi.Member }) => { |
| | | selectedRows.value = [row]; |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | async function resetQueryState() { |
| | | selectedRows.value = []; |
| | | await gridApi.grid.clearRadioRow(); |
| | | await gridApi.formApi.resetForm(); |
| | | } |
| | | |
| | | async function openModal(selectedId?: number) { |
| | | open.value = true; |
| | | preSelectedId.value = selectedId; |
| | | await nextTick(); |
| | | await resetQueryState(); |
| | | await gridApi.query(); |
| | | await nextTick(); |
| | | await applyPreSelection(); |
| | | } |
| | | |
| | | async function closeModal() { |
| | | open.value = false; |
| | | await resetQueryState(); |
| | | } |
| | | |
| | | function handleConfirm() { |
| | | const row = selectedRows.value[0]; |
| | | if (!row) { |
| | | message.warning('è¯·éæ©ä¸ä½ä¼åä½ä¸ºç³æ¥äºº'); |
| | | return; |
| | | } |
| | | emit('selected', [row]); |
| | | open.value = false; |
| | | } |
| | | |
| | | defineExpose({ open: openModal }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="éæ©ç³æ¥äººï¼ä¼åï¼" |
| | | width="72%" |
| | | :destroy-on-close="true" |
| | | @ok="handleConfirm" |
| | | @cancel="closeModal" |
| | | > |
| | | <Grid table-title="ä¼åå表" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { UmsMemberApi } from '#/api/ums/member'; |
| | | |
| | | import { computed, ref, useAttrs, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Input, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { getMember } from '#/api/ums/member'; |
| | | |
| | | import MemberSelectDialog from './member-select-dialog.vue'; |
| | | |
| | | defineOptions({ name: 'ResearchMemberSelect', inheritAttrs: false }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | allowClear?: boolean; |
| | | disabled?: boolean; |
| | | modelValue?: number; |
| | | placeholder?: string; |
| | | }>(), |
| | | { |
| | | allowClear: true, |
| | | disabled: false, |
| | | modelValue: undefined, |
| | | placeholder: 'è¯·éæ©ç³æ¥äººï¼ä¼åï¼', |
| | | }, |
| | | ); |
| | | const emit = defineEmits<{ |
| | | change: [item: UmsMemberApi.Member | undefined]; |
| | | 'update:modelValue': [value: number | undefined]; |
| | | }>(); |
| | | |
| | | const attrs = useAttrs(); |
| | | const dialogRef = ref<InstanceType<typeof MemberSelectDialog>>(); |
| | | const hovering = ref(false); |
| | | const selectedItem = ref<UmsMemberApi.Member>(); |
| | | |
| | | const displayLabel = computed(() => selectedItem.value?.name ?? ''); |
| | | const showClear = computed( |
| | | () => |
| | | props.allowClear && |
| | | !props.disabled && |
| | | hovering.value && |
| | | props.modelValue !== null, |
| | | ); |
| | | |
| | | async function resolveMemberById(id: number | undefined) { |
| | | if (!id) { |
| | | selectedItem.value = undefined; |
| | | return; |
| | | } |
| | | if (selectedItem.value?.id === id) { |
| | | return; |
| | | } |
| | | selectedItem.value = await getMember(id); |
| | | } |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | (value) => { |
| | | resolveMemberById(value); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | function clearSelected() { |
| | | selectedItem.value = undefined; |
| | | emit('update:modelValue', undefined); |
| | | emit('change', undefined); |
| | | } |
| | | |
| | | function handleClick(event: MouseEvent) { |
| | | if (props.disabled) { |
| | | return; |
| | | } |
| | | const target = event.target as HTMLElement; |
| | | if (showClear.value && target.closest('.ant-input-suffix')) { |
| | | event.stopPropagation(); |
| | | clearSelected(); |
| | | return; |
| | | } |
| | | dialogRef.value?.open(props.modelValue ?? undefined); |
| | | } |
| | | |
| | | function handleSelected(rows: UmsMemberApi.Member[]) { |
| | | const item = rows[0]; |
| | | if (!item) { |
| | | return; |
| | | } |
| | | selectedItem.value = item; |
| | | emit('update:modelValue', item.id); |
| | | emit('change', item); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div |
| | | v-bind="attrs" |
| | | class="w-full" |
| | | :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'" |
| | | @click="handleClick" |
| | | @mouseenter="hovering = true" |
| | | @mouseleave="hovering = false" |
| | | > |
| | | <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false"> |
| | | <template #title> |
| | | <div v-if="selectedItem" class="leading-6"> |
| | | <div>ä¼åç¼å·ï¼{{ selectedItem.no || '-' }}</div> |
| | | <div>èç³»çµè¯ï¼{{ selectedItem.contactPhone || '-' }}</div> |
| | | <div>å·¥ä½åä½ï¼{{ selectedItem.workUnit || '-' }}</div> |
| | | <div>èç§°ï¼{{ selectedItem.title || '-' }}</div> |
| | | </div> |
| | | </template> |
| | | <Input |
| | | :disabled="disabled" |
| | | :placeholder="placeholder" |
| | | :value="displayLabel" |
| | | readonly |
| | | > |
| | | <template #suffix> |
| | | <IconifyIcon |
| | | class="size-4" |
| | | :icon="showClear ? 'lucide:circle-x' : 'lucide:search'" |
| | | /> |
| | | </template> |
| | | </Input> |
| | | </Tooltip> |
| | | </div> |
| | | <MemberSelectDialog ref="dialogRef" @selected="handleSelected" /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { DefaultOptionType } from 'ant-design-vue/es/select'; |
| | | |
| | | import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ResearchGuidelineApi } from '#/api/research/guideline'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { erpPriceInputFormatter } from '@vben/utils'; |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | |
| | | import { ResearchMemberSelect } from './components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** 课é¢ç³æ¥ä¹¦ç¶æï¼å¯¹åºåå
¸ research_application_statusï¼ */ |
| | | export const ApplicationStatus = { |
| | | DRAFT: 0, // è稿 |
| | | APPROVING: 15, // 审æ¹ä¸ |
| | | APPROVED: 20, // å·²ç«é¡¹ |
| | | REJECTED: 35, // 已驳å |
| | | } as const; |
| | | |
| | | /** ç»è´¹æ¨ä»ç¶æï¼å¯¹åºåå
¸ research_fund_statusï¼ */ |
| | | export const FundStatus = { |
| | | PENDING: 0, // å¾
æ¨ä» |
| | | PAID: 20, // å·²æ¨ä» |
| | | } as const; |
| | | |
| | | /** 课é¢ç³æ¥ä¹¦è¡¨åé
ç½® */ |
| | | export function useFormSchema( |
| | | formType: FormType, |
| | | formApi?: VbenFormApi, |
| | | guidelines: ResearchGuidelineApi.Guideline[] = [], |
| | | ): VbenFormSchema[] { |
| | | const guidelineOptions: DefaultOptionType[] = guidelines.map((item) => ({ |
| | | label: item.no ? `${item.no} ${item.title}` : item.title, |
| | | value: item.id, |
| | | })); |
| | | |
| | | const readonly = formType === 'detail'; |
| | | |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ç³æ¥ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ç³æ¥ç¼å·ç±ç³»ç»èªå¨çæ', |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => formType !== 'create', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_APPLICATION_STATUS, 'number'), |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => formType !== 'create', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'guidelineId', |
| | | label: 'æå±è¯¾é¢æå', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: guidelineOptions, |
| | | placeholder: 'è¯·éæ©è¯¾é¢æå', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | disabled: readonly, |
| | | class: '!w-full', |
| | | onChange: (value: number) => { |
| | | const guideline = guidelines.find((item) => item.id === value); |
| | | formApi?.setFieldValue('category', guideline?.category); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'category', |
| | | label: '课é¢ç±»å«', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, 'number'), |
| | | disabled: true, |
| | | placeholder: 'ç±æéè¯¾é¢æåèªå¨å¸¦å
¥', |
| | | }, |
| | | help: 'ç±æéè¯¾é¢æåèªå¨å¸¦å
¥ï¼æ éå¡«å', |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: '课é¢åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è¯¾é¢åç§°', |
| | | maxlength: 255, |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'memberId', |
| | | label: 'ç³æ¥äººï¼ä¼åï¼', |
| | | component: markRaw(ResearchMemberSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç³æ¥äººï¼éæ©åèªå¨å¸¦å
¥åä½ãèç§°ä¸èç³»æ¹å¼', |
| | | disabled: readonly, |
| | | onChange: (member: Record<string, unknown> | undefined) => { |
| | | if (!formApi) { |
| | | return; |
| | | } |
| | | formApi.setFieldValue('applicantName', member?.name ?? ''); |
| | | formApi.setFieldValue('workUnit', member?.workUnit ?? ''); |
| | | formApi.setFieldValue('jobTitle', member?.title ?? ''); |
| | | formApi.setFieldValue('contactPhone', member?.contactPhone ?? ''); |
| | | formApi.setFieldValue('contactEmail', member?.contactEmail ?? ''); |
| | | }, |
| | | }, |
| | | help: 'éæ©ä¼ååèªå¨å¸¦å
¥å§å/åä½/èç§°/èç³»çµè¯/é®ç®±ï¼ä»å¯æå·¥è°æ´', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'applicantName', |
| | | label: 'ç³æ¥äººå§å/åä½åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç³æ¥äººå§åæåä½åç§°', |
| | | maxlength: 64, |
| | | disabled: readonly, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'workUnit', |
| | | label: 'å·¥ä½åä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å·¥ä½åä½', |
| | | maxlength: 128, |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'jobTitle', |
| | | label: 'èç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç§°', |
| | | maxlength: 64, |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactPhone', |
| | | label: 'èç³»çµè¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç³»çµè¯', |
| | | maxlength: 20, |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactEmail', |
| | | label: 'èç³»é®ç®±', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç³»é®ç®±', |
| | | maxlength: 128, |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'budgetAmount', |
| | | label: 'ç³è¯·ç»è´¹ï¼å
ï¼', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥ç³è¯·ç»è´¹', |
| | | precision: 2, |
| | | min: 0, |
| | | formatter: erpPriceInputFormatter, |
| | | disabled: readonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'teamMembers', |
| | | label: '课é¢ç»æå', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: 'æ¯è¡ä¸æ¡ï¼æ ¼å¼ï¼å§å|åä½|èç§°\nä¾å¦ï¼æå|XX大å¦|坿æ', |
| | | autoSize: { minRows: 3, maxRows: 6 }, |
| | | disabled: readonly, |
| | | }, |
| | | help: 'æ¯è¡ä¸æ¡ï¼ç¨ç«çº¿åéï¼å§å|åä½|èç§°', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'planContent', |
| | | label: 'ç³æ¥ä¹¦æ£æ', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: |
| | | '请填åç ç©¶èæ¯ä¸æä¹ãç ç©¶ç®æ ãç ç©¶æ¹æ¡ä¸ææ¯è·¯çº¿ã颿ææä¸æççå
容', |
| | | autoSize: { minRows: 8, maxRows: 16 }, |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'ç³æ¥ä¹¦éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | showDescription: !readonly, |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ç³æ¥ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç³æ¥ç¼å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: '课é¢åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è¯¾é¢åç§°', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'category', |
| | | label: '课é¢ç±»å«', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, 'number'), |
| | | placeholder: 'è¯·éæ©è¯¾é¢ç±»å«', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_APPLICATION_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'createTime', |
| | | label: 'å建æ¶é´', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表åé
ç½® */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | type: 'checkbox', |
| | | width: 50, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'no', |
| | | title: 'ç³æ¥ç¼å·', |
| | | width: 180, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'title', |
| | | title: '课é¢åç§°', |
| | | minWidth: 220, |
| | | }, |
| | | { |
| | | field: 'guidelineTitle', |
| | | title: 'æå±è¯¾é¢æå', |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | field: 'category', |
| | | title: '课é¢ç±»å«', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'applicantName', |
| | | title: 'ç³æ¥äºº', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'workUnit', |
| | | title: 'å·¥ä½åä½', |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'budgetAmount', |
| | | title: 'ç³è¯·ç»è´¹ï¼å
ï¼', |
| | | minWidth: 130, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'reviewAvgScore', |
| | | title: 'è¯å®¡åå', |
| | | minWidth: 100, |
| | | formatter: ({ cellValue }) => |
| | | cellValue === null || cellValue === undefined |
| | | ? '-' |
| | | : Number(cellValue).toFixed(2), |
| | | }, |
| | | { |
| | | field: 'reviewPass', |
| | | title: 'è¯å®¡ç»è®º', |
| | | minWidth: 110, |
| | | slots: { default: 'reviewPass' }, |
| | | }, |
| | | { |
| | | field: 'researchNo', |
| | | title: 'ç«é¡¹ç¼å·', |
| | | width: 180, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 95, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.RESEARCH_APPLICATION_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: 'éä»¶', |
| | | minWidth: 80, |
| | | slots: { default: 'hasAttachment' }, |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | | title: 'å建人', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 330, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ°å¼ç©ºå¼å ä½ï¼VXE å formatterï¼ */ |
| | | function dashIfEmpty({ cellValue }: { cellValue: unknown }) { |
| | | return cellValue === null || cellValue === undefined ? '-' : String(cellValue); |
| | | } |
| | | |
| | | /** ä¸å®¶è¯å®¡æç»åï¼è¯å®¡é¢æ¿ / 审æ¹è¯¦æ
å
±ç¨ï¼ */ |
| | | export function useReviewColumns( |
| | | showActions = false, |
| | | ): VxeTableGridOptions['columns'] { |
| | | const columns: VxeTableGridOptions['columns'] = [ |
| | | { type: 'seq', title: 'åºå·', width: 55, fixed: 'left' }, |
| | | { field: 'evaluatorName', title: 'ä¸å®¶å§å', minWidth: 100 }, |
| | | { field: 'evaluatorUnit', title: 'ä¸å®¶åä½', minWidth: 160 }, |
| | | { field: 'evaluatorTitle', title: 'ä¸å®¶èç§°', minWidth: 100 }, |
| | | { |
| | | field: 'topicScore', |
| | | title: 'éé¢ä»·å¼ï¼0.3ï¼', |
| | | minWidth: 120, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'planScore', |
| | | title: 'æ¹æ¡å¯è¡æ§ï¼0.3ï¼', |
| | | minWidth: 130, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'teamScore', |
| | | title: 'å¢éåºç¡ï¼0.2ï¼', |
| | | minWidth: 120, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'resultScore', |
| | | title: '颿ææï¼0.2ï¼', |
| | | minWidth: 120, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'compositeScore', |
| | | title: 'å æå计', |
| | | minWidth: 110, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'ranking', |
| | | title: 'æå', |
| | | width: 70, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { |
| | | field: 'conclusion', |
| | | title: 'è¯å®¡ç»è®º', |
| | | minWidth: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.RESEARCH_REVIEW_CONCLUSION }, |
| | | }, |
| | | }, |
| | | { field: 'opinion', title: 'è¯å®¡æè§', minWidth: 200 }, |
| | | { field: 'creatorName', title: 'å½å
¥äºº', minWidth: 100 }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å½å
¥æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | ]; |
| | | if (showActions) { |
| | | columns.push({ |
| | | title: 'æä½', |
| | | width: 130, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }); |
| | | } |
| | | return columns; |
| | | } |
| | | |
| | | /** ç»è´¹æ¨ä»è®°å½åï¼æ¨ä»é¢æ¿ / 审æ¹è¯¦æ
å
±ç¨ï¼ */ |
| | | export function useFundColumns( |
| | | showActions = false, |
| | | ): VxeTableGridOptions['columns'] { |
| | | const columns: VxeTableGridOptions['columns'] = [ |
| | | { type: 'seq', title: 'åºå·', width: 55 }, |
| | | { field: 'batchName', title: 'æ¨ä»æ¹æ¬¡', minWidth: 130 }, |
| | | { |
| | | field: 'amount', |
| | | title: 'æ¨ä»éé¢ï¼å
ï¼', |
| | | minWidth: 130, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'payDate', |
| | | title: 'æ¨ä»æ¥æ', |
| | | width: 120, |
| | | formatter: dashIfEmpty, |
| | | }, |
| | | { field: 'payee', title: 'æ¶æ¬¾åä½/è´¦æ·å', minWidth: 180 }, |
| | | { |
| | | field: 'status', |
| | | title: 'æ¨ä»ç¶æ', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.RESEARCH_FUND_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: 'æ¨ä»åè¯', |
| | | minWidth: 100, |
| | | slots: { default: 'hasAttachment' }, |
| | | }, |
| | | { field: 'creatorName', title: 'ç»è®°äºº', minWidth: 100 }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'ç»è®°æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | ]; |
| | | if (showActions) { |
| | | columns.push({ |
| | | title: 'æä½', |
| | | width: 130, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }); |
| | | } |
| | | return columns; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | import type { ResearchFundRecordApi } from '#/api/research/fund'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { erpPriceInputFormatter, formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getApplication, getTopicReviewList } from '#/api/research/application'; |
| | | import { getFundRecordList } from '#/api/research/fund'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | import { |
| | | FundStatus, |
| | | useFundColumns, |
| | | useReviewColumns, |
| | | } from '../../data'; |
| | | |
| | | defineOptions({ name: 'ResearchApplicationApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<ResearchApplicationApi.Application>(); |
| | | const reviews = ref<ResearchApplicationApi.TopicReview[]>([]); |
| | | const fundRecords = ref<ResearchFundRecordApi.FundRecord[]>([]); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** éé¢å段空å¼å ä½ */ |
| | | function amountRender(val: string | number | undefined) { |
| | | return val === null || val === undefined || val === '' |
| | | ? '-' |
| | | : (erpPriceInputFormatter(val) as string); |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'ç³æ¥ç¼å·', field: 'no', render: textRender }, |
| | | { label: 'ç³æ¥äººå§å/åä½åç§°', field: 'applicantName', render: textRender }, |
| | | { label: '课é¢åç§°', field: 'title', render: textRender }, |
| | | { label: 'æå±è¯¾é¢æå', field: 'guidelineTitle', render: textRender }, |
| | | { |
| | | label: '课é¢ç±»å«', |
| | | field: 'category', |
| | | render: (val) => |
| | | h(DictTag, { |
| | | type: DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, |
| | | value: val, |
| | | }), |
| | | }, |
| | | { |
| | | label: 'ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { |
| | | type: DICT_TYPE.RESEARCH_APPLICATION_STATUS, |
| | | value: val, |
| | | }), |
| | | }, |
| | | { label: 'å·¥ä½åä½', field: 'workUnit', render: textRender }, |
| | | { label: 'èç§°', field: 'jobTitle', render: textRender }, |
| | | { label: 'èç³»çµè¯', field: 'contactPhone', render: textRender }, |
| | | { label: 'èç³»é®ç®±', field: 'contactEmail', render: textRender }, |
| | | { |
| | | label: 'ç³è¯·ç»è´¹ï¼å
ï¼', |
| | | field: 'budgetAmount', |
| | | render: amountRender, |
| | | }, |
| | | { |
| | | label: 'è¯å®¡å æåå', |
| | | field: 'reviewAvgScore', |
| | | render: (val) => |
| | | val === null || val === undefined |
| | | ? 'æªæ±æ»' |
| | | : Number(val).toFixed(2), |
| | | }, |
| | | { |
| | | label: 'è¯å®¡ç»è®º', |
| | | field: 'reviewPass', |
| | | render: (val) => |
| | | val === null || val === undefined |
| | | ? 'æªæ±æ»' |
| | | : h( |
| | | Tag, |
| | | { color: val === 1 ? 'success' : 'error' }, |
| | | () => (val === 1 ? 'æ¨èç«é¡¹' : '䏿¨èç«é¡¹'), |
| | | ), |
| | | }, |
| | | { label: 'ç«é¡¹ç¼å·', field: 'researchNo', render: textRender }, |
| | | { |
| | | label: 'ç«é¡¹æ¶é´', |
| | | field: 'approveTime', |
| | | render: (val) => (val ? (formatDateTime(val) as string) : '-'), |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => (val ? (formatDateTime(val) as string) : '-'), |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | const [ReviewGrid, reviewGridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useReviewColumns(false), |
| | | data: [], |
| | | minHeight: 200, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | } as VxeTableGridOptions<ResearchApplicationApi.TopicReview>, |
| | | }); |
| | | |
| | | const [FundGrid, fundGridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useFundColumns(false), |
| | | data: [], |
| | | minHeight: 200, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | } as VxeTableGridOptions<ResearchFundRecordApi.FundRecord>, |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | const applicationId = Number(props.id); |
| | | const [application, reviewList, recordList] = await Promise.all([ |
| | | getApplication(applicationId), |
| | | getTopicReviewList(applicationId), |
| | | getFundRecordList(applicationId), |
| | | ]); |
| | | formData.value = application; |
| | | reviews.value = reviewList; |
| | | fundRecords.value = recordList; |
| | | reviewGridApi.setGridOptions({ data: reviewList }); |
| | | fundGridApi.setGridOptions({ data: recordList }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- ç³æ¥ä¹¦åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">课é¢ç³æ¥ä¹¦åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | |
| | | <!-- 课é¢ç»æå --> |
| | | <div |
| | | v-if="formData?.teamMembers" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">课é¢ç»æå</span> |
| | | </div> |
| | | <pre class="whitespace-pre-wrap break-all font-sans">{{ |
| | | formData.teamMembers |
| | | }}</pre> |
| | | </div> |
| | | |
| | | <!-- ç³æ¥ä¹¦æ£æ --> |
| | | <div |
| | | v-if="formData?.planContent" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">ç³æ¥ä¹¦æ£æ</span> |
| | | </div> |
| | | <pre class="whitespace-pre-wrap break-all font-sans">{{ |
| | | formData.planContent |
| | | }}</pre> |
| | | </div> |
| | | |
| | | <!-- ä¸å®¶è¯å®¡æç» --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">ä¸å®¶è¯å®¡æç»</span> |
| | | <span v-if="reviews.length" class="text-gray-500"> |
| | | ï¼å
± {{ reviews.length }} ä½ä¸å®¶ï¼ |
| | | </span> |
| | | </div> |
| | | <ReviewGrid v-if="reviews.length" /> |
| | | <p v-else class="text-gray-500">ææ ä¸å®¶è¯å®¡è®°å½</p> |
| | | </div> |
| | | |
| | | <!-- ç»è´¹æ¨ä»è®°å½ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">ç»è´¹æ¨ä»è®°å½</span> |
| | | </div> |
| | | <FundGrid v-if="fundRecords.length"> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'warning'"> |
| | | {{ |
| | | row.hasAttachment |
| | | ? 'å·²ä¸ä¼ ' |
| | | : row.status === FundStatus.PAID |
| | | ? '缺失' |
| | | : 'ä¸éè¦' |
| | | }} |
| | | </Tag> |
| | | </template> |
| | | </FundGrid> |
| | | <p v-else class="text-gray-500">ææ ç»è´¹æ¨ä»è®°å½</p> |
| | | </div> |
| | | |
| | | <!-- ç³æ¥ä¹¦éä»¶ --> |
| | | <div |
| | | v-if="formData?.attachmentList?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">ç³æ¥ä¹¦éä»¶</span> |
| | | </div> |
| | | <div class="flex flex-wrap gap-3"> |
| | | <a |
| | | v-for="file in formData.attachmentList" |
| | | :key="file.id" |
| | | :href="file.previewURL || file.url" |
| | | target="_blank" |
| | | class="text-blue-500 hover:underline" |
| | | > |
| | | {{ file.name || `éä»¶-${file.id}` }} |
| | | </a> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteApplication, |
| | | getApplicationApproveProcessList, |
| | | getApplicationPage, |
| | | publishApplicationNotice, |
| | | } from '#/api/research/application'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { |
| | | ApplicationStatus, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | } from './data'; |
| | | import FundPanel from './modules/fund-panel.vue'; |
| | | import Form from './modules/form.vue'; |
| | | import ProcessSelectModal from './modules/process-select-modal.vue'; |
| | | import ReviewPanel from './modules/review-panel.vue'; |
| | | |
| | | /** 课é¢ç³æ¥ä¹¦å表 */ |
| | | defineOptions({ name: 'ResearchApplication' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ProcessModal, processModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessSelectModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ReviewModal, reviewModalApi] = useVbenModal({ |
| | | connectedComponent: ReviewPanel, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [FundModal, fundModalApi] = useVbenModal({ |
| | | connectedComponent: FundPanel, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ°å¢ç³æ¥ä¹¦ */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾ç³æ¥ä¹¦ï¼ä»
è稿ã已驳åå¯ç¼è¾ï¼ */ |
| | | function handleEdit(row: ResearchApplicationApi.Application) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çç³æ¥ä¹¦è¯¦æ
*/ |
| | | function handleDetail(row: ResearchApplicationApi.Application) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å é¤ç³æ¥ä¹¦ï¼ä»
è稿å¯å ï¼ */ |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting'), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await Promise.all(ids.map((id) => deleteApplication(id))); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨åè¡¨ï¼æµç¨ key ä¸åæ» */ |
| | | async function handleSubmit(row: ResearchApplicationApi.Application) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const processList = await getApplicationApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | | message.warning( |
| | | 'ææ å¯ç¨å®¡æ¹æµç¨ï¼è¯·å
å° BPM æµç¨è®¾è®¡å¨åå¸è¯¾é¢ç«é¡¹å®¡æ¹æµç¨', |
| | | ); |
| | | return; |
| | | } |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** ä¸å®¶è¯å®¡é¢æ¿ */ |
| | | function handleReview(row: ResearchApplicationApi.Application) { |
| | | reviewModalApi.setData({ id: row.id }).open(); |
| | | } |
| | | |
| | | /** ç»è´¹æ¨ä»é¢æ¿ */ |
| | | function handleFund(row: ResearchApplicationApi.Application) { |
| | | fundModalApi.setData({ id: row.id }).open(); |
| | | } |
| | | |
| | | /** åå¸ç«é¡¹éç¥ */ |
| | | async function handlePublishNotice(row: ResearchApplicationApi.Application) { |
| | | const hideLoading = message.loading({ content: 'åå¸ä¸...', duration: 0 }); |
| | | try { |
| | | await publishApplicationNotice(row.id!); |
| | | message.success('ç«é¡¹éç¥å·²åå¸å°ç³»ç»å
¬åï¼å¯å¨å
¬åæ¨¡åæ¥ç'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¿åº¦ */ |
| | | function handleProcessDetail(row: ResearchApplicationApi.Application) { |
| | | if (!row.processInstanceId) { |
| | | message.warning('è¯¥ç³æ¥ä¹¦å°æªæäº¤å®¡æ¹'); |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ |
| | | records, |
| | | }: { |
| | | records: ResearchApplicationApi.Application[]; |
| | | }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getApplicationPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<ResearchApplicationApi.Application>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ProcessModal @success="handleRefresh" /> |
| | | <ReviewModal @success="handleRefresh" /> |
| | | <FundModal @success="handleRefresh" /> |
| | | <Grid table-title="课é¢ç³æ¥ä¹¦å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['课é¢ç³æ¥ä¹¦']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['research:application:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['research:application:delete'], |
| | | popConfirm: { |
| | | title: 'æ¯å¦å 餿é䏿°æ®ï¼ï¼ä»
èç¨¿ç¶æå¯å é¤ï¼', |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #reviewPass="{ row }"> |
| | | <Tag v-if="row.reviewPass === 1" color="success">æ¨èç«é¡¹</Tag> |
| | | <Tag v-else-if="row.reviewPass === 0" color="error">䏿¨èç«é¡¹</Tag> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'default'"> |
| | | {{ row.hasAttachment ? 'æ' : 'æ ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['research:application:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['research:application:update'], |
| | | ifShow: () => |
| | | row.status === ApplicationStatus.DRAFT || |
| | | row.status === ApplicationStatus.REJECTED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['research:application:submit'], |
| | | ifShow: () => |
| | | row.status === ApplicationStatus.DRAFT || |
| | | row.status === ApplicationStatus.REJECTED, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ä¸å®¶è¯å®¡', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['research:review:query'], |
| | | onClick: handleReview.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ç»è´¹æ¨ä»', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['research:fund:query'], |
| | | onClick: handleFund.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åå¸ç«é¡¹éç¥', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['research:application:notice'], |
| | | ifShow: () => |
| | | row.status === ApplicationStatus.APPROVED && !row.noticeId, |
| | | onClick: handlePublishNotice.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['research:application:query'], |
| | | ifShow: () => !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['research:application:delete'], |
| | | ifShow: () => row.status === ApplicationStatus.DRAFT, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | import type { ResearchGuidelineApi } from '#/api/research/guideline'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createApplication, |
| | | getApplication, |
| | | updateApplication, |
| | | } from '#/api/research/application'; |
| | | import { |
| | | getGuideline, |
| | | getPublishedGuidelineList, |
| | | } from '#/api/research/guideline'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'ResearchApplicationForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formType = ref<FormType>('create'); |
| | | /** ä¾ãæå±è¯¾é¢æåã䏿ä¸ç±»å«å¸¦åºä½¿ç¨ */ |
| | | const guidelines = ref<ResearchGuidelineApi.Guideline[]>([]); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return $t('ui.actionTitle.create', ['课é¢ç³æ¥ä¹¦']); |
| | | } |
| | | if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['课é¢ç³æ¥ä¹¦']); |
| | | } |
| | | return '课é¢ç³æ¥ä¹¦è¯¦æ
'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 130, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** åªæäº¤å端 SaveReqVO æ¥åçåæ®µï¼é¿å
åä¼ category/reviewAvgScore çæ´¾çåæ®µ */ |
| | | async function buildSubmitData() { |
| | | const values = (await formApi.getValues()) as ResearchApplicationApi.Application; |
| | | return { |
| | | id: values.id, |
| | | guidelineId: values.guidelineId, |
| | | title: values.title, |
| | | memberId: values.memberId, |
| | | applicantName: values.applicantName, |
| | | workUnit: values.workUnit, |
| | | jobTitle: values.jobTitle, |
| | | contactPhone: values.contactPhone, |
| | | contactEmail: values.contactEmail, |
| | | teamMembers: values.teamMembers, |
| | | budgetAmount: values.budgetAmount, |
| | | planContent: values.planContent, |
| | | remark: values.remark, |
| | | // é件为å
¨éæ¿æ¢è¯ä¹ï¼æ¸
空å
¨é¨éä»¶æ¶å¿
é¡»æ¾å¼ä¼ 空æ°ç»ï¼ |
| | | // ä¼ null å端ä¼è§ä¸ºãä¸å¨éä»¶ãï¼å¯¼è´å 餿ä½ä¸çæ |
| | | blobIds: values.blobIds ?? [], |
| | | } as ResearchApplicationApi.Application; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = await buildSubmitData(); |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createApplication(data) |
| | | : updateApplication(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | guidelines.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | modalApi.lock(); |
| | | try { |
| | | if (data?.id) { |
| | | const application = await getApplication(data.id); |
| | | guidelines.value = await resolveGuidelineOptions(application); |
| | | formApi.setState({ |
| | | schema: useFormSchema(formType.value, formApi, guidelines.value), |
| | | }); |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | // å·²æé件转æä¸ä¼ ç»ä»¶å¯è¯å«çæä»¶å¯¹è±¡ï¼æäº¤æ¶æ valueKey=id æå为 blobId |
| | | const blobIds = (application.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues({ ...application, blobIds }, false); |
| | | } else { |
| | | guidelines.value = await getPublishedGuidelineList(); |
| | | formApi.setState({ |
| | | schema: useFormSchema(formType.value, formApi, guidelines.value), |
| | | }); |
| | | formApi.setDisabled(false); |
| | | await formApi.resetForm(); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** |
| | | * ç¼è¾/详æ
æ¶æåå¯è½å·²æªæ¢ï¼ä¸å¨ãå·²åå¸ãå表éï¼ï¼ |
| | | * éè¦åç¬ååå½åæå±æåè¡¥è¿é项ï¼å¦å䏿æ¾ç¤ºä¸ºç©ºã |
| | | */ |
| | | async function resolveGuidelineOptions( |
| | | application: ResearchApplicationApi.Application, |
| | | ): Promise<ResearchGuidelineApi.Guideline[]> { |
| | | const published = await getPublishedGuidelineList(); |
| | | const currentId = application.guidelineId; |
| | | if (!currentId || published.some((item) => item.id === currentId)) { |
| | | return published; |
| | | } |
| | | const current = await getGuideline(currentId); |
| | | return [...published, current]; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="getTitle" |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ResearchFundRecordApi } from '#/api/research/fund'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createFundRecord, updateFundRecord } from '#/api/research/fund'; |
| | | |
| | | import { FundStatus } from '../data'; |
| | | |
| | | defineOptions({ name: 'ResearchFundRecordForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const isEdit = ref(false); |
| | | /** ç³è¯·ç»è´¹åå»å·²ç»è®°æ¨ä»éé¢åçå¯ç¨ä½é¢ */ |
| | | const remainder = ref<number>(); |
| | | |
| | | const alertMessage = computed(() => { |
| | | if (remainder.value === undefined) { |
| | | return ''; |
| | | } |
| | | return `æ¬è¯¾é¢ç³è¯·ç»è´¹å¯ç¨ä½é¢ï¼${remainder.value.toFixed(2)} å
ï¼æ¨æ¬¾éé¢ç´¯è®¡ä¸å¾è¶
è¿ç³è¯·ç»è´¹`; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 130, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'applicationId', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'batchName', |
| | | label: 'æ¨ä»æ¹æ¬¡', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'å¦ï¼ç¬¬ä¸æ', |
| | | maxlength: 64, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'amount', |
| | | label: 'æ¨ä»éé¢ï¼å
ï¼', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥æ¬æ¬¡æ¨ä»éé¢', |
| | | precision: 2, |
| | | min: 0, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'payDate', |
| | | label: 'æ¨ä»æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æ¨ä»æ¥æ', |
| | | // å端为 LocalDateï¼åªè½ä¼ yyyy-MM-dd |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'payee', |
| | | label: 'æ¶æ¬¾åä½/è´¦æ·å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¶æ¬¾åä½æè´¦æ·å', |
| | | maxlength: 128, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'æ¨ä»ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_FUND_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©æ¨ä»ç¶æ', |
| | | }, |
| | | rules: 'selectRequired', |
| | | help: 'éæ©ãå·²æ¨ä»ãæ¶å¿
é¡»ä¸ä¼ æ¨ä»åè¯éä»¶', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'æ¨ä»åè¯', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | showDescription: true, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | const values = |
| | | (await formApi.getValues()) as ResearchFundRecordApi.FundRecord; |
| | | // ãå·²æ¨ä»ãå¿
é¡»æåè¯ï¼å
å¨å端æ¦ä¸éï¼é¿å
ç¨æ·ç½ç䏿¬¡è¯·æ± |
| | | if ( |
| | | values.status === FundStatus.PAID && |
| | | (!values.blobIds || values.blobIds.length === 0) |
| | | ) { |
| | | message.warning('æ¨ä»ç¶æä¸ºãå·²æ¨ä»ãæ¶ï¼è¯·å
ä¸ä¼ æ¨ä»åè¯éä»¶'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // é件为å
¨éæ¿æ¢è¯ä¹ï¼æ¸
空å
¨é¨åè¯æ¶å¿
é¡»æ¾å¼ä¼ 空æ°ç» |
| | | const data = { |
| | | id: values.id, |
| | | applicationId: values.applicationId, |
| | | batchName: values.batchName, |
| | | amount: values.amount, |
| | | payDate: values.payDate, |
| | | payee: values.payee, |
| | | status: values.status, |
| | | remark: values.remark, |
| | | blobIds: values.blobIds ?? [], |
| | | } as ResearchFundRecordApi.FundRecord; |
| | | try { |
| | | await (data.id ? updateFundRecord(data) : createFundRecord(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('ä¿åæå'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | isEdit.value = false; |
| | | remainder.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | applicationId: number; |
| | | remainder?: number; |
| | | record?: ResearchFundRecordApi.FundRecord; |
| | | }>(); |
| | | isEdit.value = !!data.record; |
| | | remainder.value = data.remainder; |
| | | await formApi.resetForm(); |
| | | if (data.record) { |
| | | const blobIds = (data.record.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues( |
| | | { |
| | | ...data.record, |
| | | applicationId: data.applicationId, |
| | | blobIds, |
| | | }, |
| | | false, |
| | | ); |
| | | } else { |
| | | await formApi.setValues( |
| | | { |
| | | applicationId: data.applicationId, |
| | | status: FundStatus.PENDING, |
| | | }, |
| | | false, |
| | | ); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="isEdit ? 'ç¼è¾æ¨æ¬¾è®°å½' : 'ç»è®°æ¨æ¬¾è®°å½'" |
| | | class="w-1/2" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <Alert |
| | | v-if="alertMessage" |
| | | class="mx-3 mb-3" |
| | | type="info" |
| | | show-icon |
| | | :message="alertMessage" |
| | | /> |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | import type { ResearchFundRecordApi } from '#/api/research/fund'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Alert, message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getApplication } from '#/api/research/application'; |
| | | import { deleteFundRecord, getFundRecordList } from '#/api/research/fund'; |
| | | |
| | | import { ApplicationStatus, FundStatus, useFundColumns } from '../data'; |
| | | import FundForm from './fund-form.vue'; |
| | | |
| | | defineOptions({ name: 'ResearchFundPanel' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const applicationId = ref<number>(); |
| | | const application = ref<ResearchApplicationApi.Application>(); |
| | | const records = ref<ResearchFundRecordApi.FundRecord[]>([]); |
| | | |
| | | /** ä»
ãå·²ç«é¡¹ãçç³æ¥åå¯ç»è®°æ¨æ¬¾ */ |
| | | const editable = computed( |
| | | () => application.value?.status === ApplicationStatus.APPROVED, |
| | | ); |
| | | |
| | | /** å·²ç»è®°æ¨ä»éé¢å计 */ |
| | | const registeredAmount = computed(() => |
| | | records.value.reduce((sum, item) => sum + (Number(item.amount) || 0), 0), |
| | | ); |
| | | |
| | | /** ç³è¯·ç»è´¹å¯ç¨ä½é¢ï¼è´æ°è¡¨ç¤ºå·²è¶
é¢ç»è®°ï¼ */ |
| | | const remainder = computed( |
| | | () => (Number(application.value?.budgetAmount) || 0) - registeredAmount.value, |
| | | ); |
| | | |
| | | const [FundFormModal, fundFormModalApi] = useVbenModal({ |
| | | connectedComponent: FundForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useFundColumns(true), |
| | | data: [], |
| | | minHeight: 280, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | } as VxeTableGridOptions<ResearchFundRecordApi.FundRecord>, |
| | | }); |
| | | |
| | | async function refresh() { |
| | | if (!applicationId.value) { |
| | | return; |
| | | } |
| | | const [recordList, applicationDetail] = await Promise.all([ |
| | | getFundRecordList(applicationId.value), |
| | | getApplication(applicationId.value), |
| | | ]); |
| | | records.value = recordList; |
| | | application.value = applicationDetail; |
| | | gridApi.setGridOptions({ data: recordList }); |
| | | } |
| | | |
| | | /** æ¨æ¬¾è®°å½å¢å æ¹åï¼æ¬é¢æ¿éè½½ï¼åæ¶éç¥åè¡¨é¡µå·æ° */ |
| | | async function handleChanged() { |
| | | await refresh(); |
| | | emit('success'); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | fundFormModalApi |
| | | .setData({ applicationId: applicationId.value!, remainder: remainder.value }) |
| | | .open(); |
| | | } |
| | | |
| | | function handleEdit(row: ResearchFundRecordApi.FundRecord) { |
| | | fundFormModalApi |
| | | .setData({ |
| | | applicationId: applicationId.value!, |
| | | // ç¼è¾æ¶æå½åè®°å½èªèº«éé¢å åï¼é¿å
ç¨æ·çå°ãä½é¢ãåå°äº§ç误解 |
| | | remainder: remainder.value + (Number(row.amount) || 0), |
| | | record: row, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | async function handleDelete(row: ResearchFundRecordApi.FundRecord) { |
| | | const hideLoading = message.loading({ content: 'å é¤ä¸...', duration: 0 }); |
| | | try { |
| | | await deleteFundRecord(row.id!); |
| | | message.success('å 餿å'); |
| | | await handleChanged(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | applicationId.value = undefined; |
| | | application.value = undefined; |
| | | records.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number }>(); |
| | | applicationId.value = data.id; |
| | | modalApi.lock(); |
| | | try { |
| | | const [recordList, applicationDetail] = await Promise.all([ |
| | | getFundRecordList(data.id), |
| | | getApplication(data.id), |
| | | ]); |
| | | records.value = recordList; |
| | | application.value = applicationDetail; |
| | | gridApi.setGridOptions({ data: recordList }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ç»è´¹æ¨ä»" class="w-4/5" :show-confirm-button="false"> |
| | | <div class="space-y-3"> |
| | | <Alert |
| | | v-if="!editable" |
| | | type="info" |
| | | show-icon |
| | | message="ä»
ãå·²ç«é¡¹ãç课é¢ç³æ¥ä¹¦å¯ä»¥ç»è®°ç»è´¹æ¨ä»è®°å½" |
| | | /> |
| | | <div class="flex flex-wrap items-center gap-6 rounded border border-border p-3"> |
| | | <div> |
| | | <span class="text-gray-500">课é¢åç§°ï¼</span> |
| | | <span class="font-medium">{{ application?.title || '-' }}</span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-500">ç«é¡¹ç¼å·ï¼</span> |
| | | <span class="font-medium">{{ application?.researchNo || 'æªç«é¡¹' }}</span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-500">ç³è¯·ç»è´¹ï¼</span> |
| | | <span class="font-medium"> |
| | | {{ Number(application?.budgetAmount || 0).toFixed(2) }} å
|
| | | </span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-500">å·²ç»è®°æ¨ä»ï¼</span> |
| | | <span class="font-medium">{{ registeredAmount.toFixed(2) }} å
</span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-500">å¯ç¨ä½é¢ï¼</span> |
| | | <span |
| | | class="font-medium" |
| | | :class="remainder < 0 ? 'text-red-500' : ''" |
| | | > |
| | | {{ remainder.toFixed(2) }} å
|
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="editable"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç»è®°æ¨æ¬¾è®°å½', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['research:fund:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </div> |
| | | |
| | | <Grid> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'warning'"> |
| | | {{ |
| | | row.hasAttachment |
| | | ? 'å·²ä¸ä¼ ' |
| | | : row.status === FundStatus.PAID |
| | | ? '缺失' |
| | | : 'ä¸éè¦' |
| | | }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | v-if="editable" |
| | | :actions="[ |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['research:fund:update'], |
| | | ifShow: () => row.status === FundStatus.PENDING, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['research:fund:delete'], |
| | | ifShow: () => row.status === FundStatus.PENDING, |
| | | popConfirm: { |
| | | title: `确认å é¤ã${row.batchName || 'æ¬æ¡'}ãæ¨æ¬¾è®°å½åï¼`, |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </div> |
| | | </Modal> |
| | | <FundFormModal @success="handleChanged" /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Select } from 'ant-design-vue'; |
| | | |
| | | import { submitApplication } from '#/api/research/application'; |
| | | |
| | | defineOptions({ name: 'ResearchApplicationProcessSelect' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const applicationId = ref<number>(); |
| | | const processList = ref<ResearchApplicationApi.ProcessDefinition[]>([]); |
| | | const selectedProcessKey = ref<string>(''); |
| | | |
| | | /** æµç¨ key åªè½æ¥èªåç«¯å®æ¶å表ï¼ç¦æ¢åç«¯åæ» */ |
| | | const processOptions = computed(() => |
| | | processList.value.map((item) => ({ label: item.name, value: item.key })), |
| | | ); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await submitApplication(applicationId.value!, selectedProcessKey.value); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æäº¤æå'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | applicationId.value = undefined; |
| | | selectedProcessKey.value = ''; |
| | | processList.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | id: number; |
| | | processList: ResearchApplicationApi.ProcessDefinition[]; |
| | | }>(); |
| | | applicationId.value = data.id; |
| | | processList.value = data.processList || []; |
| | | // é»è®¤éæ©ç¬¬ä¸ä¸ªæµç¨ |
| | | if (processList.value.length > 0) { |
| | | selectedProcessKey.value = processList.value[0]!.key; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="éæ©å®¡æ¹æµç¨" class="w-1/3"> |
| | | <div class="mb-4"> |
| | | <span class="mr-2">å®¡æ¹æµç¨ï¼</span> |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | :options="processOptions" |
| | | style="width: 240px" |
| | | placeholder="è¯·éæ©å®¡æ¹æµç¨" |
| | | /> |
| | | </div> |
| | | <p class="text-sm text-gray-500"> |
| | | æäº¤å便¬¡æµè½¬ï¼ç§ä¹¦å¤å½¢å¼å®¡æ¥ â ä¸å®¶è¯å®¡ â å¦ä¼ç«é¡¹å®¡æ¹ |
| | | </p> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createTopicReview, |
| | | updateTopicReview, |
| | | } from '#/api/research/application'; |
| | | |
| | | defineOptions({ name: 'ResearchTopicReviewForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | /** å个è¯åç»´åº¦ä¸æéï¼ä¸å端 ResearchTopicReviewServiceImpl ä¿æä¸è´ï¼ */ |
| | | const SCORE_ITEMS = [ |
| | | { fieldName: 'topicScore', label: 'éé¢ä»·å¼ä¸æä¹', weight: '0.3' }, |
| | | { fieldName: 'planScore', label: 'ç ç©¶æ¹æ¡å¯è¡æ§', weight: '0.3' }, |
| | | { fieldName: 'teamScore', label: 'å¢éä¸ç ç©¶åºç¡', weight: '0.2' }, |
| | | { fieldName: 'resultScore', label: '颿ææä¸æç', weight: '0.2' }, |
| | | ] as const; |
| | | |
| | | const schema: VbenFormSchema[] = [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'applicationId', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'evaluatorName', |
| | | label: 'ä¸å®¶å§å', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ä¸å®¶å§å', |
| | | maxlength: 64, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'evaluatorUnit', |
| | | label: 'ä¸å®¶åä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ä¸å®¶æå¨åä½', |
| | | maxlength: 128, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'evaluatorTitle', |
| | | label: 'ä¸å®¶èç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ä¸å®¶èç§°', |
| | | maxlength: 64, |
| | | }, |
| | | }, |
| | | ...SCORE_ITEMS.map((item) => ({ |
| | | fieldName: item.fieldName, |
| | | label: `${item.label}ï¼æé ${item.weight}ï¼`, |
| | | component: 'InputNumber' as const, |
| | | componentProps: { |
| | | class: '!w-full', |
| | | placeholder: '0 ~ 100', |
| | | min: 0, |
| | | max: 100, |
| | | precision: 2, |
| | | addonAfter: 'å', |
| | | }, |
| | | })), |
| | | { |
| | | fieldName: 'conclusion', |
| | | label: 'è¯å®¡ç»è®º', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_REVIEW_CONCLUSION, 'number'), |
| | | placeholder: 'è¯·éæ©è¯å®¡ç»è®º', |
| | | allowClear: true, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'opinion', |
| | | label: 'è¯å®¡æè§', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¯¹è¯¥è¯¾é¢çè¯å®¡æè§', |
| | | maxlength: 1000, |
| | | showCount: true, |
| | | autoSize: { minRows: 4, maxRows: 8 }, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 170, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema, |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = |
| | | (await formApi.getValues()) as ResearchApplicationApi.TopicReview; |
| | | try { |
| | | await (data.id |
| | | ? updateTopicReview(data) |
| | | : createTopicReview(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('ä¿åæåï¼å æåè®¡ä¸æåå·²èªå¨éç®'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | applicationId: number; |
| | | review?: ResearchApplicationApi.TopicReview; |
| | | }>(); |
| | | await formApi.resetForm(); |
| | | if (data.review) { |
| | | await formApi.setValues( |
| | | { ...data.review, applicationId: data.applicationId }, |
| | | false, |
| | | ); |
| | | } else { |
| | | await formApi.setValues({ applicationId: data.applicationId }, false); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ä¸å®¶è¯åå½å
¥" class="w-1/2" :close-on-click-modal="false"> |
| | | <Form class="mx-3" /> |
| | | <p class="mx-3 text-sm text-gray-500"> |
| | | å æå计 = éé¢ä»·å¼Ã0.3 + æ¹æ¡å¯è¡æ§Ã0.3 + å¢éåºç¡Ã0.2 + 颿ææÃ0.2ï¼ç±ç³»ç»èªå¨è®¡ç® |
| | | </p> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ResearchApplicationApi } from '#/api/research/application'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Alert, message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteTopicReview, |
| | | getApplication, |
| | | getTopicReviewList, |
| | | } from '#/api/research/application'; |
| | | |
| | | import { ApplicationStatus, useReviewColumns } from '../data'; |
| | | import ReviewForm from './review-form.vue'; |
| | | |
| | | defineOptions({ name: 'ResearchTopicReviewPanel' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const applicationId = ref<number>(); |
| | | const application = ref<ResearchApplicationApi.Application>(); |
| | | const reviews = ref<ResearchApplicationApi.TopicReview[]>([]); |
| | | |
| | | /** ä»
ã审æ¹ä¸ãçç³æ¥åå¯å¢å æ¹ä¸å®¶è¯å */ |
| | | const editable = computed( |
| | | () => application.value?.status === ApplicationStatus.APPROVING, |
| | | ); |
| | | |
| | | const STATUS_LABELS: Record<number, string> = { |
| | | [ApplicationStatus.DRAFT]: 'è稿', |
| | | [ApplicationStatus.APPROVING]: '审æ¹ä¸', |
| | | [ApplicationStatus.APPROVED]: 'å·²ç«é¡¹', |
| | | [ApplicationStatus.REJECTED]: '已驳å', |
| | | }; |
| | | |
| | | const statusLabel = computed(() => |
| | | application.value?.status === undefined || |
| | | application.value?.status === null |
| | | ? 'æªç¥' |
| | | : (STATUS_LABELS[application.value.status] ?? 'æªç¥'), |
| | | ); |
| | | |
| | | const [ReviewFormModal, reviewFormModalApi] = useVbenModal({ |
| | | connectedComponent: ReviewForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: useReviewColumns(true), |
| | | data: [], |
| | | minHeight: 300, |
| | | autoResize: true, |
| | | border: true, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | pagerConfig: { |
| | | enabled: false, |
| | | }, |
| | | toolbarConfig: { |
| | | enabled: false, |
| | | }, |
| | | } as VxeTableGridOptions<ResearchApplicationApi.TopicReview>, |
| | | }); |
| | | |
| | | /** æåè¯å®¡æç»å¹¶åæ¥ä¸»åæ±æ»å¼ï¼å端å¨å¢å æ¹åå·²éç®ååä¸ç»è®ºï¼ */ |
| | | async function refresh() { |
| | | if (!applicationId.value) { |
| | | return; |
| | | } |
| | | const [reviewList, applicationDetail] = await Promise.all([ |
| | | getTopicReviewList(applicationId.value), |
| | | getApplication(applicationId.value), |
| | | ]); |
| | | reviews.value = reviewList; |
| | | application.value = applicationDetail; |
| | | gridApi.setGridOptions({ data: reviewList }); |
| | | } |
| | | |
| | | /** è¯å®¡æç»åååï¼æ¬é¢æ¿éè½½ï¼åæ¶éç¥åè¡¨é¡µå·æ°ï¼ååä¼å±ç¤ºå¨åè¡¨ï¼ */ |
| | | async function handleChanged() { |
| | | await refresh(); |
| | | emit('success'); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | reviewFormModalApi |
| | | .setData({ applicationId: applicationId.value! }) |
| | | .open(); |
| | | } |
| | | |
| | | function handleEdit(row: ResearchApplicationApi.TopicReview) { |
| | | reviewFormModalApi |
| | | .setData({ applicationId: applicationId.value!, review: row }) |
| | | .open(); |
| | | } |
| | | |
| | | async function handleDelete(row: ResearchApplicationApi.TopicReview) { |
| | | const hideLoading = message.loading({ content: 'å é¤ä¸...', duration: 0 }); |
| | | try { |
| | | await deleteTopicReview(row.id!); |
| | | message.success('å 餿åï¼åå䏿åå·²éæ°æ±æ»'); |
| | | await handleChanged(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | applicationId.value = undefined; |
| | | application.value = undefined; |
| | | reviews.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number }>(); |
| | | applicationId.value = data.id; |
| | | modalApi.lock(); |
| | | try { |
| | | await refresh(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ä¸å®¶è¯å®¡" class="w-4/5" :show-confirm-button="false"> |
| | | <div class="space-y-3"> |
| | | <Alert |
| | | v-if="!editable" |
| | | type="info" |
| | | show-icon |
| | | :message="`å½åç³æ¥åç¶æä¸ºã${statusLabel}ãï¼ä»
ã审æ¹ä¸ãç¶æå¯å½å
¥æä¿®æ¹ä¸å®¶è¯å`" |
| | | /> |
| | | <div class="flex flex-wrap items-center gap-6 rounded border border-border p-3"> |
| | | <div> |
| | | <span class="text-gray-500">课é¢åç§°ï¼</span> |
| | | <span class="font-medium">{{ application?.title || '-' }}</span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-500">è¯å®¡å æååï¼</span> |
| | | <span class="font-medium"> |
| | | {{ |
| | | application?.reviewAvgScore === null || |
| | | application?.reviewAvgScore === undefined |
| | | ? 'æªæ±æ»' |
| | | : Number(application.reviewAvgScore).toFixed(2) |
| | | }} |
| | | </span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-500">è¯å®¡ç»è®ºï¼</span> |
| | | <Tag |
| | | v-if="application?.reviewPass !== null && application?.reviewPass !== undefined" |
| | | :color="application.reviewPass === 1 ? 'success' : 'error'" |
| | | > |
| | | {{ application.reviewPass === 1 ? 'æ¨èç«é¡¹' : '䏿¨èç«é¡¹' }} |
| | | </Tag> |
| | | <span v-else>æªæ±æ»</span> |
| | | </div> |
| | | <div> |
| | | <span class="text-gray-500">å·²å½å
¥ä¸å®¶æ°ï¼</span> |
| | | <span class="font-medium">{{ reviews.length }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="editable"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'å½å
¥ä¸å®¶è¯å', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['research:review:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </div> |
| | | |
| | | <Grid> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | v-if="editable" |
| | | :actions="[ |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['research:review:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['research:review:delete'], |
| | | popConfirm: { |
| | | title: `确认å é¤ä¸å®¶ã${row.evaluatorName}ãçè¯åè®°å½åï¼å é¤ååå䏿åå°éæ°æ±æ»`, |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </div> |
| | | </Modal> |
| | | <ReviewFormModal @success="handleChanged" /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** è¯¾é¢æååå¸ç¶æï¼å¯¹åºåå
¸ research_guideline_statusï¼ */ |
| | | export const GuidelineStatus = { |
| | | DRAFT: 0, // è稿 |
| | | PUBLISHED: 20, // å·²åå¸ |
| | | CLOSED: 30, // å·²æªæ¢ |
| | | } as const; |
| | | |
| | | /** 表åé
ç½® */ |
| | | export function useFormSchema(formType: FormType): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æåç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'æåç¼å·ç±ç³»ç»èªå¨çæ', |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => formType !== 'create', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æååç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è¯¾é¢æååç§°', |
| | | maxlength: 255, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'category', |
| | | label: '课é¢ç±»å«', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, 'number'), |
| | | placeholder: 'è¯·éæ©è¯¾é¢ç±»å«', |
| | | allowClear: true, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'applyStartTime', |
| | | label: 'ç³æ¥å¼å§æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç³æ¥å¼å§æ¶é´', |
| | | showTime: true, |
| | | // å端为 LocalDateTimeï¼å¿
须带æ¶åç§ |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | style: { width: '100%' }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'applyEndTime', |
| | | label: 'ç³æ¥æªæ¢æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç³æ¥æªæ¢æ¶é´', |
| | | showTime: true, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | style: { width: '100%' }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | help: 'æªæ¢æ¶é´ä¹åä¸è½åæäº¤è¯¾é¢ç³æ¥ä¹¦', |
| | | }, |
| | | { |
| | | fieldName: 'content', |
| | | label: 'æåæ£æ', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æåæ£æï¼ç ç©¶æ¹åãç³æ¥è¦æ±çï¼', |
| | | autoSize: { minRows: 6, maxRows: 12 }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'æåéä»¶', |
| | | component: 'FileUpload', |
| | | // valueKey 为 idï¼æäº¤æ¶ formApi 伿æä»¶å¯¹è±¡æå为 blobId æ°ç» |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | showDescription: formType !== 'detail', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'æåç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æåç¼å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'æååç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æååç§°', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'category', |
| | | label: '课é¢ç±»å«', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, 'number'), |
| | | placeholder: 'è¯·éæ©è¯¾é¢ç±»å«', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'publishStatus', |
| | | label: 'åå¸ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©åå¸ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表åé
ç½® */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | type: 'checkbox', |
| | | width: 50, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'no', |
| | | title: 'æåç¼å·', |
| | | width: 180, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'title', |
| | | title: 'æååç§°', |
| | | minWidth: 220, |
| | | }, |
| | | { |
| | | field: 'category', |
| | | title: '课é¢ç±»å«', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'applyStartTime', |
| | | title: 'ç³æ¥å¼å§æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'applyEndTime', |
| | | title: 'ç³æ¥æªæ¢æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'publishStatus', |
| | | title: 'åå¸ç¶æ', |
| | | minWidth: 95, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.RESEARCH_GUIDELINE_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'publishTime', |
| | | title: 'å叿¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: 'éä»¶', |
| | | minWidth: 80, |
| | | slots: { default: 'hasAttachment' }, |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | | title: 'å建人', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 260, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ResearchGuidelineApi } from '#/api/research/guideline'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | closeGuideline, |
| | | deleteGuideline, |
| | | getGuidelinePage, |
| | | publishGuideline, |
| | | } from '#/api/research/guideline'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { GuidelineStatus, useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** è¯¾é¢æåå表 */ |
| | | defineOptions({ name: 'ResearchGuideline' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ°å¢æå */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾æåï¼ä»
è稿å¯ç¼è¾ï¼ */ |
| | | function handleEdit(row: ResearchGuidelineApi.Guideline) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çæå详æ
*/ |
| | | function handleDetail(row: ResearchGuidelineApi.Guideline) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å 餿åï¼ä»
èç¨¿ä¸æ ç³æ¥ä¹¦å¯å ï¼ */ |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting'), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await Promise.all(ids.map((id) => deleteGuideline(id))); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** å叿å */ |
| | | async function handlePublish(row: ResearchGuidelineApi.Guideline) { |
| | | const hideLoading = message.loading({ content: 'åå¸ä¸...', duration: 0 }); |
| | | try { |
| | | await publishGuideline(row.id!); |
| | | message.success('å叿åï¼ä¼åç°å¨å¯ä»¥æ®æ¤æäº¤è¯¾é¢ç³æ¥'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æªæ¢æå */ |
| | | async function handleClose(row: ResearchGuidelineApi.Guideline) { |
| | | const hideLoading = message.loading({ content: 'æªæ¢ä¸...', duration: 0 }); |
| | | try { |
| | | await closeGuideline(row.id!); |
| | | message.success('å·²æªæ¢ï¼è¯¥æåä¸åæ¥åæ°ç课é¢ç³æ¥'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ |
| | | records, |
| | | }: { |
| | | records: ResearchGuidelineApi.Guideline[]; |
| | | }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getGuidelinePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<ResearchGuidelineApi.Guideline>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="è¯¾é¢æåå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['è¯¾é¢æå']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['research:guideline:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['research:guideline:delete'], |
| | | popConfirm: { |
| | | title: 'æ¯å¦å 餿é䏿°æ®ï¼ï¼ä»
èç¨¿ç¶æä¸æ 课é¢ç³æ¥ä¹¦çæåå¯å é¤ï¼', |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'default'"> |
| | | {{ row.hasAttachment ? 'æ' : 'æ ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['research:guideline:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['research:guideline:update'], |
| | | ifShow: () => row.publishStatus === GuidelineStatus.DRAFT, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åå¸', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['research:guideline:publish'], |
| | | ifShow: () => row.publishStatus === GuidelineStatus.DRAFT, |
| | | popConfirm: { |
| | | title: `确认å叿å ${row.no} åï¼åå¸åä¸å¯åä¿®æ¹ï¼ä¼å坿®æ¤æäº¤è¯¾é¢ç³æ¥`, |
| | | confirm: handlePublish.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: 'æªæ¢', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['research:guideline:publish'], |
| | | ifShow: () => row.publishStatus === GuidelineStatus.PUBLISHED, |
| | | popConfirm: { |
| | | title: `ç¡®è®¤æªæ¢æå ${row.no} åï¼æªæ¢åä¸åæ¥åæ°ç课é¢ç³æ¥`, |
| | | confirm: handleClose.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['research:guideline:delete'], |
| | | ifShow: () => row.publishStatus === GuidelineStatus.DRAFT, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { ResearchGuidelineApi } from '#/api/research/guideline'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createGuideline, |
| | | getGuideline, |
| | | updateGuideline, |
| | | } from '#/api/research/guideline'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | defineOptions({ name: 'ResearchGuidelineForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return $t('ui.actionTitle.create', ['è¯¾é¢æå']); |
| | | } |
| | | if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['è¯¾é¢æå']); |
| | | } |
| | | return 'è¯¾é¢æå详æ
'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useFormSchema(formType.value), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as ResearchGuidelineApi.Guideline; |
| | | // é件为å
¨éæ¿æ¢è¯ä¹ï¼æ¸
空å
¨é¨éä»¶æ¶å¿
é¡»æ¾å¼ä¼ 空æ°ç»ï¼ |
| | | // ä¼ null å端ä¼è§ä¸ºãä¸å¨éä»¶ãï¼å¯¼è´å 餿ä½ä¸çæ |
| | | data.blobIds = data.blobIds ?? []; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createGuideline(data) |
| | | : updateGuideline(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | formApi.setState({ schema: useFormSchema(formType.value) }); |
| | | if (!data?.id) { |
| | | await formApi.resetForm(); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const guideline = await getGuideline(data.id); |
| | | // å·²æé件转æä¸ä¼ ç»ä»¶å¯è¯å«çæä»¶å¯¹è±¡ï¼æäº¤æ¶æ valueKey=id æå为 blobId |
| | | const blobIds = (guideline.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues({ ...guideline, blobIds }, false); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="getTitle" |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import { AreaCascader } from '#/components/area'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** å
¥ä¼ç³è¯·ç¶æï¼å¯¹åºåå
¸ ums_application_statusï¼ */ |
| | | export const ApplicationStatus = { |
| | | DRAFT: 0, // è稿 |
| | | APPROVING: 15, // 审æ¹ä¸ |
| | | APPROVED: 20, // å·²éè¿ |
| | | REJECTED: 35, // 已驳å |
| | | } as const; |
| | | |
| | | /** ä¼åç±»åï¼å¯¹åºåå
¸ ums_member_typeï¼ */ |
| | | export const MemberType = { |
| | | PERSONAL: 1, // 个人ä¼å |
| | | UNIT: 2, // åä½ä¼å |
| | | } as const; |
| | | |
| | | /** æ ¹æ®ä¼åç±»åè·åç级åå
¸é项 */ |
| | | export function getGradeDictOptions(memberType?: number) { |
| | | return getDictOptions( |
| | | memberType === MemberType.UNIT |
| | | ? DICT_TYPE.UMS_UNIT_GRADE |
| | | : DICT_TYPE.UMS_PERSONAL_GRADE, |
| | | 'string', |
| | | ); |
| | | } |
| | | |
| | | /** 表åé
ç½® */ |
| | | export function useFormSchema(formType: FormType): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ç³è¯·ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ç³è¯·ç¼å·ç±ç³»ç»èªå¨çæ', |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'memberType', |
| | | label: 'ä¼åç±»å', |
| | | component: 'Select', |
| | | componentProps: (_values, form) => ({ |
| | | options: getDictOptions(DICT_TYPE.UMS_MEMBER_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©ä¼åç±»å', |
| | | allowClear: true, |
| | | disabled: formType === 'detail', |
| | | onChange: () => { |
| | | // 忢ä¼åç±»å忏
空已éç级ï¼ä¸ªäººä¸åä½ä½¿ç¨ä¸åç级åå
¸ï¼ |
| | | form.setFieldValue('memberGrade', undefined); |
| | | }, |
| | | }), |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'applicantName', |
| | | label: 'ç³è¯·äºº/åä½åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '个人填åå§åï¼åä½å¡«ååä½åç§°', |
| | | maxlength: 64, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'memberGrade', |
| | | label: 'ç³è¯·ç级', |
| | | component: 'Select', |
| | | componentProps: (values) => ({ |
| | | options: getGradeDictOptions(values.memberType), |
| | | placeholder: 'è¯·éæ©ç³è¯·ç级', |
| | | allowClear: true, |
| | | disabled: formType === 'detail', |
| | | }), |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'contactPhone', |
| | | label: 'èç³»çµè¯ï¼ç»å½è´¦å·ï¼', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç³»çµè¯ï¼å°ä½ä¸ºä¼åç³»ç»ç»å½è´¦å·', |
| | | maxlength: 20, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | help: '审æ¹éè¿åå°èªå¨å建系ç»ç»å½è´¦å·ï¼ç¨æ·åä¸åå§å¯ç å为èç³»çµè¯ï¼è¯·å¡å¿
å¡«ååç¡®', |
| | | }, |
| | | { |
| | | fieldName: 'contactEmail', |
| | | label: 'é®ç®±', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é®ç®±', |
| | | maxlength: 128, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'workUnit', |
| | | label: 'å·¥ä½åä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å·¥ä½åä½ï¼ä¸ªäººä¼åå¡«åï¼', |
| | | maxlength: 128, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.PERSONAL, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'èç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç§°ï¼ä¸ªäººä¼åå¡«åï¼', |
| | | maxlength: 64, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.PERSONAL, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'legalPerson', |
| | | label: 'æ³å®ä»£è¡¨äºº', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ³å®ä»£è¡¨äººï¼åä½ä¼åå¡«åï¼', |
| | | maxlength: 64, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'organizationCode', |
| | | label: 'ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ï¼åä½ä¼åå¡«åï¼', |
| | | maxlength: 64, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'researchDirection', |
| | | label: 'ç ç©¶æ¹å', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç ç©¶æ¹å', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'regionCode', |
| | | label: 'å°åº', |
| | | component: markRaw(AreaCascader), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å°åº', |
| | | allowClear: true, |
| | | class: '!w-full', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'industry', |
| | | label: 'è¡ä¸', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_INDUSTRY, 'string'), |
| | | placeholder: 'è¯·éæ©è¡ä¸', |
| | | allowClear: true, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_APPLICATION_STATUS, 'number'), |
| | | disabled: true, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => formType !== 'create', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | showDescription: formType !== 'detail', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ç³è¯·ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç³è¯·ç¼å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_APPLICATION_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'memberType', |
| | | label: 'ä¼åç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_MEMBER_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©ä¼åç±»å', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表åé
ç½® */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | type: 'checkbox', |
| | | width: 50, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'no', |
| | | title: 'ç³è¯·ç¼å·', |
| | | width: 180, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'applicantName', |
| | | title: 'ç³è¯·äºº/åä½', |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'memberType', |
| | | title: 'ä¼åç±»å', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.UMS_MEMBER_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'memberGrade', |
| | | title: 'ç³è¯·ç级', |
| | | minWidth: 100, |
| | | slots: { default: 'memberGrade' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 95, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.UMS_APPLICATION_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: 'éä»¶', |
| | | minWidth: 80, |
| | | slots: { default: 'hasAttachment' }, |
| | | }, |
| | | { |
| | | field: 'creatorName', |
| | | title: 'å建人', |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 165, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 290, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { UmsApplicationApi } from '#/api/ums/application'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { computed, h, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { formatDateTime } from '@vben/utils'; |
| | | |
| | | import { Alert, Spin } from 'ant-design-vue'; |
| | | |
| | | import { getApplication } from '#/api/ums/application'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | import { ApplicationStatus, MemberType } from '../../data'; |
| | | |
| | | defineOptions({ name: 'UmsApplicationApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const loading = ref(false); |
| | | const formData = ref<UmsApplicationApi.Application>(); |
| | | |
| | | /** ææ¬åæ®µç©ºå¼å ä½ */ |
| | | function textRender(val: unknown) { |
| | | return val === 0 || val ? String(val) : '-'; |
| | | } |
| | | |
| | | /** ç³è¯·ççº§ï¼æä¼åç±»åå个人/åä½ä¸åçç级åå
¸ */ |
| | | function getGradeDictType(memberType?: number) { |
| | | return memberType === MemberType.UNIT |
| | | ? DICT_TYPE.UMS_UNIT_GRADE |
| | | : DICT_TYPE.UMS_PERSONAL_GRADE; |
| | | } |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | | column: 2, |
| | | schema: [ |
| | | { label: 'ç³è¯·ç¼å·', field: 'no', render: textRender }, |
| | | { |
| | | label: 'ä¼åç±»å', |
| | | field: 'memberType', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.UMS_MEMBER_TYPE, value: val }), |
| | | }, |
| | | { label: 'ç³è¯·äºº/åä½åç§°', field: 'applicantName', render: textRender }, |
| | | { |
| | | label: 'ç³è¯·ç级', |
| | | field: 'memberGrade', |
| | | render: (val, data) => |
| | | h(DictTag, { |
| | | type: getGradeDictType(data?.memberType), |
| | | value: val, |
| | | }), |
| | | }, |
| | | { |
| | | label: 'ç¶æ', |
| | | field: 'status', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.UMS_APPLICATION_STATUS, value: val }), |
| | | }, |
| | | { label: 'èç³»çµè¯', field: 'contactPhone', render: textRender }, |
| | | { label: 'é®ç®±', field: 'contactEmail', render: textRender }, |
| | | { |
| | | label: 'ç ç©¶æ¹å', |
| | | field: 'researchDirection', |
| | | render: textRender, |
| | | span: 2, |
| | | }, |
| | | { |
| | | label: 'æ³å®ä»£è¡¨äºº', |
| | | field: 'legalPerson', |
| | | render: textRender, |
| | | show: (data) => data?.memberType === MemberType.UNIT, |
| | | }, |
| | | { |
| | | label: 'ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ', |
| | | field: 'organizationCode', |
| | | render: textRender, |
| | | show: (data) => data?.memberType === MemberType.UNIT, |
| | | }, |
| | | { |
| | | label: 'å·¥ä½åä½', |
| | | field: 'workUnit', |
| | | render: textRender, |
| | | show: (data) => data?.memberType === MemberType.PERSONAL, |
| | | }, |
| | | { |
| | | label: 'èç§°', |
| | | field: 'title', |
| | | render: textRender, |
| | | show: (data) => data?.memberType === MemberType.PERSONAL, |
| | | }, |
| | | { |
| | | label: 'å°åºç¼ç ', |
| | | field: 'regionCode', |
| | | render: textRender, |
| | | }, |
| | | { |
| | | label: 'è¡ä¸', |
| | | field: 'industry', |
| | | render: (val) => h(DictTag, { type: DICT_TYPE.UMS_INDUSTRY, value: val }), |
| | | }, |
| | | { label: 'å建人', field: 'creatorName', render: textRender }, |
| | | { |
| | | label: 'å建æ¶é´', |
| | | field: 'createTime', |
| | | render: (val) => (val ? (formatDateTime(val) as string) : '-'), |
| | | }, |
| | | { label: '夿³¨', field: 'remark', render: textRender, span: 2 }, |
| | | ] as DescriptionItemSchema[], |
| | | }); |
| | | |
| | | /** è´¦å·åæ®æéï¼å®¡æ¹ä¸æç¤ºå®¡æ¹äººæ ¸å¯¹èç³»çµè¯ï¼éè¿åæç¤ºè´¦å·å·²å¼éååå§å¯ç */ |
| | | const credentialTip = computed(() => { |
| | | const status = formData.value?.status; |
| | | const phone = formData.value?.contactPhone; |
| | | if (status === ApplicationStatus.APPROVING) { |
| | | return { |
| | | type: 'warning' as const, |
| | | message: `审æ¹éè¿åå°èªå¨ä¸ºç³è¯·äººå建系ç»ç»å½è´¦å·ï¼ç¨æ·åä¸åå§å¯ç å为èç³»çµè¯ ${phone || 'æªå¡«å'}ï¼ï¼è¯·å
æ ¸å¯¹èç³»çµè¯æ¯å¦åç¡®ååéè¿ã`, |
| | | }; |
| | | } |
| | | if (status === ApplicationStatus.APPROVED) { |
| | | if (!phone) { |
| | | return { |
| | | type: 'warning' as const, |
| | | message: |
| | | 'æªå¡«åèç³»çµè¯ï¼è´¦å·ç¨æ·å/å¯ç 使ç¨äºå
åºå¼ unknownï¼è¯·å°ä¼ååè¡¨æ ¸å®å¹¶æå·¥ä¿®æ£ã', |
| | | }; |
| | | } |
| | | return { |
| | | type: 'success' as const, |
| | | message: `ç³»ç»ç»å½è´¦å·å·²èªå¨å¼éï¼ç¨æ·å ${phone}ï¼åå§å¯ç ${phone}ã请éç¥ç³è¯·äººå°½å¿«ç»å½å¹¶ä¿®æ¹å¯ç ã`, |
| | | }; |
| | | } |
| | | return null; |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | loading.value = true; |
| | | formData.value = await getApplication(Number(props.id)); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <ContentWrap class="m-2"> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="space-y-4"> |
| | | <!-- è´¦å·åæ®æé --> |
| | | <Alert |
| | | v-if="credentialTip" |
| | | :type="credentialTip.type" |
| | | :message="credentialTip.message" |
| | | show-icon |
| | | /> |
| | | <!-- å
¥ä¼ç³è¯·åºæ¬ä¿¡æ¯ --> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">å
¥ä¼ç³è¯·åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <Descriptions :data="formData" /> |
| | | </div> |
| | | <!-- è¯æææéä»¶ --> |
| | | <div |
| | | v-if="formData?.attachmentList?.length" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="inline-block h-4 w-1 rounded bg-primary" /> |
| | | <span class="text-base font-semibold">è¯æææéä»¶</span> |
| | | </div> |
| | | <div class="flex flex-wrap gap-3"> |
| | | <a |
| | | v-for="file in formData.attachmentList" |
| | | :key="file.id" |
| | | :href="file.previewURL || file.url" |
| | | target="_blank" |
| | | class="text-blue-500 hover:underline" |
| | | > |
| | | {{ file.name || file.originalFilename || `éä»¶-${file.id}` }} |
| | | </a> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </ContentWrap> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { UmsApplicationApi } from '#/api/ums/application'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteApplication, |
| | | getApplicationApproveProcessList, |
| | | getApplicationPage, |
| | | } from '#/api/ums/application'; |
| | | |
| | | import { |
| | | ApplicationStatus, |
| | | MemberType, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import ProcessSelectModal from './modules/process-select-modal.vue'; |
| | | |
| | | /** å
¥ä¼ç³è¯·å表 */ |
| | | defineOptions({ name: 'UmsApplication' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ProcessModal, processModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessSelectModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ°å¢å
¥ä¼ç³è¯· */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾å
¥ä¼ç³è¯·ï¼ä»
è稿ã已驳åå¯ç¼è¾ï¼ */ |
| | | function handleEdit(row: UmsApplicationApi.Application) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çå
¥ä¼ç³è¯·è¯¦æ
*/ |
| | | function handleDetail(row: UmsApplicationApi.Application) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å é¤å
¥ä¼ç³è¯·ï¼ä»
è稿å¯å ï¼ */ |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: 'å é¤ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await Promise.all(ids.map((id) => deleteApplication(id))); |
| | | message.success('å 餿å'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æäº¤å®¡æ¹ - å
è·åæµç¨å表 */ |
| | | async function handleSubmit(row: UmsApplicationApi.Application) { |
| | | const hideLoading = message.loading({ |
| | | content: 'è·åå®¡æ¹æµç¨...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const processList = await getApplicationApproveProcessList(); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | | message.warning('ææ å¯ç¨å®¡æ¹æµç¨ï¼è¯·å
å° BPM æµç¨è®¾è®¡å¨åå¸å
¥ä¼å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¯¦æ
*/ |
| | | function handleProcessDetail(row: UmsApplicationApi.Application) { |
| | | if (!row.processInstanceId) { |
| | | message.warning('该ç³è¯·å°æªæäº¤å®¡æ¹'); |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ |
| | | records, |
| | | }: { |
| | | records: UmsApplicationApi.Application[]; |
| | | }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | /** ç级åå
¸æ å°ï¼æ ¹æ®ä¼åç±»åæ¾ç¤ºå¯¹åºçç级åç§° */ |
| | | const personalGradeOptions = getDictOptions(DICT_TYPE.UMS_PERSONAL_GRADE, 'string'); |
| | | const unitGradeOptions = getDictOptions(DICT_TYPE.UMS_UNIT_GRADE, 'string'); |
| | | function getGradeLabel(row: UmsApplicationApi.Application) { |
| | | const options = |
| | | row.memberType === MemberType.UNIT |
| | | ? unitGradeOptions |
| | | : personalGradeOptions; |
| | | return options?.find((o) => o.value === row.memberGrade)?.label || row.memberGrade; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getApplicationPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<UmsApplicationApi.Application>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ProcessModal @success="handleRefresh" /> |
| | | <Grid table-title="å
¥ä¼ç³è¯·å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢å
¥ä¼ç³è¯·', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['ums:application:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['ums:application:delete'], |
| | | popConfirm: { |
| | | title: 'æ¯å¦å 餿é䏿°æ®ï¼ï¼ä»
èç¨¿ç¶æå¯å é¤ï¼', |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #memberGrade="{ row }"> |
| | | <span>{{ getGradeLabel(row) }}</span> |
| | | </template> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'default'"> |
| | | {{ row.hasAttachment ? 'æ' : 'æ ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: '详æ
', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['ums:application:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['ums:application:update'], |
| | | ifShow: () => |
| | | row.status === ApplicationStatus.DRAFT || |
| | | row.status === ApplicationStatus.REJECTED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['ums:application:submit'], |
| | | ifShow: () => |
| | | row.status === ApplicationStatus.DRAFT || |
| | | row.status === ApplicationStatus.REJECTED, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['ums:application:query'], |
| | | ifShow: () => !!row.processInstanceId, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['ums:application:delete'], |
| | | ifShow: () => row.status === ApplicationStatus.DRAFT, |
| | | popConfirm: { |
| | | title: `确认å é¤ç³è¯· ${row.no} åï¼`, |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { UmsApplicationApi } from '#/api/ums/application'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createApplication, |
| | | getApplication, |
| | | updateApplication, |
| | | } from '#/api/ums/application'; |
| | | |
| | | import { ApplicationStatus, useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formData = ref<UmsApplicationApi.Application>({ |
| | | memberType: 1, |
| | | applicantName: '', |
| | | memberGrade: '', |
| | | contactPhone: '', |
| | | contactEmail: '', |
| | | legalPerson: '', |
| | | organizationCode: '', |
| | | researchDirection: '', |
| | | workUnit: '', |
| | | title: '', |
| | | regionCode: '', |
| | | industry: '', |
| | | remark: '', |
| | | }); |
| | | |
| | | const formType = ref<FormType>('create'); // 表åç±»åï¼'create' | 'edit' | 'detail' |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return $t('ui.actionTitle.create', ['å
¥ä¼ç³è¯·']); |
| | | } else if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['å
¥ä¼ç³è¯·']); |
| | | } else { |
| | | return 'å
¥ä¼ç³è¯·è¯¦æ
'; |
| | | } |
| | | }); |
| | | |
| | | /** è´¦å·åæ®æéï¼ä»
å¨è¯¦æ
æä¸å®¡æ¹å·²éè¿æ¶å±ç¤º */ |
| | | const credentialTip = computed(() => { |
| | | if ( |
| | | formType.value !== 'detail' || |
| | | formData.value.status !== ApplicationStatus.APPROVED |
| | | ) { |
| | | return null; |
| | | } |
| | | const phone = formData.value.contactPhone; |
| | | if (!phone) { |
| | | return { |
| | | type: 'warning' as const, |
| | | message: |
| | | 'æªå¡«åèç³»çµè¯ï¼è´¦å·ç¨æ·å/å¯ç 使ç¨äºå
åºå¼ unknownï¼è¯·å°ä¼ååè¡¨æ ¸å®å¹¶æå·¥ä¿®æ£ã', |
| | | }; |
| | | } |
| | | return { |
| | | type: 'success' as const, |
| | | message: `ç³»ç»ç»å½è´¦å·å·²èªå¨å¼éï¼ç¨æ·å ${phone}ï¼åå§å¯ç ${phone}ã请éç¥ç³è¯·äººå°½å¿«ç»å½å¹¶ä¿®æ¹å¯ç ã`, |
| | | }; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useFormSchema(formType.value), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** åå»ºææ´æ°å
¥ä¼ç³è¯· */ |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as UmsApplicationApi.Application; |
| | | // é件为å
¨éæ¿æ¢è¯ä¹ï¼æ¸
空éä»¶æ¶å¿
é¡»æ¾å¼ä¼ 空æ°ç» |
| | | data.blobIds = data.blobIds ?? []; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createApplication(data) |
| | | : updateApplication(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = {} as UmsApplicationApi.Application; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | formApi.updateSchema(useFormSchema(formType.value)); |
| | | if (!data || !data.id) { |
| | | // æ°å»ºæ¶é»è®¤ä¸ªäººä¼åç级é项 |
| | | await formApi.setValues({ memberType: 1 }, false); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getApplication(data.id); |
| | | // å·²æé件转æä¸ä¼ ç»ä»¶å¯è¯å«çæä»¶å¯¹è±¡ï¼æäº¤æ¶æ valueKey=id æå为 blobId |
| | | const blobIds = (formData.value.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues( |
| | | { ...formData.value, blobIds }, |
| | | false, |
| | | ); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="getTitle" |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Alert |
| | | v-if="credentialTip" |
| | | :type="credentialTip.type" |
| | | :message="credentialTip.message" |
| | | show-icon |
| | | class="mx-3 mb-3" |
| | | /> |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { UmsApplicationApi } from '#/api/ums/application'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Select } from 'ant-design-vue'; |
| | | |
| | | import { submitApplication } from '#/api/ums/application'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const applicationId = ref<number>(); |
| | | const processList = ref<UmsApplicationApi.ProcessDefinition[]>([]); |
| | | const selectedProcessKey = ref<string>(''); |
| | | |
| | | const processOptions = computed(() => |
| | | processList.value.map((item) => ({ label: item.name, value: item.key })), |
| | | ); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('è¯·éæ©å®¡æ¹æµç¨'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await submitApplication(applicationId.value!, selectedProcessKey.value); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success( |
| | | 'æäº¤æåï¼å®¡æ¹éè¿åå°èªå¨å建ç»å½è´¦å·ï¼ç¨æ·åä¸åå§å¯ç å为èç³»çµè¯ï¼', |
| | | 6, |
| | | ); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | applicationId.value = undefined; |
| | | selectedProcessKey.value = ''; |
| | | processList.value = []; |
| | | return; |
| | | } |
| | | const data = |
| | | modalApi.getData<{ |
| | | id: number; |
| | | processList: UmsApplicationApi.ProcessDefinition[]; |
| | | }>(); |
| | | applicationId.value = data.id; |
| | | processList.value = data.processList || []; |
| | | if (processList.value.length > 0) { |
| | | selectedProcessKey.value = processList.value[0]!.key; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="éæ©å®¡æ¹æµç¨" class="w-1/3"> |
| | | <div class="mb-4"> |
| | | <span class="mr-2">å®¡æ¹æµç¨ï¼</span> |
| | | <Select |
| | | v-model:value="selectedProcessKey" |
| | | :options="processOptions" |
| | | style="width: 200px" |
| | | placeholder="è¯·éæ©å®¡æ¹æµç¨" |
| | | /> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import { AreaCascader } from '#/components/area'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** ä¼åç±»åï¼å¯¹åºåå
¸ ums_member_typeï¼ */ |
| | | export const MemberType = { |
| | | PERSONAL: 1, // 个人ä¼å |
| | | UNIT: 2, // åä½ä¼å |
| | | } as const; |
| | | |
| | | /** ä¼åç¶æï¼å¯¹åºåå
¸ ums_member_statusï¼ */ |
| | | export const MemberStatus = { |
| | | NORMAL: 1, // æ£å¸¸ |
| | | SUSPENDED: 0, // æå |
| | | } as const; |
| | | |
| | | /** æ ¹æ®ä¼åç±»åè·åç级åå
¸é项 */ |
| | | export function getGradeDictOptions(memberType?: number) { |
| | | return getDictOptions( |
| | | memberType === MemberType.UNIT |
| | | ? DICT_TYPE.UMS_UNIT_GRADE |
| | | : DICT_TYPE.UMS_PERSONAL_GRADE, |
| | | 'string', |
| | | ); |
| | | } |
| | | |
| | | /** 表åé
ç½® */ |
| | | export function useFormSchema(formType: FormType): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ä¼åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ä¼åç¼å·ç±ç³»ç»èªå¨çæ', |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'memberType', |
| | | label: 'ä¼åç±»å', |
| | | component: 'Select', |
| | | componentProps: (_values, form) => ({ |
| | | options: getDictOptions(DICT_TYPE.UMS_MEMBER_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©ä¼åç±»å', |
| | | allowClear: true, |
| | | disabled: formType === 'detail', |
| | | onChange: () => { |
| | | // 忢ä¼åç±»å忏
空已éç级ï¼ä¸ªäººä¸åä½ä½¿ç¨ä¸åç级åå
¸ï¼ |
| | | form.setFieldValue('memberGrade', undefined); |
| | | }, |
| | | }), |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'å§å/åä½åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '个人填åå§åï¼åä½å¡«ååä½åç§°', |
| | | maxlength: 128, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'memberGrade', |
| | | label: 'ä¼åç级', |
| | | component: 'Select', |
| | | componentProps: (values) => ({ |
| | | options: getGradeDictOptions(values.memberType), |
| | | placeholder: 'è¯·éæ©ä¼åç级', |
| | | allowClear: true, |
| | | disabled: formType === 'detail', |
| | | }), |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'contactUser', |
| | | label: 'è系人', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è系人ï¼åä½ä¼åå¡«åï¼', |
| | | maxlength: 64, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactPhone', |
| | | label: 'èç³»çµè¯', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç³»çµè¯', |
| | | maxlength: 20, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contactEmail', |
| | | label: 'é®ç®±', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é®ç®±', |
| | | maxlength: 128, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'workUnit', |
| | | label: 'å·¥ä½åä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å·¥ä½åä½ï¼ä¸ªäººä¼åå¡«åï¼', |
| | | maxlength: 128, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.PERSONAL, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'èç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥èç§°ï¼ä¸ªäººä¼åå¡«åï¼', |
| | | maxlength: 64, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.PERSONAL, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'legalPerson', |
| | | label: 'æ³å®ä»£è¡¨äºº', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ³å®ä»£è¡¨äººï¼åä½ä¼åå¡«åï¼', |
| | | maxlength: 64, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'organizationCode', |
| | | label: 'ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ï¼åä½ä¼åå¡«åï¼', |
| | | maxlength: 64, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.UNIT, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'researchDirection', |
| | | label: 'ç ç©¶æ¹å', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ç ç©¶æ¹å', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'unitIntro', |
| | | label: 'åä½ç®ä»', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥åä½ç®ä»ï¼åä½ä¼åå¡«åï¼', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['memberType'], |
| | | show: (values) => values.memberType === MemberType.UNIT, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'regionCode', |
| | | label: 'å°åº', |
| | | component: markRaw(AreaCascader), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å°åº', |
| | | allowClear: true, |
| | | class: '!w-full', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'industry', |
| | | label: 'è¡ä¸', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_INDUSTRY, 'string'), |
| | | placeholder: 'è¯·éæ©è¡ä¸', |
| | | allowClear: true, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'joinDate', |
| | | label: 'å
¥ä¼æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å
¥ä¼æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'expireDate', |
| | | label: 'æææè³', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æææ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_MEMBER_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 2, maxRows: 3 }, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | showDescription: formType !== 'detail', |
| | | disabled: formType === 'detail', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ä¼åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥ä¼åç¼å·', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'å§å/åä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å§åæåä½åç§°', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'memberType', |
| | | label: 'ä¼åç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_MEMBER_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©ä¼åç±»å', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'regionCode', |
| | | label: 'å°åº', |
| | | component: markRaw(AreaCascader), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å°åº', |
| | | allowClear: true, |
| | | class: '!w-full', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'industry', |
| | | label: 'è¡ä¸', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_INDUSTRY, 'string'), |
| | | placeholder: 'è¯·éæ©è¡ä¸', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.UMS_MEMBER_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表åé
ç½® */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { |
| | | type: 'checkbox', |
| | | width: 50, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'no', |
| | | title: 'ä¼åç¼å·', |
| | | width: 180, |
| | | fixed: 'left', |
| | | }, |
| | | { |
| | | field: 'name', |
| | | title: 'å§å/åä½', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'memberType', |
| | | title: 'ä¼åç±»å', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.UMS_MEMBER_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'memberGrade', |
| | | title: 'ä¼åç级', |
| | | minWidth: 100, |
| | | slots: { default: 'memberGrade' }, |
| | | }, |
| | | { |
| | | field: 'contactPhone', |
| | | title: 'èç³»çµè¯', |
| | | minWidth: 130, |
| | | }, |
| | | { |
| | | field: 'industry', |
| | | title: 'è¡ä¸', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.UMS_INDUSTRY }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'joinDate', |
| | | title: 'å
¥ä¼æ¥æ', |
| | | width: 115, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | minWidth: 85, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.UMS_MEMBER_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'hasAttachment', |
| | | title: 'éä»¶', |
| | | minWidth: 80, |
| | | slots: { default: 'hasAttachment' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 180, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { UmsMemberApi } from '#/api/ums/member'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getMemberPage } from '#/api/ums/member'; |
| | | |
| | | import { MemberType, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** ä¼åé讯å½ï¼æå°åº/è¡ä¸æ£ç´¢ï¼åªè¯»ï¼ */ |
| | | defineOptions({ name: 'UmsMemberDirectory' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** æ¥çä¼å详æ
*/ |
| | | function handleDetail(row: UmsMemberApi.Member) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** ç级åå
¸æ å°ï¼æ ¹æ®ä¼åç±»åæ¾ç¤ºå¯¹åºçç级åç§° */ |
| | | const personalGradeOptions = getDictOptions(DICT_TYPE.UMS_PERSONAL_GRADE, 'string'); |
| | | const unitGradeOptions = getDictOptions(DICT_TYPE.UMS_UNIT_GRADE, 'string'); |
| | | function getGradeLabel(row: UmsMemberApi.Member) { |
| | | const options = |
| | | row.memberType === MemberType.UNIT ? unitGradeOptions : personalGradeOptions; |
| | | return options?.find((o) => o.value === row.memberGrade)?.label || row.memberGrade; |
| | | } |
| | | |
| | | const [Grid] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: [ |
| | | { type: 'seq', title: 'åºå·', width: 55, fixed: 'left' }, |
| | | { field: 'no', title: 'ä¼åç¼å·', width: 180, fixed: 'left' }, |
| | | { field: 'name', title: 'å§å/åä½', minWidth: 150 }, |
| | | { |
| | | field: 'memberType', |
| | | title: 'ä¼åç±»å', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.UMS_MEMBER_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'memberGrade', |
| | | title: 'ä¼åç级', |
| | | minWidth: 100, |
| | | slots: { default: 'memberGrade' }, |
| | | }, |
| | | { field: 'contactPhone', title: 'èç³»çµè¯', minWidth: 130 }, |
| | | { field: 'contactEmail', title: 'é®ç®±', minWidth: 160 }, |
| | | { field: 'workUnit', title: 'å·¥ä½åä½', minWidth: 160 }, |
| | | { |
| | | field: 'industry', |
| | | title: 'è¡ä¸', |
| | | minWidth: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.UMS_INDUSTRY }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'regionCode', |
| | | title: 'å°åº', |
| | | minWidth: 110, |
| | | }, |
| | | { |
| | | field: 'joinDate', |
| | | title: 'å
¥ä¼æ¥æ', |
| | | width: 115, |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 90, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ], |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getMemberPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<UmsMemberApi.Member>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal /> |
| | | <Grid table-title="ä¼åé讯å½"> |
| | | <template #memberGrade="{ row }"> |
| | | <span>{{ getGradeLabel(row) }}</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: '详æ
', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['ums:member:directory'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { UmsMemberApi } from '#/api/ums/member'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { deleteMember, getMemberPage } from '#/api/ums/member'; |
| | | |
| | | import { MemberType, useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** ä¼åå表 */ |
| | | defineOptions({ name: 'UmsMember' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ°å¢ä¼å */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾ä¼å */ |
| | | function handleEdit(row: UmsMemberApi.Member) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥çä¼å详æ
*/ |
| | | function handleDetail(row: UmsMemberApi.Member) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å é¤ä¼å */ |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: 'å é¤ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await Promise.all(ids.map((id) => deleteMember(id))); |
| | | message.success('å 餿å'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ records }: { records: UmsMemberApi.Member[] }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | /** ç级åå
¸æ å°ï¼æ ¹æ®ä¼åç±»åæ¾ç¤ºå¯¹åºçç级åç§° */ |
| | | const personalGradeOptions = getDictOptions(DICT_TYPE.UMS_PERSONAL_GRADE, 'string'); |
| | | const unitGradeOptions = getDictOptions(DICT_TYPE.UMS_UNIT_GRADE, 'string'); |
| | | function getGradeLabel(row: UmsMemberApi.Member) { |
| | | const options = |
| | | row.memberType === MemberType.UNIT ? unitGradeOptions : personalGradeOptions; |
| | | return options?.find((o) => o.value === row.memberGrade)?.label || row.memberGrade; |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getMemberPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<UmsMemberApi.Member>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid table-title="ä¼åå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢ä¼å', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['ums:member:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['ums:member:delete'], |
| | | popConfirm: { |
| | | title: 'æ¯å¦å 餿éä¸ä¼åï¼', |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #memberGrade="{ row }"> |
| | | <span>{{ getGradeLabel(row) }}</span> |
| | | </template> |
| | | <template #hasAttachment="{ row }"> |
| | | <Tag :color="row.hasAttachment ? 'success' : 'default'"> |
| | | {{ row.hasAttachment ? 'æ' : 'æ ' }} |
| | | </Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: '详æ
', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['ums:member:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['ums:member:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['ums:member:delete'], |
| | | popConfirm: { |
| | | title: `确认å é¤ä¼å ${row.name} åï¼`, |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { UmsMemberApi } from '#/api/ums/member'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { $t } from '@vben/locales'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createMember, getMember, updateMember } from '#/api/ums/member'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formData = ref<UmsMemberApi.Member>({ |
| | | memberType: 1, |
| | | memberGrade: '', |
| | | name: '', |
| | | contactPhone: '', |
| | | contactEmail: '', |
| | | legalPerson: '', |
| | | organizationCode: '', |
| | | researchDirection: '', |
| | | workUnit: '', |
| | | title: '', |
| | | regionCode: '', |
| | | industry: '', |
| | | remark: '', |
| | | }); |
| | | |
| | | const formType = ref<FormType>('create'); // 表åç±»åï¼'create' | 'edit' | 'detail' |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return $t('ui.actionTitle.create', ['ä¼å']); |
| | | } else if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['ä¼å']); |
| | | } else { |
| | | return 'ä¼å详æ
'; |
| | | } |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'vertical', |
| | | schema: useFormSchema(formType.value), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** åå»ºææ´æ°ä¼å */ |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as UmsMemberApi.Member; |
| | | data.blobIds = data.blobIds ?? []; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createMember(data) |
| | | : updateMember(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = {} as UmsMemberApi.Member; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | formApi.updateSchema(useFormSchema(formType.value)); |
| | | if (!data || !data.id) { |
| | | await formApi.setValues({ memberType: 1, status: 1 }, false); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getMember(data.id); |
| | | // å·²æé件转æä¸ä¼ ç»ä»¶å¯è¯å«çæä»¶å¯¹è±¡ï¼æäº¤æ¶æ valueKey=id æå为 blobId |
| | | const blobIds = (formData.value.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })); |
| | | await formApi.setValues({ ...formData.value, blobIds }, false); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="getTitle" |
| | | class="w-3/4" |
| | | :close-on-click-modal="false" |
| | | :show-confirm-button="formType !== 'detail'" |
| | | > |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |