Merge branch 'dev_NEW_pro' into dev_天津_中兴实强
# Conflicts:
# src/App.vue
# src/config.js
# src/manifest.json
# src/pages/works.vue
# src/store/modules/user.ts
已添加116个文件
已修改49个文件
已删除2个文件
| | |
| | | showSplash.value = false; |
| | | }, 5000); |
| | | |
| | | // åå§åæ¨éæå¡ |
| | | // åå§åæ¨éæå¡,ææ¶æ³¨éï¼å®¢æ·éè¦æå¼ |
| | | // initPushService(); |
| | | }); |
| | | // åå§åæ¨éæå¡ï¼uni-push 1.0ï¼ |
| | | const initPushService = () => { |
| | | return; |
| | | // #ifdef APP-PLUS |
| | | console.log("å¼å§åå§åæ¨éæå¡ï¼uni-push 1.0ï¼"); |
| | | if (typeof plus !== "undefined" && plus.push) { |
| | |
| | | <style lang="scss"> |
| | | @import "uview-plus/index.scss"; |
| | | @import "@/static/scss/index.scss"; |
| | | </style> |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** å®¡æ¹æ¨¡æ¿ç±»åæä¸¾ GET /basic/enum/TypeEnums */ |
| | | export function getTypeEnums() { |
| | | return request({ |
| | | url: "/basic/enum/TypeEnums", |
| | | method: "get", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // æ¥è¯¢åºç¡åæ°å表 |
| | | export function getBaseParamList(query) { |
| | | return request({ |
| | | url: "/technologyParam/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢åºç¡åæ° |
| | | export function addBaseParam(data) { |
| | | return request({ |
| | | url: "/technologyParam/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // ç¼è¾åºç¡åæ° |
| | | export function editBaseParam(data) { |
| | | return request({ |
| | | url: "/technologyParam/edit", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤åºç¡åæ° |
| | | export function removeBaseParam(id) { |
| | | return request({ |
| | | url: "/technologyParam/remove/" + id, |
| | | method: "delete", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // é件页颿¥å£ |
| | | import request from '@/utils/request' |
| | | |
| | | // éä»¶æ¥è¯¢ |
| | | export function attachmentList(query) { |
| | | return request({ |
| | | url: '/storageAttachment/list', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | } |
| | | |
| | | // éä»¶æ°å¢ |
| | | export function createAttachment(data) { |
| | | return request({ |
| | | url: '/storageAttachment/add', |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | // éä»¶å é¤ |
| | | export function deleteAttachment(data) { |
| | | return request({ |
| | | url: '/storageAttachment/delete', |
| | | method: 'delete', |
| | | data |
| | | }) |
| | | } |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export function approveProcessListPage(query) { |
| | | return request({ |
| | | url: '/approveProcess/list', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | return request({ |
| | | url: "/approveProcess/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | export function getDept(query) { |
| | | return request({ |
| | | url: '/approveProcess/getDept', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | return request({ |
| | | url: "/approveProcess/getDept", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | export function approveProcessGetInfo(query) { |
| | | return request({ |
| | | url: '/approveProcess/get', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | return request({ |
| | | url: "/approveProcess/get", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | // æ°å¢å®¡æ¹æµç¨ |
| | | export function approveProcessAdd(query) { |
| | | return request({ |
| | | url: '/approveProcess/add', |
| | | method: 'post', |
| | | data: query, |
| | | }) |
| | | return request({ |
| | | url: "/approveProcess/add", |
| | | method: "post", |
| | | data: query, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹å®¡æ¹æµç¨ |
| | | export function approveProcessUpdate(query) { |
| | | return request({ |
| | | url: '/approveProcess/update', |
| | | method: 'post', |
| | | data: query, |
| | | }) |
| | | return request({ |
| | | url: "/approveProcess/update", |
| | | method: "post", |
| | | data: query, |
| | | }); |
| | | } |
| | | // æäº¤å®¡æ¹ |
| | | export function updateApproveNode(query) { |
| | | return request({ |
| | | url: '/approveNode/updateApproveNode', |
| | | method: 'post', |
| | | data: query, |
| | | }) |
| | | return request({ |
| | | url: "/approveNode/updateApproveNode", |
| | | method: "post", |
| | | data: query, |
| | | }); |
| | | } |
| | | // å é¤å®¡æ¹æµç¨ |
| | | export function approveProcessDelete(query) { |
| | | return request({ |
| | | url: '/approveProcess/deleteIds', |
| | | method: 'delete', |
| | | data: query, |
| | | }) |
| | | return request({ |
| | | url: "/approveProcess/deleteIds", |
| | | method: "delete", |
| | | data: query, |
| | | }); |
| | | } |
| | | // æ¥è¯¢å®¡æ¹æµç¨ |
| | | export function approveProcessDetails(query) { |
| | | return request({ |
| | | url: '/approveNode/details/' + query, |
| | | method: 'get', |
| | | }) |
| | | } |
| | | return request({ |
| | | url: "/approveNode/details/" + query, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // 审æ¹è¯¦æ
|
| | | export function getDeliveryDetailByShippingNo(query) { |
| | | return request({ |
| | | url: "/shippingInfo/getDateilByShippingNo", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** |
| | | * 书æ¶ç®¡çç¸å
³APIæ¥å£ |
| | | * å
å«ä»åºç®¡çãè´§æ¶ç®¡çãå¾ä¹¦ç®¡ççåè½çæ¥å£ |
| | | */ |
| | | |
| | | /** |
| | | * è·åä»åºå表 |
| | | * @description è·åææä»åºçåºæ¬ä¿¡æ¯å表 |
| | | * @returns {Promise} è¿åä»åºåè¡¨æ°æ® |
| | | */ |
| | | export function getWarehouseList() { |
| | | return request({ |
| | | url: "/warehouse/tree", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * æ°å¢ä»åº |
| | | * @description å建æ°çä»åºè®°å½ |
| | | * @param {Object} data ä»åºä¿¡æ¯å¯¹è±¡ï¼å
å«ä»åºåç§°çåæ®µ |
| | | * @returns {Promise} è¿åæ°å¢ç»æ |
| | | */ |
| | | export function addWarehouse(data) { |
| | | return request({ |
| | | url: "/warehouse/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * æ´æ°ä»åºä¿¡æ¯ |
| | | * @description ä¿®æ¹ç°æä»åºçåºæ¬ä¿¡æ¯ |
| | | * @param {Object} data ä»åºä¿¡æ¯å¯¹è±¡ï¼å¿
é¡»å
å«ä»åºID |
| | | * @returns {Promise} è¿åæ´æ°ç»æ |
| | | */ |
| | | export function updateWarehouse(data) { |
| | | return request({ |
| | | url: "/warehouse/update", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * å é¤ä»åº |
| | | * @description æ ¹æ®ä»åºIDå 餿å®çä»åºè®°å½ |
| | | * @param {string|number} id ä»åºID |
| | | * @returns {Promise} è¿åå é¤ç»æ |
| | | */ |
| | | export function deleteWarehouse(data) { |
| | | return request({ |
| | | url: `/warehouse/delete/`, |
| | | method: "delete", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * è·åè´§æ¶å表 |
| | | * @description æ ¹æ®ä»åºIDè·å该ä»åºä¸çææè´§æ¶ä¿¡æ¯ |
| | | * @param {string|number} warehouseId ä»åºID |
| | | * @returns {Promise} è¿åè´§æ¶åè¡¨æ°æ® |
| | | */ |
| | | export function getShelfList(warehouseId) { |
| | | return request({ |
| | | url: `/shelf/list/${warehouseId}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * æ°å¢è´§æ¶ |
| | | * @description 卿å®ä»åºä¸å建æ°çè´§æ¶è®°å½ |
| | | * @param {Object} data è´§æ¶ä¿¡æ¯å¯¹è±¡ï¼å
å«è´§æ¶åç§°ã屿°ãåæ°çåæ®µ |
| | | * @returns {Promise} è¿åæ°å¢ç»æ |
| | | */ |
| | | export function addShelf(data) { |
| | | return request({ |
| | | url: "/warehouse/goodsShelves/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * æ´æ°è´§æ¶ä¿¡æ¯ |
| | | * @description ä¿®æ¹ç°æè´§æ¶çåºæ¬ä¿¡æ¯ |
| | | * @param {Object} data è´§æ¶ä¿¡æ¯å¯¹è±¡ï¼å¿
é¡»å
å«è´§æ¶ID |
| | | * @returns {Promise} è¿åæ´æ°ç»æ |
| | | */ |
| | | export function updateShelf(data) { |
| | | return request({ |
| | | url: "/warehouse/goodsShelves/update", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * å é¤è´§æ¶ |
| | | * @description æ ¹æ®è´§æ¶IDå 餿å®çè´§æ¶è®°å½ï¼åç«¯è¦æ±ä¼ å
¥ ID æ°ç»ï¼æ¯ææ¹éï¼ |
| | | * @param {Array<string|number>} data è´§æ¶IDæ°ç» |
| | | * @returns {Promise} è¿åå é¤ç»æ |
| | | */ |
| | | export function deleteShelf(data) { |
| | | return request({ |
| | | url: `/warehouse/goodsShelves/delete/`, |
| | | method: "delete", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * è·åä»åºç»æ |
| | | * @description è·åæå®ä»åºç宿´ç»æä¿¡æ¯ï¼å
æ¬è´§æ¶ã屿°ãåæ°ç |
| | | * @param {string|number} warehouseId ä»åºID |
| | | * @returns {Promise} è¿åä»åºç宿´ç»ææ°æ® |
| | | */ |
| | | export function getWarehouseStructure(data) { |
| | | return request({ |
| | | url: `/warehouse/goodsShelvesRowcol/list`, |
| | | method: "get", |
| | | params: data, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // ææ¡£åé
管çç¸å
³æ¥å£ |
| | | |
| | | // è·åææ¡£å表ï¼ç¨äºåé
书ç±éæ©ï¼ |
| | | export function getDocumentList() { |
| | | return request({ |
| | | url: "/documentation/list", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // åé
å页æ¥è¯¢ |
| | | export function getBorrowList(params) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/listPage", |
| | | method: "get", |
| | | params: params, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢åé
|
| | | export function addBorrow(data) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // æ´æ°åé
|
| | | export function updateBorrow(data) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/update", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤åé
|
| | | export function deleteBorrow(ids) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // è·ååç±»æ |
| | | export function getCategoryTree() { |
| | | return request({ |
| | | url: "/warehouse/documentClassification/getList", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢åç±» |
| | | export function addCategory(data) { |
| | | return request({ |
| | | url: "/warehouse/documentClassification/add", |
| | | method: "post", |
| | | data: { |
| | | category: data.category, |
| | | parentId: data.parentId, |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹åç±» |
| | | export function updateCategory(data) { |
| | | return request({ |
| | | url: "/warehouse/documentClassification/update", |
| | | method: "put", |
| | | data: { |
| | | id: data.id, |
| | | category: data.category, |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | // å é¤åç±» |
| | | export function deleteCategory(ids) { |
| | | return request({ |
| | | url: "/warehouse/documentClassification/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // è·åææ¡£å表ï¼åé¡µï¼ |
| | | export function getDocumentList(query) { |
| | | return request({ |
| | | url: "/documentation/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ææ¡£ |
| | | export function addDocument(data) { |
| | | return request({ |
| | | url: "/documentation/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹ææ¡£ |
| | | export function updateDocument(data) { |
| | | return request({ |
| | | url: "/documentation/update", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ææ¡£ |
| | | export function deleteDocument(ids) { |
| | | return request({ |
| | | url: "/documentation/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // è·åææ¡£è¯¦æ
|
| | | export function getDocumentDetail(id) { |
| | | return request({ |
| | | url: "/document/" + id, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // æç´¢ææ¡£ |
| | | export function searchDocument(query) { |
| | | return request({ |
| | | url: "/document/search", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // è·åä»åºç»æ |
| | | export function getWarehouseStructure() { |
| | | return request({ |
| | | url: "/document/warehouse/structure", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // é件管çç¸å
³æ¥å£ |
| | | // æ·»å éä»¶ |
| | | export function addDocumentationFile(data) { |
| | | return request({ |
| | | url: "/documentation/documentationFile/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // è·åéä»¶å表 |
| | | export function getDocumentationFileList(params) { |
| | | return request({ |
| | | url: "/documentation/documentationFile/listPage", |
| | | method: "get", |
| | | params: params, |
| | | }); |
| | | } |
| | | |
| | | // å é¤éä»¶ |
| | | export function deleteDocumentationFile(ids) { |
| | | return request({ |
| | | url: "/documentation/documentationFile/del", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // ææ¡£åé
管çç¸å
³æ¥å£ |
| | | export function getBorrowList(params) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/listPage", |
| | | method: "get", |
| | | params: params, |
| | | }); |
| | | } |
| | | |
| | | export function addBorrow(data) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | export function updateBorrow(data) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/update", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | export function deleteBorrow(ids) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // ç»è®¡ç¸å
³æ¥å£ |
| | | // è·åæ»ä½ç»è®¡æ°æ® |
| | | export function getDocumentationOverview() { |
| | | return request({ |
| | | url: "/documentation/overview", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·ååç±»ç»è®¡æ°æ® |
| | | export function getDocumentationCategoryStats() { |
| | | return request({ |
| | | url: "/documentation/category", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åç¶æç»è®¡æ°æ® |
| | | export function getDocumentationStatusStats() { |
| | | return request({ |
| | | url: "/documentation/status", |
| | | method: "get", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢å½è¿è®°å½ |
| | | export function getReturnListPage(query) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/listPageReturn", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // å½è¿æä½ |
| | | export function returnDocument(data) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/revent", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤å½è¿è®°å½ |
| | | export function deleteReturn(ids) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/reventDelete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | //æ ¹æ®ä¹¦ç±idæ¥è¯¢åé
è®°å½ |
| | | export function getBorrowListByDocumentationId(id) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/getByDocumentationId/"+id, |
| | | method: "get" |
| | | }); |
| | | } |
| | | |
| | | // æ´æ°åé
è®°å½ |
| | | export function updateBorrow(data) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/update", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å½è¿æ´æ° |
| | | export function reventUpdate(data) { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/reventUpdate", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // è·åææ¡£å表 |
| | | export function getDocumentList() { |
| | | return request({ |
| | | url: "/documentationBorrowManagement/list", |
| | | method: "get", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // è·åæ¡£æ¡æ»ä½ç»è®¡ |
| | | export function getDocumentStatistics() { |
| | | return request({ |
| | | url: "/fileManagement/statistics/overview", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åæ¡£æ¡åç±»ç»è®¡ |
| | | export function getCategoryStatistics() { |
| | | return request({ |
| | | url: "/fileManagement/statistics/category", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åæ¡£æ¡ç¶æç»è®¡ |
| | | export function getStatusStatistics() { |
| | | return request({ |
| | | url: "/fileManagement/statistics/status", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åæ¡£æ¡åé
ç»è®¡ |
| | | export function getBorrowStatistics() { |
| | | return request({ |
| | | url: "/fileManagement/statistics/borrow", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åæ¡£æ¡å¹´åº¦ç»è®¡ |
| | | export function getYearStatistics() { |
| | | return request({ |
| | | url: "/fileManagement/statistics/year", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åæ¡£æ¡ä½ç½®ç»è®¡ |
| | | export function getLocationStatistics() { |
| | | return request({ |
| | | url: "/fileManagement/statistics/location", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åæ¡£æ¡è¶å¿ç»è®¡ |
| | | export function getTrendStatistics(params) { |
| | | return request({ |
| | | url: "/fileManagement/statistics/trend", |
| | | method: "get", |
| | | params: params, |
| | | }); |
| | | } |
| | | |
| | | // è·åæ¡£æ¡åé
æè¡ |
| | | export function getBorrowRanking() { |
| | | return request({ |
| | | url: "/fileManagement/statistics/borrowRanking", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åæ¡£æ¡å类详æ
ç»è®¡ |
| | | export function getCategoryDetailStatistics(categoryId) { |
| | | return request({ |
| | | url: `/fileManagement/statistics/categoryDetail/${categoryId}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | |
| | | import request from "@/utils/request"; |
| | | // å页æ¥è¯¢åºåè®°å½å表 |
| | | export const getStockInventoryListPage = (params) => { |
| | | return request({ |
| | | url: "/stockInventory/pagestockInventory", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | export const getStockInventoryListPage = params => { |
| | | return request({ |
| | | url: "/stockInventory/pagestockInventory", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | // å页æ¥è¯¢èååºåè®°å½å表ï¼å
å«ååä¿¡æ¯ï¼ |
| | | export const getStockInventoryListPageCombined = params => { |
| | | return request({ |
| | | url: "/stockInventory/pageListCombinedStockInventory", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | // å建åºåè®°å½ |
| | | export const createStockInventory = (params) => { |
| | | return request({ |
| | | url: "/stockInventory/addstockInventory", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | export const createStockInventory = params => { |
| | | return request({ |
| | | url: "/stockInventory/addstockInventory", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | // åå°åºåè®°å½ |
| | | export const subtractStockInventory = (params) => { |
| | | return request({ |
| | | url: "/stockInventory/subtractStockInventory", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | export const subtractStockInventory = params => { |
| | | return request({ |
| | | url: "/stockInventory/subtractStockInventory", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | export const getStockInventoryReportList = (params) => { |
| | | return request({ |
| | | url: "/stockInventory/stockInventoryPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | export const getStockInventoryReportList = params => { |
| | | return request({ |
| | | url: "/stockInventory/stockInventoryPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | export const getStockInventoryInAndOutReportList = (params) => { |
| | | return request({ |
| | | url: "/stockInventory/stockInAndOutRecord", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | export const getStockInventoryInAndOutReportList = params => { |
| | | return request({ |
| | | url: "/stockInventory/stockInAndOutRecord", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | // å»ç»åºåè®°å½ |
| | | export const frozenStockInventory = (params) => { |
| | | return request({ |
| | | url: "/stockInventory/frozenStock", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | export const frozenStockInventory = params => { |
| | | return request({ |
| | | url: "/stockInventory/frozenStock", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | // è§£å»åºåè®°å½ |
| | | export const thawStockInventory = (params) => { |
| | | return request({ |
| | | url: "/stockInventory/thawStock", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | export const thawStockInventory = params => { |
| | | return request({ |
| | | url: "/stockInventory/thawStock", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | }; |
| | | |
| | | export const getStockInventoryByModelId = productModelId => { |
| | | return request({ |
| | | url: "/stockInventory/getByModelId", |
| | | method: "get", |
| | | params: { productModelId }, |
| | | }); |
| | | }; |
| | |
| | | import request from '@/utils/request' |
| | | import request from "@/utils/request"; |
| | | |
| | | // ç»å½æ¹æ³ |
| | | export function loginCheckFactory(username, password) { |
| | | export function loginCheckFactory(username, password, factoryId) { |
| | | const data = { |
| | | username, |
| | | password, |
| | | } |
| | | factoryId, |
| | | }; |
| | | return request({ |
| | | url: '/loginCheckFactory', |
| | | url: "/loginCheckFactory", |
| | | headers: { |
| | | isToken: false |
| | | isToken: false, |
| | | repeatSubmit: false, |
| | | }, |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // è·åç¨æ·è¯¦ç»ä¿¡æ¯ |
| | | export function getInfo() { |
| | | return request({ |
| | | url: '/getInfo', |
| | | method: 'get' |
| | | }) |
| | | url: "/getInfo", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // éåºæ¹æ³ |
| | | export function logout() { |
| | | return request({ |
| | | url: '/logout', |
| | | method: 'post' |
| | | }) |
| | | url: "/logout", |
| | | method: "post", |
| | | }); |
| | | } |
| | | |
| | | // è·åå
¬å¸å表 |
| | | export function userLoginFacotryList(params) { |
| | | return request({ |
| | | url: '/userLoginFacotryList', |
| | | method: 'get', |
| | | params: params |
| | | }) |
| | | url: "/userLoginFacotryList", |
| | | method: "get", |
| | | params: params, |
| | | }); |
| | | } |
| | | |
| | | // è·åæªè¿æå
¬åæ°é |
| | | export function noticesList(params) { |
| | | return request({ |
| | | url: '/collaborativeApproval/notice/page', |
| | | method: 'get', |
| | | params: params |
| | | }) |
| | | url: "/collaborativeApproval/notice/page", |
| | | method: "get", |
| | | params: params, |
| | | }); |
| | | } |
| | | |
| | | // åé客æ·ç«¯æ¨éæ è¯å°æå¡å¨ |
| | | export function updateClientId(data) { |
| | | return request({ |
| | | url: '/system/client/addOrUpdateClientId', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | url: "/system/client/addOrUpdateClientId", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | |
| | | // æ¥è¯¢å
¬åå表 |
| | | export function listNotice(query) { |
| | | return request({ |
| | | url: '/system/notice/list', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | url: "/system/notice/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // è·åæªè¯»æ¶æ¯æ°é |
| | | export function getNoticeCount(consigneeId) { |
| | | return request({ |
| | | url: '/system/notice/getCount', |
| | | method: 'get', |
| | | params: { consigneeId } |
| | | }) |
| | | url: "/system/notice/getCount", |
| | | method: "get", |
| | | params: { consigneeId }, |
| | | }); |
| | | } |
| | | |
| | | // æ è®°æ¶æ¯ä¸ºå·²è¯» |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** 审æ¹å®ä¾å页æ¥è¯¢ GET /approvalInstance/listPage */ |
| | | export function listApprovalInstancePage(params) { |
| | | return request({ |
| | | url: "/approvalInstance/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** æ°å»ºå®¡æ¹å®ä¾ POST /approvalInstance/save */ |
| | | export function saveApprovalInstance(approvalInstanceDto) { |
| | | return request({ |
| | | url: "/approvalInstance/save", |
| | | method: "post", |
| | | data: { approvalInstanceDto }, |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * ä¿®æ¹å®¡æ¹å®ä¾ PUT /approvalInstance/update |
| | | * @param {Object} approvalInstanceDto 审æ¹å®ä¾ï¼éå« idï¼å
¶ä½å段æä¸å¡ä¿ç/æ´æ°ï¼ |
| | | */ |
| | | export function updateApprovalInstance(approvalInstanceDto) { |
| | | return request({ |
| | | url: "/approvalInstance/update", |
| | | method: "put", |
| | | data: { approvalInstanceDto }, |
| | | }); |
| | | } |
| | | |
| | | /** 审æ¹ï¼éè¿/驳åï¼POST /approvalInstance/approve */ |
| | | export function approveApprovalInstance(approvalInstanceDto) { |
| | | return request({ |
| | | url: "/approvalInstance/approve", |
| | | method: "post", |
| | | data: { approvalInstanceDto }, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤å®¡æ¹å®ä¾ DELETE /approvalInstance/delete */ |
| | | export function deleteApprovalInstance(ids) { |
| | | const idList = (Array.isArray(ids) ? ids : [ids]).filter( |
| | | id => id != null && id !== "" |
| | | ); |
| | | return request({ |
| | | url: "/approvalInstance/delete", |
| | | method: "delete", |
| | | data: idList, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** |
| | | * æ templateType æ¥è¯¢å·²å¯ç¨æ¨¡æ¿å表ï¼é businessTypeï¼ |
| | | * GET /approvalTemplate/list/{templateType} ä¾ï¼list/1 = èªå®ä¹å·²å¯ç¨ |
| | | */ |
| | | export function listApprovalTemplateByType(templateType) { |
| | | return request({ |
| | | url: `/approvalTemplate/list/${templateType}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | /** å®¡æ¹æ¨¡æ¿å页æ¥è¯¢ */ |
| | | export function listApprovalTemplatePage(params) { |
| | | return request({ |
| | | url: "/approvalTemplate/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** å®¡æ¹æ¨¡æ¿è¯¦æ
*/ |
| | | export function getApprovalTemplateDetail(id) { |
| | | return request({ |
| | | url: `/approvalTemplate/detail/${id}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢å®¡æ¹æ¨¡æ¿ */ |
| | | export function addApprovalTemplate(data) { |
| | | return request({ |
| | | url: "/approvalTemplate/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** ä¿®æ¹å®¡æ¹æ¨¡æ¿ */ |
| | | export function updateApprovalTemplate(data) { |
| | | return request({ |
| | | url: "/approvalTemplate/update", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤å®¡æ¹æ¨¡æ¿ï¼ä¼ ID æ°ç»ï¼ */ |
| | | export function deleteApprovalTemplate(ids) { |
| | | return request({ |
| | | url: "/approvalTemplate/delete", |
| | | method: "post", |
| | | data: ids, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** å页æ¥è¯¢è´¢å¡æ¥é GET /finReimbursement/listPage */ |
| | | export function listFinReimbursementPage(params) { |
| | | return request({ |
| | | url: "/finReimbursement/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** 详æ
queryï¼Spring ç»å® finReimbursementDto.idï¼å¿ç¨ finReimbursementDto[id] */ |
| | | function buildFinReimbursementDetailParams(idOrDto) { |
| | | const raw = |
| | | typeof idOrDto === "object" && idOrDto !== null |
| | | ? idOrDto.id ?? idOrDto.reimbursementId |
| | | : idOrDto; |
| | | return { |
| | | "finReimbursementDto.id": raw, |
| | | id: raw, |
| | | }; |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¢å¡æ¥é详æ
GET /finReimbursement/detail */ |
| | | export function getFinReimbursementDetail(idOrDto) { |
| | | return request({ |
| | | url: "/finReimbursement/detail", |
| | | method: "get", |
| | | params: buildFinReimbursementDetailParams(idOrDto), |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢è´¢å¡æ¥é POST /finReimbursement/save */ |
| | | export function saveFinReimbursement(finReimbursementDto) { |
| | | return request({ |
| | | url: "/finReimbursement/save", |
| | | method: "post", |
| | | data: finReimbursementDto, |
| | | }); |
| | | } |
| | | |
| | | /** ä¿®æ¹è´¢å¡æ¥é POST /finReimbursement/update */ |
| | | export function updateFinReimbursement(finReimbursementDto) { |
| | | return request({ |
| | | url: "/finReimbursement/update", |
| | | method: "post", |
| | | data: finReimbursementDto, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤è´¢å¡æ¥é DELETE /finReimbursement/deleteï¼body 为 ID æ°ç»ï¼ */ |
| | | export function deleteFinReimbursement(ids) { |
| | | const idList = (Array.isArray(ids) ? ids : [ids]).filter( |
| | | (id) => id != null && id !== "" |
| | | ); |
| | | return request({ |
| | | url: "/finReimbursement/delete", |
| | | method: "delete", |
| | | data: idList, |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢èµ° saveï¼ä¿®æ¹èµ° updateï¼ä¸æ¥å£ææ¡£ä¸è´ï¼ */ |
| | | export function persistFinReimbursement(finReimbursementDto, isEdit = false) { |
| | | if (isEdit) { |
| | | return updateFinReimbursement(finReimbursementDto); |
| | | } |
| | | const payload = { ...finReimbursementDto }; |
| | | delete payload.id; |
| | | return saveFinReimbursement(payload); |
| | | } |
| | |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢éè´è¯¦æ
|
| | | export function getPurchaseByCode(query) { |
| | | return request({ |
| | | url: "/purchase/ledger/getPurchaseByCode", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function approveProcessGetInfo(query) { |
| | | return request({ |
| | | url: '/approveProcess/get', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // BOM å表å页æ¥è¯¢ |
| | | export function listPage(query) { |
| | | return request({ |
| | | url: "/technologyBom/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ BOM |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/technologyBom/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹ BOM |
| | | export function update(data) { |
| | | return request({ |
| | | url: "/technologyBom/update", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ BOM |
| | | export function batchDelete(ids) { |
| | | return request({ |
| | | url: "/technologyBom/batchDelete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // å¤å¶ BOM |
| | | export function copy(data) { |
| | | return request({ |
| | | url: "/technologyBom/copy", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // è·å产åå表 (ç¨äºæ°å¢BOMæ¶éæ©äº§å) |
| | | export function getProductList(query) { |
| | | return request({ |
| | | url: "/product/ledger/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // --- BOM ç»æç¸å
³ --- |
| | | |
| | | // æ ¹æ® BOM ID è·åç»æå表 |
| | | export function queryStructureList(bomId) { |
| | | return request({ |
| | | url: "/technologyBomStructure/listByBomId/" + bomId, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // ä¿å BOM ç»æ |
| | | export function addStructure(data) { |
| | | return request({ |
| | | url: "/technologyBomStructure/batchSave", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ BOM ç»æé¡¹ |
| | | export function deleteStructure(id) { |
| | | return request({ |
| | | url: "/technologyBomStructure/batchDelete/" + id, |
| | | method: "delete", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export function getProcessList(query) { |
| | | return request({ |
| | | url: "/technologyOperation/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function list() { |
| | | return request({ |
| | | url: "/technologyOperation/list", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/technologyOperation/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | export function update(data) { |
| | | return request({ |
| | | url: "/technologyOperation/update", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | export function del(ids) { |
| | | return request({ |
| | | url: "/technologyOperation/batchDelete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | export function getProcessParamList(params) { |
| | | return request({ |
| | | url: "/technologyOperationParam/list", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | export function addProcessParam(data) { |
| | | return request({ |
| | | url: "/technologyOperationParam/", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | export function editProcessParam(data) { |
| | | return request({ |
| | | url: "/technologyOperationParam/", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | export function deleteProcessParam(id) { |
| | | return request({ |
| | | url: `/technologyOperationParam/batchDelete/${id}`, |
| | | method: "delete", |
| | | }); |
| | | } |
| | | |
| | | export function getDeviceLedger(query) { |
| | | return request({ |
| | | url: "/device/ledger/getDeviceLedger", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function getBaseParamList(query) { |
| | | return request({ |
| | | url: "/technologyParam/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // å·¥èºè·¯çº¿ç¸å
³æ¥å£ |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢å·¥èºè·¯çº¿å表 |
| | | export function listPage(query) { |
| | | return request({ |
| | | url: "/technologyRouting/page", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢å·¥èºè·¯çº¿é¡¹ç®å表 |
| | | export function findProcessRouteItemList(query) { |
| | | return request({ |
| | | url: "/technologyRoutingOperation/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // è·åå·¥åºåæ°å表 |
| | | export function getProcessParamList(query) { |
| | | return request({ |
| | | url: "/technologyRoutingOperationParam/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢BOMç»æ (å·¥èºè·¯çº¿) |
| | | export function queryBomList(bomId) { |
| | | return request({ |
| | | url: "/technologyBomStructure/listByBomId/" + bomId, |
| | | method: "get", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // ç产æ¥å·¥é¡µé¢æ¥å£ |
| | | import request from "@/utils/request"; |
| | | |
| | | // è·åå·¥åºåæ°å表-ç产订å |
| | | export function findProcessParamListOrder(query) { |
| | | return request({ |
| | | url: `/productionOrderRoutingOperationParam/list`, |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // å·¦è¾¹è¡¨æ ¼çæ¥å£ (æ±æ») |
| | | export function salesLedgerProductionAccountingList(query) { |
| | | return request({ |
| | | url: "/productionAccount/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // å³è¾¹è¡¨æ ¼çæ¥å£ (æç») |
| | | export function salesLedgerProductionAccountingListProductionDetails(query) { |
| | | return request({ |
| | | url: "/productionAccount/listProductionDetails", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | |
| | | // ç产订å页颿¥å£ |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢ |
| | | export function schedulingListPage(query) { |
| | | // å页æ¥è¯¢ç产订å |
| | | export function productOrderListPage(query) { |
| | | return request({ |
| | | url: "/salesLedger/scheduling/listPage", |
| | | url: "/productionOrder/page", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | // ç产派工 |
| | | export function productionDispatch(query) { |
| | | |
| | | // çäº§è®¢åæº¯æºè¯¦æ
|
| | | export function getOrderDetail(npsNo) { |
| | | return request({ |
| | | url: "/salesLedger/scheduling/productionDispatch", |
| | | method: "post", |
| | | data: query, |
| | | url: "/productionOrder/ordeDetail", |
| | | method: "get", |
| | | params: { npsNo }, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // è·åçäº§è®¢åæ¥æºæ°æ® |
| | | export function getProductOrderSource(id) { |
| | | return request({ |
| | | url: `/productionOrder/source/${id}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // é¢æè¯¦æ
å表 |
| | | export function listMaterialPickingDetail(productionOrderId) { |
| | | return request({ |
| | | url: "/productionOrderPick/detail/" + productionOrderId, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è¡¥æè®°å½å表 |
| | | export function listMaterialSupplementRecord(query) { |
| | | return request({ |
| | | url: "/productionOrderPickRecord/feeding", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // è·å颿BOMä¿¡æ¯ (å¯éï¼å¤ç¨) |
| | | export function listMaterialPickingBom(productionOrderId) { |
| | | return request({ |
| | | url: "/productionOrder/pick/" + productionOrderId, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åç产订åå
³èçå·¥èºè·¯çº¿ä¸»ä¿¡æ¯ |
| | | export function getOrderProcessRouteMain(orderId) { |
| | | return request({ |
| | | url: "/productionOrderRouting/listMain", |
| | | method: "get", |
| | | params: { orderId }, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢BOMç»æ (ç产订å) |
| | | export function queryOrderBomList(bomId) { |
| | | return request({ |
| | | url: "/productionBomStructure/listByBomId/" + bomId, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // è·åå·¥åºåæ°å表 (ç产订å) |
| | | export function findProcessParamListOrder(query) { |
| | | return request({ |
| | | url: "/productionOrderRoutingOperationParam/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 主çäº§è®¡åæ¥å£ |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页å表 |
| | | export function productionPlanListPage(query) { |
| | | return request({ |
| | | url: "/productionPlan/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æåæ°æ® |
| | | export function loadProdData(query) { |
| | | return request({ |
| | | url: "/productionPlan/loadProdData", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ±æ»ç»è®¡ |
| | | export function summaryByProductType(query) { |
| | | return request({ |
| | | url: "/productionPlan/summaryByProductType", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // ç产æ¥å·¥é¡µé¢æ¥å£ |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢ç产æ¥å·¥ä¸»è¡¨ |
| | | export function productionProductMainListPage(query) { |
| | | return request({ |
| | | url: "/productionProductMain/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // å 餿¥å·¥ |
| | | export function productionReportDelete(query) { |
| | | return request({ |
| | | url: "/productionProductMain/delete", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢æå
¥å表 |
| | | export function productionProductInputListPage(query) { |
| | | return request({ |
| | | url: "/productionProductInput/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | |
| | | // æ ¹æ®IDè·åå·¥å详æ
|
| | | export function getProductWorkOrderById(query) { |
| | | return request({ |
| | | url: "/productWorkOrder/getProductWorkOrderById", |
| | | url: "/productionOperationTask/" + query.id, |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | // ç产æ¥å·¥ |
| | |
| | | |
| | | export function productWorkOrderPage(query) { |
| | | return request({ |
| | | url: "/productWorkOrder/page", |
| | | url: "/productionOperationTask/page", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | |
| | | |
| | | export function updateProductWorkOrder(data) { |
| | | return request({ |
| | | url: "/productWorkOrder/updateProductWorkOrder", |
| | | url: "/productionOperationTask/updateProductWorkOrder", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | |
| | | }); |
| | | } |
| | | |
| | | export function assignProductWorkOrder(data) { |
| | | return request({ |
| | | url: "/productionOperationTask/assign", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // ä¸è½½å·¥åæµè½¬å¡ï¼è¿åæä»¶æµï¼ |
| | | export function downProductWorkOrder(id) { |
| | | return request({ |
| | | url: "/productWorkOrder/down", |
| | | url: "/productionOperationTask/down", |
| | | method: "post", |
| | | data: { id }, |
| | | responseType: "blob", |
| | | }); |
| | | } |
| | | |
| | | // è·åå·¥åºç»è®¡æ°æ® |
| | | export function getOperationStatistics(query) { |
| | | return request({ |
| | | url: "/productionOperationTask/getOperation", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** å²ä½ä¸æ GET /system/post/optionselect */ |
| | | export function findPostOptions(query) { |
| | | return request({ |
| | | url: "/system/post/optionselect", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="common-upload"> |
| | | <u-upload |
| | | :fileList="internalFileList" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | :name="name" |
| | | :multiple="multiple" |
| | | :maxCount="maxCount" |
| | | :accept="accept" |
| | | :disabled="disabled" |
| | | ></u-upload> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, watch, onMounted } from 'vue'; |
| | | import { getToken } from "@/utils/auth"; |
| | | import config from "@/config"; |
| | | |
| | | const props = defineProps({ |
| | | // ç¶ç»ä»¶ä¼ å
¥çæä»¶å表ï¼å¯¹åºå端åå¨ç对象åè¡¨ï¼ |
| | | modelValue: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | // æå¤§ä¸ä¼ æ°é |
| | | maxCount: { |
| | | type: Number, |
| | | default: 9 |
| | | }, |
| | | // æ¯å¦æ¯æå¤é |
| | | multiple: { |
| | | type: Boolean, |
| | | default: true |
| | | }, |
| | | // æ¥åçæä»¶ç±»å |
| | | accept: { |
| | | type: String, |
| | | default: 'image' |
| | | }, |
| | | // ä¸ä¼ æ¥å£å¯¹åºçåæ°å |
| | | name: { |
| | | type: String, |
| | | default: 'file' |
| | | }, |
| | | // æ¯å¦ç¦ç¨ |
| | | disabled: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['update:modelValue']); |
| | | |
| | | // ç¨äº u-upload æ¾ç¤ºçå
é¨å表 |
| | | const internalFileList = ref([]); |
| | | |
| | | // çå¬å¤é¨ modelValue ååï¼åæ¥å°å
鍿¾ç¤ºå表 |
| | | watch(() => props.modelValue, (newVal) => { |
| | | if (newVal) { |
| | | internalFileList.value = newVal.map(item => ({ |
| | | ...item, |
| | | url: item.url || item.previewURL, |
| | | status: 'success', |
| | | message: '' |
| | | })); |
| | | } |
| | | }, { immediate: true, deep: true }); |
| | | |
| | | const showToast = (message) => { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: "none", |
| | | }); |
| | | }; |
| | | |
| | | // ä¸ä¼ é»è¾ |
| | | const uploadFilePromise = (url) => { |
| | | return new Promise((resolve, reject) => { |
| | | uni.uploadFile({ |
| | | url: config.baseUrl + "/common/upload", |
| | | filePath: url, |
| | | name: "files", // 注æï¼è¿éæ ¹æ®åä»£ç æ¯ "files" |
| | | header: { |
| | | Authorization: "Bearer " + getToken(), |
| | | }, |
| | | success: (res) => { |
| | | try { |
| | | const data = JSON.parse(res.data); |
| | | if (data.code === 200) { |
| | | // 妿è¿åçæ¯æ°ç»ï¼å第ä¸ä¸ªå
ç´ |
| | | const resultData = Array.isArray(data.data) ? data.data[0] : data.data; |
| | | |
| | | // å¤ç url èµå¼ |
| | | if (!resultData.url && resultData.previewURL) { |
| | | resultData.url = resultData.previewURL; |
| | | } |
| | | // å
¼å®¹å代ç ä¸ç name èµå¼ |
| | | if (!resultData.name && resultData.originalFilename) { |
| | | resultData.name = resultData.originalFilename; |
| | | } |
| | | |
| | | resolve(resultData); |
| | | } else { |
| | | reject(data.msg || "ä¸ä¼ 失败"); |
| | | } |
| | | } catch (e) { |
| | | reject("è§£æååºå¤±è´¥"); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | reject(err); |
| | | }, |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // ä¸ä¼ åçå¤ç |
| | | const afterRead = async (event) => { |
| | | let lists = [].concat(event.file); |
| | | let currentModelValue = [...props.modelValue]; |
| | | |
| | | // å
å¨å
é¨åè¡¨ä¸æ·»å å ä½ï¼ä¸ä¼ ä¸ç¶æï¼ |
| | | lists.forEach(item => { |
| | | internalFileList.value.push({ |
| | | ...item, |
| | | status: 'uploading', |
| | | message: 'ä¸ä¼ ä¸' |
| | | }); |
| | | }); |
| | | |
| | | for (let i = 0; i < lists.length; i++) { |
| | | try { |
| | | const result = await uploadFilePromise(lists[i].url); |
| | | |
| | | // æ´æ° modelValue |
| | | currentModelValue.push(result); |
| | | emit('update:modelValue', currentModelValue); |
| | | |
| | | } catch (e) { |
| | | // 妿ä¸ä¼ 失败ï¼ä»å
é¨å表ä¸ç§»é¤åææ·»å ç项 |
| | | const errorIndex = internalFileList.value.findIndex(item => item.status === 'uploading'); |
| | | if (errorIndex > -1) { |
| | | internalFileList.value.splice(errorIndex, 1); |
| | | } |
| | | showToast(typeof e === "string" ? e : "ä¸ä¼ 失败"); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // å é¤å¤ç |
| | | const deleteFile = (event) => { |
| | | const newList = [...props.modelValue]; |
| | | newList.splice(event.index, 1); |
| | | emit('update:modelValue', newList); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .common-upload { |
| | | width: 100%; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * OA 模åè·¯å¾å¸¸éï¼pages.json path ä¸å«åç¼ /ï¼ |
| | | * 导èªä½¿ç¨ï¼uni.navigateTo({ url: OA_NAV.xxx }) |
| | | */ |
| | | const P = "pages/oa"; |
| | | |
| | | export const OA_NAV = { |
| | | /** 人äºç®¡ç */ |
| | | staffArchive: `/${P}/HrManage/staff-archive/index`, |
| | | staffContract: `/${P}/HrManage/staff-contract/index`, |
| | | regularApply: `/${P}/HrManage/regular-apply/index`, |
| | | transferApply: `/${P}/HrManage/transfer-apply/index`, |
| | | resignApply: `/${P}/HrManage/resign-apply/index`, |
| | | workHandover: `/${P}/HrManage/work-handover/index`, |
| | | postManage: `/${P}/HrManage/post-manage/index`, |
| | | /** åå¤ç®¡ç */ |
| | | leaveApply: `/${P}/AttendManage/leave-apply/index`, |
| | | overtimeApply: `/${P}/AttendManage/overtime-apply/index`, |
| | | /** æ¥é管ç */ |
| | | travelReimburse: `/${P}/ReimburseManage/travel-reimburse/index`, |
| | | costReimburse: `/${P}/ReimburseManage/cost-reimburse/index`, |
| | | reimburseDetail: `/${P}/ReimburseManage/reimburse-detail/index`, |
| | | reimburseForm: `/${P}/ReimburseManage/reimburse-form/index`, |
| | | /** åå管ç */ |
| | | purchaseContract: `/${P}/ContractManage/purchase-contract/index`, |
| | | saleContract: `/${P}/ContractManage/sale-contract/index`, |
| | | /** 审æ¹ç®¡ç */ |
| | | approveList: `/${P}/ApproveManage/approve-list/index`, |
| | | approveListTemplateSelect: `/${P}/ApproveManage/approve-list/template-select`, |
| | | approveListApply: `/${P}/ApproveManage/approve-list/apply`, |
| | | approveListDetail: `/${P}/ApproveManage/approve-list/detail`, |
| | | approveListApprove: `/${P}/ApproveManage/approve-list/approve`, |
| | | approveTemplate: `/${P}/ApproveManage/approve-template/index`, |
| | | approveTemplateEdit: `/${P}/ApproveManage/approve-template/edit`, |
| | | approveTemplateDetail: `/${P}/ApproveManage/approve-template/detail`, |
| | | /** ä¼ä¸æ°é» / å
¬åéç¥ */ |
| | | enterpriseNews: `/${P}/EnterpriseNews/news-manage/index`, |
| | | noticeAnnouncement: `/${P}/NoticeAnnouncement/notice-manage/index`, |
| | | }; |
| | | |
| | | /** pages.json 注åç¨ pathï¼æ / åç¼ï¼ */ |
| | | export const OA_PAGE_PATHS = Object.fromEntries( |
| | | Object.entries(OA_NAV).map(([key, url]) => [ |
| | | key, |
| | | url.replace(/^\//, ""), |
| | | ]) |
| | | ); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { OA_NAV } from "./oaPaths.js"; |
| | | |
| | | /** |
| | | * OA 模ååç»ï¼å·¥ä½å°å±ç¤º / ææ¡£å¯¹ç
§ï¼ |
| | | */ |
| | | export const OA_MODULES = [ |
| | | { |
| | | key: "HrManage", |
| | | name: "人äºç®¡ç", |
| | | children: [ |
| | | // { label: "å工档æ¡", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.staffArchive }, |
| | | // { label: "åå·¥åå", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.staffContract }, |
| | | { label: "转æ£ç³è¯·", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.regularApply }, |
| | | { label: "è°å²ç³è¯·", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.transferApply }, |
| | | // { label: "离èç³è¯·", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.resignApply }, |
| | | { label: "å·¥ä½äº¤æ¥", icon: "/static/images/icon/gongchuguanli.svg", path: OA_NAV.workHandover }, |
| | | // { label: "å²ä½ç®¡ç", icon: "/static/images/icon/gongxuguanli.svg", path: OA_NAV.postManage }, |
| | | ], |
| | | }, |
| | | { |
| | | key: "AttendManage", |
| | | name: "åå¤ç®¡ç", |
| | | children: [ |
| | | { label: "请åç³è¯·", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.leaveApply }, |
| | | { label: "å çç³è¯·", icon: "/static/images/icon/dakaqiandao.svg", path: OA_NAV.overtimeApply }, |
| | | ], |
| | | }, |
| | | { |
| | | key: "ReimburseManage", |
| | | name: "æ¥é管ç", |
| | | children: [ |
| | | { label: "å·®æ
æ¥é", icon: "/static/images/icon/chuchaiguanli.svg", path: OA_NAV.travelReimburse }, |
| | | { label: "è´¹ç¨æ¥é", icon: "/static/images/icon/baoxiaoguanli.svg", path: OA_NAV.costReimburse }, |
| | | ], |
| | | }, |
| | | // { |
| | | // key: "ContractManage", |
| | | // name: "åå管ç", |
| | | // children: [ |
| | | // { label: "éè´åå", icon: "/static/images/icon/caigoutaizhang.svg", path: OA_NAV.purchaseContract }, |
| | | // { label: "éå®åå", icon: "/static/images/icon/xiaoshoutaizhang.svg", path: OA_NAV.saleContract }, |
| | | // ], |
| | | // }, |
| | | { |
| | | key: "ApproveManage", |
| | | name: "审æ¹ç®¡ç", |
| | | children: [ |
| | | { label: "审æ¹å表", icon: "/static/images/icon/xietongshenpi.svg", path: OA_NAV.approveList }, |
| | | { label: "å®¡æ¹æ¨¡æ¿", icon: "/static/images/icon/guizhangzhidu.svg", path: OA_NAV.approveTemplate }, |
| | | ], |
| | | }, |
| | | // { |
| | | // key: "EnterpriseNews", |
| | | // name: "ä¼ä¸æ°é»", |
| | | // children: [ |
| | | // { label: "ä¼ä¸æ°é»", icon: "/static/images/icon/zhishiku.svg", path: OA_NAV.enterpriseNews }, |
| | | // ], |
| | | // }, |
| | | // { |
| | | // key: "NoticeAnnouncement", |
| | | // name: "å
¬åéç¥", |
| | | // children: [ |
| | | // { label: "å
¬åéç¥", icon: "/static/images/icon/tongzhigonggao.svg", path: OA_NAV.noticeAnnouncement }, |
| | | // ], |
| | | // }, |
| | | ]; |
| | | |
| | | /** å·¥ä½å°æå¹³èåï¼çº¯å端é
ç½®ï¼ */ |
| | | export const OA_WORKBENCH_ITEMS = OA_MODULES.flatMap(module => |
| | | module.children.map(item => ({ |
| | | ...item, |
| | | module: module.name, |
| | | moduleKey: module.key, |
| | | })) |
| | | ); |
| | |
| | | "modules" : { |
| | | "Camera" : {}, |
| | | "Barcode" : {}, |
| | | // "Push" : {}, |
| | | "Maps" : {} |
| | | }, |
| | | /* åºç¨åå¸ä¿¡æ¯ */ |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/basicParameters/index", |
| | | "style": { |
| | | "navigationBarTitleText": "åºç¡åæ°", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/basicParameters/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "åæ°è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/processManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å·¥åºç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/processManagement/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "å·¥åºè¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/processManagement/params", |
| | | "style": { |
| | | "navigationBarTitleText": "å·¥åºåæ°é
ç½®", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/bom/index", |
| | | "style": { |
| | | "navigationBarTitleText": "BOM管ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionDesign/bom/structure", |
| | | "style": { |
| | | "navigationBarTitleText": "BOMç»æ", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/cooperativeOffice/collaborativeApproval/index1", |
| | | "style": { |
| | | "navigationBarTitleText": "å
¬åºç®¡ç", |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inspectionUpload/upload", |
| | | "style": { |
| | | "navigationBarTitleText": "ä¸ä¼ å·¡æ£è®°å½", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inspectionUpload/attachment", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¥çéä»¶", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/equipmentManagement/faultAnalysis/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æ
éåæè¿½æº¯", |
| | |
| | | "path": "pages/productionManagement/productionOrder/index", |
| | | "style": { |
| | | "navigationBarTitleText": "ç产订å", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/productionOrder/source", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¥æºæ°æ®", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/productionOrder/pickingDetail", |
| | | "style": { |
| | | "navigationBarTitleText": "é¢æè¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/processRoute/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å·¥èºè·¯çº¿", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/processRoute/items", |
| | | "style": { |
| | | "navigationBarTitleText": "路线项ç®", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/productionReporting/ledger", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¥å·¥å°è´¦", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/workOrder/index", |
| | | "style": { |
| | | "navigationBarTitleText": "ç产工å", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | // { |
| | | // "path": "pages/productionManagement/productionCosting/index", |
| | | // "style": { |
| | | // "navigationBarTitleText": "çäº§æ ¸ç®", |
| | | // "navigationStyle": "custom" |
| | | // } |
| | | // }, |
| | | { |
| | | "path": "pages/productionManagement/mainProductionPlan/index", |
| | | "style": { |
| | | "navigationBarTitleText": "主ç产计å", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/mainProductionPlan/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "ç产计å详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/productionScheduling/index", |
| | | "style": { |
| | | "navigationBarTitleText": "ç产æäº§", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/productionAccounting/index", |
| | | "style": { |
| | | "navigationBarTitleText": "çäº§æ ¸ç®", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/productionTraceability/index", |
| | | "style": { |
| | | "navigationBarTitleText": "ç产追溯", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/productionManagement/processStatistics/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å·¥åºç产å®åµ", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/receiptManagement/index", |
| | | "style": { |
| | |
| | | "style": { |
| | | "navigationBarTitleText": "æ¶æ¯ä¸å¿" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/fileManagement/borrow/index", |
| | | "style": { |
| | | "navigationBarTitleText": "åé
管ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/fileManagement/borrow/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "åé
ç»è®°", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/fileManagement/return/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å½è¿ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/fileManagement/return/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "å½è¿ç»è®°", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/staff-archive/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å工档æ¡", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/staff-contract/index", |
| | | "style": { |
| | | "navigationBarTitleText": "åå·¥åå", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/regular-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "转æ£ç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/transfer-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "è°å²ç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/resign-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "离èç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/work-handover/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å·¥ä½äº¤æ¥", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/post-manage/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å²ä½ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/AttendManage/leave-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "请åç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/AttendManage/overtime-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å çç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ReimburseManage/travel-reimburse/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å·®æ
æ¥é", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ReimburseManage/cost-reimburse/index", |
| | | "style": { |
| | | "navigationBarTitleText": "è´¹ç¨æ¥é", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ReimburseManage/reimburse-detail/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¥é详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ReimburseManage/reimburse-form/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¥éå¡«æ¥", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ContractManage/purchase-contract/index", |
| | | "style": { |
| | | "navigationBarTitleText": "éè´åå", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ContractManage/sale-contract/index", |
| | | "style": { |
| | | "navigationBarTitleText": "éå®åå", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-list/index", |
| | | "style": { |
| | | "navigationBarTitleText": "审æ¹å表", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-list/template-select", |
| | | "style": { |
| | | "navigationBarTitleText": "éæ©å®¡æ¹æ¨¡æ¿", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-list/apply", |
| | | "style": { |
| | | "navigationBarTitleText": "å起审æ¹", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-list/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "审æ¹è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-list/approve", |
| | | "style": { |
| | | "navigationBarTitleText": "审æ¹å¤ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-template/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å®¡æ¹æ¨¡æ¿", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-template/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "æ°å»ºå®¡æ¹æ¨¡æ¿", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-template/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "模æ¿è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/EnterpriseNews/news-manage/index", |
| | | "style": { |
| | | "navigationBarTitleText": "ä¼ä¸æ°é»", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/NoticeAnnouncement/notice-manage/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å
¬åéç¥", |
| | | "navigationStyle": "custom" |
| | | } |
| | | } |
| | | ], |
| | | "subPackages": [ |
| | |
| | | "navigationBarTitleText": "RuoYi", |
| | | "navigationBarBackgroundColor": "#FFFFFF" |
| | | } |
| | | } |
| | | } |
| | |
| | | <template> |
| | | <view class="approve-page"> |
| | | |
| | | <PageHeader title="å®¡æ ¸" @back="goBack" /> |
| | | |
| | | <PageHeader title="å®¡æ ¸" |
| | | @back="goBack" /> |
| | | <!-- ç³è¯·ä¿¡æ¯ --> |
| | | <view class="application-info"> |
| | | <view class="info-header"> |
| | |
| | | <text class="info-label">ç³è¯·æ¥æ</text> |
| | | <text class="info-value">{{ approvalData.approveTime }}</text> |
| | | </view> |
| | | |
| | | <!-- approveType=2 请åç¸å
³å段 --> |
| | | <template v-if="approvalData.approveType === 2"> |
| | | <view class="info-row"> |
| | |
| | | <text class="info-value">{{ approvalData.endDate || '-' }}</text> |
| | | </view> |
| | | </template> |
| | | |
| | | <!-- approveType=3 åºå·®ç¸å
³å段 --> |
| | | <view v-if="approvalData.approveType === 3" class="info-row"> |
| | | <view v-if="approvalData.approveType === 3" |
| | | class="info-row"> |
| | | <text class="info-label">åºå·®å°ç¹</text> |
| | | <text class="info-value">{{ approvalData.location || '-' }}</text> |
| | | </view> |
| | | |
| | | <!-- approveType=4 æ¥éç¸å
³å段 --> |
| | | <view v-if="approvalData.approveType === 4" class="info-row"> |
| | | <view v-if="approvalData.approveType === 4" |
| | | class="info-row"> |
| | | <text class="info-label">æ¥ééé¢</text> |
| | | <text class="info-value">{{ approvalData.price ? `Â¥${approvalData.price}` : '-' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å®¡æ¹æµç¨ --> |
| | | <view class="approval-process"> |
| | | <view class="process-header"> |
| | | <text class="process-title">å®¡æ¹æµç¨</text> |
| | | </view> |
| | | |
| | | <view class="process-steps"> |
| | | <view |
| | | v-for="(step, index) in approvalSteps" |
| | | :key="index" |
| | | class="process-step" |
| | | :class="{ |
| | | <view v-for="(step, index) in approvalSteps" |
| | | :key="index" |
| | | class="process-step" |
| | | :class="{ |
| | | 'completed': step.status === 'completed', |
| | | 'current': step.status === 'current', |
| | | 'pending': step.status === 'pending', |
| | | 'rejected': step.status === 'rejected' |
| | | }" |
| | | > |
| | | }"> |
| | | <view class="step-indicator"> |
| | | <view class="step-dot"> |
| | | <text v-if="step.status === 'completed'" class="step-icon">â</text> |
| | | <text v-else-if="step.status === 'rejected'" class="step-icon">â</text> |
| | | <text v-else class="step-number">{{ index + 1 }}</text> |
| | | <text v-if="step.status === 'completed'" |
| | | class="step-icon">â</text> |
| | | <text v-else-if="step.status === 'rejected'" |
| | | class="step-icon">â</text> |
| | | <text v-else |
| | | class="step-number">{{ index + 1 }}</text> |
| | | </view> |
| | | <view v-if="index < approvalSteps.length - 1" class="step-line"></view> |
| | | <view v-if="index < approvalSteps.length - 1" |
| | | class="step-line"></view> |
| | | </view> |
| | | |
| | | <view class="step-content"> |
| | | <view class="step-info"> |
| | | <text class="step-title">{{ step.title }}</text> |
| | | <text class="step-approver">{{ step.approverName }}</text> |
| | | <text v-if="step.approveTime" class="step-time">{{ step.approveTime }}</text> |
| | | <text v-if="step.approveTime" |
| | | class="step-time">{{ step.approveTime }}</text> |
| | | </view> |
| | | |
| | | <view v-if="step.opinion" class="step-opinion"> |
| | | <view v-if="step.opinion" |
| | | class="step-opinion"> |
| | | <text class="opinion-label">å®¡æ¹æè§ï¼</text> |
| | | <text class="opinion-content">{{ step.opinion }}</text> |
| | | </view> |
| | | <!-- ç¾åå±ç¤º --> |
| | | <view v-if="step.urlTem" class="step-opinion" style="margin-top:8px;"> |
| | | <view v-if="step.urlTem" |
| | | class="step-opinion" |
| | | style="margin-top:8px;"> |
| | | <text class="opinion-label">ç¾åï¼</text> |
| | | <image :src="step.urlTem" mode="widthFix" style="width:180px;border-radius:6px;border:1px solid #eee;" /> |
| | | <image :src="step.urlTem" |
| | | mode="widthFix" |
| | | style="width:180px;border-radius:6px;border:1px solid #eee;" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å®¡æ ¸æè§è¾å
¥ --> |
| | | <view v-if="canApprove" class="approval-input"> |
| | | <view v-if="canApprove" |
| | | class="approval-input"> |
| | | <view class="input-header"> |
| | | <text class="input-title">å®¡æ ¸æè§</text> |
| | | </view> |
| | | |
| | | <view class="input-content"> |
| | | <u-textarea |
| | | v-model="approvalOpinion" |
| | | rows="4" |
| | | placeholder="请è¾å
¥å®¡æ ¸æè§" |
| | | maxlength="200" |
| | | count |
| | | /> |
| | | <u-textarea v-model="approvalOpinion" |
| | | rows="4" |
| | | placeholder="请è¾å
¥å®¡æ ¸æè§" |
| | | maxlength="200" |
| | | count /> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- åºé¨æä½æé® --> |
| | | <view v-if="canApprove" class="footer-actions"> |
| | | <u-button class="reject-btn" @click="handleReject">驳å</u-button> |
| | | <u-button class="approve-btn" @click="handleApprove">éè¿</u-button> |
| | | <view v-if="canApprove" |
| | | class="footer-actions"> |
| | | <u-button class="reject-btn" |
| | | @click="handleReject">驳å</u-button> |
| | | <u-button class="approve-btn" |
| | | @click="handleApprove">éè¿</u-button> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed } from 'vue' |
| | | import { approveProcessGetInfo, approveProcessDetails, updateApproveNode } from '@/api/collaborativeApproval/approvalProcess' |
| | | import useUserStore from '@/store/modules/user' |
| | | const showToast = (message) => { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: 'none' |
| | | }) |
| | | } |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { ref, onMounted, computed } from "vue"; |
| | | import { |
| | | approveProcessGetInfo, |
| | | approveProcessDetails, |
| | | updateApproveNode, |
| | | } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | const showToast = message => { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: "none", |
| | | }); |
| | | }; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | |
| | | const userStore = useUserStore() |
| | | const approvalData = ref({}) |
| | | const approvalSteps = ref([]) |
| | | const approvalOpinion = ref('') |
| | | const approveId = ref('') |
| | | const userStore = useUserStore(); |
| | | const approvalData = ref({}); |
| | | const approvalSteps = ref([]); |
| | | const approvalOpinion = ref(""); |
| | | const approveId = ref(""); |
| | | |
| | | // ä»è¯¦æ
æ¥å£åæ®µå¯¹é½ canApproveï¼ä»
彿 isShen çèç¹æ¶å¯å®¡æ¹ |
| | | const canApprove = computed(() => { |
| | | return approvalSteps.value.some(step => step.isShen === true) |
| | | }) |
| | | // ä»è¯¦æ
æ¥å£åæ®µå¯¹é½ canApproveï¼ä»
彿 isShen çèç¹æ¶å¯å®¡æ¹ |
| | | const canApprove = computed(() => { |
| | | return approvalSteps.value.some(step => step.isShen === true); |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | approveId.value = uni.getStorageSync('approveId') |
| | | if (approveId.value) { |
| | | loadApprovalData() |
| | | } |
| | | }) |
| | | |
| | | const loadApprovalData = () => { |
| | | // åºæ¬ç³è¯·ä¿¡æ¯ |
| | | approveProcessGetInfo({ id: approveId.value }).then(res => { |
| | | approvalData.value = res.data || {} |
| | | }) |
| | | // 审æ¹èç¹è¯¦æ
|
| | | approveProcessDetails(approveId.value).then(res => { |
| | | const list = Array.isArray(res.data) ? res.data : [] |
| | | // ä¿ååå§èç¹æ°æ®ä¾æäº¤ä½¿ç¨ |
| | | activities.value = list |
| | | |
| | | approvalSteps.value = list.map((it, idx) => { |
| | | // èç¹ç¶ææ å°ï¼1=éè¿ï¼2=ä¸éè¿ï¼å¦åçæ¯å¦å½å(isShen)ï¼åé»è®¤ä¸ºå¾
å¤ç |
| | | let status = 'pending' |
| | | if (it.approveNodeStatus === 1) status = 'completed' |
| | | else if (it.approveNodeStatus === 2) status = 'rejected' |
| | | else if (it.isShen) status = 'current' |
| | | return { |
| | | title: `第${idx + 1}æ¥å®¡æ¹`, |
| | | approverName: it.approveNodeUser || 'æªç¥ç¨æ·', |
| | | status, |
| | | approveTime: it.approveTime || null, |
| | | opinion: it.approveNodeReason || '', |
| | | urlTem: it.urlTem || '', |
| | | isShen: !!it.isShen |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | const goBack = () => { |
| | | uni.removeStorageSync('approveId'); |
| | | uni.navigateBack() |
| | | } |
| | | |
| | | const submitForm = (status) => { |
| | | // å¯éï¼æ ¡éªå®¡æ ¸æè§ |
| | | if (!approvalOpinion.value?.trim()) { |
| | | showToast('请è¾å
¥å®¡æ ¸æè§') |
| | | return |
| | | } |
| | | // æ¾å°å½åå¯å®¡æ¹èç¹ |
| | | const filteredActivities = activities.value.filter(activity => activity.isShen) |
| | | if (!filteredActivities.length) { |
| | | showToast('å½åæ å¯å®¡æ¹èç¹') |
| | | return |
| | | } |
| | | // åå
¥ç¶æåæè§ |
| | | filteredActivities[0].approveNodeStatus = status |
| | | filteredActivities[0].approveNodeReason = approvalOpinion.value || '' |
| | | // è®¡ç®æ¯å¦ä¸ºæå䏿¥ |
| | | const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length - 1 |
| | | // è°ç¨å端 |
| | | updateApproveNode({ ...filteredActivities[0], isLast }).then(() => { |
| | | const msg = status === 1 ? '审æ¹éè¿' : '审æ¹å·²é©³å' |
| | | showToast(msg) |
| | | // æç¤ºåè¿åä¸ä¸ä¸ªé¡µé¢ |
| | | setTimeout(() => { |
| | | goBack() // å
鍿¯ uni.navigateBack() |
| | | }, 800) |
| | | }) |
| | | } |
| | | |
| | | const handleApprove = () => { |
| | | uni.showModal({ |
| | | title: '确认æä½', |
| | | content: 'ç¡®å®è¦éè¿æ¤å®¡æ¹åï¼', |
| | | success: (res) => { |
| | | if (res.confirm) submitForm(1) |
| | | onMounted(() => { |
| | | approveId.value = uni.getStorageSync("approveId"); |
| | | if (approveId.value) { |
| | | loadApprovalData(); |
| | | } |
| | | }) |
| | | } |
| | | }); |
| | | |
| | | const handleReject = () => { |
| | | uni.showModal({ |
| | | title: '确认æä½', |
| | | content: 'ç¡®å®è¦é©³åæ¤å®¡æ¹åï¼', |
| | | success: (res) => { |
| | | if (res.confirm) submitForm(2) |
| | | const loadApprovalData = () => { |
| | | // åºæ¬ç³è¯·ä¿¡æ¯ |
| | | approveProcessGetInfo({ id: approveId.value }).then(res => { |
| | | approvalData.value = res.data || {}; |
| | | }); |
| | | // 审æ¹èç¹è¯¦æ
|
| | | approveProcessDetails(approveId.value).then(res => { |
| | | const list = Array.isArray(res.data) ? res.data : []; |
| | | // ä¿ååå§èç¹æ°æ®ä¾æäº¤ä½¿ç¨ |
| | | activities.value = list; |
| | | |
| | | approvalSteps.value = list.map((it, idx) => { |
| | | // èç¹ç¶ææ å°ï¼1=éè¿ï¼2=ä¸éè¿ï¼å¦åçæ¯å¦å½å(isShen)ï¼åé»è®¤ä¸ºå¾
å¤ç |
| | | let status = "pending"; |
| | | if (it.approveNodeStatus === 1) status = "completed"; |
| | | else if (it.approveNodeStatus === 2) status = "rejected"; |
| | | else if (it.isShen) status = "current"; |
| | | return { |
| | | title: `第${idx + 1}æ¥å®¡æ¹`, |
| | | approverName: it.approveNodeUser || "æªç¥ç¨æ·", |
| | | status, |
| | | approveTime: it.approveTime || null, |
| | | opinion: it.approveNodeReason || "", |
| | | urlTem: it.urlTem || "", |
| | | isShen: !!it.isShen, |
| | | }; |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.removeStorageSync("approveId"); |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const submitForm = status => { |
| | | // å¯éï¼æ ¡éªå®¡æ ¸æè§ |
| | | if (!approvalOpinion.value?.trim()) { |
| | | showToast("请è¾å
¥å®¡æ ¸æè§"); |
| | | return; |
| | | } |
| | | }) |
| | | } |
| | | // åå§èç¹æ°æ®ï¼ç¨äºæäº¤é»è¾ï¼ |
| | | const activities = ref([]) |
| | | // æ¾å°å½åå¯å®¡æ¹èç¹ |
| | | const filteredActivities = activities.value.filter( |
| | | activity => activity.isShen |
| | | ); |
| | | if (!filteredActivities.length) { |
| | | showToast("å½åæ å¯å®¡æ¹èç¹"); |
| | | return; |
| | | } |
| | | // åå
¥ç¶æåæè§ |
| | | filteredActivities[0].approveNodeStatus = status; |
| | | filteredActivities[0].approveNodeReason = approvalOpinion.value || ""; |
| | | // è®¡ç®æ¯å¦ä¸ºæå䏿¥ |
| | | const isLast = |
| | | activities.value.findIndex(a => a.isShen) === activities.value.length - 1; |
| | | // è°ç¨å端 |
| | | updateApproveNode({ ...filteredActivities[0], isLast }).then(() => { |
| | | const msg = status === 1 ? "审æ¹éè¿" : "审æ¹å·²é©³å"; |
| | | showToast(msg); |
| | | // æç¤ºåè¿åä¸ä¸ä¸ªé¡µé¢ |
| | | setTimeout(() => { |
| | | goBack(); // å
鍿¯ uni.navigateBack() |
| | | }, 800); |
| | | }); |
| | | }; |
| | | |
| | | const handleApprove = () => { |
| | | uni.showModal({ |
| | | title: "确认æä½", |
| | | content: "ç¡®å®è¦éè¿æ¤å®¡æ¹åï¼", |
| | | success: res => { |
| | | if (res.confirm) submitForm(1); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const handleReject = () => { |
| | | uni.showModal({ |
| | | title: "确认æä½", |
| | | content: "ç¡®å®è¦é©³åæ¤å®¡æ¹åï¼", |
| | | success: res => { |
| | | if (res.confirm) submitForm(2); |
| | | }, |
| | | }); |
| | | }; |
| | | // åå§èç¹æ°æ®ï¼ç¨äºæäº¤é»è¾ï¼ |
| | | const activities = ref([]); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .approve-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 80px; |
| | | } |
| | | |
| | | .header { |
| | | display: flex; |
| | | align-items: center; |
| | | background: #fff; |
| | | padding: 16px 20px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 100; |
| | | } |
| | | |
| | | .title { |
| | | flex: 1; |
| | | text-align: center; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .application-info { |
| | | background: #fff; |
| | | margin: 16px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .info-header { |
| | | padding: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | .info-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .info-content { |
| | | padding: 16px; |
| | | } |
| | | |
| | | .info-row { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | .approve-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 80px; |
| | | } |
| | | } |
| | | |
| | | .info-label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | width: 80px; |
| | | flex-shrink: 0; |
| | | } |
| | | .header { |
| | | display: flex; |
| | | align-items: center; |
| | | background: #fff; |
| | | padding: 16px 20px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 100; |
| | | } |
| | | |
| | | .info-value { |
| | | font-size: 14px; |
| | | color: #333; |
| | | flex: 1; |
| | | } |
| | | .title { |
| | | flex: 1; |
| | | text-align: center; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .approval-process { |
| | | background: #fff; |
| | | margin: 16px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | .application-info { |
| | | background: #fff; |
| | | margin: 16px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .process-header { |
| | | padding: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | background: #f8f9fa; |
| | | } |
| | | .info-header { |
| | | padding: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | .process-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | .info-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .process-steps { |
| | | padding: 20px; |
| | | } |
| | | .info-content { |
| | | padding: 16px; |
| | | } |
| | | |
| | | .process-step { |
| | | display: flex; |
| | | position: relative; |
| | | margin-bottom: 24px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | |
| | | .step-line { |
| | | display: none; |
| | | .info-row { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .step-indicator { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | margin-right: 16px; |
| | | } |
| | | .info-label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | width: 80px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .step-dot { |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | position: relative; |
| | | z-index: 2; |
| | | } |
| | | .info-value { |
| | | font-size: 14px; |
| | | color: #333; |
| | | flex: 1; |
| | | } |
| | | |
| | | .process-step.completed .step-dot { |
| | | background: #52c41a; |
| | | color: #fff; |
| | | } |
| | | .approval-process { |
| | | background: #fff; |
| | | margin: 16px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .process-step.current .step-dot { |
| | | background: #1890ff; |
| | | color: #fff; |
| | | animation: pulse 2s infinite; |
| | | } |
| | | .process-header { |
| | | padding: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | .process-step.pending .step-dot { |
| | | background: #d9d9d9; |
| | | color: #999; |
| | | } |
| | | .process-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .step-line { |
| | | width: 2px; |
| | | height: 40px; |
| | | background: #d9d9d9; |
| | | margin-top: 8px; |
| | | } |
| | | .process-steps { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .process-step.completed .step-line { |
| | | background: #52c41a; |
| | | } |
| | | .process-step { |
| | | display: flex; |
| | | position: relative; |
| | | margin-bottom: 24px; |
| | | |
| | | .process-step.rejected .step-dot { |
| | | background: #ff4d4f; |
| | | color: #fff; |
| | | } |
| | | .process-step.rejected .step-line { |
| | | background: #ff4d4f; |
| | | } |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | |
| | | .step-content { |
| | | flex: 1; |
| | | padding-top: 4px; |
| | | } |
| | | .step-line { |
| | | display: none; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .step-info { |
| | | margin-bottom: 8px; |
| | | } |
| | | .step-indicator { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | margin-right: 16px; |
| | | } |
| | | |
| | | .step-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | display: block; |
| | | margin-bottom: 4px; |
| | | } |
| | | .step-dot { |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | position: relative; |
| | | z-index: 2; |
| | | } |
| | | |
| | | .step-approver { |
| | | font-size: 14px; |
| | | color: #666; |
| | | display: block; |
| | | margin-bottom: 4px; |
| | | } |
| | | .process-step.completed .step-dot { |
| | | background: #52c41a; |
| | | color: #fff; |
| | | } |
| | | |
| | | .step-time { |
| | | font-size: 12px; |
| | | color: #999; |
| | | display: block; |
| | | } |
| | | .process-step.current .step-dot { |
| | | background: #1890ff; |
| | | color: #fff; |
| | | animation: pulse 2s infinite; |
| | | } |
| | | |
| | | .step-opinion { |
| | | background: #f8f9fa; |
| | | padding: 12px; |
| | | border-radius: 8px; |
| | | border-left: 4px solid #52c41a; |
| | | } |
| | | .process-step.pending .step-dot { |
| | | background: #d9d9d9; |
| | | color: #999; |
| | | } |
| | | |
| | | .opinion-label { |
| | | font-size: 12px; |
| | | color: #666; |
| | | display: block; |
| | | margin-bottom: 4px; |
| | | } |
| | | .step-line { |
| | | width: 2px; |
| | | height: 40px; |
| | | background: #d9d9d9; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .opinion-content { |
| | | font-size: 14px; |
| | | color: #333; |
| | | line-height: 1.5; |
| | | } |
| | | .process-step.completed .step-line { |
| | | background: #52c41a; |
| | | } |
| | | |
| | | .approval-input { |
| | | background: #fff; |
| | | margin: 16px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | .process-step.rejected .step-dot { |
| | | background: #ff4d4f; |
| | | color: #fff; |
| | | } |
| | | .process-step.rejected .step-line { |
| | | background: #ff4d4f; |
| | | } |
| | | |
| | | .input-header { |
| | | padding: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | background: #f8f9fa; |
| | | } |
| | | .step-content { |
| | | flex: 1; |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | .input-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | .step-info { |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .input-content { |
| | | padding: 16px; |
| | | } |
| | | .step-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | display: block; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .footer-actions { |
| | | position: fixed; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: #fff; |
| | | display: flex; |
| | | justify-content: space-around; |
| | | align-items: center; |
| | | padding: 16px; |
| | | box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); |
| | | z-index: 1000; |
| | | } |
| | | .step-approver { |
| | | font-size: 14px; |
| | | color: #666; |
| | | display: block; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .reject-btn { |
| | | .step-time { |
| | | font-size: 12px; |
| | | color: #999; |
| | | display: block; |
| | | } |
| | | |
| | | .step-opinion { |
| | | background: #f8f9fa; |
| | | padding: 12px; |
| | | border-radius: 8px; |
| | | border-left: 4px solid #52c41a; |
| | | } |
| | | |
| | | .opinion-label { |
| | | font-size: 12px; |
| | | color: #666; |
| | | display: block; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .opinion-content { |
| | | font-size: 14px; |
| | | color: #333; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .approval-input { |
| | | background: #fff; |
| | | margin: 16px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .input-header { |
| | | padding: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | .input-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .input-content { |
| | | padding: 16px; |
| | | } |
| | | |
| | | .footer-actions { |
| | | position: fixed; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: #fff; |
| | | display: flex; |
| | | justify-content: space-around; |
| | | align-items: center; |
| | | padding: 16px; |
| | | box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); |
| | | z-index: 1000; |
| | | } |
| | | |
| | | .reject-btn { |
| | | width: 120px; |
| | | background: #ff4d4f; |
| | | color: #fff; |
| | |
| | | background: #52c41a; |
| | | color: #fff; |
| | | } |
| | | |
| | | |
| | | /* éé
u-buttonæ ·å¼ */ |
| | | :deep(.u-button) { |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | @keyframes pulse { |
| | | 0% { |
| | | box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7); |
| | | @keyframes pulse { |
| | | 0% { |
| | | box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7); |
| | | } |
| | | 70% { |
| | | box-shadow: 0 0 0 10px rgba(24, 144, 255, 0); |
| | | } |
| | | 100% { |
| | | box-shadow: 0 0 0 0 rgba(24, 144, 255, 0); |
| | | } |
| | | } |
| | | 70% { |
| | | box-shadow: 0 0 0 10px rgba(24, 144, 255, 0); |
| | | .signature-section { |
| | | background: #fff; |
| | | padding: 12px 16px 16px; |
| | | border-top: 1px solid #f0f0f0; |
| | | } |
| | | 100% { |
| | | box-shadow: 0 0 0 0 rgba(24, 144, 255, 0); |
| | | .signature-header { |
| | | margin-bottom: 8px; |
| | | } |
| | | } |
| | | .signature-section { |
| | | background: #fff; |
| | | padding: 12px 16px 16px; |
| | | border-top: 1px solid #f0f0f0; |
| | | } |
| | | .signature-header { |
| | | margin-bottom: 8px; |
| | | } |
| | | .signature-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | .signature-box { |
| | | width: 100%; |
| | | height: 180px; |
| | | background: #fff; |
| | | border: 1px dashed #d9d9d9; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | .signature-actions { |
| | | margin-top: 8px; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | } |
| | | .signature-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | .signature-box { |
| | | width: 100%; |
| | | height: 180px; |
| | | background: #fff; |
| | | border: 1px dashed #d9d9d9; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | .signature-actions { |
| | | margin-top: 8px; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader title="å®¡æ¹æµç¨" |
| | | <PageHeader :title="operationType === 'detail' ? '详æ
' : 'å®¡æ¹æµç¨'" |
| | | @back="goBack" /> |
| | | <!-- 表ååºå --> |
| | | <u-form ref="formRef" |
| | |
| | | :rules="rules" |
| | | :model="form" |
| | | label-width="140rpx"> |
| | | <u-form-item prop="approveReason" |
| | | label="æµç¨ç¼å·"> |
| | | <u-input v-model="form.approveId" |
| | | disabled |
| | | placeholder="èªå¨ç¼å·" /> |
| | | </u-form-item> |
| | | <u-form-item prop="approveReason" |
| | | :label="approveType === 5 ? 'éè´äºç±' : 'ç³è¯·äºç±'" |
| | | required> |
| | | <u-input v-model="form.approveReason" |
| | | type="textarea" |
| | | rows="2" |
| | | auto-height |
| | | maxlength="200" |
| | | :placeholder="approveType === 5 ? '请è¾å
¥éè´äºç±' : '请è¾å
¥ç³è¯·äºç±'" |
| | | show-word-limit /> |
| | | </u-form-item> |
| | | <u-form-item prop="approveDeptName" |
| | | label="ç³è¯·é¨é¨" |
| | | required> |
| | | <!-- <u-input v-model="form.approveDeptName" |
| | | <template v-if="operationType !== 'detail'"> |
| | | <u-form-item prop="approveReason" |
| | | label="æµç¨ç¼å·"> |
| | | <u-input v-model="form.approveId" |
| | | disabled |
| | | placeholder="èªå¨ç¼å·" /> |
| | | </u-form-item> |
| | | <u-form-item prop="approveReason" |
| | | :label="approveType === 5 ? 'éè´äºç±' : 'ç³è¯·äºç±'" |
| | | required> |
| | | <u-input v-model="form.approveReason" |
| | | type="textarea" |
| | | rows="2" |
| | | auto-height |
| | | maxlength="200" |
| | | :placeholder="approveType === 5 ? '请è¾å
¥éè´äºç±' : '请è¾å
¥ç³è¯·äºç±'" |
| | | show-word-limit /> |
| | | </u-form-item> |
| | | <u-form-item prop="approveDeptName" |
| | | label="ç³è¯·é¨é¨" |
| | | required> |
| | | <!-- <u-input v-model="form.approveDeptName" |
| | | placeholder="è¯·éæ©ç³è¯·é¨é¨" /> --> |
| | | <u-input v-model="form.approveDeptName" |
| | | readonly |
| | | placeholder="è¯·éæ©ç³è¯·é¨é¨" |
| | | @click="showPicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showPicker = true"></up-icon> |
| | | </template> |
| | | </u-form-item> |
| | | <u-form-item prop="approveUser" |
| | | <u-input v-model="form.approveDeptName" |
| | | readonly |
| | | placeholder="è¯·éæ©ç³è¯·é¨é¨" |
| | | @click="showPicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showPicker = true"></up-icon> |
| | | </template> |
| | | </u-form-item> |
| | | <!-- <u-form-item prop="approveUser" |
| | | label="ç³è¯·äºº" |
| | | required> |
| | | <u-input v-model="form.approveUserName" |
| | |
| | | <up-icon name="arrow-right" |
| | | @click="showDatePicker"></up-icon> |
| | | </template> |
| | | </u-form-item> |
| | | <!-- approveType=2 请åç¸å
³å段 --> |
| | | <template v-if="approveType === 2"> |
| | | <u-form-item prop="startDate" |
| | | label="å¼å§æ¶é´" |
| | | </u-form-item> --> |
| | | <!-- approveType=2 请åç¸å
³å段 --> |
| | | <template v-if="approveType === 2"> |
| | | <u-form-item prop="startDate" |
| | | label="å¼å§æ¶é´" |
| | | required> |
| | | <u-input v-model="form.startDate" |
| | | readonly |
| | | placeholder="请åå¼å§æ¶é´" |
| | | @click="showStartDatePicker" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showStartDatePicker"></up-icon> |
| | | </template> |
| | | </u-form-item> |
| | | <u-form-item prop="endDate" |
| | | label="ç»ææ¶é´" |
| | | required> |
| | | <u-input v-model="form.endDate" |
| | | readonly |
| | | placeholder="请åç»ææ¶é´" |
| | | @click="showEndDatePicker" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showEndDatePicker"></up-icon> |
| | | </template> |
| | | </u-form-item> |
| | | </template> |
| | | <!-- approveType=3 åºå·®ç¸å
³å段 --> |
| | | <u-form-item v-if="approveType === 3" |
| | | prop="location" |
| | | label="åºå·®å°ç¹" |
| | | required> |
| | | <u-input v-model="form.startDate" |
| | | readonly |
| | | placeholder="请åå¼å§æ¶é´" |
| | | @click="showStartDatePicker" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showStartDatePicker"></up-icon> |
| | | </template> |
| | | <u-input v-model="form.location" |
| | | placeholder="请è¾å
¥åºå·®å°ç¹" |
| | | clearable /> |
| | | </u-form-item> |
| | | <u-form-item prop="endDate" |
| | | label="ç»ææ¶é´" |
| | | <!-- approveType=4 æ¥éç¸å
³å段 --> |
| | | <u-form-item v-if="approveType === 4" |
| | | prop="price" |
| | | label="æ¥ééé¢" |
| | | required> |
| | | <u-input v-model="form.endDate" |
| | | readonly |
| | | placeholder="请åç»ææ¶é´" |
| | | @click="showEndDatePicker" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showEndDatePicker"></up-icon> |
| | | </template> |
| | | <u-input v-model="form.price" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ¥ééé¢" |
| | | clearable /> |
| | | </u-form-item> |
| | | </template> |
| | | <!-- approveType=3 åºå·®ç¸å
³å段 --> |
| | | <u-form-item v-if="approveType === 3" |
| | | prop="location" |
| | | label="åºå·®å°ç¹" |
| | | required> |
| | | <u-input v-model="form.location" |
| | | placeholder="请è¾å
¥åºå·®å°ç¹" |
| | | clearable /> |
| | | </u-form-item> |
| | | <!-- approveType=4 æ¥éç¸å
³å段 --> |
| | | <u-form-item v-if="approveType === 4" |
| | | prop="price" |
| | | label="æ¥ééé¢" |
| | | required> |
| | | <u-input v-model="form.price" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ¥ééé¢" |
| | | clearable /> |
| | | <!-- æ¥ä»·å®¡æ¹è¯¦æ
--> |
| | | <view v-if="isQuotationApproval" |
| | | style="margin: 20rpx 0;"> |
| | | <u-divider text="æ¥ä»·è¯¦æ
" |
| | | text-size="28rpx" |
| | | color="#2979ff"></u-divider> |
| | | <u-skeleton :loading="quotationLoading" |
| | | rows="3" |
| | | animated> |
| | | <view v-if="!currentQuotation || !currentQuotation.quotationNo" |
| | | style="padding: 40rpx; text-align: center; color: #999;"> |
| | | æªæ¥è¯¢å°å¯¹åºæ¥ä»·è¯¦æ
|
| | | </view> |
| | | <view v-else> |
| | | <u-cell-group :border="false"> |
| | | <u-cell title="æ¥ä»·åå·" |
| | | :value="currentQuotation.quotationNo"></u-cell> |
| | | <u-cell title="客æ·åç§°" |
| | | :value="currentQuotation.customer"></u-cell> |
| | | <u-cell title="ä¸å¡å" |
| | | :value="currentQuotation.salesperson"></u-cell> |
| | | <u-cell title="æ¥ä»·æ¥æ" |
| | | :value="currentQuotation.quotationDate"></u-cell> |
| | | <u-cell title="æææè³" |
| | | :value="currentQuotation.validDate"></u-cell> |
| | | <u-cell title="仿¬¾æ¹å¼" |
| | | :value="currentQuotation.paymentMethod"></u-cell> |
| | | <u-cell title="æ¥ä»·æ»é¢"> |
| | | <template #value> |
| | | <text style="font-size: 32rpx; color: #e6a23c; font-weight: bold;"> |
| | | ¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }} |
| | | </text> |
| | | </template> |
| | | </u-cell> |
| | | </u-cell-group> |
| | | <view style="margin-top: 20rpx; padding: 0 30rpx;"> |
| | | <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产åæç»</view> |
| | | <view v-for="(item, index) in (currentQuotation.products || [])" |
| | | :key="index" |
| | | style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;"> |
| | | <view style="display: flex; justify-content: space-between;"> |
| | | <text style="font-weight: bold;">{{ item.product }}</text> |
| | | <text style="color: #e6a23c;">Â¥{{ Number(item.unitPrice ?? 0).toFixed(2) }}</text> |
| | | </view> |
| | | <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;"> |
| | | è§æ ¼: {{ item.specification }} | åä½: {{ item.unit }} |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="currentQuotation.remark" |
| | | style="margin-top: 20rpx; padding: 0 30rpx;"> |
| | | <view style="font-size: 28rpx; font-weight: bold;">夿³¨</view> |
| | | <view style="font-size: 26rpx; color: #666; margin-top: 10rpx;">{{ currentQuotation.remark }}</view> |
| | | </view> |
| | | </view> |
| | | </u-skeleton> |
| | | </view> |
| | | <!-- éè´å®¡æ¹è¯¦æ
--> |
| | | <view v-if="isPurchaseApproval" |
| | | style="margin: 20rpx 0;"> |
| | | <u-divider text="éè´è¯¦æ
" |
| | | text-size="28rpx" |
| | | color="#2979ff"></u-divider> |
| | | <u-skeleton :loading="purchaseLoading" |
| | | rows="3" |
| | | animated> |
| | | <view v-if="!currentPurchase || !currentPurchase.purchaseContractNumber" |
| | | style="padding: 40rpx; text-align: center; color: #999;"> |
| | | æªæ¥è¯¢å°å¯¹åºéè´è¯¦æ
|
| | | </view> |
| | | <view v-else> |
| | | <u-cell-group :border="false"> |
| | | <u-cell title="éè´ååå·" |
| | | :value="currentPurchase.purchaseContractNumber"></u-cell> |
| | | <u-cell title="ä¾åºååç§°" |
| | | :value="currentPurchase.supplierName"></u-cell> |
| | | <u-cell title="项ç®åç§°" |
| | | :value="currentPurchase.projectName"></u-cell> |
| | | <u-cell title="éå®ååå·" |
| | | :value="currentPurchase.salesContractNo"></u-cell> |
| | | <u-cell title="ç¾è®¢æ¥æ" |
| | | :value="currentPurchase.executionDate"></u-cell> |
| | | <u-cell title="å½å
¥æ¥æ" |
| | | :value="currentPurchase.entryDate"></u-cell> |
| | | <u-cell title="仿¬¾æ¹å¼" |
| | | :value="currentPurchase.paymentMethod"></u-cell> |
| | | <u-cell title="ååéé¢"> |
| | | <template #value> |
| | | <text style="font-size: 32rpx; color: #e6a23c; font-weight: bold;"> |
| | | ¥{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }} |
| | | </text> |
| | | </template> |
| | | </u-cell> |
| | | </u-cell-group> |
| | | <view style="margin-top: 20rpx; padding: 0 30rpx;"> |
| | | <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产åæç»</view> |
| | | <view v-for="(item, index) in (currentPurchase.productData || [])" |
| | | :key="index" |
| | | style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;"> |
| | | <view style="display: flex; justify-content: space-between;"> |
| | | <text style="font-weight: bold;">{{ item.productCategory }}</text> |
| | | <text style="color: #e6a23c;">Â¥{{ Number(item.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</text> |
| | | </view> |
| | | <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;"> |
| | | è§æ ¼: {{ item.specificationModel }} | æ°é: {{ item.quantity }} {{ item.unit }} |
| | | </view> |
| | | <view style="font-size: 24rpx; color: #999; margin-top: 4rpx;"> |
| | | å«ç¨åä»·: Â¥{{ Number(item.taxInclusiveUnitPrice ?? 0).toFixed(2) }} |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </u-skeleton> |
| | | </view> |
| | | <!-- å货审æ¹è¯¦æ
--> |
| | | <view v-if="isDeliveryApproval" |
| | | style="margin: 20rpx 0;"> |
| | | <u-divider text="å货详æ
" |
| | | text-size="28rpx" |
| | | color="#2979ff"></u-divider> |
| | | <u-skeleton :loading="deliveryLoading" |
| | | rows="3" |
| | | animated> |
| | | <view v-if="!currentDelivery || !currentDelivery.shippingInfo" |
| | | style="padding: 40rpx; text-align: center; color: #999;"> |
| | | æªæ¥è¯¢å°å¯¹åºå货详æ
|
| | | </view> |
| | | <view v-else> |
| | | <u-cell-group :border="false"> |
| | | <u-cell title="éå®è®¢å" |
| | | :value="currentDelivery.shippingInfo.salesContractNo || '--'"></u-cell> |
| | | <u-cell title="å货订åå·" |
| | | :value="currentDelivery.shippingInfo.shippingNo || '--'"></u-cell> |
| | | <u-cell title="客æ·åç§°" |
| | | :value="currentDelivery.shippingInfo.customerName || '--'"></u-cell> |
| | | <u-cell title="åè´§ç±»å" |
| | | :value="currentDelivery.shippingInfo.type || '--'"></u-cell> |
| | | <u-cell title="åè´§æ¥æ" |
| | | :value="currentDelivery.shippingInfo.shippingDate || '--'"></u-cell> |
| | | <u-cell title="å®¡æ ¸ç¶æ" |
| | | :value="currentDelivery.shippingInfo.status || '--'"></u-cell> |
| | | <u-cell title="å货车çå·" |
| | | :value="currentDelivery.shippingInfo.shippingCarNumber || '--'"></u-cell> |
| | | <u-cell title="å¿«éå
¬å¸" |
| | | :value="currentDelivery.shippingInfo.expressCompany || '--'"></u-cell> |
| | | <u-cell title="å¿«éåå·" |
| | | :value="currentDelivery.shippingInfo.expressNumber || '--'"></u-cell> |
| | | </u-cell-group> |
| | | <view style="margin-top: 20rpx; padding: 0 30rpx;"> |
| | | <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">产åæç»</view> |
| | | <view v-for="(item, index) in deliveryProductList" |
| | | :key="index" |
| | | style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;"> |
| | | <view style="display: flex; justify-content: space-between;"> |
| | | <text style="font-weight: bold;">{{ item.productName }}</text> |
| | | <text style="color: #2979ff;">æ°é: {{ item.deliveryQuantity }}</text> |
| | | </view> |
| | | <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;"> |
| | | è§æ ¼: {{ item.specificationModel }} |
| | | </view> |
| | | <view v-if="item.batchNo" |
| | | style="font-size: 24rpx; color: #999; margin-top: 4rpx;"> |
| | | æ¹å·: {{ item.batchNo }} |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length" |
| | | style="margin-top: 20rpx; padding: 0 30rpx;"> |
| | | <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">åè´§å¾ç</view> |
| | | <CommonUpload :model-value="currentDelivery.shippingInfo.storageBlobVOs" |
| | | disabled /> |
| | | </view> |
| | | </view> |
| | | </u-skeleton> |
| | | </view> |
| | | <u-form-item v-if="operationType !== 'detail'" |
| | | label="å¾çéä»¶" |
| | | prop="storageBlobDTOS" |
| | | border-bottom> |
| | | <CommonUpload v-model="form.storageBlobDTOS" /> |
| | | </u-form-item> |
| | | </u-form> |
| | | <!-- éæ©å¨å¼¹çª --> |
| | | <up-action-sheet :show="showPicker" |
| | | :actions="productOptions" |
| | | title="éæ©é¨é¨" |
| | | @select="onConfirm" |
| | | @close="showPicker = false" /> |
| | | <!-- æ¥æéæ©å¨ --> |
| | | <up-popup :show="showDate" |
| | | mode="bottom" |
| | | @close="showDate = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="currentDate" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDate = false" |
| | | mode="date" /> |
| | | </up-popup> |
| | | <!-- 请åå¼å§æ¶é´éæ©å¨ --> |
| | | <up-popup :show="showStartDate" |
| | | mode="bottom" |
| | | @close="showStartDate = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="startDateValue" |
| | | @confirm="onStartDateConfirm" |
| | | @cancel="showStartDate = false" |
| | | mode="date" /> |
| | | </up-popup> |
| | | <!-- 请åç»ææ¶é´éæ©å¨ --> |
| | | <up-popup :show="showEndDate" |
| | | mode="bottom" |
| | | @close="showEndDate = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="endDateValue" |
| | | @confirm="onEndDateConfirm" |
| | | @cancel="showEndDate = false" |
| | | mode="date" /> |
| | | </up-popup> |
| | | <!-- å®¡æ ¸æµç¨åºå --> |
| | | <view class="approval-process"> |
| | | <view class="approval-header"> |
| | | <text class="approval-title">å®¡æ ¸æµç¨</text> |
| | | <text class="approval-desc">æ¯ä¸ªæ¥éª¤åªè½éæ©ä¸ä¸ªå®¡æ¹äºº</text> |
| | | </view> |
| | | <view class="approval-steps"> |
| | | <view v-for="(step, stepIndex) in approverNodes" |
| | | :key="stepIndex" |
| | | class="approval-step"> |
| | | <view class="step-dot"></view> |
| | | <view class="step-title"> |
| | | <text>审æ¹äºº</text> |
| | | </view> |
| | | <view class="approver-container"> |
| | | <view v-if="step.nickName" |
| | | class="approver-item"> |
| | | <view class="approver-avatar"> |
| | | <text class="avatar-text">{{ step.nickName.charAt(0) }}</text> |
| | | <view class="status-dot"></view> |
| | | </view> |
| | | <view class="approver-info"> |
| | | <text class="approver-name">{{ step.nickName }}</text> |
| | | </view> |
| | | <view class="delete-approver-btn" |
| | | @click="removeApprover(stepIndex)">Ã</view> |
| | | </view> |
| | | <view v-else |
| | | class="add-approver-btn" |
| | | @click="addApprover(stepIndex)"> |
| | | <view class="add-circle">+</view> |
| | | <text class="add-label">鿩审æ¹äºº</text> |
| | | </view> |
| | | </view> |
| | | <view class="step-line" |
| | | v-if="stepIndex < approverNodes.length - 1"></view> |
| | | <view class="delete-step-btn" |
| | | v-if="approverNodes.length > 1" |
| | | @click="removeApprovalStep(stepIndex)">å é¤èç¹</view> |
| | | </view> |
| | | </view> |
| | | <view class="add-step-btn"> |
| | | <u-button icon="plus" |
| | | plain |
| | | type="primary" |
| | | style="width: 100%" |
| | | @click="addApprovalStep">æ°å¢èç¹</u-button> |
| | | </view> |
| | | </view> |
| | | <template v-if="operationType !== 'detail'"> |
| | | <up-action-sheet :show="showPicker" |
| | | :actions="productOptions" |
| | | title="éæ©é¨é¨" |
| | | @select="onConfirm" |
| | | @close="showPicker = false" /> |
| | | <!-- æ¥æéæ©å¨ --> |
| | | <up-popup :show="showDate" |
| | | mode="bottom" |
| | | @close="showDate = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="currentDate" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDate = false" |
| | | mode="date" /> |
| | | </up-popup> |
| | | <!-- 请åå¼å§æ¶é´éæ©å¨ --> |
| | | <up-popup :show="showStartDate" |
| | | mode="bottom" |
| | | @close="showStartDate = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="startDateValue" |
| | | @confirm="onStartDateConfirm" |
| | | @cancel="showStartDate = false" |
| | | mode="date" /> |
| | | </up-popup> |
| | | <!-- 请åç»ææ¶é´éæ©å¨ --> |
| | | <up-popup :show="showEndDate" |
| | | mode="bottom" |
| | | @close="showEndDate = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="endDateValue" |
| | | @confirm="onEndDateConfirm" |
| | | @cancel="showEndDate = false" |
| | | mode="date" /> |
| | | </up-popup> |
| | | </template> |
| | | <!-- åºé¨æé® --> |
| | | <view class="footer-btns"> |
| | | <view class="footer-btns" |
| | | v-if="operationType !== 'detail'"> |
| | | <u-button class="cancel-btn" |
| | | @click="goBack">åæ¶</u-button> |
| | | <u-button class="save-btn" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue"; |
| | | import { |
| | | ref, |
| | | onMounted, |
| | | onUnmounted, |
| | | reactive, |
| | | toRefs, |
| | | computed, |
| | | watch, |
| | | } from "vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { |
| | |
| | | approveProcessGetInfo, |
| | | approveProcessAdd, |
| | | approveProcessUpdate, |
| | | getDeliveryDetailByShippingNo, |
| | | } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger"; |
| | | const showToast = message => { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: "none", |
| | | }); |
| | | }; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | | tempFileIds: [], |
| | | approverList: [], // æ°å¢å段ï¼å卿æèç¹ç审æ¹äººid |
| | | storageBlobDTOS: [], |
| | | startDate: "", |
| | | endDate: "", |
| | | location: "", |
| | |
| | | const productOptions = ref([]); |
| | | const operationType = ref(""); |
| | | const currentApproveStatus = ref(""); |
| | | const approverNodes = ref([]); |
| | | const userList = ref([]); |
| | | const formRef = ref(null); |
| | | const message = ref(""); |
| | | const showDate = ref(false); |
| | |
| | | const endDateValue = ref(Date.now()); |
| | | const userStore = useUserStore(); |
| | | const approveType = ref(0); |
| | | const isInitialLoading = ref(false); |
| | | |
| | | const quotationLoading = ref(false); |
| | | const currentQuotation = ref({}); |
| | | const purchaseLoading = ref(false); |
| | | const currentPurchase = ref({}); |
| | | const deliveryLoading = ref(false); |
| | | const currentDelivery = ref({}); |
| | | const deliveryProductList = ref([]); |
| | | |
| | | const isQuotationApproval = computed(() => Number(approveType.value) === 6); |
| | | const isPurchaseApproval = computed(() => Number(approveType.value) === 5); |
| | | const isDeliveryApproval = computed(() => Number(approveType.value) === 7); |
| | | |
| | | const getProductOptions = () => { |
| | | getDept().then(res => { |
| | |
| | | })); |
| | | }); |
| | | }; |
| | | const fileList = ref([]); |
| | | let nextApproverId = 2; |
| | | const getCurrentinfo = () => { |
| | | userStore.getInfo().then(res => { |
| | | form.value.approveDeptId = res.user.tenantId; |
| | | console.log(res.user.tenantId, "res.user.tenantId"); |
| | | }); |
| | | }; |
| | | |
| | | // æ¾ç¤ºæ¥æéæ©å¨ |
| | | const showDatePicker = () => { |
| | | showDate.value = true; |
| | | }; |
| | | |
| | | // ç¡®è®¤æ¥æéæ© |
| | | const onDateConfirm = e => { |
| | | form.value.approveTime = formatDateToYMD(e.value); |
| | | currentDate.value = formatDateToYMD(e.value); |
| | | showDate.value = false; |
| | | }; |
| | | |
| | | // æ¾ç¤ºè¯·åå¼å§æ¶é´éæ©å¨ |
| | | const showStartDatePicker = () => { |
| | | showStartDate.value = true; |
| | | }; |
| | | |
| | | // 确认请åå¼å§æ¶é´éæ© |
| | | const onStartDateConfirm = e => { |
| | | form.value.startDate = formatDateToYMD(e.value); |
| | | showStartDate.value = false; |
| | | }; |
| | | |
| | | const showEndDatePicker = () => { |
| | | showEndDate.value = true; |
| | | }; |
| | | |
| | | // 确认请åç»ææ¶é´éæ© |
| | | const onEndDateConfirm = e => { |
| | | form.value.endDate = formatDateToYMD(e.value); |
| | | showEndDate.value = false; |
| | | }; |
| | | |
| | | const fetchDetailData = async row => { |
| | | // æ¥ä»·å®¡æ¹ |
| | | if (isQuotationApproval.value) { |
| | | const quotationNo = row?.approveReason; |
| | | if (quotationNo) { |
| | | quotationLoading.value = true; |
| | | getQuotationList({ quotationNo }) |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | | currentQuotation.value = records[0] || {}; |
| | | }) |
| | | .finally(() => { |
| | | quotationLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // éè´å®¡æ¹ |
| | | if (isPurchaseApproval.value) { |
| | | const purchaseContractNumber = row?.approveReason; |
| | | if (purchaseContractNumber) { |
| | | purchaseLoading.value = true; |
| | | getPurchaseByCode({ purchaseContractNumber }) |
| | | .then(res => { |
| | | currentPurchase.value = res; |
| | | }) |
| | | .catch(err => { |
| | | console.error("æ¥è¯¢éè´è¯¦æ
失败:", err); |
| | | }) |
| | | .finally(() => { |
| | | purchaseLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // åè´§å®¡æ¹ |
| | | if (isDeliveryApproval.value) { |
| | | const deliveryNo = row?.approveReason; |
| | | if (deliveryNo) { |
| | | deliveryLoading.value = true; |
| | | currentDelivery.value = {}; |
| | | deliveryProductList.value = []; |
| | | getDeliveryDetailByShippingNo({ shippingNo: deliveryNo }) |
| | | .then(res => { |
| | | const detailData = res?.data || res || {}; |
| | | currentDelivery.value = detailData; |
| | | deliveryProductList.value = |
| | | detailData.shippingProductDetailDtoList || []; |
| | | }) |
| | | .catch(err => { |
| | | console.error("æ¥è¯¢å货详æ
失败:", err); |
| | | }) |
| | | .finally(() => { |
| | | deliveryLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // çå¬å®¡æ¹äºç±ååï¼å¦ææ¯ç¹å®å®¡æ¹ç±»ååå°è¯è·å详æ
|
| | | watch( |
| | | () => form.value.approveReason, |
| | | newVal => { |
| | | if (isInitialLoading.value) return; |
| | | if ( |
| | | newVal && |
| | | (isQuotationApproval.value || |
| | | isPurchaseApproval.value || |
| | | isDeliveryApproval.value) |
| | | ) { |
| | | // å»¶è¿ä¸ä¼å请æ±ï¼é¿å
è¾å
¥è¿ç¨ä¸é¢ç¹è§¦å |
| | | debounceFetchDetail(); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | let timer = null; |
| | | const debounceFetchDetail = () => { |
| | | if (timer) clearTimeout(timer); |
| | | timer = setTimeout(() => { |
| | | fetchDetailData(form.value); |
| | | }, 800); |
| | | }; |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | getProductOptions(); |
| | | userListNoPageByTenantId().then(res => { |
| | | userList.value = res.data; |
| | | }); |
| | | form.value.approveUser = userStore.id; |
| | | form.value.approveUserName = userStore.nickName; |
| | | form.value.approveTime = getCurrentDate(); |
| | |
| | | approveType.value = uni.getStorageSync("approveType") || 0; |
| | | |
| | | // 妿æ¯ç¼è¾æ¨¡å¼ï¼ä»æ¬å°åå¨è·åæ°æ® |
| | | if (operationType.value === "edit") { |
| | | if (operationType.value === "edit" || operationType.value === "detail") { |
| | | const storedData = uni.getStorageSync("invoiceLedgerEditRow"); |
| | | if (storedData) { |
| | | const row = JSON.parse(storedData); |
| | | fileList.value = row.commonFileList || []; |
| | | form.value.tempFileIds = fileList.value.map(file => file.id); |
| | | currentApproveStatus.value = row.approveStatus; |
| | | |
| | | approveProcessGetInfo({ id: row.approveId, approveReason: "1" }).then( |
| | | res => { |
| | | isInitialLoading.value = true; |
| | | approveProcessGetInfo({ id: row.approveId, approveReason: "1" }) |
| | | .then(res => { |
| | | form.value = { ...res.data }; |
| | | // 忾审æ¹äºº |
| | | if (res.data && res.data.approveUserIds) { |
| | | const userIds = res.data.approveUserIds.split(","); |
| | | approverNodes.value = userIds.map((userId, idx) => { |
| | | const userIdNum = parseInt(userId.trim()); |
| | | // ä»userList䏿¾å°å¯¹åºçç¨æ·ä¿¡æ¯ |
| | | const userInfo = userList.value.find( |
| | | user => user.userId === userIdNum |
| | | ); |
| | | return { |
| | | id: idx + 1, |
| | | userId: userIdNum, |
| | | nickName: userInfo ? userInfo.nickName : null, |
| | | }; |
| | | }); |
| | | nextApproverId = userIds.length + 1; |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ï¼åå§åä¸ä¸ªç©ºç审æ¹èç¹ |
| | | approverNodes.value = [{ id: 1, userId: null, nickName: null }]; |
| | | nextApproverId = 2; |
| | | // 设置å¾çå表æ¾ç¤º |
| | | const fileData = |
| | | res.data.storageBlobVOS || res.data.commonFileList || []; |
| | | if (fileData.length > 0) { |
| | | form.value.storageBlobDTOS = fileData; |
| | | } |
| | | } |
| | | ); |
| | | // è·åé¢å¤è¯¦æ
|
| | | fetchDetailData(res.data); |
| | | }) |
| | | .finally(() => { |
| | | // å»¶è¿ä¸ä¼éç½®ï¼ç¡®ä¿ watch ä¸ä¼è¢«è§¦å |
| | | setTimeout(() => { |
| | | isInitialLoading.value = false; |
| | | }, 100); |
| | | }); |
| | | } |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ï¼åå§åä¸ä¸ªç©ºç审æ¹èç¹ |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | } |
| | | |
| | | // çå¬èç³»äººéæ©äºä»¶ |
| | | uni.$on("selectContact", handleSelectContact); |
| | | } catch (error) { |
| | | console.error("è·åé¨é¨æ°æ®å¤±è´¥:", error); |
| | | console.error("è·åæ°æ®å¤±è´¥:", error); |
| | | } |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | | // ç§»é¤äºä»¶çå¬ |
| | | uni.$off("selectContact", handleSelectContact); |
| | | }); |
| | | onUnmounted(() => {}); |
| | | |
| | | const onConfirm = item => { |
| | | // 设置éä¸çé¨é¨ |
| | |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | // æ£æ¥æ¯ä¸ªå®¡æ¹æ¥éª¤æ¯å¦é½æå®¡æ¹äºº |
| | | const hasEmptyStep = approverNodes.value.some(step => !step.nickName); |
| | | if (hasEmptyStep) { |
| | | showToast("请为æ¯ä¸ªå®¡æ¹æ¥éª¤éæ©å®¡æ¹äºº"); |
| | | return; |
| | | } |
| | | |
| | | // æå¨æ£æ¥å¿
å¡«åæ®µï¼é²æ¢å æ°æ®ç±»åé®é¢å¯¼è´çæ ¡éªå¤±è´¥ |
| | | if (!form.value.approveReason || !form.value.approveReason.trim()) { |
| | | showToast("请è¾å
¥ç³è¯·äºç±"); |
| | |
| | | .then(valid => { |
| | | if (valid) { |
| | | // è¡¨åæ ¡éªéè¿ï¼å¯ä»¥æäº¤æ°æ® |
| | | // æ¶éææèç¹ç审æ¹äººid |
| | | console.log("approverNodes---", approverNodes.value); |
| | | form.value.approveUserIds = approverNodes.value |
| | | .map(node => node.userId) |
| | | .join(","); |
| | | form.value.approveType = approveType.value; |
| | | form.value.approveDeptId = Number(form.value.approveDeptId); |
| | | // const submitForm = { |
| | | // approveDeptId: form.value.approveDeptId, |
| | | // approveDeptName: form.value.approveDeptName, |
| | | // approveReason: form.value.approveReason, |
| | | // approveTime: form.value.approveTime, |
| | | // approveType: form.value.approveType, |
| | | // approveUser: form.value.approveUser, |
| | | // approveUserIds: form.value.approveUserIds, |
| | | // endDate: form.value.endDate, |
| | | // startDate: form.value.startDate, |
| | | // }; |
| | | // console.log("form.value---", form.value); |
| | | // console.log("submitForm", submitForm); |
| | | |
| | | if (operationType.value === "add" || currentApproveStatus.value == 3) { |
| | | approveProcessAdd(form.value).then(res => { |
| | |
| | | }); |
| | | }; |
| | | |
| | | // å¤çèç³»äººéæ©ç»æ |
| | | const handleSelectContact = data => { |
| | | const { stepIndex, contact } = data; |
| | | // å°éä¸çè系人设置为对åºå®¡æ¹æ¥éª¤ç审æ¹äºº |
| | | approverNodes.value[stepIndex].userId = contact.userId; |
| | | approverNodes.value[stepIndex].nickName = contact.nickName; |
| | | }; |
| | | |
| | | const addApprover = stepIndex => { |
| | | // 跳转å°èç³»äººéæ©é¡µé¢ |
| | | uni.setStorageSync("stepIndex", stepIndex); |
| | | uni.navigateTo({ |
| | | url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect", |
| | | }); |
| | | }; |
| | | |
| | | const addApprovalStep = () => { |
| | | // æ·»å æ°çå®¡æ¹æ¥éª¤ |
| | | approverNodes.value.push({ userId: null, nickName: null }); |
| | | }; |
| | | |
| | | const removeApprover = stepIndex => { |
| | | // ç§»é¤å®¡æ¹äºº |
| | | approverNodes.value[stepIndex].userId = null; |
| | | approverNodes.value[stepIndex].nickName = null; |
| | | }; |
| | | |
| | | const removeApprovalStep = stepIndex => { |
| | | // ç¡®ä¿è³å°ä¿çä¸ä¸ªå®¡æ¹æ¥éª¤ |
| | | if (approverNodes.value.length > 1) { |
| | | approverNodes.value.splice(stepIndex, 1); |
| | | } else { |
| | | uni.showToast({ |
| | | title: "è³å°éè¦ä¸ä¸ªå®¡æ¹æ¥éª¤", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | }; |
| | | // æ¾ç¤ºæ¥æéæ©å¨ |
| | | const showDatePicker = () => { |
| | | showDate.value = true; |
| | | }; |
| | | |
| | | // ç¡®è®¤æ¥æéæ© |
| | | const onDateConfirm = e => { |
| | | form.value.approveTime = formatDateToYMD(e.value); |
| | | currentDate.value = formatDateToYMD(e.value); |
| | | showDate.value = false; |
| | | }; |
| | | |
| | | // æ¾ç¤ºè¯·åå¼å§æ¶é´éæ©å¨ |
| | | const showStartDatePicker = () => { |
| | | showStartDate.value = true; |
| | | }; |
| | | |
| | | // 确认请åå¼å§æ¶é´éæ© |
| | | const onStartDateConfirm = e => { |
| | | form.value.startDate = formatDateToYMD(e.value); |
| | | showStartDate.value = false; |
| | | }; |
| | | |
| | | const showEndDatePicker = () => { |
| | | showEndDate.value = true; |
| | | }; |
| | | |
| | | // 确认请åç»ææ¶é´éæ© |
| | | const onEndDateConfirm = e => { |
| | | form.value.endDate = formatDateToYMD(e.value); |
| | | showEndDate.value = false; |
| | | }; |
| | | |
| | | // è·åå½åæ¥æå¹¶æ ¼å¼å为 YYYY-MM-DD |
| | | function getCurrentDate() { |
| | | const today = new Date(); |
| | |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .approval-process { |
| | | background: #fff; |
| | | margin: 16px; |
| | | border-radius: 16px; |
| | | padding: 16px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); |
| | | } |
| | | |
| | | .approval-header { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .approval-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | display: block; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .approval-desc { |
| | | font-size: 12px; |
| | | color: #999; |
| | | } |
| | | |
| | | /* æ ·å¼å¢å¼ºä¸ºâç®æ´å°åå飿 ¼â */ |
| | | .approval-steps { |
| | | padding-left: 22px; |
| | | position: relative; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 11px; |
| | | top: 40px; |
| | | bottom: 40px; |
| | | width: 2px; |
| | | background: linear-gradient( |
| | | to bottom, |
| | | #e6f7ff 0%, |
| | | #bae7ff 50%, |
| | | #91d5ff 100% |
| | | ); |
| | | border-radius: 1px; |
| | | } |
| | | } |
| | | |
| | | .approval-step { |
| | | position: relative; |
| | | margin-bottom: 24px; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: -18px; |
| | | top: 14px; // ä» 8px è°æ´ä¸º 14pxï¼ä¸æåä¸å¿å¯¹é½ |
| | | width: 12px; |
| | | height: 12px; |
| | | background: #fff; |
| | | border: 3px solid #006cfb; |
| | | border-radius: 50%; |
| | | z-index: 2; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | } |
| | | |
| | | .step-title { |
| | | top: 12px; |
| | | margin-bottom: 12px; |
| | | position: relative; |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | .step-title text { |
| | | font-size: 14px; |
| | | color: #666; |
| | | background: #f0f0f0; |
| | | padding: 4px 12px; |
| | | border-radius: 12px; |
| | | position: relative; |
| | | line-height: 1.4; // ç¡®ä¿æåè¡é«ä¸è´ |
| | | } |
| | | |
| | | .approver-item { |
| | | display: flex; |
| | | align-items: center; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); |
| | | border-radius: 16px; |
| | | padding: 16px; |
| | | gap: 12px; |
| | | position: relative; |
| | | border: 1px solid #e6f7ff; |
| | | box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .approver-avatar { |
| | | width: 48px; |
| | | height: 48px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | position: relative; |
| | | box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); |
| | | } |
| | | |
| | | .avatar-text { |
| | | color: #fff; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .approver-info { |
| | | flex: 1; |
| | | position: relative; |
| | | } |
| | | |
| | | .approver-name { |
| | | display: block; |
| | | font-size: 16px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | position: relative; |
| | | } |
| | | |
| | | .approver-dept { |
| | | font-size: 12px; |
| | | color: #999; |
| | | background: rgba(0, 108, 251, 0.05); |
| | | padding: 2px 8px; |
| | | border-radius: 8px; |
| | | display: inline-block; |
| | | position: relative; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 4px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 2px; |
| | | height: 2px; |
| | | background: #006cfb; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | |
| | | .delete-approver-btn { |
| | | font-size: 16px; |
| | | color: #ff4d4f; |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(255, 77, 79, 0.1) 0%, |
| | | rgba(255, 77, 79, 0.05) 100% |
| | | ); |
| | | width: 28px; |
| | | height: 28px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | transition: all 0.3s ease; |
| | | position: relative; |
| | | } |
| | | |
| | | .add-approver-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%); |
| | | border: 2px dashed #006cfb; |
| | | border-radius: 16px; |
| | | padding: 20px; |
| | | color: #006cfb; |
| | | font-size: 14px; |
| | | position: relative; |
| | | transition: all 0.3s ease; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 50%; |
| | | transform: translate(-50%, -50%); |
| | | width: 32px; |
| | | height: 32px; |
| | | border: 2px solid #006cfb; |
| | | border-radius: 50%; |
| | | opacity: 0; |
| | | transition: all 0.3s ease; |
| | | } |
| | | } |
| | | |
| | | .delete-step-btn { |
| | | color: #ff4d4f; |
| | | font-size: 12px; |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(255, 77, 79, 0.1) 0%, |
| | | rgba(255, 77, 79, 0.05) 100% |
| | | ); |
| | | padding: 6px 12px; |
| | | border-radius: 12px; |
| | | display: inline-block; |
| | | position: relative; |
| | | transition: all 0.3s ease; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 6px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #ff4d4f; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | |
| | | .step-line { |
| | | display: none; // éè忥ç线æ¡ï¼ä½¿ç¨ä¼ªå
ç´ ä»£æ¿ |
| | | } |
| | | |
| | | .add-step-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | .account-detail { |
| | | background-color: #fff; |
| | | } |
| | | .footer-btns { |
| | | position: fixed; |
| | |
| | | background: linear-gradient(140deg, #00baff 0%, #006cfb 100%); |
| | | box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2); |
| | | border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; |
| | | } |
| | | |
| | | // å¨ç»å®ä¹ |
| | | @keyframes pulse { |
| | | 0% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | } |
| | | 50% { |
| | | transform: scale(1.2); |
| | | opacity: 0.7; |
| | | } |
| | | 100% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | } |
| | | } |
| | | |
| | | @keyframes rotate { |
| | | 0% { |
| | | transform: rotate(0deg); |
| | | } |
| | | 100% { |
| | | transform: rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | @keyframes ripple { |
| | | 0% { |
| | | transform: translate(-50%, -50%) scale(0.8); |
| | | opacity: 1; |
| | | } |
| | | 100% { |
| | | transform: translate(-50%, -50%) scale(1.6); |
| | | opacity: 0; |
| | | } |
| | | } |
| | | |
| | | /* 妿已æ .step-lineï¼è¿éæ´ç²¾åå®ä½å°å·¦ä¾§ä¸å°åç¹å¯¹é½ */ |
| | | .step-line { |
| | | position: absolute; |
| | | left: 4px; |
| | | top: 48px; |
| | | width: 2px; |
| | | height: calc(100% - 48px); |
| | | background: #e5e7eb; |
| | | } |
| | | |
| | | .approver-container { |
| | | display: flex; |
| | | align-items: center; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); |
| | | border-radius: 16px; |
| | | gap: 12px; |
| | | padding: 10px 0; |
| | | background: transparent; |
| | | border: none; |
| | | box-shadow: none; |
| | | } |
| | | |
| | | .approver-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 8px 10px; |
| | | background: transparent; |
| | | border: none; |
| | | box-shadow: none; |
| | | border-radius: 0; |
| | | } |
| | | |
| | | .approver-avatar { |
| | | position: relative; |
| | | width: 40px; |
| | | height: 40px; |
| | | border-radius: 50%; |
| | | background: #f3f4f6; |
| | | border: 2px solid #e5e7eb; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | animation: none; /* ç¦ç¨æè½¬çå¨ç»ï¼åå½ç®æ´ */ |
| | | } |
| | | |
| | | .avatar-text { |
| | | font-size: 14px; |
| | | color: #374151; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .add-approver-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | background: transparent; |
| | | border: none; |
| | | box-shadow: none; |
| | | padding: 0; |
| | | } |
| | | |
| | | .add-approver-btn .add-circle { |
| | | width: 40px; |
| | | height: 40px; |
| | | border: 2px dashed #a0aec0; |
| | | border-radius: 50%; |
| | | color: #6b7280; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 22px; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .add-approver-btn .add-label { |
| | | color: #3b82f6; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | |
| | | </view> |
| | | <view class="detail-row"> |
| | | <view class="actions"> |
| | | <!-- <u-button type="primary" |
| | | <u-button type="primary" |
| | | size="small" |
| | | class="action-btn edit" |
| | | :disabled="item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4 || item.approveStatus == 8" |
| | | v-if="!(item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4 || item.approveStatus == 8 || item.approveType == 5 || item.approveType == 6 || item.approveType == 7)" |
| | | @click="handleItemClick(item)"> |
| | | ç¼è¾ |
| | | </u-button> --> |
| | | </u-button> |
| | | <u-button type="info" |
| | | v-if="item.approveType == 5 || item.approveType == 6 || item.approveType == 7" |
| | | size="small" |
| | | class="action-btn detail" |
| | | @click="handleDetailClick(item)"> |
| | | 详æ
|
| | | </u-button> |
| | | <u-button type="success" |
| | | size="small" |
| | | class="action-btn approve" |
| | |
| | | <text>ææ å®¡æ¹æ°æ®</text> |
| | | </view> |
| | | <!-- æµ®å¨æä½æé® --> |
| | | <!-- <view class="fab-button" |
| | | <view class="fab-button" |
| | | v-if="props.approveType != 5 && props.approveType != 6 && props.approveType != 7" |
| | | @click="handleAdd"> |
| | | <up-icon name="plus" |
| | | size="24" |
| | | color="#ffffff"></up-icon> |
| | | </view> --> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | }); |
| | | }; |
| | | |
| | | // æ¥ç详æ
|
| | | const handleDetailClick = item => { |
| | | uni.setStorageSync("invoiceLedgerEditRow", JSON.stringify(item)); |
| | | uni.setStorageSync("operationType", "detail"); |
| | | uni.setStorageSync("approveId", item.approveId); |
| | | uni.setStorageSync("approveType", props.approveType); |
| | | uni.navigateTo({ |
| | | url: "/pages/cooperativeOffice/collaborativeApproval/detail", |
| | | }); |
| | | }; |
| | | |
| | | // æ·»å æ°è®°å½ |
| | | const handleAdd = () => { |
| | | uni.setStorageSync("operationType", "add"); |
| | |
| | | placeholder="请è¾å
¥æ¥ä¿®äºº" |
| | | clearable /> |
| | | </u-form-item> |
| | | <u-form-item label="维修人" |
| | | prop="maintenanceName" |
| | | border-bottom> |
| | | <u-input v-model="form.maintenanceName" |
| | | placeholder="请è¾å
¥ç»´ä¿®äºº" |
| | | clearable /> |
| | | </u-form-item> |
| | | <u-form-item label="维修项ç®" |
| | | prop="machineryCategory" |
| | | border-bottom> |
| | | <u-input v-model="form.machineryCategory" |
| | | placeholder="请è¾å
¥ç»´ä¿®é¡¹ç®" |
| | | clearable /> |
| | | </u-form-item> |
| | | <u-form-item label="æ
éç°è±¡" |
| | | prop="remark" |
| | | required |
| | |
| | | clearable |
| | | count |
| | | maxlength="200" /> |
| | | </u-form-item> |
| | | <u-form-item label="å¾çéä»¶" |
| | | prop="storageBlobDTOs" |
| | | border-bottom> |
| | | <CommonUpload v-model="form.storageBlobDTOs" /> |
| | | </u-form-item> |
| | | </u-cell-group> |
| | | <!-- æäº¤æé® --> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted, onUnmounted } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { onShow, onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import { getDeviceLedger } from "@/api/equipmentManagement/ledger"; |
| | | import { |
| | | addRepair, |
| | |
| | | // 表åå¼ç¨ |
| | | const formRef = ref(null); |
| | | const operationType = ref("add"); |
| | | const repairId = ref(""); |
| | | const loading = ref(false); |
| | | const showDevice = ref(false); |
| | | const showDate = ref(false); |
| | | const pickerDateValue = ref(Date.now()); |
| | | |
| | | onLoad(options => { |
| | | if (options.id) { |
| | | repairId.value = options.id; |
| | | } |
| | | getPageParams(); |
| | | }); |
| | | |
| | | // 设å¤é项 |
| | | const deviceOptions = ref([]); |
| | |
| | | deviceModel: undefined, // è§æ ¼åå· |
| | | repairTime: dayjs().format("YYYY-MM-DD"), // æ¥ä¿®æ¥æ |
| | | repairName: undefined, // æ¥ä¿®äºº |
| | | maintenanceName: undefined, // 维修人 |
| | | machineryCategory: undefined, // ç»´ä¿®é¡¹ç® |
| | | remark: undefined, // æ
éç°è±¡ |
| | | storageBlobDTOs: [], // å¾çéä»¶ |
| | | }); |
| | | |
| | | // æ¥ä¿®ç¶æé项 |
| | |
| | | form.value.deviceModel = data.deviceModel; |
| | | form.value.repairTime = dayjs(data.repairTime).format("YYYY-MM-DD"); |
| | | form.value.repairName = data.repairName; |
| | | form.value.maintenanceName = data.maintenanceName; |
| | | form.value.machineryCategory = data.machineryCategory; |
| | | form.value.remark = data.remark; |
| | | form.value.storageBlobDTOs = data.storageBlobVOs || []; |
| | | repairStatusText.value = |
| | | repairStatusOptions.value.find(item => item.value == data.status) |
| | | ?.name || ""; |
| | |
| | | }; |
| | | |
| | | onShow(() => { |
| | | // 页颿¾ç¤ºæ¶è·ååæ° |
| | | getPageParams(); |
| | | // 页颿¾ç¤ºæ¶é»è¾ |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | // 页é¢å è½½æ¶è·å设å¤å表ååæ° |
| | | // 页é¢å è½½æ¶è·å设å¤å表 |
| | | loadDeviceName(); |
| | | getPageParams(); |
| | | }); |
| | | |
| | | // ç»ä»¶å¸è½½æ¶æ¸
ç宿¶å¨ |
| | |
| | | |
| | | // åå¤æäº¤æ°æ® |
| | | const submitData = { ...form.value }; |
| | | |
| | | const { code } = id |
| | | ? await editRepair({ id: id, ...submitData }) |
| | | : await addRepair(submitData); |
| | |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.removeStorageSync("repairId"); |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // è·å页é¢åæ° |
| | | const getPageParams = () => { |
| | | // 使ç¨uni.getStorageSyncè·åid |
| | | const id = uni.getStorageSync("repairId"); |
| | | |
| | | // æ ¹æ®æ¯å¦æidåæ°æ¥å¤ææ¯æ°å¢è¿æ¯ç¼è¾ |
| | | if (id) { |
| | | if (repairId.value) { |
| | | // ç¼è¾æ¨¡å¼ï¼è·å详æ
|
| | | loadForm(id); |
| | | // å¯éï¼è·å忏
é¤åå¨çidï¼é¿å
å½±ååç»æä½ |
| | | uni.removeStorageSync("repairId"); |
| | | loadForm(repairId.value); |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | loadForm(); |
| | |
| | | |
| | | // è·å页é¢ID |
| | | const getPageId = () => { |
| | | // 使ç¨uni.getStorageSyncè·åid |
| | | const id = uni.getStorageSync("repairId"); |
| | | return id; |
| | | return repairId.value; |
| | | }; |
| | | </script> |
| | | |
| | |
| | | <text class="detail-value">{{ item.repairName || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ
éç°è±¡</text> |
| | | <text class="detail-value">{{ item.remark || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">维修人</text> |
| | | <text class="detail-value">{{ item.maintenanceName || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">维修项ç®</text> |
| | | <text class="detail-value">{{ item.machineryCategory || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ
éç°è±¡</text> |
| | | <text class="detail-value">{{ item.remark || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç»´ä¿®ç»æ</text> |
| | |
| | | const edit = id => { |
| | | if (!id) return; |
| | | // 使ç¨uni.setStorageSyncåå¨id |
| | | uni.setStorageSync("repairId", id); |
| | | // uni.setStorageSync("repairId", id); |
| | | uni.navigateTo({ |
| | | url: "/pages/equipmentManagement/repair/add", |
| | | url: "/pages/equipmentManagement/repair/add?id=" + id, |
| | | }); |
| | | }; |
| | | |
| | |
| | | <template> |
| | | <view class="upkeep-add"> |
| | | <!-- 使ç¨éç¨é¡µé¢å¤´é¨ç»ä»¶ --> |
| | | <PageHeader :title="operationType === 'edit' ? 'ç¼è¾ä¿å
»è®¡å' : 'æ°å¢ä¿å
»è®¡å'" @back="goBack" /> |
| | | |
| | | <!-- 表åå
容 --> |
| | | <u-form ref="formRef" :model="form" :rules="formRules" label-width="110px"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <u-form-item label="设å¤åç§°" prop="deviceNameText" required border-bottom> |
| | | <u-input |
| | | v-model="form.deviceNameText" |
| | | placeholder="è¯·éæ©è®¾å¤åç§°" |
| | | readonly |
| | | @click="showDevicePicker" |
| | | clearable |
| | | /> |
| | | <template #right> |
| | | <u-icon name="scan" @click="startScan" class="scan-icon" /> |
| | | </template> |
| | | </u-form-item> |
| | | |
| | | <u-form-item label="è§æ ¼åå·" prop="deviceModel" border-bottom> |
| | | <u-input |
| | | v-model="form.deviceModel" |
| | | placeholder="请è¾å
¥è§æ ¼åå·" |
| | | readonly |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | |
| | | <u-form-item label="计åä¿å
»æ¥æ" prop="maintenancePlanTime" required border-bottom> |
| | | <u-input |
| | | v-model="form.maintenancePlanTime" |
| | | placeholder="è¯·éæ©è®¡åä¿å
»æ¥æ" |
| | | readonly |
| | | @click="showDatePicker" |
| | | clearable |
| | | /> |
| | | <template #right> |
| | | <u-icon name="arrow-right" @click="showDatePicker" /> |
| | | </template> |
| | | </u-form-item> |
| | | |
| | | <!-- æäº¤æé® --> |
| | | <view class="footer-btns"> |
| | | <u-button class="cancel-btn" @click="goBack">åæ¶</u-button> |
| | | <u-button class="save-btn" @click="sendForm" :loading="loading">ä¿å</u-button> |
| | | </view> |
| | | </u-form> |
| | | |
| | | <!-- 设å¤éæ©å¨ --> |
| | | <up-action-sheet |
| | | :show="showDevice" |
| | | :actions="deviceActions" |
| | | title="éæ©è®¾å¤" |
| | | @select="onDeviceConfirm" |
| | | @close="showDevice = false" |
| | | /> |
| | | <up-datetime-picker |
| | | :show="showDate" |
| | | v-model="pickerDateValue" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDate = false" |
| | | mode="date" |
| | | /> |
| | | |
| | | </view> |
| | | <view class="upkeep-add"> |
| | | <!-- 使ç¨éç¨é¡µé¢å¤´é¨ç»ä»¶ --> |
| | | <PageHeader :title="operationType === 'edit' ? 'ç¼è¾ä¿å
»è®¡å' : 'æ°å¢ä¿å
»è®¡å'" |
| | | @back="goBack" /> |
| | | <!-- 表åå
容 --> |
| | | <u-form ref="formRef" |
| | | :model="form" |
| | | :rules="formRules" |
| | | label-width="110px"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <u-form-item label="设å¤åç§°" |
| | | prop="deviceNameText" |
| | | required |
| | | border-bottom> |
| | | <u-input v-model="form.deviceNameText" |
| | | placeholder="è¯·éæ©è®¾å¤åç§°" |
| | | readonly |
| | | @click="showDevicePicker" |
| | | clearable /> |
| | | <template #right> |
| | | <u-icon name="scan" |
| | | @click="startScan" |
| | | class="scan-icon" /> |
| | | </template> |
| | | </u-form-item> |
| | | <u-form-item label="è§æ ¼åå·" |
| | | prop="deviceModel" |
| | | border-bottom> |
| | | <u-input v-model="form.deviceModel" |
| | | placeholder="请è¾å
¥è§æ ¼åå·" |
| | | readonly |
| | | clearable /> |
| | | </u-form-item> |
| | | <u-form-item label="计åä¿å
»æ¥æ" |
| | | prop="maintenancePlanTime" |
| | | required |
| | | border-bottom> |
| | | <u-input v-model="form.maintenancePlanTime" |
| | | placeholder="è¯·éæ©è®¡åä¿å
»æ¥æ" |
| | | readonly |
| | | @click="showDatePicker" |
| | | clearable /> |
| | | <template #right> |
| | | <u-icon name="arrow-right" |
| | | @click="showDatePicker" /> |
| | | </template> |
| | | </u-form-item> |
| | | <u-form-item label="ä¿å
»äºº" |
| | | prop="maintenancePerson" |
| | | border-bottom> |
| | | <u-input v-model="form.maintenancePerson" |
| | | placeholder="请è¾å
¥ä¿å
»äºº" |
| | | clearable /> |
| | | </u-form-item> |
| | | <u-form-item label="ä¿å
»é¡¹ç®" |
| | | prop="machineryCategory" |
| | | border-bottom> |
| | | <u-input v-model="form.machineryCategory" |
| | | placeholder="请è¾å
¥ä¿å
»é¡¹ç®" |
| | | clearable /> |
| | | </u-form-item> |
| | | <u-form-item label="éä»¶å¾ç" |
| | | prop="storageBlobDTOs" |
| | | border-bottom> |
| | | <CommonUpload v-model="form.storageBlobDTOs" /> |
| | | </u-form-item> |
| | | <!-- æäº¤æé® --> |
| | | <view class="footer-btns"> |
| | | <u-button class="cancel-btn" |
| | | @click="goBack">åæ¶</u-button> |
| | | <u-button class="save-btn" |
| | | @click="sendForm" |
| | | :loading="loading">ä¿å</u-button> |
| | | </view> |
| | | </u-form> |
| | | <!-- 设å¤éæ©å¨ --> |
| | | <up-action-sheet :show="showDevice" |
| | | :actions="deviceActions" |
| | | title="éæ©è®¾å¤" |
| | | @select="onDeviceConfirm" |
| | | @close="showDevice = false" /> |
| | | <up-datetime-picker :show="showDate" |
| | | v-model="pickerDateValue" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDate = false" |
| | | mode="date" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted, onUnmounted } from 'vue'; |
| | | import { onShow } from '@dcloudio/uni-app'; |
| | | import PageHeader from '@/components/PageHeader.vue'; |
| | | import { getDeviceLedger } from '@/api/equipmentManagement/ledger'; |
| | | import { addUpkeep, editUpkeep, getUpkeepById } from '@/api/equipmentManagement/upkeep'; |
| | | import dayjs from "dayjs"; |
| | | import { formatDateToYMD } from '@/utils/ruoyi'; |
| | | import { ref, computed, onMounted, onUnmounted } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import { getDeviceLedger } from "@/api/equipmentManagement/ledger"; |
| | | import { |
| | | addUpkeep, |
| | | editUpkeep, |
| | | getUpkeepById, |
| | | } from "@/api/equipmentManagement/upkeep"; |
| | | import dayjs from "dayjs"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | |
| | | defineOptions({ |
| | | name: "设å¤ä¿å
»è®¡å表å", |
| | | }); |
| | | const showToast = (message) => { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: 'none' |
| | | }) |
| | | } |
| | | defineOptions({ |
| | | name: "设å¤ä¿å
»è®¡å表å", |
| | | }); |
| | | const showToast = message => { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: "none", |
| | | }); |
| | | }; |
| | | |
| | | // 表åå¼ç¨ |
| | | const formRef = ref(null); |
| | | const operationType = ref('add'); |
| | | const loading = ref(false); |
| | | const showDevice = ref(false); |
| | | const showDate = ref(false); |
| | | const pickerDateValue = ref(Date.now()); |
| | | const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]); |
| | | // 表åå¼ç¨ |
| | | const formRef = ref(null); |
| | | const operationType = ref("add"); |
| | | const loading = ref(false); |
| | | const showDevice = ref(false); |
| | | const showDate = ref(false); |
| | | const pickerDateValue = ref(Date.now()); |
| | | const currentDate = ref([ |
| | | new Date().getFullYear(), |
| | | new Date().getMonth() + 1, |
| | | new Date().getDate(), |
| | | ]); |
| | | |
| | | // 设å¤é项 |
| | | const deviceOptions = ref([]); |
| | | const deviceNameText = ref(''); |
| | | // 转æ¢ä¸º action-sheet éè¦çæ ¼å¼ |
| | | const deviceActions = computed(() => { |
| | | return deviceOptions.value.map(item => ({ |
| | | text: item.deviceName, |
| | | value: item.id, |
| | | data: item |
| | | })); |
| | | }); |
| | | // 设å¤é项 |
| | | const deviceOptions = ref([]); |
| | | const deviceNameText = ref(""); |
| | | // 转æ¢ä¸º action-sheet éè¦çæ ¼å¼ |
| | | const deviceActions = computed(() => { |
| | | return deviceOptions.value.map(item => ({ |
| | | text: item.deviceName, |
| | | value: item.id, |
| | | data: item, |
| | | })); |
| | | }); |
| | | |
| | | // æ«ç ç¸å
³ç¶æ |
| | | const isScanning = ref(false); |
| | | const scanTimer = ref(null); |
| | | // æ«ç ç¸å
³ç¶æ |
| | | const isScanning = ref(false); |
| | | const scanTimer = ref(null); |
| | | |
| | | // 表åéªè¯è§å |
| | | const formRules = { |
| | | deviceLedgerId: [{ required: true, trigger: "change", message: "è¯·éæ©è®¾å¤åç§°" }], |
| | | maintenancePlanTime: [{ required: true, trigger: "change", message: "è¯·éæ©è®¡åä¿å
»æ¥æ" }], |
| | | }; |
| | | // 表åéªè¯è§å |
| | | const formRules = { |
| | | deviceLedgerId: [ |
| | | { required: true, trigger: "change", message: "è¯·éæ©è®¾å¤åç§°" }, |
| | | ], |
| | | maintenancePlanTime: [ |
| | | { required: true, trigger: "change", message: "è¯·éæ©è®¡åä¿å
»æ¥æ" }, |
| | | ], |
| | | }; |
| | | |
| | | // ä½¿ç¨ ref 声æè¡¨åæ°æ® |
| | | const form = ref({ |
| | | deviceLedgerId: undefined, // 设å¤ID |
| | | deviceModel: undefined, // è§æ ¼åå· |
| | | maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // 计åä¿å
»æ¥æ |
| | | }); |
| | | // ä½¿ç¨ ref 声æè¡¨åæ°æ® |
| | | const form = ref({ |
| | | deviceLedgerId: undefined, // 设å¤ID |
| | | deviceModel: undefined, // è§æ ¼åå· |
| | | maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // 计åä¿å
»æ¥æ |
| | | maintenancePerson: undefined, // ä¿å
»äºº |
| | | machineryCategory: undefined, // ä¿å
»é¡¹ç® |
| | | storageBlobDTOs: [], // éä»¶å¾ç |
| | | }); |
| | | |
| | | // å 载设å¤å表 |
| | | const loadDeviceName = async () => { |
| | | try { |
| | | const { data } = await getDeviceLedger(); |
| | | deviceOptions.value = data || []; |
| | | } catch (e) { |
| | | showToast('è·å设å¤å表失败'); |
| | | } |
| | | }; |
| | | // å 载设å¤å表 |
| | | const loadDeviceName = async () => { |
| | | try { |
| | | const { data } = await getDeviceLedger(); |
| | | deviceOptions.value = data || []; |
| | | } catch (e) { |
| | | showToast("è·å设å¤å表失败"); |
| | | } |
| | | }; |
| | | |
| | | // å è½½è¡¨åæ°æ®ï¼ç¼è¾æ¨¡å¼ï¼ |
| | | const loadForm = async (id) => { |
| | | if (id) { |
| | | operationType.value = 'edit'; |
| | | try { |
| | | const { code, data } = await getUpkeepById(id); |
| | | if (code == 200) { |
| | | form.value.deviceLedgerId = data.deviceLedgerId; |
| | | form.value.deviceModel = data.deviceModel; |
| | | form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD"); |
| | | // 设置设å¤åç§°æ¾ç¤º |
| | | const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId); |
| | | if (device) { |
| | | form.value.deviceNameText = device.deviceName; |
| | | } |
| | | } |
| | | } catch (e) { |
| | | showToast('è·å详æ
失败'); |
| | | } |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | operationType.value = 'add'; |
| | | } |
| | | }; |
| | | // å è½½è¡¨åæ°æ®ï¼ç¼è¾æ¨¡å¼ï¼ |
| | | const loadForm = async id => { |
| | | if (id) { |
| | | operationType.value = "edit"; |
| | | try { |
| | | const { code, data } = await getUpkeepById(id); |
| | | if (code == 200) { |
| | | form.value.deviceLedgerId = data.deviceLedgerId; |
| | | form.value.deviceModel = data.deviceModel; |
| | | form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format( |
| | | "YYYY-MM-DD" |
| | | ); |
| | | form.value.maintenancePerson = data.maintenancePerson; |
| | | form.value.machineryCategory = data.machineryCategory; |
| | | form.value.storageBlobDTOs = data.storageBlobVOs || []; |
| | | // 设置设å¤åç§°æ¾ç¤º |
| | | const device = deviceOptions.value.find( |
| | | item => item.id === data.deviceLedgerId |
| | | ); |
| | | if (device) { |
| | | form.value.deviceNameText = device.deviceName; |
| | | } |
| | | } |
| | | } catch (e) { |
| | | showToast("è·å详æ
失败"); |
| | | } |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | operationType.value = "add"; |
| | | } |
| | | }; |
| | | |
| | | // æ«æäºç»´ç åè½ |
| | | const startScan = () => { |
| | | if (isScanning.value) { |
| | | showToast('æ£å¨æ«æä¸ï¼è¯·ç¨å...'); |
| | | return; |
| | | } |
| | | |
| | | // è°ç¨uni-appçæ«ç API |
| | | uni.scanCode({ |
| | | scanType: ['qrCode', 'barCode'], |
| | | success: (res) => { |
| | | handleScanResult(res.result); |
| | | }, |
| | | fail: (err) => { |
| | | console.error('æ«ç 失败:', err); |
| | | showToast('æ«ç 失败ï¼è¯·éè¯'); |
| | | } |
| | | }); |
| | | }; |
| | | // æ«æäºç»´ç åè½ |
| | | const startScan = () => { |
| | | if (isScanning.value) { |
| | | showToast("æ£å¨æ«æä¸ï¼è¯·ç¨å..."); |
| | | return; |
| | | } |
| | | |
| | | // å¤çæ«ç ç»æ |
| | | const handleScanResult = (scanResult) => { |
| | | if (!scanResult) { |
| | | showToast('æ«ç ç»æä¸ºç©º'); |
| | | return; |
| | | } |
| | | |
| | | isScanning.value = true; |
| | | showToast('æ«ç æå'); |
| | | |
| | | // 3ç§åå¤çæ«ç ç»æ |
| | | scanTimer.value = setTimeout(() => { |
| | | processScanResult(scanResult); |
| | | isScanning.value = false; |
| | | }, 1000); |
| | | }; |
| | | function getDeviceIdByRegExp(url) { |
| | | // å¹é
deviceId=åé¢çæ°å |
| | | const reg = /deviceId=(\d+)/; |
| | | const match = url.match(reg); |
| | | // 妿å¹é
å°ç»æï¼è¿åæ°åç±»åï¼å¦åè¿ånull |
| | | return match ? Number(match[1]) : null; |
| | | } |
| | | // å¤çæ«ç ç»æå¹¶å¹é
è®¾å¤ |
| | | const processScanResult = (scanResult) => { |
| | | const deviceId = getDeviceIdByRegExp(scanResult); |
| | | const matchedDevice = deviceOptions.value.find(item => item.id == deviceId); |
| | | |
| | | if (matchedDevice) { |
| | | // æ¾å°å¹é
ç设å¤ï¼èªå¨å¡«å
|
| | | form.value.deviceLedgerId = matchedDevice.id; |
| | | form.value.deviceNameText = matchedDevice.deviceName; |
| | | form.value.deviceModel = matchedDevice.deviceModel; |
| | | showToast('设å¤ä¿¡æ¯å·²èªå¨å¡«å
'); |
| | | } else { |
| | | // æªæ¾å°å¹é
çè®¾å¤ |
| | | showToast('æªæ¾å°å¹é
ç设å¤ï¼è¯·æå¨éæ©'); |
| | | } |
| | | }; |
| | | // è°ç¨uni-appçæ«ç API |
| | | uni.scanCode({ |
| | | scanType: ["qrCode", "barCode"], |
| | | success: res => { |
| | | handleScanResult(res.result); |
| | | }, |
| | | fail: err => { |
| | | console.error("æ«ç 失败:", err); |
| | | showToast("æ«ç 失败ï¼è¯·éè¯"); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // æ¾ç¤ºè®¾å¤éæ©å¨ |
| | | const showDevicePicker = () => { |
| | | showDevice.value = true; |
| | | }; |
| | | // å¤çæ«ç ç»æ |
| | | const handleScanResult = scanResult => { |
| | | if (!scanResult) { |
| | | showToast("æ«ç ç»æä¸ºç©º"); |
| | | return; |
| | | } |
| | | |
| | | // 确认设å¤éæ© |
| | | const onDeviceConfirm = (selected) => { |
| | | // selected è¿åçæ¯éä¸é¡¹ |
| | | form.value.deviceLedgerId = selected.value; |
| | | form.value.deviceNameText = selected.name; |
| | | const selectedDevice = deviceOptions.value.find(item => item.id === selected.value); |
| | | if (selectedDevice) { |
| | | form.value.deviceModel = selectedDevice.deviceModel; |
| | | } |
| | | showDevice.value = false; |
| | | }; |
| | | isScanning.value = true; |
| | | showToast("æ«ç æå"); |
| | | |
| | | // æ¾ç¤ºæ¥æéæ©å¨ |
| | | const showDatePicker = () => { |
| | | showDate.value = true; |
| | | }; |
| | | // 3ç§åå¤çæ«ç ç»æ |
| | | scanTimer.value = setTimeout(() => { |
| | | processScanResult(scanResult); |
| | | isScanning.value = false; |
| | | }, 1000); |
| | | }; |
| | | function getDeviceIdByRegExp(url) { |
| | | // å¹é
deviceId=åé¢çæ°å |
| | | const reg = /deviceId=(\d+)/; |
| | | const match = url.match(reg); |
| | | // 妿å¹é
å°ç»æï¼è¿åæ°åç±»åï¼å¦åè¿ånull |
| | | return match ? Number(match[1]) : null; |
| | | } |
| | | // å¤çæ«ç ç»æå¹¶å¹é
è®¾å¤ |
| | | const processScanResult = scanResult => { |
| | | const deviceId = getDeviceIdByRegExp(scanResult); |
| | | const matchedDevice = deviceOptions.value.find(item => item.id == deviceId); |
| | | |
| | | // ç¡®è®¤æ¥æéæ© |
| | | const onDateConfirm = (e) => { |
| | | form.value.maintenancePlanTime = formatDateToYMD(e.value); |
| | | showDate.value = false; |
| | | }; |
| | | if (matchedDevice) { |
| | | // æ¾å°å¹é
ç设å¤ï¼èªå¨å¡«å
|
| | | form.value.deviceLedgerId = matchedDevice.id; |
| | | form.value.deviceNameText = matchedDevice.deviceName; |
| | | form.value.deviceModel = matchedDevice.deviceModel; |
| | | showToast("设å¤ä¿¡æ¯å·²èªå¨å¡«å
"); |
| | | } else { |
| | | // æªæ¾å°å¹é
çè®¾å¤ |
| | | showToast("æªæ¾å°å¹é
ç设å¤ï¼è¯·æå¨éæ©"); |
| | | } |
| | | }; |
| | | |
| | | onShow(() => { |
| | | // 页颿¾ç¤ºæ¶è·ååæ° |
| | | getPageParams(); |
| | | }); |
| | | // æ¾ç¤ºè®¾å¤éæ©å¨ |
| | | const showDevicePicker = () => { |
| | | showDevice.value = true; |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | // 页é¢å è½½æ¶è·å设å¤å表ååæ° |
| | | loadDeviceName(); |
| | | getPageParams(); |
| | | }); |
| | | // 确认设å¤éæ© |
| | | const onDeviceConfirm = selected => { |
| | | // selected è¿åçæ¯éä¸é¡¹ |
| | | form.value.deviceLedgerId = selected.value; |
| | | form.value.deviceNameText = selected.name; |
| | | const selectedDevice = deviceOptions.value.find( |
| | | item => item.id === selected.value |
| | | ); |
| | | if (selectedDevice) { |
| | | form.value.deviceModel = selectedDevice.deviceModel; |
| | | } |
| | | showDevice.value = false; |
| | | }; |
| | | |
| | | // ç»ä»¶å¸è½½æ¶æ¸
ç宿¶å¨ |
| | | onUnmounted(() => { |
| | | if (scanTimer.value) { |
| | | clearTimeout(scanTimer.value); |
| | | } |
| | | }); |
| | | // æ¾ç¤ºæ¥æéæ©å¨ |
| | | const showDatePicker = () => { |
| | | showDate.value = true; |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const sendForm = async () => { |
| | | try { |
| | | // æå¨éªè¯è¡¨å |
| | | const valid = await formRef.value.validate(); |
| | | if (!valid) return; |
| | | |
| | | loading.value = true; |
| | | const id = getPageId(); |
| | | |
| | | // åå¤æäº¤æ°æ® |
| | | const submitData = { ...form.value }; |
| | | // ç¡®ä¿æ¥ææ ¼å¼æ£ç¡® |
| | | if (submitData.maintenancePlanTime && !submitData.maintenancePlanTime.includes(':')) { |
| | | submitData.maintenancePlanTime = submitData.maintenancePlanTime + ' 00:00:00'; |
| | | } |
| | | |
| | | const { code } = id |
| | | ? await editUpkeep({ id: id, ...submitData }) |
| | | : await addUpkeep(submitData); |
| | | |
| | | if (code == 200) { |
| | | showToast(`${id ? "ç¼è¾" : "æ°å¢"}计åæå`); |
| | | setTimeout(() => { |
| | | uni.navigateBack(); |
| | | }, 1500); |
| | | } else { |
| | | loading.value = false; |
| | | } |
| | | } catch (e) { |
| | | loading.value = false; |
| | | showToast('表åéªè¯å¤±è´¥'); |
| | | } |
| | | }; |
| | | // ç¡®è®¤æ¥æéæ© |
| | | const onDateConfirm = e => { |
| | | form.value.maintenancePlanTime = formatDateToYMD(e.value); |
| | | showDate.value = false; |
| | | }; |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | // æ¸
é¤åå¨çid |
| | | uni.removeStorageSync('repairId'); |
| | | uni.navigateBack(); |
| | | }; |
| | | onShow(() => { |
| | | // 页颿¾ç¤ºæ¶è·ååæ° |
| | | getPageParams(); |
| | | }); |
| | | |
| | | // è·å页é¢åæ° |
| | | const getPageParams = () => { |
| | | // 仿¬å°åå¨è·åid |
| | | const id = uni.getStorageSync('repairId'); |
| | | |
| | | // æ ¹æ®æ¯å¦æidåæ°æ¥å¤ææ¯æ°å¢è¿æ¯ç¼è¾ |
| | | if (id) { |
| | | // ç¼è¾æ¨¡å¼ï¼è·å详æ
|
| | | loadForm(id); |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | loadForm(); |
| | | } |
| | | }; |
| | | onMounted(() => { |
| | | // 页é¢å è½½æ¶è·å设å¤å表ååæ° |
| | | loadDeviceName(); |
| | | getPageParams(); |
| | | }); |
| | | |
| | | // è·å页é¢ID |
| | | const getPageId = () => { |
| | | // 仿¬å°åå¨è·åid |
| | | return uni.getStorageSync('repairId'); |
| | | }; |
| | | // ç»ä»¶å¸è½½æ¶æ¸
ç宿¶å¨ |
| | | onUnmounted(() => { |
| | | if (scanTimer.value) { |
| | | clearTimeout(scanTimer.value); |
| | | } |
| | | }); |
| | | |
| | | // æäº¤è¡¨å |
| | | const sendForm = async () => { |
| | | try { |
| | | // æå¨éªè¯è¡¨å |
| | | const valid = await formRef.value.validate(); |
| | | if (!valid) return; |
| | | |
| | | loading.value = true; |
| | | const id = getPageId(); |
| | | |
| | | // åå¤æäº¤æ°æ® |
| | | const submitData = { ...form.value, status: 0 }; |
| | | |
| | | // ç¡®ä¿æ¥ææ ¼å¼æ£ç¡® |
| | | if ( |
| | | submitData.maintenancePlanTime && |
| | | !submitData.maintenancePlanTime.includes(":") |
| | | ) { |
| | | submitData.maintenancePlanTime = |
| | | submitData.maintenancePlanTime + " 00:00:00"; |
| | | } |
| | | |
| | | const { code } = id |
| | | ? await editUpkeep({ id: id, ...submitData }) |
| | | : await addUpkeep(submitData); |
| | | |
| | | if (code == 200) { |
| | | showToast(`${id ? "ç¼è¾" : "æ°å¢"}计åæå`); |
| | | setTimeout(() => { |
| | | uni.navigateBack(); |
| | | }, 1500); |
| | | } else { |
| | | loading.value = false; |
| | | } |
| | | } catch (e) { |
| | | loading.value = false; |
| | | showToast("表åéªè¯å¤±è´¥"); |
| | | } |
| | | }; |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | // æ¸
é¤åå¨çid |
| | | uni.removeStorageSync("repairId"); |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // è·å页é¢åæ° |
| | | const getPageParams = () => { |
| | | // 仿¬å°åå¨è·åid |
| | | const id = uni.getStorageSync("repairId"); |
| | | |
| | | // æ ¹æ®æ¯å¦æidåæ°æ¥å¤ææ¯æ°å¢è¿æ¯ç¼è¾ |
| | | if (id) { |
| | | // ç¼è¾æ¨¡å¼ï¼è·å详æ
|
| | | loadForm(id); |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | loadForm(); |
| | | } |
| | | }; |
| | | |
| | | // è·å页é¢ID |
| | | const getPageId = () => { |
| | | // 仿¬å°åå¨è·åid |
| | | return uni.getStorageSync("repairId"); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import '@/static/scss/form-common.scss'; |
| | | .upkeep-add { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 5rem; |
| | | } |
| | | @import "@/static/scss/form-common.scss"; |
| | | .upkeep-add { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 5rem; |
| | | } |
| | | |
| | | .footer-btns { |
| | | position: fixed; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: #fff; |
| | | display: flex; |
| | | justify-content: space-around; |
| | | align-items: center; |
| | | padding: 0.75rem 0; |
| | | box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05); |
| | | z-index: 1000; |
| | | } |
| | | .footer-btns { |
| | | position: fixed; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: #fff; |
| | | display: flex; |
| | | justify-content: space-around; |
| | | align-items: center; |
| | | padding: 0.75rem 0; |
| | | box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05); |
| | | z-index: 1000; |
| | | } |
| | | |
| | | .cancel-btn { |
| | | font-weight: 400; |
| | | font-size: 1rem; |
| | | color: #FFFFFF; |
| | | width: 6.375rem; |
| | | background: #C7C9CC; |
| | | box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); |
| | | border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; |
| | | } |
| | | .cancel-btn { |
| | | font-weight: 400; |
| | | font-size: 1rem; |
| | | color: #ffffff; |
| | | width: 6.375rem; |
| | | background: #c7c9cc; |
| | | box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2); |
| | | border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; |
| | | } |
| | | |
| | | .save-btn { |
| | | font-weight: 400; |
| | | font-size: 1rem; |
| | | color: #FFFFFF; |
| | | width: 14rem; |
| | | background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%); |
| | | box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2); |
| | | border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; |
| | | } |
| | | .save-btn { |
| | | font-weight: 400; |
| | | font-size: 1rem; |
| | | color: #ffffff; |
| | | width: 14rem; |
| | | background: linear-gradient(140deg, #00baff 0%, #006cfb 100%); |
| | | box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2); |
| | | border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; |
| | | } |
| | | |
| | | // ååºå¼è°æ´ |
| | | @media (max-width: 768px) { |
| | | .submit-section { |
| | | padding: 12px; |
| | | } |
| | | } |
| | | // ååºå¼è°æ´ |
| | | @media (max-width: 768px) { |
| | | .submit-section { |
| | | padding: 12px; |
| | | } |
| | | } |
| | | |
| | | .tip-text { |
| | | padding: 4px 16px 0 16px; |
| | | font-size: 12px; |
| | | color: #888; |
| | | } |
| | | .tip-text { |
| | | padding: 4px 16px 0 16px; |
| | | font-size: 12px; |
| | | color: #888; |
| | | } |
| | | |
| | | .scan-icon { |
| | | color: #1989fa; |
| | | font-size: 18px; |
| | | margin-left: 8px; |
| | | cursor: pointer; |
| | | } |
| | | .scan-icon { |
| | | color: #1989fa; |
| | | font-size: 18px; |
| | | margin-left: 8px; |
| | | cursor: pointer; |
| | | } |
| | | </style> |
| | |
| | | <view v-if="fileList.length > 0" |
| | | class="file-list"> |
| | | <view v-for="(file, index) in fileList" |
| | | :key="file.id || index" |
| | | :key="file.storageAttachmentId || file.id || index" |
| | | class="file-item"> |
| | | <!-- æä»¶å¾æ --> |
| | | <!-- <view class="file-icon" |
| | |
| | | </view> --> |
| | | <!-- æä»¶ä¿¡æ¯ --> |
| | | <view class="file-info"> |
| | | <text class="file-name">{{ file.name }}</text> |
| | | <text class="file-name">{{ file.originalFilename || file.name }}</text> |
| | | <!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} · {{ file.uploadTime || file.createTime }}</text> --> |
| | | </view> |
| | | <!-- æä½æé® --> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import config from "@/config"; |
| | | import { getToken } from "@/utils/auth"; |
| | | // import { saveAs } from "file-saver"; |
| | | import { |
| | | listMaintenanceTaskFiles, |
| | | addMaintenanceTaskFile, |
| | | delMaintenanceTaskFile, |
| | | } from "@/api/equipmentManagement/upkeep"; |
| | | attachmentList, |
| | | createAttachment, |
| | | deleteAttachment, |
| | | } from "@/api/basicData/storageAttachment"; |
| | | import { blobValidate } from "@/utils/ruoyi"; |
| | | |
| | | // éä»¶å表 |
| | |
| | | // const fileType = fileName.split(".").pop(); |
| | | // 3. æé ä¿åæä»¶ä¿¡æ¯çåæ° |
| | | const saveData = { |
| | | name: fileName, |
| | | deviceMaintenanceId: upkeepId.value, |
| | | url: res.data.tempPath || "", |
| | | application: "file", |
| | | recordType: recordType.value, |
| | | recordId: upkeepId.value, |
| | | storageBlobDTOs: [ |
| | | { |
| | | name: fileName, |
| | | url: |
| | | res.data.url || |
| | | res.data.previewURL || |
| | | res.data.tempPath || |
| | | "", |
| | | ...res.data, |
| | | }, |
| | | ], |
| | | }; |
| | | console.log(saveData, "ä¿åæä»¶ä¿¡æ¯åæ°"); |
| | | // 4. è°ç¨ addRuleFile æ¥å£ä¿åæä»¶ä¿¡æ¯ |
| | | addMaintenanceTaskFile(saveData) |
| | | // 4. è°ç¨ createAttachment æ¥å£ä¿åæä»¶ä¿¡æ¯ |
| | | createAttachment(saveData) |
| | | .then(addRes => { |
| | | if (addRes.code === 200) { |
| | | // 5. æ·»å å°æä»¶å表 |
| | | const newFile = { |
| | | ...addRes.data, |
| | | uploadTime: new Date().toLocaleString(), |
| | | }; |
| | | // fileList.value.push(newFile); |
| | | // 5. å·æ°å表 |
| | | getFileList(); |
| | | showToast("ä¸ä¼ æå"); |
| | | } else { |
| | |
| | | }; |
| | | // ä¸è½½æä»¶ |
| | | const downloadFile = file => { |
| | | var url = |
| | | config.baseUrl + |
| | | "/common/download?fileName=" + |
| | | encodeURIComponent(file.url) + |
| | | "&delete=true"; |
| | | console.log(url, "url"); |
| | | let url = file.downloadURL || file.previewURL || file.url; |
| | | |
| | | if (!url) { |
| | | showToast("æä»¶å°åæ æ"); |
| | | return; |
| | | } |
| | | |
| | | // 妿䏿¯å®æ´çURLï¼åæ¼æ¥ |
| | | if (!url.startsWith("http")) { |
| | | url = |
| | | config.baseUrl + |
| | | "/common/download?fileName=" + |
| | | encodeURIComponent(url) + |
| | | "&delete=true"; |
| | | } |
| | | |
| | | console.log(url, "ä¸è½½å°å"); |
| | | |
| | | uni.showLoading({ title: "æ£å¨ä¸è½½...", mask: true }); |
| | | uni |
| | | .downloadFile({ |
| | | url: url, |
| | | responseType: "blob", |
| | | header: { Authorization: "Bearer " + getToken() }, |
| | | }) |
| | | .then(res => { |
| | | uni.hideLoading(); |
| | | let osType = uni.getStorageSync("deviceInfo").osName; |
| | | let filePath = res.tempFilePath; |
| | | if (osType === "ios") { |
| | |
| | | success: res => {}, |
| | | fail: err => { |
| | | console.log("uni.openDocument--fail"); |
| | | reject(err); |
| | | }, |
| | | }); |
| | | } else { |
| | |
| | | uni.showToast({ |
| | | icon: "none", |
| | | mask: true, |
| | | title: |
| | | "æä»¶å·²ä¿åï¼Android/data/uni.UNI720216F/apps/__UNI__720216F/" + |
| | | fileRes.savedFilePath, //ä¿åè·¯å¾ |
| | | duration: 3000, |
| | | title: "æä»¶å·²ä¸è½½å¹¶å°è¯æå¼", |
| | | duration: 2000, |
| | | }); |
| | | setTimeout(() => { |
| | | //æå¼ææ¡£æ¥ç |
| | |
| | | }, |
| | | fail: err => { |
| | | console.log("uni.save--fail"); |
| | | reject(err); |
| | | }, |
| | | }); |
| | | } |
| | | // const isBlob = blobValidate(res.data); |
| | | // if (isBlob) { |
| | | // const blob = new Blob([res.data], { type: "text/plain" }); |
| | | // const url = URL.createObjectURL(blob); |
| | | // const downloadLink = document.getElementById("downloadLink"); |
| | | // downloadLink.href = url; |
| | | // downloadLink.download = file.name; |
| | | // downloadLink.click(); |
| | | // showToast("ä¸è½½æå"); |
| | | // } else { |
| | | // showToast("ä¸è½½å¤±è´¥"); |
| | | // } |
| | | }) |
| | | .catch(err => { |
| | | uni.hideLoading(); |
| | | console.error("ä¸è½½å¤±è´¥:", err); |
| | | showToast("ä¸è½½å¤±è´¥"); |
| | | }); |
| | |
| | | content: `ç¡®å®è¦å é¤éä»¶ "${file.name}" åï¼`, |
| | | success: res => { |
| | | if (res.confirm) { |
| | | deleteFile(file.id, index); |
| | | deleteFile(file.storageAttachmentId || file.id, index); |
| | | } |
| | | }, |
| | | }); |
| | |
| | | mask: true, |
| | | }); |
| | | |
| | | delMaintenanceTaskFile([fileId]) |
| | | deleteAttachment([fileId]) |
| | | .then(res => { |
| | | uni.hideLoading(); |
| | | if (res.code === 200) { |
| | |
| | | icon: "none", |
| | | }); |
| | | }; |
| | | const rulesRegulationsManagementId = ref(""); |
| | | const upkeepId = ref(""); |
| | | const recordType = ref(""); |
| | | |
| | | // 页é¢å è½½æ¶è·ååæ° |
| | | onLoad(options => { |
| | | if (options.recordId) { |
| | | upkeepId.value = options.recordId; |
| | | } else { |
| | | upkeepId.value = uni.getStorageSync("upkeepId"); |
| | | } |
| | | |
| | | if (options.recordType) { |
| | | recordType.value = options.recordType; |
| | | } else { |
| | | recordType.value = "device_maintenance"; // é»è®¤å
¼å®¹ |
| | | } |
| | | |
| | | getFileList(); |
| | | }); |
| | | |
| | | // 页é¢å è½½æ¶ |
| | | onMounted(() => { |
| | | // ä» API è·åéä»¶å表 |
| | | |
| | | // 仿¬å°åå¨è·å rulesRegulationsManagementId |
| | | rulesRegulationsManagementId.value = uni.getStorageSync( |
| | | "rulesRegulationsManagement" |
| | | ); |
| | | upkeepId.value = uni.getStorageSync("upkeepId"); |
| | | getFileList(); |
| | | // getFileList(); // onLoad ä¸å·²ç»è°ç¨äº |
| | | }); |
| | | |
| | | // è·åéä»¶å表 |
| | | const getFileList = () => { |
| | | if (!upkeepId.value) return; |
| | | |
| | | uni.showLoading({ |
| | | title: "å è½½ä¸...", |
| | | mask: true, |
| | | }); |
| | | |
| | | listMaintenanceTaskFiles({ |
| | | current: 1, |
| | | size: 100, |
| | | deviceMaintenanceId: upkeepId.value, |
| | | rulesRegulationsManagementId: upkeepId.value, |
| | | attachmentList({ |
| | | recordType: recordType.value, |
| | | recordId: upkeepId.value, |
| | | }) |
| | | .then(res => { |
| | | uni.hideLoading(); |
| | | if (res.code === 200) { |
| | | fileList.value = res.data.records || []; |
| | | fileList.value = res.data || []; |
| | | } else { |
| | | showToast("è·åéä»¶å表失败"); |
| | | } |
| | |
| | | <text class="detail-value">{{ formatDateTime(item.createTime) || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä¿å
»äºº</text> |
| | | <text class="detail-value">{{ item.maintenancePerson || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä¿å
»é¡¹ç®</text> |
| | | <text class="detail-value">{{ item.machineryCategory || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®é
ä¿å
»äºº</text> |
| | | <text class="detail-value">{{ item.maintenanceActuallyName || '-' }}</text> |
| | | </view> |
| | |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä¿å
ȍȾ</text> |
| | | <view class="detail-value"> |
| | | <text class="detail-value">{{ item.maintenanceResult || '-' }}</text> |
| | | <!-- <view class="detail-value"> |
| | | <u-tag v-if="item.maintenanceResult === 1" |
| | | type="success" |
| | | size="mini"> |
| | |
| | | ç»´ä¿® |
| | | </u-tag> |
| | | <text v-if="item.maintenanceResult === undefined || item.maintenanceResult === null">-</text> |
| | | </view> |
| | | </view> --> |
| | | </view> |
| | | </view> |
| | | <!-- æé®åºå --> |
| | |
| | | }; |
| | | // æ°å¢éä»¶ - 跳转å°éä»¶é¡µé¢ |
| | | const addFile = id => { |
| | | // ä½¿ç¨æ¬å°åå¨ä¼ éid |
| | | uni.setStorageSync("upkeepId", id); |
| | | uni.navigateTo({ |
| | | url: "/pages/equipmentManagement/upkeep/fileList", |
| | | url: `/pages/equipmentManagement/upkeep/fileList?recordId=${id}&recordType=device_maintenance`, |
| | | }); |
| | | }; |
| | | |
| | |
| | | <!-- ä¸ä¼ éä»¶ --> |
| | | <u-form-item v-if="form.status == '1'" |
| | | label="ä¿å
»éä»¶" |
| | | prop="storageBlobDTOs" |
| | | border-bottom> |
| | | <view class="simple-upload-area"> |
| | | <view class="upload-buttons"> |
| | | <u-button type="primary" |
| | | @click="chooseMedia('image')" |
| | | :loading="uploading" |
| | | :disabled="uploadFiles.length >= uploadConfig.limit" |
| | | :customStyle="{ marginRight: '10px', flex: 1 }"> |
| | | <u-icon name="camera" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px;"></u-icon> |
| | | {{ uploading ? 'ä¸ä¼ ä¸...' : 'æç
§' }} |
| | | </u-button> |
| | | <!-- <u-button type="success" |
| | | @click="chooseMedia('video')" |
| | | :loading="uploading" |
| | | :disabled="uploadFiles.length >= uploadConfig.limit" |
| | | :customStyle="{ flex: 1 }"> |
| | | <uni-icons type="videocam" |
| | | name="videocam" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px;"></uni-icons> |
| | | {{ uploading ? 'ä¸ä¼ ä¸...' : 'æè§é¢' }} |
| | | </u-button> --> |
| | | </view> |
| | | <!-- ä¸ä¼ è¿åº¦ --> |
| | | <view v-if="uploading" |
| | | class="upload-progress"> |
| | | <u-line-progress :percentage="uploadProgress" |
| | | :showText="true" |
| | | activeColor="#409eff"></u-line-progress> |
| | | </view> |
| | | <!-- ä¸ä¼ çæä»¶å表 --> |
| | | <view v-if="uploadFiles.length > 0" |
| | | class="file-list"> |
| | | <view v-for="(file, index) in uploadFiles" |
| | | :key="index" |
| | | class="file-item"> |
| | | <view class="file-preview-container"> |
| | | <!-- {{formatFileUrl(file.url)}} --> |
| | | <image v-if="file.type === 'image' || isImageFile(file)" |
| | | :src="formatFileUrl(file.url || file.tempFilePath || file.path || file.downloadUrl)" |
| | | class="file-preview" |
| | | mode="aspectFill" /> |
| | | <view v-else-if="file.type === 'video'" |
| | | class="video-preview"> |
| | | <uni-icons type="videocam" |
| | | name="videocam" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px;"></uni-icons> |
| | | <text class="video-text">è§é¢</text> |
| | | </view> |
| | | <!-- å é¤æé® --> |
| | | <view class="delete-btn" |
| | | @click="removeFile(index)"> |
| | | <u-icon name="close" |
| | | size="12" |
| | | color="#fff"></u-icon> |
| | | </view> |
| | | </view> |
| | | <view class="file-info"> |
| | | <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? 'å¾ç' : 'è§é¢') |
| | | }}</text> |
| | | <text class="file-size">{{ formatFileSize(file.size) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="uploadFiles.length === 0" |
| | | class="empty-state"> |
| | | <text>è¯·éæ©è¦ä¸ä¼ çä¿å
»å¾ç</text> |
| | | </view> |
| | | </view> |
| | | <CommonUpload v-model="form.storageBlobDTOs" /> |
| | | </u-form-item> |
| | | <!-- æäº¤æé® --> |
| | | <view class="footer-btns"> |
| | |
| | | import { ref, onMounted, reactive } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import { addMaintenance } from "@/api/equipmentManagement/upkeep"; |
| | | import { getSparePartsList } from "@/api/equipmentManagement/repair"; |
| | | import useUserStore from "@/store/modules/user"; |
| | |
| | | const sparePartsQtyRaw = ref(""); |
| | | |
| | | // æä»¶ä¸ä¼ ç¸å
³ |
| | | const uploadFiles = ref([]); |
| | | const uploading = ref(false); |
| | | const uploadProgress = ref(0); |
| | | const number = ref(0); |
| | |
| | | maintenanceResult: undefined, // ä¿å
ȍȾ |
| | | maintenanceActuallyTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // å®é
ä¿å
»æ¥æï¼åªæ¾ç¤ºæ¥æï¼ |
| | | sparePartsIds: undefined, // 设å¤å¤ä»¶ID |
| | | storageBlobDTOs: [], // ä¿å
»éä»¶ |
| | | }); |
| | | |
| | | // æ¸
é¤è¡¨åæ ¡éªç¶æ |
| | |
| | | maintenanceResult: undefined, |
| | | maintenanceActuallyTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | sparePartsIds: [], |
| | | storageBlobDTOs: [], |
| | | }; |
| | | maintenancestatusText.value = ""; |
| | | selectedSpareParts.value = []; |
| | |
| | | } else if (form.value.maintenanceResult === undefined) { |
| | | isValid = false; |
| | | errorMessage = "è¯·éæ©ä¿å
ȍȾ"; |
| | | } else if (uploadFiles.value.length === 0 && form.value.status == "1") { |
| | | } else if ( |
| | | (!form.value.storageBlobDTOs || |
| | | form.value.storageBlobDTOs.length === 0) && |
| | | form.value.status == "1" |
| | | ) { |
| | | isValid = false; |
| | | errorMessage = "请ä¸ä¼ ä¿å
ȍ
§ç"; |
| | | } |
| | |
| | | |
| | | const submitData = { |
| | | ...form.value, |
| | | imagesFile: form.value.status == "1" ? uploadFiles.value : [], |
| | | sparePartsIds: spareIds.length ? spareIds.join(",") : "", |
| | | sparePartsQty: spareIds.length |
| | | ? spareIds.map(pid => sparePartQtyMap?.[pid] ?? 1).join(",") |
| | |
| | | // éç½®éæ©çå¤ä»¶ |
| | | selectedSpareParts.value = []; |
| | | // éç½®ä¸ä¼ çæä»¶ |
| | | uploadFiles.value = []; |
| | | form.value.storageBlobDTOs = []; |
| | | uploading.value = false; |
| | | uploadProgress.value = 0; |
| | | maintenancestatusText.value = ""; |
| | |
| | | sparePartsIds.value = itemData.sparePartsIds; |
| | | |
| | | // å¡«å
éä»¶æ°æ® |
| | | if (itemData.files && itemData.files.length > 0) { |
| | | uploadFiles.value = itemData.files.map(file => ({ |
| | | if (itemData.storageBlobVOs && itemData.storageBlobVOs.length > 0) { |
| | | form.value.storageBlobDTOs = itemData.storageBlobVOs; |
| | | } else if (itemData.files && itemData.files.length > 0) { |
| | | form.value.storageBlobDTOs = itemData.files.map(file => ({ |
| | | id: file.id, |
| | | name: file.name || file.bucketFilename || file.originalFilename, |
| | | url: file.url || file.downloadUrl, |
| | |
| | | size: file.size || file.byteSize, |
| | | })); |
| | | } else if (itemData.uploadFiles && itemData.uploadFiles.length > 0) { |
| | | uploadFiles.value = itemData.uploadFiles.map(file => ({ |
| | | form.value.storageBlobDTOs = itemData.uploadFiles.map(file => ({ |
| | | id: file.id, |
| | | name: file.name || file.bucketFilename || file.originalFilename, |
| | | url: file.url || file.downloadUrl || file.tempFilePath || file.path, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="borrow-edit"> |
| | | <PageHeader :title="pageTitle" @back="goBack" /> |
| | | |
| | | <up-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="110" |
| | | > |
| | | <up-form-item label="åé
人" prop="borrower" required> |
| | | <up-input |
| | | v-model="form.borrower" |
| | | placeholder="请è¾å
¥åé
人" |
| | | clearable |
| | | :disabled="isReturned" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="åé
书ç±" prop="documentationId" required> |
| | | <up-input |
| | | v-model="displayDocName" |
| | | placeholder="è¯·éæ©åé
书ç±" |
| | | readonly |
| | | :disabled="isEdit" |
| | | @click="!isEdit && (showDocPicker = true)" |
| | | /> |
| | | <template #right> |
| | | <up-icon v-if="!isEdit" name="arrow-right" @click="showDocPicker = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="åé
æ¥æ" prop="borrowDate" required> |
| | | <up-input |
| | | v-model="form.borrowDate" |
| | | placeholder="è¯·éæ©åé
æ¥æ" |
| | | readonly |
| | | @click="!isReturned && (showBorrowDatePicker = true)" |
| | | :disabled="isReturned" |
| | | /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" @click="!isReturned && (showBorrowDatePicker = true)"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="åºå½è¿æ¥æ" prop="dueReturnDate" required> |
| | | <up-input |
| | | v-model="form.dueReturnDate" |
| | | placeholder="è¯·éæ©åºå½è¿æ¥æ" |
| | | readonly |
| | | @click="!isReturned && (showDueDatePicker = true)" |
| | | :disabled="isReturned" |
| | | /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" @click="!isReturned && (showDueDatePicker = true)"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="åé
ç®ç" prop="borrowPurpose" required> |
| | | <up-input |
| | | v-model="form.borrowPurpose" |
| | | placeholder="请è¾å
¥åé
ç®ç" |
| | | clearable |
| | | :disabled="isReturned" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="夿³¨"> |
| | | <up-textarea |
| | | v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨ä¿¡æ¯" |
| | | height="80" |
| | | border="none" |
| | | :disabled="isReturned" |
| | | /> |
| | | </up-form-item> |
| | | </up-form> |
| | | |
| | | <FooterButtons |
| | | v-if="!isReturned" |
| | | :loading="loading" |
| | | :confirmText="isEdit ? 'ä¿å' : 'æ°å¢'" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" |
| | | /> |
| | | |
| | | <!-- åé
æ¥æéæ©å¨ --> |
| | | <up-popup :show="showBorrowDatePicker" mode="bottom" @close="showBorrowDatePicker = false"> |
| | | <up-datetime-picker |
| | | :show="true" |
| | | v-model="borrowDateValue" |
| | | @confirm="onBorrowDateConfirm" |
| | | @cancel="showBorrowDatePicker = false" |
| | | mode="date" |
| | | /> |
| | | </up-popup> |
| | | |
| | | <!-- åºå½è¿æ¥æéæ©å¨ --> |
| | | <up-popup :show="showDueDatePicker" mode="bottom" @close="showDueDatePicker = false"> |
| | | <up-datetime-picker |
| | | :show="true" |
| | | v-model="dueReturnDateValue" |
| | | @confirm="onDueDateConfirm" |
| | | @cancel="showDueDatePicker = false" |
| | | mode="date" |
| | | /> |
| | | </up-popup> |
| | | |
| | | <!-- ææ¡£éæ©å¨ --> |
| | | <up-action-sheet |
| | | :show="showDocPicker" |
| | | :actions="documentOptions" |
| | | title="éæ©åé
书ç±" |
| | | @select="onDocSelect" |
| | | @close="showDocPicker = false" |
| | | /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { addBorrow, updateBorrow, getDocumentList } from "@/api/fileManagement/borrow"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const borrowId = ref(""); |
| | | const isEdit = ref(false); |
| | | |
| | | // å¼¹çªæ¾ç¤ºç¶æ |
| | | const showDocPicker = ref(false); |
| | | const showBorrowDatePicker = ref(false); |
| | | const showDueDatePicker = ref(false); |
| | | |
| | | // æ°æ® |
| | | const documentList = ref([]); |
| | | const borrowDateValue = ref(Date.now()); |
| | | const dueReturnDateValue = ref(Date.now()); |
| | | const displayDocName = ref(""); // ç¨äºæ¾ç¤ºçææ¡£åç§° |
| | | |
| | | const form = ref({ |
| | | id: "", |
| | | documentationId: "", |
| | | borrower: "", |
| | | borrowPurpose: "", |
| | | borrowDate: "", |
| | | dueReturnDate: "", |
| | | remark: "", |
| | | borrowStatus: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | borrower: [{ required: true, message: "请è¾å
¥åé
人", trigger: "blur" }], |
| | | documentationId: [{ required: true, message: "è¯·éæ©åé
书ç±", trigger: "change" }], |
| | | borrowPurpose: [{ required: true, message: "请è¾å
¥åé
ç®ç", trigger: "blur" }], |
| | | borrowDate: [{ required: true, message: "è¯·éæ©åé
æ¥æ", trigger: "change" }], |
| | | dueReturnDate: [{ required: true, message: "è¯·éæ©åºå½è¿æ¥æ", trigger: "change" }], |
| | | }; |
| | | |
| | | // 页颿 é¢ |
| | | const pageTitle = computed(() => { |
| | | if (isEdit.value) { |
| | | return form.value.borrowStatus === "å½è¿" ? "åé
详æ
" : "ç¼è¾åé
"; |
| | | } |
| | | return "æ°å¢åé
"; |
| | | }); |
| | | |
| | | // æ¯å¦å·²å½è¿ |
| | | const isReturned = computed(() => { |
| | | return form.value.borrowStatus === "å½è¿"; |
| | | }); |
| | | |
| | | // ææ¡£é项ï¼ä»
æ°å¢æ¨¡å¼ä½¿ç¨ï¼ |
| | | const documentOptions = computed(() => { |
| | | return documentList.value.map((item) => ({ |
| | | name: item.docName || item.name, |
| | | id: item.id, |
| | | })); |
| | | }); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.removeStorageSync("borrowEditData"); |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // å è½½ææ¡£å表ï¼ä»
æ°å¢æ¨¡å¼éè¦ï¼ |
| | | const loadDocumentList = async () => { |
| | | try { |
| | | const res = await getDocumentList(); |
| | | if (res.code === 200) { |
| | | documentList.value = res.data || []; |
| | | } |
| | | } catch (error) { |
| | | console.error("è·åææ¡£å表失败", error); |
| | | } |
| | | }; |
| | | |
| | | // ææ¡£éæ©ç¡®è®¤ |
| | | const onDocSelect = (e) => { |
| | | form.value.documentationId = e.id; |
| | | displayDocName.value = e.name; |
| | | showDocPicker.value = false; |
| | | }; |
| | | |
| | | // åé
æ¥æç¡®è®¤ |
| | | const onBorrowDateConfirm = (e) => { |
| | | const date = new Date(e.value); |
| | | form.value.borrowDate = formatDate(date); |
| | | showBorrowDatePicker.value = false; |
| | | }; |
| | | |
| | | // åºå½è¿æ¥æç¡®è®¤ |
| | | const onDueDateConfirm = (e) => { |
| | | const date = new Date(e.value); |
| | | form.value.dueReturnDate = formatDate(date); |
| | | showDueDatePicker.value = false; |
| | | }; |
| | | |
| | | // æ ¼å¼åæ¥æ |
| | | const formatDate = (date) => { |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const handleSubmit = () => { |
| | | // 妿已å½è¿ï¼ç¦æ¢æäº¤ |
| | | if (isReturned.value) { |
| | | uni.showToast({ title: "å·²å½è¿çåé
è®°å½ä¸è½ç¼è¾", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | formRef.value |
| | | .validate() |
| | | .then(async () => { |
| | | try { |
| | | loading.value = true; |
| | | |
| | | if (isEdit.value) { |
| | | // ç¼è¾æ¨¡å¼ |
| | | const res = await updateBorrow({ |
| | | id: form.value.id, |
| | | borrower: form.value.borrower, |
| | | borrowPurpose: form.value.borrowPurpose, |
| | | borrowDate: form.value.borrowDate, |
| | | dueReturnDate: form.value.dueReturnDate, |
| | | remark: form.value.remark, |
| | | }); |
| | | |
| | | if (res.code === 200) { |
| | | uni.showToast({ title: "ç¼è¾æå", icon: "success" }); |
| | | setTimeout(() => { |
| | | goBack(); |
| | | }, 1500); |
| | | } else { |
| | | uni.showToast({ title: res.msg || "ç¼è¾å¤±è´¥", icon: "none" }); |
| | | } |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | const res = await addBorrow({ |
| | | documentationId: form.value.documentationId, |
| | | borrower: form.value.borrower, |
| | | borrowPurpose: form.value.borrowPurpose, |
| | | borrowDate: form.value.borrowDate, |
| | | dueReturnDate: form.value.dueReturnDate, |
| | | borrowStatus: "åé
", |
| | | remark: form.value.remark, |
| | | }); |
| | | |
| | | if (res.code === 200) { |
| | | uni.showToast({ title: "æ°å¢æå", icon: "success" }); |
| | | setTimeout(() => { |
| | | goBack(); |
| | | }, 1500); |
| | | } else { |
| | | uni.showToast({ title: res.msg || "æ°å¢å¤±è´¥", icon: "none" }); |
| | | } |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ title: "æä½å¤±è´¥", icon: "none" }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | // éªè¯å¤±è´¥ |
| | | }); |
| | | }; |
| | | |
| | | // 页é¢å è½½ |
| | | onLoad((options) => { |
| | | if (options.id) { |
| | | // ç¼è¾æ¨¡å¼ |
| | | isEdit.value = true; |
| | | borrowId.value = options.id; |
| | | |
| | | // ä» storage è·åç¼è¾æ°æ® |
| | | const editDataStr = uni.getStorageSync("borrowEditData"); |
| | | if (editDataStr) { |
| | | try { |
| | | const data = JSON.parse(editDataStr); |
| | | Object.assign(form.value, data); |
| | | borrowDateValue.value = new Date(data.borrowDate).getTime(); |
| | | dueReturnDateValue.value = new Date(data.dueReturnDate).getTime(); |
| | | // ç´æ¥ä½¿ç¨ä¼ éçææ¡£åç§°æ¾ç¤ºï¼å°è¯å¤ä¸ªå¯è½çåæ®µå |
| | | displayDocName.value = data.docName || data.documentationName || data.fileName || data.name || ""; |
| | | } catch (e) { |
| | | console.error("è§£æç¼è¾æ°æ®å¤±è´¥", e); |
| | | } |
| | | } |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ï¼å è½½ææ¡£å表并设置é»è®¤æ¥æ |
| | | loadDocumentList(); |
| | | const today = new Date(); |
| | | form.value.borrowDate = formatDate(today); |
| | | borrowDateValue.value = today.getTime(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .borrow-edit { |
| | | min-height: 100vh; |
| | | background: #f5f5f5; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <!-- 使ç¨éç¨é¡µé¢å¤´é¨ç»ä»¶ --> |
| | | <PageHeader title="åé
管ç" @back="goBack" /> |
| | | |
| | | <!-- æç´¢åçéåºå --> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input |
| | | class="search-text" |
| | | placeholder="请è¾å
¥åé
人æç´¢" |
| | | v-model="searchForm.borrower" |
| | | @change="getList" |
| | | clearable |
| | | /> |
| | | </view> |
| | | <view class="filter-button" @click="getList"> |
| | | <up-icon name="search" size="24" color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- åé
å表 --> |
| | | <view class="ledger-list" v-if="borrowList.length > 0"> |
| | | <view v-for="(item, index) in borrowList" :key="index"> |
| | | <view class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" size="16" color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.docName || '-' }}</text> |
| | | </view> |
| | | <view class="item-tag" :class="getStatusClass(item.borrowStatus)"> |
| | | <text class="tag-text">{{ item.borrowStatus }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åé
人</text> |
| | | <text class="detail-value">{{ item.borrower || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åé
ç®ç</text> |
| | | <text class="detail-value">{{ item.borrowPurpose || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åé
æ¥æ</text> |
| | | <text class="detail-value">{{ item.borrowDate || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºå½è¿æ¥æ</text> |
| | | <text class="detail-value">{{ item.dueReturnDate || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row" v-if="item.remark"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remark }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="detail-buttons"> |
| | | <u-button |
| | | v-if="item.borrowStatus !== 'å½è¿'" |
| | | class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | @click.stop="goEdit(item)" |
| | | > |
| | | ç¼è¾ |
| | | </u-button> |
| | | <u-button |
| | | v-if="item.borrowStatus !== 'å½è¿'" |
| | | class="detail-button" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click.stop="handleDelete(item)" |
| | | > |
| | | å é¤ |
| | | </u-button> |
| | | <u-button |
| | | v-if="item.borrowStatus === 'å½è¿'" |
| | | class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | plain |
| | | @click.stop="goView(item)" |
| | | > |
| | | æ¥ç |
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-else class="no-data"> |
| | | <text>ææ åé
è®°å½</text> |
| | | </view> |
| | | |
| | | <!-- æµ®å¨æä½æé® --> |
| | | <view class="fab-button" @click="goAdd"> |
| | | <up-icon name="plus" size="24" color="#ffffff"></up-icon> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { getBorrowList, deleteBorrow } from "@/api/fileManagement/borrow"; |
| | | |
| | | // æ¥è¯¢è¡¨å |
| | | const searchForm = reactive({ |
| | | borrower: "", |
| | | }); |
| | | |
| | | // åé
åè¡¨æ°æ® |
| | | const borrowList = ref([]); |
| | | |
| | | // å页ç¸å
³ |
| | | const pagination = reactive({ |
| | | currentPage: 1, |
| | | pageSize: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // è·åç¶ææ ·å¼ç±» |
| | | const getStatusClass = (status) => { |
| | | if (status === "å½è¿") return "tag-success"; |
| | | if (status === "åé
") return "tag-warning"; |
| | | return "tag-default"; |
| | | }; |
| | | |
| | | // å è½½åé
å表 |
| | | const getList = async () => { |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | |
| | | const query = { |
| | | page: -1, |
| | | size: -1, |
| | | borrower: searchForm.borrower || undefined, |
| | | }; |
| | | |
| | | try { |
| | | const res = await getBorrowList(query); |
| | | if (res.code === 200) { |
| | | borrowList.value = res.data.records || []; |
| | | pagination.total = res.data.total || 0; |
| | | } else { |
| | | uni.showToast({ title: res.msg || "è·ååé
å表失败", icon: "none" }); |
| | | borrowList.value = []; |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·ååé
å表失败", icon: "none" }); |
| | | borrowList.value = []; |
| | | } finally { |
| | | uni.hideLoading(); |
| | | } |
| | | }; |
| | | |
| | | // è·³è½¬å°æ°å¢é¡µé¢ |
| | | const goAdd = () => { |
| | | uni.navigateTo({ |
| | | url: "/pages/fileManagement/borrow/edit", |
| | | }); |
| | | }; |
| | | |
| | | // 跳转å°ç¼è¾é¡µé¢ |
| | | const goEdit = (item) => { |
| | | uni.setStorageSync("borrowEditData", JSON.stringify(item)); |
| | | uni.navigateTo({ |
| | | url: `/pages/fileManagement/borrow/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | // è·³è½¬å°æ¥ç页é¢ï¼å·²å½è¿è®°å½ï¼ |
| | | const goView = (item) => { |
| | | uni.setStorageSync("borrowEditData", JSON.stringify(item)); |
| | | uni.navigateTo({ |
| | | url: `/pages/fileManagement/borrow/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = (row) => { |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", |
| | | confirmText: "确认", |
| | | cancelText: "åæ¶", |
| | | success: async (res) => { |
| | | if (res.confirm) { |
| | | try { |
| | | uni.showLoading({ title: "å é¤ä¸...", mask: true }); |
| | | const result = await deleteBorrow([row.id]); |
| | | if (result.code === 200) { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | } else { |
| | | uni.showToast({ title: result.msg || "å é¤å¤±è´¥", icon: "none" }); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | } finally { |
| | | uni.hideLoading(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | // æ ç¾æ ·å¼ |
| | | .item-tag { |
| | | border-radius: 4px; |
| | | padding: 2px 8px; |
| | | |
| | | &.tag-success { |
| | | background: #4caf50; |
| | | } |
| | | |
| | | &.tag-warning { |
| | | background: #ff9800; |
| | | } |
| | | |
| | | &.tag-default { |
| | | background: #9e9e9e; |
| | | } |
| | | } |
| | | |
| | | .tag-text { |
| | | font-size: 11px; |
| | | color: #ffffff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | // æé®æ ·å¼ |
| | | .detail-buttons { |
| | | padding: 12px 0; |
| | | display: flex; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .detail-button { |
| | | flex: 1; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="return-edit"> |
| | | <PageHeader :title="pageTitle" @back="goBack" /> |
| | | |
| | | <up-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="110" |
| | | > |
| | | <up-form-item label="ææ¡£" prop="borrowId" required> |
| | | <up-input |
| | | v-model="displayDocName" |
| | | placeholder="è¯·éæ©ææ¡£" |
| | | readonly |
| | | :disabled="isEdit" |
| | | @click="!isEdit && (showDocPicker = true)" |
| | | /> |
| | | <template #right> |
| | | <up-icon v-if="!isEdit" name="arrow-right" @click="showDocPicker = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="åé
人" prop="borrower"> |
| | | <up-input |
| | | v-model="form.borrower" |
| | | placeholder="éæ©ææ¡£åèªå¨å¸¦åº" |
| | | disabled |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="å½è¿äºº" prop="returner" required> |
| | | <up-input |
| | | v-model="form.returner" |
| | | placeholder="请è¾å
¥å½è¿äºº" |
| | | clearable |
| | | :disabled="isReturned" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="å½è¿æ¥æ" prop="returnDate" required> |
| | | <up-input |
| | | v-model="form.returnDate" |
| | | placeholder="è¯·éæ©å½è¿æ¥æ" |
| | | readonly |
| | | @click="!isReturned && (showReturnDatePicker = true)" |
| | | :disabled="isReturned" |
| | | /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" @click="!isReturned && (showReturnDatePicker = true)"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="åºå½è¿æ¥æ" prop="dueReturnDate"> |
| | | <up-input |
| | | v-model="form.dueReturnDate" |
| | | placeholder="éæ©ææ¡£åèªå¨å¸¦åº" |
| | | disabled |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="夿³¨è¯´æ" prop="remark"> |
| | | <up-textarea |
| | | v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨è¯´æ" |
| | | height="80" |
| | | border="none" |
| | | :disabled="isReturned" |
| | | /> |
| | | </up-form-item> |
| | | </up-form> |
| | | |
| | | <FooterButtons |
| | | v-if="!isReturned" |
| | | :loading="loading" |
| | | :confirmText="isEdit ? 'ä¿å' : 'æ°å¢'" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" |
| | | /> |
| | | |
| | | <!-- ææ¡£éæ©å¨ --> |
| | | <up-action-sheet |
| | | :show="showDocPicker" |
| | | :actions="documentOptions" |
| | | title="éæ©ææ¡£" |
| | | @select="onDocSelect" |
| | | @close="showDocPicker = false" |
| | | /> |
| | | |
| | | <!-- å½è¿æ¥æéæ©å¨ --> |
| | | <up-popup :show="showReturnDatePicker" mode="bottom" @close="showReturnDatePicker = false"> |
| | | <up-datetime-picker |
| | | :show="true" |
| | | v-model="returnDateValue" |
| | | @confirm="onReturnDateConfirm" |
| | | @cancel="showReturnDatePicker = false" |
| | | mode="date" |
| | | /> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { returnDocument, reventUpdate, getDocumentList } from "@/api/fileManagement/return"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const returnId = ref(""); |
| | | const isEdit = ref(false); |
| | | |
| | | // å¼¹çªæ¾ç¤ºç¶æ |
| | | const showDocPicker = ref(false); |
| | | const showReturnDatePicker = ref(false); |
| | | |
| | | // æ°æ® |
| | | const documentList = ref([]); |
| | | const returnDateValue = ref(Date.now()); |
| | | const displayDocName = ref(""); // ç¨äºæ¾ç¤ºçææ¡£åç§° |
| | | |
| | | const form = ref({ |
| | | id: "", |
| | | borrowId: "", |
| | | documentationId: "", |
| | | borrower: "", |
| | | returner: "", |
| | | borrowStatus: "", |
| | | returnDate: "", |
| | | dueReturnDate: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | borrowId: [{ required: true, message: "è¯·éæ©ææ¡£", trigger: "change" }], |
| | | returner: [{ required: true, message: "请è¾å
¥å½è¿äºº", trigger: "blur" }], |
| | | returnDate: [{ required: true, message: "è¯·éæ©å½è¿æ¥æ", trigger: "change" }], |
| | | }; |
| | | |
| | | // 页颿 é¢ |
| | | const pageTitle = computed(() => { |
| | | if (isEdit.value) { |
| | | return form.value.borrowStatus === "å½è¿" ? "å½è¿è¯¦æ
" : "ç¼è¾å½è¿"; |
| | | } |
| | | return "æ°å¢å½è¿"; |
| | | }); |
| | | |
| | | // æ¯å¦å·²å½è¿ |
| | | const isReturned = computed(() => { |
| | | return form.value.borrowStatus === "å½è¿"; |
| | | }); |
| | | |
| | | // ææ¡£é项ï¼ä»
æ°å¢æ¨¡å¼ä½¿ç¨ï¼ |
| | | const documentOptions = computed(() => { |
| | | return documentList.value.map((item) => ({ |
| | | name: item.docName || item.name, |
| | | id: item.id, |
| | | borrower: item.borrower, |
| | | dueReturnDate: item.dueReturnDate, |
| | | })); |
| | | }); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.removeStorageSync("returnEditData"); |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // å è½½ææ¡£å表ï¼ä»
æ°å¢æ¨¡å¼éè¦ï¼ |
| | | const loadDocumentList = async () => { |
| | | try { |
| | | const res = await getDocumentList(); |
| | | if (res.code === 200) { |
| | | documentList.value = res.data || []; |
| | | } |
| | | } catch (error) { |
| | | console.error("è·åææ¡£å表失败", error); |
| | | } |
| | | }; |
| | | |
| | | // ææ¡£éæ©ç¡®è®¤ |
| | | const onDocSelect = (e) => { |
| | | form.value.borrowId = e.id; |
| | | form.value.documentationId = e.id; |
| | | displayDocName.value = e.name; |
| | | // èªå¨å¸¦åºåé
人ååºå½è¿æ¥æ |
| | | form.value.borrower = e.borrower || ""; |
| | | form.value.dueReturnDate = e.dueReturnDate || ""; |
| | | showDocPicker.value = false; |
| | | }; |
| | | |
| | | // å½è¿æ¥æç¡®è®¤ |
| | | const onReturnDateConfirm = (e) => { |
| | | const date = new Date(e.value); |
| | | form.value.returnDate = formatDate(date); |
| | | showReturnDatePicker.value = false; |
| | | }; |
| | | |
| | | // æ ¼å¼åæ¥æ |
| | | const formatDate = (date) => { |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const handleSubmit = () => { |
| | | // 妿已å½è¿ï¼ç¦æ¢æäº¤ |
| | | if (isReturned.value) { |
| | | uni.showToast({ title: "å·²å½è¿çè®°å½ä¸è½ç¼è¾", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | formRef.value |
| | | .validate() |
| | | .then(async () => { |
| | | try { |
| | | loading.value = true; |
| | | |
| | | if (isEdit.value) { |
| | | // ç¼è¾æ¨¡å¼ |
| | | const res = await reventUpdate({ |
| | | id: form.value.id, |
| | | documentationId: form.value.documentationId, |
| | | borrower: form.value.borrower, |
| | | returner: form.value.returner, |
| | | borrowStatus: form.value.borrowStatus, |
| | | returnDate: form.value.returnDate, |
| | | dueReturnDate: form.value.dueReturnDate, |
| | | remark: form.value.remark, |
| | | }); |
| | | |
| | | if (res.code === 200) { |
| | | uni.showToast({ title: "ç¼è¾æå", icon: "success" }); |
| | | setTimeout(() => { |
| | | goBack(); |
| | | }, 1500); |
| | | } else { |
| | | uni.showToast({ title: res.msg || "ç¼è¾å¤±è´¥", icon: "none" }); |
| | | } |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | const res = await returnDocument({ |
| | | borrowId: form.value.borrowId, |
| | | borrower: form.value.borrower, |
| | | returner: form.value.returner, |
| | | borrowStatus: "å½è¿", |
| | | returnDate: form.value.returnDate, |
| | | dueReturnDate: form.value.dueReturnDate, |
| | | remark: form.value.remark, |
| | | }); |
| | | |
| | | if (res.code === 200) { |
| | | uni.showToast({ title: "æ°å¢æå", icon: "success" }); |
| | | setTimeout(() => { |
| | | goBack(); |
| | | }, 1500); |
| | | } else { |
| | | uni.showToast({ title: res.msg || "æ°å¢å¤±è´¥", icon: "none" }); |
| | | } |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ title: "æä½å¤±è´¥", icon: "none" }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | // éªè¯å¤±è´¥ |
| | | }); |
| | | }; |
| | | |
| | | // 页é¢å è½½ |
| | | onLoad((options) => { |
| | | if (options.id) { |
| | | // ç¼è¾æ¨¡å¼ |
| | | isEdit.value = true; |
| | | returnId.value = options.id; |
| | | |
| | | // ä» storage è·åç¼è¾æ°æ® |
| | | const editDataStr = uni.getStorageSync("returnEditData"); |
| | | if (editDataStr) { |
| | | try { |
| | | const data = JSON.parse(editDataStr); |
| | | Object.assign(form.value, data); |
| | | returnDateValue.value = new Date(data.returnDate).getTime(); |
| | | // ç´æ¥ä½¿ç¨ä¼ éç docName æ¾ç¤º |
| | | displayDocName.value = data.docName || ""; |
| | | } catch (e) { |
| | | console.error("è§£æç¼è¾æ°æ®å¤±è´¥", e); |
| | | } |
| | | } |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ï¼å è½½ææ¡£å表并设置é»è®¤æ¥æ |
| | | loadDocumentList(); |
| | | const today = new Date(); |
| | | form.value.returnDate = formatDate(today); |
| | | returnDateValue.value = today.getTime(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .return-edit { |
| | | min-height: 100vh; |
| | | background: #f5f5f5; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <!-- 使ç¨éç¨é¡µé¢å¤´é¨ç»ä»¶ --> |
| | | <PageHeader title="å½è¿ç®¡ç" @back="goBack" /> |
| | | |
| | | <!-- æç´¢åçéåºå --> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input |
| | | class="search-text" |
| | | placeholder="请è¾å
¥åé
人æç´¢" |
| | | v-model="searchForm.borrower" |
| | | @change="getList" |
| | | clearable |
| | | /> |
| | | </view> |
| | | <view class="filter-button" @click="getList"> |
| | | <up-icon name="search" size="24" color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å½è¿å表 --> |
| | | <view class="ledger-list" v-if="returnList.length > 0"> |
| | | <view v-for="(item, index) in returnList" :key="index"> |
| | | <view class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" size="16" color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.docName || '-' }}</text> |
| | | </view> |
| | | <view class="item-tag" :class="getStatusClass(item.borrowStatus)"> |
| | | <text class="tag-text">{{ item.borrowStatus }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åé
人</text> |
| | | <text class="detail-value">{{ item.borrower || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å½è¿äºº</text> |
| | | <text class="detail-value">{{ item.returner || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å½è¿æ¥æ</text> |
| | | <text class="detail-value">{{ item.returnDate || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºå½è¿æ¥æ</text> |
| | | <text class="detail-value">{{ item.dueReturnDate || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row" v-if="item.remark"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remark }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="detail-buttons"> |
| | | <u-button |
| | | v-if="item.borrowStatus !== 'å½è¿'" |
| | | class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | @click.stop="goEdit(item)" |
| | | > |
| | | ç¼è¾ |
| | | </u-button> |
| | | <u-button |
| | | v-if="item.borrowStatus !== 'å½è¿'" |
| | | class="detail-button" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click.stop="handleDelete(item)" |
| | | > |
| | | å é¤ |
| | | </u-button> |
| | | <u-button |
| | | v-if="item.borrowStatus === 'å½è¿'" |
| | | class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | plain |
| | | @click.stop="goView(item)" |
| | | > |
| | | æ¥ç |
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-else class="no-data"> |
| | | <text>ææ å½è¿è®°å½</text> |
| | | </view> |
| | | |
| | | <!-- æµ®å¨æä½æé® --> |
| | | <view class="fab-button" @click="goAdd"> |
| | | <up-icon name="plus" size="24" color="#ffffff"></up-icon> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { getReturnListPage, deleteReturn } from "@/api/fileManagement/return"; |
| | | |
| | | // æ¥è¯¢è¡¨å |
| | | const searchForm = reactive({ |
| | | borrower: "", |
| | | }); |
| | | |
| | | // å½è¿åè¡¨æ°æ® |
| | | const returnList = ref([]); |
| | | |
| | | // å页ç¸å
³ |
| | | const pagination = reactive({ |
| | | currentPage: 1, |
| | | pageSize: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // è·åç¶ææ ·å¼ç±» |
| | | const getStatusClass = (status) => { |
| | | if (status === "å½è¿") return "tag-success"; |
| | | if (status === "åé
") return "tag-warning"; |
| | | return "tag-default"; |
| | | }; |
| | | |
| | | // å è½½å½è¿å表 |
| | | const getList = async () => { |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | |
| | | const query = { |
| | | page: -1, |
| | | size: -1, |
| | | borrower: searchForm.borrower || undefined, |
| | | }; |
| | | |
| | | try { |
| | | const res = await getReturnListPage(query); |
| | | if (res.code === 200) { |
| | | returnList.value = res.data.records || []; |
| | | pagination.total = res.data.total || 0; |
| | | } else { |
| | | uni.showToast({ title: res.msg || "è·åå½è¿å表失败", icon: "none" }); |
| | | returnList.value = []; |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·åå½è¿å表失败", icon: "none" }); |
| | | returnList.value = []; |
| | | } finally { |
| | | uni.hideLoading(); |
| | | } |
| | | }; |
| | | |
| | | // è·³è½¬å°æ°å¢é¡µé¢ |
| | | const goAdd = () => { |
| | | uni.navigateTo({ |
| | | url: "/pages/fileManagement/return/edit", |
| | | }); |
| | | }; |
| | | |
| | | // 跳转å°ç¼è¾é¡µé¢ |
| | | const goEdit = (item) => { |
| | | uni.setStorageSync("returnEditData", JSON.stringify(item)); |
| | | uni.navigateTo({ |
| | | url: `/pages/fileManagement/return/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | // è·³è½¬å°æ¥ç页é¢ï¼å·²å½è¿è®°å½ï¼ |
| | | const goView = (item) => { |
| | | uni.setStorageSync("returnEditData", JSON.stringify(item)); |
| | | uni.navigateTo({ |
| | | url: `/pages/fileManagement/return/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = (row) => { |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", |
| | | confirmText: "确认", |
| | | cancelText: "åæ¶", |
| | | success: async (res) => { |
| | | if (res.confirm) { |
| | | try { |
| | | uni.showLoading({ title: "å é¤ä¸...", mask: true }); |
| | | const result = await deleteReturn([row.id]); |
| | | if (result.code === 200) { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | } else { |
| | | uni.showToast({ title: result.msg || "å é¤å¤±è´¥", icon: "none" }); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | } finally { |
| | | uni.hideLoading(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | // æ ç¾æ ·å¼ |
| | | .item-tag { |
| | | border-radius: 4px; |
| | | padding: 2px 8px; |
| | | |
| | | &.tag-success { |
| | | background: #4caf50; |
| | | } |
| | | |
| | | &.tag-warning { |
| | | background: #ff9800; |
| | | } |
| | | |
| | | &.tag-default { |
| | | background: #9e9e9e; |
| | | } |
| | | } |
| | | |
| | | .tag-text { |
| | | font-size: 11px; |
| | | color: #ffffff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | // æé®æ ·å¼ |
| | | .detail-buttons { |
| | | padding: 12px 0; |
| | | display: flex; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .detail-button { |
| | | flex: 1; |
| | | } |
| | | </style> |
| | |
| | | <scroll-view class="scroll" scroll-y> |
| | | <!-- é¡¶é¨ Bannerï¼æ¾å
¥æ»å¨åºåï¼é页é¢ä¸èµ·æ»å¨ï¼ä¸åºå®å¨é¡¶é¨ --> |
| | | <view class="hero-section"> |
| | | <view class="bg-img"> |
| | | <view class="hero-content"> |
| | | <view class="hero-ornaments"> |
| | | <view class="hero-glow glow-left" /> |
| | | <view class="hero-glow glow-right" /> |
| | | <view class="hero-mist mist-top" /> |
| | | <view class="hero-mist mist-bottom" /> |
| | | <view class="hero-curve curve-main" /> |
| | | <view class="hero-curve curve-sub" /> |
| | | <view class="hero-banner"> |
| | | <view class="hero-top"> |
| | | <view class="hero-copy"> |
| | | <view class="hero-badge">ç»è¥çæ¿</view> |
| | | <text class="hero-title">{{ heroTitle }}</text> |
| | | <text class="hero-subtitle">{{ heroSubtitle }}</text> |
| | | <view class="hero-meta"> |
| | | <text |
| | | v-for="item in heroMetaItems" |
| | | :key="item" |
| | | class="hero-meta-item" |
| | | > |
| | | {{ item }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <view class="hero-avatar"> |
| | | <text class="hero-avatar-text">{{ heroInitial }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="hero-wave"></view> |
| | | <view class="hero-panels"> |
| | | <view |
| | | v-for="item in heroMetrics" |
| | | :key="item.label" |
| | | class="hero-panel" |
| | | > |
| | | <text class="hero-panel-label">{{ item.label }}</text> |
| | | <text class="hero-panel-value">{{ item.value }}</text> |
| | | <text class="hero-panel-hint">{{ item.hint }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å¿«æ·å
¥å£ --> |
| | | <view class="quick-section"> |
| | | <view v-if="quickTools.length" class="quick-section"> |
| | | <up-grid :border="false" col="4"> |
| | | <up-grid-item |
| | | v-for="item in quickTools" |
| | |
| | | </view> |
| | | |
| | | <!-- æ°æ®æ»è§ --> |
| | | <view class="section"> |
| | | <view v-if="hasOverviewSection" class="section"> |
| | | <view class="section-header"> |
| | | <view class="section-title"> |
| | | <view class="title-bar" /> |
| | |
| | | </view> |
| | | |
| | | <view v-show="overviewExpanded" class="overview"> |
| | | <view class="overview-card sales"> |
| | | <view v-if="canShowSalesOverview" class="overview-card sales"> |
| | | <view class="card-left"> |
| | | <text class="card-title">é宿°æ®</text> |
| | | <view class="card-metrics"> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="overview-card purchase"> |
| | | <view v-if="canShowPurchaseOverview" class="overview-card purchase"> |
| | | <view class="card-left"> |
| | | <text class="card-title">éè´æ°æ®</text> |
| | | <view class="card-metrics"> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="overview-card stock"> |
| | | <view v-if="canShowStockOverview" class="overview-card stock"> |
| | | <view class="card-left"> |
| | | <text class="card-title">åºåæ°æ®</text> |
| | | <view class="card-metrics"> |
| | |
| | | </view> |
| | | |
| | | <!-- 客æ·ååéé¢åæ --> |
| | | <view class="section"> |
| | | <view v-if="canShowContractAnalysis" class="section"> |
| | | <view class="section-header"> |
| | | <view class="section-title"> |
| | | <view class="title-bar" /> |
| | |
| | | import { analysisCustomerContractAmounts, getBusiness } from "@/api/viewIndex"; |
| | | import { createVersionUpgradeChecker } from "@/utils/versionUpgrade"; |
| | | import DownloadProgressMask from "@/components/DownloadProgressMask.vue"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | |
| | | const imgNum1 = "/static/images/index/num1.png"; |
| | | const imgNum2 = "/static/images/index/num2.png"; |
| | | const imgNum3 = "/static/images/index/num3.png"; |
| | | |
| | | const quickTools = [ |
| | | const userStore = useUserStore(); |
| | | |
| | | const quickToolSource = [ |
| | | { |
| | | label: "ç产æ¥å·¥", |
| | | icon: "/static/images/icon/shengchanbaogong.svg", |
| | |
| | | // route: "/pages/equipmentManagement/repair/index", |
| | | // }, |
| | | ]; |
| | | const quickTools = ref([...quickToolSource]); |
| | | const allowedMenuTitles = ref(new Set()); |
| | | |
| | | const isCanvas2d = ref(false); |
| | | |
| | |
| | | uni.showToast({ title: "æ´å¤åè½å¾
æ¥å
¥", icon: "none" }); |
| | | } |
| | | |
| | | |
| | | function filterQuickToolsByRoutes() { |
| | | const routers = userStore.routers || []; |
| | | |
| | | if (!routers || routers.length === 0) { |
| | | allowedMenuTitles.value = new Set(); |
| | | quickTools.value = [...quickToolSource]; |
| | | return; |
| | | } |
| | | |
| | | const titles = new Set(); |
| | | const collectMenuTitles = (routes) => { |
| | | if (!Array.isArray(routes)) return; |
| | | routes.forEach((route) => { |
| | | if (route.meta && route.meta.title) { |
| | | titles.add(route.meta.title); |
| | | } |
| | | if (route.children && route.children.length > 0) { |
| | | collectMenuTitles(route.children); |
| | | } |
| | | }); |
| | | }; |
| | | collectMenuTitles(routers); |
| | | allowedMenuTitles.value = titles; |
| | | |
| | | quickTools.value = quickToolSource.filter((item) => |
| | | titles.has(item.label) |
| | | ); |
| | | } |
| | | |
| | | function hasAnyPermission(titles) { |
| | | const titleSet = allowedMenuTitles.value; |
| | | if (!titleSet || titleSet.size === 0) return true; |
| | | return titles.some((title) => titleSet.has(title)); |
| | | } |
| | | |
| | | const canShowSalesOverview = computed(() => hasAnyPermission(["éå®å°è´¦"])); |
| | | const canShowPurchaseOverview = computed(() => hasAnyPermission(["éè´å°è´¦"])); |
| | | const canShowStockOverview = computed(() => hasAnyPermission(["åºå管ç"])); |
| | | const hasOverviewSection = computed( |
| | | () => |
| | | canShowSalesOverview.value || |
| | | canShowPurchaseOverview.value || |
| | | canShowStockOverview.value |
| | | ); |
| | | const canShowContractAnalysis = computed(() => |
| | | hasAnyPermission(["éå®å°è´¦", "å®¢æ·æ¡£æ¡", "客æ·å¾æ¥"]) |
| | | ); |
| | | |
| | | const userDisplayName = computed( |
| | | () => userStore.nickName |
| | | ); |
| | | const heroInitial = computed(() => userDisplayName.value.slice(0, 1).toUpperCase()); |
| | | const heroTitle = computed(() => `ä½ å¥½ï¼${userDisplayName.value}`); |
| | | const heroSubtitle = computed( |
| | | () => userStore.currentFactoryName || "å½åè´¦å·å·²è¿å
¥ä¸å¡é¦é¡µ" |
| | | ); |
| | | const heroMetaItems = computed(() => { |
| | | const items = []; |
| | | if (userStore.roleName) items.push(userStore.roleName); |
| | | if (userStore.currentLoginTime) items.push(`ç»å½äº ${userStore.currentLoginTime}`); |
| | | if (!items.length) items.push("å½åä¸å¡æ¦è§"); |
| | | return items; |
| | | }); |
| | | const visibleSectionCount = computed(() => { |
| | | let count = 0; |
| | | if (quickTools.value.length > 0) count += 1; |
| | | if (hasOverviewSection.value) count += 1; |
| | | if (canShowContractAnalysis.value) count += 1; |
| | | return count; |
| | | }); |
| | | const heroMetrics = computed(() => { |
| | | const items = []; |
| | | |
| | | if (canShowSalesOverview.value) { |
| | | items.push({ |
| | | label: "éå®", |
| | | value: overviewCards.value.sales.today, |
| | | hint: "æ¬æè¥ä¸é¢", |
| | | }); |
| | | } |
| | | if (canShowPurchaseOverview.value) { |
| | | items.push({ |
| | | label: "éè´", |
| | | value: overviewCards.value.purchase.today, |
| | | hint: "æ¬æéè´é¢", |
| | | }); |
| | | } |
| | | if (canShowStockOverview.value) { |
| | | items.push({ |
| | | label: "åºå", |
| | | value: overviewCards.value.stock.today, |
| | | hint: "å½ååºåé", |
| | | }); |
| | | } |
| | | if (canShowContractAnalysis.value && items.length < 3) { |
| | | items.push({ |
| | | label: "åå", |
| | | value: contractSummaryView.value.sumText, |
| | | hint: "客æ·ååé¢", |
| | | }); |
| | | } |
| | | if (items.length < 3) { |
| | | items.push({ |
| | | label: "å¿«æ·", |
| | | value: String(quickTools.value.length), |
| | | hint: "å¯ç¨å
¥å£", |
| | | }); |
| | | } |
| | | if (items.length < 3) { |
| | | items.push({ |
| | | label: "模å", |
| | | value: String(visibleSectionCount.value), |
| | | hint: "å¯è§æ¿å", |
| | | }); |
| | | } |
| | | |
| | | return items.slice(0, 3); |
| | | }); |
| | | |
| | | function getByPath(obj, path) { |
| | | if (!obj || !path) return undefined; |
| | |
| | | async function loadHome() { |
| | | chartReady.value = false; |
| | | try { |
| | | const [bRes, cRes] = await Promise.all([getBusiness(), analysisCustomerContractAmounts()]); |
| | | const businessPromise = hasOverviewSection.value |
| | | ? getBusiness() |
| | | : Promise.resolve({ data: {} }); |
| | | const contractPromise = canShowContractAnalysis.value |
| | | ? analysisCustomerContractAmounts() |
| | | : Promise.resolve({ data: { item: [], sum: "0", chain: "0", yny: "0" } }); |
| | | const [bRes, cRes] = await Promise.all([businessPromise, contractPromise]); |
| | | businessRaw.value = bRes?.data || {}; |
| | | const cData = cRes?.data || {}; |
| | | contractSummary.value = { |
| | |
| | | isCanvas2d.value = false; |
| | | } |
| | | triggerVersionCheck("onMounted"); |
| | | loadHome(); |
| | | userStore |
| | | .getRouters() |
| | | .then(() => { |
| | | filterQuickToolsByRoutes(); |
| | | loadHome(); |
| | | }) |
| | | .catch(() => { |
| | | filterQuickToolsByRoutes(); |
| | | loadHome(); |
| | | }); |
| | | }); |
| | | |
| | | onShow(() => { |
| | | triggerVersionCheck("onShow"); |
| | | filterQuickToolsByRoutes(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | } |
| | | } |
| | | .hero-section { |
| | | margin: 0 12px; |
| | | margin-bottom: 12px; |
| | | animation: fadeInUp 0.6s ease-out 0.1s both; |
| | | margin: 0 14px 12px; |
| | | animation: fadeInUp 0.6s ease-out 0.1s both; |
| | | } |
| | | |
| | | .bg-img { |
| | | width: 100%; |
| | | height: 10.25rem; |
| | | background: |
| | | linear-gradient(135deg, rgba(234, 245, 255, 0.98) 0%, rgba(220, 239, 255, 0.94) 42%, rgba(244, 250, 255, 0.96) 100%), |
| | | url("/static/images/banner/backview.png") center/cover no-repeat; |
| | | border-radius: 18px; |
| | | position: relative; |
| | | overflow: hidden; |
| | | box-shadow: 0 12px 30px rgba(118, 154, 186, 0.16); |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | inset: 0; |
| | | background: |
| | | radial-gradient(circle at 14% 22%, rgba(255, 255, 255, 0.95) 0, rgba(255, 255, 255, 0) 28%), |
| | | radial-gradient(circle at 84% 18%, rgba(191, 226, 255, 0.7) 0, rgba(191, 226, 255, 0) 26%), |
| | | linear-gradient(180deg, rgba(255, 255, 255, 0.46) 0%, rgba(255, 255, 255, 0.16) 42%, rgba(206, 229, 247, 0.22) 100%); |
| | | pointer-events: none; |
| | | } |
| | | |
| | | &::after { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 18%; |
| | | bottom: -44px; |
| | | width: 64%; |
| | | height: 88px; |
| | | background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.72) 0%, rgba(255, 255, 255, 0) 72%); |
| | | border-radius: 50%; |
| | | filter: blur(10px); |
| | | pointer-events: none; |
| | | } |
| | | .hero-banner { |
| | | position: relative; |
| | | overflow: hidden; |
| | | border-radius: 18px; |
| | | padding: 18px 16px 16px; |
| | | min-height: 182px; |
| | | background: |
| | | linear-gradient(135deg, rgba(22, 74, 170, 0.92) 0%, rgba(33, 115, 185, 0.88) 48%, rgba(18, 156, 144, 0.82) 100%), |
| | | url("/static/images/banner/backview.png") center right / cover no-repeat; |
| | | box-shadow: 0 14px 34px rgba(29, 78, 137, 0.2); |
| | | border: 1px solid rgba(255, 255, 255, 0.18); |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | inset: 0; |
| | | background: |
| | | linear-gradient(120deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.02) 32%, rgba(255, 255, 255, 0) 60%), |
| | | radial-gradient(circle at top right, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0) 34%); |
| | | pointer-events: none; |
| | | } |
| | | |
| | | &::after { |
| | | content: ""; |
| | | position: absolute; |
| | | right: -28px; |
| | | bottom: -34px; |
| | | width: 156px; |
| | | height: 156px; |
| | | border-radius: 50%; |
| | | background: radial-gradient(circle, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0) 72%); |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | |
| | | .hero-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | padding: 16px 16px 14px; |
| | | height: 100%; |
| | | backdrop-filter: blur(2px); |
| | | .hero-top { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .hero-ornaments { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | .hero-copy { |
| | | min-width: 0; |
| | | flex: 1; |
| | | } |
| | | |
| | | .hero-glow { |
| | | position: absolute; |
| | | border-radius: 50%; |
| | | filter: blur(4px); |
| | | background: radial-gradient(circle, rgba(255, 255, 255, 0.96) 0%, rgba(255, 255, 255, 0) 72%); |
| | | opacity: 0.9; |
| | | .hero-badge { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | height: 26px; |
| | | padding: 0 10px; |
| | | border-radius: 999px; |
| | | background: rgba(255, 255, 255, 0.16); |
| | | border: 1px solid rgba(255, 255, 255, 0.18); |
| | | color: rgba(255, 255, 255, 0.92); |
| | | font-size: 11px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .hero-glow.glow-left { |
| | | left: -10px; |
| | | top: 8px; |
| | | width: 120px; |
| | | height: 120px; |
| | | .hero-title { |
| | | display: block; |
| | | margin-top: 12px; |
| | | color: #ffffff; |
| | | font-size: 24px; |
| | | font-weight: 700; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .hero-glow.glow-right { |
| | | right: -20px; |
| | | top: 4px; |
| | | width: 144px; |
| | | height: 144px; |
| | | background: radial-gradient(circle, rgba(207, 234, 255, 0.92) 0%, rgba(207, 234, 255, 0) 74%); |
| | | .hero-subtitle { |
| | | display: block; |
| | | margin-top: 8px; |
| | | color: rgba(255, 255, 255, 0.84); |
| | | font-size: 13px; |
| | | line-height: 1.45; |
| | | } |
| | | |
| | | .hero-mist { |
| | | position: absolute; |
| | | border-radius: 999px; |
| | | background: linear-gradient(90deg, rgba(255, 255, 255, 0.52), rgba(255, 255, 255, 0.08)); |
| | | border: 1px solid rgba(255, 255, 255, 0.34); |
| | | backdrop-filter: blur(10px); |
| | | box-shadow: 0 10px 24px rgba(154, 190, 219, 0.14); |
| | | .hero-meta { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | margin-top: 12px; |
| | | } |
| | | |
| | | .hero-mist.mist-top { |
| | | left: 18px; |
| | | top: 20px; |
| | | width: 112px; |
| | | height: 18px; |
| | | .hero-meta-item { |
| | | padding: 3px 10px; |
| | | border-radius: 999px; |
| | | background: rgba(255, 255, 255, 0.12); |
| | | color: rgba(255, 255, 255, 0.86); |
| | | font-size: 11px; |
| | | line-height: 18px; |
| | | } |
| | | |
| | | .hero-mist.mist-bottom { |
| | | left: 18px; |
| | | top: 48px; |
| | | width: 72px; |
| | | height: 10px; |
| | | opacity: 0.82; |
| | | .hero-avatar { |
| | | position: relative; |
| | | z-index: 1; |
| | | width: 52px; |
| | | height: 52px; |
| | | flex: 0 0 52px; |
| | | border-radius: 16px; |
| | | background: rgba(255, 255, 255, 0.18); |
| | | border: 1px solid rgba(255, 255, 255, 0.22); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3); |
| | | } |
| | | |
| | | .hero-curve { |
| | | position: absolute; |
| | | border-radius: 999px; |
| | | border: 2px solid rgba(255, 255, 255, 0.72); |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.12)); |
| | | box-shadow: |
| | | 0 10px 26px rgba(154, 190, 219, 0.16), |
| | | inset 0 1px 0 rgba(255, 255, 255, 0.8); |
| | | backdrop-filter: blur(10px); |
| | | .hero-avatar-text { |
| | | color: #ffffff; |
| | | font-size: 20px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .hero-curve.curve-main { |
| | | right: 18px; |
| | | bottom: 22px; |
| | | width: 176px; |
| | | height: 84px; |
| | | transform: rotate(-9deg); |
| | | border-top-left-radius: 90px; |
| | | border-bottom-right-radius: 90px; |
| | | opacity: 1; |
| | | .hero-panels { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | margin-top: 18px; |
| | | } |
| | | |
| | | .hero-curve.curve-sub { |
| | | right: 96px; |
| | | bottom: 60px; |
| | | width: 104px; |
| | | height: 40px; |
| | | transform: rotate(-9deg); |
| | | border-top-left-radius: 60px; |
| | | border-bottom-right-radius: 60px; |
| | | opacity: 0.9; |
| | | .hero-panel { |
| | | min-width: 0; |
| | | padding: 12px 10px; |
| | | border-radius: 14px; |
| | | background: rgba(11, 25, 48, 0.18); |
| | | border: 1px solid rgba(255, 255, 255, 0.14); |
| | | backdrop-filter: blur(10px); |
| | | } |
| | | |
| | | .hero-wave { |
| | | height: 1.1rem; |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(244, 249, 253, 0.96) 100%); |
| | | margin-top: -1px; |
| | | position: relative; |
| | | .hero-panel-label { |
| | | display: block; |
| | | color: rgba(255, 255, 255, 0.7); |
| | | font-size: 11px; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .hero-panel-value { |
| | | display: block; |
| | | margin-top: 8px; |
| | | color: #ffffff; |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | line-height: 1.2; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .hero-panel-hint { |
| | | display: block; |
| | | margin-top: 6px; |
| | | color: rgba(255, 255, 255, 0.72); |
| | | font-size: 11px; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .safe-top { |
| | |
| | | |
| | | <script setup> |
| | | import { onMounted, reactive, ref } from "vue"; |
| | | import { OA_WORKBENCH_ITEMS } from "@/config/oaWorkbench.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | |
| | | { icon: "/static/images/icon/baojiaguanli.svg", label: "æ¥ä»·å®¡æ¹" }, |
| | | { icon: "/static/images/icon/fahuoguanli.svg", label: "å货审æ¹" }, |
| | | ], |
| | | "OAåå
¬": OA_WORKBENCH_ITEMS.map(item => ({ ...item })), |
| | | }; |
| | | |
| | | // å¤ç常ç¨åè½ç¹å» |
| | | const handleCommonItemClick = item => { |
| | | if (item.path) { |
| | | uni.navigateTo({ url: item.path }); |
| | | return; |
| | | } |
| | | const url = routeMapping[item.label]; |
| | | if (url) { |
| | | uni.navigateTo({ url }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="attachment-page"> |
| | | <!-- 页é¢å¤´é¨ --> |
| | | <PageHeader :title="`æ¥çéä»¶ - ${taskInfo?.taskName || ''}`" |
| | | @back="goBack" /> |
| | | <!-- 页é¢å
容 --> |
| | | <view class="attachment-content"> |
| | | <!-- åç±»æ ç¾é¡µ --> |
| | | <view class="attachment-tabs"> |
| | | <view class="tab-item" |
| | | :class="{ active: currentViewType === 'before' }" |
| | | @click="switchViewType('before')"> |
| | | ç产å ({{ getAttachmentsByType(0).length }}) |
| | | </view> |
| | | <view class="tab-item" |
| | | :class="{ active: currentViewType === 'after' }" |
| | | @click="switchViewType('after')"> |
| | | çäº§ä¸ ({{ getAttachmentsByType(1).length }}) |
| | | </view> |
| | | <view class="tab-item" |
| | | :class="{ active: currentViewType === 'issue' }" |
| | | @click="switchViewType('issue')"> |
| | | ç产å ({{ getAttachmentsByType(2).length }}) |
| | | </view> |
| | | </view> |
| | | <!-- å½ååç±»çéä»¶å表 --> |
| | | <view class="attachment-list-container"> |
| | | <view v-if="getCurrentViewAttachments().length > 0" |
| | | class="attachment-list"> |
| | | <view v-for="(file, index) in getCurrentViewAttachments()" |
| | | :key="index" |
| | | class="attachment-item" |
| | | @click="previewAttachment(file)"> |
| | | <view class="attachment-preview-container"> |
| | | <image v-if="isImageFile(file)" |
| | | :src="file.url || file.downloadUrl" |
| | | class="attachment-preview" |
| | | mode="aspectFill" /> |
| | | <view v-else |
| | | class="attachment-video-preview"> |
| | | <u-icon name="video" |
| | | size="40" |
| | | color="#409eff"></u-icon> |
| | | <text class="video-text">è§é¢</text> |
| | | </view> |
| | | </view> |
| | | <view class="attachment-info"> |
| | | <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || 'éä»¶' }}</text> |
| | | <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="attachment-empty"> |
| | | <text class="empty-text">该åç±»ææ éä»¶</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- è§é¢é¢è§å¼¹çª --> |
| | | <view v-if="showVideoDialog" |
| | | class="video-modal-overlay" |
| | | @click="closeVideoPreview"> |
| | | <view class="video-modal-container" |
| | | @click.stop> |
| | | <view class="video-modal-header"> |
| | | <text class="video-modal-title">{{ currentVideoFile?.originalFilename || 'è§é¢é¢è§' }}</text> |
| | | <view class="close-btn-video" |
| | | @click="closeVideoPreview"> |
| | | <u-icon name="close" |
| | | size="20" |
| | | color="#fff"></u-icon> |
| | | </view> |
| | | </view> |
| | | <view class="video-modal-body"> |
| | | <video v-if="currentVideoFile" |
| | | :src="currentVideoFile.url || currentVideoFile.downloadUrl" |
| | | class="video-player" |
| | | controls |
| | | autoplay |
| | | @error="handleVideoError"></video> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import config from "@/config"; |
| | | |
| | | // ä»»å¡ä¿¡æ¯ |
| | | const taskInfo = ref(null); |
| | | |
| | | // éä»¶å表 |
| | | const attachmentList = ref([]); |
| | | |
| | | // å½åæ¥çç±»å |
| | | const currentViewType = ref("before"); // 'before', 'after', 'issue' |
| | | |
| | | // è§é¢é¢è§ç¸å
³ç¶æ |
| | | const showVideoDialog = ref(false); |
| | | const currentVideoFile = ref(null); |
| | | |
| | | // æä»¶è®¿é®åºç¡å |
| | | const filePreviewBase = config.fileUrl; |
| | | |
| | | // 页é¢å è½½ |
| | | onLoad(options => { |
| | | if (options.taskInfo) { |
| | | try { |
| | | taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo)); |
| | | loadAttachments(); |
| | | } catch (e) { |
| | | console.error("è§£æä»»å¡ä¿¡æ¯å¤±è´¥:", e); |
| | | uni.showToast({ |
| | | title: "å 载失败", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | // å è½½éä»¶æ°æ® |
| | | const loadAttachments = () => { |
| | | const task = taskInfo.value; |
| | | if (!task) return; |
| | | |
| | | attachmentList.value = []; |
| | | |
| | | // åç«¯åæ¾åæ®µ (VOä¼å
) |
| | | const beforeList = Array.isArray(task?.commonFileListBeforeVO) |
| | | ? task.commonFileListBeforeVO |
| | | : Array.isArray(task?.commonFileListBefore) |
| | | ? task.commonFileListBefore |
| | | : []; |
| | | |
| | | const duringList = Array.isArray(task?.commonFileListVO) |
| | | ? task.commonFileListVO |
| | | : Array.isArray(task?.commonFileListAfter) |
| | | ? task.commonFileListAfter |
| | | : []; // å
¼å®¹æ§é»è¾æå½åä¸ä¸è´ |
| | | |
| | | const afterList = Array.isArray(task?.commonFileListAfterVO) |
| | | ? task.commonFileListAfterVO |
| | | : Array.isArray(task?.commonFileListIssue) |
| | | ? task.commonFileListIssue |
| | | : []; |
| | | |
| | | // 妿 VO 齿²¡æï¼å°è¯ä» commonFileList è¿æ»¤ |
| | | const allList = Array.isArray(task?.commonFileList) |
| | | ? task.commonFileList |
| | | : []; |
| | | |
| | | const finalBefore = |
| | | beforeList.length > 0 ? beforeList : allList.filter(f => f?.type === 10); |
| | | const finalDuring = |
| | | duringList.length > 0 ? duringList : allList.filter(f => f?.type === 11); |
| | | const finalAfter = |
| | | afterList.length > 0 ? afterList : allList.filter(f => f?.type === 12); |
| | | |
| | | const mapToViewFile = (file, viewType) => { |
| | | // å
¼å®¹ previewURL, previewUrl, url, downloadURL, downloadUrl |
| | | const rawUrl = |
| | | file?.previewURL || |
| | | file?.previewUrl || |
| | | file?.url || |
| | | file?.downloadURL || |
| | | file?.downloadUrl || |
| | | ""; |
| | | const u = normalizeFileUrl(rawUrl); |
| | | |
| | | return { |
| | | ...file, |
| | | type: viewType, |
| | | name: |
| | | file?.originalFilename || file?.bucketFilename || file?.name || "éä»¶", |
| | | bucketFilename: file?.bucketFilename || file?.name, |
| | | originalFilename: file?.originalFilename || file?.name, |
| | | url: u, |
| | | downloadUrl: u, |
| | | size: file?.byteSize || file?.size || 0, |
| | | }; |
| | | }; |
| | | |
| | | attachmentList.value.push(...finalBefore.map(f => mapToViewFile(f, 0))); |
| | | attachmentList.value.push(...finalDuring.map(f => mapToViewFile(f, 1))); |
| | | attachmentList.value.push(...finalAfter.map(f => mapToViewFile(f, 2))); |
| | | }; |
| | | |
| | | // å°å端è¿åçæä»¶å°åè§èæå¯è®¿é®URL |
| | | const normalizeFileUrl = rawUrl => { |
| | | try { |
| | | if (!rawUrl || typeof rawUrl !== "string") return ""; |
| | | const url = rawUrl.trim(); |
| | | if (!url) return ""; |
| | | if (/^https?:\/\//i.test(url)) return url; |
| | | if (url.startsWith("/")) return `${filePreviewBase}${url}`; |
| | | |
| | | // Windows path -> web path |
| | | if (/^[a-zA-Z]:\\/.test(url)) { |
| | | const normalized = url.replace(/\\/g, "/"); |
| | | const idx = normalized.indexOf("/prod/"); |
| | | if (idx >= 0) { |
| | | const relative = normalized.slice(idx + "/prod/".length); |
| | | return `${filePreviewBase}/${relative}`; |
| | | } |
| | | return normalized; |
| | | } |
| | | |
| | | return `${filePreviewBase}/${url.replace(/^\//, "")}`; |
| | | } catch (e) { |
| | | return rawUrl || ""; |
| | | } |
| | | }; |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // 忢æ¥çç±»å |
| | | const switchViewType = type => { |
| | | currentViewType.value = type; |
| | | }; |
| | | |
| | | // æ ¹æ®typeè·å对åºåç±»çéä»¶ |
| | | const getAttachmentsByType = typeValue => { |
| | | return attachmentList.value.filter(file => file.type === typeValue) || []; |
| | | }; |
| | | |
| | | // è·åå½åæ¥çç±»åçéä»¶ |
| | | const getCurrentViewAttachments = () => { |
| | | switch (currentViewType.value) { |
| | | case "before": |
| | | return getAttachmentsByType(0); |
| | | case "after": |
| | | return getAttachmentsByType(1); |
| | | case "issue": |
| | | return getAttachmentsByType(2); |
| | | default: |
| | | return []; |
| | | } |
| | | }; |
| | | |
| | | // 夿æ¯å¦ä¸ºå¾çæä»¶ |
| | | const isImageFile = file => { |
| | | if (file.contentType && file.contentType.startsWith("image/")) { |
| | | return true; |
| | | } |
| | | if (file.type === "image") return true; |
| | | |
| | | const name = file.bucketFilename || file.originalFilename || file.name || ""; |
| | | const ext = name.split(".").pop()?.toLowerCase(); |
| | | return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext); |
| | | }; |
| | | |
| | | // é¢è§éä»¶ |
| | | const previewAttachment = file => { |
| | | if (isImageFile(file)) { |
| | | const imageUrls = getCurrentViewAttachments() |
| | | .filter(f => isImageFile(f)) |
| | | .map(f => f.url || f.downloadUrl); |
| | | |
| | | uni.previewImage({ |
| | | urls: imageUrls, |
| | | current: file.url || file.downloadUrl, |
| | | }); |
| | | } else { |
| | | showVideoPreview(file); |
| | | } |
| | | }; |
| | | |
| | | // æ¾ç¤ºè§é¢é¢è§ |
| | | const showVideoPreview = file => { |
| | | currentVideoFile.value = file; |
| | | showVideoDialog.value = true; |
| | | }; |
| | | |
| | | // å
³éè§é¢é¢è§ |
| | | const closeVideoPreview = () => { |
| | | showVideoDialog.value = false; |
| | | currentVideoFile.value = null; |
| | | }; |
| | | |
| | | // è§é¢ææ¾é误å¤ç |
| | | const handleVideoError = () => { |
| | | uni.showToast({ |
| | | title: "è§é¢ææ¾å¤±è´¥", |
| | | icon: "error", |
| | | }); |
| | | }; |
| | | |
| | | // æ ¼å¼åæä»¶å¤§å° |
| | | const formatFileSize = size => { |
| | | if (!size) return ""; |
| | | if (size < 1024) return size + "B"; |
| | | if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB"; |
| | | return (size / (1024 * 1024)).toFixed(1) + "MB"; |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .attachment-page { |
| | | min-height: 100vh; |
| | | background-color: #f5f5f5; |
| | | } |
| | | |
| | | .attachment-content { |
| | | padding: 15px; |
| | | } |
| | | |
| | | /* æ ç¾é¡µæ ·å¼ */ |
| | | .attachment-tabs { |
| | | display: flex; |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | margin-bottom: 15px; |
| | | padding: 4px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .tab-item { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 12px 8px; |
| | | font-size: 14px; |
| | | color: #666; |
| | | border-radius: 8px; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .tab-item.active { |
| | | background: #409eff; |
| | | color: #fff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | /* éä»¶åè¡¨æ ·å¼ */ |
| | | .attachment-list-container { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 15px; |
| | | min-height: 400px; |
| | | } |
| | | |
| | | .attachment-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 15px; |
| | | } |
| | | |
| | | .attachment-item { |
| | | width: calc(33.33% - 10px); |
| | | background: #f8f9fa; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .attachment-item:active { |
| | | transform: scale(0.98); |
| | | } |
| | | |
| | | .attachment-preview-container { |
| | | width: 100%; |
| | | height: 120px; |
| | | background: #e9ecef; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .attachment-preview { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .attachment-video-preview { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .video-text { |
| | | font-size: 12px; |
| | | color: #666; |
| | | } |
| | | |
| | | .attachment-info { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .attachment-name { |
| | | font-size: 12px; |
| | | color: #333; |
| | | display: block; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .attachment-size { |
| | | font-size: 10px; |
| | | color: #999; |
| | | } |
| | | |
| | | /* ç©ºç¶ææ ·å¼ */ |
| | | .attachment-empty { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 80px 20px; |
| | | color: #999; |
| | | } |
| | | |
| | | .empty-text { |
| | | margin-top: 15px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | /* è§é¢å¼¹çªæ ·å¼ */ |
| | | .video-modal-overlay { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.9); |
| | | z-index: 10000; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .video-modal-container { |
| | | width: 100%; |
| | | max-width: 800px; |
| | | background: #000; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .video-modal-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 15px 20px; |
| | | background: #1a1a1a; |
| | | } |
| | | |
| | | .video-modal-title { |
| | | font-size: 16px; |
| | | color: #fff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .close-btn-video { |
| | | width: 32px; |
| | | height: 32px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background: rgba(255, 255, 255, 0.1); |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | .video-modal-body { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .video-player { |
| | | width: 100%; |
| | | height: 400px; |
| | | border-radius: 8px; |
| | | } |
| | | </style> |
| | |
| | | > |
| | | <view class="popup-content"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">ä¸ä¼ </text> |
| | | <text class="popup-title">å·¡æ£è®°å½ä¸ä¼ </text> |
| | | </view> |
| | | |
| | | <view class="upload-container"> |
| | | <!-- å¼å¸¸ç¶æéæ© --> |
| | | <view class="form-container"> |
| | | <view class="title">ç产å</view> |
| | | <u-upload |
| | | :fileList="beforeModelValue" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | name="before" |
| | | multiple |
| | | :maxCount="10" |
| | | :maxSize="5 * 1024 * 1024" |
| | | accept="image/*" |
| | | :previewFullImage="true" |
| | | ></u-upload> |
| | | <view class="title">å·¡æ£ç¶æ</view> |
| | | <view class="exception-section"> |
| | | <view class="exception-options"> |
| | | <view |
| | | class="exception-option" |
| | | :class="{ active: hasException === false }" |
| | | @click="setExceptionStatus(false)" |
| | | > |
| | | <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon> |
| | | <text class="option-text">æ£å¸¸</text> |
| | | </view> |
| | | <view |
| | | class="exception-option" |
| | | :class="{ active: hasException === true }" |
| | | @click="setExceptionStatus(true)" |
| | | > |
| | | <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon> |
| | | <text class="option-text">åå¨å¼å¸¸</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å¼å¸¸æè¿°ï¼ä»
å¨å¼å¸¸æ¶æ¾ç¤ºï¼ --> |
| | | <view class="form-container" v-if="hasException === true"> |
| | | <view class="title">å¼å¸¸æè¿°</view> |
| | | <u-input |
| | | v-model="exceptionDescription" |
| | | type="textarea" |
| | | :maxlength="500" |
| | | placeholder="请æè¿°å¼å¸¸æ
åµ..." |
| | | :customStyle="{ padding: '10px', backgroundColor: '#f5f5f5' }" |
| | | /> |
| | | </view> |
| | | |
| | | <view class="form-container"> |
| | | <view class="title">ç产å</view> |
| | | <u-upload |
| | | :fileList="afterModelValue" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | name="after" |
| | | multiple |
| | | :maxCount="10" |
| | | :maxSize="5 * 1024 * 1024" |
| | | accept="image/*" |
| | | :previewFullImage="true" |
| | | ></u-upload> |
| | | </view> |
| | | |
| | | <view class="form-container"> |
| | | <view class="title">ç产é®é¢</view> |
| | | <u-upload |
| | | :fileList="issueModelValue" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | name="issue" |
| | | multiple |
| | | :maxCount="10" |
| | | :maxSize="5 * 1024 * 1024" |
| | | accept="image/*" |
| | | :previewFullImage="true" |
| | | ></u-upload> |
| | | <!-- ä¸ä¼ åºåï¼ä»
å¨å¼å¸¸æ¶æ¾ç¤ºï¼ --> |
| | | <template v-if="hasException === true"> |
| | | <view class="form-container"> |
| | | <view class="title">ç产å</view> |
| | | <u-upload |
| | | :fileList="beforeModelValue" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | name="before" |
| | | multiple |
| | | :maxCount="10" |
| | | :maxSize="5 * 1024 * 1024" |
| | | accept="image/*" |
| | | :previewFullImage="true" |
| | | ></u-upload> |
| | | </view> |
| | | |
| | | <view class="form-container"> |
| | | <view class="title">ç产å</view> |
| | | <u-upload |
| | | :fileList="afterModelValue" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | name="after" |
| | | multiple |
| | | :maxCount="10" |
| | | :maxSize="5 * 1024 * 1024" |
| | | accept="image/*" |
| | | :previewFullImage="true" |
| | | ></u-upload> |
| | | </view> |
| | | |
| | | <view class="form-container"> |
| | | <view class="title">ç产é®é¢</view> |
| | | <u-upload |
| | | :fileList="issueModelValue" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | name="issue" |
| | | multiple |
| | | :maxCount="10" |
| | | :maxSize="5 * 1024 * 1024" |
| | | accept="image/*" |
| | | :previewFullImage="true" |
| | | ></u-upload> |
| | | </view> |
| | | </template> |
| | | |
| | | <!-- æ£å¸¸ç¶ææç¤º --> |
| | | <view class="form-container normal-tip" v-if="hasException === false"> |
| | | <u-icon name="info-circle" size="40" color="#52c41a"></u-icon> |
| | | <text class="tip-text">设å¤è¿è¡æ£å¸¸ï¼æ éä¸ä¼ ç
§ç</text> |
| | | </view> |
| | | </view> |
| | | |
| | |
| | | const afterModelValue = ref([]) |
| | | const issueModelValue = ref([]) |
| | | const infoData = ref(null) |
| | | |
| | | // å¼å¸¸ç¶æï¼null=æªéæ©, false=æ£å¸¸, true=å¼å¸¸ |
| | | const hasException = ref(null) |
| | | // å¼å¸¸æè¿° |
| | | const exceptionDescription = ref('') |
| | | |
| | | // 计ç®ä¸ä¼ URL |
| | | const uploadFileUrl = computed(() => { |
| | |
| | | } |
| | | } |
| | | |
| | | // 设置å¼å¸¸ç¶æ |
| | | const setExceptionStatus = (status) => { |
| | | hasException.value = status |
| | | } |
| | | |
| | | // æäº¤è¡¨å |
| | | const submitForm = async () => { |
| | | try { |
| | | // æ£æ¥æ¯å¦éæ©äºå·¡æ£ç¶æ |
| | | if (hasException.value === null) { |
| | | uni.showToast({ |
| | | title: 'è¯·éæ©å·¡æ£ç¶æ', |
| | | icon: 'none' |
| | | }) |
| | | return |
| | | } |
| | | |
| | | // 妿æ¯å¼å¸¸ç¶æï¼æ£æ¥æ¯å¦æä¸ä¼ æä»¶ |
| | | if (hasException.value === true) { |
| | | const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length |
| | | if (totalFiles === 0) { |
| | | uni.showToast({ |
| | | title: '请ä¸ä¼ å¼å¸¸ç
§ç', |
| | | icon: 'none' |
| | | }) |
| | | return |
| | | } |
| | | // æ£æ¥æ¯å¦å¡«åäºå¼å¸¸æè¿° |
| | | if (!exceptionDescription.value.trim()) { |
| | | uni.showToast({ |
| | | title: '请填åå¼å¸¸æè¿°', |
| | | icon: 'none' |
| | | }) |
| | | return |
| | | } |
| | | } |
| | | |
| | | let arr = [] |
| | | if (beforeModelValue.value.length > 0) { |
| | | arr.push(...beforeModelValue.value.map(item => ({ ...item, statusType: 0 }))) |
| | |
| | | |
| | | // æäº¤æ°æ® |
| | | infoData.value.storageBlobDTO = arr |
| | | infoData.value.hasException = hasException.value |
| | | infoData.value.exceptionDescription = exceptionDescription.value |
| | | await submitInspectionRecord({ ...infoData.value }) |
| | | |
| | | uni.showToast({ |
| | |
| | | beforeModelValue.value = [] |
| | | afterModelValue.value = [] |
| | | issueModelValue.value = [] |
| | | hasException.value = null |
| | | exceptionDescription.value = '' |
| | | } |
| | | |
| | | // å
³éå¼¹æ¡ |
| | |
| | | border-top: 1px solid #f0f0f0; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | // å¼å¸¸ç¶æéæ©æ ·å¼ |
| | | .exception-section { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .exception-options { |
| | | display: flex; |
| | | gap: 15px; |
| | | } |
| | | |
| | | .exception-option { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding: 15px 20px; |
| | | border: 2px solid #e0e0e0; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | background-color: #fff; |
| | | |
| | | &.active { |
| | | border-color: #1890ff; |
| | | background-color: #e6f7ff; |
| | | } |
| | | |
| | | &:active { |
| | | opacity: 0.8; |
| | | } |
| | | } |
| | | |
| | | .option-text { |
| | | font-size: 14px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | // æ£å¸¸ç¶ææç¤ºæ ·å¼ |
| | | .normal-tip { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 40px 20px; |
| | | background-color: #f6ffed; |
| | | border: 1px dashed #b7eb8f; |
| | | border-radius: 8px; |
| | | |
| | | .tip-text { |
| | | margin-top: 15px; |
| | | font-size: 14px; |
| | | color: #52c41a; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <text class="detail-value">{{ item.taskId || item.id }}</text> |
| | | </view> |
| | | <view class="detail-item"> |
| | | <text class="detail-label">å·¡æ£é¡¹ç®</text> |
| | | <text class="detail-value">{{ item.inspectionProject || 'æ ' }}</text> |
| | | </view> |
| | | <view class="detail-item"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remarks || 'æ ' }}</text> |
| | | </view> |
| | |
| | | size="small" |
| | | type="primary" |
| | | inverted></uni-tag> |
| | | <uni-tag v-else="" |
| | | <uni-tag v-else |
| | | text="æªå·¡æ£" |
| | | size="small" |
| | | type="warning" |
| | |
| | | <view v-if="taskTableData?.length === 0" |
| | | class="no-data"> |
| | | <text>ææ æ°æ®</text> |
| | | </view> |
| | | </view> |
| | | <!-- å¾çä¸ä¼ å¼¹çª - åçå®ç° --> |
| | | <view v-if="showUploadDialog" |
| | | class="custom-modal-overlay" |
| | | @click="closeUploadDialog"> |
| | | <view class="custom-modal-container" |
| | | @click.stop> |
| | | <view class="upload-popup-content"> |
| | | <view class="upload-popup-header"> |
| | | <text class="upload-popup-title">ä¸ä¼ å·¡æ£è®°å½</text> |
| | | </view> |
| | | <view class="upload-popup-body"> |
| | | <!-- åç±»æ ç¾é¡µ --> |
| | | <view class="upload-tabs"> |
| | | <view class="tab-item" |
| | | :class="{ active: currentUploadType === 'before' }" |
| | | @click="switchUploadType('before')"> |
| | | ç产å |
| | | </view> |
| | | <view class="tab-item" |
| | | :class="{ active: currentUploadType === 'after' }" |
| | | @click="switchUploadType('after')"> |
| | | çäº§ä¸ |
| | | </view> |
| | | <view class="tab-item" |
| | | :class="{ active: currentUploadType === 'issue' }" |
| | | @click="switchUploadType('issue')"> |
| | | ç产å |
| | | </view> |
| | | </view> |
| | | <!-- å¼å¸¸ç¶æéæ© --> |
| | | <view class="exception-section"> |
| | | <text class="section-title">æ¯å¦åå¨å¼å¸¸ï¼</text> |
| | | <view class="exception-options"> |
| | | <view class="exception-option" |
| | | :class="{ active: hasException === false }" |
| | | @click="setExceptionStatus(false)"> |
| | | <u-icon name="checkmark-circle" |
| | | size="20" |
| | | color="#52c41a"></u-icon> |
| | | <text>æ£å¸¸</text> |
| | | </view> |
| | | <view class="exception-option" |
| | | :class="{ active: hasException === true }" |
| | | @click="setExceptionStatus(true)"> |
| | | <u-icon name="close-circle" |
| | | size="20" |
| | | color="#ff4d4f"></u-icon> |
| | | <text>åå¨å¼å¸¸</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- å½ååç±»çä¸ä¼ åºå --> |
| | | <view class="simple-upload-area"> |
| | | <view class="upload-buttons"> |
| | | <u-button type="primary" |
| | | @click="chooseMedia('image')" |
| | | :loading="uploading" |
| | | :disabled="getCurrentFiles().length >= uploadConfig.limit" |
| | | :customStyle="{ marginRight: '10px', flex: 1 }"> |
| | | <u-icon name="camera" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px;"></u-icon> |
| | | {{ uploading ? 'ä¸ä¼ ä¸...' : 'æç
§' }} |
| | | </u-button> |
| | | <u-button type="success" |
| | | @click="chooseMedia('video')" |
| | | :loading="uploading" |
| | | :disabled="getCurrentFiles().length >= uploadConfig.limit" |
| | | :customStyle="{ flex: 1 }"> |
| | | <uni-icons type="videocam" |
| | | name="videocam" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px;"></uni-icons> |
| | | {{ uploading ? 'ä¸ä¼ ä¸...' : 'æè§é¢' }} |
| | | </u-button> |
| | | </view> |
| | | <!-- ä¸ä¼ è¿åº¦ --> |
| | | <view v-if="uploading" |
| | | class="upload-progress"> |
| | | <u-line-progress :percentage="uploadProgress" |
| | | :showText="true" |
| | | activeColor="#409eff"></u-line-progress> |
| | | </view> |
| | | <!-- å½ååç±»çæä»¶å表 --> |
| | | <view v-if="getCurrentFiles().length > 0" |
| | | class="file-list"> |
| | | <view v-for="(file, index) in getCurrentFiles()" |
| | | :key="index" |
| | | class="file-item"> |
| | | <view class="file-preview-container"> |
| | | <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)" |
| | | :src="file.url || file.tempFilePath || file.path || file.downloadUrl" |
| | | class="file-preview" |
| | | mode="aspectFill" /> |
| | | <view v-else-if="file.type === 'video'" |
| | | class="video-preview"> |
| | | <uni-icons type="videocam" |
| | | name="videocam" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px;"></uni-icons> |
| | | <text class="video-text">è§é¢</text> |
| | | </view> |
| | | <!-- å é¤æé® --> |
| | | <view class="delete-btn" |
| | | @click="removeFile(index)"> |
| | | <u-icon name="close" |
| | | size="12" |
| | | color="#fff"></u-icon> |
| | | </view> |
| | | </view> |
| | | <view class="file-info"> |
| | | <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? 'å¾ç' : 'è§é¢') |
| | | }}</text> |
| | | <text class="file-size">{{ formatFileSize(file.size) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="getCurrentFiles().length === 0" |
| | | class="empty-state"> |
| | | <text>è¯·éæ©è¦ä¸ä¼ ç{{ getUploadTypeText() }}å¾çæè§é¢</text> |
| | | </view> |
| | | <!-- ç»è®¡ä¿¡æ¯ --> |
| | | <view class="upload-summary"> |
| | | <text class="summary-text"> |
| | | ç产å: {{ beforeModelValue.length }}个æä»¶ | |
| | | ç产ä¸: {{ afterModelValue.length }}个æä»¶ | |
| | | ç产å: {{ issueModelValue.length }}个æä»¶ |
| | | </text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="upload-popup-footer"> |
| | | <u-button @click="closeUploadDialog" |
| | | :customStyle="{ marginRight: '10px' }">åæ¶</u-button> |
| | | <u-button v-if="hasException === true" |
| | | type="warning" |
| | | @click="goToRepair" |
| | | :customStyle="{ marginRight: '10px' }"> |
| | | æ°å¢æ¥ä¿® |
| | | </u-button> |
| | | <u-button type="primary" |
| | | @click="submitUpload">æäº¤</u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- æ¥çéä»¶å¼¹çª --> |
| | | <view v-if="showAttachmentDialog" |
| | | class="custom-modal-overlay" |
| | | @click="closeAttachmentDialog"> |
| | | <view class="custom-modal-container" |
| | | @click.stop> |
| | | <view class="attachment-popup-content"> |
| | | <view class="attachment-popup-header"> |
| | | <text class="attachment-popup-title">æ¥çéä»¶ - {{ currentViewTask?.taskName }}</text> |
| | | <view class="close-btn-attachment" |
| | | @click="closeAttachmentDialog"> |
| | | <u-icon name="close" |
| | | size="16" |
| | | color="#666"></u-icon> |
| | | </view> |
| | | </view> |
| | | <view class="attachment-popup-body"> |
| | | <!-- åç±»æ ç¾é¡µ --> |
| | | <view class="attachment-tabs"> |
| | | <view class="tab-item" |
| | | :class="{ active: currentViewType === 'before' }" |
| | | @click="switchViewType('before')"> |
| | | ç产å ({{ getAttachmentsByType(0).length }}) |
| | | </view> |
| | | <view class="tab-item" |
| | | :class="{ active: currentViewType === 'after' }" |
| | | @click="switchViewType('after')"> |
| | | çäº§ä¸ ({{ getAttachmentsByType(1).length }}) |
| | | </view> |
| | | <view class="tab-item" |
| | | :class="{ active: currentViewType === 'issue' }" |
| | | @click="switchViewType('issue')"> |
| | | ç产å ({{ getAttachmentsByType(2).length }}) |
| | | </view> |
| | | </view> |
| | | <!-- å½ååç±»çéä»¶å表 --> |
| | | <view class="attachment-content"> |
| | | <view v-if="getCurrentViewAttachments().length > 0" |
| | | class="attachment-list"> |
| | | <view v-for="(file, index) in getCurrentViewAttachments()" |
| | | :key="index" |
| | | class="attachment-item" |
| | | @click="previewAttachment(file)"> |
| | | <view class="attachment-preview-container"> |
| | | <image v-if="file.type === 'image' || isImageFile(file)" |
| | | :src="file.url || file.downloadUrl" |
| | | class="attachment-preview" |
| | | mode="aspectFill" /> |
| | | <view v-else |
| | | class="attachment-video-preview"> |
| | | <u-icon name="video" |
| | | size="24" |
| | | color="#409eff"></u-icon> |
| | | <text class="video-text">è§é¢</text> |
| | | </view> |
| | | </view> |
| | | <view class="attachment-info"> |
| | | <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || 'éä»¶' |
| | | }}</text> |
| | | <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="attachment-empty"> |
| | | <text>该åç±»ææ éä»¶</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- è§é¢é¢è§å¼¹çª --> |
| | |
| | | const currentScanningTask = ref(null); |
| | | const infoData = ref(null); |
| | | |
| | | // ä¸ä¼ ç¸å
³ç¶æ |
| | | const showUploadDialog = ref(false); |
| | | const uploadFiles = ref([]); // ä¿çç¨äºå
¼å®¹æ§ |
| | | const uploadStatusType = ref(0); |
| | | const uploading = ref(false); |
| | | const uploadProgress = ref(0); |
| | | const number = ref(0); |
| | | const uploadList = ref([]); |
| | | |
| | | // ä¸ä¸ªåç±»çä¸ä¼ ç¶æ |
| | | const beforeModelValue = ref([]); // ç产å |
| | | const afterModelValue = ref([]); // çäº§ä¸ |
| | | const issueModelValue = ref([]); // ç产å |
| | | |
| | | // å½åæ¿æ´»çä¸ä¼ ç±»å |
| | | const currentUploadType = ref("before"); // 'before', 'after', 'issue' |
| | | |
| | | // æ¥çéä»¶ç¸å
³ç¶æ |
| | | const showAttachmentDialog = ref(false); |
| | | const currentViewTask = ref(null); |
| | | const currentViewType = ref("before"); // 'before', 'after', 'issue' |
| | | const attachmentList = ref([]); // å½åæ¥çä»»å¡çéä»¶å表 |
| | | |
| | | // è§é¢é¢è§ç¸å
³ç¶æ |
| | | const showVideoDialog = ref(false); |
| | | const currentVideoFile = ref(null); |
| | | |
| | | // å¼å¸¸ç¶æ |
| | | const hasException = ref(null); // null: æªéæ©, true: åå¨å¼å¸¸, false: æ£å¸¸ |
| | | |
| | | // ä¸ä¼ é
ç½® |
| | | const uploadConfig = { |
| | | action: "/file/upload", |
| | | limit: 10, |
| | | fileSize: 50, // MB |
| | | fileType: ["jpg", "jpeg", "png", "mp4", "mov"], |
| | | maxVideoDuration: 60, // ç§ |
| | | }; |
| | | |
| | | // 计ç®ä¸ä¼ URL |
| | | const uploadFileUrl = computed(() => { |
| | | const baseUrl = config.baseUrl; |
| | | |
| | | return baseUrl + uploadConfig.action; |
| | | }); |
| | | |
| | | // 计ç®è¯·æ±å¤´ |
| | | const headers = computed(() => { |
| | | const token = getToken(); |
| | | return token ? { Authorization: "Bearer " + token } : {}; |
| | | }); |
| | | |
| | | // 请æ±åæ¶æ å¿ï¼ç¨äºåæ¶æ£å¨è¿è¡çè¯·æ± |
| | | let isRequestCancelled = false; |
| | |
| | | onUnmounted(() => { |
| | | // è®¾ç½®åæ¶æ å¿ï¼é»æ¢åç»ç弿¥æä½ |
| | | isRequestCancelled = true; |
| | | |
| | | // å
³éä¸ä¼ å¼¹çª |
| | | if (showUploadDialog.value) { |
| | | showUploadDialog.value = false; |
| | | } |
| | | }); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | |
| | | |
| | | const getFileStatus = record => { |
| | | let _beforeProduction = |
| | | record.beforeProduction && record.beforeProduction.length; |
| | | (record.commonFileListBeforeVO && record.commonFileListBeforeVO.length) || |
| | | (record.commonFileListBefore && record.commonFileListBefore.length); |
| | | let _afterProduction = |
| | | record.afterProduction && record.afterProduction.length; |
| | | (record.commonFileListVO && record.commonFileListVO.length) || |
| | | (record.commonFileListAfter && record.commonFileListAfter.length); |
| | | let _productionIssues = |
| | | record.productionIssues && record.productionIssues.length; |
| | | (record.commonFileListAfterVO && record.commonFileListAfterVO.length) || |
| | | (record.commonFileListIssue && record.commonFileListIssue.length); |
| | | |
| | | if (_beforeProduction && _afterProduction && _productionIssues) { |
| | | return 2; |
| | | } else if (_beforeProduction || _afterProduction || _productionIssues) { |
| | |
| | | } |
| | | }; |
| | | |
| | | // æå¼ä¸ä¼ å¼¹çª |
| | | // æå¼ä¸ä¼ é¡µé¢ |
| | | const openUploadDialog = task => { |
| | | // 设置任å¡ä¿¡æ¯å°infoData |
| | | if (task) { |
| | | infoData.value = { |
| | | ...task, |
| | | taskId: task.taskId || task.id, |
| | | storageBlobDTO: [], // åå§åæä»¶å表 |
| | | }; |
| | | } |
| | | |
| | | // 设置ä¸ä¼ ç¶æç±»åï¼å¯ä»¥æ ¹æ®ä»»å¡ç±»å设置ä¸åçç¶æï¼ |
| | | uploadStatusType.value = 0; // é»è®¤ç¶æ |
| | | |
| | | // æ¸
空ä¹åçæä»¶ |
| | | uploadFiles.value = []; |
| | | |
| | | // æ¾ç¤ºä¸ä¼ å¼¹çª |
| | | showUploadDialog.value = true; |
| | | }; |
| | | |
| | | // å
³éä¸ä¼ å¼¹çª |
| | | const closeUploadDialog = () => { |
| | | showUploadDialog.value = false; |
| | | uploadFiles.value = []; |
| | | // æ¸
çä¸ä¸ªåç±»çæ°æ® |
| | | beforeModelValue.value = []; |
| | | afterModelValue.value = []; |
| | | issueModelValue.value = []; |
| | | currentUploadType.value = "before"; |
| | | hasException.value = null; // éç½®å¼å¸¸ç¶æ |
| | | infoData.value = null; // æ¸
ç任塿°æ® |
| | | }; |
| | | |
| | | // 忢ä¸ä¼ ç±»å |
| | | const switchUploadType = type => { |
| | | currentUploadType.value = type; |
| | | }; |
| | | |
| | | // è·åå½ååç±»çæä»¶å表 |
| | | const getCurrentFiles = () => { |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | return beforeModelValue.value || []; |
| | | case "after": |
| | | return afterModelValue.value || []; |
| | | case "issue": |
| | | return issueModelValue.value || []; |
| | | default: |
| | | return []; |
| | | } |
| | | }; |
| | | |
| | | // è·åä¸ä¼ ç±»åææ¬ |
| | | const getUploadTypeText = () => { |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | return "ç产å"; |
| | | case "after": |
| | | return "ç产ä¸"; |
| | | case "issue": |
| | | return "ç产å"; |
| | | default: |
| | | return ""; |
| | | } |
| | | }; |
| | | |
| | | // å¤çä¸ä¼ æä»¶æ´æ° |
| | | const handleUploadUpdate = files => { |
| | | uploadFiles.value = files; |
| | | }; |
| | | |
| | | // 设置å¼å¸¸ç¶æ |
| | | const setExceptionStatus = status => { |
| | | hasException.value = status; |
| | | }; |
| | | |
| | | // è·³è½¬å°æ°å¢æ¥ä¿®é¡µé¢ |
| | | const goToRepair = () => { |
| | | try { |
| | | // åå¨å½åä»»å¡ä¿¡æ¯å°æ¬å°åå¨ï¼ä¾æ¥ä¿®é¡µé¢ä½¿ç¨ |
| | | const taskInfo = { |
| | | taskId: infoData.value?.taskId || infoData.value?.id, |
| | | taskName: infoData.value?.taskName, |
| | | inspectionLocation: infoData.value?.inspectionLocation, |
| | | inspector: infoData.value?.inspector, |
| | | // ä¼ éå½åä¸ä¼ çæä»¶ä¿¡æ¯ |
| | | uploadedFiles: { |
| | | before: beforeModelValue.value, |
| | | after: afterModelValue.value, |
| | | issue: issueModelValue.value, |
| | | }, |
| | | }; |
| | | |
| | | uni.setStorageSync("repairTaskInfo", JSON.stringify(taskInfo)); |
| | | |
| | | // è·³è½¬å°æ°å¢æ¥ä¿®é¡µé¢ |
| | | uni.navigateTo({ |
| | | url: "/pages/equipmentManagement/repair/add", |
| | | }); |
| | | |
| | | // å
³éä¸ä¼ å¼¹çª |
| | | closeUploadDialog(); |
| | | } catch (error) { |
| | | console.error("跳转æ¥ä¿®é¡µé¢å¤±è´¥:", error); |
| | | uni.showToast({ |
| | | title: "跳转失败ï¼è¯·éè¯", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // æäº¤ä¸ä¼ |
| | | const submitUpload = async () => { |
| | | try { |
| | | // æ£æ¥æ¯å¦éæ©äºå¼å¸¸ç¶æ |
| | | if (hasException.value === null) { |
| | | uni.showToast({ |
| | | title: "è¯·éæ©æ¯å¦åå¨å¼å¸¸", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // æ£æ¥æ¯å¦æä»»ä½æä»¶ä¸ä¼ |
| | | const totalFiles = |
| | | beforeModelValue.value.length + |
| | | afterModelValue.value.length + |
| | | issueModelValue.value.length; |
| | | if (totalFiles === 0) { |
| | | uni.showToast({ |
| | | title: "请å
ä¸ä¼ æä»¶", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // æ¾ç¤ºæäº¤ä¸çå è½½æç¤º |
| | | showLoadingToast("æäº¤ä¸..."); |
| | | |
| | | // æç
§æ¨çé»è¾åå¹¶ææåç±»çæä»¶ |
| | | let arr = []; |
| | | if (beforeModelValue.value.length > 0) { |
| | | arr.push(...beforeModelValue.value); |
| | | } |
| | | if (afterModelValue.value.length > 0) { |
| | | arr.push(...afterModelValue.value); |
| | | } |
| | | if (issueModelValue.value.length > 0) { |
| | | arr.push(...issueModelValue.value); |
| | | } |
| | | |
| | | // ä¼ ç»å端çä¸´æ¶æä»¶IDå表ï¼tempFileIdsï¼ |
| | | // å
¼å®¹ï¼æäºæ¥å£å¯è½è¿å tempId / tempFileId / id |
| | | let tempFileIds = []; |
| | | if (arr !== null && arr.length > 0) { |
| | | tempFileIds = arr |
| | | .map(item => item?.tempId ?? item?.tempFileId ?? item?.id) |
| | | .filter(v => v !== undefined && v !== null && v !== ""); |
| | | } |
| | | |
| | | // æäº¤æ°æ® |
| | | infoData.value.storageBlobDTO = arr; |
| | | // æ·»å å¼å¸¸ç¶æä¿¡æ¯ |
| | | infoData.value.hasException = hasException.value; |
| | | infoData.value.tempFileIds = tempFileIds; |
| | | const result = await uploadInspectionTask({ ...infoData.value }); |
| | | |
| | | // æ£æ¥æäº¤ç»æ |
| | | if (result && (result.code === 200 || result.success)) { |
| | | // æäº¤æå |
| | | closeToast(); // å
³éå è½½æç¤º |
| | | |
| | | uni.showToast({ |
| | | title: "æäº¤æå", |
| | | icon: "success", |
| | | }); |
| | | |
| | | // å
³éå¼¹çª |
| | | closeUploadDialog(); |
| | | |
| | | // å·æ°å表 |
| | | setTimeout(() => { |
| | | reloadPage(); |
| | | }, 500); |
| | | } else { |
| | | // æäº¤å¤±è´¥ |
| | | closeToast(); |
| | | uni.showToast({ |
| | | title: result?.msg || result?.message || "æäº¤å¤±è´¥", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | console.error("æäº¤ä¸ä¼ 失败:", error); |
| | | closeToast(); // å
³éå è½½æç¤º |
| | | |
| | | let errorMessage = "æäº¤å¤±è´¥"; |
| | | if (error.message) { |
| | | errorMessage = error.message; |
| | | } else if (error.msg) { |
| | | errorMessage = error.msg; |
| | | } else if (typeof error === "string") { |
| | | errorMessage = error; |
| | | } |
| | | |
| | | uni.showToast({ |
| | | title: errorMessage, |
| | | icon: "error", |
| | | }); |
| | | } |
| | | // å°ä»»å¡ä¿¡æ¯ä¼ éå°ä¸ä¼ é¡µé¢ |
| | | const taskData = encodeURIComponent(JSON.stringify(task)); |
| | | uni.navigateTo({ |
| | | url: `/pages/inspectionUpload/upload?taskInfo=${taskData}`, |
| | | }); |
| | | }; |
| | | |
| | | // å¾çä¸ä¼ (å¯éæ©å¾çä¸ä¼ æè
æ¯ç¸æºæç
§) |
| | | const startUploadForTask = async (task, type) => { |
| | | // ç´æ¥æå¼ä¸ä¼ å¼¹çª |
| | | // æå¼ä¸ä¼ é¡µé¢ |
| | | openUploadDialog(task); |
| | | }; |
| | | |
| | | // æ¥çéä»¶ |
| | | // æ¥çéä»¶ - 跳转å°éä»¶é¡µé¢ |
| | | const viewAttachments = async task => { |
| | | try { |
| | | currentViewTask.value = task; |
| | | currentViewType.value = "before"; |
| | | |
| | | // è§£ææ°çæ°æ®ç»æ |
| | | attachmentList.value = []; |
| | | |
| | | // åç«¯åæ¾åæ®µï¼ä½ æä¾çæ°æ®ç»æï¼ï¼ |
| | | // - commonFileListBeforeï¼ç产åï¼é常 type=10ï¼ |
| | | // - commonFileListAfterï¼ç产ä¸ï¼é常 type=11ï¼ |
| | | // - commonFileListï¼å¯è½æ¯å
¨é¨/å
åºï¼è¥å
å«ç产åï¼ä¸è¬ type=12ï¼ |
| | | const allList = Array.isArray(task?.commonFileList) |
| | | ? task.commonFileList |
| | | : []; |
| | | const beforeList = Array.isArray(task?.commonFileListBefore) |
| | | ? task.commonFileListBefore |
| | | : allList.filter(f => f?.type === 10); |
| | | const afterList = Array.isArray(task?.commonFileListAfter) |
| | | ? task.commonFileListAfter |
| | | : allList.filter(f => f?.type === 11); |
| | | // 妿å端åç»è¡¥äº commonFileListIssueï¼åä¼å
ç¨ï¼å¦åä» commonFileList éæ type=12 å
åº |
| | | const issueList = Array.isArray(task?.commonFileListIssue) |
| | | ? task.commonFileListIssue |
| | | : allList.filter(f => f?.type === 12); |
| | | |
| | | const mapToViewFile = (file, viewType) => { |
| | | const u = normalizeFileUrl(file?.url || file?.downloadUrl || ""); |
| | | return { |
| | | ...file, |
| | | // ç¨äºä¸æ ç¾é¡µåç»ï¼0=ç产å 1=çäº§ä¸ 2=ç产å |
| | | type: viewType, |
| | | name: file?.name || file?.originalFilename || file?.bucketFilename, |
| | | bucketFilename: file?.bucketFilename || file?.name, |
| | | originalFilename: file?.originalFilename || file?.name, |
| | | url: u, |
| | | downloadUrl: u, |
| | | size: file?.size || file?.byteSize, |
| | | }; |
| | | }; |
| | | |
| | | attachmentList.value.push(...beforeList.map(f => mapToViewFile(f, 0))); |
| | | attachmentList.value.push(...afterList.map(f => mapToViewFile(f, 1))); |
| | | attachmentList.value.push(...issueList.map(f => mapToViewFile(f, 2))); |
| | | |
| | | showAttachmentDialog.value = true; |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: "è·åé件失败", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // å
³ééä»¶æ¥çå¼¹çª |
| | | const closeAttachmentDialog = () => { |
| | | showAttachmentDialog.value = false; |
| | | currentViewTask.value = null; |
| | | attachmentList.value = []; |
| | | currentViewType.value = "before"; |
| | | }; |
| | | |
| | | // 忢æ¥çç±»å |
| | | const switchViewType = type => { |
| | | currentViewType.value = type; |
| | | }; |
| | | |
| | | // æ ¹æ®typeè·å对åºåç±»çéä»¶ |
| | | const getAttachmentsByType = typeValue => { |
| | | return attachmentList.value.filter(file => file.type === typeValue) || []; |
| | | }; |
| | | // è·åtypeå¼ |
| | | const getTabType = () => { |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | return 10; |
| | | case "after": |
| | | return 11; |
| | | case "issue": |
| | | return 12; |
| | | default: |
| | | return 10; |
| | | } |
| | | }; |
| | | // è·åå½åæ¥çç±»åçéä»¶ |
| | | const getCurrentViewAttachments = () => { |
| | | switch (currentViewType.value) { |
| | | case "before": |
| | | return getAttachmentsByType(0); |
| | | case "after": |
| | | return getAttachmentsByType(1); |
| | | case "issue": |
| | | return getAttachmentsByType(2); |
| | | default: |
| | | return []; |
| | | } |
| | | const taskData = encodeURIComponent(JSON.stringify(task)); |
| | | uni.navigateTo({ |
| | | url: `/pages/inspectionUpload/attachment?taskInfo=${taskData}`, |
| | | }); |
| | | }; |
| | | |
| | | // 夿æ¯å¦ä¸ºå¾çæä»¶ |
| | |
| | | title: "è§é¢ææ¾å¤±è´¥", |
| | | icon: "error", |
| | | }); |
| | | }; |
| | | |
| | | // æç
§/æè§é¢ï¼çæºä¼å
ç¨ chooseMediaï¼ä¸æ¯æåéçº§ï¼ |
| | | const chooseMedia = type => { |
| | | if (getCurrentFiles().length >= uploadConfig.limit) { |
| | | uni.showToast({ |
| | | title: `æå¤åªè½éæ©${uploadConfig.limit}个æä»¶`, |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const remaining = uploadConfig.limit - getCurrentFiles().length; |
| | | |
| | | // ä¼å
ï¼chooseMediaï¼æ¯æ image/videoï¼ |
| | | if (typeof uni.chooseMedia === "function") { |
| | | uni.chooseMedia({ |
| | | count: Math.min(remaining, 1), |
| | | mediaType: [type || "image"], |
| | | sizeType: ["compressed", "original"], |
| | | sourceType: ["camera"], |
| | | success: res => { |
| | | try { |
| | | const files = res?.tempFiles || []; |
| | | if (!files.length) throw new Error("æªè·åå°æä»¶"); |
| | | |
| | | files.forEach((tf, idx) => { |
| | | const filePath = tf.tempFilePath || tf.path || ""; |
| | | const fileType = tf.fileType || type || "image"; |
| | | const ext = fileType === "video" ? "mp4" : "jpg"; |
| | | const file = { |
| | | tempFilePath: filePath, |
| | | path: filePath, |
| | | type: fileType, |
| | | name: `${fileType}_${Date.now()}_${idx}.${ext}`, |
| | | size: tf.size || 0, |
| | | duration: tf.duration || 0, |
| | | createTime: Date.now(), |
| | | uid: Date.now() + Math.random() + idx, |
| | | }; |
| | | handleBeforeUpload(file); |
| | | }); |
| | | } catch (e) { |
| | | console.error("å¤çææç»æå¤±è´¥:", e); |
| | | uni.showToast({ title: "å¤çæä»¶å¤±è´¥", icon: "error" }); |
| | | } |
| | | }, |
| | | fail: err => { |
| | | console.error("ææå¤±è´¥:", err); |
| | | uni.showToast({ title: "ææå¤±è´¥", icon: "error" }); |
| | | }, |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // é级ï¼chooseImage / chooseVideo |
| | | if (type === "video") { |
| | | chooseVideo(); |
| | | } else { |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | sizeType: ["compressed", "original"], |
| | | sourceType: ["camera"], |
| | | success: res => { |
| | | const tempFilePath = res?.tempFilePaths?.[0]; |
| | | const tempFile = res?.tempFiles?.[0] || {}; |
| | | if (!tempFilePath) return; |
| | | handleBeforeUpload({ |
| | | tempFilePath, |
| | | path: tempFilePath, |
| | | type: "image", |
| | | name: `photo_${Date.now()}.jpg`, |
| | | size: tempFile.size || 0, |
| | | createTime: Date.now(), |
| | | uid: Date.now() + Math.random(), |
| | | }); |
| | | }, |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // æç
§ |
| | | const chooseImage = () => { |
| | | if (uploadFiles.value.length >= uploadConfig.limit) { |
| | | uni.showToast({ |
| | | title: `æå¤åªè½ææ${uploadConfig.limit}个æä»¶`, |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.chooseMedia({ |
| | | count: 1, |
| | | mediaType: ["image", "video"], |
| | | sizeType: ["compressed", "original"], |
| | | sourceType: ["camera"], |
| | | success: res => { |
| | | try { |
| | | if (!res.tempFiles || res.tempFiles.length === 0) { |
| | | throw new Error("æªè·åå°å¾çæä»¶"); |
| | | } |
| | | |
| | | const tempFilePath = res.tempFiles[0]; |
| | | const tempFile = |
| | | res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {}; |
| | | |
| | | const file = { |
| | | tempFilePath: tempFilePath, |
| | | path: tempFilePath, // ä¿æå
¼å®¹æ§ |
| | | type: "image", |
| | | name: `photo_${Date.now()}.jpg`, |
| | | size: tempFile.size || 0, |
| | | createTime: new Date().getTime(), |
| | | uid: Date.now() + Math.random(), |
| | | }; |
| | | |
| | | handleBeforeUpload(file); |
| | | } catch (error) { |
| | | console.error("å¤çæç
§ç»æå¤±è´¥:", error); |
| | | uni.showToast({ |
| | | title: "å¤çå¾ç失败", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }, |
| | | fail: err => { |
| | | console.error("æç
§å¤±è´¥:", err); |
| | | uni.showToast({ |
| | | title: "æç
§å¤±è´¥: " + (err.errMsg || "æªç¥é误"), |
| | | icon: "error", |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // æè§é¢ |
| | | const chooseVideo = () => { |
| | | if (uploadFiles.value.length >= uploadConfig.limit) { |
| | | uni.showToast({ |
| | | title: `æå¤åªè½ææ${uploadConfig.limit}个æä»¶`, |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.chooseVideo({ |
| | | sourceType: ["camera"], |
| | | maxDuration: uploadConfig.maxVideoDuration, |
| | | camera: "back", |
| | | success: res => { |
| | | try { |
| | | if (!res.tempFilePath) { |
| | | throw new Error("æªè·åå°è§é¢æä»¶"); |
| | | } |
| | | |
| | | const file = { |
| | | tempFilePath: res.tempFilePath, |
| | | path: res.tempFilePath, // ä¿æå
¼å®¹æ§ |
| | | type: "video", |
| | | name: `video_${Date.now()}.mp4`, |
| | | size: res.size || 0, |
| | | duration: res.duration || 0, |
| | | createTime: new Date().getTime(), |
| | | uid: Date.now() + Math.random(), |
| | | }; |
| | | |
| | | handleBeforeUpload(file); |
| | | } catch (error) { |
| | | console.error("å¤çæè§é¢ç»æå¤±è´¥:", error); |
| | | uni.showToast({ |
| | | title: "å¤çè§é¢å¤±è´¥", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }, |
| | | fail: err => { |
| | | console.error("æè§é¢å¤±è´¥:", err); |
| | | uni.showToast({ |
| | | title: "æè§é¢å¤±è´¥: " + (err.errMsg || "æªç¥é误"), |
| | | icon: "error", |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // å 餿件 |
| | | const removeFile = index => { |
| | | uni.showModal({ |
| | | title: "确认å é¤", |
| | | content: "ç¡®å®è¦å é¤è¿ä¸ªæä»¶åï¼", |
| | | success: res => { |
| | | if (res.confirm) { |
| | | // æ ¹æ®å½åä¸ä¼ ç±»åå é¤å¯¹åºåç±»çæä»¶ |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | beforeModelValue.value.splice(index, 1); |
| | | break; |
| | | case "after": |
| | | afterModelValue.value.splice(index, 1); |
| | | break; |
| | | case "issue": |
| | | issueModelValue.value.splice(index, 1); |
| | | break; |
| | | } |
| | | uni.showToast({ |
| | | title: "å 餿å", |
| | | icon: "success", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // æ£æ¥ç½ç»è¿æ¥ |
| | | const checkNetworkConnection = () => { |
| | | return new Promise(resolve => { |
| | | uni.getNetworkType({ |
| | | success: res => { |
| | | if (res.networkType === "none") { |
| | | resolve(false); |
| | | } else { |
| | | resolve(true); |
| | | } |
| | | }, |
| | | fail: () => { |
| | | resolve(false); |
| | | }, |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // ä¸ä¼ åæ ¡éª |
| | | const handleBeforeUpload = async file => { |
| | | // æ ¡éªæä»¶ç±»å |
| | | if ( |
| | | uploadConfig.fileType && |
| | | Array.isArray(uploadConfig.fileType) && |
| | | uploadConfig.fileType.length > 0 |
| | | ) { |
| | | const fileName = file.name || ""; |
| | | const fileExtension = fileName |
| | | ? fileName.split(".").pop().toLowerCase() |
| | | : ""; |
| | | |
| | | // æ ¹æ®æä»¶ç±»åç¡®å®ææçæ©å±å |
| | | let expectedTypes = []; |
| | | if (file.type === "image") { |
| | | expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"]; |
| | | } else if (file.type === "video") { |
| | | expectedTypes = ["mp4", "mov", "avi", "wmv"]; |
| | | } |
| | | |
| | | // æ£æ¥æä»¶æ©å±åæ¯å¦å¨å
许çç±»åä¸ |
| | | if (fileExtension && expectedTypes.length > 0) { |
| | | const isAllowed = expectedTypes.some( |
| | | type => uploadConfig.fileType.includes(type) && type === fileExtension |
| | | ); |
| | | |
| | | if (!isAllowed) { |
| | | uni.showToast({ |
| | | title: `æä»¶æ ¼å¼ä¸æ¯æï¼è¯·ææ ${expectedTypes.join("/")} æ ¼å¼çæä»¶`, |
| | | icon: "none", |
| | | }); |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // æ ¡éªéè¿ï¼å¼å§ä¸ä¼ |
| | | uploadFile(file); |
| | | return true; |
| | | }; |
| | | |
| | | // æä»¶ä¸ä¼ å¤çï¼çæºèµ° uni.uploadFileï¼ |
| | | const uploadFile = async file => { |
| | | uploading.value = true; |
| | | uploadProgress.value = 0; |
| | | number.value++; // å¢å ä¸ä¼ è®¡æ° |
| | | |
| | | // ç¡®ä¿tokenåå¨ |
| | | const token = getToken(); |
| | | if (!token) { |
| | | handleUploadError("ç¨æ·æªç»å½"); |
| | | return; |
| | | } |
| | | |
| | | const typeValue = getTabType(); // ç产å:10, ç产ä¸:11, ç产å:12 |
| | | |
| | | uploadWithUniUploadFile( |
| | | file, |
| | | file.tempFilePath || file.path || "", |
| | | typeValue, |
| | | token |
| | | ); |
| | | }; |
| | | |
| | | // 使ç¨uni.uploadFileä¸ä¼ ï¼éH5ç¯å¢æH5åéæ¹æ¡ï¼ |
| | | const uploadWithUniUploadFile = (file, filePath, typeValue, token) => { |
| | | if (!filePath) { |
| | | handleUploadError("æä»¶è·¯å¾ä¸åå¨"); |
| | | return; |
| | | } |
| | | |
| | | const uploadTask = uni.uploadFile({ |
| | | url: uploadFileUrl.value, |
| | | filePath: filePath, |
| | | name: "file", |
| | | formData: { |
| | | type: typeValue, |
| | | }, |
| | | header: { |
| | | Authorization: `Bearer ${token}`, |
| | | }, |
| | | success: res => { |
| | | try { |
| | | if (res.statusCode === 200) { |
| | | const response = JSON.parse(res.data); |
| | | if (response.code === 200) { |
| | | handleUploadSuccess(response, file); |
| | | uni.showToast({ |
| | | title: "ä¸ä¼ æå", |
| | | icon: "success", |
| | | }); |
| | | } else { |
| | | handleUploadError(response.msg || "æå¡å¨è¿åé误"); |
| | | } |
| | | } else { |
| | | handleUploadError(`æå¡å¨é误ï¼ç¶æç : ${res.statusCode}`); |
| | | } |
| | | } catch (e) { |
| | | console.error("è§£æååºå¤±è´¥:", e); |
| | | console.error("åå§ååºæ°æ®:", res.data); |
| | | handleUploadError("ååºæ°æ®è§£æå¤±è´¥: " + e.message); |
| | | } |
| | | }, |
| | | fail: err => { |
| | | console.error("ä¸ä¼ 失败:", err.errMsg || err); |
| | | number.value--; // ä¸ä¼ 失败æ¶åå°è®¡æ° |
| | | |
| | | let errorMessage = "ä¸ä¼ 失败"; |
| | | if (err.errMsg) { |
| | | if (err.errMsg.includes("statusCode: null")) { |
| | | errorMessage = "ç½ç»è¿æ¥å¤±è´¥ï¼è¯·æ£æ¥ç½ç»è®¾ç½®"; |
| | | } else if (err.errMsg.includes("timeout")) { |
| | | errorMessage = "ä¸ä¼ è¶
æ¶ï¼è¯·éè¯"; |
| | | } else if (err.errMsg.includes("fail")) { |
| | | errorMessage = "ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»è¿æ¥"; |
| | | } else { |
| | | errorMessage = err.errMsg; |
| | | } |
| | | } |
| | | |
| | | handleUploadError(errorMessage); |
| | | }, |
| | | complete: () => { |
| | | uploading.value = false; |
| | | uploadProgress.value = 0; |
| | | }, |
| | | }); |
| | | |
| | | // çå¬ä¸ä¼ è¿åº¦ |
| | | if (uploadTask && uploadTask.onProgressUpdate) { |
| | | uploadTask.onProgressUpdate(res => { |
| | | uploadProgress.value = res.progress; |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // ä¸ä¼ 失败å¤ç |
| | | const handleUploadError = (message = "ä¸ä¼ æä»¶å¤±è´¥", showRetry = false) => { |
| | | uploading.value = false; |
| | | uploadProgress.value = 0; |
| | | |
| | | if (showRetry) { |
| | | uni.showModal({ |
| | | title: "ä¸ä¼ 失败", |
| | | content: message + "ï¼æ¯å¦éè¯ï¼", |
| | | success: res => { |
| | | if (res.confirm) { |
| | | // ç¨æ·éæ©éè¯ï¼è¿éå¯ä»¥éæ°è§¦åä¸ä¼ |
| | | } |
| | | }, |
| | | }); |
| | | } else { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // ä¸ä¼ æååè° |
| | | const handleUploadSuccess = (res, file) => { |
| | | console.log("ä¸ä¼ æåååº:", res); |
| | | |
| | | // å¤çä¸åçæ°æ®ç»æï¼å¯è½æ¯æ°ç»ï¼ä¹å¯è½æ¯å个对象 |
| | | let uploadedFile = null; |
| | | uploadedFile = res.data; |
| | | |
| | | if (!uploadedFile) { |
| | | console.error("æ æ³è§£æä¸ä¼ ååºæ°æ®:", res); |
| | | number.value--; // ä¸ä¼ 失败æ¶åå°è®¡æ° |
| | | handleUploadError("ä¸ä¼ ååºæ°æ®æ ¼å¼é误", false); |
| | | return; |
| | | } |
| | | |
| | | // æ ¹æ®å½åä¸ä¼ ç±»å设置typeåæ®µ |
| | | let typeValue = 0; // é»è®¤ä¸ºç产å |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | typeValue = 0; |
| | | break; |
| | | case "after": |
| | | typeValue = 1; |
| | | break; |
| | | case "issue": |
| | | typeValue = 2; |
| | | break; |
| | | } |
| | | |
| | | // ç¡®ä¿ä¸ä¼ çæä»¶æ°æ®å®æ´ï¼å
å«idåtype |
| | | const fileData = { |
| | | ...file, |
| | | id: uploadedFile.id, // æ·»å æå¡å¨è¿åçid |
| | | tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id, |
| | | url: |
| | | uploadedFile.url || |
| | | uploadedFile.downloadUrl || |
| | | file.tempFilePath || |
| | | file.path, |
| | | bucketFilename: |
| | | uploadedFile.bucketFilename || uploadedFile.originalFilename || file.name, |
| | | downloadUrl: uploadedFile.downloadUrl || uploadedFile.url, |
| | | size: uploadedFile.size || uploadedFile.byteSize || file.size, |
| | | createTime: uploadedFile.createTime || new Date().getTime(), |
| | | type: typeValue, // æ·»å ç±»ååæ®µï¼0=ç产å, 1=ç产ä¸, 2=ç产å |
| | | }; |
| | | |
| | | uploadList.value.push(fileData); |
| | | |
| | | // ç«å³æ·»å å°å¯¹åºçåç±»ï¼ä¸çå¾
æææä»¶ä¸ä¼ 宿 |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | beforeModelValue.value.push(fileData); |
| | | break; |
| | | case "after": |
| | | afterModelValue.value.push(fileData); |
| | | break; |
| | | case "issue": |
| | | issueModelValue.value.push(fileData); |
| | | break; |
| | | } |
| | | |
| | | // éç½®ä¸ä¼ å表ï¼å ä¸ºå·²ç»æ·»å å°å¯¹åºåç±»äºï¼ |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | }; |
| | | |
| | | // ä¸ä¼ ç»æå¤çï¼å·²åºå¼ï¼ç°å¨å¨handleUploadSuccessä¸ç´æ¥å¤çï¼ |
| | | const uploadedSuccessfully = () => { |
| | | // æ¤å½æ°å·²ä¸å使ç¨ï¼æä»¶ä¸ä¼ æååç«å³æ·»å å°å¯¹åºåç±» |
| | | }; |
| | | |
| | | // æ ¼å¼åæä»¶å¤§å° |
| | | const formatFileSize = size => { |
| | | if (!size) return ""; |
| | | if (size < 1024) return size + "B"; |
| | | if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB"; |
| | | return (size / (1024 * 1024)).toFixed(1) + "MB"; |
| | | }; |
| | | </script> |
| | | |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | /* ä¸ä¼ å¼¹çªæ ·å¼ */ |
| | | .upload-popup-content { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | width: 100%; |
| | | min-height: 300px; |
| | | max-height: 70vh; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); |
| | | } |
| | | |
| | | .upload-popup-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 15px 20px; |
| | | border-bottom: 1px solid #eee; |
| | | } |
| | | |
| | | .upload-popup-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .upload-popup-body { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .upload-popup-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 15px 20px; |
| | | border-top: 1px solid #eee; |
| | | gap: 10px; |
| | | } |
| | | |
| | | /* ç®åä¸ä¼ ç»ä»¶æ ·å¼ */ |
| | | .simple-upload-area { |
| | | padding: 15px; |
| | | } |
| | | |
| | | .upload-buttons { |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .file-list { |
| | | margin-top: 15px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 8px; |
| | | border: 1px solid #e9ecef; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | transition: all 0.3s ease; |
| | | width: calc(50% - 6px); |
| | | min-width: 120px; |
| | | } |
| | | |
| | | .file-item:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .file-preview-container { |
| | | position: relative; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .file-preview { |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 8px; |
| | | object-fit: cover; |
| | | border: 2px solid #f0f0f0; |
| | | } |
| | | |
| | | .video-preview { |
| | | width: 80px; |
| | | height: 80px; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); |
| | | border-radius: 8px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 2px solid #f0f0f0; |
| | | } |
| | | |
| | | .video-text { |
| | | font-size: 12px; |
| | | color: #666; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .delete-btn { |
| | | position: absolute; |
| | | top: -6px; |
| | | right: -6px; |
| | | width: 20px; |
| | | height: 20px; |
| | | background: #ff4757; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .delete-btn:hover { |
| | | background: #ff3742; |
| | | transform: scale(1.1); |
| | | } |
| | | |
| | | .file-info { |
| | | text-align: center; |
| | | width: 100%; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 12px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | display: block; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | max-width: 100px; |
| | | } |
| | | |
| | | .file-size { |
| | | font-size: 10px; |
| | | color: #999; |
| | | margin-top: 2px; |
| | | display: block; |
| | | } |
| | | |
| | | .empty-state { |
| | | text-align: center; |
| | | padding: 40px 20px; |
| | | color: #999; |
| | | font-size: 14px; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | border: 2px dashed #ddd; |
| | | } |
| | | |
| | | .upload-progress { |
| | | margin: 15px 0; |
| | | padding: 0 10px; |
| | | } |
| | | |
| | | /* ä¸ä¼ æ ç¾é¡µæ ·å¼ */ |
| | | .upload-tabs { |
| | | display: flex; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | margin-bottom: 15px; |
| | | padding: 4px; |
| | | } |
| | | |
| | | .tab-item { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 8px 12px; |
| | | font-size: 14px; |
| | | color: #666; |
| | | border-radius: 6px; |
| | | transition: all 0.3s ease; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .tab-item.active { |
| | | background: #409eff; |
| | | color: #fff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .tab-item:hover:not(.active) { |
| | | background: #e9ecef; |
| | | color: #333; |
| | | } |
| | | |
| | | /* å¼å¸¸ç¶æéæ©æ ·å¼ */ |
| | | .exception-section { |
| | | margin-bottom: 20px; |
| | | padding: 15px; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | border: 1px solid #e9ecef; |
| | | } |
| | | |
| | | .section-title { |
| | | display: block; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .exception-options { |
| | | display: flex; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .exception-option { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding: 12px 16px; |
| | | background: #fff; |
| | | border: 2px solid #e9ecef; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | |
| | | .exception-option.active { |
| | | border-color: #409eff; |
| | | background: #f0f8ff; |
| | | color: #409eff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .exception-option:hover:not(.active) { |
| | | border-color: #d9d9d9; |
| | | background: #fafafa; |
| | | } |
| | | |
| | | /* ç»è®¡ä¿¡æ¯æ ·å¼ */ |
| | | .upload-summary { |
| | | margin-top: 15px; |
| | | padding: 10px; |
| | | background: #f8f9fa; |
| | | border-radius: 6px; |
| | | border-left: 3px solid #409eff; |
| | | } |
| | | |
| | | .summary-text { |
| | | font-size: 12px; |
| | | color: #666; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | /* æ¥çéä»¶å¼¹çªæ ·å¼ */ |
| | | .attachment-popup-content { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | width: 100%; |
| | | min-height: 400px; |
| | | max-height: 70vh; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); |
| | | } |
| | | |
| | | .attachment-popup-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 15px 20px; |
| | | border-bottom: 1px solid #eee; |
| | | background: #f8f9fa; |
| | | } |
| | | |
| | | .attachment-popup-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .close-btn-attachment { |
| | | width: 28px; |
| | | height: 28px; |
| | | border-radius: 50%; |
| | | background: #f5f5f5; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .close-btn-attachment:hover { |
| | | background: #e9ecef; |
| | | transform: scale(1.1); |
| | | } |
| | | |
| | | .attachment-popup-body { |
| | | flex: 1; |
| | | padding: 15px 20px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .attachment-tabs { |
| | | display: flex; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | margin-bottom: 15px; |
| | | padding: 4px; |
| | | } |
| | | |
| | | .attachment-content { |
| | | min-height: 200px; |
| | | } |
| | | |
| | | .attachment-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .attachment-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 8px; |
| | | border: 1px solid #e9ecef; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | transition: all 0.3s ease; |
| | | width: calc(33.33% - 8px); |
| | | min-width: 100px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .attachment-item:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .attachment-preview-container { |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .attachment-preview { |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 8px; |
| | | object-fit: cover; |
| | | border: 2px solid #f0f0f0; |
| | | } |
| | | |
| | | .attachment-video-preview { |
| | | width: 80px; |
| | | height: 80px; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); |
| | | border-radius: 8px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 2px solid #f0f0f0; |
| | | } |
| | | |
| | | .attachment-info { |
| | | text-align: center; |
| | | width: 100%; |
| | | } |
| | | |
| | | .attachment-name { |
| | | font-size: 12px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | display: block; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | max-width: 80px; |
| | | } |
| | | |
| | | .attachment-size { |
| | | font-size: 10px; |
| | | color: #999; |
| | | margin-top: 2px; |
| | | display: block; |
| | | } |
| | | |
| | | .attachment-empty { |
| | | text-align: center; |
| | | padding: 60px 20px; |
| | | color: #999; |
| | | font-size: 14px; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | border: 2px dashed #ddd; |
| | | } |
| | | |
| | | /* è§é¢é¢è§å¼¹çªæ ·å¼ */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="inspection-upload-page"> |
| | | <!-- 页é¢å¤´é¨ --> |
| | | <PageHeader title="ä¸ä¼ å·¡æ£è®°å½" |
| | | @back="goBack" /> |
| | | <!-- 页é¢å
容 --> |
| | | <view class="upload-content"> |
| | | <!-- ä»»å¡ä¿¡æ¯å¡ç --> |
| | | <view class="task-info-card" |
| | | v-if="taskInfo"> |
| | | <view class="task-info-header"> |
| | | <text class="task-name">{{ taskInfo.taskName }}</text> |
| | | </view> |
| | | <view class="task-info-body"> |
| | | <view class="info-item"> |
| | | <text class="info-label">ä»»å¡ID</text> |
| | | <text class="info-value">{{ taskInfo.taskId || taskInfo.id }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å·¡æ£ä½ç½®</text> |
| | | <text class="info-value">{{ taskInfo.inspectionLocation || '-' }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">æ§è¡äºº</text> |
| | | <text class="info-value">{{ taskInfo.inspector || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- å¼å¸¸ç¶æéæ© --> |
| | | <view class="section-card"> |
| | | <view class="section-title">å·¡æ£ç¶æ</view> |
| | | <view class="exception-options"> |
| | | <view class="exception-option" |
| | | :class="{ active: hasException === false }" |
| | | @click="setExceptionStatus(false)"> |
| | | <u-icon name="checkmark-circle" |
| | | size="20" |
| | | color="#52c41a"></u-icon> |
| | | <text class="option-text">æ£å¸¸</text> |
| | | </view> |
| | | <view class="exception-option" |
| | | :class="{ active: hasException === true }" |
| | | @click="setExceptionStatus(true)"> |
| | | <u-icon name="close-circle" |
| | | size="20" |
| | | color="#ff4d4f"></u-icon> |
| | | <text class="option-text">åå¨å¼å¸¸</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- å¼å¸¸æè¿°ï¼ä»
å¨å¼å¸¸æ¶æ¾ç¤ºï¼ --> |
| | | <view class="section-card" |
| | | v-if="hasException === true"> |
| | | <view class="section-title">å¼å¸¸æè¿°</view> |
| | | <textarea v-model="abnormalDescription" |
| | | class="exception-textarea" |
| | | maxlength="500" |
| | | placeholder="请æè¿°å¼å¸¸æ
åµ..." /> |
| | | </view> |
| | | <!-- åç±»æ ç¾é¡µï¼ä»
å¨å¼å¸¸æ¶æ¾ç¤ºï¼ --> |
| | | <view class="section-card" |
| | | v-if="hasException === true"> |
| | | <view class="upload-tabs"> |
| | | <view class="tab-item" |
| | | :class="{ active: currentUploadType === 'before' }" |
| | | @click="switchUploadType('before')"> |
| | | ç产å |
| | | </view> |
| | | <view class="tab-item" |
| | | :class="{ active: currentUploadType === 'after' }" |
| | | @click="switchUploadType('after')"> |
| | | çäº§ä¸ |
| | | </view> |
| | | <view class="tab-item" |
| | | :class="{ active: currentUploadType === 'issue' }" |
| | | @click="switchUploadType('issue')"> |
| | | ç产å |
| | | </view> |
| | | </view> |
| | | <!-- å½ååç±»çä¸ä¼ åºå --> |
| | | <view class="upload-area"> |
| | | <view class="upload-buttons"> |
| | | <u-button type="primary" |
| | | @click="chooseMedia('image')" |
| | | :loading="uploading" |
| | | :disabled="getCurrentFiles().length >= uploadConfig.limit" |
| | | :customStyle="{ marginRight: '10px', flex: 1 }"> |
| | | <u-icon name="camera" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px"></u-icon> |
| | | {{ uploading ? 'ä¸ä¼ ä¸...' : 'æç
§' }} |
| | | </u-button> |
| | | <u-button type="success" |
| | | @click="chooseMedia('video')" |
| | | :loading="uploading" |
| | | :disabled="getCurrentFiles().length >= uploadConfig.limit" |
| | | :customStyle="{ flex: 1 }"> |
| | | <uni-icons type="videocam" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px"></uni-icons> |
| | | {{ uploading ? 'ä¸ä¼ ä¸...' : 'æè§é¢' }} |
| | | </u-button> |
| | | </view> |
| | | <!-- ä¸ä¼ è¿åº¦ --> |
| | | <view v-if="uploading" |
| | | class="upload-progress"> |
| | | <u-line-progress :percentage="uploadProgress" |
| | | :showText="true" |
| | | activeColor="#409eff"></u-line-progress> |
| | | </view> |
| | | <!-- å½ååç±»çæä»¶å表 --> |
| | | <view v-if="getCurrentFiles().length > 0" |
| | | class="file-list"> |
| | | <view v-for="(file, index) in getCurrentFiles()" |
| | | :key="index" |
| | | class="file-item"> |
| | | <view class="file-preview-container"> |
| | | <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)" |
| | | :src="file.url || file.tempFilePath || file.path || file.downloadUrl" |
| | | class="file-preview" |
| | | mode="aspectFill" /> |
| | | <view v-else-if="file.type === 'video'" |
| | | class="video-preview"> |
| | | <uni-icons type="videocam" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px"></uni-icons> |
| | | <text class="video-text">è§é¢</text> |
| | | </view> |
| | | <!-- å é¤æé® --> |
| | | <view class="delete-btn" |
| | | @click="removeFile(index)"> |
| | | <u-icon name="close" |
| | | size="12" |
| | | color="#fff"></u-icon> |
| | | </view> |
| | | </view> |
| | | <view class="file-info"> |
| | | <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? 'å¾ç' : 'è§é¢') }}</text> |
| | | <text class="file-size">{{ formatFileSize(file.size) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="getCurrentFiles().length === 0" |
| | | class="empty-state"> |
| | | <text>è¯·éæ©è¦ä¸ä¼ ç{{ getUploadTypeText() }}å¾çæè§é¢</text> |
| | | </view> |
| | | </view> |
| | | <!-- ç»è®¡ä¿¡æ¯ --> |
| | | <view class="upload-summary"> |
| | | <text class="summary-text"> |
| | | ç产å: {{ beforeModelValue.length }}个æä»¶ | |
| | | ç产ä¸: {{ afterModelValue.length }}个æä»¶ | |
| | | ç产å: {{ issueModelValue.length }}个æä»¶ |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <!-- æ£å¸¸ç¶ææç¤º --> |
| | | <view class="normal-tip-card" |
| | | v-if="hasException === false"> |
| | | <u-icon name="info-circle" |
| | | size="60" |
| | | color="#52c41a"></u-icon> |
| | | <text class="tip-text">设å¤è¿è¡æ£å¸¸ï¼æ éä¸ä¼ ç
§ç</text> |
| | | </view> |
| | | </view> |
| | | <!-- åºé¨æé® --> |
| | | <view class="footer-buttons"> |
| | | <u-button @click="goBack" |
| | | :customStyle="{ marginRight: '10px' }">åæ¶</u-button> |
| | | <u-button v-if="hasException === true" |
| | | type="warning" |
| | | @click="goToRepair" |
| | | :customStyle="{ marginRight: '10px' }"> |
| | | æ°å¢æ¥ä¿® |
| | | </u-button> |
| | | <u-button type="primary" |
| | | @click="submitUpload">æäº¤</u-button> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { uploadInspectionTask } from "@/api/inspectionManagement"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import config from "@/config"; |
| | | |
| | | // ä»»å¡ä¿¡æ¯ |
| | | const taskInfo = ref(null); |
| | | |
| | | // ä¸ä¼ ç¸å
³ç¶æ |
| | | const uploading = ref(false); |
| | | const uploadProgress = ref(0); |
| | | |
| | | // ä¸ä¸ªåç±»çä¸ä¼ ç¶æ |
| | | const beforeModelValue = ref([]); // ç产å |
| | | const afterModelValue = ref([]); // çäº§ä¸ |
| | | const issueModelValue = ref([]); // ç产å |
| | | |
| | | // å½åæ¿æ´»çä¸ä¼ ç±»å |
| | | const currentUploadType = ref("before"); // 'before', 'after', 'issue' |
| | | |
| | | // å¼å¸¸ç¶æ |
| | | const hasException = ref(null); // null: æªéæ©, true: åå¨å¼å¸¸, false: æ£å¸¸ |
| | | // å¼å¸¸æè¿° |
| | | const abnormalDescription = ref(""); |
| | | |
| | | // ä¸ä¼ é
ç½® |
| | | const uploadConfig = { |
| | | action: "/common/upload", |
| | | limit: 10, |
| | | fileSize: 50, // MB |
| | | fileType: ["jpg", "jpeg", "png", "mp4", "mov"], |
| | | maxVideoDuration: 60, // ç§ |
| | | }; |
| | | |
| | | // 计ç®ä¸ä¼ URL |
| | | const uploadFileUrl = computed(() => { |
| | | const baseUrl = config.baseUrl; |
| | | return baseUrl + uploadConfig.action; |
| | | }); |
| | | |
| | | // 页é¢å è½½ |
| | | onLoad(options => { |
| | | if (options.taskInfo) { |
| | | try { |
| | | const info = JSON.parse(decodeURIComponent(options.taskInfo)); |
| | | taskInfo.value = info; |
| | | |
| | | // åæ¾é»è¾ï¼ä» taskInfo 䏿¢å¤å·²ä¸ä¼ çæä»¶ |
| | | const mapFiles = list => { |
| | | if (!list || !Array.isArray(list)) return []; |
| | | return list.map(item => { |
| | | // å¤ç URLï¼å»é¤å¯è½çç©ºæ ¼ |
| | | const finalUrl = (item.url || item.previewURL || "").trim(); |
| | | // èªå¨æ¨ææä»¶ç±»å |
| | | let fileType = item.type; |
| | | if (!fileType && item.contentType) { |
| | | fileType = item.contentType.startsWith("video") ? "video" : "image"; |
| | | } else if (!fileType) { |
| | | fileType = "image"; // é»è®¤å¾ç |
| | | } |
| | | |
| | | return { |
| | | ...item, |
| | | url: finalUrl, |
| | | name: item.name || item.originalFilename, |
| | | tempId: item.tempId || item.id || item.tempFileId, |
| | | size: item.size || item.byteSize || 0, // æ å°å¤§å°å段 |
| | | type: fileType, |
| | | status: "success", |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | // ä¿®æ£å段æ å°ï¼BeforeVO(ç产å), VO(ç产ä¸), AfterVO(ç产å) |
| | | if ( |
| | | info.commonFileListBeforeVO && |
| | | Array.isArray(info.commonFileListBeforeVO) |
| | | ) { |
| | | beforeModelValue.value = mapFiles(info.commonFileListBeforeVO); |
| | | } |
| | | console.log(beforeModelValue.value, "beforeModelValue"); |
| | | |
| | | if (info.commonFileListVO && Array.isArray(info.commonFileListVO)) { |
| | | afterModelValue.value = mapFiles(info.commonFileListVO); |
| | | } |
| | | if ( |
| | | info.commonFileListAfterVO && |
| | | Array.isArray(info.commonFileListAfterVO) |
| | | ) { |
| | | issueModelValue.value = mapFiles(info.commonFileListAfterVO); |
| | | } |
| | | |
| | | // 妿æå¼å¸¸æè¿°ï¼ä¹æ¢å¤ |
| | | if (info.abnormalDescription) { |
| | | abnormalDescription.value = info.abnormalDescription; |
| | | } |
| | | // 妿æå¼å¸¸ç¶æï¼ä¹æ¢å¤ |
| | | if (info.hasException !== undefined && info.hasException !== null) { |
| | | hasException.value = info.hasException; |
| | | } else if ( |
| | | info.inspectionResult !== undefined && |
| | | info.inspectionResult !== null |
| | | ) { |
| | | // 0-å¼å¸¸ï¼1-æ£å¸¸ |
| | | hasException.value = String(info.inspectionResult) === "0"; |
| | | } |
| | | |
| | | // èªå¨å
åºï¼å¦æåå¨å·²ä¸ä¼ æä»¶ï¼åå¿
ç¶æ¯å¼å¸¸ç¶æï¼ç¡®ä¿ UI æ£å¸¸æ¾ç¤º |
| | | if ( |
| | | !hasException.value && |
| | | (beforeModelValue.value.length > 0 || |
| | | afterModelValue.value.length > 0 || |
| | | issueModelValue.value.length > 0) |
| | | ) { |
| | | hasException.value = true; |
| | | } |
| | | } catch (e) { |
| | | console.error("è§£æä»»å¡ä¿¡æ¯å¤±è´¥:", e); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // 忢ä¸ä¼ ç±»å |
| | | const switchUploadType = type => { |
| | | currentUploadType.value = type; |
| | | }; |
| | | |
| | | // è·åå½ååç±»çæä»¶å表 |
| | | const getCurrentFiles = () => { |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | return beforeModelValue.value || []; |
| | | case "after": |
| | | return afterModelValue.value || []; |
| | | case "issue": |
| | | return issueModelValue.value || []; |
| | | default: |
| | | return []; |
| | | } |
| | | }; |
| | | |
| | | // è·åä¸ä¼ ç±»åææ¬ |
| | | const getUploadTypeText = () => { |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | return "ç产å"; |
| | | case "after": |
| | | return "ç产ä¸"; |
| | | case "issue": |
| | | return "ç产å"; |
| | | default: |
| | | return ""; |
| | | } |
| | | }; |
| | | |
| | | // 设置å¼å¸¸ç¶æ |
| | | const setExceptionStatus = status => { |
| | | hasException.value = status; |
| | | }; |
| | | |
| | | // è·³è½¬å°æ°å¢æ¥ä¿®é¡µé¢ |
| | | const goToRepair = () => { |
| | | try { |
| | | const taskData = { |
| | | taskId: taskInfo.value?.taskId || taskInfo.value?.id, |
| | | taskName: taskInfo.value?.taskName, |
| | | inspectionLocation: taskInfo.value?.inspectionLocation, |
| | | inspector: taskInfo.value?.inspector, |
| | | hasException: hasException.value, |
| | | inspectionResult: hasException.value ? 0 : 1, // 0-å¼å¸¸ï¼1-æ£å¸¸ |
| | | commonFileListBeforeDTO: beforeModelValue.value, |
| | | commonFileListDTO: afterModelValue.value, |
| | | commonFileListAfterDTO: issueModelValue.value, |
| | | uploadedFiles: { |
| | | before: beforeModelValue.value, |
| | | after: afterModelValue.value, |
| | | issue: issueModelValue.value, |
| | | }, |
| | | }; |
| | | |
| | | uni.setStorageSync("repairTaskInfo", JSON.stringify(taskData)); |
| | | |
| | | uni.navigateTo({ |
| | | url: "/pages/equipmentManagement/repair/add", |
| | | }); |
| | | } catch (error) { |
| | | console.error("跳转æ¥ä¿®é¡µé¢å¤±è´¥:", error); |
| | | uni.showToast({ |
| | | title: "跳转失败ï¼è¯·éè¯", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // æäº¤ä¸ä¼ |
| | | const submitUpload = async () => { |
| | | try { |
| | | // æ£æ¥æ¯å¦éæ©äºå¼å¸¸ç¶æ |
| | | if (hasException.value === null) { |
| | | uni.showToast({ |
| | | title: "è¯·éæ©å·¡æ£ç¶æ", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 妿æ¯å¼å¸¸ç¶æï¼æ£æ¥æ¯å¦æä¸ä¼ æä»¶åæè¿° |
| | | if (hasException.value === true) { |
| | | const totalFiles = |
| | | beforeModelValue.value.length + |
| | | afterModelValue.value.length + |
| | | issueModelValue.value.length; |
| | | if (totalFiles === 0) { |
| | | uni.showToast({ |
| | | title: "请ä¸ä¼ å¼å¸¸ç
§ç", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | // æ£æ¥æ¯å¦å¡«åäºå¼å¸¸æè¿° |
| | | if (!abnormalDescription.value.trim()) { |
| | | uni.showToast({ |
| | | title: "请填åå¼å¸¸æè¿°", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // æ¾ç¤ºæäº¤ä¸çå è½½æç¤º |
| | | uni.showLoading({ |
| | | title: "æäº¤ä¸...", |
| | | mask: true, |
| | | }); |
| | | |
| | | // æç
§é»è¾åå¹¶ææåç±»çæä»¶ç¨äºæåID |
| | | const allFiles = [ |
| | | ...beforeModelValue.value, |
| | | ...afterModelValue.value, |
| | | ...issueModelValue.value, |
| | | ]; |
| | | |
| | | // ä¼ ç»å端çä¸´æ¶æä»¶IDå表 |
| | | let tempFileIds = []; |
| | | if (allFiles.length > 0) { |
| | | tempFileIds = allFiles |
| | | .map(item => item?.tempId ?? item?.tempFileId ?? item?.id) |
| | | .filter(v => v !== undefined && v !== null && v !== ""); |
| | | } |
| | | |
| | | // æäº¤æ°æ® |
| | | const submitData = { |
| | | ...taskInfo.value, |
| | | commonFileListBeforeDTO: beforeModelValue.value, // ç产å |
| | | commonFileListDTO: afterModelValue.value, // çäº§ä¸ |
| | | commonFileListAfterDTO: issueModelValue.value, // ç产å |
| | | hasException: hasException.value, |
| | | inspectionResult: hasException.value ? 0 : 1, // 0-å¼å¸¸ï¼1-æ£å¸¸ |
| | | abnormalDescription: abnormalDescription.value, |
| | | tempFileIds: tempFileIds, |
| | | }; |
| | | |
| | | const result = await uploadInspectionTask(submitData); |
| | | |
| | | // æ£æ¥æäº¤ç»æ |
| | | if (result && (result.code === 200 || result.success)) { |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: "æäº¤æå", |
| | | icon: "success", |
| | | }); |
| | | |
| | | // è¿ååè¡¨é¡µå¹¶å·æ° |
| | | setTimeout(() => { |
| | | uni.navigateBack(); |
| | | }, 500); |
| | | } else { |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: result?.msg || result?.message || "æäº¤å¤±è´¥", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | } catch (error) { |
| | | console.error("æäº¤ä¸ä¼ 失败:", error); |
| | | uni.hideLoading(); |
| | | uni.showToast({ |
| | | title: error?.message || "æäº¤å¤±è´¥", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // æ ¼å¼åæä»¶å¤§å° |
| | | const formatFileSize = size => { |
| | | if (!size) return "0 B"; |
| | | const units = ["B", "KB", "MB", "GB"]; |
| | | let index = 0; |
| | | let fileSize = size; |
| | | while (fileSize >= 1024 && index < units.length - 1) { |
| | | fileSize /= 1024; |
| | | index++; |
| | | } |
| | | return `${fileSize.toFixed(2)} ${units[index]}`; |
| | | }; |
| | | |
| | | // æç
§/æè§é¢ |
| | | const chooseMedia = type => { |
| | | if (getCurrentFiles().length >= uploadConfig.limit) { |
| | | uni.showToast({ |
| | | title: `æå¤åªè½éæ©${uploadConfig.limit}个æä»¶`, |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const remaining = uploadConfig.limit - getCurrentFiles().length; |
| | | |
| | | // ä¼å
ä½¿ç¨ chooseMedia |
| | | if (typeof uni.chooseMedia === "function") { |
| | | uni.chooseMedia({ |
| | | count: Math.min(remaining, 1), |
| | | mediaType: [type || "image"], |
| | | sizeType: ["compressed", "original"], |
| | | sourceType: ["camera"], |
| | | success: res => { |
| | | try { |
| | | const files = res?.tempFiles || []; |
| | | if (!files.length) throw new Error("æªè·åå°æä»¶"); |
| | | |
| | | files.forEach((tf, idx) => { |
| | | const filePath = tf.tempFilePath || tf.path || ""; |
| | | const fileType = tf.fileType || type || "image"; |
| | | const ext = fileType === "video" ? "mp4" : "jpg"; |
| | | const file = { |
| | | tempFilePath: filePath, |
| | | path: filePath, |
| | | type: fileType, |
| | | name: `${fileType}_${Date.now()}_${idx}.${ext}`, |
| | | size: tf.size || 0, |
| | | duration: tf.duration || 0, |
| | | createTime: Date.now(), |
| | | }; |
| | | uploadFile(file); |
| | | }); |
| | | } catch (err) { |
| | | uni.showToast({ title: err.message || "å¤çæä»¶å¤±è´¥", icon: "none" }); |
| | | } |
| | | }, |
| | | fail: err => { |
| | | console.error("éæ©åªä½å¤±è´¥:", err); |
| | | uni.showToast({ title: "éæ©å¤±è´¥", icon: "none" }); |
| | | }, |
| | | }); |
| | | } else { |
| | | // éçº§æ¹æ¡ |
| | | if (type === "video") { |
| | | uni.chooseVideo({ |
| | | sourceType: ["camera"], |
| | | success: res => { |
| | | const file = { |
| | | tempFilePath: res.tempFilePath, |
| | | path: res.tempFilePath, |
| | | type: "video", |
| | | name: `video_${Date.now()}.mp4`, |
| | | size: res.size || 0, |
| | | duration: res.duration || 0, |
| | | createTime: Date.now(), |
| | | }; |
| | | uploadFile(file); |
| | | }, |
| | | fail: () => { |
| | | uni.showToast({ title: "éæ©è§é¢å¤±è´¥", icon: "none" }); |
| | | }, |
| | | }); |
| | | } else { |
| | | uni.chooseImage({ |
| | | count: Math.min(remaining, 9), |
| | | sizeType: ["compressed"], |
| | | sourceType: ["camera"], |
| | | success: res => { |
| | | const list = res.tempFilePaths || res.tempFiles || []; |
| | | list.forEach((src, idx) => { |
| | | const path = typeof src === "string" ? src : src.path; |
| | | const file = { |
| | | tempFilePath: path, |
| | | path: path, |
| | | type: "image", |
| | | name: `image_${Date.now()}_${idx}.jpg`, |
| | | size: 0, |
| | | createTime: Date.now(), |
| | | }; |
| | | uploadFile(file); |
| | | }); |
| | | }, |
| | | fail: () => { |
| | | uni.showToast({ title: "éæ©å¾ç失败", icon: "none" }); |
| | | }, |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // ä¸ä¼ å个æä»¶ |
| | | const uploadFile = file => { |
| | | const token = getToken(); |
| | | if (!token) { |
| | | uni.showToast({ title: "ç¨æ·æªç»å½", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | uploading.value = true; |
| | | uploadProgress.value = 0; |
| | | |
| | | const uploadTask = uni.uploadFile({ |
| | | url: uploadFileUrl.value, |
| | | filePath: file.tempFilePath, |
| | | name: "files", |
| | | header: { |
| | | Authorization: `Bearer ${token}`, |
| | | }, |
| | | formData: { |
| | | type: getTabType(), |
| | | }, |
| | | success: res => { |
| | | try { |
| | | const data = JSON.parse(res.data); |
| | | if (data.code === 200) { |
| | | // å
¼å®¹ CommonUpload.vue çå¤çé»è¾ |
| | | const resultData = Array.isArray(data.data) |
| | | ? data.data[0] |
| | | : data.data; |
| | | |
| | | // å¤ç url å name èµå¼ |
| | | const finalUrl = resultData.url || resultData.previewURL; |
| | | const finalName = resultData.name || resultData.originalFilename; |
| | | const finalId = |
| | | resultData.tempId || resultData.id || resultData.tempFileId; |
| | | |
| | | const uploadedFile = { |
| | | ...file, |
| | | ...resultData, // å
å«å端è¿åçææåæ®µ |
| | | url: finalUrl, |
| | | name: finalName, |
| | | tempId: finalId, |
| | | status: "success", |
| | | }; |
| | | |
| | | // æ ¹æ®å½åç±»åæ·»å å°å¯¹åºæ°ç» |
| | | if (currentUploadType.value === "before") { |
| | | beforeModelValue.value.push(uploadedFile); |
| | | } else if (currentUploadType.value === "after") { |
| | | afterModelValue.value.push(uploadedFile); |
| | | } else if (currentUploadType.value === "issue") { |
| | | issueModelValue.value.push(uploadedFile); |
| | | } |
| | | |
| | | uni.showToast({ title: "ä¸ä¼ æå", icon: "success" }); |
| | | } else { |
| | | uni.showToast({ title: data.msg || "ä¸ä¼ 失败", icon: "none" }); |
| | | } |
| | | } catch (e) { |
| | | uni.showToast({ title: "è§£æååºå¤±è´¥", icon: "none" }); |
| | | } |
| | | }, |
| | | fail: err => { |
| | | console.error("ä¸ä¼ 失败:", err); |
| | | uni.showToast({ title: "ä¸ä¼ 失败", icon: "none" }); |
| | | }, |
| | | complete: () => { |
| | | uploading.value = false; |
| | | }, |
| | | }); |
| | | |
| | | // çå¬ä¸ä¼ è¿åº¦ |
| | | uploadTask.onProgressUpdate(res => { |
| | | uploadProgress.value = res.progress; |
| | | }); |
| | | }; |
| | | |
| | | // è·åtypeå¼ |
| | | const getTabType = () => { |
| | | switch (currentUploadType.value) { |
| | | case "before": |
| | | return 10; |
| | | case "after": |
| | | return 11; |
| | | case "issue": |
| | | return 12; |
| | | default: |
| | | return 10; |
| | | } |
| | | }; |
| | | |
| | | // å 餿件 |
| | | const removeFile = index => { |
| | | const files = getCurrentFiles(); |
| | | files.splice(index, 1); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .inspection-upload-page { |
| | | min-height: 100vh; |
| | | background-color: #f5f5f5; |
| | | padding-bottom: 80px; |
| | | } |
| | | |
| | | .upload-content { |
| | | padding: 15px; |
| | | } |
| | | |
| | | /* ä»»å¡ä¿¡æ¯å¡ç */ |
| | | .task-info-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 15px; |
| | | margin-bottom: 15px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .task-info-header { |
| | | margin-bottom: 12px; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .task-name { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .task-info-body { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .info-label { |
| | | font-size: 13px; |
| | | color: #999; |
| | | } |
| | | |
| | | .info-value { |
| | | font-size: 13px; |
| | | color: #666; |
| | | } |
| | | |
| | | /* éç¨å¡çæ ·å¼ */ |
| | | .section-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 15px; |
| | | margin-bottom: 15px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | /* å¼å¸¸ç¶æéæ© */ |
| | | .exception-options { |
| | | display: flex; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .exception-option { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding: 14px 16px; |
| | | background: #f8f9fa; |
| | | border: 2px solid #e9ecef; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .exception-option.active { |
| | | border-color: #409eff; |
| | | background: #f0f8ff; |
| | | } |
| | | |
| | | .option-text { |
| | | font-size: 14px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | /* å¼å¸¸æè¿° */ |
| | | .exception-textarea { |
| | | width: 100%; |
| | | min-height: 100px; |
| | | padding: 12px; |
| | | background: #f8f9fa; |
| | | border: 1px solid #e9ecef; |
| | | border-radius: 8px; |
| | | font-size: 14px; |
| | | color: #333; |
| | | resize: none; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .exception-textarea:focus { |
| | | outline: none; |
| | | border-color: #409eff; |
| | | background: #fff; |
| | | } |
| | | |
| | | /* åç±»æ ç¾é¡µ */ |
| | | .upload-tabs { |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .tab-item { |
| | | flex: 1; |
| | | padding: 10px; |
| | | text-align: center; |
| | | background: #f5f5f5; |
| | | border-radius: 6px; |
| | | font-size: 13px; |
| | | color: #666; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .tab-item.active { |
| | | background: #409eff; |
| | | color: #fff; |
| | | } |
| | | |
| | | /* ä¸ä¼ åºå */ |
| | | .upload-area { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .upload-buttons { |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .upload-progress { |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | /* æä»¶å表 */ |
| | | .file-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .file-item { |
| | | width: calc(33.33% - 7px); |
| | | } |
| | | |
| | | .file-preview-container { |
| | | position: relative; |
| | | width: 100%; |
| | | aspect-ratio: 1; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | background: #f5f5f5; |
| | | } |
| | | |
| | | .file-preview { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .video-preview { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background: #333; |
| | | } |
| | | |
| | | .video-text { |
| | | font-size: 12px; |
| | | color: #fff; |
| | | margin-top: 5px; |
| | | } |
| | | |
| | | .delete-btn { |
| | | position: absolute; |
| | | top: 5px; |
| | | right: 5px; |
| | | width: 22px; |
| | | height: 22px; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .file-info { |
| | | margin-top: 5px; |
| | | } |
| | | |
| | | .file-name { |
| | | display: block; |
| | | font-size: 11px; |
| | | color: #666; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .file-size { |
| | | display: block; |
| | | font-size: 10px; |
| | | color: #999; |
| | | margin-top: 2px; |
| | | } |
| | | |
| | | .empty-state { |
| | | text-align: center; |
| | | padding: 30px; |
| | | color: #999; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | /* ç»è®¡ä¿¡æ¯ */ |
| | | .upload-summary { |
| | | margin-top: 15px; |
| | | padding: 10px; |
| | | background: #f8f9fa; |
| | | border-radius: 6px; |
| | | border-left: 3px solid #409eff; |
| | | } |
| | | |
| | | .summary-text { |
| | | font-size: 12px; |
| | | color: #666; |
| | | } |
| | | |
| | | /* æ£å¸¸ç¶ææç¤º */ |
| | | .normal-tip-card { |
| | | background: #f6ffed; |
| | | border: 2px dashed #b7eb8f; |
| | | border-radius: 12px; |
| | | padding: 50px 20px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .normal-tip-card .tip-text { |
| | | margin-top: 15px; |
| | | font-size: 16px; |
| | | color: #52c41a; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | /* åºé¨æé® */ |
| | | .footer-buttons { |
| | | position: fixed; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | display: flex; |
| | | padding: 15px; |
| | | background: #fff; |
| | | border-top: 1px solid #f0f0f0; |
| | | box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="record-container"> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input class="search-text" |
| | | placeholder="请è¾å
¥äº§å大类" |
| | | v-model="searchForm.productName" |
| | | @confirm="handleQuery" |
| | | clearable /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="handleQuery"> |
| | | <up-icon name="search" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <scroll-view scroll-y |
| | | class="ledger-list" |
| | | v-if="tableData.length > 0" |
| | | @scrolltolower="loadMore"> |
| | | <view v-for="item in tableData" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.productName }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è§æ ¼åå·</text> |
| | | <text class="detail-value">{{ item.model }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åä½</text> |
| | | <text class="detail-value">{{ item.unit }}</text> |
| | | </view> |
| | | <view class="detail-row" |
| | | @click="handleShowBatch(item.batchNo)"> |
| | | <text class="detail-label">æ¹å·</text> |
| | | <view class="detail-value batch-no-wrapper"> |
| | | <text class="batch-no-text" |
| | | :class="{ 'clickable': isBatchClickable(item.batchNo) }"> |
| | | {{ formatBatchNo(item.batchNo) }} |
| | | </text> |
| | | <up-icon v-if="isBatchClickable(item.batchNo)" |
| | | name="arrow-right" |
| | | size="14" |
| | | color="#2979ff"></up-icon> |
| | | </view> |
| | | </view> |
| | | <view class="quantity-section"> |
| | | <view class="quantity-box qualified"> |
| | | <text class="q-label">åæ ¼åºå</text> |
| | | <text class="q-value">{{ item.qualifiedQuantity }}</text> |
| | | </view> |
| | | <view class="quantity-box unqualified"> |
| | | <text class="q-label">ä¸åæ ¼åºå</text> |
| | | <text class="q-value">{{ item.unQualifiedQuantity }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="quantity-section"> |
| | | <view class="quantity-box locked"> |
| | | <text class="q-label">åæ ¼å»ç»</text> |
| | | <text class="q-value">{{ item.qualifiedLockedQuantity }}</text> |
| | | </view> |
| | | <view class="quantity-box locked"> |
| | | <text class="q-label">ä¸åæ ¼å»ç»</text> |
| | | <text class="q-value">{{ item.unQualifiedLockedQuantity }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºåé¢è¦</text> |
| | | <text class="detail-value">{{ item.warnNum }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remark || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ´æ°æ¶é´</text> |
| | | <text class="detail-value">{{ item.updateTime }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <up-loadmore :status="loadStatus" /> |
| | | </scroll-view> |
| | | <view v-else-if="!loading" |
| | | class="no-data"> |
| | | <up-empty mode="data" |
| | | text="ææ åºåæ°æ®"></up-empty> |
| | | </view> |
| | | <!-- æ¹å·åè¡¨å¼¹çª --> |
| | | <up-popup v-model:show="showBatchPopup" |
| | | @close="showBatchPopup = false" |
| | | mode="bottom" |
| | | round="20" |
| | | closeable> |
| | | <view class="batch-popup-content"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">æ¹å·è¯¦æ
</text> |
| | | </view> |
| | | <scroll-view scroll-y |
| | | class="batch-list-scroll"> |
| | | <view class="batch-list"> |
| | | <view v-for="(batch, index) in currentBatchList" |
| | | :key="index" |
| | | class="batch-item"> |
| | | <view class="batch-index-box"> |
| | | <text class="batch-index">{{ index + 1 }}</text> |
| | | </view> |
| | | <text class="batch-text">{{ batch }}</text> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js"; |
| | | |
| | | const props = defineProps({ |
| | | productId: { |
| | | type: Number, |
| | | required: true, |
| | | }, |
| | | }); |
| | | |
| | | const tableData = ref([]); |
| | | const loading = ref(false); |
| | | const loadStatus = ref("loadmore"); |
| | | const page = reactive({ current: 1, size: 10 }); |
| | | const total = ref(0); |
| | | const searchForm = reactive({ |
| | | productName: "", |
| | | topParentProductId: props.productId, |
| | | }); |
| | | |
| | | const showBatchPopup = ref(false); |
| | | const currentBatchList = ref([]); |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | tableData.value = []; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | if (loading.value) return; |
| | | loading.value = true; |
| | | loadStatus.value = "loading"; |
| | | |
| | | getStockInventoryListPageCombined({ |
| | | ...searchForm, |
| | | current: page.current, |
| | | size: page.size, |
| | | }) |
| | | .then(res => { |
| | | loading.value = false; |
| | | const records = res.data.records || []; |
| | | tableData.value = |
| | | page.current === 1 ? records : [...tableData.value, ...records]; |
| | | total.value = res.data.total; |
| | | loadStatus.value = |
| | | tableData.value.length >= total.value ? "nomore" : "loadmore"; |
| | | }) |
| | | .catch(() => { |
| | | loading.value = false; |
| | | loadStatus.value = "loadmore"; |
| | | }); |
| | | }; |
| | | |
| | | const loadMore = () => { |
| | | if (loadStatus.value === "loadmore") { |
| | | page.current++; |
| | | getList(); |
| | | } |
| | | }; |
| | | |
| | | const handleShowBatch = batchNo => { |
| | | if (!batchNo) return; |
| | | // æ¯æéå·ãç©ºæ ¼ææ¢è¡åé |
| | | currentBatchList.value = batchNo |
| | | .split(/[,ï¼\s\n]+/) |
| | | .filter(item => item.trim() !== ""); |
| | | if (currentBatchList.value.length > 0) { |
| | | showBatchPopup.value = true; |
| | | } |
| | | }; |
| | | |
| | | const formatBatchNo = batchNo => { |
| | | if (!batchNo) return "-"; |
| | | if (batchNo.length > 25) { |
| | | return batchNo.substring(0, 25) + "..."; |
| | | } |
| | | return batchNo; |
| | | }; |
| | | |
| | | const isBatchClickable = batchNo => { |
| | | if (!batchNo) return false; |
| | | return batchNo.length > 25 || batchNo.includes(",") || batchNo.includes("ï¼"); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .record-container { |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | .search-section { |
| | | padding: 20rpx; |
| | | background-color: #ffffff; |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .search-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | background-color: #f2f2f2; |
| | | border-radius: 40rpx; |
| | | padding: 0 30rpx; |
| | | height: 80rpx; |
| | | } |
| | | |
| | | .search-input { |
| | | flex: 1; |
| | | } |
| | | |
| | | .search-text { |
| | | font-size: 28rpx; |
| | | } |
| | | |
| | | .filter-button { |
| | | padding-left: 20rpx; |
| | | } |
| | | |
| | | .ledger-list { |
| | | padding: 20rpx; |
| | | box-sizing: border-box; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .ledger-item { |
| | | background-color: #ffffff; |
| | | border-radius: 16rpx; |
| | | padding: 30rpx; |
| | | margin-bottom: 20rpx; |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .item-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20rpx; |
| | | } |
| | | |
| | | .item-left { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .document-icon { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | background: linear-gradient(135deg, #2979ff, #1565c0); |
| | | border-radius: 8rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 16rpx; |
| | | } |
| | | |
| | | .item-id { |
| | | font-size: 30rpx; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | |
| | | .item-details { |
| | | .detail-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | margin-bottom: 16rpx; |
| | | font-size: 26rpx; |
| | | |
| | | .detail-label { |
| | | color: #909399; |
| | | } |
| | | |
| | | .detail-value { |
| | | color: #303133; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .batch-no-wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | max-width: 70%; |
| | | |
| | | .batch-no-text { |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | |
| | | &.clickable { |
| | | color: #2979ff; |
| | | text-decoration: underline; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .batch-popup-content { |
| | | background-color: #fff; |
| | | padding: 30rpx; |
| | | max-height: 70vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .popup-header { |
| | | padding-bottom: 30rpx; |
| | | border-bottom: 1rpx solid #ebeef5; |
| | | margin-bottom: 20rpx; |
| | | text-align: center; |
| | | |
| | | .popup-title { |
| | | font-size: 32rpx; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | } |
| | | |
| | | .batch-list-scroll { |
| | | flex: 1; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .batch-list { |
| | | padding: 10rpx 0; |
| | | |
| | | .batch-item { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 24rpx 0; |
| | | border-bottom: 1rpx solid #f2f6fc; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .batch-index-box { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | background-color: #f0f2f5; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 20rpx; |
| | | |
| | | .batch-index { |
| | | font-size: 20rpx; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .batch-text { |
| | | font-size: 28rpx; |
| | | color: #303133; |
| | | flex: 1; |
| | | word-break: break-all; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .quantity-section { |
| | | display: flex; |
| | | gap: 20rpx; |
| | | margin: 20rpx 0; |
| | | |
| | | .quantity-box { |
| | | flex: 1; |
| | | padding: 16rpx; |
| | | border-radius: 8rpx; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | |
| | | .q-label { |
| | | font-size: 22rpx; |
| | | margin-bottom: 8rpx; |
| | | } |
| | | |
| | | .q-value { |
| | | font-size: 32rpx; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | &.qualified { |
| | | background-color: #ecf5ff; |
| | | color: #409eff; |
| | | } |
| | | |
| | | &.unqualified { |
| | | background-color: #fef0f0; |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | &.locked { |
| | | background-color: #f4f4f5; |
| | | color: #909399; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .no-data { |
| | | padding-top: 200rpx; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <view class="app-container"> |
| | | <PageHeader title="åºå管ç" @back="goBack" /> |
| | | <up-tabs :list="tabs" @click="handleTabClick" :current="activeTab"/> |
| | | <swiper class="swiper-box" :current="activeTab" @change="handleSwiperChange"> |
| | | <swiper-item class="swiper-item"> |
| | | <qualified-record /> |
| | | </swiper-item> |
| | | <swiper-item class="swiper-item"> |
| | | <unqualified-record /> |
| | | </swiper-item> |
| | | </swiper> |
| | | <PageHeader title="åºå管ç" |
| | | @back="goBack" /> |
| | | <view v-if="loading" |
| | | class="loading-state"> |
| | | <up-loading-icon text="å è½½ä¸..."></up-loading-icon> |
| | | </view> |
| | | <template v-else> |
| | | <up-tabs :list="tabs" |
| | | @click="handleTabClick" |
| | | :current="activeTab" /> |
| | | <swiper class="swiper-box" |
| | | :current="activeTab" |
| | | @change="handleSwiperChange"> |
| | | <swiper-item class="swiper-item" |
| | | v-for="tab in products" |
| | | :key="tab.id"> |
| | | <record :product-id="tab.id" |
| | | v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" /> |
| | | </swiper-item> |
| | | </swiper> |
| | | </template> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from 'vue'; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import QualifiedRecord from "./Qualified.vue"; |
| | | import UnqualifiedRecord from "./Unqualified.vue"; |
| | | import { ref, onMounted } from "vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import Record from "./Record.vue"; |
| | | import { productTreeList } from "@/api/basicData/product.js"; |
| | | |
| | | const activeTab = ref(0); |
| | | const tabs = ref([ |
| | | { name: 'åæ ¼åºå' }, |
| | | { name: 'ä¸åæ ¼åºå' } |
| | | ]); |
| | | const activeTab = ref(0); |
| | | const tabs = ref([]); |
| | | const products = ref([]); |
| | | const loading = ref(false); |
| | | const initializedTabs = ref([]); |
| | | |
| | | const handleTabClick = (item) => { |
| | | activeTab.value = item.index; |
| | | }; |
| | | const handleTabClick = item => { |
| | | activeTab.value = item.index; |
| | | if (!initializedTabs.value.includes(products.value[item.index].id)) { |
| | | initializedTabs.value.push(products.value[item.index].id); |
| | | } |
| | | }; |
| | | |
| | | const handleSwiperChange = (e) => { |
| | | activeTab.value = e.detail.current; |
| | | }; |
| | | const handleSwiperChange = e => { |
| | | const index = e.detail.current; |
| | | activeTab.value = index; |
| | | if (!initializedTabs.value.includes(products.value[index].id)) { |
| | | initializedTabs.value.push(products.value[index].id); |
| | | } |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | const fetchProducts = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const res = await productTreeList(); |
| | | // è¿æ»¤æ ¹èç¹äº§å |
| | | products.value = res |
| | | .filter(item => item.parentId === null) |
| | | .map(({ id, productName }) => ({ id, productName })); |
| | | tabs.value = products.value.map(p => ({ name: p.productName })); |
| | | |
| | | if (products.value.length > 0) { |
| | | activeTab.value = 0; |
| | | initializedTabs.value = [products.value[0].id]; |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | fetchProducts(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 100vh; |
| | | background-color: #f8f9fa; |
| | | } |
| | | .swiper-box { |
| | | flex: 1; |
| | | } |
| | | .swiper-item { |
| | | height: 100%; |
| | | } |
| | | :deep(.up-tabs) { |
| | | background-color: #fff; |
| | | } |
| | | .app-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 100vh; |
| | | background-color: #f8f9fa; |
| | | } |
| | | .loading-state { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .swiper-box { |
| | | flex: 1; |
| | | } |
| | | .swiper-item { |
| | | height: 100%; |
| | | } |
| | | :deep(.up-tabs) { |
| | | background-color: #fff; |
| | | } |
| | | </style> |
| | |
| | | clearable |
| | | type="password"></up-input> |
| | | </view> |
| | | <!-- <view class="input-item flex align-center" |
| | | v-if="factoryList.length > 0"> |
| | | <up-input prefixIcon="home" |
| | | placeholder="è¯·éæ©å·¥å" |
| | | border="bottom" |
| | | readonly |
| | | @click="showFactorySelect = true" |
| | | v-model="selectedFactoryName" |
| | | clearable></up-input> |
| | | <up-action-sheet :show="showFactorySelect" |
| | | :actions="factoryList" |
| | | title="è¯·éæ©å·¥å" |
| | | @close="showFactorySelect = false" |
| | | @select="handleFactorySelect"></up-action-sheet> |
| | | </view> --> |
| | | <view> |
| | | <button @click="handleLogin" |
| | | class="login-btn cu-btn block bg-blue lg round">ç»å½</button> |
| | |
| | | const loginForm = ref({ |
| | | userName: "", |
| | | password: "", |
| | | currentFatoryName: "", |
| | | factoryId: "", |
| | | }); |
| | | const factoryList = ref([]); // å
¬å¸å表 |
| | | const showFactorySelect = ref(false); |
| | | const selectedFactoryName = ref(""); |
| | | |
| | | const handleFactorySelect = e => { |
| | | loginForm.value.factoryId = e.id; |
| | | selectedFactoryName.value = e.name; |
| | | showFactorySelect.value = false; |
| | | }; |
| | | |
| | | // ä¿åå¯ç å°æ¬å°åå¨ |
| | | function savePassword() { |
| | |
| | | id: item.deptId, |
| | | name: item.deptName, |
| | | })); |
| | | // å¦æåªæä¸ä¸ªå·¥åï¼é»è®¤éä¸ |
| | | if (factoryList.value.length === 1) { |
| | | loginForm.value.factoryId = factoryList.value[0].id; |
| | | selectedFactoryName.value = factoryList.value[0].name; |
| | | } |
| | | } else { |
| | | // 妿res.data䏿¯æ°ç»ï¼è®¾ç½®ä¸ºç©ºæ°ç» |
| | | factoryList.value = []; |
| | | loginForm.value.factoryId = ""; |
| | | selectedFactoryName.value = ""; |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | showToast("è·åå
¬å¸å表失败:", error); |
| | | factoryList.value = []; |
| | | loginForm.value.factoryId = ""; |
| | | selectedFactoryName.value = ""; |
| | | }); |
| | | } else { |
| | | factoryList.value = []; |
| | | loginForm.value.factoryId = ""; |
| | | selectedFactoryName.value = ""; |
| | | } |
| | | } |
| | | |
| | |
| | | showToast("请è¾å
¥æ¨çè´¦å·"); |
| | | } else if (loginForm.value.password === "") { |
| | | showToast("请è¾å
¥æ¨çå¯ç "); |
| | | } else if (factoryList.value.length > 0 && loginForm.value.factoryId === "") { |
| | | showToast("è¯·éæ©å·¥å"); |
| | | } else { |
| | | showToast("ç»å½ä¸ï¼è¯·èå¿çå¾
..."); |
| | | pwdLogin(); |
| | |
| | | const accountInfo = uni.getAccountInfoSync(); |
| | | if (accountInfo?.miniProgram?.version) { |
| | | versionName.value = accountInfo.miniProgram.version; |
| | | console.log("[login-version] å½åç¯å¢=MP-WEIXINï¼çæ¬=", versionName.value); |
| | | console.log( |
| | | "[login-version] å½åç¯å¢=MP-WEIXINï¼çæ¬=", |
| | | versionName.value |
| | | ); |
| | | } |
| | | } catch (e) { |
| | | // è·å失败æ¶ä½¿ç¨é»è®¤å¼ |
| | |
| | | // @ts-ignore |
| | | const appid = plus.runtime.appid; |
| | | // @ts-ignore |
| | | plus.runtime.getProperty(appid, (info) => { |
| | | plus.runtime.getProperty(appid, info => { |
| | | const v = info?.version || info?.versionName || ""; |
| | | if (v) { |
| | | versionName.value = String(v); |
| | | console.log("[login-version] å½åç¯å¢=APP-PLUSï¼çæ¬=", versionName.value); |
| | | console.log( |
| | | "[login-version] å½åç¯å¢=APP-PLUSï¼çæ¬=", |
| | | versionName.value |
| | | ); |
| | | } else { |
| | | console.log("[login-version] APP-PLUS è·åå°ççæ¬åæ®µä¸ºç©ºï¼ä½¿ç¨é»è®¤å¼:", versionName.value); |
| | | console.log( |
| | | "[login-version] APP-PLUS è·åå°ççæ¬åæ®µä¸ºç©ºï¼ä½¿ç¨é»è®¤å¼:", |
| | | versionName.value |
| | | ); |
| | | } |
| | | console.log("[login-version] æç»çæ¬å·:", versionName.value); |
| | | }); |
| | | } else { |
| | | console.log("[login-version] APP-PLUS ç¯å¢ä¸ç¼ºå° getPropertyï¼ä½¿ç¨é»è®¤å¼:", versionName.value); |
| | | console.log( |
| | | "[login-version] APP-PLUS ç¯å¢ä¸ç¼ºå° getPropertyï¼ä½¿ç¨é»è®¤å¼:", |
| | | versionName.value |
| | | ); |
| | | console.log("[login-version] æç»çæ¬å·:", versionName.value); |
| | | } |
| | | // #endif |
| | |
| | | const handleTabChange = val => { |
| | | console.log(val); |
| | | activeTab.value = val.id; |
| | | page.current = 2; |
| | | page.current = 1; |
| | | loadMessages(false); |
| | | }; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 审æ¹å®ä¾è¯¦æ
å±ç¤ºï¼åºæ¬ä¿¡æ¯ + å¡«æ¥ + æµç¨ + 审æ¹è®°å½ |
| | | --> |
| | | <template> |
| | | <view class="detail-body"> |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">åºæ¬ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="info-rows"> |
| | | <view class="info-row"> |
| | | <text class="info-label">ä¸å¡åå·</text> |
| | | <text class="info-value">{{ row.instanceNo || row.id || "â" }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">审æ¹ç¶æ</text> |
| | | <u-tag :type="statusTagType(row.status)" |
| | | :text="statusLabel(row.status)" |
| | | size="mini" /> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">模æ¿åç§°</text> |
| | | <text class="info-value">{{ row.templateName || "â" }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">ä¸å¡åç§°</text> |
| | | <text class="info-value">{{ row.businessName || "â" }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">ç³è¯·äºº</text> |
| | | <text class="info-value">{{ row.applicantName || "â" }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">ç³è¯·æ é¢</text> |
| | | <text class="info-value">{{ row.title || "â" }}</text> |
| | | </view> |
| | | <view v-if="rejectReason" |
| | | class="info-row"> |
| | | <text class="info-label">驳ååå </text> |
| | | <text class="info-value reject-text">{{ rejectReason }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="info-label">ç³è¯·æ¶é´</text> |
| | | <text class="info-value">{{ formatDateTime(row.applyTime || row.createTime) }}</text> |
| | | </view> |
| | | <view v-if="row.finishTime" |
| | | class="info-row"> |
| | | <text class="info-label">宿æ¶é´</text> |
| | | <text class="info-value">{{ formatDateTime(row.finishTime) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">å¡«æ¥å
容</text> |
| | | </view> |
| | | <view v-if="displayFields.length" |
| | | class="info-rows"> |
| | | <view v-for="field in displayFields" |
| | | :key="field.key" |
| | | class="info-row"> |
| | | <text class="info-label">{{ field.label }}</text> |
| | | <text class="info-value">{{ displayFieldValue(field) }}</text> |
| | | </view> |
| | | <view v-for="(extra, idx) in moduleExtraRows" |
| | | :key="`extra-${idx}`" |
| | | class="info-row"> |
| | | <text class="info-label">{{ extra.label }}</text> |
| | | <text class="info-value">{{ extra.value }}</text> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-hint">ææ å¡«æ¥å
容</view> |
| | | </view> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">å®¡æ¹æµç¨ï¼{{ flowNodes.length }} 项ï¼</text> |
| | | </view> |
| | | <view v-if="flowNodes.length" |
| | | class="flow-wrap"> |
| | | <view v-for="(node, nodeIndex) in flowNodes" |
| | | :key="nodeIndex" |
| | | class="flow-node-block"> |
| | | <view class="flow-node-card"> |
| | | <view class="node-header"> |
| | | <view class="node-level-badge">{{ node.levelNo }}</view> |
| | | <text class="node-level-text">第{{ levelLabel(node.levelNo) }}级</text> |
| | | <u-tag size="mini" |
| | | :type="node.approveType === 'OR' ? 'warning' : 'primary'" |
| | | :text="node.approveType === 'OR' ? 'æç¾' : 'ä¼ç¾'" |
| | | plain /> |
| | | </view> |
| | | <view class="approver-list"> |
| | | <view v-for="(a, aIdx) in node.approvers" |
| | | :key="aIdx" |
| | | class="approver-row"> |
| | | <text class="approver-name">{{ a.approverName }}</text> |
| | | <u-tag v-if="a.taskStatus" |
| | | size="mini" |
| | | :type="taskStatusTagType(a.taskStatus)" |
| | | :text="taskStatusText(a.taskStatus)" |
| | | plain /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="nodeIndex < flowNodes.length - 1" |
| | | class="flow-connector-line" /> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-hint">ææ æµç¨èç¹</view> |
| | | </view> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">审æ¹è®°å½</text> |
| | | </view> |
| | | <view v-if="approvalRecords.length" |
| | | class="record-list"> |
| | | <view v-for="(rec, index) in approvalRecords" |
| | | :key="rec.id ?? index" |
| | | class="record-item"> |
| | | <view class="record-head"> |
| | | <text class="record-operator">{{ rec.operatorName }}</text> |
| | | <u-tag size="mini" |
| | | :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'error' : 'info'" |
| | | :text="recordActionLabel(rec.result)" |
| | | plain /> |
| | | </view> |
| | | <text class="record-time">{{ rec.time }}</text> |
| | | <text class="record-opinion">{{ rec.opinion || "æ æè§" }}</text> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-hint">ææ å®¡æ¹è®°å½</view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../../_utils/approvalModuleRegistry.js"; |
| | | import { |
| | | computeLeaveDurationDisplay, |
| | | computeOvertimeHoursDisplay, |
| | | } from "../../../_utils/approvalModuleApplyExtras.js"; |
| | | import { resolveInstanceFormPayload } from "../../../_utils/approvalModuleListSearch.js"; |
| | | import { |
| | | businessStatusTagType, |
| | | businessStatusText, |
| | | displayFieldValue, |
| | | formatDateTime, |
| | | getRejectReasonFromRecords, |
| | | instanceStatusTagType, |
| | | instanceStatusText, |
| | | mapApprovalRecords, |
| | | mapTasksToFlowNodes, |
| | | recordActionLabel, |
| | | resolveInstanceDisplayFields, |
| | | taskStatusTagType, |
| | | taskStatusText, |
| | | } from "../../../_utils/approveListUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | moduleKey: { type: String, default: "" }, |
| | | }); |
| | | |
| | | const LEVEL_TEXT = ["", "ä¸", "äº", "ä¸", "å", "äº", "å
", "ä¸", "å
«", "ä¹", "å"]; |
| | | |
| | | const isBusinessModule = computed(() => |
| | | [ |
| | | APPROVAL_MODULE_KEYS.LEAVE, |
| | | APPROVAL_MODULE_KEYS.OVERTIME, |
| | | APPROVAL_MODULE_KEYS.TRANSFER, |
| | | APPROVAL_MODULE_KEYS.REGULAR, |
| | | APPROVAL_MODULE_KEYS.WORK_HANDOVER, |
| | | ].includes(props.moduleKey) |
| | | ); |
| | | |
| | | const statusLabel = status => |
| | | isBusinessModule.value ? businessStatusText(status) : instanceStatusText(status); |
| | | |
| | | const statusTagType = status => |
| | | isBusinessModule.value ? businessStatusTagType(status) : instanceStatusTagType(status); |
| | | |
| | | const displayFields = computed(() => resolveInstanceDisplayFields(props.row)); |
| | | |
| | | const moduleExtraRows = computed(() => { |
| | | const rows = []; |
| | | const { fields, formPayload } = resolveInstanceFormPayload(props.row); |
| | | const payload = { ...formPayload }; |
| | | (fields || []).forEach(f => { |
| | | if (f?.key && payload[f.key] == null) payload[f.key] = f.value ?? ""; |
| | | }); |
| | | if (props.moduleKey === APPROVAL_MODULE_KEYS.LEAVE) { |
| | | const balance = payload.leaveBalanceDays; |
| | | if (balance != null && balance !== "") { |
| | | rows.push({ label: "åæä½é¢", value: `${balance} 天` }); |
| | | } |
| | | const days = computeLeaveDurationDisplay(fields, payload); |
| | | if (days) rows.push({ label: "è¯·åæ¶é¿", value: `${days} 天` }); |
| | | } |
| | | if (props.moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) { |
| | | const hours = computeOvertimeHoursDisplay(fields, payload); |
| | | if (hours) rows.push({ label: "å çæ¶é¿", value: `${hours} å°æ¶` }); |
| | | } |
| | | if (props.moduleKey === APPROVAL_MODULE_KEYS.TRANSFER) { |
| | | const post = payload.originalPostName || payload.originalPost; |
| | | if (post) rows.push({ label: "åå²ä½", value: post }); |
| | | } |
| | | return rows; |
| | | }); |
| | | |
| | | const flowNodes = computed(() => mapTasksToFlowNodes(props.row?.tasks)); |
| | | |
| | | const approvalRecords = computed(() => |
| | | mapApprovalRecords(props.row?.records) |
| | | ); |
| | | |
| | | const rejectReason = computed(() => |
| | | getRejectReasonFromRecords(props.row?.records) |
| | | ); |
| | | |
| | | const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | $primary: #2979ff; |
| | | $text: #1f2d3d; |
| | | $text-muted: #909399; |
| | | |
| | | .detail-body { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .section-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05); |
| | | } |
| | | |
| | | .section-head { |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | padding-left: 10px; |
| | | border-left: 3px solid $primary; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .info-rows { |
| | | padding: 4px 16px 12px; |
| | | } |
| | | |
| | | .info-row { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #f5f6f8; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .info-label { |
| | | flex-shrink: 0; |
| | | font-size: 14px; |
| | | color: $text-muted; |
| | | min-width: 72px; |
| | | } |
| | | |
| | | .info-value { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: $text; |
| | | text-align: right; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .reject-text { |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | .flow-wrap { |
| | | padding: 10px 16px 14px; |
| | | } |
| | | |
| | | .flow-node-block { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .flow-node-card { |
| | | background: #fafbfd; |
| | | border: 1px solid #e8eef5; |
| | | border-radius: 10px; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .node-header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .node-level-badge { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 8px; |
| | | background: $primary; |
| | | color: #fff; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .node-level-text { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .approver-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .approver-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .approver-name { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .flow-connector-line { |
| | | width: 2px; |
| | | height: 12px; |
| | | background: #d0dff0; |
| | | margin: 4px auto; |
| | | } |
| | | |
| | | .record-list { |
| | | padding: 8px 16px 14px; |
| | | } |
| | | |
| | | .record-item { |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .record-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .record-operator { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .record-time { |
| | | display: block; |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .record-opinion { |
| | | display: block; |
| | | margin-top: 6px; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .empty-hint { |
| | | padding: 12px 16px 16px; |
| | | font-size: 13px; |
| | | color: $text-muted; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / åèµ·å®¡æ¹ |
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-list/apply |
| | | --> |
| | | <template> |
| | | <view class="approve-apply-page"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | |
| | | <scroll-view class="form-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <view v-if="loading" |
| | | class="loading-wrap"> |
| | | <up-loading-icon mode="circle" /> |
| | | <text class="loading-text">å è½½ä¸...</text> |
| | | </view> |
| | | <template v-else-if="detail"> |
| | | <up-form :model="form" |
| | | label-width="100" |
| | | input-align="right"> |
| | | <u-cell-group title="åºæ¬ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="å®¡æ¹æ é¢" |
| | | required |
| | | class="form-item-name"> |
| | | <up-input v-model="form.title" |
| | | class="name-input-inline" |
| | | placeholder="请è¾å
¥å®¡æ¹æ é¢" |
| | | maxlength="100" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="å®¡æ¹æ¨¡æ¿" |
| | | class="form-item-readonly"> |
| | | <up-input :model-value="templateName" |
| | | readonly /> |
| | | </up-form-item> |
| | | <up-form-item label="ç³è¯·äºº" |
| | | class="form-item-readonly"> |
| | | <up-input :model-value="displayApplicantName" |
| | | readonly /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">å¡«æ¥å
容</text> |
| | | </view> |
| | | <view v-if="formConfigData.prompt" |
| | | class="form-prompt"> |
| | | {{ formConfigData.prompt }} |
| | | </view> |
| | | <up-form v-if="formConfigData.fields.length" |
| | | :model="formValues" |
| | | label-width="100" |
| | | input-align="right" |
| | | class="dynamic-form"> |
| | | <up-form-item v-for="field in displayTemplateFields" |
| | | :key="field.key" |
| | | :label="field.label" |
| | | :required="!!field.required" |
| | | :label-position="formItemLabelPosition(field)" |
| | | :class="formItemClass(field)"> |
| | | <up-textarea v-if="isTextareaField(field)" |
| | | v-model="formValues[field.key]" |
| | | :placeholder="`请è¾å
¥${field.label}`" |
| | | maxlength="500" |
| | | border="surround" |
| | | height="80" /> |
| | | <view v-else-if="isDatetimerangeField(field)" |
| | | class="daterange-fill"> |
| | | <view class="range-fill-row" |
| | | @click="openRangePicker(field, 'start')"> |
| | | <text class="range-fill-label">å¼å§</text> |
| | | <up-input :model-value="getRangePartDisplay(field, 'start')" |
| | | placeholder="å¼å§æ¶é´" |
| | | readonly /> |
| | | <up-icon name="calendar" |
| | | size="16" |
| | | color="#909399" /> |
| | | </view> |
| | | <text class="range-fill-sep">è³</text> |
| | | <view class="range-fill-row" |
| | | @click="openRangePicker(field, 'end')"> |
| | | <text class="range-fill-label">ç»æ</text> |
| | | <up-input :model-value="getRangePartDisplay(field, 'end')" |
| | | placeholder="ç»ææ¶é´" |
| | | readonly /> |
| | | <up-icon name="calendar" |
| | | size="16" |
| | | color="#909399" /> |
| | | </view> |
| | | </view> |
| | | <view v-else-if="isDateLikeField(field)" |
| | | class="field-trigger" |
| | | @click="openDatePicker(field)"> |
| | | <up-input :model-value="formatFieldDisplayValue(field, formValues[field.key])" |
| | | :placeholder="`è¯·éæ©${field.label}`" |
| | | readonly /> |
| | | <up-icon :name="getDatePickerMode(field) === 'time' ? 'clock' : 'calendar'" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | <view v-else-if="isSelectField(field)" |
| | | class="field-trigger" |
| | | @click="openSelectPicker(field)"> |
| | | <up-input :model-value="getSelectDisplayText(field)" |
| | | :placeholder="`è¯·éæ©${field.label}`" |
| | | readonly /> |
| | | <up-icon name="arrow-right" |
| | | size="16" |
| | | color="#c0c4cc" /> |
| | | </view> |
| | | <up-input v-else |
| | | v-model="formValues[field.key]" |
| | | :type="isNumberField(field) ? 'digit' : 'text'" |
| | | :placeholder="`请è¾å
¥${field.label}`" |
| | | clearable /> |
| | | </up-form-item> |
| | | </up-form> |
| | | <view v-else |
| | | class="empty-hint">è¯¥æ¨¡æ¿ææ å¡«æ¥é¡¹</view> |
| | | |
| | | <!-- 请åï¼åæä½é¢ + æ¶é¿èªå¨è®¡ç® --> |
| | | <view v-if="isLeaveModule" |
| | | class="module-extra-block"> |
| | | <up-form :model="extraForm" |
| | | label-width="100" |
| | | input-align="right" |
| | | class="dynamic-form"> |
| | | <up-form-item label="åæä½é¢" |
| | | required |
| | | class="form-item-inline"> |
| | | <up-input v-model="extraForm.leaveBalanceDays" |
| | | type="digit" |
| | | placeholder="请è¾å
¥å¤©æ°" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="è¯·åæ¶é¿" |
| | | class="form-item-inline"> |
| | | <view class="readonly-with-unit"> |
| | | <up-input :model-value="leaveDurationText" |
| | | readonly |
| | | placeholder="æ ¹æ®è¯·åæ¶é´èªå¨è®¡ç®" /> |
| | | <text class="unit-text">天</text> |
| | | </view> |
| | | </up-form-item> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <!-- å çï¼æ¶é¿èªå¨è®¡ç® --> |
| | | <view v-if="isOvertimeModule" |
| | | class="module-extra-block"> |
| | | <up-form label-width="100" |
| | | input-align="right" |
| | | class="dynamic-form"> |
| | | <up-form-item label="å çæ¶é¿" |
| | | class="form-item-inline"> |
| | | <view class="readonly-with-unit"> |
| | | <up-input :model-value="overtimeHoursText" |
| | | readonly |
| | | placeholder="æ ¹æ®å çæ¶é´èªå¨è®¡ç®" /> |
| | | <text class="unit-text">å°æ¶</text> |
| | | </view> |
| | | </up-form-item> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <!-- è°å²ï¼åå²ä½èªå¨å¸¦åº --> |
| | | <view v-if="isTransferModule" |
| | | class="module-extra-block"> |
| | | <up-form label-width="100" |
| | | input-align="right" |
| | | class="dynamic-form"> |
| | | <up-form-item label="åå²ä½" |
| | | class="form-item-readonly"> |
| | | <up-input :model-value="extraForm.originalPostName" |
| | | readonly |
| | | placeholder="éæ©ç³è¯·äººåèªå¨å¸¦åº" /> |
| | | </up-form-item> |
| | | </up-form> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">å®¡æ¹æµç¨</text> |
| | | </view> |
| | | <view v-if="detail.nodes?.length" |
| | | class="flow-wrap"> |
| | | <view v-for="(node, nodeIndex) in detail.nodes" |
| | | :key="node.id || nodeIndex" |
| | | class="flow-node-block"> |
| | | <view class="flow-node-card"> |
| | | <view class="node-header"> |
| | | <view class="node-level-badge">{{ node.levelNo || nodeIndex + 1 }}</view> |
| | | <text class="node-level-text">第{{ levelLabel(node.levelNo || nodeIndex + 1) }}级</text> |
| | | </view> |
| | | <view class="approve-type-row approve-type-row--readonly"> |
| | | <view class="type-btn" |
| | | :class="{ active: node.approveType !== 'OR' }"> |
| | | ä¼ç¾ |
| | | </view> |
| | | <view class="type-btn" |
| | | :class="{ active: node.approveType === 'OR' }"> |
| | | æç¾ |
| | | </view> |
| | | </view> |
| | | <view class="approver-list"> |
| | | <view v-for="(approver, aIdx) in node.approvers || []" |
| | | :key="approver.id || aIdx" |
| | | class="approver-chip"> |
| | | <view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view> |
| | | <text class="approver-name">{{ approver.approverName || "-" }}</text> |
| | | </view> |
| | | <text v-if="!(node.approvers || []).length" |
| | | class="empty-hint inline">ææ å®¡æ¹äºº</text> |
| | | </view> |
| | | </view> |
| | | <view v-if="nodeIndex < detail.nodes.length - 1" |
| | | class="flow-connector"> |
| | | <view class="flow-connector-line" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-hint">ææ å®¡æ¹èç¹</view> |
| | | </view> |
| | | </template> |
| | | <view v-else |
| | | class="empty-wrap"> |
| | | <up-empty mode="data" |
| | | text="æªè·åå°æ¨¡æ¿è¯¦æ
" /> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <FooterButtons v-if="!loading && detail" |
| | | cancel-text="åæ¶" |
| | | :confirm-text="confirmText" |
| | | :loading="submitting" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | |
| | | <up-popup :show="showDatePicker" |
| | | mode="bottom" |
| | | @close="showDatePicker = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="datePickerTs" |
| | | :mode="datePickerMode" |
| | | @confirm="onDateConfirm" |
| | | @cancel="onDatePickerCancel" /> |
| | | </up-popup> |
| | | |
| | | <up-action-sheet :show="showSelectSheet" |
| | | :title="selectSheetTitle" |
| | | :actions="selectSheetActions" |
| | | @select="onSelectOption" |
| | | @close="showSelectSheet = false" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js"; |
| | | import { |
| | | saveApprovalInstance, |
| | | updateApprovalInstance, |
| | | } from "@/api/oa/approvalInstance.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import { findPostOptions } from "@/api/system/post.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js"; |
| | | import { |
| | | computeLeaveDurationDisplay, |
| | | computeOvertimeHoursDisplay, |
| | | displayTemplateFieldsByModule, |
| | | findApplicantTemplateField, |
| | | findLeaveTimeTemplateField, |
| | | findOvertimeTimeTemplateField, |
| | | inferModuleKeyFromRow, |
| | | loadModuleExtrasFromRow, |
| | | resolveOriginalPostName, |
| | | syncModuleExtrasToFormValues, |
| | | unwrapUserArray, |
| | | userById, |
| | | validateModuleExtras, |
| | | buildPostIdToNameMap, |
| | | } from "../../_utils/approvalModuleApplyExtras.js"; |
| | | import { |
| | | formatDatetimerangeDisplay, |
| | | formatFieldDateValue, |
| | | formatFieldDisplayValue, |
| | | getDatePickerMode, |
| | | getFieldInitialValue, |
| | | getFieldOptionLabel, |
| | | isDatetimerangeField, |
| | | isDateLikeField, |
| | | isNumberField, |
| | | isSelectField, |
| | | isTextareaField, |
| | | joinDatetimerangeValue, |
| | | mergeFormConfigForEdit, |
| | | parseDatetimerangeValue, |
| | | resolveFieldOptions, |
| | | parseApprovalFormConfig, |
| | | parseFieldDateToTs, |
| | | } from "../../_utils/approvalFormField.js"; |
| | | |
| | | import { EDIT_STORAGE_KEY } from "../../_utils/approveListUtils.js"; |
| | | |
| | | const LEVEL_TEXT = ["", "ä¸", "äº", "ä¸", "å", "äº", "å
", "ä¸", "å
«", "ä¹", "å"]; |
| | | |
| | | const userStore = useUserStore(); |
| | | const moduleKey = ref(""); |
| | | const templateId = ref(""); |
| | | const instanceId = ref(""); |
| | | const instanceRow = ref(null); |
| | | const detail = ref(null); |
| | | const loading = ref(false); |
| | | const submitting = ref(false); |
| | | const formValues = reactive({}); |
| | | const form = reactive({ title: "" }); |
| | | const extraForm = reactive({ |
| | | leaveBalanceDays: undefined, |
| | | originalPostName: "", |
| | | }); |
| | | const postIdToName = ref({}); |
| | | const transferUserPool = ref([]); |
| | | |
| | | const isLeaveModule = computed( |
| | | () => moduleKey.value === APPROVAL_MODULE_KEYS.LEAVE |
| | | ); |
| | | const isOvertimeModule = computed( |
| | | () => moduleKey.value === APPROVAL_MODULE_KEYS.OVERTIME |
| | | ); |
| | | const isTransferModule = computed( |
| | | () => moduleKey.value === APPROVAL_MODULE_KEYS.TRANSFER |
| | | ); |
| | | |
| | | const showDatePicker = ref(false); |
| | | const datePickerTs = ref(Date.now()); |
| | | const activeDateField = ref(null); |
| | | const activeRangePart = ref("start"); |
| | | |
| | | const datePickerMode = computed(() => { |
| | | const field = activeDateField.value; |
| | | if (!field) return "date"; |
| | | if (isDatetimerangeField(field)) return "datetime"; |
| | | return getDatePickerMode(field); |
| | | }); |
| | | |
| | | const showSelectSheet = ref(false); |
| | | const activeSelectField = ref(null); |
| | | const pickerUserList = ref([]); |
| | | const pickerDeptList = ref([]); |
| | | |
| | | const isEditMode = computed(() => !!instanceId.value); |
| | | |
| | | const pageTitle = computed(() => (isEditMode.value ? "ä¿®æ¹å®¡æ¹" : "å起审æ¹")); |
| | | const confirmText = computed(() => (isEditMode.value ? "ä¿åä¿®æ¹" : "æäº¤å®¡æ¹")); |
| | | |
| | | const applicantName = computed( |
| | | () => userStore.nickName || userStore.name || "-" |
| | | ); |
| | | |
| | | const displayApplicantName = computed( |
| | | () => instanceRow.value?.applicantName || applicantName.value |
| | | ); |
| | | |
| | | const templateName = computed( |
| | | () => detail.value?.templateName || instanceRow.value?.templateName || "-" |
| | | ); |
| | | |
| | | const formConfigData = computed(() => { |
| | | if (isEditMode.value) { |
| | | return mergeFormConfigForEdit( |
| | | detail.value?.formConfig, |
| | | instanceRow.value?.formConfig |
| | | ); |
| | | } |
| | | return parseApprovalFormConfig(detail.value?.formConfig); |
| | | }); |
| | | |
| | | const displayTemplateFields = computed(() => |
| | | displayTemplateFieldsByModule(moduleKey.value, formConfigData.value.fields) |
| | | ); |
| | | |
| | | const leaveDurationText = computed(() => { |
| | | if (!isLeaveModule.value) return ""; |
| | | const fields = formConfigData.value.fields; |
| | | const timeField = findLeaveTimeTemplateField(fields); |
| | | if (timeField?.key) void formValues[timeField.key]; |
| | | return computeLeaveDurationDisplay(fields, formValues); |
| | | }); |
| | | |
| | | const overtimeHoursText = computed(() => { |
| | | if (!isOvertimeModule.value) return ""; |
| | | const fields = formConfigData.value.fields; |
| | | const timeField = findOvertimeTimeTemplateField(fields); |
| | | if (timeField?.key) void formValues[timeField.key]; |
| | | return computeOvertimeHoursDisplay(fields, formValues); |
| | | }); |
| | | |
| | | const applicantPickerValue = computed(() => { |
| | | const f = findApplicantTemplateField(formConfigData.value.fields); |
| | | return f?.key ? formValues[f.key] : undefined; |
| | | }); |
| | | |
| | | const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n); |
| | | |
| | | const selectSheetTitle = computed( |
| | | () => (activeSelectField.value?.label ? `éæ©${activeSelectField.value.label}` : "è¯·éæ©") |
| | | ); |
| | | |
| | | const selectSheetActions = computed(() => { |
| | | const field = activeSelectField.value; |
| | | if (!field) return []; |
| | | return resolveFieldOptions(field, { |
| | | users: pickerUserList.value, |
| | | depts: pickerDeptList.value, |
| | | }).map(opt => ({ |
| | | name: opt.label, |
| | | value: opt.value, |
| | | })); |
| | | }); |
| | | |
| | | const formItemClass = field => { |
| | | if (isTextareaField(field)) return "form-item-textarea"; |
| | | if (isDatetimerangeField(field)) return "form-item-daterange"; |
| | | if (isSelectField(field) || isDateLikeField(field)) return "form-item-select"; |
| | | return "form-item-inline"; |
| | | }; |
| | | |
| | | /** å¤è¡ææ¬ãæ¥æèå´ï¼æ ç¾ç½®é¡¶ï¼é¿å
é¿ææ¡å¨çªåå
æè¡ */ |
| | | const formItemLabelPosition = field => { |
| | | if (isTextareaField(field) || isDatetimerangeField(field)) return "top"; |
| | | return "left"; |
| | | }; |
| | | |
| | | const getRangePartDisplay = (field, part) => { |
| | | const parts = parseDatetimerangeValue(formValues[field.key]); |
| | | const val = part === "start" ? parts.start : parts.end; |
| | | return val ? formatFieldDisplayValue({ type: "datetime" }, val) : ""; |
| | | }; |
| | | |
| | | const openRangePicker = (field, part) => { |
| | | activeDateField.value = field; |
| | | activeRangePart.value = part; |
| | | const parts = parseDatetimerangeValue(formValues[field.key]); |
| | | const val = part === "start" ? parts.start : parts.end; |
| | | datePickerTs.value = parseFieldDateToTs(val) ?? Date.now(); |
| | | showDatePicker.value = true; |
| | | }; |
| | | |
| | | const getSelectDisplayText = field => { |
| | | const stored = formValues[field.key]; |
| | | const options = resolveFieldOptions(field, { |
| | | users: pickerUserList.value, |
| | | depts: pickerDeptList.value, |
| | | }); |
| | | const matched = options.find( |
| | | opt => |
| | | String(opt.value) === String(stored) || String(opt.label) === String(stored) |
| | | ); |
| | | return ( |
| | | matched?.label || |
| | | getFieldOptionLabel(field, stored) || |
| | | (stored !== undefined && stored !== null ? String(stored) : "") |
| | | ); |
| | | }; |
| | | |
| | | const initFormValues = fields => { |
| | | Object.keys(formValues).forEach(key => { |
| | | delete formValues[key]; |
| | | }); |
| | | fields.forEach(field => { |
| | | if (!field?.key) return; |
| | | formValues[field.key] = getFieldInitialValue(field); |
| | | }); |
| | | }; |
| | | |
| | | const openSelectPicker = field => { |
| | | const options = resolveFieldOptions(field, { |
| | | users: pickerUserList.value, |
| | | depts: pickerDeptList.value, |
| | | }); |
| | | if (!options.length) { |
| | | uni.showToast({ title: "è¯¥åæ®µæªé
ç½®ä¸æé项", icon: "none" }); |
| | | return; |
| | | } |
| | | activeSelectField.value = field; |
| | | showSelectSheet.value = true; |
| | | }; |
| | | |
| | | const onSelectOption = action => { |
| | | const key = activeSelectField.value?.key; |
| | | if (key) { |
| | | formValues[key] = action.value; |
| | | } |
| | | showSelectSheet.value = false; |
| | | activeSelectField.value = null; |
| | | }; |
| | | |
| | | const openDatePicker = field => { |
| | | activeDateField.value = field; |
| | | const current = formValues[field.key]; |
| | | datePickerTs.value = parseFieldDateToTs(current) ?? Date.now(); |
| | | showDatePicker.value = true; |
| | | }; |
| | | |
| | | const onDatePickerCancel = () => { |
| | | showDatePicker.value = false; |
| | | activeDateField.value = null; |
| | | }; |
| | | |
| | | const onDateConfirm = e => { |
| | | const ts = e?.value ?? datePickerTs.value; |
| | | const field = activeDateField.value; |
| | | if (field?.key) { |
| | | if (isDatetimerangeField(field)) { |
| | | const parts = parseDatetimerangeValue(formValues[field.key]); |
| | | const formatted = formatFieldDateValue({ type: "datetime" }, ts); |
| | | formValues[field.key] = joinDatetimerangeValue( |
| | | activeRangePart.value === "start" ? formatted : parts.start, |
| | | activeRangePart.value === "end" ? formatted : parts.end |
| | | ); |
| | | } else { |
| | | formValues[field.key] = formatFieldDateValue(field, ts); |
| | | } |
| | | } |
| | | onDatePickerCancel(); |
| | | }; |
| | | |
| | | const validateForm = () => { |
| | | if (!form.title?.trim()) { |
| | | uni.showToast({ title: "请è¾å
¥å®¡æ¹æ é¢", icon: "none" }); |
| | | return false; |
| | | } |
| | | for (const field of displayTemplateFields.value) { |
| | | if (!field.required) continue; |
| | | const val = formValues[field.key]; |
| | | if (val === undefined || val === null || String(val).trim() === "") { |
| | | const action = |
| | | isSelectField(field) || isDateLikeField(field) || isDatetimerangeField(field) |
| | | ? "è¯·éæ©" |
| | | : "请填å"; |
| | | uni.showToast({ title: `${action}${field.label}`, icon: "none" }); |
| | | return false; |
| | | } |
| | | if (isDatetimerangeField(field)) { |
| | | const { start, end } = parseDatetimerangeValue(val); |
| | | if (!start || !end) { |
| | | uni.showToast({ title: `è¯·å®æ´éæ©${field.label}`, icon: "none" }); |
| | | return false; |
| | | } |
| | | } |
| | | if (isSelectField(field)) { |
| | | const options = resolveFieldOptions(field, { |
| | | users: pickerUserList.value, |
| | | depts: pickerDeptList.value, |
| | | }); |
| | | if ( |
| | | options.length && |
| | | !options.some( |
| | | opt => |
| | | String(opt.value) === String(val) || String(opt.label) === String(val) |
| | | ) |
| | | ) { |
| | | uni.showToast({ title: `${field.label}éé¡¹æ æ`, icon: "none" }); |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | if (!detail.value?.nodes?.length) { |
| | | uni.showToast({ title: "æ¨¡æ¿æªé
ç½®å®¡æ¹æµç¨", icon: "none" }); |
| | | return false; |
| | | } |
| | | const moduleMsg = validateModuleExtras( |
| | | moduleKey.value, |
| | | formConfigData.value.fields, |
| | | formValues, |
| | | extraForm |
| | | ); |
| | | if (moduleMsg) { |
| | | uni.showToast({ title: moduleMsg, icon: "none" }); |
| | | return false; |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | const buildFormConfigPayload = () => { |
| | | syncModuleExtrasToFormValues( |
| | | moduleKey.value, |
| | | formValues, |
| | | extraForm, |
| | | formConfigData.value.fields |
| | | ); |
| | | const allFields = formConfigData.value.fields || []; |
| | | return JSON.stringify({ |
| | | prompt: formConfigData.value.prompt, |
| | | fields: allFields.map(field => ({ |
| | | ...field, |
| | | value: formValues[field.key] ?? "", |
| | | })), |
| | | }); |
| | | }; |
| | | |
| | | const buildSavePayload = () => ({ |
| | | templateId: detail.value.id, |
| | | templateName: detail.value.templateName, |
| | | businessType: detail.value.businessType, |
| | | title: form.title.trim(), |
| | | status: "PENDING", |
| | | currentLevel: 1, |
| | | applicantId: userStore.id, |
| | | applicantName: applicantName.value, |
| | | applyTime: parseTime(new Date()), |
| | | deptId: userStore.currentDeptId || undefined, |
| | | formConfig: buildFormConfigPayload(), |
| | | }); |
| | | |
| | | const buildUpdatePayload = () => { |
| | | const row = instanceRow.value || {}; |
| | | return { |
| | | id: instanceId.value, |
| | | instanceNo: row.instanceNo, |
| | | templateId: row.templateId ?? detail.value?.id, |
| | | templateName: row.templateName ?? detail.value?.templateName, |
| | | businessId: row.businessId, |
| | | businessType: row.businessType ?? detail.value?.businessType, |
| | | title: form.title.trim(), |
| | | status: row.status || "PENDING", |
| | | currentLevel: row.currentLevel, |
| | | applicantId: row.applicantId, |
| | | applicantName: row.applicantName, |
| | | applyTime: row.applyTime, |
| | | finishTime: row.finishTime, |
| | | createUser: row.createUser, |
| | | createTime: row.createTime, |
| | | updateUser: row.updateUser, |
| | | updateTime: row.updateTime, |
| | | deptId: row.deptId, |
| | | deleted: row.deleted, |
| | | formConfig: buildFormConfigPayload(), |
| | | approveAction: row.approveAction, |
| | | approveComment: row.approveComment, |
| | | }; |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | if (!validateForm() || submitting.value) return; |
| | | |
| | | submitting.value = true; |
| | | const submitApi = isEditMode.value |
| | | ? updateApprovalInstance |
| | | : saveApprovalInstance; |
| | | const payload = isEditMode.value ? buildUpdatePayload() : buildSavePayload(); |
| | | |
| | | submitApi(payload) |
| | | .then(() => { |
| | | uni.showToast({ |
| | | title: isEditMode.value ? "ä¿®æ¹æå" : "æäº¤æå", |
| | | icon: "success", |
| | | }); |
| | | if (isEditMode.value) { |
| | | uni.removeStorageSync(EDIT_STORAGE_KEY); |
| | | } |
| | | setTimeout(() => { |
| | | uni.navigateBack({ delta: isEditMode.value ? 1 : 2 }); |
| | | }, 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ |
| | | title: isEditMode.value ? "ä¿®æ¹å¤±è´¥" : "æäº¤å¤±è´¥", |
| | | icon: "none", |
| | | }); |
| | | }) |
| | | .finally(() => { |
| | | submitting.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const loadTemplateDetail = () => { |
| | | if (!templateId.value) return Promise.resolve(); |
| | | return getApprovalTemplateDetail(templateId.value) |
| | | .then(res => { |
| | | detail.value = res?.data || null; |
| | | if (!detail.value) { |
| | | uni.showToast({ title: "æªè·åå°æ¨¡æ¿è¯¦æ
", icon: "none" }); |
| | | } |
| | | return detail.value; |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "è·å模æ¿è¯¦æ
失败", icon: "none" }); |
| | | return null; |
| | | }); |
| | | }; |
| | | |
| | | const loadForCreate = async () => { |
| | | loading.value = true; |
| | | detail.value = null; |
| | | try { |
| | | await loadTemplateDetail(); |
| | | if (!detail.value) return; |
| | | initFormValues(displayTemplateFields.value); |
| | | resetModuleExtras(); |
| | | if (!form.title && detail.value.templateName) { |
| | | form.title = `${detail.value.templateName}ç³è¯·`; |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const loadForEdit = async () => { |
| | | const row = uni.getStorageSync(EDIT_STORAGE_KEY); |
| | | if (!row || String(row.id) !== String(instanceId.value)) { |
| | | uni.showToast({ title: "æªè·åå°å®¡æ¹æ°æ®", icon: "none" }); |
| | | setTimeout(() => uni.navigateBack(), 500); |
| | | return; |
| | | } |
| | | instanceRow.value = row; |
| | | if (!moduleKey.value) { |
| | | moduleKey.value = inferModuleKeyFromRow(row); |
| | | } |
| | | templateId.value = row.templateId; |
| | | form.title = row.title || ""; |
| | | |
| | | loading.value = true; |
| | | detail.value = null; |
| | | try { |
| | | await loadTemplateDetail(); |
| | | if (!detail.value) return; |
| | | initFormValues(displayTemplateFields.value); |
| | | applyModuleExtrasFromRow(); |
| | | if (isTransferModule.value) { |
| | | await ensureTransferLookupData(); |
| | | syncOriginalPostFromApplicant(applicantPickerValue.value); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | function resetModuleExtras() { |
| | | extraForm.leaveBalanceDays = undefined; |
| | | extraForm.originalPostName = ""; |
| | | } |
| | | |
| | | function applyModuleExtrasFromRow() { |
| | | const loaded = loadModuleExtrasFromRow( |
| | | moduleKey.value, |
| | | instanceRow.value, |
| | | formValues |
| | | ); |
| | | if (loaded.leaveBalanceDays != null) { |
| | | extraForm.leaveBalanceDays = loaded.leaveBalanceDays; |
| | | } |
| | | if (loaded.originalPostName) { |
| | | extraForm.originalPostName = loaded.originalPostName; |
| | | } |
| | | } |
| | | |
| | | async function ensureTransferLookupData() { |
| | | if (!transferUserPool.value.length) { |
| | | try { |
| | | const res = await userListNoPageByTenantId(); |
| | | transferUserPool.value = unwrapUserArray(res); |
| | | } catch { |
| | | transferUserPool.value = []; |
| | | } |
| | | } |
| | | if (!Object.keys(postIdToName.value).length) { |
| | | try { |
| | | const res = await findPostOptions(); |
| | | const rows = res?.data ?? res?.rows ?? []; |
| | | postIdToName.value = buildPostIdToNameMap(Array.isArray(rows) ? rows : []); |
| | | } catch { |
| | | postIdToName.value = {}; |
| | | } |
| | | } |
| | | } |
| | | |
| | | function syncOriginalPostFromApplicant(uid) { |
| | | if (!isTransferModule.value) return; |
| | | if (!uid) { |
| | | extraForm.originalPostName = ""; |
| | | return; |
| | | } |
| | | const user = userById(transferUserPool.value, uid); |
| | | extraForm.originalPostName = resolveOriginalPostName(user, postIdToName.value); |
| | | } |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const loadPickerSourceData = () => { |
| | | userListNoPageByTenantId() |
| | | .then(res => { |
| | | pickerUserList.value = res?.data || []; |
| | | }) |
| | | .catch(() => { |
| | | pickerUserList.value = []; |
| | | }); |
| | | getDept() |
| | | .then(res => { |
| | | pickerDeptList.value = res?.data || []; |
| | | }) |
| | | .catch(() => { |
| | | pickerDeptList.value = []; |
| | | }); |
| | | }; |
| | | |
| | | watch(applicantPickerValue, async uid => { |
| | | if (!isTransferModule.value) return; |
| | | await ensureTransferLookupData(); |
| | | syncOriginalPostFromApplicant(uid); |
| | | }); |
| | | |
| | | onLoad(options => { |
| | | moduleKey.value = options?.moduleKey || ""; |
| | | loadPickerSourceData(); |
| | | if (isTransferModule.value) { |
| | | ensureTransferLookupData(); |
| | | } |
| | | if (options?.id) { |
| | | instanceId.value = options.id; |
| | | loadForEdit(); |
| | | return; |
| | | } |
| | | if (options?.templateId) { |
| | | templateId.value = options.templateId; |
| | | loadForCreate(); |
| | | return; |
| | | } |
| | | uni.showToast({ title: "缺å°é¡µé¢åæ°", icon: "none" }); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | $primary: #2979ff; |
| | | $text: #1f2d3d; |
| | | $text-secondary: #606266; |
| | | $text-muted: #909399; |
| | | $bg-page: #f0f3f8; |
| | | $radius-lg: 12px; |
| | | $radius-md: 10px; |
| | | $shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05); |
| | | |
| | | .approve-apply-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | background: $bg-page; |
| | | } |
| | | |
| | | .form-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding: 10px 12px calc(96px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .loading-wrap { |
| | | padding: 48px 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .loading-text { |
| | | font-size: 14px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 10px; |
| | | border-radius: $radius-lg; |
| | | overflow: hidden; |
| | | box-shadow: $shadow-card; |
| | | } |
| | | |
| | | :deep(.form-section .u-cell-group__title) { |
| | | padding: 12px 16px 8px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: $text !important; |
| | | background: #fff !important; |
| | | } |
| | | |
| | | :deep(.form-section .u-form-item) { |
| | | padding: 0 16px !important; |
| | | } |
| | | |
| | | :deep(.form-section .u-form-item__body) { |
| | | padding: 10px 0 !important; |
| | | min-height: auto !important; |
| | | } |
| | | |
| | | :deep(.form-item-name .u-form-item__body) { |
| | | flex-direction: row !important; |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-name .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.name-input-inline), |
| | | :deep(.name-input-inline .u-input__content) { |
| | | width: 100% !important; |
| | | flex: 1 !important; |
| | | } |
| | | |
| | | :deep(.name-input-inline input), |
| | | :deep(.name-input-inline .u-input__content__field-wrapper__field) { |
| | | width: 100% !important; |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | :deep(.form-item-readonly .u-form-item__body) { |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-readonly .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.form-item-readonly .u-input__content__field-wrapper__field) { |
| | | text-align: right !important; |
| | | color: #303133 !important; |
| | | } |
| | | |
| | | .dynamic-form { |
| | | padding: 0 0 4px; |
| | | } |
| | | |
| | | :deep(.dynamic-form .u-form-item) { |
| | | padding: 0 16px !important; |
| | | } |
| | | |
| | | :deep(.dynamic-form .u-form-item__body) { |
| | | padding: 10px 0 !important; |
| | | min-height: auto !important; |
| | | } |
| | | |
| | | :deep(.form-item-inline .u-form-item__body) { |
| | | flex-direction: row !important; |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-inline .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.form-item-inline input), |
| | | :deep(.form-item-inline .u-input__content__field-wrapper__field) { |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | :deep(.form-item-select .u-form-item__body) { |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-select .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.form-item-textarea .u-form-item__body), |
| | | :deep(.form-item-daterange .u-form-item__body) { |
| | | flex-direction: column !important; |
| | | align-items: stretch !important; |
| | | padding: 10px 0 12px !important; |
| | | } |
| | | |
| | | :deep(.form-item-textarea .u-form-item__body__left), |
| | | :deep(.form-item-daterange .u-form-item__body__left) { |
| | | width: 100% !important; |
| | | max-width: 100% !important; |
| | | margin-bottom: 8px !important; |
| | | padding-right: 0 !important; |
| | | } |
| | | |
| | | :deep(.form-item-textarea .u-form-item__body__left__content__label), |
| | | :deep(.form-item-daterange .u-form-item__body__left__content__label) { |
| | | white-space: normal !important; |
| | | line-height: 1.45 !important; |
| | | font-size: 14px !important; |
| | | } |
| | | |
| | | :deep(.form-item-textarea .u-form-item__body__right), |
| | | :deep(.form-item-daterange .u-form-item__body__right) { |
| | | width: 100% !important; |
| | | flex: none !important; |
| | | } |
| | | |
| | | :deep(.form-item-textarea .u-form-item__content), |
| | | :deep(.form-item-daterange .u-form-item__content) { |
| | | width: 100% !important; |
| | | justify-content: stretch !important; |
| | | } |
| | | |
| | | :deep(.dynamic-form .u-form-item__body__left__content__label) { |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .field-trigger { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | gap: 6px; |
| | | width: 100%; |
| | | min-width: 0; |
| | | } |
| | | |
| | | :deep(.field-trigger .u-input) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | } |
| | | |
| | | :deep(.field-trigger .u-input__content__field-wrapper__field) { |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | .daterange-fill { |
| | | width: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .range-fill-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 8px 10px; |
| | | background: #f7f9fc; |
| | | border: 1px solid #eef1f6; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .range-fill-label { |
| | | flex-shrink: 0; |
| | | width: 36px; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .range-fill-sep { |
| | | font-size: 12px; |
| | | color: #c0c4cc; |
| | | text-align: center; |
| | | } |
| | | |
| | | :deep(.range-fill-row .u-input) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | } |
| | | |
| | | .section-card { |
| | | margin-bottom: 10px; |
| | | background: #fff; |
| | | border-radius: $radius-lg; |
| | | overflow: hidden; |
| | | box-shadow: $shadow-card; |
| | | } |
| | | |
| | | .section-head { |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | padding-left: 10px; |
| | | border-left: 3px solid $primary; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .form-prompt { |
| | | margin: 12px 16px 0; |
| | | padding: 10px 12px; |
| | | font-size: 13px; |
| | | color: $text-secondary; |
| | | background: #f8fafc; |
| | | border-radius: 8px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .flow-wrap { |
| | | padding: 10px 16px 14px; |
| | | } |
| | | |
| | | .flow-node-block { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .flow-node-card { |
| | | background: #fafbfd; |
| | | border: 1px solid #e8eef5; |
| | | border-radius: $radius-md; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .node-header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .node-level-badge { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 8px; |
| | | background: $primary; |
| | | color: #fff; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .node-level-text { |
| | | flex: 1; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .approve-type-row { |
| | | display: flex; |
| | | background: #f0f3f8; |
| | | border-radius: 8px; |
| | | padding: 3px; |
| | | margin-bottom: 10px; |
| | | |
| | | &--readonly { |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | |
| | | .type-btn { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 8px 0; |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | border-radius: 6px; |
| | | |
| | | &.active { |
| | | background: #fff; |
| | | color: $primary; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .approver-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .approver-chip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 6px 12px 6px 6px; |
| | | background: #fff; |
| | | border: 1px solid #dce8f8; |
| | | border-radius: 24px; |
| | | box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06); |
| | | } |
| | | |
| | | .approver-avatar { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .approver-name { |
| | | font-size: 13px; |
| | | color: $text; |
| | | max-width: 120px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .flow-connector { |
| | | display: flex; |
| | | justify-content: center; |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .flow-connector-line { |
| | | width: 2px; |
| | | height: 14px; |
| | | background: #d0dff0; |
| | | } |
| | | |
| | | .empty-hint { |
| | | padding: 12px 16px 16px; |
| | | font-size: 13px; |
| | | color: $text-muted; |
| | | |
| | | &.inline { |
| | | padding: 0; |
| | | } |
| | | } |
| | | |
| | | .empty-wrap { |
| | | padding: 48px 20px; |
| | | } |
| | | |
| | | .module-extra-block { |
| | | margin-top: 8px; |
| | | padding-top: 8px; |
| | | border-top: 1px dashed #e8ecf0; |
| | | } |
| | | |
| | | .readonly-with-unit { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | width: 100%; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .readonly-with-unit :deep(.u-input) { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .unit-text { |
| | | flex-shrink: 0; |
| | | font-size: 14px; |
| | | color: $text-muted; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / 审æ¹å¤ç |
| | | å·®æ
/è´¹ç¨æ¥éä½¿ç¨æ¥é详æ
+ 审æ¹å表 approve æ¥å£ |
| | | --> |
| | | <template> |
| | | <view class="oa-detail-page"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | |
| | | <scroll-view v-if="displayReady" |
| | | class="oa-detail-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <ReimburseInstanceDetailBody v-if="isReimburse" |
| | | :reimburse-row="reimburseRow" |
| | | :module-key="detailModuleKey" /> |
| | | |
| | | <ApproveInstanceDetailBody v-else |
| | | :row="row" |
| | | :module-key="detailModuleKey" /> |
| | | |
| | | <view class="section-card opinion-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">å®¡æ¹æè§</text> |
| | | </view> |
| | | <view class="opinion-wrap"> |
| | | <up-textarea v-model="approveOpinion" |
| | | placeholder="éè¿å¯ç空ï¼é©³å请填åå
·ä½åå " |
| | | maxlength="500" |
| | | count |
| | | height="100" |
| | | border="surround" /> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view v-else |
| | | class="oa-empty"> |
| | | <up-empty mode="data" |
| | | :text="loading ? 'å è½½ä¸' : 'æªè·åå°å®¡æ¹æ°æ®'" /> |
| | | </view> |
| | | |
| | | <view v-if="displayReady" |
| | | class="oa-page-footer"> |
| | | <text class="oa-footer-btn btn-default" |
| | | :class="{ 'is-disabled': submitting }" |
| | | @click="goBack">åæ¶</text> |
| | | <text class="oa-footer-btn btn-danger" |
| | | :class="{ 'is-disabled': submitting }" |
| | | @click="submitApprove('rejected')">驳å</text> |
| | | <text class="oa-footer-btn btn-success" |
| | | :class="{ 'is-disabled': submitting }" |
| | | @click="submitApprove('approved')">éè¿</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import ApproveInstanceDetailBody from "./_components/ApproveInstanceDetailBody.vue"; |
| | | import ReimburseInstanceDetailBody from "../../ReimburseManage/_components/ReimburseInstanceDetailBody.vue"; |
| | | import { approveApprovalInstance } from "@/api/oa/approvalInstance.js"; |
| | | import { |
| | | buildApproveInstanceDto, |
| | | canApproveInstance, |
| | | loadInstanceRow, |
| | | } from "../../_utils/approveListUtils.js"; |
| | | import { inferModuleKeyFromRow } from "../../_utils/approvalModuleApplyExtras.js"; |
| | | import { |
| | | inferReimburseModuleKeyFromInstance, |
| | | isReimburseApprovalInstance, |
| | | loadReimburseDetailForInstance, |
| | | } from "../../_utils/reimburseApproveBridge.js"; |
| | | import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js"; |
| | | |
| | | const row = ref(null); |
| | | const reimburseRow = ref(null); |
| | | const loading = ref(false); |
| | | const approveOpinion = ref(""); |
| | | const submitting = ref(false); |
| | | |
| | | const isReimburse = computed(() => isReimburseApprovalInstance(row.value)); |
| | | |
| | | const detailModuleKey = computed(() => { |
| | | if (isReimburse.value) { |
| | | return ( |
| | | reimburseRow.value?.moduleKey || |
| | | inferReimburseModuleKeyFromInstance(row.value) |
| | | ); |
| | | } |
| | | return inferModuleKeyFromRow(row.value); |
| | | }); |
| | | |
| | | const pageTitle = computed(() => { |
| | | if (isReimburse.value) { |
| | | const label = getApprovalModuleConfig(detailModuleKey.value)?.label || "æ¥é"; |
| | | return `${label}审æ¹`; |
| | | } |
| | | return "审æ¹å¤ç"; |
| | | }); |
| | | |
| | | const displayReady = computed(() => |
| | | isReimburse.value ? Boolean(reimburseRow.value) : Boolean(row.value) |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const submitApprove = uiResult => { |
| | | if (!row.value?.id || submitting.value) return; |
| | | |
| | | if (uiResult === "rejected" && !(approveOpinion.value || "").trim()) { |
| | | uni.showToast({ title: "é©³åæ¶è¯·å¡«åå®¡æ¹æè§", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | submitting.value = true; |
| | | const dto = buildApproveInstanceDto( |
| | | row.value.id, |
| | | uiResult, |
| | | approveOpinion.value |
| | | ); |
| | | |
| | | approveApprovalInstance(dto) |
| | | .then(() => { |
| | | uni.showToast({ |
| | | title: uiResult === "approved" ? "å·²éè¿" : "已驳å", |
| | | icon: "success", |
| | | }); |
| | | setTimeout(() => { |
| | | const pages = getCurrentPages(); |
| | | const prevRoute = pages[pages.length - 2]?.route || ""; |
| | | const delta = prevRoute.includes("approve-list/detail") ? 2 : 1; |
| | | uni.navigateBack({ delta }); |
| | | }, 400); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å®¡æ¹æä½å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | submitting.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(async options => { |
| | | if (!options?.id) { |
| | | uni.showToast({ title: "缺å°å®¡æ¹ ID", icon: "none" }); |
| | | setTimeout(goBack, 500); |
| | | return; |
| | | } |
| | | const cached = loadInstanceRow(options.id); |
| | | if (!cached) { |
| | | uni.showToast({ title: "请ä»å表è¿å
¥", icon: "none" }); |
| | | setTimeout(goBack, 500); |
| | | return; |
| | | } |
| | | if (!canApproveInstance(cached)) { |
| | | uni.showToast({ title: "å½åå®¡æ¹æ 鿍å¤ç", icon: "none" }); |
| | | setTimeout(goBack, 500); |
| | | return; |
| | | } |
| | | row.value = cached; |
| | | if (isReimburseApprovalInstance(cached)) { |
| | | loading.value = true; |
| | | try { |
| | | const { reimburseRow: mapped } = await loadReimburseDetailForInstance(cached); |
| | | reimburseRow.value = mapped; |
| | | } catch { |
| | | uni.showToast({ title: "å è½½æ¥é详æ
失败", icon: "none" }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "../../_styles/oa-approval-list.scss"; |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / 审æ¹è¯¦æ
|
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-list/detail |
| | | --> |
| | | <template> |
| | | <view class="oa-detail-page"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | |
| | | <scroll-view v-if="displayReady" |
| | | class="oa-detail-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <ReimburseInstanceDetailBody v-if="isReimburse" |
| | | :reimburse-row="reimburseRow" |
| | | :module-key="detailModuleKey" /> |
| | | <ApproveInstanceDetailBody v-else |
| | | :row="row" |
| | | :module-key="detailModuleKey" /> |
| | | </scroll-view> |
| | | |
| | | <view v-else |
| | | class="oa-empty"> |
| | | <up-empty mode="data" |
| | | :text="loading ? 'å è½½ä¸' : 'æªè·åå°å®¡æ¹æ°æ®'" /> |
| | | </view> |
| | | |
| | | <view v-if="displayReady" |
| | | class="oa-page-footer"> |
| | | <text class="oa-footer-btn btn-default" |
| | | @click="goBack">è¿å</text> |
| | | <text v-if="showEdit" |
| | | class="oa-footer-btn btn-warn" |
| | | @click="goEdit">ä¿®æ¹</text> |
| | | <text v-if="showApprove && !fromBusiness" |
| | | class="oa-footer-btn btn-primary" |
| | | @click="goApprove">å»å®¡æ¹</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import ApproveInstanceDetailBody from "./_components/ApproveInstanceDetailBody.vue"; |
| | | import ReimburseInstanceDetailBody from "../../ReimburseManage/_components/ReimburseInstanceDetailBody.vue"; |
| | | import { OA_NAV } from "@/config/oaPaths.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { |
| | | canApproveInstance, |
| | | canEditBusinessInstanceRow, |
| | | canModifyInstance, |
| | | loadInstanceRow, |
| | | stashInstanceRow, |
| | | } from "../../_utils/approveListUtils.js"; |
| | | import { inferModuleKeyFromRow } from "../../_utils/approvalModuleApplyExtras.js"; |
| | | import { |
| | | inferReimburseModuleKeyFromInstance, |
| | | isReimburseApprovalInstance, |
| | | loadReimburseDetailForInstance, |
| | | resolveFinReimbursementIdFromInstance, |
| | | stashReimburseEditFromApprove, |
| | | } from "../../_utils/reimburseApproveBridge.js"; |
| | | import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js"; |
| | | import { canEditReimbursementRow } from "../../_utils/finReimbursementMappers.js"; |
| | | |
| | | const userStore = useUserStore(); |
| | | const fromBusiness = ref(false); |
| | | const row = ref(null); |
| | | const reimburseRow = ref(null); |
| | | const loading = ref(false); |
| | | |
| | | const detailModuleKey = computed(() => { |
| | | if (isReimburse.value) { |
| | | return ( |
| | | reimburseRow.value?.moduleKey || |
| | | inferReimburseModuleKeyFromInstance(row.value) |
| | | ); |
| | | } |
| | | return inferModuleKeyFromRow(row.value); |
| | | }); |
| | | |
| | | const isReimburse = computed(() => isReimburseApprovalInstance(row.value)); |
| | | |
| | | const pageTitle = computed(() => { |
| | | if (isReimburse.value) { |
| | | return getApprovalModuleConfig(detailModuleKey.value)?.label |
| | | ? `${getApprovalModuleConfig(detailModuleKey.value).label}详æ
` |
| | | : "æ¥é详æ
"; |
| | | } |
| | | return "审æ¹è¯¦æ
"; |
| | | }); |
| | | |
| | | const displayReady = computed(() => |
| | | isReimburse.value ? Boolean(reimburseRow.value) : Boolean(row.value) |
| | | ); |
| | | |
| | | const showEdit = computed(() => { |
| | | if (isReimburse.value) { |
| | | return canEditReimbursementRow(reimburseRow.value); |
| | | } |
| | | if (fromBusiness.value) { |
| | | return canEditBusinessInstanceRow(row.value); |
| | | } |
| | | return canModifyInstance(row.value, userStore); |
| | | }); |
| | | |
| | | const showApprove = computed(() => canApproveInstance(row.value)); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | if (!showEdit.value) return; |
| | | if (isReimburse.value) { |
| | | const mk = detailModuleKey.value; |
| | | const rid = resolveFinReimbursementIdFromInstance(row.value); |
| | | if (rid == null) { |
| | | uni.showToast({ title: "æ æ³ä¿®æ¹ï¼ç¼ºå°æ¥éå ID", icon: "none" }); |
| | | return; |
| | | } |
| | | stashReimburseEditFromApprove(mk, rid); |
| | | uni.navigateTo({ |
| | | url: `${OA_NAV.reimburseForm}?moduleKey=${mk}&mode=edit&reimbursementId=${rid}`, |
| | | }); |
| | | return; |
| | | } |
| | | if (!row.value?.id) return; |
| | | const mk = detailModuleKey.value; |
| | | const q = mk ? `&moduleKey=${mk}` : ""; |
| | | uni.navigateTo({ |
| | | url: `${OA_NAV.approveListApply}?id=${row.value.id}${q}`, |
| | | }); |
| | | }; |
| | | |
| | | const goApprove = () => { |
| | | if (!row.value?.id) return; |
| | | stashInstanceRow(row.value); |
| | | uni.navigateTo({ |
| | | url: `${OA_NAV.approveListApprove}?id=${row.value.id}`, |
| | | }); |
| | | }; |
| | | |
| | | onLoad(async options => { |
| | | fromBusiness.value = options?.from === "business"; |
| | | if (!options?.id) { |
| | | uni.showToast({ title: "缺å°å®¡æ¹ ID", icon: "none" }); |
| | | setTimeout(goBack, 500); |
| | | return; |
| | | } |
| | | const cached = loadInstanceRow(options.id); |
| | | if (!cached) { |
| | | uni.showToast({ title: "请ä»å表è¿å
¥è¯¦æ
", icon: "none" }); |
| | | setTimeout(goBack, 500); |
| | | return; |
| | | } |
| | | row.value = cached; |
| | | if (isReimburseApprovalInstance(cached)) { |
| | | loading.value = true; |
| | | try { |
| | | const { reimburseRow: mapped } = await loadReimburseDetailForInstance(cached); |
| | | reimburseRow.value = mapped; |
| | | } catch { |
| | | uni.showToast({ title: "å è½½æ¥é详æ
失败", icon: "none" }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "../../_styles/oa-approval-list.scss"; |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / 审æ¹å表 |
| | | --> |
| | | <template> |
| | | <view class="oa-approval-page"> |
| | | <PageHeader title="审æ¹å表" |
| | | @back="goBack" /> |
| | | |
| | | <view class="oa-toolbar"> |
| | | <view class="oa-filter-chip active-search"> |
| | | <up-icon name="search" |
| | | size="18" |
| | | color="#666" /> |
| | | <up-input v-model="queryParams.keyword" |
| | | class="chip-input" |
| | | placeholder="å®¡æ¹æ é¢ / 审æ¹ç¼å·" |
| | | clearable |
| | | border="none" |
| | | @confirm="handleSearch" /> |
| | | </view> |
| | | <view class="oa-icon-btn" |
| | | @click="handleSearch"> |
| | | <up-icon name="search" |
| | | size="20" |
| | | color="#2979ff" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="oa-list-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false" |
| | | :style="{ height: listScrollHeight + 'px' }" |
| | | @scrolltolower="loadMore"> |
| | | <view v-if="list.length" |
| | | class="oa-card-list"> |
| | | <view v-for="item in list" |
| | | :key="item.id" |
| | | class="oa-card" |
| | | @click="openDetail(item)"> |
| | | <view class="oa-card-head"> |
| | | <view class="oa-card-title-wrap"> |
| | | <text class="oa-card-title">{{ item.title || item.instanceNo || "-" }}</text> |
| | | <text v-if="item.templateName" |
| | | class="oa-card-sub">{{ item.templateName }}</text> |
| | | </view> |
| | | <text :class="['oa-status', businessStatusClass(item.status)]"> |
| | | {{ statusText(item.status) }} |
| | | </text> |
| | | </view> |
| | | |
| | | <view class="oa-card-body"> |
| | | <view class="oa-info-grid"> |
| | | <view class="oa-info-row"> |
| | | <text class="oa-info-label">审æ¹ç¼å·</text> |
| | | <text class="oa-info-value">{{ item.instanceNo || "-" }}</text> |
| | | </view> |
| | | <view v-if="item.businessName" |
| | | class="oa-info-row"> |
| | | <text class="oa-info-label">ä¸å¡åç§°</text> |
| | | <text class="oa-info-value">{{ item.businessName }}</text> |
| | | </view> |
| | | <view class="oa-info-row"> |
| | | <text class="oa-info-label">ç³è¯·äºº</text> |
| | | <text class="oa-info-value">{{ item.applicantName || "-" }}</text> |
| | | </view> |
| | | <view class="oa-info-row"> |
| | | <text class="oa-info-label">å½å审æ¹äºº</text> |
| | | <text class="oa-info-value">{{ currentApproverName(item) }}</text> |
| | | </view> |
| | | <view class="oa-info-row"> |
| | | <text class="oa-info-label">ç³è¯·æ¶é´</text> |
| | | <text class="oa-info-value">{{ formatDateTime(item.applyTime) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="canModify(item) || item.isApprove" |
| | | class="oa-card-foot" |
| | | @click.stop> |
| | | <text v-if="canModify(item)" |
| | | class="oa-foot-btn btn-edit" |
| | | @click="goModify(item)">ç¼è¾</text> |
| | | <text v-if="item.isApprove" |
| | | class="oa-foot-btn btn-approve" |
| | | @click="handleApprove(item)">审æ¹</text> |
| | | </view> |
| | | </view> |
| | | <up-loadmore :status="pageStatus" /> |
| | | </view> |
| | | <view v-else |
| | | class="oa-empty"> |
| | | <up-empty mode="list" |
| | | text="ææ å®¡æ¹æ°æ®" /> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { listApprovalInstancePage } from "@/api/oa/approvalInstance.js"; |
| | | import { OA_NAV } from "@/config/oaPaths.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import { |
| | | businessStatusClass, |
| | | businessStatusText, |
| | | canModifyInstance, |
| | | stashInstanceRow, |
| | | } from "../../_utils/approveListUtils.js"; |
| | | import { |
| | | inferReimburseModuleKeyFromInstance, |
| | | resolveFinReimbursementIdFromInstance, |
| | | stashReimburseEditFromApprove, |
| | | } from "../../_utils/reimburseApproveBridge.js"; |
| | | |
| | | const userStore = useUserStore(); |
| | | const queryParams = reactive({ keyword: "" }); |
| | | const list = ref([]); |
| | | const pageStatus = ref("loadmore"); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | const listScrollHeight = ref(400); |
| | | |
| | | function calcListScrollHeight() { |
| | | const sys = uni.getSystemInfoSync(); |
| | | const statusBar = sys.statusBarHeight || 0; |
| | | const navBar = 44; |
| | | const toolbar = 56; |
| | | const fabGap = 16; |
| | | listScrollHeight.value = Math.max( |
| | | 200, |
| | | sys.windowHeight - statusBar - navBar - toolbar - fabGap |
| | | ); |
| | | } |
| | | |
| | | const statusText = status => businessStatusText(status); |
| | | |
| | | const formatDateTime = val => { |
| | | if (!val) return "-"; |
| | | return parseTime(val, "{y}-{m}-{d} {h}:{i}:{s}") || String(val); |
| | | }; |
| | | |
| | | const canModify = item => canModifyInstance(item, userStore); |
| | | |
| | | const currentApproverName = item => { |
| | | const tasks = item?.tasks; |
| | | if (!Array.isArray(tasks) || !tasks.length) return "-"; |
| | | const pending = tasks.find(t => t.taskStatus === "PENDING"); |
| | | if (pending?.approverName) return pending.approverName; |
| | | const names = [...new Set(tasks.map(t => t.approverName).filter(Boolean))]; |
| | | return names.length ? names.join("ã") : "-"; |
| | | }; |
| | | |
| | | const buildListParams = () => { |
| | | const keyword = queryParams.keyword?.trim(); |
| | | const dto = {}; |
| | | if (keyword) { |
| | | if (/[\u4e00-\u9fa5]/.test(keyword)) { |
| | | dto.title = keyword; |
| | | } else { |
| | | dto.instanceNo = keyword; |
| | | } |
| | | } |
| | | return { current: page.current, size: page.size, ...dto }; |
| | | }; |
| | | |
| | | const getList = () => { |
| | | if (pageStatus.value === "loading" || pageStatus.value === "nomore") return; |
| | | pageStatus.value = "loading"; |
| | | listApprovalInstancePage(buildListParams()) |
| | | .then(res => { |
| | | const pageData = res?.data || {}; |
| | | const records = pageData.records || []; |
| | | const total = pageData.total ?? 0; |
| | | if (page.current === 1) { |
| | | list.value = records; |
| | | } else { |
| | | list.value = [...list.value, ...records]; |
| | | } |
| | | page.total = total; |
| | | if (list.value.length >= total || records.length < page.size) { |
| | | pageStatus.value = "nomore"; |
| | | } else { |
| | | pageStatus.value = "loadmore"; |
| | | page.current += 1; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | if (page.current === 1) list.value = []; |
| | | pageStatus.value = "loadmore"; |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const handleSearch = () => { |
| | | page.current = 1; |
| | | pageStatus.value = "loadmore"; |
| | | list.value = []; |
| | | getList(); |
| | | }; |
| | | |
| | | const loadMore = () => { |
| | | if (pageStatus.value === "loadmore") getList(); |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | const goAdd = () => uni.navigateTo({ url: OA_NAV.approveListTemplateSelect }); |
| | | |
| | | const openDetail = item => { |
| | | if (!item?.id) return; |
| | | stashInstanceRow(item); |
| | | uni.navigateTo({ url: `${OA_NAV.approveListDetail}?id=${item.id}` }); |
| | | }; |
| | | |
| | | const goModify = item => { |
| | | if (!canModify(item)) { |
| | | uni.showToast({ title: "ä»
è¿è¡ä¸çæ¬äººç³è¯·å¯ç¼è¾", icon: "none" }); |
| | | return; |
| | | } |
| | | const mk = inferReimburseModuleKeyFromInstance(item); |
| | | if (mk) { |
| | | const rid = resolveFinReimbursementIdFromInstance(item); |
| | | if (rid == null) { |
| | | uni.showToast({ title: "æ æ³ä¿®æ¹ï¼ç¼ºå°æ¥éå ID", icon: "none" }); |
| | | return; |
| | | } |
| | | stashReimburseEditFromApprove(mk, rid); |
| | | uni.navigateTo({ |
| | | url: `${OA_NAV.reimburseForm}?moduleKey=${mk}&mode=edit&reimbursementId=${rid}`, |
| | | }); |
| | | return; |
| | | } |
| | | if (!item?.id) return; |
| | | stashInstanceRow(item); |
| | | uni.navigateTo({ url: `${OA_NAV.approveListApply}?id=${item.id}` }); |
| | | }; |
| | | |
| | | const handleApprove = item => { |
| | | if (!item?.id) return; |
| | | if (!item.isApprove) { |
| | | uni.showToast({ title: "å½åå®¡æ¹æ 鿍å¤ç", icon: "none" }); |
| | | return; |
| | | } |
| | | stashInstanceRow(item); |
| | | uni.navigateTo({ url: `${OA_NAV.approveListApprove}?id=${item.id}` }); |
| | | }; |
| | | |
| | | onMounted(() => calcListScrollHeight()); |
| | | |
| | | onShow(() => { |
| | | calcListScrollHeight(); |
| | | handleSearch(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | @import "../../_styles/oa-approval-list.scss"; |
| | | |
| | | .active-search { |
| | | padding-right: 4px; |
| | | } |
| | | |
| | | .chip-input { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | :deep(.chip-input .u-input__content) { |
| | | background: transparent !important; |
| | | padding: 0 !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / éæ©å®¡æ¹æ¨¡æ¿ |
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-list/template-select |
| | | Tabï¼TypeEnums â businessTypeï¼å表ï¼GET /approvalTemplate/list/1ï¼èªå®ä¹å·²å¯ç¨ï¼åæ businessType çé |
| | | --> |
| | | <template> |
| | | <view class="template-select-page sales-account"> |
| | | <PageHeader :title="pageHeaderTitle" |
| | | @back="goBack" /> |
| | | |
| | | <view v-if="typeOptions.length && !moduleKey" |
| | | class="step-section"> |
| | | <view class="tabs-wrap"> |
| | | <up-tabs :list="tabList" |
| | | :current="activeTab" |
| | | line-color="#2979ff" |
| | | @click="onTabClick" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="useAllTemplatesFallback && allTemplates.length" |
| | | class="fallback-hint"> |
| | | <text>å½åç±»å䏿 å¹é
模æ¿ï¼å·²æ¾ç¤ºå
¨é¨ {{ allTemplates.length }} 个å¯ç¨æ¨¡æ¿</text> |
| | | </view> |
| | | |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input v-model="keyword" |
| | | class="search-text" |
| | | placeholder="请è¾å
¥æ¨¡æ¿åç§°" |
| | | clearable /> |
| | | </view> |
| | | <view class="filter-button"> |
| | | <up-icon name="search" |
| | | size="24" |
| | | color="#999" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="list-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <view v-if="loading" |
| | | class="loading-wrap"> |
| | | <up-loading-icon mode="circle" /> |
| | | <text class="loading-text">å è½½ä¸...</text> |
| | | </view> |
| | | <view v-else-if="displayList.length" |
| | | class="ledger-list"> |
| | | <view v-for="item in displayList" |
| | | :key="item.id" |
| | | class="ledger-item ledger-item--clickable" |
| | | @click="selectTemplate(item)"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="item-id">{{ item.templateName || "-" }}</text> |
| | | </view> |
| | | <u-tag :type="enabledTagType(item.enabled)" |
| | | :text="enabledText(item.enabled)" /> |
| | | </view> |
| | | <up-divider /> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">审æ¹ç±»å</text> |
| | | <text class="detail-value">{{ businessTypeText(item.businessType) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">审æ¹èç¹</text> |
| | | <text class="detail-value">{{ nodeCount(item) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">模æ¿è¯´æ</text> |
| | | <text class="detail-value">{{ item.description || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-wrap"> |
| | | <up-empty mode="list" |
| | | :text="emptyText" /> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { OA_NAV } from "@/config/oaPaths.js"; |
| | | import { |
| | | buildTypeLabelMap, |
| | | fetchApprovalTemplateTypes, |
| | | buildTypeOptionsFromTemplates, |
| | | FALLBACK_BUSINESS_TYPE_OPTIONS, |
| | | fetchEnabledApprovalTemplates, |
| | | filterTemplatesByBusinessType, |
| | | filterTemplatesByBusinessTypes, |
| | | getBusinessTypeLabel, |
| | | pickTabIndexWithTemplates, |
| | | } from "../../_utils/approvalTemplateType.js"; |
| | | import { |
| | | getApprovalModuleConfig, |
| | | getModuleMatchingBusinessTypes, |
| | | resolveModuleBusinessType, |
| | | } from "../../_utils/approvalModuleRegistry.js"; |
| | | |
| | | const moduleKey = ref(""); |
| | | const typeOptions = ref([]); |
| | | const typeLabelMap = ref({}); |
| | | /** å
¨é¨èªå®ä¹å·²å¯ç¨æ¨¡æ¿ï¼list/1 䏿¬¡æåï¼ */ |
| | | const allTemplates = ref([]); |
| | | const activeTab = ref(0); |
| | | const keyword = ref(""); |
| | | const loading = ref(false); |
| | | |
| | | const tabList = computed(() => |
| | | typeOptions.value.map(opt => ({ name: opt.name })) |
| | | ); |
| | | |
| | | const moduleConfig = computed(() => |
| | | moduleKey.value ? getApprovalModuleConfig(moduleKey.value) : null |
| | | ); |
| | | |
| | | const pageHeaderTitle = computed(() => { |
| | | if (moduleConfig.value?.label) { |
| | | return `éæ©${moduleConfig.value.label}模æ¿`; |
| | | } |
| | | return "éæ©å®¡æ¹æ¨¡æ¿"; |
| | | }); |
| | | |
| | | const moduleBusinessTypes = computed(() => { |
| | | if (!moduleKey.value) return []; |
| | | return getModuleMatchingBusinessTypes(moduleKey.value, typeOptions.value); |
| | | }); |
| | | |
| | | const currentTypeOption = computed(() => typeOptions.value[activeTab.value]); |
| | | |
| | | /** æ moduleKey ä¸å½å Tab çä¸å°æ¶ï¼å±ç¤ºå
¨é¨æ¨¡æ¿é¿å
ãææ°æ®å´ç©ºç½ã */ |
| | | const useAllTemplatesFallback = computed(() => { |
| | | if (moduleKey.value) return false; |
| | | if (!allTemplates.value.length) return false; |
| | | const businessType = currentTypeOption.value?.value; |
| | | if (businessType == null || businessType === "") return true; |
| | | return filterTemplatesByBusinessType(allTemplates.value, businessType).length === 0; |
| | | }); |
| | | |
| | | const currentSource = computed(() => { |
| | | if (moduleKey.value && moduleBusinessTypes.value.length) { |
| | | const filtered = filterTemplatesByBusinessTypes( |
| | | allTemplates.value, |
| | | moduleBusinessTypes.value |
| | | ); |
| | | if (filtered.length) return filtered; |
| | | return allTemplates.value; |
| | | } |
| | | if (useAllTemplatesFallback.value) { |
| | | return allTemplates.value; |
| | | } |
| | | const businessType = currentTypeOption.value?.value; |
| | | return filterTemplatesByBusinessType(allTemplates.value, businessType); |
| | | }); |
| | | |
| | | const displayList = computed(() => { |
| | | const kw = keyword.value?.trim().toLowerCase(); |
| | | if (!kw) return currentSource.value; |
| | | return currentSource.value.filter(item => |
| | | (item.templateName || "").toLowerCase().includes(kw) |
| | | ); |
| | | }); |
| | | |
| | | const emptyText = computed(() => { |
| | | if (allTemplates.value.length === 0) { |
| | | return "ææ å·²å¯ç¨çå®¡æ¹æ¨¡æ¿ï¼è¯·å
å¨ãå®¡æ¹æ¨¡æ¿ãä¸å建并å¯ç¨"; |
| | | } |
| | | if (moduleConfig.value?.label) { |
| | | return `ææ ${moduleConfig.value.label}å¯ç¨æ¨¡æ¿`; |
| | | } |
| | | if (useAllTemplatesFallback.value) { |
| | | return "å½åç±»å䏿 å¹é
模æ¿"; |
| | | } |
| | | const typeName = currentTypeOption.value?.name || "该审æ¹ç±»å"; |
| | | return `ææ ${typeName}ä¸ç模æ¿ï¼å¯åæ¢ä¸æ¹ç±»åï¼`; |
| | | }); |
| | | |
| | | const businessTypeText = type => |
| | | getBusinessTypeLabel(type, typeLabelMap.value); |
| | | |
| | | const enabledText = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "å¯ç¨"; |
| | | if (val === "0") return "åç¨"; |
| | | return "-"; |
| | | }; |
| | | |
| | | const enabledTagType = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "success"; |
| | | if (val === "0") return "info"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const nodeCount = item => { |
| | | const count = item?.nodes?.length; |
| | | return count != null ? `${count} 个` : "-"; |
| | | }; |
| | | |
| | | const initPage = async () => { |
| | | loading.value = true; |
| | | keyword.value = ""; |
| | | allTemplates.value = []; |
| | | try { |
| | | const [opts, templates] = await Promise.all([ |
| | | fetchApprovalTemplateTypes(), |
| | | fetchEnabledApprovalTemplates(), |
| | | ]); |
| | | let resolvedOpts = opts?.length ? opts : buildTypeOptionsFromTemplates(templates); |
| | | if (!resolvedOpts.length) { |
| | | resolvedOpts = [...FALLBACK_BUSINESS_TYPE_OPTIONS]; |
| | | } |
| | | typeOptions.value = resolvedOpts; |
| | | typeLabelMap.value = buildTypeLabelMap(resolvedOpts); |
| | | allTemplates.value = templates; |
| | | |
| | | if (!templates.length) { |
| | | uni.showToast({ |
| | | title: "æªè·åå°å·²å¯ç¨æ¨¡æ¿", |
| | | icon: "none", |
| | | duration: 2500, |
| | | }); |
| | | } |
| | | |
| | | if (moduleKey.value) { |
| | | const resolved = resolveModuleBusinessType(moduleKey.value, opts); |
| | | const idx = opts.findIndex( |
| | | opt => String(opt.value) === String(resolved) |
| | | ); |
| | | activeTab.value = |
| | | idx >= 0 ? idx : pickTabIndexWithTemplates(resolvedOpts, templates); |
| | | } else { |
| | | activeTab.value = pickTabIndexWithTemplates(resolvedOpts, templates); |
| | | } |
| | | } catch { |
| | | typeOptions.value = []; |
| | | typeLabelMap.value = {}; |
| | | allTemplates.value = []; |
| | | uni.showToast({ title: "å 载模æ¿å¤±è´¥", icon: "none" }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const onTabClick = item => { |
| | | activeTab.value = item?.index ?? 0; |
| | | keyword.value = ""; |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const selectTemplate = item => { |
| | | if (!item?.id) return; |
| | | if (String(item.enabled) === "0") { |
| | | uni.showToast({ title: "该模æ¿å·²åç¨", icon: "none" }); |
| | | return; |
| | | } |
| | | const base = `${OA_NAV.approveListApply}?templateId=${item.id}`; |
| | | uni.navigateTo({ |
| | | url: moduleKey.value ? `${base}&moduleKey=${moduleKey.value}` : base, |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | moduleKey.value = options?.moduleKey || ""; |
| | | initPage(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .template-select-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .fallback-hint { |
| | | margin: 8px 12px 0; |
| | | padding: 8px 12px; |
| | | font-size: 12px; |
| | | color: #e6a23c; |
| | | background: #fdf6ec; |
| | | border-radius: 6px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .step-section { |
| | | background: #fff; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .step-label { |
| | | display: block; |
| | | padding: 10px 16px 0; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .step-hint { |
| | | display: flex; |
| | | align-items: baseline; |
| | | justify-content: space-between; |
| | | padding: 10px 16px 4px; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .step-desc { |
| | | flex-shrink: 0; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .tabs-wrap { |
| | | padding: 0 12px 4px; |
| | | } |
| | | |
| | | .list-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding-bottom: env(safe-area-inset-bottom); |
| | | } |
| | | |
| | | .loading-wrap { |
| | | padding: 48px 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .loading-text { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .empty-wrap { |
| | | padding: 48px 20px; |
| | | } |
| | | |
| | | .ledger-item--clickable:active { |
| | | opacity: 0.92; |
| | | } |
| | | |
| | | .card-footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-top: 10px; |
| | | padding-top: 10px; |
| | | border-top: 1px dashed #e8ecf0; |
| | | } |
| | | |
| | | .card-footer-tip { |
| | | font-size: 13px; |
| | | color: #2979ff; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / å®¡æ¹æ¨¡æ¿è¯¦æ
|
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-template/detail |
| | | --> |
| | | <template> |
| | | <view class="template-detail-page"> |
| | | <PageHeader title="模æ¿è¯¦æ
" |
| | | @back="goBack" /> |
| | | |
| | | <scroll-view class="detail-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <view v-if="loading" |
| | | class="loading-wrap"> |
| | | <up-loading-icon mode="circle" /> |
| | | <text class="loading-text">å è½½ä¸...</text> |
| | | </view> |
| | | <template v-else-if="detail"> |
| | | <view class="section"> |
| | | <view class="section-title">åºæ¬ä¿¡æ¯</view> |
| | | <view class="info-list"> |
| | | <view class="info-item"> |
| | | <text class="info-label">模æ¿åç§°</text> |
| | | <text class="info-value">{{ detail.templateName || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">审æ¹ç±»å</text> |
| | | <text class="info-value">{{ businessTypeText(detail.businessType) }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å¯ç¨ç¶æ</text> |
| | | <text class="info-value" |
| | | :class="enabledClass(detail.enabled)"> |
| | | {{ enabledText(detail.enabled) }} |
| | | </text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">模æ¿è¯´æ</text> |
| | | <text class="info-value">{{ detail.description || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å建人</text> |
| | | <text class="info-value">{{ detail.createdUserName || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å建æ¶é´</text> |
| | | <text class="info-value">{{ detail.createTime || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="section"> |
| | | <view class="section-title">å¡«æ¥é
ç½®</view> |
| | | <view class="info-list"> |
| | | <view class="info-item"> |
| | | <text class="info-label">å¡«æ¥æç¤º</text> |
| | | <text class="info-value">{{ formConfigData.prompt || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | <view v-if="formConfigData.fields.length" |
| | | class="field-block"> |
| | | <view v-for="(field, index) in formConfigData.fields" |
| | | :key="field.key || index" |
| | | class="field-card"> |
| | | <view class="field-card-head"> |
| | | <text class="field-card-name">{{ field.label }}</text> |
| | | <text class="field-tag">{{ fieldTypeLabel(field.type) }}</text> |
| | | <text v-if="field.required" |
| | | class="field-tag field-tag--req">å¿
å¡«</text> |
| | | </view> |
| | | <text v-if="field.defaultValue" |
| | | class="field-card-default"> |
| | | é»è®¤å¼ï¼{{ field.defaultValue }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-hint">ææ å¡«æ¥é¡¹</view> |
| | | </view> |
| | | |
| | | <view class="section"> |
| | | <view class="section-title">å®¡æ¹æµç¨</view> |
| | | <view v-if="detail.nodes?.length" |
| | | class="flow-list"> |
| | | <view v-for="(node, index) in detail.nodes" |
| | | :key="node.id || index" |
| | | class="flow-card"> |
| | | <view class="flow-card-head"> |
| | | <text class="flow-level">第{{ levelLabel(node.levelNo || index + 1) }}级</text> |
| | | <text class="flow-type">{{ approveTypeText(node.approveType) }}</text> |
| | | </view> |
| | | <view class="approver-tags"> |
| | | <text v-for="(approver, aIdx) in node.approvers || []" |
| | | :key="approver.id || aIdx" |
| | | class="approver-tag"> |
| | | {{ approver.approverName || "-" }} |
| | | </text> |
| | | <text v-if="!(node.approvers || []).length" |
| | | class="empty-hint inline">ææ å®¡æ¹äºº</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-hint">ææ å®¡æ¹èç¹</view> |
| | | </view> |
| | | </template> |
| | | <view v-else |
| | | class="empty-wrap"> |
| | | <up-empty mode="data" |
| | | text="æªè·åå°æ¨¡æ¿è¯¦æ
" /> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <FooterButtons v-if="!loading && detail" |
| | | cancel-text="è¿å" |
| | | confirm-text="ç¼è¾" |
| | | @cancel="goBack" |
| | | @confirm="goEdit" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js"; |
| | | import { getFieldEditorTypeLabel } from "../../_utils/approvalFormField.js"; |
| | | import { |
| | | buildTypeLabelMap, |
| | | fetchApprovalTemplateTypes, |
| | | getTemplateTypeLabel, |
| | | } from "../../_utils/approvalTemplateType.js"; |
| | | |
| | | const EDIT_STORAGE_KEY = "oa_approve_template_edit_row"; |
| | | const LEVEL_TEXT = ["", "ä¸", "äº", "ä¸", "å", "äº", "å
", "ä¸", "å
«", "ä¹", "å"]; |
| | | |
| | | const templateId = ref(""); |
| | | const detail = ref(null); |
| | | const loading = ref(false); |
| | | const typeLabelMap = ref({}); |
| | | |
| | | const formConfigData = computed(() => { |
| | | const raw = detail.value?.formConfig; |
| | | if (!raw) return { prompt: "", fields: [] }; |
| | | try { |
| | | const obj = typeof raw === "string" ? JSON.parse(raw) : raw; |
| | | return { |
| | | prompt: obj?.prompt || "", |
| | | fields: Array.isArray(obj?.fields) ? obj.fields : [], |
| | | }; |
| | | } catch { |
| | | return { prompt: "", fields: [] }; |
| | | } |
| | | }); |
| | | |
| | | const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n); |
| | | |
| | | const businessTypeText = type => |
| | | getTemplateTypeLabel(type, typeLabelMap.value); |
| | | |
| | | const enabledText = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "å¯ç¨"; |
| | | if (val === "0") return "åç¨"; |
| | | return "-"; |
| | | }; |
| | | |
| | | const enabledClass = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "status-on"; |
| | | if (val === "0") return "status-off"; |
| | | return ""; |
| | | }; |
| | | |
| | | const fieldTypeLabel = type => getFieldEditorTypeLabel(type); |
| | | |
| | | const approveTypeText = type => (type === "OR" ? "æç¾" : "ä¼ç¾"); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const goEdit = () => { |
| | | if (!templateId.value || !detail.value) return; |
| | | uni.setStorageSync(EDIT_STORAGE_KEY, detail.value); |
| | | uni.navigateTo({ |
| | | url: `/pages/oa/ApproveManage/approve-template/edit?id=${templateId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const loadDetail = () => { |
| | | if (!templateId.value) return; |
| | | loading.value = true; |
| | | detail.value = null; |
| | | getApprovalTemplateDetail(templateId.value) |
| | | .then(res => { |
| | | detail.value = res?.data || null; |
| | | if (!detail.value) { |
| | | uni.showToast({ title: "æªè·åå°è¯¦æ
", icon: "none" }); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "è·å详æ
失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | fetchApprovalTemplateTypes() |
| | | .then(opts => { |
| | | typeLabelMap.value = buildTypeLabelMap(opts); |
| | | }) |
| | | .catch(() => {}); |
| | | if (options?.id) { |
| | | templateId.value = options.id; |
| | | loadDetail(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .template-detail-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | background: #f0f3f8; |
| | | } |
| | | |
| | | .detail-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding: 10px 12px calc(96px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .loading-wrap { |
| | | padding: 48px 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .loading-text { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .section { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | margin-bottom: 10px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05); |
| | | } |
| | | |
| | | .section-title { |
| | | padding: 12px 16px; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | border-left: 3px solid #2979ff; |
| | | padding-left: 13px; |
| | | } |
| | | |
| | | .info-list { |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | padding: 11px 16px; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | gap: 12px; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .info-label { |
| | | flex-shrink: 0; |
| | | width: 88px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .info-value { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | text-align: right; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .status-on { |
| | | color: #18a058; |
| | | } |
| | | |
| | | .status-off { |
| | | color: #909399; |
| | | } |
| | | |
| | | .field-block { |
| | | padding: 0 12px 12px; |
| | | } |
| | | |
| | | .field-card { |
| | | padding: 10px 12px; |
| | | margin-bottom: 8px; |
| | | background: #f8fafc; |
| | | border-radius: 8px; |
| | | border: 1px solid #eef2f6; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .field-card-head { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .field-card-name { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | |
| | | .field-tag { |
| | | font-size: 11px; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | color: #2979ff; |
| | | background: #ecf5ff; |
| | | |
| | | &--req { |
| | | color: #f56c6c; |
| | | background: #fef0f0; |
| | | } |
| | | } |
| | | |
| | | .field-card-default { |
| | | display: block; |
| | | margin-top: 6px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .flow-list { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .flow-card { |
| | | padding: 12px; |
| | | margin-bottom: 8px; |
| | | background: #f8fafc; |
| | | border-radius: 8px; |
| | | border: 1px solid #eef2f6; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .flow-card-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .flow-level { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .flow-type { |
| | | font-size: 13px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .approver-tags { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .approver-tag { |
| | | padding: 4px 10px; |
| | | font-size: 13px; |
| | | color: #303133; |
| | | background: #fff; |
| | | border: 1px solid #dce8f8; |
| | | border-radius: 16px; |
| | | } |
| | | |
| | | .empty-hint { |
| | | padding: 12px 16px 16px; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | |
| | | &.inline { |
| | | padding: 0; |
| | | } |
| | | } |
| | | |
| | | .empty-wrap { |
| | | padding: 48px 20px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / æ°å»ºå®¡æ¹æ¨¡æ¿ |
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-template/edit |
| | | --> |
| | | <template> |
| | | <view class="template-edit-page"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | |
| | | <scroll-view class="form-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="88" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="åºæ¬ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="模æ¿åç§°" |
| | | prop="templateName" |
| | | required |
| | | class="form-item-name"> |
| | | <up-input v-model="form.templateName" |
| | | class="name-input-inline" |
| | | placeholder="请è¾å
¥æ¨¡æ¿åç§°" |
| | | maxlength="50" |
| | | :disabled="isSystemTemplate" |
| | | :clearable="!isSystemTemplate" /> |
| | | </up-form-item> |
| | | <up-form-item label="审æ¹ç±»å" |
| | | prop="businessType" |
| | | required |
| | | class="form-item-select" |
| | | :class="{ 'form-item-select--disabled': isSystemTemplate }" |
| | | @click="openBusinessTypeSheet"> |
| | | <up-input :model-value="businessTypeText" |
| | | placeholder="è¯·éæ©å®¡æ¹ç±»å" |
| | | readonly |
| | | :disabled="isSystemTemplate" /> |
| | | <template v-if="!isSystemTemplate" |
| | | #right> |
| | | <up-icon name="arrow-right" |
| | | @click.stop="openBusinessTypeSheet" /> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="å¯ç¨ç¶æ" |
| | | class="form-item-switch"> |
| | | <view class="switch-wrap"> |
| | | <up-switch v-model="enabledBool" /> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item label="模æ¿è¯´æ" |
| | | class="form-item-desc" |
| | | label-position="top"> |
| | | <view class="desc-input-shell"> |
| | | <up-textarea v-model="form.description" |
| | | placeholder="éå¡«" |
| | | maxlength="200" |
| | | border="none" |
| | | height="72" /> |
| | | </view> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head section-head--between"> |
| | | <view class="section-head-left"> |
| | | <text class="section-title">å¡«æ¥é¡¹é
ç½®</text> |
| | | <text class="section-count">å
± {{ formConfig.fields.length }} 项</text> |
| | | </view> |
| | | <view class="head-actions"> |
| | | <text class="head-link head-link--import" |
| | | :class="{ 'head-link--disabled': !canImportTemplate }" |
| | | @click="openTemplateImport">ä»å·²ææ¨¡æ¿å¯¼å
¥</text> |
| | | <text class="head-link head-link--primary" |
| | | @click="openFieldEditor()">+ æ·»å å¡«æ¥é¡¹</text> |
| | | </view> |
| | | </view> |
| | | <view class="section-body"> |
| | | <view class="prompt-row"> |
| | | <text class="prompt-label">å¡«æ¥æç¤º</text> |
| | | <up-input v-model="formConfig.prompt" |
| | | class="prompt-input" |
| | | placeholder="éå¡«" |
| | | maxlength="200" |
| | | clearable /> |
| | | </view> |
| | | <view v-if="formConfig.fields.length" |
| | | class="field-list"> |
| | | <view v-for="(field, index) in formConfig.fields" |
| | | :key="field.key" |
| | | class="field-item" |
| | | :class="{ 'field-item--locked': isFieldLocked(field) }" |
| | | @click="onFieldItemClick(field, index)"> |
| | | <view class="field-order">{{ index + 1 }}</view> |
| | | <view class="field-main"> |
| | | <view class="field-title-row"> |
| | | <text class="field-name">{{ field.label }}</text> |
| | | <view class="field-tags"> |
| | | <text class="type-tag" |
| | | :class="fieldTypeTagClass(field.type)"> |
| | | {{ fieldTypeLabel(field.type) }} |
| | | </text> |
| | | <text v-if="field.required" |
| | | class="req-tag">å¿
å¡«</text> |
| | | </view> |
| | | </view> |
| | | <text class="field-key">{{ field.key }}</text> |
| | | <text v-if="field.defaultValue" |
| | | class="field-default"> |
| | | é»è®¤ï¼{{ formatFieldDefaultPreview(field) }} |
| | | </text> |
| | | </view> |
| | | <view v-if="!isFieldLocked(field)" |
| | | class="field-actions" |
| | | @click.stop> |
| | | <view class="icon-btn icon-btn--edit" |
| | | @click.stop="openFieldEditor(field, index)"> |
| | | <up-icon name="edit-pen" |
| | | size="16" |
| | | color="#2979ff" /> |
| | | </view> |
| | | <view class="icon-btn icon-btn--del" |
| | | @click.stop="removeField(index)"> |
| | | <up-icon name="trash" |
| | | size="16" |
| | | color="#f56c6c" /> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="field-lock-tag">å
ç½®</view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-mini"> |
| | | <text>ææ å¡«æ¥é¡¹</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">å®¡æ¹æµç¨</text> |
| | | </view> |
| | | <view class="flow-wrap"> |
| | | <view v-for="(node, nodeIndex) in flowNodes" |
| | | :key="node._key" |
| | | class="flow-node-block"> |
| | | <view class="flow-node-card"> |
| | | <view class="node-header"> |
| | | <view class="node-level-badge">{{ nodeIndex + 1 }}</view> |
| | | <text class="node-level-text">第{{ levelLabel(nodeIndex + 1) }}级</text> |
| | | <view v-if="flowNodes.length > 1" |
| | | class="node-delete" |
| | | @click="removeNode(nodeIndex)"> |
| | | <up-icon name="trash" |
| | | size="16" |
| | | color="#f56c6c" /> |
| | | </view> |
| | | </view> |
| | | <view class="approve-type-row"> |
| | | <view class="type-btn" |
| | | :class="{ active: node.approveType === 'AND' }" |
| | | @click="node.approveType = 'AND'"> |
| | | ä¼ç¾ |
| | | </view> |
| | | <view class="type-btn" |
| | | :class="{ active: node.approveType === 'OR' }" |
| | | @click="node.approveType = 'OR'"> |
| | | æç¾ |
| | | </view> |
| | | </view> |
| | | <view class="approver-list"> |
| | | <view v-for="(approver, approverIndex) in node.approvers" |
| | | :key="`${node._key}-${approver.approverId}-${approverIndex}`" |
| | | class="approver-chip"> |
| | | <view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view> |
| | | <text class="approver-name">{{ approver.approverName }}</text> |
| | | <view class="approver-remove" |
| | | hover-class="approver-remove--active" |
| | | @tap.stop="removeApprover(nodeIndex, approverIndex)" |
| | | @click.stop="removeApprover(nodeIndex, approverIndex)"> |
| | | <text class="remove-icon">Ã</text> |
| | | </view> |
| | | </view> |
| | | <view class="add-approver" |
| | | @click="openUserPicker(nodeIndex)"> |
| | | <up-icon name="plus" |
| | | size="14" |
| | | color="#2979ff" /> |
| | | <text>æ·»å </text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="nodeIndex < flowNodes.length - 1" |
| | | class="flow-connector"> |
| | | <view class="flow-connector-line" /> |
| | | </view> |
| | | </view> |
| | | <view class="add-node-bar" |
| | | @click="addNode"> |
| | | <up-icon name="plus-circle" |
| | | size="20" |
| | | color="#2979ff" /> |
| | | <text>æ·»å 级次</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </up-form> |
| | | </scroll-view> |
| | | |
| | | <FooterButtons :loading="submitting" |
| | | confirm-text="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | |
| | | <up-action-sheet :show="showTemplateImportSheet" |
| | | title="ä»å·²ææ¨¡æ¿å¯¼å
¥" |
| | | :actions="templateImportActions" |
| | | @select="onSelectImportTemplate" |
| | | @close="showTemplateImportSheet = false" /> |
| | | |
| | | <up-popup :show="showFieldEditor" |
| | | mode="bottom" |
| | | round="16" |
| | | @close="closeFieldEditor"> |
| | | <view class="field-editor"> |
| | | <view class="sheet-handle" /> |
| | | <view class="editor-header"> |
| | | <text class="editor-title">{{ editingFieldIndex >= 0 ? "ç¼è¾å¡«æ¥é¡¹" : "æ·»å å¡«æ¥é¡¹" }}</text> |
| | | <text class="editor-subtitle">é
ç½®åæ®µå±æ§ãæ ¡éªä¸é»è®¤å¼</text> |
| | | </view> |
| | | <scroll-view class="editor-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <view class="editor-form"> |
| | | <view class="editor-section-card"> |
| | | <view class="editor-section-head"> |
| | | <text class="editor-section-title">åºç¡ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="editor-cell"> |
| | | <text class="editor-label required">æ¾ç¤ºåç§°</text> |
| | | <view class="editor-input-box"> |
| | | <up-input v-model="fieldDraft.label" |
| | | placeholder="å¦ï¼æ¥é说æ" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | </view> |
| | | <view class="editor-cell"> |
| | | <text class="editor-label required">åæ®µæ è¯</text> |
| | | <view class="editor-input-box"> |
| | | <up-input v-model="fieldDraft.key" |
| | | placeholder="å¦ï¼summary" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | </view> |
| | | <view class="editor-cell editor-cell--tap" |
| | | @click="openFieldTypePicker"> |
| | | <text class="editor-label required">æ§ä»¶ç±»å</text> |
| | | <view class="picker-value-row"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !fieldDraft.type }"> |
| | | {{ fieldDraftTypeText || "è¯·éæ©" }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#b0b8c4" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="editor-section-card"> |
| | | <view class="editor-section-head"> |
| | | <text class="editor-section-title">æ ¡éªä¸æ ¼å¼</text> |
| | | </view> |
| | | <view class="editor-cell editor-cell--switch"> |
| | | <view class="switch-label-wrap"> |
| | | <text class="editor-label">æ¯å¦å¿
å¡«</text> |
| | | <text class="switch-hint">æäº¤å®¡æ¹æ¶é¡»å¡«å该项</text> |
| | | </view> |
| | | <up-switch v-model="fieldDraft.required" |
| | | active-color="#2979ff" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="isSelectDraft" |
| | | class="editor-section-card"> |
| | | <view class="editor-section-head"> |
| | | <text class="editor-section-title">䏿é项</text> |
| | | </view> |
| | | <view class="editor-cell editor-cell--tap" |
| | | @click="openOptionSourcePicker"> |
| | | <text class="editor-label">éé¡¹æ¥æº</text> |
| | | <view class="picker-value-row"> |
| | | <text class="picker-value">{{ fieldDraftOptionSourceText }}</text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#b0b8c4" /> |
| | | </view> |
| | | </view> |
| | | <view v-if="fieldDraft.optionSource === 'manual'" |
| | | class="manual-options"> |
| | | <text class="manual-options-title">æå¨é项</text> |
| | | <view class="manual-options-table"> |
| | | <view class="option-table-head"> |
| | | <text class="option-col option-col--idx" /> |
| | | <text class="option-col option-col--label">æ¾ç¤ºææ¬</text> |
| | | <text class="option-col option-col--value">é项å¼</text> |
| | | <text class="option-col option-col--action" /> |
| | | </view> |
| | | <view v-for="(opt, optIndex) in fieldDraft.options" |
| | | :key="optIndex" |
| | | class="option-card"> |
| | | <text class="option-idx">{{ optIndex + 1 }}</text> |
| | | <view class="option-input-wrap"> |
| | | <up-input v-model="opt.label" |
| | | placeholder="å¦ï¼å·¥ä½æ¥å ç" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | <view class="option-input-wrap option-input-wrap--value"> |
| | | <up-input v-model="opt.value" |
| | | placeholder="å¦ï¼0" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | <view class="option-del" |
| | | hover-class="option-del--active" |
| | | @click.stop="removeDraftOption(optIndex)"> |
| | | <up-icon name="trash" |
| | | size="16" |
| | | color="#f56c6c" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="add-option-btn" |
| | | hover-class="add-option-btn--active" |
| | | @click="addDraftOption"> |
| | | <up-icon name="plus-circle" |
| | | size="16" |
| | | color="#2979ff" /> |
| | | <text>æ·»å é项</text> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="option-source-tip"> |
| | | <up-icon name="info-circle" |
| | | size="14" |
| | | color="#909399" /> |
| | | <text>åèµ·å®¡æ¹æ¶å°èªå¨å è½½{{ fieldDraftOptionSourceText }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="editor-section-card"> |
| | | <view class="editor-section-head"> |
| | | <text class="editor-section-title">é»è®¤å¼</text> |
| | | </view> |
| | | <text class="default-hint"> |
| | | éæ©è¯¥æ¨¡æ¿æäº¤å®¡æ¹æ¶èªå¨é¢å¡«ï¼ç¨æ·ä»å¯ä¿®æ¹ |
| | | </text> |
| | | <view class="editor-cell editor-cell--value"> |
| | | <up-textarea v-if="fieldDraft.type === 'textarea'" |
| | | v-model="fieldDraft.defaultValue" |
| | | placeholder="éå¡«" |
| | | maxlength="500" |
| | | border="surround" |
| | | height="72" /> |
| | | <view v-else-if="fieldDraft.type === 'date'" |
| | | class="picker-value-row picker-value-row--tap" |
| | | @click="openDefaultDatePicker"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !fieldDraft.defaultValue }"> |
| | | {{ fieldDraft.defaultValue || "éæ©æ¥æ" }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | <view v-else-if="isDatetimerangeDraft" |
| | | class="daterange-default-wrap"> |
| | | <view class="daterange-default-item" |
| | | @click="openDefaultRangePicker('start')"> |
| | | <text class="daterange-default-label">å¼å§æ¶é´</text> |
| | | <view class="picker-value-row picker-value-row--tap"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !defaultRangeStart }"> |
| | | {{ defaultRangeStart || "éæ©å¼å§æ¶é´" }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | </view> |
| | | <view class="daterange-default-item" |
| | | @click="openDefaultRangePicker('end')"> |
| | | <text class="daterange-default-label">ç»ææ¶é´</text> |
| | | <view class="picker-value-row picker-value-row--tap"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !defaultRangeEnd }"> |
| | | {{ defaultRangeEnd || "éæ©ç»ææ¶é´" }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else-if="isSelectDraft" |
| | | class="picker-value-row picker-value-row--tap" |
| | | @click="openDefaultSelectSheet"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !fieldDraft.defaultValue }"> |
| | | {{ defaultSelectDisplayText || "éå¡«" }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#b0b8c4" /> |
| | | </view> |
| | | <view v-else |
| | | class="editor-input-box"> |
| | | <up-input v-model="fieldDraft.defaultValue" |
| | | :type="fieldDraft.type === 'number' ? 'digit' : 'text'" |
| | | placeholder="éå¡«" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | <view class="editor-footer"> |
| | | <view class="editor-btn editor-btn--cancel" |
| | | @click="closeFieldEditor">åæ¶</view> |
| | | <view class="editor-btn editor-btn--confirm" |
| | | @click="confirmFieldEditor">ç¡®å®</view> |
| | | </view> |
| | | |
| | | <view v-if="inlinePickerShow" |
| | | class="editor-picker-layer"> |
| | | <view class="editor-picker-mask" |
| | | @click="closeInlinePicker" /> |
| | | <view class="editor-picker-panel"> |
| | | <view class="editor-picker-head"> |
| | | <text class="editor-picker-cancel" |
| | | @click="closeInlinePicker">åæ¶</text> |
| | | <text class="editor-picker-title">{{ inlinePickerTitle }}</text> |
| | | <text class="editor-picker-placeholder" /> |
| | | </view> |
| | | <scroll-view class="editor-picker-scroll" |
| | | scroll-y> |
| | | <view v-for="(item, pickerIndex) in inlinePickerOptions" |
| | | :key="`${inlinePickerMode}-${pickerIndex}-${item.value}`" |
| | | class="editor-picker-item" |
| | | :class="{ 'editor-picker-item--active': isInlinePickerItemActive(item) }" |
| | | @click="onInlinePickerSelect(item)"> |
| | | <text>{{ item.name }}</text> |
| | | <up-icon v-if="isInlinePickerItemActive(item)" |
| | | name="checkmark" |
| | | size="18" |
| | | color="#2979ff" /> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <up-popup :show="showDefaultDatePicker" |
| | | mode="bottom" |
| | | @close="closeDefaultDatePicker"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="defaultDateTs" |
| | | :mode="defaultDatePickerMode" |
| | | @confirm="onDefaultDatePickerConfirm" |
| | | @cancel="closeDefaultDatePicker" /> |
| | | </up-popup> |
| | | |
| | | <up-popup :show="showUserPicker" |
| | | mode="bottom" |
| | | round="16" |
| | | @close="closeUserPicker"> |
| | | <view class="user-picker"> |
| | | <view class="sheet-handle" /> |
| | | <view class="picker-head"> |
| | | <text class="picker-cancel" |
| | | @click="closeUserPicker">åæ¶</text> |
| | | <text class="picker-title">鿩审æ¹äºº</text> |
| | | <text class="picker-confirm" |
| | | @click="confirmUserPicker"> |
| | | ç¡®å®{{ pickerSelectedIds.length ? `(${pickerSelectedIds.length})` : "" }} |
| | | </text> |
| | | </view> |
| | | <scroll-view class="user-scroll" |
| | | scroll-y> |
| | | <view v-for="user in availableUsers" |
| | | :key="user.userId" |
| | | class="user-item" |
| | | :class="{ selected: isUserSelected(user.userId) }" |
| | | @click="toggleUser(user)"> |
| | | <view class="user-avatar">{{ (user.nickName || "?").charAt(0) }}</view> |
| | | <text class="user-name">{{ user.nickName }}</text> |
| | | <view class="user-check" |
| | | :class="{ checked: isUserSelected(user.userId) }"> |
| | | <up-icon v-if="isUserSelected(user.userId)" |
| | | name="checkmark" |
| | | size="14" |
| | | color="#fff" /> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <up-action-sheet :show="showBusinessTypeSheet" |
| | | title="鿩审æ¹ç±»å" |
| | | :actions="businessTypeActions" |
| | | @select="onSelectBusinessType" |
| | | @close="showBusinessTypeSheet = false" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, reactive, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { |
| | | addApprovalTemplate, |
| | | getApprovalTemplateDetail, |
| | | listApprovalTemplatePage, |
| | | updateApprovalTemplate, |
| | | } from "@/api/oa/approvalTemplate.js"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { |
| | | buildFieldConfigPayload, |
| | | createEmptyFieldOption, |
| | | parseApprovalFormConfig, |
| | | FIELD_EDITOR_TYPE_OPTIONS, |
| | | FIELD_OPTION_SOURCE_OPTIONS, |
| | | getFieldEditorTypeLabel, |
| | | getFieldOptionLabel, |
| | | getFieldOptionSource, |
| | | getFieldOptionSourceLabel, |
| | | isDatetimerangeField, |
| | | isSelectField, |
| | | formatDatetimerangeDisplay, |
| | | formatFieldDateValue, |
| | | joinDatetimerangeValue, |
| | | parseDatetimerangeValue, |
| | | parseFieldDateToTs, |
| | | resolveFieldOptions, |
| | | } from "../../_utils/approvalFormField.js"; |
| | | import { |
| | | fetchApprovalTemplateTypes, |
| | | isSystemApprovalTemplate, |
| | | } from "../../_utils/approvalTemplateType.js"; |
| | | |
| | | const EDIT_STORAGE_KEY = "oa_approve_template_edit_row"; |
| | | |
| | | const LEVEL_TEXT = ["", "ä¸", "äº", "ä¸", "å", "äº", "å
", "ä¸", "å
«", "ä¹", "å"]; |
| | | |
| | | const formRef = ref(); |
| | | const submitting = ref(false); |
| | | const userList = ref([]); |
| | | const templateId = ref(null); |
| | | |
| | | const showTemplateImportSheet = ref(false); |
| | | const importTemplateList = ref([]); |
| | | const showFieldEditor = ref(false); |
| | | const inlinePickerShow = ref(false); |
| | | const inlinePickerTitle = ref(""); |
| | | const inlinePickerOptions = ref([]); |
| | | const inlinePickerMode = ref(""); |
| | | const showUserPicker = ref(false); |
| | | const showDefaultDatePicker = ref(false); |
| | | const defaultDatePickerMode = ref("date"); |
| | | const defaultRangePickerPart = ref("start"); |
| | | const defaultDateTs = ref(Date.now()); |
| | | const deptList = ref([]); |
| | | |
| | | const editingFieldIndex = ref(-1); |
| | | const editingNodeIndex = ref(-1); |
| | | const pickerSelectedIds = ref([]); |
| | | /** ç³»ç»æ¨¡æ¿å è½½æ¶éå®çå¡«æ¥é¡¹ keyï¼ä¸å¯ç¼è¾/å é¤ */ |
| | | const lockedFieldKeys = ref(new Set()); |
| | | |
| | | const form = reactive({ |
| | | templateName: "", |
| | | businessType: null, |
| | | templateType: 1, |
| | | enabled: "1", |
| | | description: "", |
| | | }); |
| | | |
| | | const formConfig = reactive({ |
| | | prompt: "", |
| | | fields: [], |
| | | }); |
| | | |
| | | const fieldDraft = reactive({ |
| | | label: "", |
| | | key: "", |
| | | type: "text", |
| | | defaultValue: "", |
| | | required: true, |
| | | optionSource: "manual", |
| | | options: [createEmptyFieldOption()], |
| | | }); |
| | | |
| | | let nodeKeySeed = 1; |
| | | |
| | | const createNode = () => ({ |
| | | _key: `node_${nodeKeySeed++}`, |
| | | approveType: "AND", |
| | | approvers: [], |
| | | }); |
| | | |
| | | const flowNodes = ref([createNode()]); |
| | | |
| | | const rules = { |
| | | templateName: [{ required: true, message: "请è¾å
¥æ¨¡æ¿åç§°", trigger: "blur" }], |
| | | businessType: [ |
| | | { |
| | | validator: (_rule, value, callback) => { |
| | | if (value === "" || value === null || value === undefined) { |
| | | callback(new Error("è¯·éæ©å®¡æ¹ç±»å")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | const businessTypeOptions = ref([]); |
| | | const showBusinessTypeSheet = ref(false); |
| | | |
| | | const businessTypeActions = computed(() => |
| | | businessTypeOptions.value.map(opt => ({ |
| | | name: opt.name, |
| | | value: opt.value, |
| | | })) |
| | | ); |
| | | |
| | | const businessTypeText = computed(() => { |
| | | const matched = businessTypeOptions.value.find( |
| | | opt => String(opt.value) === String(form.businessType) |
| | | ); |
| | | return matched?.name || ""; |
| | | }); |
| | | |
| | | const canImportTemplate = computed(() => !isSystemTemplate.value); |
| | | |
| | | const templateImportActions = computed(() => |
| | | importTemplateList.value.map(item => { |
| | | const typeTag = isSystemApprovalTemplate(item) ? "ç³»ç»" : "èªå®ä¹"; |
| | | return { |
| | | name: `ã${typeTag}ã${item.templateName || `模æ¿${item.id}`}`, |
| | | value: String(item.id), |
| | | }; |
| | | }) |
| | | ); |
| | | |
| | | const isSelectDraft = computed(() => isSelectField(fieldDraft)); |
| | | |
| | | const isDatetimerangeDraft = computed(() => isDatetimerangeField(fieldDraft)); |
| | | |
| | | const defaultRangeParts = computed(() => |
| | | parseDatetimerangeValue(fieldDraft.defaultValue) |
| | | ); |
| | | |
| | | const defaultRangeStart = computed(() => defaultRangeParts.value.start); |
| | | |
| | | const defaultRangeEnd = computed(() => defaultRangeParts.value.end); |
| | | |
| | | const fieldDraftTypeText = computed(() => getFieldEditorTypeLabel(fieldDraft.type)); |
| | | |
| | | const fieldDraftOptionSourceText = computed(() => |
| | | getFieldOptionSourceLabel(fieldDraft.optionSource) |
| | | ); |
| | | |
| | | const defaultSelectActions = computed(() => { |
| | | const options = resolveFieldOptions(fieldDraft, { |
| | | users: userList.value, |
| | | depts: deptList.value, |
| | | }); |
| | | return [ |
| | | { name: "ä¸è®¾ç½®", value: "" }, |
| | | ...options.map(opt => ({ |
| | | name: opt.label, |
| | | value: opt.value, |
| | | })), |
| | | ]; |
| | | }); |
| | | |
| | | const defaultSelectDisplayText = computed(() => { |
| | | if (!fieldDraft.defaultValue) return ""; |
| | | return ( |
| | | getFieldOptionLabel(fieldDraft, fieldDraft.defaultValue) || |
| | | String(fieldDraft.defaultValue) |
| | | ); |
| | | }); |
| | | |
| | | const enabledBool = computed({ |
| | | get: () => form.enabled === "1", |
| | | set: val => { |
| | | form.enabled = val ? "1" : "0"; |
| | | }, |
| | | }); |
| | | |
| | | const isEditMode = computed(() => templateId.value != null && templateId.value !== ""); |
| | | |
| | | const isSystemTemplate = computed(() => isSystemApprovalTemplate(form)); |
| | | |
| | | const isFieldLocked = field => |
| | | isSystemTemplate.value && lockedFieldKeys.value.has(field?.key); |
| | | |
| | | const pageTitle = computed(() => |
| | | isEditMode.value ? "ç¼è¾å®¡æ¹æ¨¡æ¿" : "æ°å»ºå®¡æ¹æ¨¡æ¿" |
| | | ); |
| | | |
| | | const mapNodesFromRow = nodes => { |
| | | if (!Array.isArray(nodes) || !nodes.length) { |
| | | return [createNode()]; |
| | | } |
| | | return nodes.map(node => ({ |
| | | _key: `node_${nodeKeySeed++}`, |
| | | id: node.id, |
| | | templateId: node.templateId, |
| | | approveType: node.approveType || "AND", |
| | | approvers: (node.approvers || []).map((approver, idx) => ({ |
| | | id: approver.id, |
| | | nodeId: approver.nodeId, |
| | | templateId: approver.templateId, |
| | | approverId: approver.approverId, |
| | | approverName: approver.approverName, |
| | | sortNo: approver.sortNo ?? idx + 1, |
| | | })), |
| | | })); |
| | | }; |
| | | |
| | | const fillFormFromRow = row => { |
| | | if (!row) return; |
| | | templateId.value = row.id; |
| | | form.templateName = row.templateName || ""; |
| | | const parsedBusiness = Number(row.businessType); |
| | | form.businessType = Number.isNaN(parsedBusiness) |
| | | ? row.businessType |
| | | : parsedBusiness; |
| | | const parsedTemplateType = Number(row.templateType); |
| | | form.templateType = Number.isNaN(parsedTemplateType) ? 1 : parsedTemplateType; |
| | | form.enabled = String(row.enabled ?? "1"); |
| | | form.description = row.description || ""; |
| | | |
| | | const config = parseApprovalFormConfig(row.formConfig); |
| | | formConfig.prompt = config.prompt; |
| | | formConfig.fields = config.fields; |
| | | lockedFieldKeys.value = isSystemApprovalTemplate(row) |
| | | ? new Set(config.fields.map(f => f.key).filter(Boolean)) |
| | | : new Set(); |
| | | flowNodes.value = mapNodesFromRow(row.nodes); |
| | | }; |
| | | |
| | | const availableUsers = computed(() => { |
| | | const node = flowNodes.value[editingNodeIndex.value]; |
| | | if (!node) return userList.value; |
| | | const selectedIds = new Set(node.approvers.map(a => a.approverId)); |
| | | return userList.value.filter(user => !selectedIds.has(user.userId)); |
| | | }); |
| | | |
| | | const levelLabel = n => LEVEL_TEXT[n] || String(n); |
| | | |
| | | const fieldTypeLabel = type => getFieldEditorTypeLabel(type); |
| | | |
| | | const formatFieldDefaultPreview = field => { |
| | | if (isDatetimerangeField(field)) { |
| | | return formatDatetimerangeDisplay(field.defaultValue) || field.defaultValue; |
| | | } |
| | | return field.defaultValue; |
| | | }; |
| | | |
| | | const fieldTypeTagClass = type => { |
| | | const map = { |
| | | text: "type-tag--text", |
| | | textarea: "type-tag--area", |
| | | number: "type-tag--num", |
| | | date: "type-tag--date", |
| | | datetimerange: "type-tag--date", |
| | | select: "type-tag--select", |
| | | }; |
| | | return map[type] || "type-tag--text"; |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const openBusinessTypeSheet = () => { |
| | | if (isSystemTemplate.value) return; |
| | | if (!businessTypeOptions.value.length) { |
| | | uni.showToast({ title: "审æ¹ç±»åå è½½ä¸", icon: "none" }); |
| | | return; |
| | | } |
| | | showBusinessTypeSheet.value = true; |
| | | }; |
| | | |
| | | const onSelectBusinessType = action => { |
| | | form.businessType = action.value; |
| | | showBusinessTypeSheet.value = false; |
| | | formRef.value?.validateField?.("businessType"); |
| | | }; |
| | | |
| | | const applyImportedFormConfig = (config, sourceName = "") => { |
| | | const parsed = { |
| | | prompt: config?.prompt || "", |
| | | fields: (config?.fields || []).map(field => ({ ...field })), |
| | | }; |
| | | formConfig.prompt = parsed.prompt; |
| | | formConfig.fields = parsed.fields; |
| | | const tip = sourceName ? `已导å
¥ã${sourceName}ã` : "已导å
¥å¡«æ¥é
ç½®"; |
| | | uni.showToast({ title: tip, icon: "success" }); |
| | | }; |
| | | |
| | | const doImportFormConfig = (config, sourceName) => { |
| | | const hasExisting = |
| | | !!formConfig.prompt?.trim() || formConfig.fields.length > 0; |
| | | if (!hasExisting) { |
| | | applyImportedFormConfig(config, sourceName); |
| | | return; |
| | | } |
| | | uni.showModal({ |
| | | title: "导å
¥ç¡®è®¤", |
| | | content: `å°ä½¿ç¨ã${sourceName}ãçå¡«æ¥é
ç½®è¦çå½åå
å®¹ï¼æ¯å¦ç»§ç»ï¼`, |
| | | success: res => { |
| | | if (res.confirm) { |
| | | applyImportedFormConfig(config, sourceName); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const applyTemplateImport = templateIdValue => { |
| | | const row = importTemplateList.value.find( |
| | | item => String(item.id) === String(templateIdValue) |
| | | ); |
| | | const sourceName = row?.templateName || "æé模æ¿"; |
| | | const applyFromDetail = detail => { |
| | | const config = parseApprovalFormConfig(detail?.formConfig); |
| | | if (!config.fields.length && !config.prompt) { |
| | | uni.showToast({ title: "è¯¥æ¨¡æ¿æ å¡«æ¥é
ç½®", icon: "none" }); |
| | | return; |
| | | } |
| | | doImportFormConfig(config, sourceName); |
| | | }; |
| | | |
| | | if (row?.formConfig) { |
| | | applyFromDetail(row); |
| | | return; |
| | | } |
| | | |
| | | uni.showLoading({ title: "å è½½é
ç½®...", mask: true }); |
| | | getApprovalTemplateDetail(templateIdValue) |
| | | .then(res => applyFromDetail(res?.data)) |
| | | .catch(() => { |
| | | uni.showToast({ title: "è·å模æ¿é
置失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | const openTemplateImport = () => { |
| | | if (!canImportTemplate.value) { |
| | | uni.showToast({ title: "ç³»ç»å
置模æ¿ä¸å¯å¯¼å
¥", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | listApprovalTemplatePage({ |
| | | page: { current: 1, size: 200 }, |
| | | approvalTemplateDto: {}, |
| | | }) |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | | importTemplateList.value = records.filter( |
| | | item => |
| | | item?.id != null && String(item.id) !== String(templateId.value) |
| | | ); |
| | | if (!importTemplateList.value.length) { |
| | | uni.showToast({ title: "ææ å¯å¯¼å
¥ç模æ¿", icon: "none" }); |
| | | return; |
| | | } |
| | | showTemplateImportSheet.value = true; |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å 载模æ¿å表失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | const onSelectImportTemplate = action => { |
| | | showTemplateImportSheet.value = false; |
| | | const value = String(action?.value ?? ""); |
| | | if (!value) return; |
| | | applyTemplateImport(value); |
| | | }; |
| | | |
| | | const resetFieldDraft = () => { |
| | | fieldDraft.label = ""; |
| | | fieldDraft.key = ""; |
| | | fieldDraft.type = "text"; |
| | | fieldDraft.defaultValue = ""; |
| | | fieldDraft.required = true; |
| | | fieldDraft.optionSource = "manual"; |
| | | fieldDraft.options = [createEmptyFieldOption()]; |
| | | }; |
| | | |
| | | const resolveActionValue = (action, options) => { |
| | | if (action?.value !== undefined && action?.value !== null) { |
| | | return action.value; |
| | | } |
| | | const name = action?.name; |
| | | if (name == null) return undefined; |
| | | return options.find(opt => opt.name === name)?.value; |
| | | }; |
| | | |
| | | const onSelectFieldType = action => { |
| | | const nextType = resolveActionValue(action, FIELD_EDITOR_TYPE_OPTIONS); |
| | | if (!nextType || fieldDraft.type === nextType) return; |
| | | fieldDraft.type = nextType; |
| | | fieldDraft.defaultValue = ""; |
| | | if (!isSelectField(fieldDraft)) { |
| | | fieldDraft.optionSource = "manual"; |
| | | fieldDraft.options = [createEmptyFieldOption()]; |
| | | } else if (!fieldDraft.options?.length) { |
| | | fieldDraft.options = [createEmptyFieldOption()]; |
| | | } |
| | | }; |
| | | |
| | | const openInlinePicker = (title, options, mode) => { |
| | | inlinePickerTitle.value = title; |
| | | inlinePickerOptions.value = options; |
| | | inlinePickerMode.value = mode; |
| | | inlinePickerShow.value = true; |
| | | }; |
| | | |
| | | const closeInlinePicker = () => { |
| | | inlinePickerShow.value = false; |
| | | inlinePickerMode.value = ""; |
| | | inlinePickerOptions.value = []; |
| | | }; |
| | | |
| | | const isInlinePickerItemActive = item => { |
| | | if (inlinePickerMode.value === "fieldType") { |
| | | return String(fieldDraft.type) === String(item.value); |
| | | } |
| | | if (inlinePickerMode.value === "optionSource") { |
| | | return String(fieldDraft.optionSource) === String(item.value); |
| | | } |
| | | if (inlinePickerMode.value === "defaultValue") { |
| | | const val = fieldDraft.defaultValue; |
| | | if (val === "" || val === undefined || val === null) { |
| | | return item.value === "" || item.value === undefined || item.value === null; |
| | | } |
| | | return String(val) === String(item.value); |
| | | } |
| | | return false; |
| | | }; |
| | | |
| | | const onInlinePickerSelect = item => { |
| | | if (inlinePickerMode.value === "fieldType") { |
| | | onSelectFieldType(item); |
| | | } else if (inlinePickerMode.value === "optionSource") { |
| | | onSelectOptionSource(item); |
| | | } else if (inlinePickerMode.value === "defaultValue") { |
| | | onSelectDefaultOption(item); |
| | | } |
| | | closeInlinePicker(); |
| | | }; |
| | | |
| | | const openFieldTypePicker = () => { |
| | | openInlinePicker( |
| | | "æ§ä»¶ç±»å", |
| | | FIELD_EDITOR_TYPE_OPTIONS.map(item => ({ |
| | | name: item.name, |
| | | value: item.value, |
| | | })), |
| | | "fieldType" |
| | | ); |
| | | }; |
| | | |
| | | const onSelectOptionSource = action => { |
| | | const nextSource = resolveActionValue(action, FIELD_OPTION_SOURCE_OPTIONS); |
| | | if (!nextSource) return; |
| | | fieldDraft.optionSource = nextSource; |
| | | fieldDraft.defaultValue = ""; |
| | | if (nextSource === "manual" && !fieldDraft.options?.length) { |
| | | fieldDraft.options = [createEmptyFieldOption()]; |
| | | } |
| | | }; |
| | | |
| | | const openOptionSourcePicker = () => { |
| | | openInlinePicker( |
| | | "éé¡¹æ¥æº", |
| | | FIELD_OPTION_SOURCE_OPTIONS.map(item => ({ |
| | | name: item.name, |
| | | value: item.value, |
| | | })), |
| | | "optionSource" |
| | | ); |
| | | }; |
| | | |
| | | const addDraftOption = () => { |
| | | fieldDraft.options.push(createEmptyFieldOption()); |
| | | }; |
| | | |
| | | const removeDraftOption = index => { |
| | | if (fieldDraft.options.length <= 1) { |
| | | fieldDraft.options[0] = createEmptyFieldOption(); |
| | | return; |
| | | } |
| | | fieldDraft.options.splice(index, 1); |
| | | }; |
| | | |
| | | const openDefaultSelectSheet = () => { |
| | | const options = resolveFieldOptions(fieldDraft, { |
| | | users: userList.value, |
| | | depts: deptList.value, |
| | | }); |
| | | if (!options.length) { |
| | | uni.showToast({ title: "请å
é
ç½®ä¸æé项", icon: "none" }); |
| | | return; |
| | | } |
| | | openInlinePicker("é»è®¤å¼", defaultSelectActions.value, "defaultValue"); |
| | | }; |
| | | |
| | | const onSelectDefaultOption = action => { |
| | | fieldDraft.defaultValue = |
| | | action.value === undefined || action.value === null |
| | | ? "" |
| | | : String(action.value); |
| | | }; |
| | | |
| | | const closeDefaultDatePicker = () => { |
| | | showDefaultDatePicker.value = false; |
| | | defaultDatePickerMode.value = "date"; |
| | | defaultRangePickerPart.value = "start"; |
| | | }; |
| | | |
| | | const openDefaultDatePicker = () => { |
| | | defaultDatePickerMode.value = "date"; |
| | | const parsed = Date.parse(fieldDraft.defaultValue); |
| | | defaultDateTs.value = Number.isNaN(parsed) ? Date.now() : parsed; |
| | | showDefaultDatePicker.value = true; |
| | | }; |
| | | |
| | | const openDefaultRangePicker = part => { |
| | | defaultDatePickerMode.value = "datetime"; |
| | | defaultRangePickerPart.value = part; |
| | | const parts = parseDatetimerangeValue(fieldDraft.defaultValue); |
| | | const val = part === "start" ? parts.start : parts.end; |
| | | defaultDateTs.value = parseFieldDateToTs(val) ?? Date.now(); |
| | | showDefaultDatePicker.value = true; |
| | | }; |
| | | |
| | | const onDefaultDatePickerConfirm = e => { |
| | | const ts = e?.value ?? defaultDateTs.value; |
| | | if (defaultDatePickerMode.value === "datetime") { |
| | | const parts = parseDatetimerangeValue(fieldDraft.defaultValue); |
| | | const formatted = formatFieldDateValue({ type: "datetime" }, ts); |
| | | fieldDraft.defaultValue = joinDatetimerangeValue( |
| | | defaultRangePickerPart.value === "start" ? formatted : parts.start, |
| | | defaultRangePickerPart.value === "end" ? formatted : parts.end |
| | | ); |
| | | } else { |
| | | fieldDraft.defaultValue = formatDateToYMD(ts); |
| | | } |
| | | closeDefaultDatePicker(); |
| | | }; |
| | | |
| | | const onFieldItemClick = (field, index) => { |
| | | if (isFieldLocked(field)) return; |
| | | openFieldEditor(field, index); |
| | | }; |
| | | |
| | | const openFieldEditor = (field, index = -1) => { |
| | | if (field && isFieldLocked(field)) { |
| | | uni.showToast({ title: "ç³»ç»å
置填æ¥é¡¹ä¸å¯ä¿®æ¹", icon: "none" }); |
| | | return; |
| | | } |
| | | editingFieldIndex.value = index; |
| | | if (field) { |
| | | fieldDraft.label = field.label || ""; |
| | | fieldDraft.key = field.key || ""; |
| | | fieldDraft.type = field.type || "text"; |
| | | fieldDraft.defaultValue = field.defaultValue ?? ""; |
| | | fieldDraft.required = !!field.required; |
| | | fieldDraft.optionSource = getFieldOptionSource(field); |
| | | fieldDraft.options = normalizeDraftOptions(field); |
| | | } else { |
| | | resetFieldDraft(); |
| | | } |
| | | defaultDateTs.value = Date.now(); |
| | | showFieldEditor.value = true; |
| | | }; |
| | | |
| | | const closeFieldEditor = () => { |
| | | closeInlinePicker(); |
| | | showFieldEditor.value = false; |
| | | editingFieldIndex.value = -1; |
| | | }; |
| | | |
| | | const normalizeDraftOptions = field => { |
| | | const options = field?.options; |
| | | if (!Array.isArray(options) || !options.length) { |
| | | return [createEmptyFieldOption()]; |
| | | } |
| | | return options.map(opt => ({ |
| | | label: opt?.label ?? "", |
| | | value: opt?.value != null ? String(opt.value) : "", |
| | | })); |
| | | }; |
| | | |
| | | const buildFieldKey = label => { |
| | | const base = (label || "field") |
| | | .trim() |
| | | .replace(/\s+/g, "_") |
| | | .replace(/[^\w\u4e00-\u9fa5]/g, ""); |
| | | let key = base || "field"; |
| | | let index = 1; |
| | | while (formConfig.fields.some((item, idx) => item.key === key && idx !== editingFieldIndex.value)) { |
| | | key = `${base}_${index++}`; |
| | | } |
| | | return key; |
| | | }; |
| | | |
| | | const confirmFieldEditor = () => { |
| | | if ( |
| | | editingFieldIndex.value >= 0 && |
| | | isFieldLocked(formConfig.fields[editingFieldIndex.value]) |
| | | ) { |
| | | uni.showToast({ title: "ç³»ç»å
置填æ¥é¡¹ä¸å¯ä¿®æ¹", icon: "none" }); |
| | | return; |
| | | } |
| | | if (!fieldDraft.label?.trim()) { |
| | | uni.showToast({ title: "请è¾å
¥æ¾ç¤ºåç§°", icon: "none" }); |
| | | return; |
| | | } |
| | | const existingKey = |
| | | editingFieldIndex.value >= 0 |
| | | ? formConfig.fields[editingFieldIndex.value]?.key |
| | | : null; |
| | | const draftKey = fieldDraft.key?.trim() || existingKey || buildFieldKey(fieldDraft.label); |
| | | if (!draftKey) { |
| | | uni.showToast({ title: "请è¾å
¥å段æ è¯", icon: "none" }); |
| | | return; |
| | | } |
| | | const duplicateKey = formConfig.fields.some( |
| | | (item, idx) => item.key === draftKey && idx !== editingFieldIndex.value |
| | | ); |
| | | if (duplicateKey) { |
| | | uni.showToast({ title: "åæ®µæ è¯å·²åå¨", icon: "none" }); |
| | | return; |
| | | } |
| | | if (isSelectField(fieldDraft) && fieldDraft.optionSource === "manual") { |
| | | const validOptions = (fieldDraft.options || []).filter( |
| | | opt => opt.label?.trim() && opt.value?.trim() |
| | | ); |
| | | if (!validOptions.length) { |
| | | uni.showToast({ title: "请è³å°é
ç½®ä¸ä¸ªä¸æé项", icon: "none" }); |
| | | return; |
| | | } |
| | | } |
| | | const payload = buildFieldConfigPayload( |
| | | { ...fieldDraft, key: draftKey }, |
| | | existingKey |
| | | ); |
| | | if (editingFieldIndex.value >= 0) { |
| | | formConfig.fields.splice(editingFieldIndex.value, 1, payload); |
| | | } else { |
| | | formConfig.fields.push(payload); |
| | | } |
| | | closeFieldEditor(); |
| | | }; |
| | | |
| | | const removeField = index => { |
| | | const field = formConfig.fields[index]; |
| | | if (isFieldLocked(field)) { |
| | | uni.showToast({ title: "ç³»ç»å
置填æ¥é¡¹ä¸å¯å é¤", icon: "none" }); |
| | | return; |
| | | } |
| | | formConfig.fields.splice(index, 1); |
| | | }; |
| | | |
| | | const addNode = () => { |
| | | flowNodes.value.push(createNode()); |
| | | }; |
| | | |
| | | const removeNode = index => { |
| | | if (flowNodes.value.length <= 1) { |
| | | uni.showToast({ title: "è³å°ä¿çä¸ä¸ªå®¡æ¹èç¹", icon: "none" }); |
| | | return; |
| | | } |
| | | flowNodes.value.splice(index, 1); |
| | | }; |
| | | |
| | | const openUserPicker = nodeIndex => { |
| | | editingNodeIndex.value = nodeIndex; |
| | | pickerSelectedIds.value = []; |
| | | showUserPicker.value = true; |
| | | }; |
| | | |
| | | const closeUserPicker = () => { |
| | | showUserPicker.value = false; |
| | | editingNodeIndex.value = -1; |
| | | pickerSelectedIds.value = []; |
| | | }; |
| | | |
| | | const isUserSelected = userId => pickerSelectedIds.value.includes(userId); |
| | | |
| | | const toggleUser = user => { |
| | | const ids = pickerSelectedIds.value; |
| | | const index = ids.indexOf(user.userId); |
| | | if (index >= 0) { |
| | | ids.splice(index, 1); |
| | | } else { |
| | | ids.push(user.userId); |
| | | } |
| | | }; |
| | | |
| | | const confirmUserPicker = () => { |
| | | const node = flowNodes.value[editingNodeIndex.value]; |
| | | if (!node) { |
| | | closeUserPicker(); |
| | | return; |
| | | } |
| | | const selectedUsers = userList.value.filter(user => |
| | | pickerSelectedIds.value.includes(user.userId) |
| | | ); |
| | | if (!selectedUsers.length) { |
| | | uni.showToast({ title: "è¯·éæ©å®¡æ¹äºº", icon: "none" }); |
| | | return; |
| | | } |
| | | const startSort = node.approvers.length; |
| | | selectedUsers.forEach((user, idx) => { |
| | | node.approvers.push({ |
| | | approverId: user.userId, |
| | | approverName: user.nickName, |
| | | sortNo: startSort + idx + 1, |
| | | }); |
| | | }); |
| | | closeUserPicker(); |
| | | }; |
| | | |
| | | const removeApprover = (nodeIndex, approverIndex) => { |
| | | const node = flowNodes.value[nodeIndex]; |
| | | if (!node?.approvers?.length) return; |
| | | const next = node.approvers |
| | | .filter((_, idx) => idx !== approverIndex) |
| | | .map((item, idx) => ({ |
| | | ...item, |
| | | sortNo: idx + 1, |
| | | })); |
| | | node.approvers = next; |
| | | }; |
| | | |
| | | const validateFlow = () => { |
| | | if (!flowNodes.value.length) { |
| | | uni.showToast({ title: "请é
ç½®å®¡æ¹æµç¨", icon: "none" }); |
| | | return false; |
| | | } |
| | | const emptyNode = flowNodes.value.find(node => !node.approvers.length); |
| | | if (emptyNode) { |
| | | uni.showToast({ title: "请为æ¯ä¸ªå®¡æ¹èç¹æ·»å 审æ¹äºº", icon: "none" }); |
| | | return false; |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | const buildSubmitPayload = () => { |
| | | const tid = templateId.value; |
| | | const payload = { |
| | | templateName: form.templateName.trim(), |
| | | enabled: form.enabled, |
| | | description: form.description?.trim() || "", |
| | | businessType: form.businessType, |
| | | templateType: form.templateType, |
| | | formConfig: JSON.stringify({ |
| | | prompt: formConfig.prompt?.trim() || "", |
| | | fields: formConfig.fields, |
| | | }), |
| | | nodes: flowNodes.value.map((node, index) => { |
| | | const nodePayload = { |
| | | levelNo: index + 1, |
| | | approveType: node.approveType, |
| | | approvers: node.approvers.map((approver, approverIndex) => { |
| | | const approverPayload = { |
| | | approverId: approver.approverId, |
| | | approverName: approver.approverName, |
| | | sortNo: approverIndex + 1, |
| | | }; |
| | | if (isEditMode.value) { |
| | | if (approver.id != null) approverPayload.id = approver.id; |
| | | if (approver.nodeId != null) approverPayload.nodeId = approver.nodeId; |
| | | else if (node.id != null) approverPayload.nodeId = node.id; |
| | | if (approver.templateId != null) approverPayload.templateId = approver.templateId; |
| | | else if (tid != null) approverPayload.templateId = tid; |
| | | } |
| | | return approverPayload; |
| | | }), |
| | | }; |
| | | if (isEditMode.value) { |
| | | if (node.id != null) nodePayload.id = node.id; |
| | | if (node.templateId != null) nodePayload.templateId = node.templateId; |
| | | else if (tid != null) nodePayload.templateId = tid; |
| | | } |
| | | return nodePayload; |
| | | }), |
| | | }; |
| | | |
| | | if (isEditMode.value) { |
| | | payload.id = tid; |
| | | } |
| | | |
| | | return payload; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid || !validateFlow()) return; |
| | | |
| | | submitting.value = true; |
| | | const submitApi = isEditMode.value ? updateApprovalTemplate : addApprovalTemplate; |
| | | submitApi(buildSubmitPayload()) |
| | | .then(() => { |
| | | uni.showToast({ |
| | | title: isEditMode.value ? "ä¿®æ¹æå" : "ä¿åæå", |
| | | icon: "success", |
| | | }); |
| | | uni.removeStorageSync(EDIT_STORAGE_KEY); |
| | | setTimeout(() => { |
| | | uni.navigateBack(); |
| | | }, 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ |
| | | title: isEditMode.value ? "ä¿®æ¹å¤±è´¥" : "ä¿å失败", |
| | | icon: "none", |
| | | }); |
| | | }) |
| | | .finally(() => { |
| | | submitting.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | const row = uni.getStorageSync(EDIT_STORAGE_KEY); |
| | | if (row && String(row.id) === String(options.id)) { |
| | | fillFormFromRow(row); |
| | | } else { |
| | | templateId.value = options.id; |
| | | uni.showToast({ title: "æªè·åå°æ¨¡æ¿æ°æ®", icon: "none" }); |
| | | } |
| | | uni.removeStorageSync(EDIT_STORAGE_KEY); |
| | | } |
| | | }); |
| | | |
| | | const loadTemplateTypes = () => |
| | | fetchApprovalTemplateTypes() |
| | | .then(opts => { |
| | | businessTypeOptions.value = opts; |
| | | if (!templateId.value && opts.length) { |
| | | const matched = opts.some( |
| | | opt => String(opt.value) === String(form.businessType) |
| | | ); |
| | | if (!matched) { |
| | | form.businessType = opts[0].value; |
| | | } |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "è·å审æ¹ç±»å失败", icon: "none" }); |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | loadTemplateTypes(); |
| | | userListNoPageByTenantId() |
| | | .then(res => { |
| | | userList.value = res?.data || []; |
| | | }) |
| | | .catch(() => { |
| | | userList.value = []; |
| | | }); |
| | | getDept() |
| | | .then(res => { |
| | | deptList.value = res?.data || []; |
| | | }) |
| | | .catch(() => { |
| | | deptList.value = []; |
| | | }); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | $primary: #2979ff; |
| | | $primary-light: #ecf5ff; |
| | | $text: #1f2d3d; |
| | | $text-secondary: #606266; |
| | | $text-muted: #909399; |
| | | $border: #ebeef5; |
| | | $bg-page: #f0f3f8; |
| | | $radius-lg: 12px; |
| | | $radius-md: 10px; |
| | | $shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05); |
| | | |
| | | .template-edit-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | background: $bg-page; |
| | | } |
| | | |
| | | .form-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding: 10px 12px calc(96px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .section-card { |
| | | margin-bottom: 10px; |
| | | background: #fff; |
| | | border-radius: $radius-lg; |
| | | overflow: hidden; |
| | | box-shadow: $shadow-card; |
| | | } |
| | | |
| | | .section-head { |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | |
| | | &--between { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | } |
| | | |
| | | .section-head-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | padding-left: 10px; |
| | | border-left: 3px solid $primary; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .section-count { |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | padding-left: 13px; |
| | | } |
| | | |
| | | .head-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .head-link { |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | |
| | | &--import { |
| | | color: $text-secondary; |
| | | padding: 6px 12px; |
| | | border: 1px solid #dce3ed; |
| | | border-radius: 8px; |
| | | background: #fff; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | &--disabled { |
| | | color: #c0c4cc; |
| | | border-color: #ebeef5; |
| | | background: #f5f7fa; |
| | | } |
| | | |
| | | &--primary { |
| | | color: #fff; |
| | | font-weight: 500; |
| | | padding: 6px 14px; |
| | | border: none; |
| | | border-radius: 8px; |
| | | background: linear-gradient(135deg, #4d8dff 0%, #2979ff 100%); |
| | | box-shadow: 0 2px 8px rgba(41, 121, 255, 0.25); |
| | | } |
| | | } |
| | | |
| | | :deep(.form-item-select--disabled .u-form-item__body) { |
| | | opacity: 0.65; |
| | | } |
| | | |
| | | .section-body { |
| | | padding: 2px 16px 14px; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 10px; |
| | | border-radius: $radius-lg; |
| | | overflow: hidden; |
| | | box-shadow: $shadow-card; |
| | | } |
| | | |
| | | :deep(.form-section .u-cell-group__title) { |
| | | padding: 12px 16px 8px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: $text !important; |
| | | background: #fff !important; |
| | | } |
| | | |
| | | :deep(.form-section .u-form-item) { |
| | | padding: 0 16px !important; |
| | | } |
| | | |
| | | :deep(.form-section .u-form-item__body) { |
| | | padding: 10px 0 !important; |
| | | min-height: auto !important; |
| | | } |
| | | |
| | | :deep(.form-item-name .u-form-item__body) { |
| | | flex-direction: row !important; |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-name .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.name-input-inline), |
| | | :deep(.name-input-inline .u-input__content) { |
| | | width: 100% !important; |
| | | flex: 1 !important; |
| | | } |
| | | |
| | | :deep(.name-input-inline input), |
| | | :deep(.name-input-inline .u-input__content__field-wrapper__field) { |
| | | width: 100% !important; |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | :deep(.form-item-select .u-form-item__body) { |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-select .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.form-item-select .u-input__content__field-wrapper__field) { |
| | | text-align: right !important; |
| | | } |
| | | |
| | | :deep(.form-item-switch .u-form-item__body) { |
| | | flex-direction: row !important; |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-switch .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | display: flex !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | .switch-wrap { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.form-item-desc .u-form-item__body) { |
| | | flex-direction: column !important; |
| | | align-items: stretch !important; |
| | | padding: 10px 0 12px !important; |
| | | } |
| | | |
| | | :deep(.form-item-desc .u-form-item__content) { |
| | | justify-content: stretch !important; |
| | | width: 100% !important; |
| | | } |
| | | |
| | | .desc-input-shell { |
| | | width: 100%; |
| | | box-sizing: border-box; |
| | | padding: 8px 12px; |
| | | background: #fff; |
| | | border: 1px solid #dcdfe6; |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | :deep(.desc-input-shell .u-textarea), |
| | | :deep(.desc-input-shell textarea) { |
| | | width: 100% !important; |
| | | font-size: 15px !important; |
| | | text-align: left !important; |
| | | } |
| | | |
| | | .form-row-item { |
| | | margin: 0 !important; |
| | | padding: 0 !important; |
| | | } |
| | | |
| | | :deep(.form-row-item .u-form-item__body) { |
| | | padding: 0; |
| | | } |
| | | |
| | | :deep(.form-row-item .u-form-item__body__right__message) { |
| | | margin-top: 4px; |
| | | padding-left: 0; |
| | | } |
| | | |
| | | .form-row { |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | &--column { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | gap: 8px; |
| | | } |
| | | |
| | | &--compact { |
| | | padding-top: 8px; |
| | | } |
| | | } |
| | | |
| | | .form-row-label { |
| | | display: block; |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | margin-bottom: 8px; |
| | | |
| | | &.required::before { |
| | | content: "*"; |
| | | color: #f56c6c; |
| | | margin-right: 3px; |
| | | } |
| | | } |
| | | |
| | | .form-row--column .form-row-label { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .prompt-row { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 12px 0; |
| | | margin-bottom: 4px; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .prompt-label { |
| | | flex-shrink: 0; |
| | | width: 88px; |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | } |
| | | |
| | | .prompt-input { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | :deep(.prompt-input), |
| | | :deep(.prompt-input .u-input__content) { |
| | | width: 100% !important; |
| | | } |
| | | |
| | | :deep(.prompt-input input), |
| | | :deep(.prompt-input .u-input__content__field-wrapper__field) { |
| | | width: 100% !important; |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | .input-box, |
| | | .textarea-box { |
| | | background: #f7f9fc; |
| | | border-radius: 10px; |
| | | border: 1px solid #eef1f6; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .textarea-box { |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .field-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .field-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 14px; |
| | | background: #fff; |
| | | border-radius: $radius-md; |
| | | border: 1px solid #e8eef5; |
| | | box-shadow: 0 1px 4px rgba(31, 45, 61, 0.04); |
| | | transition: border-color 0.2s, box-shadow 0.2s; |
| | | |
| | | &:active:not(.field-item--locked) { |
| | | border-color: #c6daf5; |
| | | box-shadow: 0 2px 8px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | &--locked { |
| | | background: #fafbfd; |
| | | } |
| | | } |
| | | |
| | | .field-order { |
| | | width: 28px; |
| | | height: 28px; |
| | | border-radius: 8px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .field-main { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .field-title-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .field-name { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .field-tags { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .field-key { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | font-family: ui-monospace, monospace; |
| | | } |
| | | |
| | | .field-lock-tag { |
| | | flex-shrink: 0; |
| | | font-size: 11px; |
| | | color: #909399; |
| | | padding: 4px 8px; |
| | | background: #f0f2f5; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .type-tag { |
| | | font-size: 11px; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | |
| | | &--text { |
| | | color: #2979ff; |
| | | background: #ecf5ff; |
| | | } |
| | | |
| | | &--area { |
| | | color: #7c5cfc; |
| | | background: #f3efff; |
| | | } |
| | | |
| | | &--num { |
| | | color: #e6a23c; |
| | | background: #fdf6ec; |
| | | } |
| | | |
| | | &--date { |
| | | color: #18a058; |
| | | background: #e8faf0; |
| | | } |
| | | |
| | | &--select { |
| | | color: #9c27b0; |
| | | background: #f6edfc; |
| | | } |
| | | } |
| | | |
| | | .req-tag { |
| | | font-size: 11px; |
| | | padding: 2px 6px; |
| | | color: #f56c6c; |
| | | background: #fef0f0; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .field-default { |
| | | display: block; |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .field-actions { |
| | | display: flex; |
| | | gap: 6px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .icon-btn { |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | | &--edit { |
| | | background: #ecf5ff; |
| | | } |
| | | |
| | | &--del { |
| | | background: #fef0f0; |
| | | } |
| | | } |
| | | |
| | | .empty-mini { |
| | | padding: 32px 16px; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | color: $text-muted; |
| | | background: #fafbfd; |
| | | border: 1px dashed #dce8f5; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | .flow-wrap { |
| | | padding: 10px 16px 14px; |
| | | } |
| | | |
| | | .flow-node-block { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .flow-node-card { |
| | | background: #fafbfd; |
| | | border: 1px solid #e8eef5; |
| | | border-radius: $radius-md; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .node-header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .node-level-badge { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 8px; |
| | | background: $primary; |
| | | color: #fff; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .node-level-text { |
| | | flex: 1; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .node-delete { |
| | | padding: 6px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .approve-type-row { |
| | | display: flex; |
| | | background: #f0f3f8; |
| | | border-radius: 8px; |
| | | padding: 3px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .type-btn { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 8px 0; |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | border-radius: 6px; |
| | | |
| | | &.active { |
| | | background: #fff; |
| | | color: $primary; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .approver-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .approver-chip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 6px 12px 6px 6px; |
| | | background: #fff; |
| | | border: 1px solid #dce8f8; |
| | | border-radius: 24px; |
| | | box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06); |
| | | } |
| | | |
| | | .approver-avatar { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .approver-name { |
| | | font-size: 13px; |
| | | color: $text; |
| | | max-width: 80px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .approver-remove { |
| | | flex-shrink: 0; |
| | | width: 22px; |
| | | height: 22px; |
| | | margin-left: 2px; |
| | | border-radius: 50%; |
| | | background: #f2f3f5; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .approver-remove--active { |
| | | background: #fde2e2; |
| | | } |
| | | |
| | | .remove-icon { |
| | | font-size: 16px; |
| | | line-height: 1; |
| | | color: #909399; |
| | | font-weight: 300; |
| | | } |
| | | |
| | | .add-approver { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | padding: 8px 14px; |
| | | border: 1.5px dashed #a8cfff; |
| | | border-radius: 24px; |
| | | background: $primary-light; |
| | | color: $primary; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .flow-connector { |
| | | display: flex; |
| | | justify-content: center; |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .flow-connector-line { |
| | | width: 2px; |
| | | height: 14px; |
| | | background: #d0dff0; |
| | | } |
| | | |
| | | .add-node-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | margin-top: 8px; |
| | | padding: 11px; |
| | | border: 1px dashed #c6daf5; |
| | | border-radius: $radius-md; |
| | | color: $primary; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .sheet-handle { |
| | | width: 36px; |
| | | height: 4px; |
| | | margin: 10px auto 4px; |
| | | background: #d8dde6; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .field-editor .sheet-handle { |
| | | background: #c8ced8; |
| | | } |
| | | |
| | | .field-editor { |
| | | position: relative; |
| | | display: flex; |
| | | flex-direction: column; |
| | | max-height: 88vh; |
| | | background: #f5f7fb; |
| | | border-radius: 16px 16px 0 0; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .user-picker { |
| | | position: relative; |
| | | padding: 0 18px calc(18px + env(safe-area-inset-bottom)); |
| | | background: #fff; |
| | | max-height: 85vh; |
| | | } |
| | | |
| | | .editor-header { |
| | | padding: 4px 20px 12px; |
| | | background: #fff; |
| | | text-align: center; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | .editor-subtitle { |
| | | display: block; |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .editor-picker-layer { |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | z-index: 20; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .editor-picker-mask { |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.45); |
| | | } |
| | | |
| | | .editor-picker-panel { |
| | | position: relative; |
| | | z-index: 1; |
| | | background: #fff; |
| | | border-radius: 16px 16px 0 0; |
| | | max-height: 55vh; |
| | | padding-bottom: env(safe-area-inset-bottom); |
| | | } |
| | | |
| | | .editor-picker-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 14px 18px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .editor-picker-cancel { |
| | | font-size: 15px; |
| | | color: #909399; |
| | | min-width: 48px; |
| | | } |
| | | |
| | | .editor-picker-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .editor-picker-placeholder { |
| | | min-width: 48px; |
| | | } |
| | | |
| | | .editor-picker-scroll { |
| | | max-height: calc(55vh - 52px); |
| | | } |
| | | |
| | | .editor-picker-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 16px 18px; |
| | | font-size: 16px; |
| | | color: $text; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | |
| | | &--active { |
| | | color: $primary; |
| | | background: #f5f9ff; |
| | | } |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .editor-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | max-height: 62vh; |
| | | } |
| | | |
| | | .editor-form { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | padding: 12px 16px 16px; |
| | | } |
| | | |
| | | .editor-section-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 14px 14px 4px; |
| | | box-shadow: 0 1px 6px rgba(31, 45, 61, 0.05); |
| | | } |
| | | |
| | | .editor-section-head { |
| | | margin-bottom: 10px; |
| | | padding-bottom: 10px; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | } |
| | | |
| | | .editor-section-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | padding-left: 8px; |
| | | border-left: 3px solid $primary; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .editor-cell { |
| | | margin-bottom: 14px; |
| | | |
| | | &--tap:active .picker-value-row { |
| | | background: #eef4ff; |
| | | border-color: #c6daf5; |
| | | } |
| | | |
| | | &--switch { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | padding: 4px 0 10px; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | &--value { |
| | | margin-bottom: 10px; |
| | | } |
| | | } |
| | | |
| | | .switch-label-wrap { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2px; |
| | | } |
| | | |
| | | .switch-hint { |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .editor-input-box { |
| | | background: #f7f9fc; |
| | | border: 1px solid #e8ecf2; |
| | | border-radius: 10px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.editor-input-box .u-input) { |
| | | background: transparent !important; |
| | | } |
| | | |
| | | .default-hint { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | line-height: 1.5; |
| | | margin: -4px 0 10px; |
| | | padding: 0 2px; |
| | | } |
| | | |
| | | .manual-options { |
| | | margin: 4px 0 12px; |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | .manual-options-title { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .manual-options-table { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .option-table-head { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 0 4px 4px; |
| | | } |
| | | |
| | | .option-col { |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | font-weight: 500; |
| | | |
| | | &--idx { |
| | | width: 22px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | &--label { |
| | | flex: 1.4; |
| | | min-width: 0; |
| | | } |
| | | |
| | | &--value { |
| | | flex: 0.9; |
| | | min-width: 72px; |
| | | } |
| | | |
| | | &--action { |
| | | width: 32px; |
| | | flex-shrink: 0; |
| | | } |
| | | } |
| | | |
| | | .option-card { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 8px 10px; |
| | | background: #f8fafc; |
| | | border: 1px solid #e8ecf2; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | .option-idx { |
| | | width: 22px; |
| | | height: 22px; |
| | | flex-shrink: 0; |
| | | border-radius: 6px; |
| | | background: #eef2f8; |
| | | color: $text-muted; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | line-height: 22px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .option-input-wrap { |
| | | flex: 1.4; |
| | | min-width: 0; |
| | | background: #fff; |
| | | border: 1px solid #e4e8ef; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | |
| | | &--value { |
| | | flex: 0.9; |
| | | min-width: 72px; |
| | | } |
| | | } |
| | | |
| | | :deep(.option-input-wrap .u-input) { |
| | | background: transparent !important; |
| | | } |
| | | |
| | | :deep(.option-input-wrap input), |
| | | :deep(.option-input-wrap .u-input__content__field-wrapper__field) { |
| | | font-size: 14px !important; |
| | | height: 36px !important; |
| | | min-height: 36px !important; |
| | | padding: 0 10px !important; |
| | | } |
| | | |
| | | .option-del { |
| | | flex-shrink: 0; |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 8px; |
| | | background: #fff; |
| | | border: 1px solid #fde2e2; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .option-del--active { |
| | | background: #fef0f0; |
| | | } |
| | | |
| | | .add-option-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | margin-top: 10px; |
| | | padding: 11px; |
| | | border: 1.5px dashed #b8d4ff; |
| | | border-radius: 10px; |
| | | background: linear-gradient(180deg, #f8fbff 0%, #f0f6ff 100%); |
| | | color: $primary; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .add-option-btn--active { |
| | | background: #e8f2ff; |
| | | border-color: $primary; |
| | | } |
| | | |
| | | .option-source-tip { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | gap: 6px; |
| | | padding: 10px 12px; |
| | | margin-bottom: 10px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .editor-title { |
| | | display: block; |
| | | font-size: 17px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .picker-value-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | min-height: 44px; |
| | | padding: 0 14px; |
| | | background: #f7f9fc; |
| | | border: 1px solid #e8ecf2; |
| | | border-radius: 10px; |
| | | gap: 8px; |
| | | transition: background 0.15s, border-color 0.15s; |
| | | |
| | | &--tap:active { |
| | | background: #eef4ff; |
| | | border-color: #c6daf5; |
| | | } |
| | | } |
| | | |
| | | .picker-value { |
| | | flex: 1; |
| | | min-width: 0; |
| | | font-size: 15px; |
| | | color: $text; |
| | | text-align: left; |
| | | line-height: 1.4; |
| | | |
| | | &--placeholder { |
| | | color: #c0c4cc; |
| | | } |
| | | } |
| | | |
| | | .editor-label { |
| | | display: block; |
| | | font-size: 13px; |
| | | font-weight: 500; |
| | | color: $text-secondary; |
| | | margin-bottom: 8px; |
| | | |
| | | &.required::before { |
| | | content: "*"; |
| | | color: #f56c6c; |
| | | margin-right: 3px; |
| | | } |
| | | } |
| | | |
| | | .editor-cell--switch .editor-label { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .daterange-default-wrap { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .daterange-default-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .daterange-default-label { |
| | | font-size: 13px; |
| | | color: $text-secondary; |
| | | } |
| | | |
| | | .type-chip-grid { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .type-chip { |
| | | text-align: center; |
| | | padding: 10px 6px; |
| | | font-size: 13px; |
| | | color: $text-secondary; |
| | | background: #f7f9fc; |
| | | border: 1px solid #eef1f6; |
| | | border-radius: 8px; |
| | | |
| | | &.active { |
| | | color: $primary; |
| | | background: $primary-light; |
| | | border-color: $primary; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .editor-footer { |
| | | display: flex; |
| | | gap: 12px; |
| | | padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); |
| | | background: #fff; |
| | | border-top: 1px solid #eef0f4; |
| | | box-shadow: 0 -4px 12px rgba(31, 45, 61, 0.06); |
| | | } |
| | | |
| | | .editor-btn { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 12px 0; |
| | | border-radius: 10px; |
| | | font-size: 15px; |
| | | font-weight: 500; |
| | | |
| | | &--cancel { |
| | | color: $text-secondary; |
| | | background: #f5f7fa; |
| | | border: 1px solid #e4e7ed; |
| | | } |
| | | |
| | | &--confirm { |
| | | color: #fff; |
| | | background: linear-gradient(135deg, #4d8dff 0%, #2979ff 100%); |
| | | box-shadow: 0 4px 12px rgba(41, 121, 255, 0.35); |
| | | } |
| | | |
| | | &--confirm:active { |
| | | opacity: 0.9; |
| | | } |
| | | } |
| | | |
| | | .picker-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding-bottom: 14px; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .picker-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .picker-cancel { |
| | | font-size: 15px; |
| | | color: $text-muted; |
| | | min-width: 48px; |
| | | } |
| | | |
| | | .picker-confirm { |
| | | font-size: 15px; |
| | | color: $primary; |
| | | font-weight: 600; |
| | | min-width: 48px; |
| | | text-align: right; |
| | | } |
| | | |
| | | .user-scroll { |
| | | max-height: 52vh; |
| | | } |
| | | |
| | | .user-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 14px 4px; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | border-radius: 10px; |
| | | margin-bottom: 4px; |
| | | transition: background 0.2s; |
| | | |
| | | &.selected { |
| | | background: #f5f9ff; |
| | | } |
| | | } |
| | | |
| | | .user-avatar { |
| | | width: 40px; |
| | | height: 40px; |
| | | border-radius: 12px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .user-name { |
| | | flex: 1; |
| | | font-size: 15px; |
| | | color: $text; |
| | | } |
| | | |
| | | .user-check { |
| | | width: 22px; |
| | | height: 22px; |
| | | border-radius: 50%; |
| | | border: 2px solid #dcdfe6; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | |
| | | &.checked { |
| | | background: $primary; |
| | | border-color: $primary; |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / å®¡æ¹æ¨¡æ¿ |
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-template/index |
| | | --> |
| | | <template> |
| | | <view class="approve-template-page sales-account"> |
| | | <PageHeader title="å®¡æ¹æ¨¡æ¿" |
| | | @back="goBack" /> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input v-model="queryParams.templateName" |
| | | class="search-text" |
| | | placeholder="请è¾å
¥æ¨¡æ¿åç§°" |
| | | clearable |
| | | @confirm="handleSearch" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="handleSearch"> |
| | | <up-icon name="search" |
| | | size="24" |
| | | color="#999" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="list-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false" |
| | | @scrolltolower="loadMore"> |
| | | <view v-if="list.length" |
| | | class="ledger-list"> |
| | | <view v-for="item in list" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="item-id">{{ item.templateName || "-" }}</text> |
| | | </view> |
| | | <u-tag :type="enabledTagType(item.enabled)" |
| | | :text="enabledText(item.enabled)" /> |
| | | </view> |
| | | <up-divider /> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">审æ¹ç±»å</text> |
| | | <text class="detail-value">{{ businessTypeText(item.businessType) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">审æ¹èç¹</text> |
| | | <text class="detail-value">{{ nodeCount(item) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">模æ¿è¯´æ</text> |
| | | <text class="detail-value">{{ item.description || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å建人</text> |
| | | <text class="detail-value">{{ item.createdUserName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å建æ¶é´</text> |
| | | <text class="detail-value">{{ item.createTime || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click.stop="goDetail(item)"> |
| | | 详æ
|
| | | </up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click.stop="goEdit(item)"> |
| | | ç¼è¾ |
| | | </up-button> |
| | | <up-button v-if="!isSystemTemplate(item)" |
| | | class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click.stop="handleDelete(item)"> |
| | | å é¤ |
| | | </up-button> |
| | | </view> |
| | | </view> |
| | | <up-loadmore :status="pageStatus" /> |
| | | </view> |
| | | <view v-else |
| | | class="empty-wrap"> |
| | | <up-empty mode="list" |
| | | text="ææ å®¡æ¹æ¨¡æ¿æ°æ®" /> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { |
| | | deleteApprovalTemplate, |
| | | listApprovalTemplatePage, |
| | | } from "@/api/oa/approvalTemplate.js"; |
| | | import { |
| | | buildTypeLabelMap, |
| | | fetchApprovalTemplateTypes, |
| | | getTemplateTypeLabel, |
| | | isSystemApprovalTemplate, |
| | | } from "../../_utils/approvalTemplateType.js"; |
| | | |
| | | const EDIT_STORAGE_KEY = "oa_approve_template_edit_row"; |
| | | const typeLabelMap = ref({}); |
| | | |
| | | const queryParams = reactive({ |
| | | templateName: "", |
| | | }); |
| | | |
| | | const list = ref([]); |
| | | const pageStatus = ref("loadmore"); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | const buildListParams = () => ({ |
| | | page: { |
| | | current: page.current, |
| | | size: page.size, |
| | | }, |
| | | approvalTemplateDto: { |
| | | templateName: queryParams.templateName?.trim() || undefined, |
| | | }, |
| | | }); |
| | | |
| | | const enabledText = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "å¯ç¨"; |
| | | if (val === "0") return "åç¨"; |
| | | return "-"; |
| | | }; |
| | | |
| | | const enabledTagType = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "success"; |
| | | if (val === "0") return "info"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const businessTypeText = type => |
| | | getTemplateTypeLabel(type, typeLabelMap.value); |
| | | |
| | | const isSystemTemplate = isSystemApprovalTemplate; |
| | | |
| | | const loadTemplateTypes = () => |
| | | fetchApprovalTemplateTypes() |
| | | .then(opts => { |
| | | typeLabelMap.value = buildTypeLabelMap(opts); |
| | | }) |
| | | .catch(() => {}); |
| | | |
| | | const nodeCount = item => { |
| | | const count = item?.nodes?.length; |
| | | return count != null ? `${count} 个` : "-"; |
| | | }; |
| | | |
| | | const getList = () => { |
| | | if (pageStatus.value === "loading" || pageStatus.value === "nomore") return; |
| | | |
| | | pageStatus.value = "loading"; |
| | | listApprovalTemplatePage(buildListParams()) |
| | | .then(res => { |
| | | const pageData = res?.data || {}; |
| | | const records = pageData.records || []; |
| | | const total = pageData.total ?? 0; |
| | | |
| | | if (page.current === 1) { |
| | | list.value = records; |
| | | } else { |
| | | list.value = [...list.value, ...records]; |
| | | } |
| | | |
| | | page.total = total; |
| | | if (list.value.length >= total || records.length < page.size) { |
| | | pageStatus.value = "nomore"; |
| | | } else { |
| | | pageStatus.value = "loadmore"; |
| | | page.current += 1; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | if (page.current === 1) { |
| | | list.value = []; |
| | | } |
| | | pageStatus.value = "loadmore"; |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const handleSearch = () => { |
| | | page.current = 1; |
| | | pageStatus.value = "loadmore"; |
| | | list.value = []; |
| | | getList(); |
| | | }; |
| | | |
| | | const loadMore = () => { |
| | | if (pageStatus.value === "loadmore") { |
| | | getList(); |
| | | } |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync(EDIT_STORAGE_KEY); |
| | | uni.navigateTo({ |
| | | url: "/pages/oa/ApproveManage/approve-template/edit", |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | if (!item?.id) return; |
| | | uni.navigateTo({ |
| | | url: `/pages/oa/ApproveManage/approve-template/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | if (!item?.id) return; |
| | | uni.setStorageSync(EDIT_STORAGE_KEY, item); |
| | | uni.navigateTo({ |
| | | url: `/pages/oa/ApproveManage/approve-template/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | if (isSystemTemplate(item)) { |
| | | uni.showToast({ title: "ç³»ç»å
置模æ¿ä¸å¯å é¤", icon: "none" }); |
| | | return; |
| | | } |
| | | const name = item.templateName || "该模æ¿"; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: `ç¡®å®å é¤ã${name}ãåï¼å é¤åæ æ³æ¢å¤ã`, |
| | | confirmText: "å é¤", |
| | | confirmColor: "#f56c6c", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å é¤ä¸...", mask: true }); |
| | | deleteApprovalTemplate([item.id]) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | handleSearch(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | loadTemplateTypes(); |
| | | handleSearch(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .approve-template-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .list-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding-bottom: calc(80px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .empty-wrap { |
| | | padding: 48px 20px; |
| | | } |
| | | |
| | | .action-buttons { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | margin-top: 12px; |
| | | padding-top: 12px; |
| | | border-top: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .action-btn { |
| | | min-width: 72px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / åå¤ç®¡ç / 请åç³è¯· |
| | | è·¯ç±ï¼/pages/oa/AttendManage/leave-apply/index |
| | | --> |
| | | <template> |
| | | <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.LEAVE" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js"; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / åå¤ç®¡ç / å çç³è¯· |
| | | è·¯ç±ï¼/pages/oa/AttendManage/overtime-apply/index |
| | | --> |
| | | <template> |
| | | <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.OVERTIME" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js"; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / åå管ç / éè´åå |
| | | è·¯ç±ï¼/pages/oa/ContractManage/purchase-contract/index |
| | | 说æï¼è·³è½¬è³éè´å°è´¦ /pages/procurementManagement/procurementLedger/index |
| | | --> |
| | | <template> |
| | | <view class="redirect-page" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - åå管ç - éè´ååï¼è·³è½¬éè´å°è´¦ï¼ */ |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | |
| | | onLoad(() => { |
| | | uni.redirectTo({ |
| | | url: "/pages/procurementManagement/procurementLedger/index", |
| | | }); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .redirect-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / åå管ç / éå®åå |
| | | è·¯ç±ï¼/pages/oa/ContractManage/sale-contract/index |
| | | 说æï¼è·³è½¬è³éå®å°è´¦ /pages/sales/salesAccount/index |
| | | --> |
| | | <template> |
| | | <view class="redirect-page" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - åå管ç - éå®ååï¼è·³è½¬éå®å°è´¦ï¼ */ |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | |
| | | onLoad(() => { |
| | | uni.redirectTo({ |
| | | url: "/pages/sales/salesAccount/index", |
| | | }); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .redirect-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / ä¼ä¸æ°é» |
| | | è·¯ç±ï¼/pages/oa/EnterpriseNews/news-manage/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - ä¼ä¸æ°é» */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "EnterpriseNews/news-manage"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / å²ä½ç®¡ç |
| | | è·¯ç±ï¼/pages/oa/HrManage/post-manage/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - å²ä½ç®¡ç */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/post-manage"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / 转æ£ç³è¯· |
| | | è·¯ç±ï¼/pages/oa/HrManage/regular-apply/index |
| | | --> |
| | | <template> |
| | | <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.REGULAR" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js"; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / 离èç³è¯· |
| | | è·¯ç±ï¼/pages/oa/HrManage/resign-apply/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - 离èç³è¯· */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/resign-apply"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / åå·¥æ¡£æ¡ |
| | | è·¯ç±ï¼/pages/oa/HrManage/staff-archive/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - åå·¥æ¡£æ¡ */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/staff-archive"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / åå·¥åå |
| | | è·¯ç±ï¼/pages/oa/HrManage/staff-contract/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - åå·¥åå */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/staff-contract"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / è°å²ç³è¯· |
| | | è·¯ç±ï¼/pages/oa/HrManage/transfer-apply/index |
| | | --> |
| | | <template> |
| | | <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.TRANSFER" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js"; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / å·¥ä½äº¤æ¥ |
| | | è·¯ç±ï¼/pages/oa/HrManage/work-handover/index |
| | | --> |
| | | <template> |
| | | <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.WORK_HANDOVER" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js"; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / å
¬åéç¥ |
| | | è·¯ç±ï¼/pages/oa/NoticeAnnouncement/notice-manage/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - å
¬åéç¥ */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "NoticeAnnouncement/notice-manage"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | æ¥éå®¡æ¹æµç¨ï¼å¯æç´¢é人ï¼ç¹éå³ç¡®è®¤ï¼ |
| | | --> |
| | | <template> |
| | | <view class="flow-wrap"> |
| | | <view v-for="(item, index) in innerList" |
| | | :key="item._uid" |
| | | class="flow-node-block"> |
| | | <view class="flow-node-card"> |
| | | <view class="node-header"> |
| | | <view class="node-level-badge">{{ index + 1 }}</view> |
| | | <text class="node-level-text">第{{ levelLabel(index + 1) }}级审æ¹</text> |
| | | <view v-if="innerList.length > 1" |
| | | class="node-delete" |
| | | @click="remove(index)"> |
| | | <up-icon name="trash" |
| | | size="16" |
| | | color="#f56c6c" /> |
| | | </view> |
| | | </view> |
| | | <view class="approver-row" |
| | | @click="openPicker(index)"> |
| | | <view class="approver-avatar" |
| | | :style="{ backgroundColor: avatarColor(item.approverName) }"> |
| | | {{ (item.approverName || '+').charAt(0) }} |
| | | </view> |
| | | <view class="approver-meta"> |
| | | <text class="approver-name">{{ item.approverName || 'ç¹å»éæ©å®¡æ¹äºº' }}</text> |
| | | <text class="approver-hint">æ¯ææç´¢å§åæå·¥å·</text> |
| | | </view> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#c0c4cc" /> |
| | | </view> |
| | | </view> |
| | | <view v-if="index < innerList.length - 1" |
| | | class="flow-connector"> |
| | | <view class="flow-connector-line" /> |
| | | </view> |
| | | </view> |
| | | <view class="add-node-bar" |
| | | @click="addNode"> |
| | | <up-icon name="plus-circle" |
| | | size="18" |
| | | color="#2979ff" /> |
| | | <text>æ·»å 审æ¹çº§æ¬¡</text> |
| | | </view> |
| | | |
| | | <OaUserSearchPicker v-model:show="pickerShow" |
| | | v-model="pickerUserId" |
| | | title="鿩审æ¹äºº" |
| | | :users="userOptions" |
| | | :show-self-quick="false" |
| | | @select="onUserSelected" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, watch } from "vue"; |
| | | import OaUserSearchPicker from "../../_components/OaUserSearchPicker.vue"; |
| | | import { userAvatarColor } from "../../_utils/userPickerUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Array, default: () => [] }, |
| | | userOptions: { type: Array, default: () => [] }, |
| | | }); |
| | | const emit = defineEmits(["update:modelValue"]); |
| | | |
| | | const innerList = ref([]); |
| | | const pickerShow = ref(false); |
| | | const pickerUserId = ref(""); |
| | | const editingIndex = ref(-1); |
| | | |
| | | function newUid() { |
| | | return `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; |
| | | } |
| | | |
| | | function levelLabel(n) { |
| | | const t = ["ä¸", "äº", "ä¸", "å", "äº", "å
", "ä¸", "å
«"]; |
| | | return t[n - 1] || String(n); |
| | | } |
| | | |
| | | function avatarColor(name) { |
| | | return userAvatarColor(name); |
| | | } |
| | | |
| | | function mapIn(rows) { |
| | | return (rows || []).map((n, i) => ({ |
| | | _uid: n._uid || newUid(), |
| | | nodeOrder: n.nodeOrder ?? i + 1, |
| | | signMode: n.signMode || "countersign", |
| | | approverId: n.approverId ?? "", |
| | | approverName: n.approverName || "", |
| | | id: n.id, |
| | | templateId: n.templateId, |
| | | })); |
| | | } |
| | | |
| | | function mapOut() { |
| | | return innerList.value.map((n, i) => ({ |
| | | nodeOrder: i + 1, |
| | | signMode: n.signMode || "countersign", |
| | | approverId: n.approverId, |
| | | approverName: n.approverName, |
| | | id: n.id, |
| | | templateId: n.templateId, |
| | | })); |
| | | } |
| | | |
| | | function syncEmit() { |
| | | emit("update:modelValue", mapOut()); |
| | | } |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | v => { |
| | | innerList.value = mapIn(v); |
| | | if (!innerList.value.length) { |
| | | innerList.value = [ |
| | | { _uid: newUid(), nodeOrder: 1, signMode: "countersign", approverId: "", approverName: "" }, |
| | | ]; |
| | | } |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ); |
| | | |
| | | function addNode() { |
| | | innerList.value.push({ |
| | | _uid: newUid(), |
| | | nodeOrder: innerList.value.length + 1, |
| | | signMode: "countersign", |
| | | approverId: "", |
| | | approverName: "", |
| | | }); |
| | | syncEmit(); |
| | | } |
| | | |
| | | function remove(index) { |
| | | if (innerList.value.length <= 1) { |
| | | uni.showToast({ title: "è³å°ä¿çä¸ä¸ªå®¡æ¹èç¹", icon: "none" }); |
| | | return; |
| | | } |
| | | innerList.value.splice(index, 1); |
| | | syncEmit(); |
| | | } |
| | | |
| | | function openPicker(index) { |
| | | editingIndex.value = index; |
| | | pickerUserId.value = innerList.value[index]?.approverId || ""; |
| | | pickerShow.value = true; |
| | | } |
| | | |
| | | function onUserSelected(u) { |
| | | const node = innerList.value[editingIndex.value]; |
| | | if (!node) return; |
| | | node.approverId = u.userId ?? u.id; |
| | | node.approverName = u.nickName || u.userName || ""; |
| | | syncEmit(); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .flow-node-card { |
| | | background: #f8f9fb; |
| | | border-radius: 10px; |
| | | padding: 12px; |
| | | border: 1px solid #eef0f3; |
| | | } |
| | | .node-header { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 10px; |
| | | } |
| | | .node-level-badge { |
| | | width: 22px; |
| | | height: 22px; |
| | | border-radius: 50%; |
| | | background: #2979ff; |
| | | color: #fff; |
| | | font-size: 12px; |
| | | text-align: center; |
| | | line-height: 22px; |
| | | margin-right: 8px; |
| | | } |
| | | .node-level-text { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | font-weight: 500; |
| | | } |
| | | .approver-row { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 12px; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | } |
| | | .approver-avatar { |
| | | width: 36px; |
| | | height: 36px; |
| | | border-radius: 50%; |
| | | color: #fff; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | .approver-meta { |
| | | flex: 1; |
| | | margin-left: 10px; |
| | | min-width: 0; |
| | | } |
| | | .approver-name { |
| | | display: block; |
| | | font-size: 15px; |
| | | color: #303133; |
| | | } |
| | | .approver-hint { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: #c0c4cc; |
| | | margin-top: 2px; |
| | | } |
| | | .flow-connector { |
| | | display: flex; |
| | | justify-content: center; |
| | | padding: 6px 0; |
| | | } |
| | | .flow-connector-line { |
| | | width: 2px; |
| | | height: 14px; |
| | | background: #dcdfe6; |
| | | } |
| | | .add-node-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | padding: 14px 0 4px; |
| | | color: #2979ff; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | æ¥éæç»åæ¡ç¼è¾ï¼åºé¨å¼¹å±ï¼ |
| | | --> |
| | | <template> |
| | | <up-popup :show="show" |
| | | mode="bottom" |
| | | round="16" |
| | | :safe-area-inset-bottom="true" |
| | | @close="close"> |
| | | <view class="detail-sheet"> |
| | | <view class="sheet-handle" /> |
| | | <view class="sheet-head"> |
| | | <text class="sheet-cancel" |
| | | @click="close">åæ¶</text> |
| | | <text class="sheet-title">{{ title }}</text> |
| | | <text class="sheet-confirm" |
| | | @click="confirm">ä¿å</text> |
| | | </view> |
| | | |
| | | <scroll-view scroll-y |
| | | class="sheet-body" |
| | | :show-scrollbar="false"> |
| | | <view class="sheet-group"> |
| | | <view class="sheet-cell sheet-cell--tap" |
| | | @click="showDatePicker = true"> |
| | | <text class="sheet-label required">åç¥¨æ¥æ</text> |
| | | <view class="sheet-value-wrap"> |
| | | <text class="sheet-value" |
| | | :class="{ placeholder: !draft.invoiceDate }"> |
| | | {{ draft.invoiceDate || 'è¯·éæ©' }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#c0c4cc" /> |
| | | </view> |
| | | </view> |
| | | <view class="sheet-cell sheet-cell--tap" |
| | | @click="showSubjectSheet = true"> |
| | | <text class="sheet-label required">è´¹ç¨ç§ç®</text> |
| | | <view class="sheet-value-wrap"> |
| | | <text class="sheet-value" |
| | | :class="{ placeholder: !draft.expenseSubject }"> |
| | | {{ subjectText }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#c0c4cc" /> |
| | | </view> |
| | | </view> |
| | | <view class="sheet-cell"> |
| | | <text class="sheet-label required">éé¢</text> |
| | | <view class="sheet-input-wrap"> |
| | | <up-input v-model="draft.amount" |
| | | type="digit" |
| | | placeholder="0.00" |
| | | border="none" |
| | | input-align="right" /> |
| | | <text class="sheet-unit">å
</text> |
| | | </view> |
| | | </view> |
| | | <view class="sheet-cell sheet-cell--col"> |
| | | <text class="sheet-label">æè¿°</text> |
| | | <view class="sheet-textarea-wrap"> |
| | | <up-textarea v-model="draft.description" |
| | | placeholder="è´¹ç¨è¯´æï¼éå¡«ï¼" |
| | | maxlength="200" |
| | | border="none" |
| | | height="64" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="showDelete" |
| | | class="sheet-delete" |
| | | @click="emit('delete')"> |
| | | å 餿¬æ¡æç» |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | |
| | | <up-action-sheet :show="showSubjectSheet" |
| | | title="è´¹ç¨ç§ç®" |
| | | :actions="subjectActions" |
| | | @select="onSubjectSelect" |
| | | @close="showSubjectSheet = false" /> |
| | | |
| | | <up-popup :show="showDatePicker" |
| | | mode="bottom" |
| | | round="16" |
| | | @close="showDatePicker = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="datePickerTs" |
| | | mode="date" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" /> |
| | | </up-popup> |
| | | </up-popup> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import { expenseSubjectLabel as costSubjectLabel } from "../_utils/costReimburseUtils.js"; |
| | | import { expenseSubjectLabel as travelSubjectLabel } from "../_utils/travelReimburseUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | show: { type: Boolean, default: false }, |
| | | modelValue: { type: Object, default: () => ({}) }, |
| | | index: { type: Number, default: 0 }, |
| | | isTravel: { type: Boolean, default: true }, |
| | | subjectOptions: { type: Array, default: () => [] }, |
| | | showDelete: { type: Boolean, default: true }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:show", "update:modelValue", "confirm", "delete"]); |
| | | |
| | | const draft = reactive({ |
| | | invoiceDate: "", |
| | | expenseSubject: "", |
| | | amount: "", |
| | | description: "", |
| | | }); |
| | | |
| | | const showDatePicker = ref(false); |
| | | const showSubjectSheet = ref(false); |
| | | const datePickerTs = ref(Date.now()); |
| | | |
| | | const title = computed(() => `æç» ${props.index + 1}`); |
| | | |
| | | const subjectActions = computed(() => |
| | | (props.subjectOptions || []).map(x => ({ name: x.label, value: x.value })) |
| | | ); |
| | | |
| | | const subjectText = computed(() => resolveSubjectLabel(draft.expenseSubject)); |
| | | |
| | | function resolveSubjectLabel(v) { |
| | | if (!v) return "è¯·éæ©"; |
| | | const labelFn = props.isTravel ? travelSubjectLabel : costSubjectLabel; |
| | | const t = labelFn(v); |
| | | if (t && t !== "â") return t; |
| | | const hit = (props.subjectOptions || []).find(x => x.value === v || x.label === v); |
| | | return hit?.label || v; |
| | | } |
| | | |
| | | watch( |
| | | () => props.show, |
| | | v => { |
| | | if (v && props.modelValue) { |
| | | Object.assign(draft, { |
| | | invoiceDate: "", |
| | | expenseSubject: "", |
| | | amount: "", |
| | | description: "", |
| | | ...JSON.parse(JSON.stringify(props.modelValue)), |
| | | }); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | function close() { |
| | | emit("update:show", false); |
| | | } |
| | | |
| | | function confirm() { |
| | | if (!draft.invoiceDate) { |
| | | uni.showToast({ title: "è¯·éæ©åç¥¨æ¥æ", icon: "none" }); |
| | | return; |
| | | } |
| | | if (!draft.expenseSubject) { |
| | | uni.showToast({ title: "è¯·éæ©è´¹ç¨ç§ç®", icon: "none" }); |
| | | return; |
| | | } |
| | | if (draft.amount === "" || draft.amount == null) { |
| | | uni.showToast({ title: "请填åéé¢", icon: "none" }); |
| | | return; |
| | | } |
| | | emit("update:modelValue", { ...draft }); |
| | | emit("confirm", { ...draft }); |
| | | emit("update:show", false); |
| | | } |
| | | |
| | | function onSubjectSelect(action) { |
| | | draft.expenseSubject = action.value; |
| | | showSubjectSheet.value = false; |
| | | } |
| | | |
| | | function onDateConfirm(e) { |
| | | const ts = e?.value ?? datePickerTs.value; |
| | | draft.invoiceDate = parseTime(ts, "{y}-{m}-{d}"); |
| | | showDatePicker.value = false; |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .detail-sheet { |
| | | background: #fff; |
| | | border-radius: 16px 16px 0 0; |
| | | max-height: 85vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | .sheet-handle { |
| | | width: 36px; |
| | | height: 4px; |
| | | background: #e4e7ed; |
| | | border-radius: 2px; |
| | | margin: 8px auto 4px; |
| | | } |
| | | .sheet-head { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 8px 16px 12px; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | } |
| | | .sheet-cancel { |
| | | font-size: 15px; |
| | | color: #909399; |
| | | min-width: 48px; |
| | | } |
| | | .sheet-title { |
| | | flex: 1; |
| | | text-align: center; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | .sheet-confirm { |
| | | font-size: 15px; |
| | | color: #2979ff; |
| | | font-weight: 600; |
| | | min-width: 48px; |
| | | text-align: right; |
| | | } |
| | | .sheet-body { |
| | | max-height: 70vh; |
| | | padding-bottom: env(safe-area-inset-bottom); |
| | | } |
| | | .sheet-group { |
| | | margin: 12px 16px; |
| | | background: #f8f9fb; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | .sheet-cell { |
| | | display: flex; |
| | | align-items: center; |
| | | min-height: 52px; |
| | | padding: 12px 14px; |
| | | background: #fff; |
| | | border-bottom: 1px solid #f5f6f8; |
| | | &--col { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | &--tap:active { |
| | | background: #fafbfc; |
| | | } |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | .sheet-label { |
| | | width: 80px; |
| | | font-size: 15px; |
| | | color: #303133; |
| | | flex-shrink: 0; |
| | | &.required::before { |
| | | content: "*"; |
| | | color: #f56c6c; |
| | | margin-right: 2px; |
| | | } |
| | | } |
| | | .sheet-value-wrap { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | gap: 4px; |
| | | } |
| | | .sheet-value { |
| | | font-size: 15px; |
| | | color: #303133; |
| | | &.placeholder { |
| | | color: #c0c4cc; |
| | | } |
| | | } |
| | | .sheet-input-wrap { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | } |
| | | .sheet-unit { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-left: 4px; |
| | | } |
| | | .sheet-textarea-wrap { |
| | | width: 100%; |
| | | margin-top: 8px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 4px 8px; |
| | | } |
| | | .sheet-delete { |
| | | margin: 16px; |
| | | text-align: center; |
| | | font-size: 15px; |
| | | color: #f56c6c; |
| | | padding: 14px; |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | border: 1px solid #fde2e2; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | å·®æ
/è´¹ç¨æ¥é详æ
å±ç¤ºï¼å表详æ
/ 审æ¹è¯¦æ
å
±ç¨ï¼ |
| | | --> |
| | | <template> |
| | | <view class="rd-body"> |
| | | <!-- æ¦è¦ --> |
| | | <view class="rd-hero"> |
| | | <view class="rd-hero-top"> |
| | | <text class="rd-bill-no">{{ billNo }}</text> |
| | | <text :class="['rd-status', statusCssClass]">{{ statusText }}</text> |
| | | </view> |
| | | <text class="rd-reason">{{ reasonText }}</text> |
| | | <view class="rd-amount-row"> |
| | | <text class="rd-amount-label">ç³è¯·éé¢</text> |
| | | <text class="rd-amount">{{ amountText }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ç³è¯·äºº --> |
| | | <view class="rd-section"> |
| | | <view class="rd-section-hd"> |
| | | <text class="rd-section-title">ç³è¯·äºº</text> |
| | | </view> |
| | | <view class="rd-group"> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">å§å</text> |
| | | <text class="rd-value">{{ r.applicantName || "â" }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">åå·¥ç¼å·</text> |
| | | <text class="rd-value">{{ r.applicantCode || r.applicantNo || "â" }}</text> |
| | | </view> |
| | | <view v-if="r.applicantDeptName || r.deptName" |
| | | class="rd-cell"> |
| | | <text class="rd-label">é¨é¨</text> |
| | | <text class="rd-value">{{ r.applicantDeptName || r.deptName }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- åºå·® / è´¹ç¨ --> |
| | | <view class="rd-section"> |
| | | <view class="rd-section-hd"> |
| | | <text class="rd-section-title">{{ isTravel ? "åºå·®ä¿¡æ¯" : "è´¹ç¨ä¿¡æ¯" }}</text> |
| | | </view> |
| | | <view class="rd-group"> |
| | | <template v-if="isTravel"> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">åºå·®å¼å§</text> |
| | | <text class="rd-value">{{ formatTime(r.travelStartTime) }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">åºå·®ç»æ</text> |
| | | <text class="rd-value">{{ formatTime(r.travelEndTime) }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">åºå·®å¤©æ°</text> |
| | | <text class="rd-value">{{ travelDaysText }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">åºå·®å°</text> |
| | | <text class="rd-value">{{ r.departurePlace || "â" }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">ç®çå°</text> |
| | | <text class="rd-value">{{ r.destination || "â" }}</text> |
| | | </view> |
| | | </template> |
| | | <view v-else |
| | | class="rd-cell"> |
| | | <text class="rd-label">è´¹ç¨ç±»å</text> |
| | | <text class="rd-value">{{ expenseTypeText }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å·®æ
æ å --> |
| | | <view v-if="isTravel && hasTravelStandard" |
| | | class="rd-section"> |
| | | <view class="rd-section-hd"> |
| | | <text class="rd-section-title">å·®æ
æ å</text> |
| | | </view> |
| | | <view class="rd-group"> |
| | | <view v-if="r.hotelStandard != null" |
| | | class="rd-cell"> |
| | | <text class="rd-label">é
åºæ å</text> |
| | | <text class="rd-value">{{ r.hotelStandard }} å
/æ</text> |
| | | </view> |
| | | <view v-if="r.hotelDays != null" |
| | | class="rd-cell"> |
| | | <text class="rd-label">ä½å®¿å¤©æ°</text> |
| | | <text class="rd-value">{{ r.hotelDays }} 天</text> |
| | | </view> |
| | | <view v-if="r.livingSubsidy != null" |
| | | class="rd-cell"> |
| | | <text class="rd-label">çæ´»è¡¥è´´</text> |
| | | <text class="rd-value">{{ r.livingSubsidy }} å
</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">æ åæ è®°</text> |
| | | <text class="rd-value">{{ r.standardTag || (r.needSpecialApproval ? "è¶
æ¯éç¹æ¹" : "卿 åå
") }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æ¶æ¬¾ --> |
| | | <view class="rd-section"> |
| | | <view class="rd-section-hd"> |
| | | <text class="rd-section-title">æ¶æ¬¾ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="rd-group"> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">æ¶æ¬¾äºº</text> |
| | | <text class="rd-value">{{ r.payeeName || r.payee || "â" }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">æ¶æ¬¾è´¦å·</text> |
| | | <text class="rd-value">{{ r.payeeAccount || "â" }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">弿·æ¯è¡</text> |
| | | <text class="rd-value">{{ r.payeeBank || r.bankBranch || "â" }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æ¥éæç» --> |
| | | <view class="rd-section"> |
| | | <view class="rd-section-hd"> |
| | | <text class="rd-section-title">æ¥éæç»</text> |
| | | <text class="rd-section-count">å
± {{ detailRows.length }} æ¡</text> |
| | | </view> |
| | | <view v-if="detailRows.length" |
| | | class="rd-group"> |
| | | <view v-for="(d, idx) in detailRows" |
| | | :key="'d-' + idx" |
| | | class="rd-detail-item"> |
| | | <view class="rd-detail-head"> |
| | | <text class="rd-detail-badge">{{ idx + 1 }}</text> |
| | | <text class="rd-detail-title">{{ detailSubject(d) }}</text> |
| | | <text class="rd-detail-amount">{{ detailAmount(d) }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">åç¥¨æ¥æ</text> |
| | | <text class="rd-value">{{ d.invoiceDate || "â" }}</text> |
| | | </view> |
| | | <view v-if="d.description" |
| | | class="rd-cell"> |
| | | <text class="rd-label">æè¿°</text> |
| | | <text class="rd-value">{{ d.description }}</text> |
| | | </view> |
| | | <view v-if="d.invoiceNo" |
| | | class="rd-cell"> |
| | | <text class="rd-label">å票å·</text> |
| | | <text class="rd-value">{{ d.invoiceNo }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="rd-group"> |
| | | <view class="rd-empty">ææ æ¥éæç»</view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- éä»¶ --> |
| | | <view v-if="attachmentList.length" |
| | | class="rd-section"> |
| | | <view class="rd-section-hd"> |
| | | <text class="rd-section-title">å票éä»¶</text> |
| | | </view> |
| | | <view class="rd-group"> |
| | | <view v-for="(f, i) in attachmentList" |
| | | :key="i" |
| | | class="rd-attach" |
| | | @click="openAttachment(f)"> |
| | | {{ f.name || "éä»¶" }} |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å®¡æ¹æµç¨ï¼tasksï¼ --> |
| | | <view class="rd-section"> |
| | | <view class="rd-section-hd"> |
| | | <text class="rd-section-title">å®¡æ¹æµç¨</text> |
| | | <text class="rd-section-count">{{ flowNodesList.length }} 级</text> |
| | | </view> |
| | | <view v-if="flowNodesList.length" |
| | | class="rd-group"> |
| | | <view v-for="(node, nodeIndex) in flowNodesList" |
| | | :key="nodeIndex" |
| | | class="rd-flow-node"> |
| | | <view class="rd-flow-line"> |
| | | <view class="rd-flow-dot" /> |
| | | <view v-if="nodeIndex < flowNodesList.length - 1" |
| | | class="rd-flow-bar" /> |
| | | </view> |
| | | <view class="rd-flow-body"> |
| | | <text class="rd-flow-level">第{{ node.levelNo }}级 · {{ node.approveType === 'OR' ? 'æç¾' : 'ä¼ç¾' }}</text> |
| | | <view v-for="(a, ai) in node.approvers" |
| | | :key="ai" |
| | | class="rd-flow-approver"> |
| | | <view class="rd-flow-avatar" |
| | | :style="{ backgroundColor: avatarColor(a.approverName) }"> |
| | | {{ (a.approverName || "?").charAt(0) }} |
| | | </view> |
| | | <view class="rd-flow-approver-meta"> |
| | | <text class="rd-flow-name">{{ a.approverName || "â" }}</text> |
| | | <text v-if="a.taskStatus" |
| | | class="rd-flow-status">{{ taskStatusLabel(a.taskStatus) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="rd-group"> |
| | | <view class="rd-empty">ææ å®¡æ¹èç¹</view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 审æ¹è®°å½ï¼tasks ççï¼ --> |
| | | <view class="rd-section"> |
| | | <view class="rd-section-hd"> |
| | | <text class="rd-section-title">审æ¹è®°å½</text> |
| | | <text class="rd-section-count">{{ approvalRecords.length }} æ¡</text> |
| | | </view> |
| | | <view v-if="approvalRecords.length" |
| | | class="rd-group"> |
| | | <view v-for="(rec, index) in approvalRecords" |
| | | :key="rec.id ?? index" |
| | | class="rd-record-item"> |
| | | <view class="rd-record-head"> |
| | | <text class="rd-record-operator">{{ rec.operatorName }}</text> |
| | | <text class="rd-record-tag" |
| | | :class="'rd-record-tag--' + rec.result">{{ recordLabel(rec.result) }}</text> |
| | | </view> |
| | | <text v-if="rec.time" |
| | | class="rd-record-time">{{ rec.time }}</text> |
| | | <text class="rd-record-opinion">{{ rec.opinion || "æ æè§" }}</text> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="rd-group"> |
| | | <view class="rd-empty">ææ å®¡æ¹è®°å½</view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="rd-section"> |
| | | <view class="rd-group"> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">å建æ¶é´</text> |
| | | <text class="rd-value">{{ formatTime(r.createTime) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import { isTravelReimbursementType } from "../../_utils/finReimbursementMappers.js"; |
| | | import { |
| | | billStatusCssClass, |
| | | billStatusLabel, |
| | | } from "../../_utils/finReimbursementMappers.js"; |
| | | import { expenseCategoryLabel, EXPENSE_SUBJECT_OPTIONS as COST_SUBJECTS } from "../_utils/costReimburseUtils.js"; |
| | | import { EXPENSE_SUBJECT_OPTIONS as TRAVEL_SUBJECTS } from "../_utils/travelReimburseUtils.js"; |
| | | import { |
| | | resolveExpenseSubjectLabel, |
| | | formatDetailAmount, |
| | | } from "../_utils/expenseDetailDisplay.js"; |
| | | import { userAvatarColor } from "../../_utils/userPickerUtils.js"; |
| | | import { |
| | | mapTasksToFlowNodes, |
| | | recordActionLabel, |
| | | taskStatusText, |
| | | } from "../../_utils/approveListUtils.js"; |
| | | import config from "@/config.js"; |
| | | |
| | | const props = defineProps({ |
| | | reimburseRow: { type: Object, default: () => ({}) }, |
| | | moduleKey: { type: String, default: "" }, |
| | | }); |
| | | |
| | | const r = computed(() => props.reimburseRow || {}); |
| | | |
| | | const isTravel = computed(() => |
| | | isTravelReimbursementType(r.value.reimbursementType ?? props.moduleKey) |
| | | ); |
| | | |
| | | const billNo = computed(() => r.value.billNo || r.value.reimburseNo || "â"); |
| | | const statusText = computed(() => |
| | | billStatusLabel(r.value.billStatus ?? r.value.status) |
| | | ); |
| | | const statusCssClass = computed(() => |
| | | billStatusCssClass(r.value) |
| | | ); |
| | | const reasonText = computed( |
| | | () => r.value.reason || r.value.reimburseReason || "â" |
| | | ); |
| | | const amountText = computed(() => |
| | | r.value.applyAmount != null ? String(r.value.applyAmount) : "â" |
| | | ); |
| | | |
| | | const expenseTypeText = computed(() => |
| | | expenseCategoryLabel(r.value.expenseCategory) || r.value.expenseType || "â" |
| | | ); |
| | | |
| | | const travelDaysText = computed(() => { |
| | | const d = r.value.travelDays ?? r.value.travel?.travelDays; |
| | | return d != null ? `${d} 天` : "â"; |
| | | }); |
| | | |
| | | const hasTravelStandard = computed(() => { |
| | | const row = r.value; |
| | | return ( |
| | | row.hotelStandard != null || |
| | | row.hotelDays != null || |
| | | row.livingSubsidy != null || |
| | | row.standardTag || |
| | | row.needSpecialApproval |
| | | ); |
| | | }); |
| | | |
| | | const subjectOptions = computed(() => |
| | | isTravel.value ? TRAVEL_SUBJECTS : COST_SUBJECTS |
| | | ); |
| | | |
| | | const detailRows = computed(() => { |
| | | const list = r.value.expenseDetails || r.value.details || []; |
| | | return Array.isArray(list) ? list : []; |
| | | }); |
| | | |
| | | const attachmentList = computed(() => { |
| | | const list = |
| | | r.value.attachmentList || |
| | | r.value.storageBlobVOList || |
| | | r.value.invoiceAttachments || |
| | | []; |
| | | return Array.isArray(list) ? list : []; |
| | | }); |
| | | |
| | | const approvalRecords = computed(() => { |
| | | const list = r.value.approvalRecords || []; |
| | | return Array.isArray(list) ? list : []; |
| | | }); |
| | | |
| | | /** æµç¨å±ç¤ºä¼å
ç¨ enrichment åç flowNodesï¼æ¥èª tasksï¼ */ |
| | | const flowNodesList = computed(() => { |
| | | const row = r.value; |
| | | if (Array.isArray(row.flowNodes) && row.flowNodes.length) { |
| | | return row.flowNodes; |
| | | } |
| | | if (Array.isArray(row.tasks) && row.tasks.length) { |
| | | return mapTasksToFlowNodes(row.tasks); |
| | | } |
| | | return []; |
| | | }); |
| | | |
| | | function taskStatusLabel(status) { |
| | | return taskStatusText(status); |
| | | } |
| | | |
| | | function recordLabel(result) { |
| | | return recordActionLabel(result); |
| | | } |
| | | |
| | | function formatTime(t) { |
| | | if (!t) return "â"; |
| | | const s = parseTime(t, "{y}-{m}-{d} {h}:{i}"); |
| | | return s || String(t).replace("T", " ").slice(0, 16); |
| | | } |
| | | |
| | | function detailSubject(d) { |
| | | return ( |
| | | resolveExpenseSubjectLabel(d.expenseSubject || d.expenseCategory, { |
| | | isTravel: isTravel.value, |
| | | subjectOptions: subjectOptions.value, |
| | | }) || "æªéç§ç®" |
| | | ); |
| | | } |
| | | |
| | | function detailAmount(d) { |
| | | return formatDetailAmount(d.amount) || "â"; |
| | | } |
| | | |
| | | function avatarColor(name) { |
| | | return userAvatarColor(name); |
| | | } |
| | | |
| | | function resolveFileUrl(f) { |
| | | let url = f?.url || f?.downloadURL || f?.previewURL || f?.fileUrl || ""; |
| | | if (!url) return ""; |
| | | if (/^https?:\/\//i.test(url)) return url; |
| | | const base = (config.baseUrl || "").replace(/\/+$/, ""); |
| | | const path = url.startsWith("/") ? url : `/${url}`; |
| | | return `${base}${path}`; |
| | | } |
| | | |
| | | function openAttachment(f) { |
| | | const url = resolveFileUrl(f); |
| | | if (!url) { |
| | | uni.showToast({ title: "æ æ³æå¼éä»¶", icon: "none" }); |
| | | return; |
| | | } |
| | | // #ifdef H5 |
| | | window.open(url, "_blank"); |
| | | // #endif |
| | | // #ifndef H5 |
| | | uni.downloadFile({ |
| | | url, |
| | | success: res => { |
| | | if (res.statusCode === 200) { |
| | | uni.openDocument({ filePath: res.tempFilePath, showMenu: true }); |
| | | } |
| | | }, |
| | | fail: () => uni.showToast({ title: "éä»¶æå¼å¤±è´¥", icon: "none" }), |
| | | }); |
| | | // #endif |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "../reimburse-detail/reimburse-detail.scss"; |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | |
| | | export const EXPENSE_CATEGORY_OPTIONS = [ |
| | | { label: "å·®æ
", value: "travel" }, |
| | | { label: "åå
¬éè´", value: "office_procurement" }, |
| | | { label: "ä¸å¡æå¾
", value: "business_entertainment" }, |
| | | { label: "交éè´¹", value: "transport" }, |
| | | { label: "é讯费", value: "communication" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ]; |
| | | |
| | | export const EXPENSE_SUBJECT_OPTIONS = [ |
| | | { label: "交éè´¹", value: "transport" }, |
| | | { label: "ä½å®¿è´¹", value: "hotel" }, |
| | | { label: "é¤é¥®è´¹", value: "meal" }, |
| | | { label: "åå
¬ç¨å", value: "office_supply" }, |
| | | { label: "æå¾
è´¹", value: "entertainment" }, |
| | | { label: "é讯费", value: "phone" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ]; |
| | | |
| | | export const CATEGORY_TEMPLATES = { |
| | | travel: { |
| | | label: "å·®æ
è´¹ç¨", |
| | | reason: "å å
¬åºå·®äº§çç交éãä½å®¿ãé¤é¥®çè´¹ç¨æ¥éã", |
| | | details: [ |
| | | { expenseSubject: "transport", description: "å¾è¿äº¤éè´¹" }, |
| | | { expenseSubject: "hotel", description: "ä½å®¿è´¹" }, |
| | | { expenseSubject: "meal", description: "åºå·®é¤é¥®" }, |
| | | ], |
| | | }, |
| | | office_procurement: { |
| | | label: "åå
¬éè´", |
| | | reason: "é¨é¨æ¥å¸¸åå
¬ç¨åãèæéè´æ¥éã", |
| | | details: [ |
| | | { expenseSubject: "office_supply", description: "åå
¬ç¨åéè´" }, |
| | | { expenseSubject: "office_supply", description: "æå°èæ" }, |
| | | ], |
| | | }, |
| | | business_entertainment: { |
| | | label: "ä¸å¡æå¾
", |
| | | reason: "å®¢æ·æ¥å¾
ãåå¡å®´è¯·çè´¹ç¨æ¥éã", |
| | | details: [ |
| | | { expenseSubject: "entertainment", description: "å®¢æ·æ¥å¾
é¤è´¹" }, |
| | | { expenseSubject: "entertainment", description: "åå¡ç¤¼å" }, |
| | | ], |
| | | }, |
| | | transport: { |
| | | label: "交éè´¹", |
| | | reason: "å¸å
éå¤ãæè½¦ãå车ç交éè´¹ç¨æ¥éã", |
| | | details: [{ expenseSubject: "transport", description: "å¸å
交é" }], |
| | | }, |
| | | communication: { |
| | | label: "é讯费", |
| | | reason: "å å
¬éè®¯ãæµéãè¯è´¹è¡¥è´´æ¥éã", |
| | | details: [{ expenseSubject: "phone", description: "è¯è´¹/æµé" }], |
| | | }, |
| | | other: { |
| | | label: "å
¶ä»è´¹ç¨", |
| | | reason: "å
¶ä»å å
¬æ¯åºè´¹ç¨æ¥éã", |
| | | details: [{ expenseSubject: "other", description: "å
¶ä»è´¹ç¨" }], |
| | | }, |
| | | }; |
| | | |
| | | export function expenseSubjectLabel(v) { |
| | | return EXPENSE_SUBJECT_OPTIONS.find(x => x.value === v)?.label || "â"; |
| | | } |
| | | |
| | | export function expenseCategoryLabel(v) { |
| | | return EXPENSE_CATEGORY_OPTIONS.find(x => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function expenseTypeToCategory(expenseType) { |
| | | const t = (expenseType || "").trim(); |
| | | const hit = EXPENSE_CATEGORY_OPTIONS.find(x => x.label === t || x.value === t); |
| | | return hit?.value || "other"; |
| | | } |
| | | |
| | | export function createEmptyExpenseDetail() { |
| | | return { |
| | | id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, |
| | | invoiceDate: "", |
| | | expenseSubject: "", |
| | | amount: "", |
| | | description: "", |
| | | }; |
| | | } |
| | | |
| | | export function createEmptyCostForm() { |
| | | return { |
| | | reimbursementId: undefined, |
| | | applicantId: "", |
| | | employeeNo: "", |
| | | employeeName: "", |
| | | expenseCategory: "other", |
| | | reimburseReason: "", |
| | | applyAmount: "", |
| | | payee: "", |
| | | payeeAccount: "", |
| | | bankBranch: "", |
| | | expenseDetails: [], |
| | | attachmentList: [], |
| | | approvalFlowNodes: [{ approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" }], |
| | | deptId: "", |
| | | deptName: "", |
| | | }; |
| | | } |
| | | |
| | | export function applyCategoryTemplate(form, category) { |
| | | const tpl = CATEGORY_TEMPLATES[category]; |
| | | if (!tpl) return; |
| | | form.expenseCategory = category; |
| | | if (!form.reimburseReason?.trim()) form.reimburseReason = tpl.reason; |
| | | form.expenseDetails = (tpl.details || []).map(d => ({ |
| | | ...createEmptyExpenseDetail(), |
| | | expenseSubject: d.expenseSubject, |
| | | description: d.description, |
| | | invoiceDate: dayjs().format("YYYY-MM-DD"), |
| | | })); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { expenseSubjectLabel as costSubjectLabel } from "./costReimburseUtils.js"; |
| | | import { expenseSubjectLabel as travelSubjectLabel } from "./travelReimburseUtils.js"; |
| | | |
| | | /** è´¹ç¨ç§ç®å±ç¤ºï¼å
¼å®¹ value / 䏿 label / API expenseCategoryï¼ */ |
| | | export function resolveExpenseSubjectLabel(v, { isTravel = true, subjectOptions = [] } = {}) { |
| | | if (!v) return ""; |
| | | const labelFn = isTravel ? travelSubjectLabel : costSubjectLabel; |
| | | const t = labelFn(v); |
| | | if (t && t !== "â") return t; |
| | | const hit = subjectOptions.find(x => x.value === v || x.label === v); |
| | | return hit?.label || String(v); |
| | | } |
| | | |
| | | export function formatDetailAmount(amount) { |
| | | if (amount === "" || amount == null) return null; |
| | | const n = Number(amount); |
| | | if (Number.isNaN(n)) return String(amount); |
| | | return `${n} å
`; |
| | | } |
| | | |
| | | /** åè¡¨è¡æè¦ */ |
| | | export function buildExpenseDetailSummary(row, opts = {}) { |
| | | const subject = resolveExpenseSubjectLabel(row?.expenseSubject, opts) || "æªéç§ç®"; |
| | | const amount = formatDetailAmount(row?.amount); |
| | | const date = row?.invoiceDate || ""; |
| | | const desc = (row?.description || "").trim(); |
| | | const parts = []; |
| | | if (date) parts.push(date); |
| | | if (desc) parts.push(desc); |
| | | const sub = parts.length ? parts.join(" · ") : "ç¹å»è¯¦æ
å®åä¿¡æ¯"; |
| | | const incomplete = !row?.invoiceDate || !row?.expenseSubject || row?.amount === "" || row?.amount == null; |
| | | return { subject, amount: amount || "é颿ªå¡«", sub, incomplete }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import { |
| | | mapApprovalRecords, |
| | | mapRecordResult, |
| | | mapTasksToFlowNodes, |
| | | } from "../../_utils/approveListUtils.js"; |
| | | |
| | | function formatDisplayTime(val) { |
| | | if (!val) return ""; |
| | | const s = parseTime(val, "{y}-{m}-{d} {h}:{i}"); |
| | | return s || String(val).replace("T", " ").slice(0, 16); |
| | | } |
| | | |
| | | function taskStatusToNodeStatus(taskStatus) { |
| | | const s = String(taskStatus ?? "").toUpperCase(); |
| | | if (["APPROVED", "COMPLETED", "FINISHED", "PASSED", "AGREE"].includes(s)) { |
| | | return "finish"; |
| | | } |
| | | if (["REJECTED", "REJECT", "REFUSE", "REFUSED"].includes(s)) { |
| | | return "error"; |
| | | } |
| | | if (["PENDING", "IN_APPROVAL", "PROCESS", "PROCESSING"].includes(s)) { |
| | | return "process"; |
| | | } |
| | | return "wait"; |
| | | } |
| | | |
| | | /** storageBlobVOList â 页é¢éä»¶ */ |
| | | export function mapReimbursementAttachments(source = {}) { |
| | | const list = |
| | | source.storageBlobVOList || |
| | | source.storageBlobDTOs || |
| | | source.storageBlobDTOS || |
| | | source.attachmentList || |
| | | source.invoiceAttachments || |
| | | []; |
| | | if (!Array.isArray(list)) return []; |
| | | return list.map((b, i) => ({ |
| | | ...b, |
| | | id: b.id ?? b.blobId ?? `att_${i}`, |
| | | name: |
| | | b.fileName || |
| | | b.originalFilename || |
| | | b.originalFileName || |
| | | b.blobName || |
| | | b.name || |
| | | "éä»¶", |
| | | url: |
| | | b.url || |
| | | b.fileUrl || |
| | | b.downloadUrl || |
| | | b.downloadURL || |
| | | b.previewUrl || |
| | | b.previewURL || |
| | | b.link || |
| | | "", |
| | | })); |
| | | } |
| | | |
| | | /** 审æ¹è®°å½å¨ tasks */ |
| | | export function mapTasksToApprovalRecords(tasks) { |
| | | const list = Array.isArray(tasks) ? tasks : []; |
| | | return list |
| | | .map((t, index) => ({ |
| | | id: t.id ?? index, |
| | | operatorName: t.approverName || t.operatorName || t.createUserName || "â", |
| | | result: mapRecordResult(t.approveAction ?? t.taskStatus ?? t.status), |
| | | opinion: t.approveComment || t.comment || t.opinion || "", |
| | | time: formatDisplayTime( |
| | | t.approveTime || t.finishTime || t.updateTime || t.createTime || "" |
| | | ), |
| | | levelNo: t.levelNo ?? t.taskLevel, |
| | | })) |
| | | .sort((a, b) => { |
| | | const la = Number(a.levelNo ?? 0); |
| | | const lb = Number(b.levelNo ?? 0); |
| | | if (la !== lb) return la - lb; |
| | | return String(a.time).localeCompare(String(b.time)); |
| | | }); |
| | | } |
| | | |
| | | export function mapTasksToApprovalFlowNodes(tasks) { |
| | | const grouped = mapTasksToFlowNodes(tasks); |
| | | return grouped.map((node, i) => { |
| | | const approvers = node.approvers || []; |
| | | const statuses = approvers.map(a => |
| | | taskStatusToNodeStatus(a.taskStatus ?? a.status) |
| | | ); |
| | | let nodeStatus = "wait"; |
| | | if (statuses.includes("error")) nodeStatus = "error"; |
| | | else if (statuses.length && statuses.every(s => s === "finish")) { |
| | | nodeStatus = "finish"; |
| | | } else if (statuses.includes("process")) nodeStatus = "process"; |
| | | |
| | | const names = approvers.map(a => a.approverName).filter(Boolean).join("ã"); |
| | | const opinions = approvers |
| | | .map(a => a.approveComment) |
| | | .filter(Boolean) |
| | | .join("ï¼"); |
| | | |
| | | return { |
| | | nodeOrder: node.levelNo ?? i + 1, |
| | | levelNo: node.levelNo ?? i + 1, |
| | | approveType: node.approveType || "AND", |
| | | approveTypeLabel: node.approveType === "OR" ? "æç¾" : "ä¼ç¾", |
| | | approvers, |
| | | approverName: names || "â", |
| | | approveOpinion: opinions, |
| | | nodeStatus, |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | export function computeApprovalFlowCurrentIndex(approvalFlowNodes = []) { |
| | | const list = approvalFlowNodes || []; |
| | | const processing = list.findIndex(n => n.nodeStatus === "process"); |
| | | if (processing >= 0) return processing; |
| | | const errorIdx = list.findIndex(n => n.nodeStatus === "error"); |
| | | if (errorIdx >= 0) return errorIdx; |
| | | return list.filter(n => n.nodeStatus === "finish").length; |
| | | } |
| | | |
| | | export function applyFinReimbursementDetailEnrichment(mapped, raw = {}) { |
| | | if (!mapped || typeof mapped !== "object") return mapped; |
| | | const source = { ...raw, ...mapped }; |
| | | const tasks = Array.isArray(source.tasks) ? source.tasks : []; |
| | | const attachments = mapReimbursementAttachments(source); |
| | | const approvalRecords = tasks.length |
| | | ? mapTasksToApprovalRecords(tasks) |
| | | : mapApprovalRecords(source.records || source.approvalRecords); |
| | | const approvalFlowNodes = Array.isArray(mapped.approvalFlowNodes) |
| | | ? mapped.approvalFlowNodes |
| | | : []; |
| | | const approvalFlowProgressNodes = tasks.length |
| | | ? mapTasksToApprovalFlowNodes(tasks) |
| | | : approvalFlowNodes; |
| | | const flowNodes = tasks.length |
| | | ? mapTasksToFlowNodes(tasks) |
| | | : mapped.flowNodes || mapped.nodes || []; |
| | | |
| | | return { |
| | | ...mapped, |
| | | tasks, |
| | | storageBlobVOList: attachments, |
| | | attachmentList: attachments, |
| | | invoiceAttachments: attachments, |
| | | approvalRecords, |
| | | approvalFlowNodes, |
| | | approvalFlowProgressNodes, |
| | | currentNodeIndex: computeApprovalFlowCurrentIndex( |
| | | approvalFlowProgressNodes.length ? approvalFlowProgressNodes : approvalFlowNodes |
| | | ), |
| | | rejectReason: |
| | | approvalRecords.find(r => r.result === "rejected")?.opinion || |
| | | source.rejectReason || |
| | | "", |
| | | flowNodes, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | |
| | | export const EXPENSE_SUBJECT_OPTIONS = [ |
| | | { label: "交éè´¹", value: "transport" }, |
| | | { label: "ä½å®¿è´¹", value: "hotel" }, |
| | | { label: "é¤é¥®è´¹", value: "meal" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ]; |
| | | |
| | | const TIER1_CITIES = ["å京", "䏿µ·", "广å·", "æ·±å³"]; |
| | | |
| | | export function expenseSubjectLabel(v) { |
| | | return EXPENSE_SUBJECT_OPTIONS.find(x => x.value === v)?.label || "â"; |
| | | } |
| | | |
| | | export function detectTravelTier(destination) { |
| | | const city = (destination || "").trim(); |
| | | if (!city) return "tier3"; |
| | | if (TIER1_CITIES.some(c => city.includes(c))) return "tier1"; |
| | | const tier2Keywords = ["æå·", "å京", "æ¦æ±", "æé½", "éåº", "西å®", "天津", "èå·", "é¿æ²", "éå·"]; |
| | | if (tier2Keywords.some(c => city.includes(c))) return "tier2"; |
| | | return "tier3"; |
| | | } |
| | | |
| | | export function getTravelStandardByTier(tier) { |
| | | const map = { |
| | | tier1: { hotelPerNight: 600, transportPerDay: 80, mealPerDay: 100, label: "ä¸çº¿åå¸" }, |
| | | tier2: { hotelPerNight: 450, transportPerDay: 60, mealPerDay: 80, label: "äºçº¿åå¸" }, |
| | | tier3: { hotelPerNight: 350, transportPerDay: 40, mealPerDay: 60, label: "å
¶ä»åå¸" }, |
| | | }; |
| | | return map[tier] || map.tier3; |
| | | } |
| | | |
| | | export function computeTravelDays(startStr, endStr) { |
| | | if (!startStr || !endStr) return null; |
| | | const t0 = dayjs(startStr); |
| | | const t1 = dayjs(endStr); |
| | | if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null; |
| | | return Math.max(1, Math.ceil(t1.diff(t0, "day", true))); |
| | | } |
| | | |
| | | export function createEmptyExpenseDetail() { |
| | | return { |
| | | id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, |
| | | invoiceDate: "", |
| | | expenseSubject: "", |
| | | amount: "", |
| | | description: "", |
| | | }; |
| | | } |
| | | |
| | | export function createEmptyTravelForm() { |
| | | return { |
| | | reimbursementId: undefined, |
| | | applicantId: "", |
| | | employeeNo: "", |
| | | employeeName: "", |
| | | reimburseReason: "", |
| | | travelStartTime: "", |
| | | travelEndTime: "", |
| | | travelDays: undefined, |
| | | departurePlace: "", |
| | | destination: "", |
| | | hotelStandard: undefined, |
| | | hotelDays: undefined, |
| | | livingSubsidy: undefined, |
| | | transportSubsidy: undefined, |
| | | lodgingLimit: undefined, |
| | | applyAmount: "", |
| | | payee: "", |
| | | payeeAccount: "", |
| | | payeeBank: "", |
| | | expenseDetails: [], |
| | | attachmentList: [], |
| | | approvalFlowNodes: [{ approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" }], |
| | | needSpecialApproval: false, |
| | | deptId: "", |
| | | deptName: "", |
| | | travelTier: "tier3", |
| | | standardTag: "", |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / æ¥é管ç / è´¹ç¨æ¥éï¼/finReimbursement/listPageï¼reimbursementType=2ï¼ |
| | | --> |
| | | <template> |
| | | <FinReimbursementListPage :module-key="APPROVAL_MODULE_KEYS.COST_REIMBURSE" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import FinReimbursementListPage from "../../_components/FinReimbursementListPage.vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js"; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | å·®æ
/è´¹ç¨æ¥é详æ
页 |
| | | --> |
| | | <template> |
| | | <view class="oa-detail-page reimburse-detail-page"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | |
| | | <view v-if="loading" |
| | | class="rd-loading-wrap"> |
| | | <up-loading-icon mode="circle" /> |
| | | <text class="rd-loading-text">å è½½ä¸...</text> |
| | | </view> |
| | | |
| | | <scroll-view v-else-if="reimburseRow" |
| | | class="oa-detail-scroll reimburse-detail-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <ReimburseInstanceDetailBody :reimburse-row="reimburseRow" |
| | | :module-key="moduleKey" /> |
| | | </scroll-view> |
| | | |
| | | <view v-else |
| | | class="oa-empty"> |
| | | <up-empty mode="data" |
| | | text="æªè·åå°æ¥éæ°æ®" /> |
| | | </view> |
| | | |
| | | <view v-if="reimburseRow && canEdit" |
| | | class="oa-page-footer"> |
| | | <text class="oa-footer-btn btn-default" |
| | | @click="goBack">è¿å</text> |
| | | <text class="oa-footer-btn btn-primary" |
| | | @click="goEdit">ä¿®æ¹</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import ReimburseInstanceDetailBody from "../_components/ReimburseInstanceDetailBody.vue"; |
| | | import { OA_NAV } from "@/config/oaPaths.js"; |
| | | import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js"; |
| | | import { |
| | | canEditReimbursementRow, |
| | | fetchFinReimbursementListItemDetail, |
| | | resolveReimbursementDeleteId, |
| | | } from "../../_utils/finReimbursementMappers.js"; |
| | | |
| | | const moduleKey = ref(""); |
| | | const reimbursementId = ref(""); |
| | | const reimburseRow = ref(null); |
| | | const loading = ref(false); |
| | | |
| | | const pageTitle = computed( |
| | | () => `${getApprovalModuleConfig(moduleKey.value)?.label || "æ¥é"}详æ
` |
| | | ); |
| | | |
| | | const canEdit = computed(() => |
| | | reimburseRow.value ? canEditReimbursementRow(reimburseRow.value) : false |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | const rid = resolveReimbursementDeleteId(reimburseRow.value); |
| | | if (rid == null) { |
| | | uni.showToast({ title: "æ æ³ä¿®æ¹", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.navigateTo({ |
| | | url: `${OA_NAV.reimburseForm}?moduleKey=${moduleKey.value}&mode=edit&reimbursementId=${rid}`, |
| | | }); |
| | | }; |
| | | |
| | | onLoad(async options => { |
| | | moduleKey.value = options?.moduleKey || ""; |
| | | reimbursementId.value = options?.reimbursementId || ""; |
| | | if (!moduleKey.value || !reimbursementId.value) { |
| | | uni.showToast({ title: "åæ°ä¸å®æ´", icon: "none" }); |
| | | setTimeout(goBack, 500); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | reimburseRow.value = await fetchFinReimbursementListItemDetail( |
| | | { reimbursementId: reimbursementId.value }, |
| | | moduleKey.value |
| | | ); |
| | | if (reimburseRow.value?.moduleKey) { |
| | | moduleKey.value = reimburseRow.value.moduleKey; |
| | | } |
| | | } catch { |
| | | uni.showToast({ title: "å 载详æ
失败", icon: "none" }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "../../_styles/oa-approval-list.scss"; |
| | | @import "./reimburse-detail.scss"; |
| | | |
| | | .rd-loading-wrap { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 80px 0; |
| | | } |
| | | .rd-loading-text { |
| | | margin-top: 12px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | .reimburse-detail-page { |
| | | min-height: 100vh; |
| | | background: #f2f4f7; |
| | | } |
| | | |
| | | .reimburse-detail-scroll { |
| | | padding-bottom: calc(72px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .rd-hero { |
| | | margin: 12px 16px 0; |
| | | padding: 16px; |
| | | background: linear-gradient(135deg, #f0f7ff 0%, #fff 55%); |
| | | border-radius: 12px; |
| | | box-shadow: 0 1px 4px rgba(15, 23, 42, 0.06); |
| | | border: 1px solid #e8f0fe; |
| | | } |
| | | |
| | | .rd-hero-top { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .rd-bill-no { |
| | | font-size: 13px; |
| | | color: #8c8c8c; |
| | | flex: 1; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .rd-status { |
| | | flex-shrink: 0; |
| | | font-size: 11px; |
| | | padding: 5px 8px; |
| | | border-radius: 4px; |
| | | font-weight: 500; |
| | | |
| | | &.status-pending { |
| | | color: #d46b08; |
| | | background: #fff7e6; |
| | | } |
| | | &.status-approved { |
| | | color: #389e0d; |
| | | background: #f6ffed; |
| | | } |
| | | &.status-rejected { |
| | | color: #cf1322; |
| | | background: #fff1f0; |
| | | } |
| | | &.status-draft { |
| | | color: #595959; |
| | | background: #f5f5f5; |
| | | } |
| | | &.status-cancelled { |
| | | color: #8c8c8c; |
| | | background: #fafafa; |
| | | } |
| | | } |
| | | |
| | | .rd-reason { |
| | | display: block; |
| | | margin-top: 10px; |
| | | font-size: 17px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | line-height: 1.45; |
| | | } |
| | | |
| | | .rd-amount-row { |
| | | display: flex; |
| | | align-items: baseline; |
| | | justify-content: space-between; |
| | | margin-top: 14px; |
| | | padding-top: 12px; |
| | | border-top: 1px dashed #e8ecf0; |
| | | } |
| | | |
| | | .rd-amount-label { |
| | | font-size: 14px; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | .rd-amount { |
| | | font-size: 22px; |
| | | font-weight: 700; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .rd-section { |
| | | margin: 12px 16px 0; |
| | | } |
| | | |
| | | .rd-section-hd { |
| | | padding: 4px 4px 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .rd-section-title { |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: #909399; |
| | | } |
| | | |
| | | .rd-section-count { |
| | | font-size: 12px; |
| | | color: #c0c4cc; |
| | | } |
| | | |
| | | .rd-group { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 1px 4px rgba(15, 23, 42, 0.04); |
| | | } |
| | | |
| | | .rd-cell { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | padding: 13px 16px; |
| | | border-bottom: 1px solid #f5f6f8; |
| | | font-size: 14px; |
| | | line-height: 1.45; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .rd-label { |
| | | width: 88px; |
| | | flex-shrink: 0; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | .rd-value { |
| | | flex: 1; |
| | | color: #303133; |
| | | text-align: right; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .rd-detail-item { |
| | | padding: 14px 16px; |
| | | border-bottom: 1px solid #f5f6f8; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .rd-detail-head { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .rd-detail-badge { |
| | | width: 24px; |
| | | height: 24px; |
| | | border-radius: 6px; |
| | | background: #ecf5ff; |
| | | color: #2979ff; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | text-align: center; |
| | | line-height: 24px; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .rd-detail-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .rd-detail-amount { |
| | | margin-left: auto; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .rd-flow-node { |
| | | display: flex; |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f5f6f8; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .rd-flow-line { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | margin-right: 12px; |
| | | width: 20px; |
| | | } |
| | | |
| | | .rd-flow-dot { |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 50%; |
| | | background: #2979ff; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .rd-flow-bar { |
| | | flex: 1; |
| | | width: 2px; |
| | | min-height: 20px; |
| | | background: #e4e7ed; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .rd-flow-body { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .rd-flow-level { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | |
| | | .rd-flow-type { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-top: 2px; |
| | | } |
| | | |
| | | .rd-flow-approver { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .rd-flow-avatar { |
| | | width: 28px; |
| | | height: 28px; |
| | | border-radius: 50%; |
| | | color: #fff; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .rd-flow-approver-meta { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .rd-flow-name { |
| | | display: block; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | } |
| | | |
| | | .rd-flow-status { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-top: 2px; |
| | | } |
| | | |
| | | .rd-record-item { |
| | | padding: 14px 16px; |
| | | border-bottom: 1px solid #f5f6f8; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .rd-record-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .rd-record-operator { |
| | | font-size: 15px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | |
| | | .rd-record-tag { |
| | | font-size: 11px; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | flex-shrink: 0; |
| | | |
| | | &--approved { |
| | | color: #389e0d; |
| | | background: #f6ffed; |
| | | } |
| | | &--rejected { |
| | | color: #cf1322; |
| | | background: #fff1f0; |
| | | } |
| | | &--pending { |
| | | color: #d46b08; |
| | | background: #fff7e6; |
| | | } |
| | | } |
| | | |
| | | .rd-record-time { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: #c0c4cc; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .rd-record-opinion { |
| | | display: block; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | margin-top: 6px; |
| | | line-height: 1.45; |
| | | } |
| | | |
| | | .rd-empty { |
| | | padding: 20px; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | color: #c0c4cc; |
| | | } |
| | | |
| | | .rd-attach { |
| | | padding: 12px 16px; |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | border-bottom: 1px solid #f5f6f8; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | å·®æ
/è´¹ç¨æ¥éæ°å¢/ç¼è¾ï¼ä¸ Web åæ®µä¸è´ï¼ç§»å¨ç«¯ä¼åé人/å¸å±ï¼ |
| | | --> |
| | | <template> |
| | | <view class="oa-detail-page reimburse-form-page"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <scroll-view class="oa-detail-scroll reimburse-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <view v-if="loading" |
| | | class="rf-loading">å è½½ä¸...</view> |
| | | |
| | | <view v-else> |
| | | <!-- ç³è¯·äºº --> |
| | | <view class="rf-section"> |
| | | <view class="rf-section-hd"> |
| | | <text class="rf-section-title">ç³è¯·äºº</text> |
| | | </view> |
| | | <view class="rf-group"> |
| | | <view class="rf-applicant-card" |
| | | :class="{ 'is-empty': !form.applicantId }" |
| | | @click="showApplicantPicker = true"> |
| | | <view class="rf-applicant-avatar" |
| | | :style="{ backgroundColor: applicantAvatarColor }"> |
| | | {{ (form.employeeName || 'é').charAt(0) }} |
| | | </view> |
| | | <view class="rf-applicant-meta"> |
| | | <text class="rf-applicant-name">{{ form.employeeName || 'è¯·éæ©åå·¥' }}</text> |
| | | <text class="rf-applicant-sub">{{ applicantDisplaySub }}</text> |
| | | </view> |
| | | <text class="rf-applicant-action">{{ form.applicantId ? 'æ´æ¢' : 'éæ©' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <view class="rf-section"> |
| | | <view class="rf-section-hd"> |
| | | <text class="rf-section-title">åºæ¬ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="rf-group"> |
| | | <view class="rf-cell rf-cell--col"> |
| | | <text class="rf-label required">æ¥éåå </text> |
| | | <view class="rf-textarea-wrap"> |
| | | <up-textarea v-model="form.reimburseReason" |
| | | placeholder="请填ååºå·®åæ¥éåå " |
| | | maxlength="2000" |
| | | border="none" |
| | | height="80" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <template v-if="isTravel"> |
| | | <view class="rf-cell rf-cell--tap" |
| | | @click="openDatePicker('travelStartTime')"> |
| | | <text class="rf-label required">åºå·®å¼å§</text> |
| | | <view class="rf-value-wrap"> |
| | | <text class="rf-value" |
| | | :class="{ placeholder: !form.travelStartTime }"> |
| | | {{ form.travelStartTime || 'è¯·éæ©' }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#c0c4cc" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell rf-cell--tap" |
| | | @click="openDatePicker('travelEndTime')"> |
| | | <text class="rf-label required">åºå·®ç»æ</text> |
| | | <view class="rf-value-wrap"> |
| | | <text class="rf-value" |
| | | :class="{ placeholder: !form.travelEndTime }"> |
| | | {{ form.travelEndTime || 'è¯·éæ©' }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#c0c4cc" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">åºå·®å¤©æ°</text> |
| | | <view class="rf-value-wrap"> |
| | | <text class="rf-value">{{ travelDaysDisplay || 'â' }}</text> |
| | | <text class="rf-value" |
| | | style="color:#909399;margin-left:4px">天</text> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label required">åºå·®å°</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.departurePlace" |
| | | placeholder="åºååå¸" |
| | | border="none" |
| | | input-align="right" |
| | | @blur="recalcTravelStandards" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label required">ç®çå°</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.destination" |
| | | placeholder="ç®çåå¸" |
| | | border="none" |
| | | input-align="right" |
| | | @blur="recalcTravelStandards" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <template v-else> |
| | | <view class="rf-cell rf-cell--tap" |
| | | @click="showCategorySheet = true"> |
| | | <text class="rf-label required">è´¹ç¨ç±»å</text> |
| | | <view class="rf-value-wrap"> |
| | | <text class="rf-value" |
| | | :class="{ placeholder: !form.expenseCategory }">{{ categoryLabel }}</text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#c0c4cc" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-chips"> |
| | | <text v-for="cat in quickCategories" |
| | | :key="cat.value" |
| | | class="rf-chip" |
| | | :class="{ active: form.expenseCategory === cat.value }" |
| | | @click="applyTemplate(cat.value)">{{ cat.label }}</text> |
| | | </view> |
| | | </template> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å·®æ
æ å --> |
| | | <view v-if="isTravel" |
| | | class="rf-section"> |
| | | <view class="rf-section-hd"> |
| | | <text class="rf-section-title">å·®æ
æ å</text> |
| | | <text class="rf-section-extra">{{ travelTierLabel }}</text> |
| | | </view> |
| | | <view v-if="overBudgetWarnings.length" |
| | | class="rf-warn-box"> |
| | | <text v-for="(w, i) in overBudgetWarnings" |
| | | :key="i" |
| | | class="rf-warn-line">{{ w }}</text> |
| | | </view> |
| | | <view class="rf-group"> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">é
åºæ å</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.hotelStandard" |
| | | type="digit" |
| | | placeholder="å
/æ" |
| | | border="none" |
| | | input-align="right" |
| | | @blur="recalcTravelStandards" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">ä½å®¿å¤©æ°</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.hotelDays" |
| | | type="number" |
| | | border="none" |
| | | input-align="right" |
| | | @blur="recalcTravelStandards" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">çæ´»è¡¥è´´</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.livingSubsidy" |
| | | type="digit" |
| | | border="none" |
| | | input-align="right" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">交é补贴</text> |
| | | <view class="rf-value-wrap"> |
| | | <text class="rf-value">建议 {{ suggestedTransportSubsidy }} å
</text> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">ä½å®¿éé¢</text> |
| | | <view class="rf-value-wrap"> |
| | | <text class="rf-value">建议 {{ suggestedHotelLimit }} å
</text> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">ç¹æ¹æ è®°</text> |
| | | <text class="rf-tag" |
| | | :class="form.needSpecialApproval ? 'rf-tag--danger' : 'rf-tag--ok'"> |
| | | {{ form.needSpecialApproval ? 'è¶
æ¯éç¹æ¹' : '卿 åå
' }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- éé¢ä¸æ¶æ¬¾ --> |
| | | <view class="rf-section"> |
| | | <view class="rf-section-hd"> |
| | | <text class="rf-section-title">éé¢ä¸æ¶æ¬¾</text> |
| | | <text class="rf-section-extra" |
| | | @click="syncApplyAmountFromDetails">ææç» {{ detailTotalAmount }} å
</text> |
| | | </view> |
| | | <view class="rf-group"> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label required">ç³è¯·éé¢</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.applyAmount" |
| | | type="digit" |
| | | placeholder="å
" |
| | | border="none" |
| | | input-align="right" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label required">æ¶æ¬¾äºº</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.payee" |
| | | placeholder="æ¶æ¬¾äºº" |
| | | border="none" |
| | | input-align="right" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">æ¶æ¬¾è´¦å·</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.payeeAccount" |
| | | placeholder="éå¡«" |
| | | border="none" |
| | | input-align="right" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label">弿·æ¯è¡</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-if="isTravel" |
| | | v-model="form.payeeBank" |
| | | placeholder="éå¡«" |
| | | border="none" |
| | | input-align="right" /> |
| | | <up-input v-else |
| | | v-model="form.bankBranch" |
| | | placeholder="éå¡«" |
| | | border="none" |
| | | input-align="right" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æ¥éæç»ï¼å表æè¦ + 详æ
æé® --> |
| | | <view class="rf-section"> |
| | | <view class="rf-section-hd"> |
| | | <text class="rf-section-title">æ¥éæç»</text> |
| | | <text class="rf-section-extra" |
| | | @click="addAndOpenDetail">+ æ°å¢</text> |
| | | </view> |
| | | <view class="rf-group" |
| | | v-if="form.expenseDetails.length"> |
| | | <view v-for="(row, idx) in form.expenseDetails" |
| | | :key="row.id || idx" |
| | | class="rf-detail-row" |
| | | :class="{ 'rf-detail-row--warn': detailSummary(row).incomplete }" |
| | | @click="openDetailEditor(idx)"> |
| | | <view class="rf-detail-index">{{ idx + 1 }}</view> |
| | | <view class="rf-detail-body"> |
| | | <view class="rf-detail-line1"> |
| | | <text class="rf-detail-subject">{{ detailSummary(row).subject }}</text> |
| | | <text class="rf-detail-amount">{{ detailSummary(row).amount }}</text> |
| | | </view> |
| | | <text class="rf-detail-line2">{{ detailSummary(row).sub }}</text> |
| | | </view> |
| | | <text class="rf-detail-action" |
| | | @click.stop="openDetailEditor(idx)">详æ
</text> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="rf-group"> |
| | | <view class="rf-empty" |
| | | @click="addAndOpenDetail">ç¹å»æ·»å æ¥éæç»</view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- éä»¶ --> |
| | | <view class="rf-section"> |
| | | <view class="rf-section-hd"> |
| | | <text class="rf-section-title">éä»¶ï¼å票ï¼</text> |
| | | </view> |
| | | <view class="rf-group"> |
| | | <view v-for="(f, i) in form.attachmentList" |
| | | :key="i" |
| | | class="rf-attach-item"> |
| | | <text>{{ f.name || 'éä»¶' }}</text> |
| | | <text class="rf-detail-del" |
| | | @click="removeAttachment(i)">å é¤</text> |
| | | </view> |
| | | <view class="rf-upload-zone" |
| | | @click="chooseAttachment"> |
| | | <up-icon name="plus-circle" |
| | | size="22" |
| | | color="#2979ff" /> |
| | | <text>ä¸ä¼ å票/éä»¶</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å®¡æ¹æµç¨ --> |
| | | <view class="rf-section"> |
| | | <view class="rf-section-hd"> |
| | | <text class="rf-section-title">å®¡æ¹æµç¨</text> |
| | | </view> |
| | | <view class="rf-group" |
| | | style="padding:12px"> |
| | | <ReimburseApprovalFlowEditor v-model="form.approvalFlowNodes" |
| | | :user-options="flowUserOptions" /> |
| | | <text class="rf-hint-row">æ¯çº§é¡»æå®å®¡æ¹äººï¼æ¯ææç´¢å§åæå·¥å·</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="oa-page-footer"> |
| | | <text class="oa-footer-btn btn-default" |
| | | @click="goBack">åæ¶</text> |
| | | <text class="oa-footer-btn btn-primary" |
| | | :class="{ 'is-disabled': submitting }" |
| | | @click="onSubmit">æäº¤</text> |
| | | </view> |
| | | |
| | | <OaUserSearchPicker v-model:show="showApplicantPicker" |
| | | v-model="form.applicantId" |
| | | title="éæ©ç³è¯·äºº" |
| | | :users="flowUserOptions" |
| | | @select="onApplicantPicked" /> |
| | | |
| | | <up-action-sheet :show="showCategorySheet" |
| | | title="è´¹ç¨ç±»å" |
| | | :actions="categoryActions" |
| | | @select="onCategorySelect" |
| | | @close="showCategorySheet = false" /> |
| | | |
| | | <ReimburseExpenseDetailSheet v-model:show="showDetailSheet" |
| | | v-model="detailDraft" |
| | | :index="editingDetailIndex" |
| | | :is-travel="isTravel" |
| | | :subject-options="expenseSubjectOptions" |
| | | @confirm="onDetailSheetConfirm" |
| | | @delete="onDetailSheetDelete" /> |
| | | |
| | | <up-popup :show="showDatePicker" |
| | | mode="bottom" |
| | | round="16" |
| | | @close="showDatePicker = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="datePickerTs" |
| | | mode="datetime" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" /> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import OaUserSearchPicker from "../../_components/OaUserSearchPicker.vue"; |
| | | import ReimburseExpenseDetailSheet from "../_components/ReimburseExpenseDetailSheet.vue"; |
| | | import config from "@/config.js"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js"; |
| | | import { consumeReimburseEditFromApprove } from "../../_utils/reimburseApproveBridge.js"; |
| | | import { EXPENSE_CATEGORY_OPTIONS } from "../_utils/costReimburseUtils.js"; |
| | | import { buildExpenseDetailSummary } from "../_utils/expenseDetailDisplay.js"; |
| | | import ReimburseApprovalFlowEditor from "../_components/ReimburseApprovalFlowEditor.vue"; |
| | | import { useFinReimburseForm } from "./useFinReimburseForm.js"; |
| | | |
| | | const moduleKey = ref(""); |
| | | const mode = ref("add"); |
| | | const reimbursementId = ref(""); |
| | | |
| | | const { |
| | | form, |
| | | isTravel, |
| | | submitting, |
| | | loading, |
| | | flowUserOptions, |
| | | travelDaysDisplay, |
| | | travelTierLabel, |
| | | suggestedTransportSubsidy, |
| | | suggestedHotelLimit, |
| | | detailTotalAmount, |
| | | overBudgetWarnings, |
| | | expenseSubjectOptions, |
| | | categoryActions, |
| | | categoryLabel, |
| | | showApplicantPicker, |
| | | applicantDisplaySub, |
| | | applicantAvatarColor, |
| | | showCategorySheet, |
| | | loadUserPool, |
| | | onApplicantPicked, |
| | | recalcTravelStandards, |
| | | syncApplyAmountFromDetails, |
| | | addExpenseDetail, |
| | | removeExpenseDetail, |
| | | applyTemplate, |
| | | initForm, |
| | | loadEdit, |
| | | submitForm, |
| | | } = useFinReimburseForm(moduleKey, mode); |
| | | |
| | | const showDatePicker = ref(false); |
| | | const datePickerField = ref(""); |
| | | const datePickerTs = ref(Date.now()); |
| | | |
| | | const showDetailSheet = ref(false); |
| | | const editingDetailIndex = ref(0); |
| | | const detailDraft = reactive({ |
| | | invoiceDate: "", |
| | | expenseSubject: "", |
| | | amount: "", |
| | | description: "", |
| | | }); |
| | | |
| | | const quickCategories = EXPENSE_CATEGORY_OPTIONS.slice(0, 4); |
| | | |
| | | const pageTitle = computed(() => { |
| | | const label = getApprovalModuleConfig(moduleKey.value)?.label || "æ¥é"; |
| | | return mode.value === "edit" ? `ç¼è¾${label}` : `æ°å¢${label}`; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | function detailSummary(row) { |
| | | return buildExpenseDetailSummary(row, { |
| | | isTravel: isTravel.value, |
| | | subjectOptions: expenseSubjectOptions.value, |
| | | }); |
| | | } |
| | | |
| | | function openDetailEditor(idx) { |
| | | editingDetailIndex.value = idx; |
| | | const row = form.expenseDetails[idx]; |
| | | if (!row) return; |
| | | Object.assign(detailDraft, JSON.parse(JSON.stringify(row))); |
| | | showDetailSheet.value = true; |
| | | } |
| | | |
| | | function addAndOpenDetail() { |
| | | addExpenseDetail(); |
| | | openDetailEditor(form.expenseDetails.length - 1); |
| | | } |
| | | |
| | | function onDetailSheetConfirm(data) { |
| | | const idx = editingDetailIndex.value; |
| | | if (form.expenseDetails[idx]) { |
| | | Object.assign(form.expenseDetails[idx], data); |
| | | } |
| | | recalcTravelStandards(); |
| | | } |
| | | |
| | | function onDetailSheetDelete() { |
| | | const idx = editingDetailIndex.value; |
| | | removeExpenseDetail(idx); |
| | | showDetailSheet.value = false; |
| | | } |
| | | |
| | | function onCategorySelect(action) { |
| | | form.expenseCategory = action.value; |
| | | applyTemplate(action.value); |
| | | showCategorySheet.value = false; |
| | | } |
| | | |
| | | function openDatePicker(field) { |
| | | datePickerField.value = field; |
| | | detailDateIndex.value = -1; |
| | | datePickerTs.value = Date.now(); |
| | | showDatePicker.value = true; |
| | | } |
| | | |
| | | function onDateConfirm(e) { |
| | | const ts = e?.value ?? datePickerTs.value; |
| | | if (datePickerField.value) { |
| | | form[datePickerField.value] = parseTime(ts, "{y}-{m}-{d} {h}:{i}:{s}"); |
| | | recalcTravelStandards(); |
| | | } |
| | | showDatePicker.value = false; |
| | | } |
| | | |
| | | function chooseAttachment() { |
| | | uni.chooseImage({ |
| | | count: 9, |
| | | success: res => { |
| | | (res.tempFilePaths || []).forEach(path => uploadOne(path)); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | function uploadOne(filePath) { |
| | | uni.uploadFile({ |
| | | url: `${config.baseUrl}/file/upload`, |
| | | filePath, |
| | | name: "file", |
| | | header: { Authorization: "Bearer " + getToken() }, |
| | | success: res => { |
| | | try { |
| | | const data = JSON.parse(res.data || "{}"); |
| | | const url = data.url || data.data?.url || ""; |
| | | const name = data.originalFilename || data.fileName || "éä»¶"; |
| | | if (!form.attachmentList) form.attachmentList = []; |
| | | form.attachmentList.push({ name, url }); |
| | | } catch { |
| | | uni.showToast({ title: "ä¸ä¼ è§£æå¤±è´¥", icon: "none" }); |
| | | } |
| | | }, |
| | | fail: () => uni.showToast({ title: "ä¸ä¼ 失败", icon: "none" }), |
| | | }); |
| | | } |
| | | |
| | | function removeAttachment(i) { |
| | | form.attachmentList.splice(i, 1); |
| | | } |
| | | |
| | | async function onSubmit() { |
| | | const ok = await submitForm(); |
| | | if (ok) setTimeout(goBack, 400); |
| | | } |
| | | |
| | | onLoad(async options => { |
| | | moduleKey.value = options?.moduleKey || ""; |
| | | mode.value = options?.mode === "edit" ? "edit" : "add"; |
| | | reimbursementId.value = options?.reimbursementId || ""; |
| | | const fromApprove = consumeReimburseEditFromApprove(); |
| | | if (fromApprove?.moduleKey) { |
| | | moduleKey.value = fromApprove.moduleKey; |
| | | mode.value = "edit"; |
| | | reimbursementId.value = String(fromApprove.reimbursementId ?? ""); |
| | | } |
| | | if (!moduleKey.value) { |
| | | uni.showToast({ title: "ç¼ºå°æ¨¡åç±»å", icon: "none" }); |
| | | setTimeout(goBack, 500); |
| | | return; |
| | | } |
| | | await loadUserPool(); |
| | | await initForm(); |
| | | if (mode.value === "edit" && reimbursementId.value) { |
| | | try { |
| | | await loadEdit(reimbursementId.value); |
| | | } catch { |
| | | uni.showToast({ title: "å 载失败", icon: "none" }); |
| | | } |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "../../_styles/oa-approval-list.scss"; |
| | | @import "./reimburse-form.scss"; |
| | | </style> |
| src/pages/oa/ReimburseManage/reimburse-form/reimburse-form.scss
src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js
src/pages/oa/ReimburseManage/travel-reimburse/index.vue
src/pages/oa/_components/ApprovalInstanceListPage.vue
src/pages/oa/_components/ApprovalModuleSearchPopup.vue
src/pages/oa/_components/FinReimbursementListPage.vue
src/pages/oa/_components/OaListPage.vue
src/pages/oa/_components/OaUserSearchPicker.vue
src/pages/oa/_styles/oa-approval-list.scss
src/pages/oa/_utils/approvalFormField.js
src/pages/oa/_utils/approvalModuleApplyExtras.js
src/pages/oa/_utils/approvalModuleListSearch.js
src/pages/oa/_utils/approvalModuleRegistry.js
src/pages/oa/_utils/approvalTemplateType.js
src/pages/oa/_utils/approveListUtils.js
src/pages/oa/_utils/finReimbursementMappers.js
src/pages/oa/_utils/oaPageRegistry.js
src/pages/oa/_utils/oaStorage.js
src/pages/oa/_utils/oaUi.js
src/pages/oa/_utils/reimburseApproveBridge.js
src/pages/oa/_utils/useOaPage.js
src/pages/oa/_utils/userPickerUtils.js
src/pages/procurementManagement/procurementLedger/detail.vue
src/pages/productionDesign/basicParameters/edit.vue
src/pages/productionDesign/basicParameters/index.vue
src/pages/productionDesign/bom/BomStructureItem.vue
src/pages/productionDesign/bom/index.vue
src/pages/productionDesign/bom/structure.vue
src/pages/productionDesign/processManagement/edit.vue
src/pages/productionDesign/processManagement/index.vue
src/pages/productionDesign/processManagement/params.vue
src/pages/productionManagement/mainProductionPlan/detail.vue
src/pages/productionManagement/mainProductionPlan/index.vue
src/pages/productionManagement/processRoute/index.vue
src/pages/productionManagement/processRoute/items.vue
src/pages/productionManagement/processStatistics/index.vue
src/pages/productionManagement/productionAccounting/index.vue
src/pages/productionManagement/productionDispatching/components/DispatchModal.vue
src/pages/productionManagement/productionDispatching/components/formDia.vue
src/pages/productionManagement/productionDispatching/index.vue
src/pages/productionManagement/productionOrder/index.vue
src/pages/productionManagement/productionOrder/pickingDetail.vue
src/pages/productionManagement/productionOrder/source.vue
src/pages/productionManagement/productionReport/index.vue
src/pages/productionManagement/productionReporting/ledger.vue
src/pages/productionManagement/productionScheduling/index.vue
src/pages/productionManagement/productionTraceability/index.vue
src/pages/qualityManagement/finalInspection/add.vue
src/pages/qualityManagement/finalInspection/detail.vue
src/pages/qualityManagement/finalInspection/index.vue
src/pages/qualityManagement/materialInspection/add.vue
src/pages/qualityManagement/materialInspection/detail.vue
src/pages/qualityManagement/materialInspection/index.vue
src/pages/qualityManagement/processInspection/add.vue
src/pages/qualityManagement/processInspection/detail.vue
src/pages/qualityManagement/processInspection/index.vue
src/pages/sales/salesAccount/goOut.vue
src/pages/sales/salesQuotation/detail.vue
src/pages/sales/salesQuotation/edit.vue
src/pages/sales/salesQuotation/index.vue
src/pages/works.vue
src/static/images/icon/baogongtaizhang.svg
src/static/images/icon/bom.svg
src/static/images/icon/gongxuguanli.svg
src/static/images/icon/gongyiluxian.svg
src/static/images/icon/guihuandengji.svg
src/static/images/icon/jichucanshu.svg
src/static/images/icon/jieyuedengji.svg
src/static/images/icon/kucunguanli.svg
src/static/images/icon/shengchandingdan.svg
src/static/images/icon/shengchanhesuan.svg
src/static/images/icon/shengchanjihua.svg
src/static/images/icon/shengchanpaichan.svg
src/static/images/icon/shengchanshikuang.svg
src/static/images/icon/shengchanzhuisu.svg
src/store/modules/user.ts
src/utils/versionUpgrade.js |