| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢ï¼warnStatus: normal æ£å¸¸ / expiring å³å°å°æ / overdue å·²é¾æï¼ |
| | | export function broadbandListPage(query) { |
| | | return request({ |
| | | url: "/broadband/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function getBroadbandDetail(id) { |
| | | return request({ |
| | | url: "/broadband/getDetail", |
| | | method: "get", |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ä¸ä¿®æ¹å
±ç¨ï¼é id æ¯å¦åå¨åºå |
| | | export function addOrUpdateBroadband(data) { |
| | | return request({ |
| | | url: "/broadband/addOrUpdate", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ï¼body 为 id æ°ç» |
| | | export function delBroadband(ids) { |
| | | return request({ |
| | | url: "/broadband/del", |
| | | method: "delete", |
| | | data: Array.isArray(ids) ? ids : [ids], |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢ï¼warnStatus: normal æ£å¸¸ / expiring å³å°å°æ / overdue å·²é¾æï¼ |
| | | export function houseLeaseListPage(query) { |
| | | return request({ |
| | | url: "/houseLease/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function getHouseLeaseDetail(id) { |
| | | return request({ |
| | | url: "/houseLease/getDetail", |
| | | method: "get", |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ä¸ä¿®æ¹å
±ç¨ï¼é id æ¯å¦åå¨åºåï¼æåº¦è´¹ç¨å表é costList ä¸èµ·æäº¤ |
| | | export function addOrUpdateHouseLease(data) { |
| | | return request({ |
| | | url: "/houseLease/addOrUpdate", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ï¼body 为 id æ°ç» |
| | | export function delHouseLease(ids) { |
| | | return request({ |
| | | url: "/houseLease/del", |
| | | method: "delete", |
| | | data: Array.isArray(ids) ? ids : [ids], |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // ---------------- è½¦è¾æ¡£æ¡ ---------------- |
| | | |
| | | export function listVehicleInfo(query) { |
| | | return request({ |
| | | url: "/vehicle/info/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 30 天å
å¹´æ£/ä¿é©å°æç车è¾ï¼æ åæ°ï¼è¿åæ°ç» |
| | | export function getVehicleWarningList() { |
| | | return request({ |
| | | url: "/vehicle/info/warningList", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | export function addVehicleInfo(data) { |
| | | return request({ |
| | | url: "/vehicle/info", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function updateVehicleInfo(data) { |
| | | return request({ |
| | | url: "/vehicle/info", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function delVehicleInfo(id) { |
| | | return request({ |
| | | url: `/vehicle/info/${id}`, |
| | | method: "delete", |
| | | }); |
| | | } |
| | | |
| | | // ---------------- 车è¾è´¹ç¨ ---------------- |
| | | // 管ç端没æåç¬çè´¹ç¨ api æä»¶ï¼ç´æ¥å
è requestï¼è¿éè¡¥é½ |
| | | |
| | | export function listVehicleCost(query) { |
| | | return request({ |
| | | url: "/vehicle/cost/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function addVehicleCost(data) { |
| | | return request({ |
| | | url: "/vehicle/cost", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function delVehicleCost(id) { |
| | | return request({ |
| | | url: `/vehicle/cost/${id}`, |
| | | method: "delete", |
| | | }); |
| | | } |
| | | |
| | | // ---------------- å æ²¹å¡ ---------------- |
| | | |
| | | export function listFuelCardPage(query) { |
| | | return request({ |
| | | url: "/vehicle/fuelCard/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // å
¨éå¡å表ï¼ä¸å页ï¼ï¼ç¨äºå 油记å½ç䏿ä¸ä½é¢å±ç¤º |
| | | export function listAllFuelCard() { |
| | | return request({ |
| | | url: "/vehicle/fuelCard/listAll", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | export function addFuelCard(data) { |
| | | return request({ |
| | | url: "/vehicle/fuelCard", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function updateFuelCard(data) { |
| | | return request({ |
| | | url: "/vehicle/fuelCard", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function delFuelCard(id) { |
| | | return request({ |
| | | url: `/vehicle/fuelCard/${id}`, |
| | | method: "delete", |
| | | }); |
| | | } |
| | | |
| | | // ---------------- å æ²¹å¡å
å¼ ---------------- |
| | | |
| | | export function listRecharge(query) { |
| | | return request({ |
| | | url: "/vehicle/fuelCardRecharge/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function addRecharge(data) { |
| | | return request({ |
| | | url: "/vehicle/fuelCardRecharge/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function delRecharge(id) { |
| | | return request({ |
| | | url: `/vehicle/fuelCardRecharge/${id}`, |
| | | method: "delete", |
| | | }); |
| | | } |
| | | |
| | | // ---------------- å æ²¹è®°å½ ---------------- |
| | | |
| | | export function listFuelRecord(query) { |
| | | return request({ |
| | | url: "/vehicle/fuelRecord/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function addFuelRecord(data) { |
| | | return request({ |
| | | url: "/vehicle/fuelRecord", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function updateFuelRecord(data) { |
| | | return request({ |
| | | url: "/vehicle/fuelRecord", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function delFuelRecord(id) { |
| | | return request({ |
| | | url: `/vehicle/fuelRecord/${id}`, |
| | | method: "delete", |
| | | }); |
| | | } |
| | | |
| | | // æäº¤ï¼ä»å æ²¹å¡æ£åä½é¢ãæ body ç POST |
| | | export function submitFuelRecord(id) { |
| | | return request({ |
| | | url: `/vehicle/fuelRecord/submit/${id}`, |
| | | method: "post", |
| | | }); |
| | | } |
| | | |
| | | // æ¤åï¼éè¿å æ²¹å¡ä½é¢ãæ body ç POST |
| | | export function revokeFuelRecord(id) { |
| | | return request({ |
| | | url: `/vehicle/fuelRecord/revoke/${id}`, |
| | | method: "post", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // 项ç®ç®¡çå
¨å±é
ç½®ï¼åååä»¶åä¼ é¢è¦æå天æ°çï¼ |
| | | export function getProjectConfig() { |
| | | return request({ |
| | | url: "/projectManagement/config", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | export function updateProjectConfig(data) { |
| | | return request({ |
| | | url: "/projectManagement/config", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢é¡¹ç®ï¼æ°å¢ä¸ç¼è¾å
±ç¨ /saveï¼é info.id åºåï¼ |
| | | export function listProject(data) { |
| | | return request({ |
| | | url: "/projectManagement/info/listPage", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢é¡¹ç®è¯¦æ
ï¼id å¨è·¯å¾ä¸ï¼POSTï¼ |
| | | export function getProject(id) { |
| | | return request({ |
| | | url: `/projectManagement/info/${id}`, |
| | | method: "post", |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ / ç¼è¾é¡¹ç® |
| | | export function saveProject(data) { |
| | | return request({ |
| | | url: "/projectManagement/info/save", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤é¡¹ç®ï¼body 为 id æ°ç»ï¼ |
| | | export function delProject(ids) { |
| | | return request({ |
| | | url: "/projectManagement/info/remove", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // æ´æ°åæ®ç¶æ |
| | | export function updateStatus(data) { |
| | | return request({ |
| | | url: "/projectManagement/info/updateStatus", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // æäº¤ï¼reviewStatus ç½® 0 |
| | | export function submitProject(data) { |
| | | return request({ |
| | | url: "/projectManagement/info/updateStatus", |
| | | method: "post", |
| | | data: { ...data, reviewStatus: 0 }, |
| | | }); |
| | | } |
| | | |
| | | // å®¡æ ¸ï¼reviewStatus ç½® 1 |
| | | export function auditProject(data) { |
| | | return request({ |
| | | url: "/projectManagement/info/updateStatus", |
| | | method: "post", |
| | | data: { ...data, reviewStatus: 1 }, |
| | | }); |
| | | } |
| | | |
| | | // é¶æ®µï¼è¿åº¦æ±æ¥ï¼ |
| | | export function saveStage(data) { |
| | | return request({ |
| | | url: "/projectManagement/info/saveStage", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function listStage(projectId) { |
| | | return request({ |
| | | url: `/projectManagement/info/listStage/${projectId}`, |
| | | method: "post", |
| | | }); |
| | | } |
| | | |
| | | export function deleteStage(stageId) { |
| | | return request({ |
| | | url: `/projectManagement/info/deleteStage/${stageId}`, |
| | | method: "post", |
| | | }); |
| | | } |
| | | |
| | | // 项ç®å½æ¡£éä»¶ |
| | | export function getProjectAttachments(id) { |
| | | return request({ |
| | | url: `/projectManagement/info/attachments/${id}`, |
| | | method: "get", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // 项ç®ç±»åï¼ç®¡ç端ã计åãï¼ä¸æ |
| | | export function listPlan(data) { |
| | | return request({ |
| | | url: "/projectManagement/plan/listPage", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // 项ç®å¢éè§è²ä¸æ |
| | | export function findRoleListPage(query) { |
| | | return request({ |
| | | url: "/projectManagement/roles/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // æ¥è¯¢æ¥åå表 |
| | | export function listInspectionReport(query) { |
| | | return request({ |
| | | url: "/quality/inspectionReport/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢æ¥åè¯¦ç» |
| | | export function getInspectionReport(id) { |
| | | return request({ |
| | | url: `/quality/inspectionReport/${id}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢æ¥å |
| | | export function addInspectionReport(data) { |
| | | return request({ |
| | | url: "/quality/inspectionReport", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹æ¥å |
| | | export function updateInspectionReport(data) { |
| | | return request({ |
| | | url: "/quality/inspectionReport", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // 注éä½åºï¼å端å
¥å为éå·åéç id ä¸²ï¼ |
| | | export function delInspectionReport(ids) { |
| | | return request({ |
| | | url: `/quality/inspectionReport/${ids}`, |
| | | method: "delete", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * åºåæ£éªæ¥åæä»¶ï¼éæ±ä¹.4ï¼ |
| | | * |
| | | * /quality/qualityInspect/downPdf æ¯ POST + body è¿åäºè¿å¶ï¼èï¼ |
| | | * request.ts ä¸éä¼ responseTypeï¼ä¼æäºè¿å¶å½ JSON è§£æï¼ |
| | | * uni.downloadFile åªæ¯æ GET |
| | | * æä»¥è¿éä¸èµ° requestï¼ç´æ¥äº¤ç» utils/file ç downloadFileByPost è½çå openDocumentã |
| | | */ |
| | | import { downloadFileByPost } from "@/utils/file"; |
| | | |
| | | export function downloadQualityInspectPdf(id, fileName = "åºåæ£éªæ¥å.pdf") { |
| | | return downloadFileByPost("/quality/qualityInspect/downPdf", { id }, fileName); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢ï¼ææ æ¥æåºé´åæ°ä¸º bidDateStart / bidDateEndï¼ |
| | | export function biddingListPage(query) { |
| | | return request({ |
| | | url: "/sales/bidding/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function getBiddingDetail(id) { |
| | | return request({ |
| | | url: "/sales/bidding/getDetail", |
| | | method: "get", |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ä¸ä¿®æ¹å
±ç¨ï¼é id æ¯å¦åå¨åºå |
| | | export function addOrUpdateBidding(data) { |
| | | return request({ |
| | | url: "/sales/bidding/addOrUpdate", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ï¼body 为 id æ°ç» |
| | | export function delBidding(ids) { |
| | | return request({ |
| | | url: "/sales/bidding/del", |
| | | method: "delete", |
| | | data: Array.isArray(ids) ? ids : [ids], |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢ |
| | | export function contractListPage(query) { |
| | | return request({ |
| | | url: "/sales/contract/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // ä¸å页çååç²¾ç®å表ï¼ä¾è¯è®¼ç®¡ççæ¨¡åçãå
³èéå®ååãä¸æä½¿ç¨ |
| | | export function contractListNoPage(params) { |
| | | return request({ |
| | | url: "/sales/contract/listNoPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | export function getContractDetail(id) { |
| | | return request({ |
| | | url: "/sales/contract/getDetail", |
| | | method: "get", |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ä¸ä¿®æ¹å
±ç¨ï¼é id æ¯å¦åå¨åºå |
| | | export function addOrUpdateContract(data) { |
| | | return request({ |
| | | url: "/sales/contract/addOrUpdate", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ï¼body 为 id æ°ç» |
| | | export function delContract(ids) { |
| | | return request({ |
| | | url: "/sales/contract/del", |
| | | method: "delete", |
| | | data: Array.isArray(ids) ? ids : [ids], |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢ï¼åè´§æ¥æåºé´åæ°ä¸º beginShippingDate / endShippingDateï¼ |
| | | export function deliveryLedgerListPage(query) { |
| | | return request({ |
| | | url: "/shippingInfo/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 详æ
ï¼å端路å¾å°±æ¯ getDateilï¼å¿"çº æ£"æ getDetailï¼ |
| | | export function getDeliveryDetail(id) { |
| | | return request({ |
| | | url: `/shippingInfo/getDateil/${id}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // åè´§å°è´¦ä¿®æ¹ |
| | | export function addOrUpdateDeliveryLedger(data) { |
| | | return request({ |
| | | url: "/shippingInfo/update", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // è¡¥å
åè´§ä¿¡æ¯åæäº¤ï¼ä¼æ£ååºåï¼ |
| | | export function deductStock(data) { |
| | | return request({ |
| | | url: "/shippingInfo/deductStock", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ï¼body 为 id æ°ç» |
| | | export function delDeliveryLedger(ids) { |
| | | return request({ |
| | | url: "/shippingInfo/delete", |
| | | method: "delete", |
| | | data: Array.isArray(ids) ? ids : [ids], |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 客æ·å¾æ¥ï¼éæ±ä¸ï¼ï¼é¡¹ç® â å®¢æ· â åå ä¸çº§å¾æ¥æ±æ»ï¼ä¸ç®¡ç端 salesManagement/receiptPaymentLedger åä¸å¥æ¥å£ |
| | | import request from "@/utils/request"; |
| | | |
| | | // é¡¶é¨ç»è®¡æ¡ |
| | | export function transactionsSummary(query) { |
| | | return request({ |
| | | url: "/metricStatistics/transactionsSummary", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 第ä¸çº§ï¼é¡¹ç®å¾æ¥æ±æ» |
| | | export function projectTransactions(query) { |
| | | return request({ |
| | | url: "/metricStatistics/projectTransactions", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 第äºçº§ï¼é¡¹ç®ä¸ç客æ·å¾æ¥ |
| | | export function customewTransactions(query) { |
| | | return request({ |
| | | url: "/metricStatistics/customewTransactions", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 第ä¸çº§ï¼å®¢æ·ä¸çåå徿¥æç» |
| | | export function customewTransactionsDetails(query) { |
| | | return request({ |
| | | url: "/metricStatistics/customewTransactionsDetails", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢ï¼ç«æ¡æ¶é´åºé´åæ°ä¸º filingDateStart / filingDateEndï¼ |
| | | export function lawsuitListPage(query) { |
| | | return request({ |
| | | url: "/sales/lawsuit/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function getLawsuitDetail(id) { |
| | | return request({ |
| | | url: "/sales/lawsuit/getDetail", |
| | | method: "get", |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ä¸ä¿®æ¹å
±ç¨ï¼é id æ¯å¦åå¨åºå |
| | | export function addOrUpdateLawsuit(data) { |
| | | return request({ |
| | | url: "/sales/lawsuit/addOrUpdate", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ï¼body 为 id æ°ç» |
| | | export function delLawsuit(ids) { |
| | | return request({ |
| | | url: "/sales/lawsuit/del", |
| | | method: "delete", |
| | | data: Array.isArray(ids) ? ids : [ids], |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 夿¡ä»¶æ¥è¯¢å¼¹å±ï¼schema 驱å¨ï¼ |
| | | |
| | | <FilterPanel v-model:show="showFilter" :model-value="query" :fields="filterFields" |
| | | @search="onSearch" @reset="onReset" /> |
| | | |
| | | 注æï¼query ç¨ reactive å®ä¹æ¶ä¸è¦å v-model="query"ã |
| | | ç»ä»¶ååçæ¯æ´ä¸ªçé对象ï¼reactive 常éä¸å¯æ´ä½éæ°èµå¼ |
| | | ï¼ä¼ç¼è¯æ `query = $event`ï¼è¿è¡æ¶æ Assignment to constant variableï¼ã |
| | | ç¨ :model-value ä¼ å
¥ + å¨ @search/@reset é Object.assign(query, values) ååï¼ |
| | | |
| | | const query = reactive({ projectName: '', bidResult: '' }); |
| | | const onSearch = values => { Object.assign(query, values); getList(); }; |
| | | |
| | | 页é¢è¥å¦ææç´¢è¾å
¥æ¡ç»å° query çæä¸ªå段ï¼ä¸¤è¾¹ä¼å¤©ç¶åæ¥ï¼åä¸ä¸ªå¯¹è±¡ï¼ã |
| | | |
| | | fields æ¯æçç±»åï¼ |
| | | { prop: 'projectName', label: '项ç®åç§°', type: 'input' } |
| | | { prop: 'bidResult', label: 'æææ ç»æ', type: 'select', options: [...] } |
| | | { prop: 'filingDate', label: 'ç«æ¡æ¶é´', type: 'date' } |
| | | { prop: 'bidDateRange', label: 'ææ æ¥æ', type: 'daterange' } |
| | | |
| | | daterange ç±»åç弿¯ [å¼å§, ç»æ] 两å
ç´ æ°ç»ï¼é¡µé¢å¨æäº¤åèªè¡ææå端è¦çåæ°å |
| | | ï¼å模åçåºé´åæ°åä¸ç»ä¸ï¼å¦ bidDateStart/bidDateEndãbeginShippingDate/endShippingDateï¼ã |
| | | --> |
| | | <template> |
| | | <up-popup :show="show" |
| | | mode="bottom" |
| | | :round="16" |
| | | @close="close"> |
| | | <view class="filter-panel"> |
| | | <view class="filter-panel__header"> |
| | | <text class="filter-panel__title">{{ title }}</text> |
| | | <u-icon name="close" |
| | | size="20" |
| | | color="#909399" |
| | | @click="close"></u-icon> |
| | | </view> |
| | | |
| | | <scroll-view scroll-y |
| | | class="filter-panel__body"> |
| | | <view v-for="field in fields" |
| | | :key="field.prop" |
| | | class="filter-panel__field"> |
| | | <text class="filter-panel__label">{{ field.label }}</text> |
| | | <view class="filter-panel__control"> |
| | | <u-input v-if="field.type === 'input'" |
| | | v-model="local[field.prop]" |
| | | :placeholder="field.placeholder || '请è¾å
¥'" |
| | | clearable /> |
| | | <FormPicker v-else-if="field.type === 'select'" |
| | | v-model="local[field.prop]" |
| | | type="select" |
| | | :options="field.options || []" |
| | | :title="`éæ©${field.label}`" |
| | | :placeholder="field.placeholder || 'è¯·éæ©'" /> |
| | | <FormPicker v-else-if="field.type === 'date'" |
| | | v-model="local[field.prop]" |
| | | type="date" |
| | | :placeholder="field.placeholder || 'è¯·éæ©'" /> |
| | | <view v-else-if="field.type === 'daterange'" |
| | | class="filter-panel__range"> |
| | | <FormPicker v-model="local[field.prop][0]" |
| | | type="date" |
| | | placeholder="å¼å§æ¥æ" /> |
| | | <text class="filter-panel__range-sep">è³</text> |
| | | <FormPicker v-model="local[field.prop][1]" |
| | | type="date" |
| | | placeholder="ç»ææ¥æ" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="filter-panel__footer"> |
| | | <u-button class="filter-panel__btn" |
| | | @click="reset">éç½®</u-button> |
| | | <u-button class="filter-panel__btn" |
| | | type="primary" |
| | | @click="search">æ¥è¯¢</u-button> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, watch } from "vue"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | |
| | | const props = defineProps({ |
| | | show: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | modelValue: { |
| | | type: Object, |
| | | default: () => ({}), |
| | | }, |
| | | fields: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | title: { |
| | | type: String, |
| | | default: "çé", |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:show", "update:modelValue", "search", "reset"]); |
| | | |
| | | const buildEmpty = () => { |
| | | const empty = {}; |
| | | props.fields.forEach(field => { |
| | | empty[field.prop] = field.type === "daterange" ? [] : ""; |
| | | }); |
| | | return empty; |
| | | }; |
| | | |
| | | // å¿
é¡»å
ç¨ buildEmpty() éºæ»¡å段ï¼å¼¹å±å³ä½¿éèæ¶ä¹ä¼æ¸²æåèç¹ï¼ |
| | | // daterange ç v-model ä¼å¯¹ local[prop][0] åå¼ï¼local[prop] 为 undefined ä¼ç´æ¥æ¥é |
| | | const local = ref(buildEmpty()); |
| | | |
| | | const syncFromParent = () => { |
| | | const source = props.modelValue || {}; |
| | | const next = buildEmpty(); |
| | | props.fields.forEach(field => { |
| | | const value = source[field.prop]; |
| | | if (field.type === "daterange") { |
| | | next[field.prop] = Array.isArray(value) ? [...value] : []; |
| | | } else if (value !== undefined && value !== null) { |
| | | next[field.prop] = value; |
| | | } |
| | | }); |
| | | local.value = next; |
| | | }; |
| | | |
| | | watch( |
| | | () => props.show, |
| | | show => { |
| | | if (show) syncFromParent(); |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | // fields æ¯å¼æ¥æ¿å°çåºæ¯ï¼è¡¥ä¸æ¬¡éºåºï¼é¿å
åæ®µç¼ºå¤± |
| | | watch( |
| | | () => props.fields, |
| | | () => { |
| | | if (props.show) { |
| | | syncFromParent(); |
| | | } else { |
| | | local.value = buildEmpty(); |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ); |
| | | |
| | | const close = () => { |
| | | emit("update:show", false); |
| | | }; |
| | | |
| | | const search = () => { |
| | | emit("update:modelValue", { ...local.value }); |
| | | emit("search", { ...local.value }); |
| | | close(); |
| | | }; |
| | | |
| | | const reset = () => { |
| | | local.value = buildEmpty(); |
| | | emit("update:modelValue", { ...local.value }); |
| | | emit("reset", { ...local.value }); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .filter-panel { |
| | | background: #ffffff; |
| | | padding: 16px; |
| | | padding-bottom: calc(16px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .filter-panel__header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .filter-panel__title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .filter-panel__body { |
| | | max-height: 55vh; |
| | | padding: 8px 0; |
| | | } |
| | | |
| | | .filter-panel__field { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #f8f8f8; |
| | | } |
| | | |
| | | .filter-panel__label { |
| | | width: 90px; |
| | | flex-shrink: 0; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .filter-panel__control { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .filter-panel__range { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .filter-panel__range-sep { |
| | | flex-shrink: 0; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .filter-panel__footer { |
| | | display: flex; |
| | | gap: 12px; |
| | | padding-top: 16px; |
| | | } |
| | | |
| | | .filter-panel__btn { |
| | | flex: 1; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | éç¨è¡¨å鿩项 |
| | | æ¾å¨ u-form-item é使ç¨ï¼æ¿ä»£å页颿åçãåªè¯» u-input + up-action-sheet/up-datetime-pickerãç»åã |
| | | |
| | | ä¸æï¼ |
| | | <u-form-item label="æå±é¡¹ç®" prop="projectId" required border-bottom> |
| | | <FormPicker v-model="form.projectId" :display-text="form.projectName" |
| | | type="select" title="鿩项ç®" :options="projectOptions" |
| | | @change="onProjectChange" /> |
| | | </u-form-item> |
| | | |
| | | æ¥æï¼ |
| | | <u-form-item label="ç«æ¡æ¶é´" prop="filingDate" required border-bottom> |
| | | <FormPicker v-model="form.filingDate" type="date" placeholder="è¯·éæ©ç«æ¡æ¶é´" /> |
| | | </u-form-item> |
| | | |
| | | options æ¯æä¸ç§åæ³ï¼åå¼ä¼å
çº§ï¼ |
| | | æ¾ç¤ºææ¬ item.name â item.text â item.label â item.displayText |
| | | å®é
å¼ item.value â item.id â item.code |
| | | ä¹å¯ä»¥ç¨ label-key / value-key æ¾å¼æå®ã |
| | | --> |
| | | <template> |
| | | <view class="form-picker"> |
| | | <u-input :modelValue="text" |
| | | :placeholder="placeholder" |
| | | :disabled="disabled" |
| | | :readonly="true" |
| | | :clearable="clearable && !disabled && !readonly" |
| | | @click="open" |
| | | @update:modelValue="onInputUpdate"> |
| | | <template #suffix> |
| | | <u-icon v-if="!disabled && !readonly" |
| | | name="arrow-right" |
| | | color="#c0c4cc" |
| | | @click="open"></u-icon> |
| | | </template> |
| | | </u-input> |
| | | |
| | | <up-action-sheet v-if="type === 'select'" |
| | | :show="showSheet" |
| | | :actions="actions" |
| | | :title="title" |
| | | @select="onSelect" |
| | | @close="showSheet = false"></up-action-sheet> |
| | | |
| | | <up-datetime-picker v-if="type === 'date'" |
| | | :show="showPicker" |
| | | v-model="pickerValue" |
| | | :mode="dateMode" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showPicker = false"></up-datetime-picker> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { parseDate } from "@/utils/warning"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: [String, Number, Boolean, null], |
| | | default: "", |
| | | }, |
| | | // select 䏿 | date æ¥æ |
| | | type: { |
| | | type: String, |
| | | default: "select", |
| | | }, |
| | | // ç´æ¥æå®æ¾ç¤ºææ¬ï¼ä¸ä¼ åæ options 忥 |
| | | displayText: { |
| | | type: String, |
| | | default: "", |
| | | }, |
| | | options: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | title: { |
| | | type: String, |
| | | default: "", |
| | | }, |
| | | placeholder: { |
| | | type: String, |
| | | default: "è¯·éæ©", |
| | | }, |
| | | labelKey: { |
| | | type: String, |
| | | default: "", |
| | | }, |
| | | valueKey: { |
| | | type: String, |
| | | default: "", |
| | | }, |
| | | // æ¥æç²¾åº¦ï¼date | datetime | time | year-month |
| | | dateMode: { |
| | | type: String, |
| | | default: "date", |
| | | }, |
| | | disabled: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | readonly: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | clearable: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "change"]); |
| | | |
| | | const showSheet = ref(false); |
| | | const showPicker = ref(false); |
| | | const pickerValue = ref(Date.now()); |
| | | |
| | | const optionValue = item => |
| | | props.valueKey |
| | | ? item?.[props.valueKey] |
| | | : (item?.value ?? item?.id ?? item?.code); |
| | | |
| | | const optionLabel = item => |
| | | props.labelKey |
| | | ? item?.[props.labelKey] |
| | | : (item?.name ?? item?.text ?? item?.label ?? item?.displayText ?? ""); |
| | | |
| | | const actions = computed(() => |
| | | (props.options || []).map(item => ({ |
| | | text: String(optionLabel(item) ?? ""), |
| | | value: optionValue(item), |
| | | data: item, |
| | | })) |
| | | ); |
| | | |
| | | const text = computed(() => { |
| | | if (props.displayText) return props.displayText; |
| | | if (props.modelValue === undefined || props.modelValue === null) return ""; |
| | | if (props.type === "date") return String(props.modelValue); |
| | | const matched = (props.options || []).find( |
| | | item => String(optionValue(item)) === String(props.modelValue) |
| | | ); |
| | | return matched ? String(optionLabel(matched)) : ""; |
| | | }); |
| | | |
| | | const open = () => { |
| | | if (props.disabled || props.readonly) return; |
| | | if (props.type === "select") { |
| | | showSheet.value = true; |
| | | return; |
| | | } |
| | | const base = parseDate(props.modelValue); |
| | | pickerValue.value = base ? base.getTime() : Date.now(); |
| | | showPicker.value = true; |
| | | }; |
| | | |
| | | // ç¨æ·ç¹å» u-input çæ¸
餿鮿¶ï¼æç©ºå¼åæ¥åºå» |
| | | const onInputUpdate = value => { |
| | | if (value !== "" && value !== undefined) return; |
| | | emit("update:modelValue", ""); |
| | | emit("change", "", null); |
| | | }; |
| | | |
| | | const onSelect = item => { |
| | | showSheet.value = false; |
| | | emit("update:modelValue", item.value); |
| | | emit("change", item.value, item.data); |
| | | }; |
| | | |
| | | const onDateConfirm = e => { |
| | | showPicker.value = false; |
| | | const value = formatDateToYMD(e.value); |
| | | emit("update:modelValue", value); |
| | | emit("change", value, null); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .form-picker { |
| | | width: 100%; |
| | | } |
| | | </style> |
| | |
| | | "navigationBarTitleText": "å
¬åéç¥", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/projectManagement/project/index", |
| | | "style": { |
| | | "navigationBarTitleText": "项ç®ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/projectManagement/project/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "项ç®è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/projectManagement/project/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "项ç®ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/projectManagement/project/archive", |
| | | "style": { |
| | | "navigationBarTitleText": "ææ¡£å½æ¡£", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/projectManagement/project/progress", |
| | | "style": { |
| | | "navigationBarTitleText": "è¿åº¦æ±æ¥", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/projectManagement/project/config", |
| | | "style": { |
| | | "navigationBarTitleText": "é¢è¦é
ç½®", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/biddingManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æææ 管ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/biddingManagement/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "æææ 详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/biddingManagement/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "æææ ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/lawsuitManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "è¯è®¼ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/lawsuitManagement/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "è¯è®¼è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/lawsuitManagement/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "è¯è®¼ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/deliveryLedger/index", |
| | | "style": { |
| | | "navigationBarTitleText": "åè´§å°è´¦", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/deliveryLedger/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "å货详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/deliveryLedger/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "è¡¥å
åè´§ä¿¡æ¯", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/contractManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "åå管ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/contractManagement/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "åå详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/sales/contractManagement/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "ååç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/qualityManagement/inspectionReport/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¥è¡¨ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/qualityManagement/inspectionReport/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¥å详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/qualityManagement/inspectionReport/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¥åç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/vehicleInfo/index", |
| | | "style": { |
| | | "navigationBarTitleText": "车è¾ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/vehicleInfo/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "车è¾è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/vehicleInfo/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "车è¾ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/fuelCard/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å æ²¹å¡ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/fuelCard/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "å æ²¹å¡è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/fuelCard/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "å æ²¹å¡ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/fuelCard/recharge", |
| | | "style": { |
| | | "navigationBarTitleText": "å æ²¹å¡å
å¼", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/fuelRecord/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å æ²¹è®°å½", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/fuelRecord/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "å æ²¹è®°å½è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/inventoryManagement/fuelRecord/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "å æ²¹è®°å½ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/cooperativeOffice/broadbandCommunication/index", |
| | | "style": { |
| | | "navigationBarTitleText": "宽带ä¸é信管ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/cooperativeOffice/broadbandCommunication/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "宽带ä¸é信详æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/cooperativeOffice/broadbandCommunication/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "宽带ä¸éä¿¡ç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/cooperativeOffice/houseLeaseManagement/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¿å±ç§èµç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/cooperativeOffice/houseLeaseManagement/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¿å±ç§èµè¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/cooperativeOffice/houseLeaseManagement/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "æ¿å±ç§èµç¼è¾", |
| | | "navigationStyle": "custom" |
| | | } |
| | | } |
| | | ], |
| | | "subPackages": [ |
| | |
| | | customerType: [{ required: true, message: "è¯·éæ©å®¢æ·åç±»", trigger: "change" }], |
| | | }; |
| | | |
| | | // ä¸å端/管ç端åå¼ä¿æä¸è´ï¼ç®¡çç«¯ä¸æä¸ºãé¶å®å®¢æ· / è¿éå客æ·ãï¼ |
| | | const customerTypeActions = [ |
| | | { name: "é¶å®å®¢æ·", value: "é¶å®å®¢æ·" }, |
| | | { name: "ç»éå客æ·", value: "ç»éå客æ·" }, |
| | | { name: "è¿éå客æ·", value: "è¿éå客æ·" }, |
| | | ]; |
| | | |
| | | const pageTitle = computed(() => |
| | |
| | | const customerName = ref(""); |
| | | const list = ref([]); |
| | | |
| | | // 客æ·åç±»åå¼å¿
é¡»ä¸å端ä¸è´ï¼ç®¡çç«¯ä¸æä¸ºãé¶å®å®¢æ· / è¿éå客æ·ãï¼ï¼ |
| | | // åæãç»éå客æ·ãä¼çä¸åºä»»ä½æ°æ® |
| | | const tabList = reactive([ |
| | | { name: "å
¨é¨å®¢æ·", value: "" }, |
| | | { name: "é¶å®å®¢æ·", value: "é¶å®å®¢æ·" }, |
| | | { name: "ç»éå客æ·", value: "ç»éå客æ·" }, |
| | | { name: "è¿éå客æ·", value: "è¿éå客æ·" }, |
| | | ]); |
| | | const tabValue = ref(0); |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="broadband-detail"> |
| | | <PageHeader title="宽带ä¸é信详æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ detail.office || "-" }}</text> |
| | | <up-tag :text="warn.text || 'æ ç¼´è´¹æ¥æ'" |
| | | :type="warnTagType(warn.level)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <view v-if="warn.text" |
| | | class="warn-banner" |
| | | :style="{ background: warn.level === 'normal' ? '#f0f9eb' : '#fef0f0' }"> |
| | | <text class="warn-banner-text" |
| | | :style="{ color: warn.color }">ç¼´è´¹ç¶æï¼{{ warn.text }}ï¼ä¸æ¬¡ç¼´è´¹ {{ detail.nextPaymentDate || "-" }}ï¼</text> |
| | | </view> |
| | | |
| | | <u-cell-group title="å®è£
ä½ç½®" |
| | | class="detail-section"> |
| | | <u-cell title="åºå" |
| | | :value="detail.area || '-'" /> |
| | | <u-cell title="åäºå¤" |
| | | :value="detail.office || '-'" /> |
| | | <u-cell title="é©»ç人å" |
| | | :value="detail.residentStaff || '-'" /> |
| | | <u-cell title="å°å" |
| | | :value="detail.address || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å®è£
ä¸ç¼´è´¹" |
| | | class="detail-section"> |
| | | <u-cell title="å®è£
æ¥æ" |
| | | :value="detail.installDate || '-'" /> |
| | | <u-cell title="å®è£
è´¹" |
| | | :value="formatAmount(detail.installFee)" /> |
| | | <u-cell title="ç¼´è´¹å¨æ" |
| | | :value="cycleText(detail.paymentCycle)" /> |
| | | <u-cell title="ç¼´è´¹æ¹å¼" |
| | | :value="detail.paymentMethod || '-'" /> |
| | | <u-cell title="é¢å" |
| | | :value="formatAmount(detail.prepaidAmount)" /> |
| | | <u-cell title="èµè´¹" |
| | | :value="formatAmount(detail.monthlyFee)" /> |
| | | <u-cell title="䏿¬¡ç¼´è´¹æ¥æ" |
| | | :value="detail.nextPaymentDate || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å¥é¤å
容" |
| | | class="detail-section"> |
| | | <u-cell title="å¥é¤å
容" |
| | | :value="detail.packageContent || '-'" /> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { getWarnInfo } from "@/utils/warning"; |
| | | import { cycleText } from "@/utils/paymentCycle"; |
| | | import { getBroadbandDetail } from "@/api/collaborativeApproval/broadbandCommunication"; |
| | | |
| | | // 宽另尿æå 15 天é¢è¦ |
| | | const WARN_DAYS = 15; |
| | | |
| | | const loading = ref(true); |
| | | const broadbandId = ref(""); |
| | | const detail = ref({}); |
| | | |
| | | const warn = computed(() => |
| | | getWarnInfo(detail.value.nextPaymentDate, WARN_DAYS) |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("broadbandDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/cooperativeOffice/broadbandCommunication/edit?id=${broadbandId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const warnTagType = level => { |
| | | if (level === "overdue") return "error"; |
| | | if (level === "warning") return "warning"; |
| | | if (level === "normal") return "success"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const data = await loadDetailData({ |
| | | cacheKey: "broadbandDetail", |
| | | id: broadbandId.value, |
| | | fetcher: getBroadbandDetail, |
| | | }); |
| | | detail.value = data || {}; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·å宽带ä¸é信详æ
失败", icon: "none" }); |
| | | detail.value = {}; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | broadbandId.value = options?.id || ""; |
| | | if (!broadbandId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .broadband-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #f4f1ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(124, 92, 252, 0.1); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .warn-banner { |
| | | margin-top: 12px; |
| | | padding: 10px 14px; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | .warn-banner-text { |
| | | font-size: 13px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="å®è£
ä½ç½®" |
| | | class="form-section"> |
| | | <up-form-item label="åºå" |
| | | prop="area" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.area" |
| | | placeholder="请è¾å
¥åºå" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="åäºå¤" |
| | | prop="office" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.office" |
| | | placeholder="请è¾å
¥åäºå¤" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="é©»ç人å" |
| | | prop="residentStaff" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.residentStaff" |
| | | placeholder="请è¾å
¥é©»ç人å" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="å°å" |
| | | prop="address" |
| | | required> |
| | | <up-textarea v-model="form.address" |
| | | placeholder="请è¾å
¥è¯¦ç»å°å" |
| | | auto-height |
| | | maxlength="200" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å®è£
ä¸ç¼´è´¹" |
| | | class="form-section"> |
| | | <up-form-item label="å®è£
æ¥æ" |
| | | prop="installDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.installDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©å®è£
æ¥æ" |
| | | @change="calculateNextPayment" /> |
| | | </up-form-item> |
| | | <up-form-item label="å®è£
è´¹" |
| | | prop="installFee" |
| | | border-bottom> |
| | | <up-input v-model="form.installFee" |
| | | type="number" |
| | | placeholder="请è¾å
¥å®è£
è´¹" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¼´è´¹å¨æ" |
| | | prop="paymentCycle" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.paymentCycle" |
| | | type="select" |
| | | title="éæ©ç¼´è´¹å¨æ" |
| | | :options="paymentCycleOptions" |
| | | placeholder="è¯·éæ©ç¼´è´¹å¨æ" |
| | | @change="calculateNextPayment" /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¼´è´¹æ¹å¼" |
| | | prop="paymentMethod" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.paymentMethod" |
| | | type="select" |
| | | title="éæ©ç¼´è´¹æ¹å¼" |
| | | :options="paymentMethodOptions" |
| | | placeholder="è¯·éæ©ç¼´è´¹æ¹å¼" /> |
| | | </up-form-item> |
| | | <up-form-item label="é¢å" |
| | | prop="prepaidAmount" |
| | | border-bottom> |
| | | <up-input v-model="form.prepaidAmount" |
| | | type="number" |
| | | placeholder="请è¾å
¥é¢åéé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="èµè´¹" |
| | | prop="monthlyFee" |
| | | border-bottom> |
| | | <up-input v-model="form.monthlyFee" |
| | | type="number" |
| | | placeholder="请è¾å
¥èµè´¹" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="䏿¬¡ç¼´è´¹æ¥æ" |
| | | prop="nextPaymentDate"> |
| | | <up-input v-model="form.nextPaymentDate" |
| | | placeholder="æå®è£
æ¥æä¸ç¼´è´¹å¨æèªå¨è®¡ç®" |
| | | disabled /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å¥é¤å
容" |
| | | class="form-section"> |
| | | <up-form-item label="å¥é¤å
容" |
| | | prop="packageContent"> |
| | | <up-textarea v-model="form.packageContent" |
| | | placeholder="请è¾å
¥å¥é¤å
容" |
| | | auto-height |
| | | maxlength="500" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </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 FormPicker from "@/components/FormPicker.vue"; |
| | | import { addMonths } from "@/utils/warning"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { |
| | | PAYMENT_CYCLE_OPTIONS, |
| | | PAYMENT_METHOD_OPTIONS, |
| | | } from "@/utils/paymentCycle"; |
| | | import { |
| | | addOrUpdateBroadband, |
| | | getBroadbandDetail, |
| | | } from "@/api/collaborativeApproval/broadbandCommunication"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const broadbandId = ref(""); |
| | | |
| | | const paymentCycleOptions = PAYMENT_CYCLE_OPTIONS; |
| | | const paymentMethodOptions = PAYMENT_METHOD_OPTIONS; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | area: "", |
| | | office: "", |
| | | residentStaff: "", |
| | | address: "", |
| | | installDate: "", |
| | | installFee: "", |
| | | paymentCycle: "", |
| | | paymentMethod: "", |
| | | prepaidAmount: "", |
| | | monthlyFee: "", |
| | | packageContent: "", |
| | | nextPaymentDate: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | area: [{ required: true, message: "请è¾å
¥åºå", trigger: "blur" }], |
| | | office: [{ required: true, message: "请è¾å
¥åäºå¤", trigger: "blur" }], |
| | | residentStaff: [ |
| | | { required: true, message: "请è¾å
¥é©»ç人å", trigger: "blur" }, |
| | | ], |
| | | address: [{ required: true, message: "请è¾å
¥å°å", trigger: "blur" }], |
| | | installDate: [ |
| | | { required: true, message: "è¯·éæ©å®è£
æ¥æ", trigger: "change" }, |
| | | ], |
| | | paymentCycle: [ |
| | | { required: true, message: "è¯·éæ©ç¼´è´¹å¨æ", trigger: "change" }, |
| | | ], |
| | | paymentMethod: [ |
| | | { required: true, message: "è¯·éæ©ç¼´è´¹æ¹å¼", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => |
| | | broadbandId.value ? "ç¼è¾å®½å¸¦ä¸éä¿¡" : "æ°å¢å®½å¸¦ä¸éä¿¡" |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const numOrNull = value => |
| | | value === "" || value === null || value === undefined |
| | | ? null |
| | | : Number.isNaN(Number(value)) |
| | | ? null |
| | | : Number(value); |
| | | |
| | | // 䏿¬¡ç¼´è´¹æ¥æ = å®è£
æ¥æ + ç¼´è´¹å¨ææï¼åªè¯» |
| | | const calculateNextPayment = () => { |
| | | const months = Number(form.value.paymentCycle); |
| | | form.value.nextPaymentDate = |
| | | form.value.installDate && Number.isFinite(months) && months > 0 |
| | | ? addMonths(form.value.installDate, months) |
| | | : ""; |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "broadbandDetail", |
| | | id: broadbandId.value, |
| | | fetcher: getBroadbandDetail, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "æªæ¾å°å®½å¸¦ä¸éä¿¡ä¿¡æ¯", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | form.value = { |
| | | id: data.id, |
| | | area: data.area || "", |
| | | office: data.office || "", |
| | | residentStaff: data.residentStaff || "", |
| | | address: data.address || "", |
| | | installDate: (data.installDate || "").slice(0, 10), |
| | | installFee: data.installFee ?? "", |
| | | paymentCycle: data.paymentCycle ?? "", |
| | | paymentMethod: data.paymentMethod || "", |
| | | prepaidAmount: data.prepaidAmount ?? "", |
| | | monthlyFee: data.monthlyFee ?? "", |
| | | packageContent: data.packageContent || "", |
| | | nextPaymentDate: (data.nextPaymentDate || "").slice(0, 10), |
| | | }; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | calculateNextPayment(); |
| | | const payload = { |
| | | id: form.value.id, |
| | | area: form.value.area, |
| | | office: form.value.office, |
| | | residentStaff: form.value.residentStaff, |
| | | address: form.value.address, |
| | | installDate: form.value.installDate, |
| | | installFee: numOrNull(form.value.installFee), |
| | | paymentCycle: numOrNull(form.value.paymentCycle), |
| | | paymentMethod: form.value.paymentMethod, |
| | | prepaidAmount: numOrNull(form.value.prepaidAmount), |
| | | monthlyFee: numOrNull(form.value.monthlyFee), |
| | | packageContent: form.value.packageContent, |
| | | nextPaymentDate: form.value.nextPaymentDate || null, |
| | | }; |
| | | |
| | | loading.value = true; |
| | | addOrUpdateBroadband(payload) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 1000); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | broadbandId.value = options.id; |
| | | form.value.id = options.id; |
| | | loadDetail(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.area" |
| | | placeholder="请è¾å
¥åºåæç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="broadbandList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in broadbandList" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="wifi" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.office || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="warnOf(item).text || 'æ ç¼´è´¹æ¥æ'" |
| | | :type="warnTagType(warnOf(item).level)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºå</text> |
| | | <text class="detail-value">{{ item.area || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">é©»ç人å</text> |
| | | <text class="detail-value">{{ item.residentStaff || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å°å</text> |
| | | <text class="detail-value">{{ item.address || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®è£
æ¥æ</text> |
| | | <text class="detail-value">{{ item.installDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®è£
è´¹</text> |
| | | <text class="detail-value">{{ formatAmount(item.installFee) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç¼´è´¹å¨æ</text> |
| | | <text class="detail-value">{{ cycleText(item.paymentCycle) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç¼´è´¹æ¹å¼</text> |
| | | <text class="detail-value">{{ item.paymentMethod || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">èµè´¹</text> |
| | | <text class="detail-value">{{ formatAmount(item.monthlyFee) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">䏿¬¡ç¼´è´¹æ¥æ</text> |
| | | <text class="detail-value">{{ item.nextPaymentDate || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ å®½å¸¦ä¸éä¿¡æ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="宽带ä¸éä¿¡çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | broadbandListPage, |
| | | delBroadband, |
| | | } from "@/api/collaborativeApproval/broadbandCommunication"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { getWarnInfo } from "@/utils/warning"; |
| | | import { cycleText } from "@/utils/paymentCycle"; |
| | | |
| | | // 宽另尿æå 15 天é¢è¦ |
| | | const WARN_DAYS = 15; |
| | | |
| | | const WARN_STATUS_OPTIONS = [ |
| | | { name: "æ£å¸¸", value: "normal" }, |
| | | { name: "å³å°å°æ", value: "expiring" }, |
| | | { name: "已龿", value: "overdue" }, |
| | | ]; |
| | | |
| | | const broadbandList = ref([]); |
| | | const showFilter = ref(false); |
| | | |
| | | const query = reactive({ |
| | | area: "", |
| | | office: "", |
| | | residentStaff: "", |
| | | warnStatus: "", |
| | | }); |
| | | |
| | | const filterFields = [ |
| | | { prop: "area", label: "åºå", type: "input", placeholder: "请è¾å
¥åºå" }, |
| | | { prop: "office", label: "åäºå¤", type: "input", placeholder: "请è¾å
¥åäºå¤" }, |
| | | { |
| | | prop: "residentStaff", |
| | | label: "é©»ç人å", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥é©»ç人å", |
| | | }, |
| | | { |
| | | prop: "warnStatus", |
| | | label: "é¢è¦ç¶æ", |
| | | type: "select", |
| | | options: WARN_STATUS_OPTIONS, |
| | | }, |
| | | ]; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("broadbandDetail"); |
| | | uni.navigateTo({ |
| | | url: "/pages/cooperativeOffice/broadbandCommunication/edit", |
| | | }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("broadbandDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/cooperativeOffice/broadbandCommunication/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("broadbandDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/cooperativeOffice/broadbandCommunication/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const warnOf = item => getWarnInfo(item?.nextPaymentDate, WARN_DAYS); |
| | | |
| | | const warnTagType = level => { |
| | | if (level === "overdue") return "error"; |
| | | if (level === "warning") return "warning"; |
| | | if (level === "normal") return "success"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const getList = () => { |
| | | broadbandListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | area: query.area || undefined, |
| | | office: query.office || undefined, |
| | | residentStaff: query.residentStaff || undefined, |
| | | warnStatus: query.warnStatus || undefined, |
| | | }) |
| | | .then(res => { |
| | | broadbandList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥å®½å¸¦ä¸éä¿¡è®°å½åï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delBroadband(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="house-lease-detail"> |
| | | <PageHeader title="æ¿å±ç§èµè¯¦æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <view class="hero-left"> |
| | | <text class="hero-title">{{ detail.office || "-" }}</text> |
| | | <text class="hero-sub">{{ detail.address || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="warn.text || 'æ å°ææ¥'" |
| | | :type="warnTagType(warn.level)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <view v-if="warn.text" |
| | | class="warn-banner" |
| | | :style="{ background: warn.level === 'normal' ? '#f0f9eb' : '#fef0f0' }"> |
| | | <text class="warn-banner-text" |
| | | :style="{ color: warn.color }">ååç¶æï¼{{ warn.text }}ï¼å°ææ¥ {{ detail.contractEndDate || "-" }}ï¼</text> |
| | | </view> |
| | | |
| | | <u-cell-group title="æ¿å±ä¸äººå" |
| | | class="detail-section"> |
| | | <u-cell title="åäºå¤" |
| | | :value="detail.office || '-'" /> |
| | | <u-cell title="使ç¨äºº" |
| | | :value="detail.userName || '-'" /> |
| | | <u-cell title="é©»ç人å" |
| | | :value="detail.residentStaff || '-'" /> |
| | | <u-cell title="å°å" |
| | | :value="detail.address || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="ååä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="èµ·ç§æ¶é´" |
| | | :value="detail.rentStartDate || '-'" /> |
| | | <u-cell title="ååèµ·ç§æ¥" |
| | | :value="detail.contractStartDate || '-'" /> |
| | | <u-cell title="ååå°ææ¥" |
| | | :value="detail.contractEndDate || '-'" /> |
| | | <u-cell title="æ¿ç§äºº" |
| | | :value="detail.tenant || '-'" /> |
| | | <u-cell title="ååç²æ¹" |
| | | :value="detail.partyA || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="è´¹ç¨ä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="çµè¯" |
| | | :value="detail.phone || '-'" /> |
| | | <u-cell title="é¢ç§¯" |
| | | :value="detail.area != null && detail.area !== '' ? String(detail.area) : '-'" /> |
| | | <u-cell title="æ¯ä»æ¹å¼" |
| | | :value="detail.paymentMethod || '-'" /> |
| | | <u-cell title="æ¼é" |
| | | :value="formatAmount(detail.deposit)" /> |
| | | <u-cell title="ç§é(å
/æ)" |
| | | :value="formatAmount(detail.rent)" /> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | |
| | | <view v-if="!loading" |
| | | class="cost-section"> |
| | | <view class="cost-header"> |
| | | <text class="cost-header-title">æåº¦è´¹ç¨</text> |
| | | <text class="cost-header-tip">{{ costList.length }} 个æ</text> |
| | | </view> |
| | | <scroll-view v-if="costList.length > 0" |
| | | scroll-x |
| | | class="cost-scroll"> |
| | | <view class="cost-table"> |
| | | <view class="cost-row cost-head"> |
| | | <text class="cost-cell col-month">æä»½</text> |
| | | <text class="cost-cell">æ¿ç§</text> |
| | | <text class="cost-cell">æ°´çµ</text> |
| | | <text class="cost-cell">ç©ä¸</text> |
| | | <text class="cost-cell">å¿«é</text> |
| | | <text class="cost-cell">宽带</text> |
| | | <text class="cost-cell">æå计</text> |
| | | </view> |
| | | <view v-for="row in costList" |
| | | :key="row.month" |
| | | class="cost-row"> |
| | | <text class="cost-cell col-month">{{ row.month }}</text> |
| | | <text class="cost-cell">{{ money(row.rent) }}</text> |
| | | <text class="cost-cell">{{ money(row.waterElectricity) }}</text> |
| | | <text class="cost-cell">{{ money(row.property) }}</text> |
| | | <text class="cost-cell">{{ money(row.express) }}</text> |
| | | <text class="cost-cell">{{ money(row.broadband) }}</text> |
| | | <text class="cost-cell cost-total">{{ rowTotal(row) }}</text> |
| | | </view> |
| | | <view class="cost-row cost-foot"> |
| | | <text class="cost-cell col-month">累计</text> |
| | | <text class="cost-cell">{{ totals.rent }}</text> |
| | | <text class="cost-cell">{{ totals.waterElectricity }}</text> |
| | | <text class="cost-cell">{{ totals.property }}</text> |
| | | <text class="cost-cell">{{ totals.express }}</text> |
| | | <text class="cost-cell">{{ totals.broadband }}</text> |
| | | <text class="cost-cell cost-total">{{ totals.total }}</text> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | <view v-else |
| | | class="cost-empty"> |
| | | <text>ææ æåº¦è´¹ç¨æ°æ®</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { getWarnInfo } from "@/utils/warning"; |
| | | import { getHouseLeaseDetail } from "@/api/collaborativeApproval/houseLeaseManagement"; |
| | | |
| | | // ååå°ææå 30 天é¢è¦ |
| | | const WARN_DAYS = 30; |
| | | |
| | | const COST_FIELDS = [ |
| | | "rent", |
| | | "waterElectricity", |
| | | "property", |
| | | "express", |
| | | "broadband", |
| | | ]; |
| | | |
| | | const loading = ref(true); |
| | | const leaseId = ref(""); |
| | | const detail = ref({}); |
| | | const costList = ref([]); |
| | | |
| | | const warn = computed(() => |
| | | getWarnInfo(detail.value.contractEndDate, WARN_DAYS) |
| | | ); |
| | | |
| | | const money = value => |
| | | value === null || value === undefined || value === "" |
| | | ? "-" |
| | | : Number(value).toFixed(2); |
| | | |
| | | const rowTotal = row => |
| | | COST_FIELDS.reduce((sum, key) => sum + (Number(row[key]) || 0), 0).toFixed(2); |
| | | |
| | | const totals = computed(() => { |
| | | const acc = { total: 0 }; |
| | | COST_FIELDS.forEach(key => { |
| | | acc[key] = 0; |
| | | }); |
| | | costList.value.forEach(row => { |
| | | COST_FIELDS.forEach(key => { |
| | | acc[key] += Number(row[key]) || 0; |
| | | }); |
| | | acc.total += Number(rowTotal(row)) || 0; |
| | | }); |
| | | const shown = { total: acc.total.toFixed(2) }; |
| | | COST_FIELDS.forEach(key => { |
| | | shown[key] = acc[key].toFixed(2); |
| | | }); |
| | | return shown; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("houseLeaseDetail", { |
| | | ...detail.value, |
| | | costList: costList.value, |
| | | }); |
| | | uni.navigateTo({ |
| | | url: `/pages/cooperativeOffice/houseLeaseManagement/edit?id=${leaseId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const warnTagType = level => { |
| | | if (level === "overdue") return "error"; |
| | | if (level === "warning") return "warning"; |
| | | if (level === "normal") return "success"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const data = await loadDetailData({ |
| | | cacheKey: "houseLeaseDetail", |
| | | id: leaseId.value, |
| | | fetcher: getHouseLeaseDetail, |
| | | }); |
| | | detail.value = data || {}; |
| | | const rows = data?.costList || data?.costVOs || []; |
| | | costList.value = Array.isArray(rows) ? rows : []; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·åæ¿å±ç§èµè¯¦æ
失败", icon: "none" }); |
| | | detail.value = {}; |
| | | costList.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | leaseId.value = options?.id || ""; |
| | | if (!leaseId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .house-lease-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eaf4ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(76, 154, 255, 0.1); |
| | | } |
| | | |
| | | .hero-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | max-width: 70%; |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | } |
| | | |
| | | .hero-sub { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .warn-banner { |
| | | margin-top: 12px; |
| | | padding: 10px 14px; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | .warn-banner-text { |
| | | font-size: 13px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | |
| | | .cost-section { |
| | | margin: 12px 12px 0; |
| | | border-radius: 12px; |
| | | background: #ffffff; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .cost-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 14px 16px 10px; |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | .cost-header-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .cost-header-tip { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .cost-scroll { |
| | | width: 100%; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .cost-table { |
| | | display: inline-block; |
| | | min-width: 100%; |
| | | padding-bottom: 6px; |
| | | } |
| | | |
| | | .cost-row { |
| | | display: flex; |
| | | align-items: center; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .cost-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .cost-head { |
| | | background: #fafbfc; |
| | | } |
| | | |
| | | .cost-head .cost-cell { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: #606266; |
| | | } |
| | | |
| | | .cost-foot { |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | .cost-foot .cost-cell { |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .cost-cell { |
| | | width: 76px; |
| | | flex: 0 0 76px; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | color: #333; |
| | | line-height: 44px; |
| | | } |
| | | |
| | | .col-month { |
| | | width: 78px; |
| | | flex: 0 0 78px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .cost-total { |
| | | color: #2979ff; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .cost-empty { |
| | | padding: 24px 16px; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="æ¿å±ä¸äººå" |
| | | class="form-section"> |
| | | <up-form-item label="åäºå¤" |
| | | prop="office" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.office" |
| | | placeholder="请è¾å
¥åäºå¤" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="使ç¨äºº" |
| | | prop="userName" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.userName" |
| | | placeholder="请è¾å
¥ä½¿ç¨äºº" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="é©»ç人å" |
| | | prop="residentStaff" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.residentStaff" |
| | | placeholder="请è¾å
¥é©»ç人å" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="å°å" |
| | | prop="address" |
| | | required> |
| | | <up-textarea v-model="form.address" |
| | | placeholder="请è¾å
¥è¯¦ç»å°å" |
| | | auto-height |
| | | maxlength="200" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="ååä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="èµ·ç§æ¶é´" |
| | | prop="rentStartDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.rentStartDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©èµ·ç§æ¶é´" /> |
| | | </up-form-item> |
| | | <up-form-item label="ååèµ·ç§æ¥" |
| | | prop="contractStartDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.contractStartDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ååèµ·ç§æ¥" |
| | | @change="rebuildMonths" /> |
| | | </up-form-item> |
| | | <up-form-item label="ååå°ææ¥" |
| | | prop="contractEndDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.contractEndDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ååå°ææ¥" |
| | | @change="rebuildMonths" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¿ç§äºº" |
| | | prop="tenant" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.tenant" |
| | | placeholder="请è¾å
¥æ¿ç§äºº" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ååç²æ¹" |
| | | prop="partyA" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.partyA" |
| | | placeholder="请è¾å
¥ååç²æ¹" |
| | | clearable /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="è´¹ç¨ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="çµè¯" |
| | | prop="phone" |
| | | border-bottom> |
| | | <up-input v-model="form.phone" |
| | | type="number" |
| | | placeholder="请è¾å
¥èç³»çµè¯" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="é¢ç§¯" |
| | | prop="area" |
| | | border-bottom> |
| | | <up-input v-model="form.area" |
| | | type="number" |
| | | placeholder="请è¾å
¥é¢ç§¯" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯ä»æ¹å¼" |
| | | prop="paymentMethod" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.paymentMethod" |
| | | type="select" |
| | | title="éæ©æ¯ä»æ¹å¼" |
| | | :options="paymentMethodOptions" |
| | | placeholder="è¯·éæ©æ¯ä»æ¹å¼" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¼é" |
| | | prop="deposit" |
| | | border-bottom> |
| | | <up-input v-model="form.deposit" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ¼é" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç§é(å
/æ)" |
| | | prop="rent" |
| | | required> |
| | | <up-input v-model="form.rent" |
| | | type="number" |
| | | placeholder="请è¾å
¥ç§é" |
| | | clearable |
| | | @change="applyRentToMonths" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <view class="cost-section"> |
| | | <view class="cost-header"> |
| | | <text class="cost-header-title">æåº¦è´¹ç¨</text> |
| | | <text class="cost-header-tip">{{ costHint }}</text> |
| | | </view> |
| | | <scroll-view v-if="costList.length > 0" |
| | | scroll-x |
| | | class="cost-scroll"> |
| | | <view class="cost-table"> |
| | | <view class="cost-row cost-head"> |
| | | <text class="cost-cell col-month">æä»½</text> |
| | | <text class="cost-cell">æ¿ç§</text> |
| | | <text class="cost-cell">æ°´çµ</text> |
| | | <text class="cost-cell">ç©ä¸</text> |
| | | <text class="cost-cell">å¿«é</text> |
| | | <text class="cost-cell">宽带</text> |
| | | <text class="cost-cell">æå计</text> |
| | | </view> |
| | | <view v-for="row in costList" |
| | | :key="row.month" |
| | | class="cost-row"> |
| | | <text class="cost-cell col-month">{{ row.month }}</text> |
| | | <view class="cost-cell"> |
| | | <up-input v-model="row.rent" |
| | | type="number" |
| | | border="none" |
| | | input-align="center" /> |
| | | </view> |
| | | <view class="cost-cell"> |
| | | <up-input v-model="row.waterElectricity" |
| | | type="number" |
| | | border="none" |
| | | input-align="center" /> |
| | | </view> |
| | | <view class="cost-cell"> |
| | | <up-input v-model="row.property" |
| | | type="number" |
| | | border="none" |
| | | input-align="center" /> |
| | | </view> |
| | | <view class="cost-cell"> |
| | | <up-input v-model="row.express" |
| | | type="number" |
| | | border="none" |
| | | input-align="center" /> |
| | | </view> |
| | | <view class="cost-cell"> |
| | | <up-input v-model="row.broadband" |
| | | type="number" |
| | | border="none" |
| | | input-align="center" /> |
| | | </view> |
| | | <text class="cost-cell cost-total">{{ rowTotal(row) }}</text> |
| | | </view> |
| | | <view class="cost-row cost-foot"> |
| | | <text class="cost-cell col-month">累计</text> |
| | | <text class="cost-cell">{{ totals.rent }}</text> |
| | | <text class="cost-cell">{{ totals.waterElectricity }}</text> |
| | | <text class="cost-cell">{{ totals.property }}</text> |
| | | <text class="cost-cell">{{ totals.express }}</text> |
| | | <text class="cost-cell">{{ totals.broadband }}</text> |
| | | <text class="cost-cell cost-total">{{ totals.total }}</text> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | <view v-else |
| | | class="cost-empty"> |
| | | <text>{{ costHint }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </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 FormPicker from "@/components/FormPicker.vue"; |
| | | import { monthRange } from "@/utils/warning"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { PAYMENT_METHOD_OPTIONS } from "@/utils/paymentCycle"; |
| | | import { |
| | | addOrUpdateHouseLease, |
| | | getHouseLeaseDetail, |
| | | } from "@/api/collaborativeApproval/houseLeaseManagement"; |
| | | |
| | | // æåº¦è´¹ç¨äºä¸ªéé¢åï¼é¡ºåºä¸è¡¨å¤´ä¸è´ |
| | | const COST_FIELDS = [ |
| | | "rent", |
| | | "waterElectricity", |
| | | "property", |
| | | "express", |
| | | "broadband", |
| | | ]; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const leaseId = ref(""); |
| | | const costList = ref([]); |
| | | |
| | | const paymentMethodOptions = PAYMENT_METHOD_OPTIONS; |
| | | |
| | | // è®°å½ä¸ä¸æ¬¡èªå¨çå
¥çç§éï¼ç¨äºå¤ææææ¿ç§æ¯ãèªå¨å¸¦åºãè¿æ¯ãææ¹è¿ã |
| | | const lastAppliedRent = ref(""); |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | office: "", |
| | | userName: "", |
| | | residentStaff: "", |
| | | address: "", |
| | | rentStartDate: "", |
| | | contractStartDate: "", |
| | | contractEndDate: "", |
| | | tenant: "", |
| | | partyA: "", |
| | | phone: "", |
| | | area: "", |
| | | paymentMethod: "", |
| | | deposit: "", |
| | | rent: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | office: [{ required: true, message: "请è¾å
¥åäºå¤", trigger: "blur" }], |
| | | userName: [{ required: true, message: "请è¾å
¥ä½¿ç¨äºº", trigger: "blur" }], |
| | | residentStaff: [ |
| | | { required: true, message: "请è¾å
¥é©»ç人å", trigger: "blur" }, |
| | | ], |
| | | address: [{ required: true, message: "请è¾å
¥å°å", trigger: "blur" }], |
| | | rentStartDate: [ |
| | | { required: true, message: "è¯·éæ©èµ·ç§æ¶é´", trigger: "change" }, |
| | | ], |
| | | contractStartDate: [ |
| | | { required: true, message: "è¯·éæ©ååèµ·ç§æ¥", trigger: "change" }, |
| | | ], |
| | | contractEndDate: [ |
| | | { required: true, message: "è¯·éæ©ååå°ææ¥", trigger: "change" }, |
| | | ], |
| | | tenant: [{ required: true, message: "请è¾å
¥æ¿ç§äºº", trigger: "blur" }], |
| | | partyA: [{ required: true, message: "请è¾å
¥ååç²æ¹", trigger: "blur" }], |
| | | paymentMethod: [ |
| | | { required: true, message: "è¯·éæ©æ¯ä»æ¹å¼", trigger: "change" }, |
| | | ], |
| | | rent: [{ required: true, message: "请è¾å
¥ç§é", trigger: "blur" }], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => |
| | | leaseId.value ? "ç¼è¾æ¿å±ç§èµ" : "æ°å¢æ¿å±ç§èµ" |
| | | ); |
| | | |
| | | const costHint = computed(() => { |
| | | if (!form.value.contractStartDate || !form.value.contractEndDate) { |
| | | return "请å
éæ©ååèµ·ç§æ¥ä¸ååå°ææ¥ï¼ç³»ç»ææä»½èªå¨çæè´¹ç¨è¡"; |
| | | } |
| | | return `å
± ${costList.value.length} 个æ`; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const numOrNull = value => |
| | | value === "" || value === null || value === undefined |
| | | ? null |
| | | : Number.isNaN(Number(value)) |
| | | ? null |
| | | : Number(value); |
| | | |
| | | const rowTotal = row => |
| | | COST_FIELDS.reduce((sum, key) => sum + (Number(row[key]) || 0), 0).toFixed(2); |
| | | |
| | | const totals = computed(() => { |
| | | const acc = { total: 0 }; |
| | | COST_FIELDS.forEach(key => { |
| | | acc[key] = 0; |
| | | }); |
| | | costList.value.forEach(row => { |
| | | COST_FIELDS.forEach(key => { |
| | | acc[key] += Number(row[key]) || 0; |
| | | }); |
| | | acc.total += Number(rowTotal(row)) || 0; |
| | | }); |
| | | const shown = { total: acc.total.toFixed(2) }; |
| | | COST_FIELDS.forEach(key => { |
| | | shown[key] = acc[key].toFixed(2); |
| | | }); |
| | | return shown; |
| | | }); |
| | | |
| | | /** |
| | | * æååèµ·æ¢æ¥æé建æä»½è¡ã |
| | | * å·²åå¨çæä»½æ month ä¿çåå¡«åå¼ï¼æ°å¢æä»½æ¿ç§é»è®¤å表åç§éã |
| | | */ |
| | | const rebuildMonths = () => { |
| | | const months = monthRange( |
| | | form.value.contractStartDate, |
| | | form.value.contractEndDate |
| | | ); |
| | | const oldMap = new Map(costList.value.map(row => [row.month, row])); |
| | | costList.value = months.map(month => { |
| | | const old = oldMap.get(month); |
| | | if (old) return old; |
| | | return { |
| | | month, |
| | | rent: form.value.rent ?? "", |
| | | waterElectricity: "", |
| | | property: "", |
| | | express: "", |
| | | broadband: "", |
| | | }; |
| | | }); |
| | | lastAppliedRent.value = form.value.rent ?? ""; |
| | | }; |
| | | |
| | | // 表åç§éååæ¶ï¼åªè¦çãæªææ¹ãçæåº¦æ¿ç§ï¼ç©ºå¼æä»çäºä¸æ¬¡èªå¨å¸¦åºçå¼ï¼ |
| | | const applyRentToMonths = () => { |
| | | const next = form.value.rent; |
| | | costList.value.forEach(row => { |
| | | const untouched = |
| | | row.rent === "" || |
| | | row.rent === null || |
| | | row.rent === undefined || |
| | | String(row.rent) === String(lastAppliedRent.value); |
| | | if (untouched) row.rent = next; |
| | | }); |
| | | lastAppliedRent.value = next ?? ""; |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "houseLeaseDetail", |
| | | id: leaseId.value, |
| | | fetcher: getHouseLeaseDetail, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "æªæ¾å°æ¿å±ç§èµä¿¡æ¯", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | form.value = { |
| | | id: data.id, |
| | | office: data.office || "", |
| | | userName: data.userName || "", |
| | | residentStaff: data.residentStaff || "", |
| | | address: data.address || "", |
| | | rentStartDate: (data.rentStartDate || "").slice(0, 10), |
| | | contractStartDate: (data.contractStartDate || "").slice(0, 10), |
| | | contractEndDate: (data.contractEndDate || "").slice(0, 10), |
| | | tenant: data.tenant || "", |
| | | partyA: data.partyA || "", |
| | | phone: data.phone || "", |
| | | area: data.area ?? "", |
| | | paymentMethod: data.paymentMethod || "", |
| | | deposit: data.deposit ?? "", |
| | | rent: data.rent ?? "", |
| | | }; |
| | | |
| | | const rows = data.costList || data.costVOs || []; |
| | | costList.value = (Array.isArray(rows) ? rows : []).map(row => ({ |
| | | month: row.month, |
| | | rent: row.rent ?? "", |
| | | waterElectricity: row.waterElectricity ?? "", |
| | | property: row.property ?? "", |
| | | express: row.express ?? "", |
| | | broadband: row.broadband ?? "", |
| | | })); |
| | | lastAppliedRent.value = form.value.rent ?? ""; |
| | | }; |
| | | |
| | | const buildCostList = () => |
| | | costList.value.map(row => ({ |
| | | ...row, |
| | | rent: numOrNull(row.rent), |
| | | waterElectricity: numOrNull(row.waterElectricity), |
| | | property: numOrNull(row.property), |
| | | express: numOrNull(row.express), |
| | | broadband: numOrNull(row.broadband), |
| | | })); |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | const payload = { |
| | | id: form.value.id, |
| | | office: form.value.office, |
| | | userName: form.value.userName, |
| | | residentStaff: form.value.residentStaff, |
| | | address: form.value.address, |
| | | rentStartDate: form.value.rentStartDate, |
| | | contractStartDate: form.value.contractStartDate, |
| | | contractEndDate: form.value.contractEndDate, |
| | | tenant: form.value.tenant, |
| | | partyA: form.value.partyA, |
| | | phone: form.value.phone, |
| | | area: numOrNull(form.value.area), |
| | | paymentMethod: form.value.paymentMethod, |
| | | deposit: numOrNull(form.value.deposit), |
| | | rent: numOrNull(form.value.rent), |
| | | costList: buildCostList(), |
| | | }; |
| | | |
| | | loading.value = true; |
| | | addOrUpdateHouseLease(payload) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 1000); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | leaseId.value = options.id; |
| | | form.value.id = options.id; |
| | | loadDetail(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | |
| | | .cost-section { |
| | | margin: 12px 12px 0; |
| | | border-radius: 12px; |
| | | background: #ffffff; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .cost-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 14px 16px 10px; |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | .cost-header-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .cost-header-tip { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .cost-scroll { |
| | | width: 100%; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .cost-table { |
| | | display: inline-block; |
| | | min-width: 100%; |
| | | padding-bottom: 6px; |
| | | } |
| | | |
| | | .cost-row { |
| | | display: flex; |
| | | align-items: center; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .cost-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .cost-head { |
| | | background: #fafbfc; |
| | | } |
| | | |
| | | .cost-head .cost-cell { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: #606266; |
| | | line-height: 36px; |
| | | } |
| | | |
| | | .cost-foot { |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | .cost-foot .cost-cell { |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | line-height: 42px; |
| | | } |
| | | |
| | | .cost-cell { |
| | | width: 76px; |
| | | flex: 0 0 76px; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | color: #333; |
| | | line-height: 44px; |
| | | } |
| | | |
| | | .col-month { |
| | | width: 78px; |
| | | flex: 0 0 78px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .cost-total { |
| | | color: #2979ff; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .cost-empty { |
| | | padding: 24px 16px; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | line-height: 1.6; |
| | | } |
| | | </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" |
| | | v-model="query.office" |
| | | placeholder="请è¾å
¥åäºå¤æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="leaseList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in leaseList" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="home" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.office || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="warnOf(item).text || 'æ å°ææ¥'" |
| | | :type="warnTagType(warnOf(item).level)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">使ç¨äºº</text> |
| | | <text class="detail-value">{{ item.userName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">é©»ç人å</text> |
| | | <text class="detail-value">{{ item.residentStaff || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å°å</text> |
| | | <text class="detail-value">{{ item.address || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ¿ç§äºº</text> |
| | | <text class="detail-value">{{ item.tenant || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ååç²æ¹</text> |
| | | <text class="detail-value">{{ item.partyA || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ååèµ·ç§æ¥</text> |
| | | <text class="detail-value">{{ item.contractStartDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ååå°ææ¥</text> |
| | | <text class="detail-value">{{ item.contractEndDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç§é(å
/æ)</text> |
| | | <text class="detail-value">{{ formatAmount(item.rent) }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ æ¿å±ç§èµæ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="æ¿å±ç§èµçé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | houseLeaseListPage, |
| | | delHouseLease, |
| | | } from "@/api/collaborativeApproval/houseLeaseManagement"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { getWarnInfo } from "@/utils/warning"; |
| | | |
| | | // ååå°ææå 30 天é¢è¦ |
| | | const WARN_DAYS = 30; |
| | | |
| | | const WARN_STATUS_OPTIONS = [ |
| | | { name: "æ£å¸¸", value: "normal" }, |
| | | { name: "å³å°å°æ", value: "expiring" }, |
| | | { name: "已龿", value: "overdue" }, |
| | | ]; |
| | | |
| | | const leaseList = ref([]); |
| | | const showFilter = ref(false); |
| | | |
| | | const query = reactive({ |
| | | office: "", |
| | | userName: "", |
| | | tenant: "", |
| | | partyA: "", |
| | | warnStatus: "", |
| | | }); |
| | | |
| | | const filterFields = [ |
| | | { prop: "office", label: "åäºå¤", type: "input", placeholder: "请è¾å
¥åäºå¤" }, |
| | | { prop: "userName", label: "使ç¨äºº", type: "input", placeholder: "请è¾å
¥ä½¿ç¨äºº" }, |
| | | { prop: "tenant", label: "æ¿ç§äºº", type: "input", placeholder: "请è¾å
¥æ¿ç§äºº" }, |
| | | { prop: "partyA", label: "ååç²æ¹", type: "input", placeholder: "请è¾å
¥ååç²æ¹" }, |
| | | { |
| | | prop: "warnStatus", |
| | | label: "é¢è¦ç¶æ", |
| | | type: "select", |
| | | options: WARN_STATUS_OPTIONS, |
| | | }, |
| | | ]; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("houseLeaseDetail"); |
| | | uni.navigateTo({ |
| | | url: "/pages/cooperativeOffice/houseLeaseManagement/edit", |
| | | }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("houseLeaseDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/cooperativeOffice/houseLeaseManagement/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("houseLeaseDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/cooperativeOffice/houseLeaseManagement/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const warnOf = item => getWarnInfo(item?.contractEndDate, WARN_DAYS); |
| | | |
| | | const warnTagType = level => { |
| | | if (level === "overdue") return "error"; |
| | | if (level === "warning") return "warning"; |
| | | if (level === "normal") return "success"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const getList = () => { |
| | | houseLeaseListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | office: query.office || undefined, |
| | | userName: query.userName || undefined, |
| | | tenant: query.tenant || undefined, |
| | | partyA: query.partyA || undefined, |
| | | warnStatus: query.warnStatus || undefined, |
| | | }) |
| | | .then(res => { |
| | | leaseList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥æ¿å±ç§èµè®°å½åï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delHouseLease(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| | |
| | | <view class="info-card"> |
| | | <view class="card-header"> |
| | | <text class="card-title">åºæ¬ä¿¡æ¯</text> |
| | | <text class="device-status">æ£å¸¸</text> |
| | | <text |
| | | class="device-status" |
| | | :class="deviceInfo.status === 1 ? 'status-enabled' : 'status-disabled'" |
| | | > |
| | | {{ deviceInfo.status === 1 ? 'å¯ç¨' : 'åç¨' }} |
| | | </text> |
| | | </view> |
| | | <view class="card-content"> |
| | | <view class="info-row"> |
| | | <text class="label">设å¤ç¼å·ï¼</text> |
| | | <text class="value">{{ deviceInfo.uniqueCode || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">设å¤åç§°ï¼</text> |
| | | <text class="value">{{ deviceInfo.deviceName || '--' }}</text> |
| | |
| | | <text class="value">{{ deviceInfo.deviceModel || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">ç产åå®¶ï¼</text> |
| | | <text class="value">{{ deviceInfo.supplierName || '--' }}</text> |
| | | <text class="label">设å¤åçï¼</text> |
| | | <text class="value">{{ deviceInfo.deviceBrand || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">设å¤ç±»åï¼</text> |
| | | <text class="value">{{ deviceInfo.type || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">åºåç¼å·ï¼</text> |
| | | <text class="value">{{ deviceInfo.factoryNumber || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">åæ¾ä½ç½®ï¼</text> |
| | | <text class="value">{{ deviceInfo.storageLocation || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">责任人ï¼</text> |
| | | <text class="value">{{ deviceInfo.responsiblePerson || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">åä½ï¼</text> |
| | | <text class="value">{{ deviceInfo.unit || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">å»ºæ¡£æ¥æï¼</text> |
| | | <text class="value">{{ formatDate(deviceInfo.filingDate) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- éè´ä¿¡æ¯ --> |
| | | <view class="info-card"> |
| | | <view class="card-header"> |
| | | <text class="card-title">éè´ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="card-content"> |
| | | <view class="info-row"> |
| | | <text class="label">ä¾åºåï¼</text> |
| | | <text class="value">{{ deviceInfo.supplierName || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">å¶é ååï¼</text> |
| | | <text class="value">{{ deviceInfo.manufacturer || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">è´ç½®æ¥æï¼</text> |
| | | <text class="value">{{ formatDate(deviceInfo.purchaseDate) }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">èµäº§åå¼ï¼</text> |
| | | <text class="value">{{ deviceInfo.originalAssetValue ?? '--' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ææ¯åæ° --> |
| | | <view class="info-card"> |
| | | <view class="card-header"> |
| | | <text class="card-title">ææ¯åæ°</text> |
| | | </view> |
| | | <view class="card-content"> |
| | | <view class="info-row"> |
| | | <text class="label">ä¸»è¦ææ¯åæ°ï¼</text> |
| | | <text class="value">{{ deviceInfo.technicalParameters || '--' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">主è¦ç¨éï¼</text> |
| | | <text class="value">{{ deviceInfo.mainPurpose || '--' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- è¿è¡ç®¡ç --> |
| | | <view class="info-card"> |
| | | <view class="card-header"> |
| | | <text class="card-title">è¿è¡ç®¡ç</text> |
| | | </view> |
| | | <view class="card-content"> |
| | | <view class="info-row"> |
| | | <text class="label">å¯ç¨æ¥æï¼</text> |
| | | <text class="value">{{ formatDate(deviceInfo.activationDate) }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">é¢è®¡è¿è¡æ¶é´ï¼</text> |
| | | <text class="value">{{ formatDate(deviceInfo.planRuntimeTime) }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">å¯ç¨ææ§ï¼</text> |
| | | <text class="value">{{ deviceInfo.isDepr === 1 ? 'æ¯' : 'å¦' }}</text> |
| | | </view> |
| | | <view v-if="deviceInfo.isDepr === 1" class="info-row"> |
| | | <text class="label">æ¯å¹´ææ§éé¢ï¼</text> |
| | | <text class="value">{{ deviceInfo.annualDepreciationAmount ?? '--' }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æ¥ä¿®è®°å½ --> |
| | | <view class="info-card"> |
| | | <view class="card-header"> |
| | | <text class="card-title">æ¥ä¿®è®°å½</text> |
| | | </view> |
| | | <view class="card-content"> |
| | | <view v-if="repairRecords.length === 0" class="empty-tip">ææ æ¥ä¿®è®°å½</view> |
| | | <view v-else class="change-record" v-for="(item, index) in repairRecords" :key="index"> |
| | | <view class="record-header"> |
| | | <text class="record-time">{{ formatDate(item.repairTime) }}</text> |
| | | <text class="record-status" :class="item.status === 1 ? 'status-approved' : 'status-rejected'"> |
| | | {{ item.status === 1 ? 'å®ç»' : 'å¾
ç»´ä¿®' }} |
| | | </text> |
| | | </view> |
| | | <view class="record-reason">æ¥ä¿®äººï¼{{ item.repairName || '--' }}ã维修人ï¼{{ item.maintenanceName || '--' }}</view> |
| | | <view class="record-reason">维修项ç®ï¼{{ item.machineryCategory || '--' }}</view> |
| | | <view class="record-reason">æ
éç°è±¡ï¼{{ item.remark || '--' }}</view> |
| | | <view class="record-reason">ç»´ä¿®ç»æï¼{{ item.maintenanceResult || '--' }}</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ä¿å
»è®°å½ --> |
| | | <view class="info-card"> |
| | | <view class="card-header"> |
| | | <text class="card-title">ä¿å
»è®°å½</text> |
| | | </view> |
| | | <view class="card-content"> |
| | | <view v-if="upkeepRecords.length === 0" class="empty-tip">ææ ä¿å
»è®°å½</view> |
| | | <view v-else class="change-record" v-for="(item, index) in upkeepRecords" :key="index"> |
| | | <view class="record-header"> |
| | | <text class="record-time">{{ formatDate(item.maintenancePlanTime) }}</text> |
| | | <text class="record-status" :class="item.status === 1 ? 'status-approved' : 'status-rejected'"> |
| | | {{ item.status === 1 ? 'å®ç»' : 'å¾
ä¿å
»' }} |
| | | </text> |
| | | </view> |
| | | <view class="record-reason">ä¿å
»é¡¹ç®ï¼{{ item.machineryCategory || '--' }}</view> |
| | | <view class="record-reason">ä¿å
»äººï¼{{ item.maintenancePerson || '--' }}ãå½å
¥äººï¼{{ item.createUserName || '--' }}</view> |
| | | <view class="record-reason">å®é
ä¿å
»æ¥æï¼{{ formatDate(item.maintenanceActuallyTime) }}</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive } from 'vue' |
| | | import { onLoad } from '@dcloudio/uni-app' |
| | | import dayjs from 'dayjs' |
| | | import PageHeader from '@/components/PageHeader.vue' |
| | | import { getDeviceInfo } from '@/api/equipmentManagement/deviceInfo' |
| | | import { listStatusChangeRecord } from '@/api/device/statusChangeRecord' |
| | | import { getRepairPage } from '@/api/equipmentManagement/repair' |
| | | import { getUpkeepPage } from '@/api/equipmentManagement/upkeep' |
| | | import { unwrapList } from '@/utils/list' |
| | | |
| | | const deviceInfo = reactive({ |
| | | uniqueCode: '', |
| | | deviceName: '', |
| | | deviceModel: '', |
| | | deviceBrand: '', |
| | | type: '', |
| | | factoryNumber: '', |
| | | storageLocation: '', |
| | | responsiblePerson: '', |
| | | filingDate: '', |
| | | supplierName: '', |
| | | manufacturer: '', |
| | | purchaseDate: '', |
| | | originalAssetValue: null, |
| | | unit: '', |
| | | technicalParameters: '', |
| | | mainPurpose: '', |
| | | status: null, |
| | | activationDate: '', |
| | | planRuntimeTime: '', |
| | | isDepr: 2, |
| | | annualDepreciationAmount: null, |
| | | }) |
| | | |
| | | const changeRecords = ref([]) |
| | | const recordsLoading = ref(false) |
| | | const repairRecords = ref([]) |
| | | const upkeepRecords = ref([]) |
| | | |
| | | // åç«¯æ¥æå¤å¸¦æ¶åç§ï¼è¿éç»ä¸åªæ¾ç¤ºå°æ¥ |
| | | const formatDate = (d) => (d ? dayjs(d).format('YYYY-MM-DD') : '--') |
| | | |
| | | const statusText = (s) => (s == 1 ? 'å¯ç¨' : 'åç¨') |
| | | |
| | |
| | | uni.navigateBack() |
| | | } |
| | | |
| | | // æ¥ä¿®/ä¿å
»å表æ¥å£åªæä¾ deviceName è¿æ»¤ï¼ä¸ç®¡ç端ä¸è´ï¼æ deviceId ç»´åº¦ï¼ |
| | | const fetchDeviceHistory = async (deviceName) => { |
| | | if (!deviceName) return |
| | | const params = { current: -1, size: -1, deviceName } |
| | | const [repair, upkeep] = await Promise.all([ |
| | | getRepairPage(params).catch(() => null), |
| | | getUpkeepPage(params).catch(() => null), |
| | | ]) |
| | | repairRecords.value = unwrapList(repair) |
| | | upkeepRecords.value = unwrapList(upkeep) |
| | | } |
| | | |
| | | onLoad((options) => { |
| | | const deviceId = options?.deviceId || '' |
| | | if (!deviceId) { |
| | |
| | | return |
| | | } |
| | | fetchDeviceInfo(deviceId) |
| | | // å±¥åéçæ¥ä¿®/ä¿å
»è®°å½è¦çåºæ¬ä¿¡æ¯æ¿å°è®¾å¤åç§°åæè½æ¥ |
| | | .then(() => fetchDeviceHistory(deviceInfo.deviceName)) |
| | | .catch(() => {}) |
| | | fetchChangeRecords(deviceId) |
| | | }) |
| | | </script> |
| | |
| | | background: #52c41a; |
| | | } |
| | | |
| | | .status-enabled { |
| | | background: #52c41a; |
| | | } |
| | | |
| | | .status-disabled { |
| | | background: #bfbfbf; |
| | | } |
| | | |
| | | .card-content { |
| | | padding: 16px 20px; |
| | | } |
| | |
| | | } |
| | | |
| | | .label { |
| | | width: 88px; |
| | | /* ãä¸»è¦ææ¯åæ°ï¼ãæ¯åæ¥æé¿çæ ç¾æ´å®½ï¼æ¹ææå
容æå¼ï¼é¿å
æ¢è¡ææªæ */ |
| | | min-width: 88px; |
| | | flex-shrink: 0; |
| | | white-space: nowrap; |
| | | color: #666666; |
| | | font-size: 14px; |
| | | } |
| | |
| | | <u-form @submit="sendForm" ref="formRef" :model="form" :rules="formRules" label-width="110"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <u-cell-group title="åºæ¬ä¿¡æ¯"> |
| | | <u-form-item label="设å¤ç¼å·" prop="uniqueCode" border-bottom> |
| | | <u-input |
| | | v-model="form.uniqueCode" |
| | | :disabled="form.autoGenerateUniqueCode || operationType === 'edit'" |
| | | :placeholder="form.autoGenerateUniqueCode ? 'ä¿ååèªå¨çæ' : '请è¾å
¥è®¾å¤ç¼å·'" |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item v-if="operationType !== 'edit'" label="èªå¨çæç¼å·" prop="autoGenerateUniqueCode" border-bottom> |
| | | <u-switch |
| | | v-model="form.autoGenerateUniqueCode" |
| | | :activeValue="true" |
| | | :inactiveValue="false" |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="设å¤åç§°" prop="deviceName" required border-bottom> |
| | | <u-input |
| | | v-model="form.deviceName" |
| | |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | <!-- <u-form-item label="设å¤åç" prop="deviceBrand" required border-bottom> |
| | | <u-form-item label="设å¤åç" prop="deviceBrand" border-bottom> |
| | | <u-input |
| | | v-model="form.deviceBrand" |
| | | placeholder="请è¾å
¥è®¾å¤åç" |
| | | clearable |
| | | /> |
| | | </u-form-item> --> |
| | | </u-form-item> |
| | | <u-form-item label="设å¤ç±»å" prop="type" required border-bottom> |
| | | <FormPicker |
| | | v-model="form.type" |
| | | type="select" |
| | | title="éæ©è®¾å¤ç±»å" |
| | | :options="deviceTypeOptions" |
| | | placeholder="è¯·éæ©è®¾å¤ç±»å" |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="åºåç¼å·" prop="factoryNumber" border-bottom> |
| | | <u-input |
| | | v-model="form.factoryNumber" |
| | | placeholder="请è¾å
¥åºåç¼å·" |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="ä¾åºå" prop="supplierName" required border-bottom> |
| | | <u-input |
| | | v-model="form.supplierName" |
| | |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | <!-- <u-form-item label="åæ¾ä½ç½®" prop="storageLocation" required border-bottom> |
| | | <u-form-item label="åæ¾ä½ç½®" prop="storageLocation" border-bottom> |
| | | <u-input |
| | | v-model="form.storageLocation" |
| | | placeholder="请è¾å
¥åæ¾ä½ç½®" |
| | | clearable |
| | | /> |
| | | </u-form-item> --> |
| | | </u-form-item> |
| | | <u-form-item label="责任人" prop="responsiblePerson" border-bottom> |
| | | <u-input |
| | | v-model="form.responsiblePerson" |
| | | placeholder="请è¾å
¥è´£ä»»äºº" |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="å»ºæ¡£æ¥æ" prop="filingDate" border-bottom> |
| | | <FormPicker |
| | | v-model="form.filingDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©å»ºæ¡£æ¥æ" |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="åä½" prop="unit" required border-bottom> |
| | | <u-input |
| | | v-model="form.unit" |
| | |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | <!-- <u-form-item label="å¯ç¨ææ§" prop="enableDepreciation" required border-bottom> |
| | | <u-switch |
| | | v-model="form.enableDepreciation" |
| | | :active-value="true" |
| | | :inactive-value="false" |
| | | /> |
| | | </u-form-item> --> |
| | | <u-form-item label="æ°é" prop="number" required border-bottom> |
| | | <u-input |
| | | v-model="form.number" |
| | |
| | | </u-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éè´ä¿¡æ¯"> |
| | | <u-form-item label="å¶é åå" prop="manufacturer" border-bottom> |
| | | <u-input |
| | | v-model="form.manufacturer" |
| | | placeholder="请è¾å
¥å¶é åå" |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="è´ç½®æ¥æ" prop="purchaseDate" border-bottom> |
| | | <FormPicker |
| | | v-model="form.purchaseDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©è´ç½®æ¥æ" |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="èµäº§åå¼" prop="originalAssetValue" border-bottom> |
| | | <u-input |
| | | v-model="form.originalAssetValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥èµäº§åå¼" |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | </u-cell-group> |
| | | <u-cell-group title="ææ¯åæ°"> |
| | | <u-form-item label="ä¸»è¦ææ¯åæ°" prop="technicalParameters" border-bottom> |
| | | <up-textarea |
| | | v-model="form.technicalParameters" |
| | | placeholder="请è¾å
¥ä¸»è¦ææ¯åæ°" |
| | | :maxlength="500" |
| | | count |
| | | auto-height |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="主è¦ç¨é" prop="mainPurpose" border-bottom> |
| | | <up-textarea |
| | | v-model="form.mainPurpose" |
| | | placeholder="请è¾å
¥ä¸»è¦ç¨é" |
| | | :maxlength="500" |
| | | count |
| | | auto-height |
| | | /> |
| | | </u-form-item> |
| | | </u-cell-group> |
| | | <u-cell-group title="è¿è¡ç®¡ç"> |
| | | <u-form-item label="设å¤ç¶æ" prop="status" required border-bottom> |
| | | <FormPicker |
| | | v-model="form.status" |
| | | type="select" |
| | | title="éæ©è®¾å¤ç¶æ" |
| | | :options="statusOptions" |
| | | placeholder="è¯·éæ©è®¾å¤ç¶æ" |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="å¯ç¨æ¥æ" prop="activationDate" border-bottom> |
| | | <FormPicker |
| | | v-model="form.activationDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©å¯ç¨æ¥æ" |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="é¢è®¡è¿è¡æ¶é´" prop="planRuntimeTime" border-bottom> |
| | | <FormPicker |
| | | v-model="form.planRuntimeTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©é¢è®¡è¿è¡æ¶é´" |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="å¯ç¨ææ§" prop="isDepr" border-bottom> |
| | | <u-switch |
| | | v-model="form.isDepr" |
| | | :activeValue="1" |
| | | :inactiveValue="2" |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item v-if="form.isDepr === 1" label="æ¯å¹´ææ§éé¢" prop="annualDepreciationAmount" border-bottom> |
| | | <u-input |
| | | v-model="form.annualDepreciationAmount" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ¯å¹´ææ§éé¢" |
| | | clearable |
| | | /> |
| | | </u-form-item> |
| | | <u-form-item label="设å¤å¾ç" border-bottom> |
| | | <CommonUpload |
| | | v-model="imageFiles" |
| | | :maxCount="20" |
| | | accept="image" |
| | | /> |
| | | </u-form-item> |
| | | </u-cell-group> |
| | | |
| | | <!-- æäº¤æé® --> |
| | | <view class="footer-btns"> |
| | | <u-button class="cancel-btn" @click="goBack">åæ¶</u-button> |
| | |
| | | import { ref, computed, onMounted } from 'vue'; |
| | | import { onShow } from '@dcloudio/uni-app'; |
| | | import PageHeader from '@/components/PageHeader.vue'; |
| | | import FormPicker from '@/components/FormPicker.vue'; |
| | | import CommonUpload from '@/components/CommonUpload.vue'; |
| | | import { getLedgerById, addLedger, editLedger } from '@/api/equipmentManagement/ledger'; |
| | | import { toFileList } from '@/utils/file'; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | calculateTaxIncludeTotalPrice, |
| | |
| | | const formRules = { |
| | | deviceName: [{ required: true, trigger: "blur", message: "请è¾å
¥" }], |
| | | deviceModel: [{ required: true, trigger: "blur", message: "请è¾å
¥" }], |
| | | deviceBrand: [{ required: true, trigger: "blur", message: "请è¾å
¥" }], |
| | | type: [{ required: true, trigger: "change", message: "è¯·éæ©è®¾å¤ç±»å" }], |
| | | supplierName: [{ required: true, trigger: "blur", message: "请è¾å
¥" }], |
| | | storageLocation: [{ required: true, trigger: "blur", message: "请è¾å
¥" }], |
| | | unit: [{ required: true, trigger: "blur", message: "请è¾å
¥" }], |
| | | // æ°åç±»ååæ®µéè¦ç¹æ®å¤çï¼ç¡®ä¿ææ°å¼æ¶ä¸ä¼è§¦åå¿
å¡«æ ¡éª |
| | | number: [{ |
| | |
| | | } |
| | | }], |
| | | createTime: [{ required: true, trigger: "change", message: "è¯·éæ©" }], |
| | | status: [{ required: true, trigger: "change", message: "è¯·éæ©è®¾å¤ç¶æ" }], |
| | | planRuntimeTime: [{ required: true, trigger: "change", message: "è¯·éæ©é¢è®¡è¿è¡æ¶é´" }], |
| | | }; |
| | | |
| | | // 设å¤ç±»ååºå®é项ï¼ä¸ç®¡ç端ä¸è´ï¼æ æ³å½ä¸çå¼ä¼ç± FormPicker çé项å±ç¤ºå
åºï¼ |
| | | const deviceTypeOptions = ref([ |
| | | { name: 'ç产设å¤', value: 'ç产设å¤' }, |
| | | { name: 'åå
¬è®¾å¤', value: 'åå
¬è®¾å¤' }, |
| | | { name: 'æ£æµè®¾å¤', value: 'æ£æµè®¾å¤' }, |
| | | { name: 'è¿è¾è®¾å¤', value: 'è¿è¾è®¾å¤' }, |
| | | { name: 'å
¶ä»è®¾å¤', value: 'å
¶ä»è®¾å¤' }, |
| | | ]); |
| | | const statusOptions = ref([ |
| | | { name: 'åç¨', value: 0 }, |
| | | { name: 'å¯ç¨', value: 1 }, |
| | | ]); |
| | | |
| | | // ä½¿ç¨ ref 声æè¡¨åæ°æ® |
| | | const form = ref({ |
| | | // åºæ¬ä¿¡æ¯ |
| | | autoGenerateUniqueCode: true, // èªå¨çæç¼å· |
| | | uniqueCode: undefined, // 设å¤ç¼å· |
| | | deviceName: undefined, // 设å¤åç§° |
| | | deviceModel: undefined, // è§æ ¼åå· |
| | | deviceBrand: undefined, // 设å¤åç |
| | | supplierName: undefined, // ä¾åºå |
| | | type: undefined, // 设å¤ç±»å |
| | | factoryNumber: undefined, // åºåç¼å· |
| | | storageLocation: undefined, // åæ¾ä½ç½® |
| | | responsiblePerson: undefined, // 责任人 |
| | | filingDate: dayjs().format("YYYY-MM-DD"), // å»ºæ¡£æ¥æ |
| | | // éè´ä¿¡æ¯ |
| | | supplierName: undefined, // ä¾åºå |
| | | manufacturer: undefined, // å¶é åå |
| | | purchaseDate: undefined, // è´ç½®æ¥æ |
| | | originalAssetValue: undefined, // èµäº§åå¼ |
| | | unit: undefined, // åä½ |
| | | enableDepreciation: false, // å¯ç¨ææ§ |
| | | number: undefined, // æ°é |
| | | number: 1, // æ°é |
| | | taxIncludingPriceUnit: undefined, // å«ç¨åä»· |
| | | taxIncludingPriceTotal: undefined, // å«ç¨æ»ä»· |
| | | taxRate: undefined, // ç¨ç |
| | | unTaxIncludingPriceTotal: undefined, // ä¸å«ç¨æ»ä»· |
| | | // ææ¯åæ° |
| | | technicalParameters: undefined, // ä¸»è¦ææ¯åæ° |
| | | mainPurpose: undefined, // 主è¦ç¨é |
| | | // è¿è¡ç®¡ç |
| | | status: 1, // 设å¤ç¶æ 0åç¨ 1å¯ç¨ |
| | | activationDate: undefined, // å¯ç¨æ¥æ |
| | | planRuntimeTime: dayjs().format("YYYY-MM-DD"), // é¢è®¡è¿è¡æ¶é´ |
| | | isDepr: 2, // æ¯å¦å¯ç¨ææ§ 1æ¯ 2å¦ |
| | | annualDepreciationAmount: undefined, // æ¯å¹´ææ§éé¢ |
| | | storageBlobDTOs: undefined, // 设å¤å¾çï¼æäº¤ï¼ |
| | | storageBlobVOs: undefined, // 设å¤å¾çï¼åæ¾ï¼ |
| | | createTime: dayjs().format("YYYY-MM-DD"), // å½å
¥æ¥æ |
| | | }); |
| | | |
| | | // 设å¤å¾çï¼CommonUpload ç» modelValueï¼æ¹å¨æ¶åæ¶ååæäº¤/åæ¾ä¸¤ä¸ªåæ®µ |
| | | const imageFiles = computed({ |
| | | get: () => form.value.storageBlobVOs || [], |
| | | set: val => { |
| | | form.value.storageBlobDTOs = val; |
| | | form.value.storageBlobVOs = val; |
| | | }, |
| | | }); |
| | | |
| | | |
| | |
| | | console.log(data); |
| | | |
| | | if (code == 200) { |
| | | // åºæ¬ä¿¡æ¯ |
| | | form.value.autoGenerateUniqueCode = false; |
| | | form.value.uniqueCode = data.uniqueCode; |
| | | form.value.deviceName = data.deviceName; |
| | | form.value.deviceModel = data.deviceModel; |
| | | form.value.deviceBrand = data.deviceBrand || ''; |
| | | form.value.supplierName = data.supplierName; |
| | | form.value.type = data.type; |
| | | form.value.factoryNumber = data.factoryNumber; |
| | | form.value.storageLocation = data.storageLocation || ''; |
| | | form.value.responsiblePerson = data.responsiblePerson; |
| | | form.value.filingDate = data.filingDate |
| | | ? dayjs(data.filingDate).format("YYYY-MM-DD") |
| | | : undefined; |
| | | |
| | | // éè´ä¿¡æ¯ |
| | | form.value.supplierName = data.supplierName; |
| | | form.value.manufacturer = data.manufacturer; |
| | | form.value.purchaseDate = data.purchaseDate |
| | | ? dayjs(data.purchaseDate).format("YYYY-MM-DD") |
| | | : undefined; |
| | | form.value.originalAssetValue = data.originalAssetValue; |
| | | form.value.unit = data.unit; |
| | | form.value.enableDepreciation = !!data.enableDepreciation; |
| | | form.value.number = data.number; |
| | | form.value.taxIncludingPriceUnit = data.taxIncludingPriceUnit; |
| | | form.value.taxIncludingPriceTotal = data.taxIncludingPriceTotal; |
| | | form.value.taxRate = data.taxRate; |
| | | form.value.unTaxIncludingPriceTotal = data.unTaxIncludingPriceTotal; |
| | | |
| | | // ææ¯åæ° |
| | | form.value.technicalParameters = data.technicalParameters; |
| | | form.value.mainPurpose = data.mainPurpose; |
| | | |
| | | // è¿è¡ç®¡ç |
| | | form.value.status = data.status != null ? data.status : 1; |
| | | form.value.activationDate = data.activationDate |
| | | ? dayjs(data.activationDate).format("YYYY-MM-DD") |
| | | : undefined; |
| | | form.value.planRuntimeTime = data.planRuntimeTime |
| | | ? dayjs(data.planRuntimeTime).format("YYYY-MM-DD") |
| | | : undefined; |
| | | form.value.isDepr = data.isDepr != null ? data.isDepr : 2; |
| | | form.value.annualDepreciationAmount = data.annualDepreciationAmount; |
| | | form.value.storageBlobVOs = toFileList(data.storageBlobVOs); |
| | | form.value.storageBlobDTOs = form.value.storageBlobVOs; |
| | | |
| | | form.value.createTime = data.createTime; |
| | | |
| | | // æ°æ®å è½½å®æåï¼é置表åéªè¯ç¶æ |
| | |
| | | |
| | | // æ°å¦è®¡ç® |
| | | const mathNum = () => { |
| | | // åªæå¨æ°å¢æ¨¡å¼æè
åæ®µç¡®å®ä¸ºç©ºæ¶ææ¾ç¤ºæç¤º |
| | | if (operationType.value !== 'edit' || (form.value.taxIncludingPriceUnit === undefined || form.value.taxIncludingPriceUnit === '')) { |
| | | if (!form.value.taxIncludingPriceUnit) { |
| | | showToast("请è¾å
¥åä»·"); |
| | | return; |
| | | } |
| | | if (operationType.value !== 'edit' || (form.value.number === undefined || form.value.number === '')) { |
| | | if (!form.value.number) { |
| | | showToast("请è¾å
¥æ°é"); |
| | | return; |
| | | } |
| | |
| | | let errorMessage = ''; |
| | | |
| | | // æ£æ¥ææ¬ç±»åå¿
å¡«åæ®µ |
| | | if (!form.value.deviceName) { |
| | | if (!form.value.autoGenerateUniqueCode && !form.value.uniqueCode) { |
| | | isValid = false; |
| | | errorMessage = '请è¾å
¥è®¾å¤ç¼å·'; |
| | | } else if (!form.value.deviceName) { |
| | | isValid = false; |
| | | errorMessage = '请è¾å
¥è®¾å¤åç§°'; |
| | | } else if (!form.value.deviceModel) { |
| | | isValid = false; |
| | | errorMessage = '请è¾å
¥è§æ ¼åå·'; |
| | | } else if (!form.value.type) { |
| | | isValid = false; |
| | | errorMessage = 'è¯·éæ©è®¾å¤ç±»å'; |
| | | } else if (!form.value.supplierName) { |
| | | isValid = false; |
| | | errorMessage = '请è¾å
¥ä¾åºå'; |
| | | } else if (!form.value.unit) { |
| | | isValid = false; |
| | | errorMessage = '请è¾å
¥åä½'; |
| | | } else if (form.value.status === undefined || form.value.status === null || form.value.status === '') { |
| | | isValid = false; |
| | | errorMessage = 'è¯·éæ©è®¾å¤ç¶æ'; |
| | | } else if (form.value.isDepr === 1 && !form.value.annualDepreciationAmount) { |
| | | isValid = false; |
| | | errorMessage = '请è¾å
¥æ¯å¹´ææ§éé¢'; |
| | | } else if (!form.value.planRuntimeTime) { |
| | | isValid = false; |
| | | errorMessage = 'è¯·éæ©é¢è®¡è¿è¡æ¶é´'; |
| | | } |
| | | |
| | | // æ£æ¥æ°åç±»åå¿
å¡«åæ®µ |
| | |
| | | |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">设å¤ç¼å·</text> |
| | | <text class="detail-value">{{ item.uniqueCode || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è§æ ¼åå·</text> |
| | | <text class="detail-value">{{ item.deviceModel || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">设å¤ç±»å</text> |
| | | <text class="detail-value">{{ item.type || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">责任人</text> |
| | | <text class="detail-value">{{ item.responsiblePerson || '-' }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">设å¤ç¶æ</text> |
| | | <text |
| | | class="detail-value" |
| | | :class="item.status === 1 ? 'status-enabled' : 'status-disabled'" |
| | | > |
| | | {{ item.status === 1 ? 'å¯ç¨' : 'åç¨' }} |
| | | </text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä¾åºå</text> |
| | |
| | | @import '@/styles/sales-common.scss'; |
| | | |
| | | // 设å¤å°è´¦ç¹ææ ·å¼ï¼æææ ·å¼é½å·²å
å«å¨å
Œ
±æ ·å¼ä¸ï¼ |
| | | |
| | | .status-enabled { |
| | | color: #67c23a; |
| | | } |
| | | |
| | | .status-disabled { |
| | | color: #f56c6c; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="fuel-card-detail"> |
| | | <PageHeader title="å æ²¹å¡è¯¦æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ detail.cardNumber || "-" }}</text> |
| | | <up-tag :text="statusLabel(detail.status)" |
| | | :type="detail.status === 'CANCELLED' ? 'error' : 'success'" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <u-cell-group title="å æ²¹å¡ä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="å æ²¹å¡å·" |
| | | :value="detail.cardNumber || '-'" /> |
| | | <u-cell title="æå±é¨é¨" |
| | | :value="detail.deptName || '-'" /> |
| | | <u-cell title="åå¡åä½" |
| | | :value="detail.issuer || '-'" /> |
| | | <u-cell title="æåä½é¢" |
| | | :value="formatAmount(detail.initialBalance)" /> |
| | | <u-cell title="å½åä½é¢" |
| | | :value="formatAmount(detail.currentBalance)" /> |
| | | <u-cell title="ç¶æ" |
| | | :value="statusLabel(detail.status)" /> |
| | | <u-cell title="夿³¨" |
| | | :value="detail.remark || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å
å¼è®°å½" |
| | | class="detail-section"> |
| | | <view class="section-tools"> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="å
å¼ç®¡ç" |
| | | @click="goRecharge" /> |
| | | </view> |
| | | <view v-if="rechargeList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ å
å¼è®°å½</text> |
| | | </view> |
| | | <view v-else |
| | | class="recharge-list"> |
| | | <view v-for="row in rechargeList" |
| | | :key="row.id" |
| | | class="recharge-item"> |
| | | <view class="recharge-main"> |
| | | <text class="recharge-amount">+{{ Number(row.rechargeAmount || 0).toFixed(2) }}</text> |
| | | <text class="recharge-date">{{ row.rechargeDate || "-" }}</text> |
| | | </view> |
| | | <view class="recharge-meta"> |
| | | <text v-if="row.createUserName" |
| | | class="recharge-user">{{ row.createUserName }}</text> |
| | | <text v-if="row.remark" |
| | | class="recharge-remark">{{ row.remark }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { |
| | | listFuelCardPage, |
| | | listRecharge, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const loading = ref(true); |
| | | const cardId = ref(""); |
| | | const detail = ref({}); |
| | | const rechargeList = ref([]); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("fuelCardDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/fuelCard/edit?id=${cardId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const goRecharge = () => { |
| | | uni.setStorageSync("fuelCardDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/fuelCard/recharge?id=${cardId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const statusLabel = status => (status === "CANCELLED" ? "注é" : "æ£å¸¸"); |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | // å æ²¹å¡æ²¡æåæ¡è¯¦æ
æ¥å£ï¼ä»åè¡¨éæ id æ |
| | | const loadCard = async () => { |
| | | const cached = uni.getStorageSync("fuelCardDetail"); |
| | | if (cached && String(cached.id) === String(cardId.value)) { |
| | | detail.value = cached; |
| | | } |
| | | try { |
| | | const res = await listFuelCardPage({ current: -1, size: -1 }); |
| | | const found = unwrapList(res).find( |
| | | item => String(item.id) === String(cardId.value) |
| | | ); |
| | | if (found) detail.value = found; |
| | | } catch (error) { |
| | | // æä¸å°å°±æ²¿ç¨ç¼å |
| | | } |
| | | }; |
| | | |
| | | const loadRecharges = () => { |
| | | listRecharge({ current: -1, size: -1, cardId: cardId.value }) |
| | | .then(res => { |
| | | rechargeList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | rechargeList.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | await loadCard(); |
| | | loadRecharges(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | cardId.value = options?.id || ""; |
| | | if (!cardId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .fuel-card-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .section-tools { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .empty-text { |
| | | padding: 16px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .recharge-list { |
| | | padding: 6px 12px 12px; |
| | | } |
| | | |
| | | .recharge-item { |
| | | padding: 12px 0; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .recharge-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .recharge-main { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .recharge-amount { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #19be6b; |
| | | } |
| | | |
| | | .recharge-date { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .recharge-meta { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .recharge-user, |
| | | .recharge-remark { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="110" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="å æ²¹å¡ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="å æ²¹å¡å·" |
| | | prop="cardNumber" |
| | | required> |
| | | <up-input v-model="form.cardNumber" |
| | | placeholder="请è¾å
¥å æ²¹å¡å·" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æå±é¨é¨" |
| | | prop="deptId" |
| | | required> |
| | | <FormPicker v-model="form.deptId" |
| | | type="select" |
| | | title="éæ©æå±é¨é¨" |
| | | :options="deptOptions" |
| | | placeholder="è¯·éæ©æå±é¨é¨" |
| | | @change="onDeptChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="åå¡åä½" |
| | | prop="issuer" |
| | | required> |
| | | <up-input v-model="form.issuer" |
| | | placeholder="请è¾å
¥åå¡åä½" |
| | | clearable /> |
| | | </up-form-item> |
| | | <!-- æåä½é¢åªå¨æ°å¢æ¶å¯å¡«ï¼ç¼è¾æ¶ä½é¢ç±å
å¼/å æ²¹è®°å½ç»´æ¤ --> |
| | | <up-form-item v-if="!cardId" |
| | | label="æåä½é¢" |
| | | prop="initialBalance"> |
| | | <up-input v-model="form.initialBalance" |
| | | type="number" |
| | | placeholder="请è¾å
¥æåä½é¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¶æ" |
| | | prop="status" |
| | | required> |
| | | <FormPicker v-model="form.status" |
| | | type="select" |
| | | title="éæ©ç¶æ" |
| | | :options="STATUS_OPTIONS" |
| | | placeholder="è¯·éæ©ç¶æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <up-textarea v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | auto-height /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { |
| | | addFuelCard, |
| | | listFuelCardPage, |
| | | updateFuelCard, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const STATUS_OPTIONS = [ |
| | | { name: "æ£å¸¸", value: "ACTIVE" }, |
| | | { name: "注é", value: "CANCELLED" }, |
| | | ]; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const cardId = ref(""); |
| | | const deptOptions = ref([]); |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | cardNumber: "", |
| | | deptId: "", |
| | | deptName: "", |
| | | issuer: "", |
| | | initialBalance: 0, |
| | | status: "ACTIVE", |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | cardNumber: [{ required: true, message: "请è¾å
¥å æ²¹å¡å·", trigger: "blur" }], |
| | | deptId: [{ required: true, message: "è¯·éæ©æå±é¨é¨", trigger: "change" }], |
| | | issuer: [{ required: true, message: "请è¾å
¥åå¡åä½", trigger: "blur" }], |
| | | status: [{ required: true, message: "è¯·éæ©ç¶æ", trigger: "change" }], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => (cardId.value ? "ç¼è¾å æ²¹å¡" : "æ°å¢å æ²¹å¡")); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const onDeptChange = (value, item) => { |
| | | form.value.deptName = item?.name || ""; |
| | | }; |
| | | |
| | | const loadDepts = async () => { |
| | | try { |
| | | const res = await getDept(); |
| | | deptOptions.value = (res?.data || []).map(item => ({ |
| | | name: item.deptName, |
| | | value: item.deptId, |
| | | })); |
| | | } catch (error) { |
| | | deptOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | // å æ²¹å¡æ²¡æåæ¡è¯¦æ
æ¥å£ï¼ä»åè¡¨éæ id æ |
| | | const fetchCard = async id => { |
| | | const res = await listFuelCardPage({ current: -1, size: -1 }); |
| | | const rows = res?.data?.records || res?.records || res?.data || []; |
| | | const found = (Array.isArray(rows) ? rows : []).find( |
| | | item => String(item.id) === String(id) |
| | | ); |
| | | return { data: found }; |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | if (!cardId.value) return; |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "fuelCardDetail", |
| | | id: cardId.value, |
| | | fetcher: fetchCard, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "è·åå æ²¹å¡è¯¦æ
失败", icon: "none" }); |
| | | return; |
| | | } |
| | | form.value = { ...form.value, ...data, id: data.id }; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | loading.value = true; |
| | | const action = cardId.value ? updateFuelCard : addFuelCard; |
| | | action({ ...form.value, id: cardId.value || undefined }) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | cardId.value = options.id; |
| | | form.value.id = options.id; |
| | | } |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | await loadDepts(); |
| | | if (cardId.value) await loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.cardNumber" |
| | | placeholder="请è¾å
¥å æ²¹å¡å·æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="cardList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in cardList" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="rmb-circle" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.cardNumber || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="statusLabel(item.status)" |
| | | :type="item.status === 'CANCELLED' ? 'error' : 'success'" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æå±é¨é¨</text> |
| | | <text class="detail-value">{{ item.deptName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åå¡åä½</text> |
| | | <text class="detail-value">{{ item.issuer || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æåä½é¢</text> |
| | | <text class="detail-value">{{ formatAmount(item.initialBalance) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å½åä½é¢</text> |
| | | <text class="detail-value highlight">{{ formatAmount(item.currentBalance) }}</text> |
| | | </view> |
| | | <view class="detail-row" |
| | | v-if="item.remark"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remark }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="success" |
| | | @click="goRecharge(item)">å
å¼ç®¡ç</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button v-if="item.status !== 'CANCELLED'" |
| | | class="action-btn" |
| | | size="small" |
| | | type="warning" |
| | | plain |
| | | @click="handleCancelCard(item)">注é</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ å æ²¹å¡æ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="å æ²¹å¡çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { |
| | | delFuelCard, |
| | | listFuelCardPage, |
| | | updateFuelCard, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const cardList = ref([]); |
| | | const showFilter = ref(false); |
| | | const deptOptions = ref([]); |
| | | |
| | | const query = reactive({ |
| | | cardNumber: "", |
| | | deptId: "", |
| | | }); |
| | | |
| | | const filterFields = computed(() => [ |
| | | { |
| | | prop: "cardNumber", |
| | | label: "å æ²¹å¡å·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥å æ²¹å¡å·", |
| | | }, |
| | | { |
| | | prop: "deptId", |
| | | label: "æå±é¨é¨", |
| | | type: "select", |
| | | options: deptOptions.value, |
| | | }, |
| | | ]); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("fuelCardDetail"); |
| | | uni.navigateTo({ url: "/pages/inventoryManagement/fuelCard/edit" }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("fuelCardDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/fuelCard/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("fuelCardDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/fuelCard/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goRecharge = item => { |
| | | uni.setStorageSync("fuelCardDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/fuelCard/recharge?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const statusLabel = status => (status === "CANCELLED" ? "注é" : "æ£å¸¸"); |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const getList = () => { |
| | | listFuelCardPage({ |
| | | current: -1, |
| | | size: -1, |
| | | cardNumber: query.cardNumber || undefined, |
| | | deptId: query.deptId || undefined, |
| | | }) |
| | | .then(res => { |
| | | cardList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleCancelCard = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "注é确认", |
| | | content: `确认注éå æ²¹å¡ã${item.cardNumber || ""}ãåï¼æ³¨éå该å¡ä¸è½ç¨äºæ°å¢å 油记å½ï¼ä½é¢å°ä¿çã`, |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | updateFuelCard({ id: item.id, status: "CANCELLED" }) |
| | | .then(() => { |
| | | uni.showToast({ title: "注éæå", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "注é失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: `确认å é¤å æ²¹å¡ã${item.cardNumber || ""}ãåï¼`, |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delFuelCard(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | let deptsLoaded = false; |
| | | |
| | | const loadDepts = () => { |
| | | if (deptsLoaded) return; |
| | | deptsLoaded = true; |
| | | getDept() |
| | | .then(res => { |
| | | deptOptions.value = (res?.data || []).map(item => ({ |
| | | name: item.deptName, |
| | | value: item.deptId, |
| | | })); |
| | | }) |
| | | .catch(() => { |
| | | deptsLoaded = false; |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | loadDepts(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="recharge-page"> |
| | | <PageHeader title="å
å¼ç®¡ç" |
| | | @back="goBack" /> |
| | | |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ card.cardNumber || "-" }}</text> |
| | | <view class="hero-balance"> |
| | | <text class="hero-label">å½åä½é¢</text> |
| | | <text class="hero-value">{{ formatAmount(currentBalance) }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="section"> |
| | | <view class="section-header"> |
| | | <text class="section-title">å
å¼è®°å½</text> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="æ°å¢å
å¼" |
| | | @click="openForm" /> |
| | | </view> |
| | | |
| | | <view v-if="rechargeList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ å
å¼è®°å½</text> |
| | | </view> |
| | | <view v-else |
| | | class="recharge-list"> |
| | | <view v-for="row in rechargeList" |
| | | :key="row.id" |
| | | class="recharge-item"> |
| | | <view class="recharge-main"> |
| | | <text class="recharge-amount">+{{ Number(row.rechargeAmount || 0).toFixed(2) }}</text> |
| | | <text class="recharge-date">{{ row.rechargeDate || "-" }}</text> |
| | | </view> |
| | | <view class="recharge-meta"> |
| | | <text v-if="row.createUserName" |
| | | class="recharge-user">{{ row.createUserName }}</text> |
| | | <text v-if="row.remark" |
| | | class="recharge-remark">{{ row.remark }}</text> |
| | | </view> |
| | | <view class="recharge-actions"> |
| | | <up-button size="mini" |
| | | type="error" |
| | | plain |
| | | text="å é¤" |
| | | @click="handleDelete(row)" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <up-popup :show="showForm" |
| | | mode="bottom" |
| | | :round="16" |
| | | @close="showForm = false"> |
| | | <view class="form-panel"> |
| | | <view class="form-panel__header"> |
| | | <text class="form-panel__title">æ°å¢å
å¼</text> |
| | | <u-icon name="close" |
| | | size="20" |
| | | color="#909399" |
| | | @click="showForm = false"></u-icon> |
| | | </view> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="90" |
| | | input-align="right"> |
| | | <up-form-item label="å
å¼éé¢" |
| | | prop="rechargeAmount" |
| | | required> |
| | | <up-input v-model="form.rechargeAmount" |
| | | type="number" |
| | | placeholder="请è¾å
¥å
å¼éé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="å
弿¥æ" |
| | | prop="rechargeDate" |
| | | required> |
| | | <up-input v-model="form.rechargeDate" |
| | | placeholder="è¯·éæ©å
弿¥æ" |
| | | readonly |
| | | @click="showDatePicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showDatePicker = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <up-input v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | clearable /> |
| | | </up-form-item> |
| | | </up-form> |
| | | <view class="form-panel__footer"> |
| | | <up-button class="form-panel__btn" |
| | | text="åæ¶" |
| | | @click="showForm = false" /> |
| | | <up-button class="form-panel__btn" |
| | | type="primary" |
| | | text="ç¡®å®" |
| | | :loading="submitting" |
| | | @click="submitForm" /> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <up-datetime-picker :show="showDatePicker" |
| | | v-model="dateValue" |
| | | mode="datetime" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import { |
| | | addRecharge, |
| | | delRecharge, |
| | | listAllFuelCard, |
| | | listRecharge, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const cardId = ref(""); |
| | | const card = ref({}); |
| | | const rechargeList = ref([]); |
| | | const showForm = ref(false); |
| | | const showDatePicker = ref(false); |
| | | const submitting = ref(false); |
| | | const formRef = ref(); |
| | | const dateValue = ref(Date.now()); |
| | | |
| | | const form = ref({ |
| | | cardId: "", |
| | | rechargeAmount: "", |
| | | rechargeDate: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | rechargeAmount: [ |
| | | { required: true, message: "请è¾å
¥å
å¼éé¢", trigger: "blur" }, |
| | | ], |
| | | rechargeDate: [ |
| | | { required: true, message: "è¯·éæ©å
弿¥æ", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | // ä½é¢ç±å
å¼/å æ²¹è®°å½å¨å端维æ¤ï¼å端åªå±ç¤º |
| | | const currentBalance = computed(() => |
| | | card.value.currentBalance === undefined ? null : card.value.currentBalance |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const loadCard = async () => { |
| | | const cached = uni.getStorageSync("fuelCardDetail"); |
| | | if (cached && String(cached.id) === String(cardId.value)) { |
| | | card.value = cached; |
| | | } |
| | | try { |
| | | const res = await listAllFuelCard(); |
| | | const rows = unwrapList(res); |
| | | const found = rows.find(item => String(item.id) === String(cardId.value)); |
| | | if (found) card.value = found; |
| | | } catch (error) { |
| | | // æä¸å°å°±æ²¿ç¨ç¼å |
| | | } |
| | | }; |
| | | |
| | | const loadRecharges = () => { |
| | | listRecharge({ current: -1, size: -1, cardId: cardId.value }) |
| | | .then(res => { |
| | | rechargeList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | rechargeList.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const refresh = () => { |
| | | loadCard(); |
| | | loadRecharges(); |
| | | }; |
| | | |
| | | const openForm = () => { |
| | | form.value = { |
| | | cardId: cardId.value, |
| | | rechargeAmount: "", |
| | | rechargeDate: parseTime(Date.now()), |
| | | remark: "", |
| | | }; |
| | | showForm.value = true; |
| | | }; |
| | | |
| | | const onDateConfirm = e => { |
| | | form.value.rechargeDate = parseTime(e.value); |
| | | showDatePicker.value = false; |
| | | }; |
| | | |
| | | const submitForm = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | submitting.value = true; |
| | | addRecharge({ |
| | | ...form.value, |
| | | rechargeAmount: Number(form.value.rechargeAmount || 0), |
| | | }) |
| | | .then(() => { |
| | | uni.showToast({ title: "å
弿å", icon: "success" }); |
| | | showForm.value = false; |
| | | refresh(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å
å¼å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | submitting.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleDelete = row => { |
| | | if (!row?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥ç¬å
å¼è®°å½åï¼ï¼å é¤åä½é¢å°èªå¨æ£åï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | delRecharge(row.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | refresh(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | cardId.value = options?.id || ""; |
| | | refresh(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .recharge-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding: 12px 12px 24px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 55%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .hero-balance { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: flex-end; |
| | | } |
| | | |
| | | .hero-label { |
| | | font-size: 12px; |
| | | color: #7a8599; |
| | | } |
| | | |
| | | .hero-value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .section { |
| | | margin-top: 12px; |
| | | padding: 12px; |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .empty-text { |
| | | padding: 24px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .recharge-item { |
| | | padding: 12px 0; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .recharge-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .recharge-main { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .recharge-amount { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #19be6b; |
| | | } |
| | | |
| | | .recharge-date { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .recharge-meta { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .recharge-user, |
| | | .recharge-remark { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .recharge-actions { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .form-panel { |
| | | padding: 16px; |
| | | padding-bottom: calc(16px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .form-panel__header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .form-panel__title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .form-panel__footer { |
| | | display: flex; |
| | | gap: 12px; |
| | | padding-top: 16px; |
| | | } |
| | | |
| | | .form-panel__btn { |
| | | flex: 1; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="fuel-record-detail"> |
| | | <PageHeader title="å æ²¹è®°å½è¯¦æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text v-if="isDraft" |
| | | class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ detail.plateNumber || "-" }}</text> |
| | | <up-tag :text="statusLabel" |
| | | :type="isDraft ? 'info' : 'success'" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <u-cell-group title="å æ²¹ä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="å æ²¹æ¥æ" |
| | | :value="detail.fuelDate || '-'" /> |
| | | <u-cell title="æå±é¨é¨" |
| | | :value="detail.deptName || '-'" /> |
| | | <u-cell title="å æ²¹è½¦è¾" |
| | | :value="detail.plateNumber || '-'" /> |
| | | <u-cell title="å æ²¹å¡å·" |
| | | :value="detail.cardNumber || '-'" /> |
| | | <u-cell title="å票å·ç " |
| | | :value="detail.invoiceNo || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éé¢ä¸éç¨" |
| | | class="detail-section"> |
| | | <u-cell title="éé¢" |
| | | :value="formatAmount(detail.amount)" /> |
| | | <u-cell title="åæ°(L)" |
| | | :value="detail.liters ?? '-'" /> |
| | | <u-cell title="èµ·å§éç¨(km)" |
| | | :value="detail.startMileage ?? '-'" /> |
| | | <u-cell title="ç»æéç¨(km)" |
| | | :value="detail.endMileage ?? '-'" /> |
| | | <u-cell title="æ¬æ¬¡è¡é©¶(km)" |
| | | :value="travelDistance" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="夿³¨" |
| | | class="detail-section"> |
| | | <u-cell title="夿³¨" |
| | | :value="detail.remark || '-'" /> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | |
| | | <view v-if="!loading" |
| | | class="footer-btns"> |
| | | <up-button class="footer-btn" |
| | | text="è¿å" |
| | | @click="goBack" /> |
| | | <up-button class="footer-btn" |
| | | :type="isDraft ? 'primary' : 'warning'" |
| | | :text="isDraft ? 'æäº¤' : 'æ¤å'" |
| | | @click="handleStatusAction" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { |
| | | listFuelRecord, |
| | | revokeFuelRecord, |
| | | submitFuelRecord, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const loading = ref(true); |
| | | const recordId = ref(""); |
| | | const detail = ref({}); |
| | | |
| | | // status å端è¿åå符串 '0' è稿 / '1' å·²æäº¤ |
| | | const isDraft = computed( |
| | | () => detail.value.status === "0" || !detail.value.status |
| | | ); |
| | | const statusLabel = computed(() => (isDraft.value ? "è稿" : "å·²æäº¤")); |
| | | |
| | | const travelDistance = computed(() => { |
| | | const start = Number(detail.value.startMileage); |
| | | const end = Number(detail.value.endMileage); |
| | | if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) { |
| | | return "-"; |
| | | } |
| | | return String(end - start); |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("fuelRecordDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/fuelRecord/edit?id=${recordId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | // å æ²¹è®°å½æ²¡æåæ¡è¯¦æ
æ¥å£ï¼ä»åè¡¨éæ id æ |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | const cached = uni.getStorageSync("fuelRecordDetail"); |
| | | if (cached && String(cached.id) === String(recordId.value)) { |
| | | detail.value = cached; |
| | | } |
| | | try { |
| | | const res = await listFuelRecord({ current: -1, size: -1 }); |
| | | const found = unwrapList(res).find( |
| | | item => String(item.id) === String(recordId.value) |
| | | ); |
| | | if (found) detail.value = found; |
| | | } catch (error) { |
| | | // æä¸å°å°±æ²¿ç¨ç¼å |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const handleStatusAction = () => { |
| | | if (!recordId.value) return; |
| | | const submitting = isDraft.value; |
| | | uni.showModal({ |
| | | title: submitting ? "æäº¤ç¡®è®¤" : "æ¤å确认", |
| | | content: submitting |
| | | ? "确认æäº¤è¯¥ç¬å 油记å½åï¼æäº¤åå°ä»å æ²¹å¡ä¸æ£é¤ä½é¢ã" |
| | | : "确认æ¤å该ç¬å 油记å½åï¼æ¤ååå°éè¿å æ²¹å¡ä½é¢ã", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | const action = submitting ? submitFuelRecord : revokeFuelRecord; |
| | | action(recordId.value) |
| | | .then(() => { |
| | | uni.showToast({ |
| | | title: submitting ? "æäº¤æå" : "æ¤åæå", |
| | | icon: "success", |
| | | }); |
| | | loadDetail(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ |
| | | title: submitting ? "æäº¤å¤±è´¥" : "æ¤å失败", |
| | | icon: "none", |
| | | }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | recordId.value = options?.id || ""; |
| | | if (!recordId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .fuel-record-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 90px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .footer-btns { |
| | | position: fixed; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | display: flex; |
| | | gap: 12px; |
| | | padding: 12px 16px; |
| | | padding-bottom: calc(12px + env(safe-area-inset-bottom)); |
| | | background: #ffffff; |
| | | box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05); |
| | | z-index: 1000; |
| | | } |
| | | |
| | | .footer-btn { |
| | | flex: 1; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="110" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="å æ²¹ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="å æ²¹è½¦è¾" |
| | | prop="vehicleId" |
| | | required> |
| | | <FormPicker v-model="form.vehicleId" |
| | | type="select" |
| | | title="éæ©å æ²¹è½¦è¾" |
| | | :options="vehicleOptions" |
| | | placeholder="è¯·éæ©å æ²¹è½¦è¾" /> |
| | | </up-form-item> |
| | | <up-form-item label="å
³èå æ²¹å¡" |
| | | prop="fuelCardId" |
| | | required> |
| | | <FormPicker v-model="form.fuelCardId" |
| | | type="select" |
| | | title="éæ©å æ²¹å¡" |
| | | :options="cardOptions" |
| | | placeholder="è¯·éæ©å æ²¹å¡" /> |
| | | <view v-if="currentBalanceText" |
| | | class="field-tip"> |
| | | <text>{{ currentBalanceText }}</text> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item label="å æ²¹æ¥æ" |
| | | prop="fuelDate" |
| | | required> |
| | | <FormPicker v-model="form.fuelDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©å æ²¹æ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="å票å·ç " |
| | | prop="invoiceNo"> |
| | | <up-input v-model="form.invoiceNo" |
| | | placeholder="请è¾å
¥å票å·ç " |
| | | clearable /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éé¢ä¸éç¨" |
| | | class="form-section"> |
| | | <up-form-item label="éé¢(å
)" |
| | | prop="amount" |
| | | required> |
| | | <up-input v-model="form.amount" |
| | | type="number" |
| | | placeholder="请è¾å
¥éé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="åæ°(L)" |
| | | prop="liters"> |
| | | <up-input v-model="form.liters" |
| | | type="number" |
| | | placeholder="请è¾å
¥åæ°" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="èµ·å§éç¨" |
| | | prop="startMileage"> |
| | | <up-input v-model="form.startMileage" |
| | | type="number" |
| | | placeholder="请è¾å
¥èµ·å§éç¨(km)" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç»æéç¨" |
| | | prop="endMileage"> |
| | | <up-input v-model="form.endMileage" |
| | | type="number" |
| | | placeholder="请è¾å
¥ç»æéç¨(km)" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <up-textarea v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | auto-height /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { |
| | | addFuelRecord, |
| | | listAllFuelCard, |
| | | listFuelRecord, |
| | | listVehicleInfo, |
| | | updateFuelRecord, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const recordId = ref(""); |
| | | const vehicleOptions = ref([]); |
| | | const allCards = ref([]); |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | vehicleId: "", |
| | | fuelCardId: "", |
| | | fuelDate: "", |
| | | invoiceNo: "", |
| | | amount: "", |
| | | liters: "", |
| | | startMileage: "", |
| | | endMileage: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | vehicleId: [{ required: true, message: "è¯·éæ©å æ²¹è½¦è¾", trigger: "change" }], |
| | | fuelCardId: [{ required: true, message: "è¯·éæ©å æ²¹å¡", trigger: "change" }], |
| | | fuelDate: [{ required: true, message: "è¯·éæ©å æ²¹æ¥æ", trigger: "change" }], |
| | | amount: [{ required: true, message: "请è¾å
¥éé¢", trigger: "blur" }], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => |
| | | recordId.value ? "ç¼è¾å 油记å½" : "æ°å¢å 油记å½" |
| | | ); |
| | | |
| | | // 已注éçå¡ä¸è½ç¨äºæ°å¢å æ²¹è®°å½ |
| | | const cardOptions = computed(() => |
| | | allCards.value |
| | | .filter(item => item.status !== "CANCELLED") |
| | | .map(item => ({ |
| | | name: `${item.cardNumber || ""} - ${item.issuer || ""}`, |
| | | value: item.id, |
| | | })) |
| | | ); |
| | | |
| | | const currentBalanceText = computed(() => { |
| | | if (!form.value.fuelCardId) return ""; |
| | | const card = allCards.value.find( |
| | | item => String(item.id) === String(form.value.fuelCardId) |
| | | ); |
| | | if (!card) return ""; |
| | | return `å½åä½é¢: ${Number(card.currentBalance || 0).toFixed(2)} å
`; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const loadOptions = async () => { |
| | | const [vehicles, cards] = await Promise.all([ |
| | | listVehicleInfo({ current: -1, size: -1 }).catch(() => ({})), |
| | | listAllFuelCard().catch(() => ({})), |
| | | ]); |
| | | const vehicleRows = |
| | | vehicles?.data?.records || vehicles?.records || vehicles?.data || []; |
| | | vehicleOptions.value = (Array.isArray(vehicleRows) ? vehicleRows : []).map( |
| | | item => ({ name: item.plateNumber, value: item.id }) |
| | | ); |
| | | const cardRows = cards?.data || []; |
| | | allCards.value = Array.isArray(cardRows) ? cardRows : []; |
| | | }; |
| | | |
| | | // å æ²¹è®°å½æ²¡æåæ¡è¯¦æ
æ¥å£ï¼ä»åè¡¨éæ id æ |
| | | const fetchRecord = async id => { |
| | | const res = await listFuelRecord({ current: -1, size: -1 }); |
| | | const rows = res?.data?.records || res?.records || res?.data || []; |
| | | const found = (Array.isArray(rows) ? rows : []).find( |
| | | item => String(item.id) === String(id) |
| | | ); |
| | | return { data: found }; |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | if (!recordId.value) return; |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "fuelRecordDetail", |
| | | id: recordId.value, |
| | | fetcher: fetchRecord, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "è·åå æ²¹è®°å½å¤±è´¥", icon: "none" }); |
| | | return; |
| | | } |
| | | form.value = { ...form.value, ...data, id: data.id }; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | const card = allCards.value.find( |
| | | item => String(item.id) === String(form.value.fuelCardId) |
| | | ); |
| | | const balance = Number(card?.currentBalance || 0); |
| | | const amount = Number(form.value.amount || 0); |
| | | if (!recordId.value && balance < amount) { |
| | | uni.showToast({ |
| | | title: `è¯¥å æ²¹å¡ä½é¢ä¸è¶³ï¼å½åä½é¢ ${balance} å
`, |
| | | icon: "none", |
| | | }); |
| | | } |
| | | |
| | | loading.value = true; |
| | | const action = recordId.value ? updateFuelRecord : addFuelRecord; |
| | | action({ |
| | | ...form.value, |
| | | id: recordId.value || undefined, |
| | | amount: amount, |
| | | liters: Number(form.value.liters || 0), |
| | | startMileage: Number(form.value.startMileage || 0), |
| | | endMileage: Number(form.value.endMileage || 0), |
| | | }) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | recordId.value = options.id; |
| | | form.value.id = options.id; |
| | | } else { |
| | | form.value.fuelDate = formatDateToYMD(Date.now()); |
| | | } |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | await loadOptions(); |
| | | if (recordId.value) await loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .field-tip { |
| | | padding-top: 4px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.plateNumber" |
| | | placeholder="请è¾å
¥è½¦çå·ç æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="recordList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in recordList" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="car-fill" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.plateNumber || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="statusLabel(item.status)" |
| | | :type="isDraft(item) ? 'info' : 'success'" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å æ²¹æ¥æ</text> |
| | | <text class="detail-value">{{ item.fuelDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æå±é¨é¨</text> |
| | | <text class="detail-value">{{ item.deptName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å æ²¹å¡å·</text> |
| | | <text class="detail-value">{{ item.cardNumber || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">éé¢</text> |
| | | <text class="detail-value highlight">{{ formatAmount(item.amount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åæ°</text> |
| | | <text class="detail-value">{{ item.liters ?? "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">èµ·å§éç¨</text> |
| | | <text class="detail-value">{{ item.startMileage ?? "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç»æéç¨</text> |
| | | <text class="detail-value">{{ item.endMileage ?? "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å票å·ç </text> |
| | | <text class="detail-value">{{ item.invoiceNo || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row" |
| | | v-if="item.remark"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remark }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button v-if="isDraft(item)" |
| | | class="action-btn" |
| | | size="small" |
| | | type="success" |
| | | @click="handleSubmitRow(item)">æäº¤</up-button> |
| | | <up-button v-if="isDraft(item)" |
| | | class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button v-if="isDraft(item)" |
| | | class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | <up-button v-if="!isDraft(item)" |
| | | class="action-btn" |
| | | size="small" |
| | | type="warning" |
| | | plain |
| | | @click="handleRevokeRow(item)">æ¤å</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ å æ²¹è®°å½æ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="å æ²¹è®°å½çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { |
| | | delFuelRecord, |
| | | listAllFuelCard, |
| | | listFuelRecord, |
| | | listVehicleInfo, |
| | | revokeFuelRecord, |
| | | submitFuelRecord, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const STATUS_OPTIONS = [ |
| | | { name: "è稿", value: "0" }, |
| | | { name: "å·²æäº¤", value: "1" }, |
| | | ]; |
| | | |
| | | const recordList = ref([]); |
| | | const showFilter = ref(false); |
| | | const deptOptions = ref([]); |
| | | const vehicleOptions = ref([]); |
| | | const cardOptions = ref([]); |
| | | |
| | | const query = reactive({ |
| | | plateNumber: "", |
| | | deptId: "", |
| | | vehicleId: "", |
| | | fuelCardId: "", |
| | | status: "", |
| | | fuelDateRange: [], |
| | | }); |
| | | |
| | | const filterFields = computed(() => [ |
| | | { |
| | | prop: "plateNumber", |
| | | label: "车çå·ç ", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥è½¦çå·ç ", |
| | | }, |
| | | { |
| | | prop: "deptId", |
| | | label: "æå±é¨é¨", |
| | | type: "select", |
| | | options: deptOptions.value, |
| | | }, |
| | | { |
| | | prop: "vehicleId", |
| | | label: "å æ²¹è½¦è¾", |
| | | type: "select", |
| | | options: vehicleOptions.value, |
| | | }, |
| | | { |
| | | prop: "fuelCardId", |
| | | label: "å æ²¹å¡", |
| | | type: "select", |
| | | options: cardOptions.value, |
| | | }, |
| | | { |
| | | prop: "status", |
| | | label: "ç¶æ", |
| | | type: "select", |
| | | options: STATUS_OPTIONS, |
| | | }, |
| | | { prop: "fuelDateRange", label: "å æ²¹æ¥æ", type: "daterange" }, |
| | | ]); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("fuelRecordDetail"); |
| | | uni.navigateTo({ url: "/pages/inventoryManagement/fuelRecord/edit" }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("fuelRecordDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/fuelRecord/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("fuelRecordDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/fuelRecord/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | // status å端è¿åå符串 '0' è稿 / '1' å·²æäº¤ |
| | | const isDraft = item => item?.status === "0" || !item?.status; |
| | | const statusLabel = status => (status === "1" ? "å·²æäº¤" : "è稿"); |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const getList = () => { |
| | | const [beginDate, endDate] = Array.isArray(query.fuelDateRange) |
| | | ? query.fuelDateRange |
| | | : []; |
| | | listFuelRecord({ |
| | | current: -1, |
| | | size: -1, |
| | | deptId: query.deptId || undefined, |
| | | vehicleId: query.vehicleId || undefined, |
| | | fuelCardId: query.fuelCardId || undefined, |
| | | status: query.status || undefined, |
| | | beginDate: beginDate || undefined, |
| | | endDate: endDate || undefined, |
| | | }) |
| | | .then(res => { |
| | | const rows = unwrapList(res); |
| | | // 车çæç´¢æ¡å¨ç鿡件鿲¡æå¯¹åºåæ°ï¼æ¬å°è¿æ»¤å
åº |
| | | recordList.value = query.plateNumber |
| | | ? rows.filter(item => |
| | | String(item.plateNumber || "").includes(query.plateNumber) |
| | | ) |
| | | : rows; |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleSubmitRow = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "æäº¤ç¡®è®¤", |
| | | content: "确认æäº¤è¯¥ç¬å 油记å½åï¼æäº¤åå°ä»å æ²¹å¡ä¸æ£é¤ä½é¢ã", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | submitFuelRecord(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "æäº¤æå", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æäº¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const handleRevokeRow = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "æ¤å确认", |
| | | content: "确认æ¤å该ç¬å 油记å½åï¼æ¤ååå°éè¿å æ²¹å¡ä½é¢ã", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | revokeFuelRecord(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "æ¤åæå", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¤å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥è稿记å½åï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delFuelRecord(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | let optionsLoaded = false; |
| | | |
| | | const loadOptions = () => { |
| | | if (optionsLoaded) return; |
| | | optionsLoaded = true; |
| | | Promise.all([ |
| | | getDept().catch(() => ({})), |
| | | listVehicleInfo({ current: -1, size: -1 }).catch(() => ({})), |
| | | listAllFuelCard().catch(() => ({})), |
| | | ]) |
| | | .then(([depts, vehicles, cards]) => { |
| | | deptOptions.value = (depts?.data || []).map(item => ({ |
| | | name: item.deptName, |
| | | value: item.deptId, |
| | | })); |
| | | vehicleOptions.value = unwrapList(vehicles).map(item => ({ |
| | | name: item.plateNumber, |
| | | value: item.id, |
| | | })); |
| | | cardOptions.value = unwrapList(cards).map(item => ({ |
| | | name: `${item.cardNumber || ""} - ${item.issuer || ""}`, |
| | | value: item.id, |
| | | })); |
| | | }) |
| | | .catch(() => { |
| | | optionsLoaded = false; |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | loadOptions(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="vehicle-detail"> |
| | | <PageHeader title="车è¾è¯¦æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ detail.plateNumber || "-" }}</text> |
| | | <up-tag :text="statusLabel(detail.vehicleStatus)" |
| | | :type="statusTagType(detail.vehicleStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <view v-if="warnItems.length" |
| | | class="warn-banner"> |
| | | <up-icon name="error-circle" |
| | | size="16" |
| | | color="#ff9900"></up-icon> |
| | | <text class="warn-text">{{ warnItems.join("ï¼") }}</text> |
| | | </view> |
| | | |
| | | <u-cell-group title="车è¾ä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="车çå·ç " |
| | | :value="detail.plateNumber || '-'" /> |
| | | <u-cell title="äº§æææ" |
| | | :value="detail.propertyOwnership || '-'" /> |
| | | <u-cell title="æå±é¨é¨" |
| | | :value="detail.deptName || '-'" /> |
| | | <u-cell title="责任人" |
| | | :value="detail.userName || '-'" /> |
| | | <u-cell title="ç¶æ" |
| | | :value="statusLabel(detail.vehicleStatus)" /> |
| | | <u-cell title="å¹´æ£å°æ" |
| | | :value="detail.inspectionExpireDate || '-'" /> |
| | | <u-cell title="ä¿é©å°æ" |
| | | :value="detail.insuranceExpireDate || '-'" /> |
| | | <u-cell title="夿³¨" |
| | | :value="detail.remark || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="è´¹ç¨ç®¡ç" |
| | | class="detail-section"> |
| | | <view class="section-tools"> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="æ°å¢è´¹ç¨" |
| | | @click="openCostForm" /> |
| | | </view> |
| | | <view v-if="costList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ è´¹ç¨è®°å½</text> |
| | | </view> |
| | | <view v-else |
| | | class="cost-list"> |
| | | <view v-for="cost in costList" |
| | | :key="cost.id" |
| | | class="cost-item"> |
| | | <view class="cost-main"> |
| | | <text class="cost-type">{{ costTypeLabel(cost.costType) }}</text> |
| | | <text class="cost-amount">{{ formatAmount(cost.amount) }}</text> |
| | | </view> |
| | | <view class="cost-meta"> |
| | | <text class="cost-date">{{ cost.costDate || "-" }}</text> |
| | | <text v-if="cost.remark" |
| | | class="cost-remark">{{ cost.remark }}</text> |
| | | </view> |
| | | <view class="cost-actions"> |
| | | <up-button size="mini" |
| | | type="error" |
| | | plain |
| | | text="å é¤" |
| | | @click="handleDeleteCost(cost)" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | |
| | | <up-popup :show="showCostForm" |
| | | mode="bottom" |
| | | :round="16" |
| | | @close="showCostForm = false"> |
| | | <view class="cost-form"> |
| | | <view class="cost-form__header"> |
| | | <text class="cost-form__title">æ°å¢è´¹ç¨</text> |
| | | <u-icon name="close" |
| | | size="20" |
| | | color="#909399" |
| | | @click="showCostForm = false"></u-icon> |
| | | </view> |
| | | <up-form ref="costFormRef" |
| | | :model="costForm" |
| | | :rules="costRules" |
| | | label-width="90" |
| | | input-align="right"> |
| | | <up-form-item label="è´¹ç¨ç±»å" |
| | | prop="costType" |
| | | required> |
| | | <FormPicker v-model="costForm.costType" |
| | | type="select" |
| | | title="éæ©è´¹ç¨ç±»å" |
| | | :options="COST_TYPE_OPTIONS" |
| | | placeholder="è¯·éæ©è´¹ç¨ç±»å" /> |
| | | </up-form-item> |
| | | <up-form-item label="éé¢(å
)" |
| | | prop="amount" |
| | | required> |
| | | <up-input v-model="costForm.amount" |
| | | type="number" |
| | | placeholder="请è¾å
¥éé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="åçæ¥æ" |
| | | prop="costDate" |
| | | required> |
| | | <FormPicker v-model="costForm.costDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©åçæ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <up-input v-model="costForm.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | clearable /> |
| | | </up-form-item> |
| | | </up-form> |
| | | <view class="cost-form__footer"> |
| | | <up-button class="cost-form__btn" |
| | | text="åæ¶" |
| | | @click="showCostForm = false" /> |
| | | <up-button class="cost-form__btn" |
| | | type="primary" |
| | | text="ç¡®å®" |
| | | :loading="costSubmitting" |
| | | @click="submitCost" /> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { getWarnInfo } from "@/utils/warning"; |
| | | import { |
| | | addVehicleCost, |
| | | delVehicleCost, |
| | | listVehicleCost, |
| | | listVehicleInfo, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const COST_TYPE_OPTIONS = [ |
| | | { name: "ä¿å
»", value: "MAINTENANCE" }, |
| | | { name: "å¹´æ£", value: "INSPECTION" }, |
| | | { name: "ä¿é©", value: "INSURANCE" }, |
| | | { name: "ç»´ä¿®", value: "REPAIR" }, |
| | | ]; |
| | | |
| | | const loading = ref(true); |
| | | const vehicleId = ref(""); |
| | | const detail = ref({}); |
| | | const costList = ref([]); |
| | | const showCostForm = ref(false); |
| | | const costSubmitting = ref(false); |
| | | const costFormRef = ref(); |
| | | |
| | | const costForm = ref({ |
| | | vehicleId: "", |
| | | costType: "", |
| | | amount: "", |
| | | costDate: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const costRules = { |
| | | costType: [{ required: true, message: "è¯·éæ©è´¹ç¨ç±»å", trigger: "change" }], |
| | | amount: [{ required: true, message: "请è¾å
¥éé¢", trigger: "blur" }], |
| | | costDate: [{ required: true, message: "è¯·éæ©åçæ¥æ", trigger: "change" }], |
| | | }; |
| | | |
| | | // éæ±åä¸ï¼å¹´æ£/ä¿é©å°æå 30 天é¢è¦ |
| | | const warnItems = computed(() => { |
| | | const items = []; |
| | | const inspection = getWarnInfo(detail.value.inspectionExpireDate, 30); |
| | | if (inspection.text && inspection.level !== "normal") { |
| | | items.push(`å¹´æ£${inspection.text}`); |
| | | } |
| | | const insurance = getWarnInfo(detail.value.insuranceExpireDate, 30); |
| | | if (insurance.text && insurance.level !== "normal") { |
| | | items.push(`ä¿é©${insurance.text}`); |
| | | } |
| | | return items; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("vehicleInfoDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/vehicleInfo/edit?id=${vehicleId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const statusLabel = status => { |
| | | if (status === "NORMAL") return "å¨ç¨"; |
| | | if (status === "IDLE") return "é²ç½®"; |
| | | if (status === "REPAIR") return "ç»´ä¿®"; |
| | | return status || "æªå¡«ç¶æ"; |
| | | }; |
| | | |
| | | const statusTagType = status => { |
| | | if (status === "NORMAL") return "success"; |
| | | if (status === "IDLE") return "info"; |
| | | if (status === "REPAIR") return "warning"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const costTypeLabel = type => |
| | | COST_TYPE_OPTIONS.find(item => item.value === type)?.name || type || "-"; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | // è½¦è¾æ¨¡å没æåæ¡è¯¦æ
æ¥å£ï¼ä»åè¡¨éæ id æ |
| | | const loadVehicle = async () => { |
| | | const cached = uni.getStorageSync("vehicleInfoDetail"); |
| | | if (cached && String(cached.id) === String(vehicleId.value)) { |
| | | detail.value = cached; |
| | | } |
| | | try { |
| | | const res = await listVehicleInfo({ current: -1, size: -1 }); |
| | | const rows = unwrapList(res); |
| | | const found = rows.find( |
| | | item => String(item.id) === String(vehicleId.value) |
| | | ); |
| | | if (found) detail.value = found; |
| | | } catch (error) { |
| | | // æä¸å°å°±æ²¿ç¨ç¼åï¼ä¸é¢è¿æç©ºæå
åº |
| | | } |
| | | }; |
| | | |
| | | const loadCosts = () => { |
| | | listVehicleCost({ current: -1, size: -1, vehicleId: vehicleId.value }) |
| | | .then(res => { |
| | | costList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | costList.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | await loadVehicle(); |
| | | loadCosts(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const openCostForm = () => { |
| | | costForm.value = { |
| | | vehicleId: vehicleId.value, |
| | | costType: "", |
| | | amount: "", |
| | | costDate: formatDateToYMD(Date.now()), |
| | | remark: "", |
| | | }; |
| | | showCostForm.value = true; |
| | | }; |
| | | |
| | | const submitCost = async () => { |
| | | const valid = await costFormRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | costSubmitting.value = true; |
| | | addVehicleCost({ |
| | | ...costForm.value, |
| | | amount: Number(costForm.value.amount || 0), |
| | | }) |
| | | .then(() => { |
| | | uni.showToast({ title: "æ·»å æå", icon: "success" }); |
| | | showCostForm.value = false; |
| | | loadCosts(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ·»å 失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | costSubmitting.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleDeleteCost = cost => { |
| | | if (!cost?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥è´¹ç¨è®°å½åï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | delVehicleCost(cost.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | loadCosts(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | vehicleId.value = options?.id || ""; |
| | | if (!vehicleId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .vehicle-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .warn-banner { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | margin-top: 12px; |
| | | padding: 10px 14px; |
| | | border-radius: 10px; |
| | | background: #fff7ea; |
| | | } |
| | | |
| | | .warn-text { |
| | | flex: 1; |
| | | font-size: 13px; |
| | | color: #e6a23c; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .section-tools { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .empty-text { |
| | | padding: 16px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .cost-list { |
| | | padding: 6px 12px 12px; |
| | | } |
| | | |
| | | .cost-item { |
| | | padding: 12px 0; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .cost-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .cost-main { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .cost-type { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .cost-amount { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .cost-meta { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .cost-date { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .cost-remark { |
| | | flex: 1; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .cost-actions { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .cost-form { |
| | | padding: 16px; |
| | | padding-bottom: calc(16px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .cost-form__header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .cost-form__title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .cost-form__footer { |
| | | display: flex; |
| | | gap: 12px; |
| | | padding-top: 16px; |
| | | } |
| | | |
| | | .cost-form__btn { |
| | | flex: 1; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="110" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="车è¾ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="车çå·ç " |
| | | prop="plateNumber" |
| | | required> |
| | | <up-input v-model="form.plateNumber" |
| | | placeholder="请è¾å
¥è½¦çå·ç " |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="äº§æææ" |
| | | prop="propertyOwnership"> |
| | | <up-input v-model="form.propertyOwnership" |
| | | placeholder="å¦ï¼èªæãç§èµç" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æå±é¨é¨" |
| | | prop="deptId" |
| | | required> |
| | | <FormPicker v-model="form.deptId" |
| | | type="select" |
| | | title="éæ©æå±é¨é¨" |
| | | :options="deptOptions" |
| | | placeholder="è¯·éæ©æå±é¨é¨" |
| | | @change="onDeptChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="责任人" |
| | | prop="userId"> |
| | | <FormPicker v-model="form.userId" |
| | | type="select" |
| | | title="éæ©è´£ä»»äºº" |
| | | :options="userOptions" |
| | | placeholder="è¯·éæ©è´£ä»»äºº" |
| | | @change="onUserChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¶æ" |
| | | prop="vehicleStatus" |
| | | required> |
| | | <FormPicker v-model="form.vehicleStatus" |
| | | type="select" |
| | | title="éæ©ç¶æ" |
| | | :options="VEHICLE_STATUS_OPTIONS" |
| | | placeholder="è¯·éæ©ç¶æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="å¹´æ£å°æ" |
| | | prop="inspectionExpireDate"> |
| | | <FormPicker v-model="form.inspectionExpireDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©å¹´æ£å°ææ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="ä¿é©å°æ" |
| | | prop="insuranceExpireDate"> |
| | | <FormPicker v-model="form.insuranceExpireDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ä¿é©å°ææ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <up-textarea v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | auto-height /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { |
| | | addVehicleInfo, |
| | | listVehicleInfo, |
| | | updateVehicleInfo, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const VEHICLE_STATUS_OPTIONS = [ |
| | | { name: "å¨ç¨", value: "NORMAL" }, |
| | | { name: "é²ç½®", value: "IDLE" }, |
| | | { name: "ç»´ä¿®", value: "REPAIR" }, |
| | | ]; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const vehicleId = ref(""); |
| | | const deptOptions = ref([]); |
| | | const userOptions = ref([]); |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | plateNumber: "", |
| | | propertyOwnership: "", |
| | | deptId: "", |
| | | deptName: "", |
| | | userId: "", |
| | | userName: "", |
| | | vehicleStatus: "NORMAL", |
| | | inspectionExpireDate: "", |
| | | insuranceExpireDate: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | plateNumber: [ |
| | | { required: true, message: "请è¾å
¥è½¦çå·ç ", trigger: "blur" }, |
| | | ], |
| | | deptId: [{ required: true, message: "è¯·éæ©æå±é¨é¨", trigger: "change" }], |
| | | vehicleStatus: [{ required: true, message: "è¯·éæ©ç¶æ", trigger: "change" }], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => (vehicleId.value ? "ç¼è¾è½¦è¾" : "æ°å¢è½¦è¾")); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const onDeptChange = (value, item) => { |
| | | form.value.deptName = item?.name || ""; |
| | | }; |
| | | |
| | | const onUserChange = (value, item) => { |
| | | form.value.userName = item?.name || ""; |
| | | }; |
| | | |
| | | const loadOptions = async () => { |
| | | const [depts, users] = await Promise.all([ |
| | | getDept().catch(() => ({})), |
| | | userListNoPageByTenantId().catch(() => ({})), |
| | | ]); |
| | | deptOptions.value = (depts?.data || []).map(item => ({ |
| | | name: item.deptName, |
| | | value: item.deptId, |
| | | })); |
| | | const rows = users?.data || []; |
| | | userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({ |
| | | name: item.nickName || item.userName || `ç¨æ·${item.userId}`, |
| | | value: item.userId, |
| | | })); |
| | | }; |
| | | |
| | | // è½¦è¾æ¨¡å没æåæ¡è¯¦æ
æ¥å£ï¼ä»åè¡¨éæ id æï¼å
è£
æ loadDetailData ææçååºå½¢ç¶ |
| | | const fetchVehicle = async id => { |
| | | const res = await listVehicleInfo({ current: -1, size: -1 }); |
| | | const rows = res?.data?.records || res?.records || res?.data || []; |
| | | const found = (Array.isArray(rows) ? rows : []).find( |
| | | item => String(item.id) === String(id) |
| | | ); |
| | | return { data: found }; |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | if (!vehicleId.value) return; |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "vehicleInfoDetail", |
| | | id: vehicleId.value, |
| | | fetcher: fetchVehicle, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "è·å车è¾è¯¦æ
失败", icon: "none" }); |
| | | return; |
| | | } |
| | | form.value = { ...form.value, ...data, id: data.id }; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | loading.value = true; |
| | | const action = vehicleId.value ? updateVehicleInfo : addVehicleInfo; |
| | | action({ ...form.value, id: vehicleId.value || undefined }) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | vehicleId.value = options.id; |
| | | form.value.id = options.id; |
| | | } |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | await loadOptions(); |
| | | if (vehicleId.value) await loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.plateNumber" |
| | | placeholder="请è¾å
¥è½¦çå·ç æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="warn-bar"> |
| | | <up-button :type="showWarning ? 'warning' : 'primary'" |
| | | size="small" |
| | | :plain="!showWarning" |
| | | :text="showWarning ? 'è¿åå
¨é¨è½¦è¾' : 'æ¥ç 30 天å
å°æé¢è¦'" |
| | | @click="toggleWarning" /> |
| | | </view> |
| | | |
| | | <view v-if="vehicleList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in vehicleList" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="car" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.plateNumber || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="statusLabel(item.vehicleStatus)" |
| | | :type="statusTagType(item.vehicleStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æå±é¨é¨</text> |
| | | <text class="detail-value">{{ item.deptName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">责任人</text> |
| | | <text class="detail-value">{{ item.userName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">äº§æææ</text> |
| | | <text class="detail-value">{{ item.propertyOwnership || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¹´æ£å°æ</text> |
| | | <text class="detail-value">{{ item.inspectionExpireDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row" |
| | | v-if="inspectionWarn(item).text"> |
| | | <text class="detail-label">å¹´æ£é¢è¦</text> |
| | | <text class="detail-value" |
| | | :style="{ color: inspectionWarn(item).color }"> |
| | | {{ inspectionWarn(item).text }} |
| | | </text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä¿é©å°æ</text> |
| | | <text class="detail-value">{{ item.insuranceExpireDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row" |
| | | v-if="insuranceWarn(item).text"> |
| | | <text class="detail-label">ä¿é©é¢è¦</text> |
| | | <text class="detail-value" |
| | | :style="{ color: insuranceWarn(item).color }"> |
| | | {{ insuranceWarn(item).text }} |
| | | </text> |
| | | </view> |
| | | <view class="detail-row" |
| | | v-if="item.remark"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remark }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="success" |
| | | @click="goCost(item)">è´¹ç¨ç®¡ç</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>{{ showWarning ? "ææ 30 天å
å°æç车è¾" : "ææ è½¦è¾æ°æ®" }}</text> |
| | | </view> |
| | | |
| | | <view v-if="!showWarning" |
| | | class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="车è¾çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { getWarnInfo } from "@/utils/warning"; |
| | | import { |
| | | delVehicleInfo, |
| | | getVehicleWarningList, |
| | | listVehicleInfo, |
| | | } from "@/api/inventoryManagement/vehicle"; |
| | | |
| | | const VEHICLE_STATUS_OPTIONS = [ |
| | | { name: "å¨ç¨", value: "NORMAL" }, |
| | | { name: "é²ç½®", value: "IDLE" }, |
| | | { name: "ç»´ä¿®", value: "REPAIR" }, |
| | | ]; |
| | | |
| | | const vehicleList = ref([]); |
| | | const showFilter = ref(false); |
| | | const showWarning = ref(false); |
| | | const deptOptions = ref([]); |
| | | |
| | | const query = reactive({ |
| | | plateNumber: "", |
| | | deptId: "", |
| | | vehicleStatus: "", |
| | | }); |
| | | |
| | | const filterFields = computed(() => [ |
| | | { |
| | | prop: "plateNumber", |
| | | label: "车çå·ç ", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥è½¦çå·ç ", |
| | | }, |
| | | { |
| | | prop: "deptId", |
| | | label: "æå±é¨é¨", |
| | | type: "select", |
| | | options: deptOptions.value, |
| | | }, |
| | | { |
| | | prop: "vehicleStatus", |
| | | label: "ç¶æ", |
| | | type: "select", |
| | | options: VEHICLE_STATUS_OPTIONS, |
| | | }, |
| | | ]); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("vehicleInfoDetail"); |
| | | uni.navigateTo({ url: "/pages/inventoryManagement/vehicleInfo/edit" }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("vehicleInfoDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/vehicleInfo/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("vehicleInfoDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/vehicleInfo/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goCost = item => { |
| | | uni.setStorageSync("vehicleInfoDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/inventoryManagement/vehicleInfo/detail?id=${item.id}&tab=cost`, |
| | | }); |
| | | }; |
| | | |
| | | const statusLabel = status => { |
| | | if (status === "NORMAL") return "å¨ç¨"; |
| | | if (status === "IDLE") return "é²ç½®"; |
| | | if (status === "REPAIR") return "ç»´ä¿®"; |
| | | return status || "æªå¡«ç¶æ"; |
| | | }; |
| | | |
| | | const statusTagType = status => { |
| | | if (status === "NORMAL") return "success"; |
| | | if (status === "IDLE") return "info"; |
| | | if (status === "REPAIR") return "warning"; |
| | | return "info"; |
| | | }; |
| | | |
| | | // éæ±åä¸ï¼è½¦è¾å¹´æ£/ä¿é©å°æå 30 天é¢è¦ |
| | | const inspectionWarn = item => getWarnInfo(item?.inspectionExpireDate, 30); |
| | | const insuranceWarn = item => getWarnInfo(item?.insuranceExpireDate, 30); |
| | | |
| | | const getList = () => { |
| | | if (showWarning.value) return; |
| | | listVehicleInfo({ |
| | | current: -1, |
| | | size: -1, |
| | | plateNumber: query.plateNumber || undefined, |
| | | deptId: query.deptId || undefined, |
| | | vehicleStatus: query.vehicleStatus || undefined, |
| | | }) |
| | | .then(res => { |
| | | vehicleList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const toggleWarning = () => { |
| | | if (showWarning.value) { |
| | | showWarning.value = false; |
| | | getList(); |
| | | return; |
| | | } |
| | | getVehicleWarningList() |
| | | .then(res => { |
| | | vehicleList.value = unwrapList(res); |
| | | showWarning.value = true; |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "è·åé¢è¦è½¦è¾å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | showWarning.value = false; |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: `确认å é¤è½¦è¾ã${item.plateNumber || ""}ãåï¼`, |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delVehicleInfo(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | let deptsLoaded = false; |
| | | |
| | | const loadDepts = () => { |
| | | if (deptsLoaded) return; |
| | | deptsLoaded = true; |
| | | getDept() |
| | | .then(res => { |
| | | deptOptions.value = (res?.data || []).map(item => ({ |
| | | name: item.deptName, |
| | | value: item.deptId, |
| | | })); |
| | | }) |
| | | .catch(() => { |
| | | deptsLoaded = false; |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | loadDepts(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .warn-bar { |
| | | padding: 0 12px 8px; |
| | | } |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| | |
| | | <text class="rd-section-title">æ¶æ¬¾ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="rd-group"> |
| | | <view v-if="!isTravel" |
| | | class="rd-cell"> |
| | | <text class="rd-label">仿¬¾æ¹</text> |
| | | <text class="rd-value">{{ r.payer || "â" }}</text> |
| | | </view> |
| | | <view class="rd-cell"> |
| | | <text class="rd-label">æ¶æ¬¾äºº</text> |
| | | <text class="rd-value">{{ r.payeeName || r.payee || "â" }}</text> |
| | |
| | | expenseCategory: "other", |
| | | reimburseReason: "", |
| | | applyAmount: "", |
| | | payer: "", |
| | | payee: "", |
| | | payeeAccount: "", |
| | | bankBranch: "", |
| | |
| | | input-align="right" /> |
| | | </view> |
| | | </view> |
| | | <view v-if="!isTravel" |
| | | class="rf-cell"> |
| | | <text class="rf-label">仿¬¾æ¹</text> |
| | | <view class="rf-input-body"> |
| | | <up-input v-model="form.payer" |
| | | placeholder="éå¡«" |
| | | border="none" |
| | | input-align="right" /> |
| | | </view> |
| | | </view> |
| | | <view class="rf-cell"> |
| | | <text class="rf-label required">æ¶æ¬¾äºº</text> |
| | | <view class="rf-input-body"> |
| | |
| | | applyAmount: row.applyAmount, |
| | | applyTime: formatReimbursementDateTime(row.createTime), |
| | | createTime: formatReimbursementDateTime(row.createTime), |
| | | payer: row.payer || "", |
| | | payee: row.payeeName || "", |
| | | payeeAccount: row.payeeAccount || "", |
| | | bankBranch: row.payeeBank || "", |
| | |
| | | reason: (form.reimburseReason || "").trim(), |
| | | applyAmount, |
| | | detailTotalAmount: detailTotal, |
| | | payer: form.payer || "", |
| | | payeeName: form.payee || "", |
| | | payeeAccount: form.payeeAccount || "", |
| | | payeeBank: form.bankBranch || form.payeeBank || "", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | æ¶è´§å°åå表çä¸è¡ |
| | | |
| | | å端 shippingAddress åªå䏿¡ï¼consignee=æ¶è´§äººãcontract=èç³»çµè¯ãaddress=å°åï¼ï¼ |
| | | æä»¥è¿ä¸ªå表å¨è¡¨åéåªä¼æä¸è¡ï¼æäº¤æ¶æ mapper å第ä¸è¡ã |
| | | --> |
| | | <template> |
| | | <view class="address-card"> |
| | | <view class="address-header"> |
| | | <text class="address-title">æ¶è´§å°å</text> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | |
| | | <view class="address-body"> |
| | | <view class="field-row"> |
| | | <text class="field-label">æ¶è´§äºº</text> |
| | | <u-input v-model="row.receiver" |
| | | placeholder="请è¾å
¥æ¶è´§äºº" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">èç³»æ¹å¼</text> |
| | | <u-input v-model="row.phone" |
| | | type="number" |
| | | placeholder="请è¾å
¥èç³»æ¹å¼" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">æ¶è´§å°å</text> |
| | | <u-input v-model="row.address" |
| | | placeholder="请è¾å
¥æ¶è´§å°å" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | defineProps({ |
| | | row: { |
| | | type: Object, |
| | | required: true, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .address-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 0 12px 12px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .address-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .address-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .address-body { |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | .field-row { |
| | | display: flex; |
| | | align-items: center; |
| | | min-height: 44px; |
| | | border-bottom: 1px solid #f5f6fa; |
| | | } |
| | | |
| | | .field-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .field-label { |
| | | width: 96px; |
| | | flex-shrink: 0; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 项ç®äº§åä¿¡æ¯å表çä¸è¡ |
| | | |
| | | row ç±ç¶çº§ productList çå
ç´ ç´æ¥ä¼ å
¥ï¼åä¸ä¸ªååºå¼å¯¹è±¡ï¼ï¼ |
| | | ç»ä»¶å
ç´æ¥æ¹ row.xxx å³å¯ï¼ä¸éè¦ emit åç¶çº§åå¹¶ã |
| | | 产å大类 / è§æ ¼åå·èµ° up-action-sheetï¼éé¡¹ç±æ¬ç»ä»¶èªå·±æåã |
| | | --> |
| | | <template> |
| | | <view class="product-card"> |
| | | <view class="product-header"> |
| | | <text class="product-title">产å {{ index + 1 }}</text> |
| | | <up-icon name="trash" |
| | | color="#ee0a24" |
| | | size="18" |
| | | @click="$emit('remove')"></up-icon> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | |
| | | <view class="product-body"> |
| | | <view class="field-row" |
| | | @click="openCategory"> |
| | | <text class="field-label">产å大类</text> |
| | | <text :class="['field-value', row.productCategory ? '' : 'placeholder']"> |
| | | {{ row.productCategory || "è¯·éæ©äº§å大类" }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#c0c4cc"></up-icon> |
| | | </view> |
| | | |
| | | <view class="field-row" |
| | | @click="openModel"> |
| | | <text class="field-label">è§æ ¼åå·</text> |
| | | <text :class="['field-value', row.specificationModel ? '' : 'placeholder']"> |
| | | {{ row.specificationModel || "è¯·éæ©è§æ ¼åå·" }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#c0c4cc"></up-icon> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">åä½</text> |
| | | <u-input v-model="row.unit" |
| | | placeholder="请è¾å
¥åä½" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">æ°é</text> |
| | | <u-input v-model="row.quantity" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ°é" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | |
| | | <view class="field-row" |
| | | @click="openTaxRate"> |
| | | <text class="field-label">ç¨ç(%)</text> |
| | | <text :class="['field-value', row.taxRate ? '' : 'placeholder']"> |
| | | {{ taxRateText || "è¯·éæ©ç¨ç" }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#c0c4cc"></up-icon> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">å«ç¨åä»·</text> |
| | | <u-input v-model="row.taxInclusiveUnitPrice" |
| | | type="number" |
| | | placeholder="请è¾å
¥å«ç¨åä»·" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">å«ç¨æ»ä»·</text> |
| | | <text class="field-value readonly">{{ formatAmount(inclusiveTotal) }}</text> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">ä¸å«ç¨æ»ä»·</text> |
| | | <u-input v-model="row.taxExclusiveTotalPrice" |
| | | type="number" |
| | | placeholder="请è¾å
¥ä¸å«ç¨æ»ä»·" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | |
| | | <view class="field-row" |
| | | @click="openInvoiceType"> |
| | | <text class="field-label">å票类å</text> |
| | | <text :class="['field-value', row.invoiceType ? '' : 'placeholder']"> |
| | | {{ row.invoiceType || "è¯·éæ©å票类å" }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#c0c4cc"></up-icon> |
| | | </view> |
| | | </view> |
| | | |
| | | <up-action-sheet :show="showCategorySheet" |
| | | title="éæ©äº§å大类" |
| | | :actions="categoryActions" |
| | | @select="onSelectCategory" |
| | | @close="showCategorySheet = false"></up-action-sheet> |
| | | |
| | | <up-action-sheet :show="showModelSheet" |
| | | title="éæ©è§æ ¼åå·" |
| | | :actions="modelActions" |
| | | @select="onSelectModel" |
| | | @close="showModelSheet = false"></up-action-sheet> |
| | | |
| | | <up-action-sheet :show="showTaxRateSheet" |
| | | title="éæ©ç¨ç" |
| | | :actions="taxRateActions" |
| | | @select="onSelectTaxRate" |
| | | @close="showTaxRateSheet = false"></up-action-sheet> |
| | | |
| | | <up-action-sheet :show="showInvoiceSheet" |
| | | title="éæ©å票类å" |
| | | :actions="INVOICE_TYPE_ACTIONS" |
| | | @select="onSelectInvoiceType" |
| | | @close="showInvoiceSheet = false"></up-action-sheet> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { modelList, productTreeList } from "@/api/basicData/product"; |
| | | import { productInclusiveTotal } from "../_utils/mapper"; |
| | | |
| | | const INVOICE_TYPE_ACTIONS = [ |
| | | { name: "墿®ç¥¨", value: "墿®ç¥¨" }, |
| | | { name: "å¢ä¸ç¥¨", value: "å¢ä¸ç¥¨" }, |
| | | ]; |
| | | |
| | | const props = defineProps({ |
| | | row: { |
| | | type: Object, |
| | | required: true, |
| | | }, |
| | | index: { |
| | | type: Number, |
| | | default: 0, |
| | | }, |
| | | // ç¨çåå
¸ï¼useDict('tax_rate') çè¿åå¼ï¼ |
| | | taxRateOptions: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | }); |
| | | |
| | | defineEmits(["remove"]); |
| | | |
| | | const categoryActions = ref([]); |
| | | const modelActions = ref([]); |
| | | const showCategorySheet = ref(false); |
| | | const showModelSheet = ref(false); |
| | | const showTaxRateSheet = ref(false); |
| | | const showInvoiceSheet = ref(false); |
| | | |
| | | const inclusiveTotal = computed(() => productInclusiveTotal(props.row)); |
| | | |
| | | const taxRateActions = computed(() => |
| | | (props.taxRateOptions || []).map(item => ({ |
| | | name: item.label, |
| | | value: item.value, |
| | | })) |
| | | ); |
| | | |
| | | const taxRateText = computed(() => { |
| | | const matched = (props.taxRateOptions || []).find( |
| | | item => String(item.value) === String(props.row.taxRate) |
| | | ); |
| | | return matched ? matched.label : ""; |
| | | }); |
| | | |
| | | const formatAmount = value => Number(value || 0).toFixed(2); |
| | | |
| | | const flattenTree = nodes => { |
| | | const result = []; |
| | | const walk = list => { |
| | | (list || []).forEach(item => { |
| | | if (item.children && item.children.length) { |
| | | walk(item.children); |
| | | } else { |
| | | result.push({ |
| | | name: item.label || item.productName || "", |
| | | value: item.id || item.value, |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | walk(nodes); |
| | | return result; |
| | | }; |
| | | |
| | | const loadCategories = () => { |
| | | productTreeList() |
| | | .then(res => { |
| | | const nodes = Array.isArray(res) ? res : res?.data || []; |
| | | categoryActions.value = flattenTree(nodes); |
| | | }) |
| | | .catch(() => { |
| | | categoryActions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const openCategory = () => { |
| | | if (!categoryActions.value.length) loadCategories(); |
| | | showCategorySheet.value = true; |
| | | }; |
| | | |
| | | const openModel = () => { |
| | | if (!props.row.productCategoryId) { |
| | | uni.showToast({ title: "请å
éæ©äº§å大类", icon: "none" }); |
| | | return; |
| | | } |
| | | if (!modelActions.value.length) { |
| | | uni.showToast({ title: "ææ è§æ ¼åå·", icon: "none" }); |
| | | return; |
| | | } |
| | | showModelSheet.value = true; |
| | | }; |
| | | |
| | | const openTaxRate = () => { |
| | | if (!taxRateActions.value.length) { |
| | | uni.showToast({ title: "ææ ç¨çé项", icon: "none" }); |
| | | return; |
| | | } |
| | | showTaxRateSheet.value = true; |
| | | }; |
| | | |
| | | const openInvoiceType = () => { |
| | | showInvoiceSheet.value = true; |
| | | }; |
| | | |
| | | const onSelectCategory = action => { |
| | | showCategorySheet.value = false; |
| | | props.row.productCategoryId = action.value; |
| | | props.row.productCategory = action.name; |
| | | props.row.productModelId = ""; |
| | | props.row.specificationModel = ""; |
| | | props.row.unit = ""; |
| | | modelActions.value = []; |
| | | modelList({ id: action.value }) |
| | | .then(res => { |
| | | const rows = res?.data?.records || res?.data || res?.records || res || []; |
| | | modelActions.value = (Array.isArray(rows) ? rows : []).map(item => ({ |
| | | name: item.model || item.specification || "", |
| | | value: item.id, |
| | | unit: item.unit, |
| | | })); |
| | | }) |
| | | .catch(() => { |
| | | modelActions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const onSelectModel = action => { |
| | | showModelSheet.value = false; |
| | | props.row.productModelId = action.value; |
| | | props.row.specificationModel = action.name; |
| | | if (action.unit) props.row.unit = action.unit; |
| | | }; |
| | | |
| | | const onSelectTaxRate = action => { |
| | | showTaxRateSheet.value = false; |
| | | props.row.taxRate = action.value; |
| | | }; |
| | | |
| | | const onSelectInvoiceType = action => { |
| | | showInvoiceSheet.value = false; |
| | | props.row.invoiceType = action.value; |
| | | }; |
| | | |
| | | // æå¼åå·é¢æ¿åå
æå·²æå¤§ç±»å¯¹åºçå巿忥ï¼ç¼è¾åæ¾åºæ¯ï¼ |
| | | onMounted(() => { |
| | | if (!props.row.productCategoryId) return; |
| | | modelList({ id: props.row.productCategoryId }) |
| | | .then(res => { |
| | | const rows = res?.data?.records || res?.data || res?.records || res || []; |
| | | modelActions.value = (Array.isArray(rows) ? rows : []).map(item => ({ |
| | | name: item.model || item.specification || "", |
| | | value: item.id, |
| | | unit: item.unit, |
| | | })); |
| | | // åªåäºåå·åç§°æ¶åæ¥ idï¼ä¿è¯éæ©å¨è½å½ä¸ |
| | | if (!props.row.productModelId && props.row.specificationModel) { |
| | | const matched = modelActions.value.find( |
| | | item => item.name === props.row.specificationModel |
| | | ); |
| | | if (matched) props.row.productModelId = matched.value; |
| | | } |
| | | }) |
| | | .catch(() => {}); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .product-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 0 12px 12px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .product-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .product-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .product-body { |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | .field-row { |
| | | display: flex; |
| | | align-items: center; |
| | | min-height: 44px; |
| | | border-bottom: 1px solid #f5f6fa; |
| | | } |
| | | |
| | | .field-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .field-label { |
| | | width: 96px; |
| | | flex-shrink: 0; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .field-value { |
| | | flex: 1; |
| | | min-width: 0; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | text-align: right; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .field-value.placeholder { |
| | | color: #c0c4cc; |
| | | } |
| | | |
| | | .field-value.readonly { |
| | | color: #909399; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 项ç®å¢éå表çä¸è¡ |
| | | |
| | | æå / è§è²ä¸æç±ç¶çº§ç»ä¸æååä¼ å
¥ï¼å¤ä¸ªè¡å
±ç¨ä¸ä»½é项ï¼ä¸éå¤è¯·æ±ï¼ã |
| | | æäº¤æ¶å端ä¸åæ¥ userName / userRoleNameï¼æä»¥éä¸åè¦æ label ä¸èµ·è®°å¨ row ä¸ã |
| | | --> |
| | | <template> |
| | | <view class="team-card"> |
| | | <view class="team-header"> |
| | | <text class="team-title">æå {{ index + 1 }}</text> |
| | | <up-icon name="trash" |
| | | color="#ee0a24" |
| | | size="18" |
| | | @click="$emit('remove')"></up-icon> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | |
| | | <view class="team-body"> |
| | | <view class="field-row"> |
| | | <text class="field-label">å§å</text> |
| | | <FormPicker v-model="row.memberId" |
| | | type="select" |
| | | title="éæ©é¡¹ç®æå" |
| | | :options="userOptions" |
| | | placeholder="è¯·éæ©æå" |
| | | @change="onMemberChange" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">项ç®ç»è§è²</text> |
| | | <FormPicker v-model="row.roleId" |
| | | type="select" |
| | | title="鿩项ç®ç»è§è²" |
| | | :options="roleOptions" |
| | | placeholder="è¯·éæ©è§è²" |
| | | @change="onRoleChange" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">è¿å
¥æ¥æ</text> |
| | | <FormPicker v-model="row.enterDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©è¿å
¥æ¥æ" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">ç¦»å¼æ¥æ</text> |
| | | <FormPicker v-model="row.leaveDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ç¦»å¼æ¥æ" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">èç³»æ¹å¼</text> |
| | | <u-input v-model="row.phone" |
| | | placeholder="请è¾å
¥èç³»æ¹å¼" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | |
| | | <view class="field-row"> |
| | | <text class="field-label">夿³¨</text> |
| | | <u-input v-model="row.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | border="none" |
| | | inputAlign="right" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | |
| | | const props = defineProps({ |
| | | row: { |
| | | type: Object, |
| | | required: true, |
| | | }, |
| | | index: { |
| | | type: Number, |
| | | default: 0, |
| | | }, |
| | | userOptions: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | roleOptions: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | }); |
| | | |
| | | defineEmits(["remove"]); |
| | | |
| | | // éä¸åæ label è®°å°è¡ä¸ï¼æäº¤æ¶é payload ä¸èµ·åç»å端 |
| | | const onMemberChange = (value, item) => { |
| | | props.row.memberName = item?.name || props.row.memberName || ""; |
| | | }; |
| | | |
| | | const onRoleChange = (value, item) => { |
| | | props.row.roleName = item?.name || props.row.roleName || ""; |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .team-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 0 12px 12px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .team-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .team-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .team-body { |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | .field-row { |
| | | display: flex; |
| | | align-items: center; |
| | | min-height: 44px; |
| | | border-bottom: 1px solid #f5f6fa; |
| | | } |
| | | |
| | | .field-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .field-label { |
| | | width: 96px; |
| | | flex-shrink: 0; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * åå
¸æ¸²æå°å·¥å
· |
| | | * |
| | | * 项ç®ç®¡ççãåæ®ç¶æ / 计åç¶æ / å®¡æ ¸ç¶æã齿¥èªå端åå
¸ |
| | | * ï¼bill_status / plan_status / project_managementï¼ï¼useDict æ¿å°çé¡¹å½¢å¦ |
| | | * { label, value, elTagType, elTagClass }ãå表ä¸è¯¦æ
é½è¦æ value æ¸²ææ |
| | | * æ ç¾ææ¡ + é¢è²ï¼è¿éæ¶ä¸å¤ã |
| | | * |
| | | * up-tag åªè®¤ info/primary/success/warning/errorï¼å端 listClass å¯è½æ¯ |
| | | * dangerï¼Element Plus è¯è¡¨ï¼ï¼å¿
须转æ¢ï¼å¦åæ ç¾ä¸æ¾ç¤ºé¢è²ã |
| | | */ |
| | | |
| | | const TAG_TYPE_MAP = { |
| | | danger: "error", |
| | | "": "info", |
| | | default: "info", |
| | | }; |
| | | |
| | | /** åå
¸å¼ â æ ç¾ææ¡ï¼æ¾ä¸å°è¿ååå¼ */ |
| | | export function dictLabel(options, value) { |
| | | if (value === undefined || value === null || value === "") return ""; |
| | | const matched = (options || []).find( |
| | | item => String(item.value) === String(value) |
| | | ); |
| | | return matched ? matched.label : String(value); |
| | | } |
| | | |
| | | /** åå
¸å¼ â up-tag å¯ç¨ç type */ |
| | | export function dictTagType(options, value) { |
| | | const matched = (options || []).find( |
| | | item => String(item.value) === String(value) |
| | | ); |
| | | const raw = matched?.elTagType ?? ""; |
| | | return TAG_TYPE_MAP[raw] || raw || "info"; |
| | | } |
| | | |
| | | /** useDict çåå
¸é¡¹ â FormPicker / FilterPanel 认è¯ç䏿é项 */ |
| | | export function toPickerOptions(options) { |
| | | return (options || []).map(item => ({ |
| | | name: item.label, |
| | | value: item.value, |
| | | })); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * 项ç®ç®¡çåæ®µæ å° |
| | | * |
| | | * å端 projectManagement çåæ®µåä¸ä¸å¡å«ä¹å·®å¼å¾å¤§ï¼æ å°å
³ç³»åèªç®¡ç端 |
| | | * views/projectManagement/Management/components/formDia.vue ä¸ projectDetail.vueï¼ |
| | | * |
| | | * info å± no=åæ®ç¼å·ãtitle=项ç®åç§°ãclientName=客æ·åç§°ã |
| | | * establishTime=ç«é¡¹æ¥æãsource=é¡¹ç®æ¥æºãmanagerName=ç«é¡¹äººã |
| | | * orderAmount=项ç®éé¢ãreviewStatus=å®¡æ ¸ç¶æã |
| | | * status=åæ®ç¶æãstage=计åç¶æãplanStartTime/planEndTime=计åèµ·æ¢ |
| | | * contractInfo å± name=è系人ãsex='ç·'/'女'ãlineaFissa=ä¼ä¸å¾®ä¿¡ãwx=微信ã |
| | | * origineEtnica=å°åãrappresentanteLegale=夿³¨ |
| | | * shippingAddress å± consignee=æ¶è´§äººãcontract=èç³»çµè¯ãaddress=å°å |
| | | * teamList 项 userId/userNameãuserRoleId/userRoleNameãjoinTime/departTimeã |
| | | * contact=èç³»æ¹å¼ |
| | | * |
| | | * åå转æ¢å
¨é¨æ¶æå¨è¿éï¼é¡µé¢éä¸åºç°è£¸å段åã |
| | | * 详æ
è¿åï¼detail.info / detail.shippingAddress / detail.contractInfo / |
| | | * detail.salesLedgerProductList / detail.info.teamList / detail.info.attachmentList |
| | | * æäº¤ç»æï¼{ info, shippingAddress, contractInfo, salesLedgerProductList } |
| | | */ |
| | | |
| | | export const GENDER_OPTIONS = [ |
| | | { name: "ç·", value: "1" }, |
| | | { name: "女", value: "2" }, |
| | | ]; |
| | | |
| | | let uidSeed = 1; |
| | | const nextUid = () => `p_${uidSeed++}`; |
| | | |
| | | const toStr = value => |
| | | value === undefined || value === null ? "" : String(value); |
| | | |
| | | // å端åçæ¯ä¸ææ§å«ï¼è¡¨åéçæ¯ '1'/'2' |
| | | const genderToValue = sex => (sex === "ç·" ? "1" : sex === "女" ? "2" : ""); |
| | | const genderFromValue = value => |
| | | value === "1" ? "ç·" : value === "2" ? "女" : ""; |
| | | |
| | | // åå
¸å¼å端å¯è½è¿åæ°åï¼éæ©å¨æå符串æ¯å¯¹ |
| | | const dictValue = value => |
| | | value === undefined || value === null || value === "" ? "" : String(value); |
| | | |
| | | const idOrNull = value => |
| | | value === undefined || value === null || value === "" ? null : value; |
| | | |
| | | // æ¶è´§å°åã产åå表é½å¯è½ä¸ºç©ºå¯¹è±¡æç¼ºå¤±ï¼ç»ä¸åæ°ç» |
| | | const asArray = value => (Array.isArray(value) ? value : []); |
| | | |
| | | /** å表è¡ï¼å表æ¥å£å段 â å±ç¤ºå段 */ |
| | | export function toListRow(record = {}) { |
| | | return { |
| | | id: record.id, |
| | | billNo: record.no ?? record.billNo ?? "", |
| | | projectName: record.title ?? record.projectName ?? "", |
| | | billStatus: dictValue(record.billStatus ?? record.status), |
| | | auditStatus: dictValue(record.reviewStatus ?? record.auditStatus), |
| | | projectStage: dictValue(record.stage ?? record.projectStage), |
| | | customerName: record.clientName ?? record.customerName ?? "", |
| | | setupDate: record.establishTime ?? record.setupDate ?? "", |
| | | projectSource: record.source ?? record.projectSource ?? "", |
| | | projectClassification: |
| | | record.departmentName ?? record.projectClassification ?? "", |
| | | projectAmount: record.orderAmount, |
| | | salesContractCount: record.salesContractCount, |
| | | raw: record, |
| | | }; |
| | | } |
| | | |
| | | /** 详æ
æ¥å£è¿å â 详æ
页åååº */ |
| | | export function toDetail(detail = {}) { |
| | | const info = detail?.info || {}; |
| | | const shippingAddress = detail?.shippingAddress || {}; |
| | | const contractInfo = detail?.contractInfo || {}; |
| | | |
| | | const rawPhase = |
| | | detail?.phaseList || |
| | | detail?.projectPhaseList || |
| | | detail?.projectStageList || |
| | | info?.phaseList || |
| | | info?.projectPhaseList; |
| | | |
| | | return { |
| | | info, |
| | | shippingAddress: { |
| | | consignee: shippingAddress.consignee ?? "", |
| | | contract: shippingAddress.contract ?? "", |
| | | address: shippingAddress.address ?? "", |
| | | }, |
| | | contractInfo: { |
| | | name: contractInfo.name ?? "", |
| | | sex: contractInfo.sex ?? "", |
| | | birthday: contractInfo.birthday ?? "", |
| | | department: contractInfo.department ?? "", |
| | | job: contractInfo.job ?? "", |
| | | phoneNumber: contractInfo.phoneNumber ?? "", |
| | | email: contractInfo.email ?? "", |
| | | qq: contractInfo.qq ?? "", |
| | | wx: contractInfo.wx ?? "", |
| | | // å端è¿ä¸¤ä¸ªå段åä¸ä¸å¡å«ä¹å®å
¨æ å
³ï¼UI 䏿ä¸å¡å«ä¹å±ç¤º |
| | | workWechat: contractInfo.lineaFissa ?? "", |
| | | address: contractInfo.origineEtnica ?? "", |
| | | remark: contractInfo.rappresentanteLegale ?? "", |
| | | }, |
| | | productList: asArray(detail?.salesLedgerProductList), |
| | | teamList: asArray(info.teamList), |
| | | attachments: asArray(info.attachmentList).map(a => ({ |
| | | id: a.id ?? a.fileId, |
| | | name: a.fileName ?? a.name ?? "", |
| | | url: a.url ?? a.fileUrl ?? a.path ?? "", |
| | | })), |
| | | // 表åéæ²¡æå¯ç¼è¾çé¶æ®µå表ï¼å端 /save ä¸åé¶æ®µï¼ï¼è¿éåªç»è¯¦æ
页åªè¯»å±ç¤ºç¨ |
| | | phaseList: asArray(rawPhase), |
| | | }; |
| | | } |
| | | |
| | | /** 详æ
æ¥å£è¿å â 表åï¼å«åè¡¨ï¼ */ |
| | | export function toForm(detail = {}) { |
| | | const d = toDetail(detail); |
| | | const info = d.info; |
| | | const contractInfo = detail?.contractInfo || {}; |
| | | const shippingAddress = detail?.shippingAddress || {}; |
| | | |
| | | const form = { |
| | | id: info.id, |
| | | billNo: info.no ?? "", |
| | | projectName: info.title ?? "", |
| | | clientId: info.clientId ?? "", |
| | | customerName: info.clientName ?? "", |
| | | parentProjectId: info.projectManagementInfoParentId ?? "", |
| | | parentProjectName: info.projectManagementInfoParentName ?? "", |
| | | projectManagementPlanId: info.projectManagementPlanId ?? "", |
| | | projectSource: info.source ?? "", |
| | | managerId: info.managerId ?? "", |
| | | creatorName: info.managerName ?? "", |
| | | salesmanId: info.salesmanId ?? "", |
| | | salesmanName: info.salesmanName ?? "", |
| | | departmentId: info.departmentId ?? "", |
| | | departmentName: info.departmentName ?? "", |
| | | setupDate: info.establishTime ?? "", |
| | | planStartDate: info.planStartTime ?? "", |
| | | planEndDate: info.planEndTime ?? "", |
| | | actualStartDate: info.actualStartTime ?? "", |
| | | actualEndDate: info.actualEndTime ?? "", |
| | | orderDate: info.orderDate ?? "", |
| | | projectAmount: info.orderAmount ?? "", |
| | | billStatus: dictValue(info.status), |
| | | auditStatus: dictValue(info.reviewStatus), |
| | | projectStage: dictValue(info.stage ?? info.projectStage), |
| | | remark: info.remark ?? "", |
| | | contactName: contractInfo.name ?? "", |
| | | contactGender: genderToValue(contractInfo.sex), |
| | | contactBirthday: contractInfo.birthday ?? "", |
| | | contactDept: contractInfo.department ?? "", |
| | | contactJob: contractInfo.job ?? "", |
| | | contactMobile: contractInfo.phoneNumber ?? "", |
| | | contactEmail: contractInfo.email ?? "", |
| | | contactQq: contractInfo.qq ?? "", |
| | | contactWechat: contractInfo.wx ?? "", |
| | | contactWorkWechat: contractInfo.lineaFissa ?? "", |
| | | contactAddress: contractInfo.origineEtnica ?? "", |
| | | contactRemark: contractInfo.rappresentanteLegale ?? "", |
| | | }; |
| | | |
| | | // æ¶è´§å°åå端åªå䏿¡ï¼è¿éç¨åè¡ç¼è¾ï¼æäº¤æ¶å第ä¸è¡ |
| | | const addressList = shippingAddress?.address |
| | | ? [ |
| | | { |
| | | receiver: shippingAddress.consignee ?? "", |
| | | phone: shippingAddress.contract ?? "", |
| | | address: shippingAddress.address ?? "", |
| | | }, |
| | | ] |
| | | : []; |
| | | |
| | | const teamList = d.teamList.map(t => ({ |
| | | memberId: t.userId ?? "", |
| | | memberName: t.userName ?? "", |
| | | roleId: t.userRoleId ?? "", |
| | | roleName: t.userRoleName ?? "", |
| | | enterDate: t.joinTime ?? "", |
| | | leaveDate: t.departTime ?? "", |
| | | phone: t.contact ?? "", |
| | | remark: t.remark ?? "", |
| | | })); |
| | | |
| | | const productList = d.productList.map(p => ({ |
| | | uid: nextUid(), |
| | | productCategoryId: p.productCategoryId ?? "", |
| | | productCategory: p.productCategory ?? "", |
| | | productModelId: p.productModelId ?? "", |
| | | specificationModel: p.specificationModel ?? "", |
| | | unit: p.unit ?? "", |
| | | quantity: p.quantity ?? "", |
| | | taxRate: p.taxRate ?? "", |
| | | taxInclusiveUnitPrice: p.taxInclusiveUnitPrice ?? "", |
| | | taxExclusiveTotalPrice: p.taxExclusiveTotalPrice ?? "", |
| | | invoiceType: p.invoiceType ?? "", |
| | | })); |
| | | |
| | | return { form, teamList, addressList, productList, attachments: d.attachments }; |
| | | } |
| | | |
| | | /** 表å â æäº¤ç»æ */ |
| | | export function toPayload({ |
| | | form, |
| | | teamList, |
| | | addressList, |
| | | productList, |
| | | attachmentIds, |
| | | }) { |
| | | const shippingRow = asArray(addressList)[0] || {}; |
| | | |
| | | const info = { |
| | | id: form.id ?? null, |
| | | no: form.billNo, |
| | | title: form.projectName, |
| | | clientId: idOrNull(form.clientId), |
| | | clientName: form.customerName, |
| | | projectManagementInfoParentId: idOrNull(form.parentProjectId), |
| | | projectManagementPlanId: idOrNull(form.projectManagementPlanId), |
| | | establishTime: form.setupDate, |
| | | source: form.projectSource, |
| | | managerId: idOrNull(form.managerId), |
| | | managerName: form.creatorName, |
| | | salesmanId: idOrNull(form.salesmanId), |
| | | salesmanName: form.salesmanName || "", |
| | | departmentId: idOrNull(form.departmentId), |
| | | departmentName: form.departmentName || "", |
| | | planStartTime: form.planStartDate, |
| | | planEndTime: form.planEndDate, |
| | | actualStartTime: form.actualStartDate, |
| | | actualEndTime: form.actualEndDate, |
| | | orderDate: form.orderDate, |
| | | orderAmount: Number(form.projectAmount) || 0, |
| | | status: form.billStatus === "" ? null : Number(form.billStatus), |
| | | reviewStatus: form.auditStatus === "" ? null : Number(form.auditStatus), |
| | | stage: form.projectStage === "" ? null : Number(form.projectStage), |
| | | remark: form.remark, |
| | | attachmentIds: asArray(attachmentIds), |
| | | // userName / userRoleName æ¯ä¸æ labelï¼å端ä¸åæ¥ï¼å¿
é¡»åç«¯å¸¦ä¸ |
| | | teamList: asArray(teamList).map(t => ({ |
| | | userId: t.memberId, |
| | | userName: t.memberName, |
| | | userRoleId: t.roleId, |
| | | userRoleName: t.roleName, |
| | | joinTime: t.enterDate, |
| | | departTime: t.leaveDate, |
| | | contact: t.phone, |
| | | remark: t.remark, |
| | | })), |
| | | }; |
| | | |
| | | const shippingAddress = { |
| | | id: undefined, |
| | | consignee: shippingRow.receiver, |
| | | contract: shippingRow.phone, |
| | | address: shippingRow.address, |
| | | }; |
| | | |
| | | const contractInfo = { |
| | | id: undefined, |
| | | name: form.contactName, |
| | | sex: genderFromValue(form.contactGender), |
| | | birthday: form.contactBirthday, |
| | | department: form.contactDept, |
| | | job: form.contactJob, |
| | | phoneNumber: form.contactMobile, |
| | | email: form.contactEmail, |
| | | qq: form.contactQq, |
| | | wx: form.contactWechat, |
| | | lineaFissa: form.contactWorkWechat, |
| | | origineEtnica: form.contactAddress, |
| | | rappresentanteLegale: form.contactRemark, |
| | | }; |
| | | |
| | | return { |
| | | info, |
| | | shippingAddress, |
| | | contractInfo, |
| | | salesLedgerProductList: asArray(productList).map(p => ({ |
| | | productCategoryId: p.productCategoryId, |
| | | productCategory: p.productCategory, |
| | | productModelId: p.productModelId, |
| | | specificationModel: p.specificationModel, |
| | | unit: p.unit, |
| | | quantity: Number(p.quantity) || 0, |
| | | taxRate: p.taxRate, |
| | | taxInclusiveUnitPrice: Number(p.taxInclusiveUnitPrice) || 0, |
| | | taxInclusiveTotalPrice: productInclusiveTotal(p), |
| | | taxExclusiveTotalPrice: Number(p.taxExclusiveTotalPrice) || 0, |
| | | invoiceType: p.invoiceType, |
| | | })), |
| | | }; |
| | | } |
| | | |
| | | /** å«ç¨æ»ä»· = å«ç¨åä»· à æ°é */ |
| | | export function productInclusiveTotal(row) { |
| | | const total = |
| | | Number(row?.taxInclusiveUnitPrice || 0) * Number(row?.quantity || 0); |
| | | return Number(total.toFixed(2)); |
| | | } |
| | | |
| | | /** 详æ
页å±ç¤ºç¨ç空产åè¡ */ |
| | | export function createEmptyProduct() { |
| | | return { |
| | | uid: nextUid(), |
| | | productCategoryId: "", |
| | | productCategory: "", |
| | | productModelId: "", |
| | | specificationModel: "", |
| | | unit: "", |
| | | quantity: "", |
| | | taxRate: "", |
| | | taxInclusiveUnitPrice: "", |
| | | taxExclusiveTotalPrice: "", |
| | | invoiceType: "", |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * 项ç®é¶æ®µ / è¿åº¦æ±æ¥å
±ç¨é»è¾ |
| | | * |
| | | * é¶æ®µæ°æ®æä¸¤ä¸ªæ¥æºï¼å«ä¹ä¸åï¼å«æ··ï¼ |
| | | * listPlan() â 项ç®ç±»åä¸çã计åèç¹ãï¼planNodeListï¼ï¼ |
| | | * æ¯æ¨¡æ¿ï¼å³å®äºé¶æ®µè¿åº¦æ¡ä¸ãè¿åº¦æ±æ¥ãçå¯é项 |
| | | * listStage(projectId) â 该项ç®å·²æ±æ¥è¿çé¶æ®µè®°å½ï¼è¯¦æ
页ã项ç®é¶æ®µãTab å±ç¤º |
| | | * |
| | | * saveStage çå
¥ååæ®µåä¸è¡¨ååæ®µåä¸ä¸è´ï¼è¡¨åç¨ phaseName/planDaysï¼ |
| | | * æ¥å£ç¨ projectManagementPlanNodeId/estimatedDurationï¼ï¼è½¬æ¢ä¹æ¶æå¨è¿éã |
| | | */ |
| | | import { listPlan } from "@/api/projectManagement/projectType"; |
| | | import { listStage } from "@/api/projectManagement/project"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | |
| | | /** åæä¸ªé¡¹ç®ç±»åä¸ç计åèç¹å表 */ |
| | | export async function fetchPlanNodeList(planId) { |
| | | if (planId === undefined || planId === null || planId === "") return []; |
| | | try { |
| | | const res = await listPlan({ current: 1, size: 999 }); |
| | | const records = unwrapList(res); |
| | | const plan = records.find(p => String(p.id) === String(planId)) || {}; |
| | | const nodes = Array.isArray(plan.planNodeList) ? plan.planNodeList : []; |
| | | // è¿åº¦æ¡ä¸ä¸æé½æ sort æåºï¼å端ä¸ä¿è¯é¡ºåº |
| | | return [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)); |
| | | } catch (error) { |
| | | return []; |
| | | } |
| | | } |
| | | |
| | | /** 计åèç¹ â 䏿é项ï¼å表页ä¸è¿åº¦æ±æ¥å
±ç¨ä¸ä»½ label è§åï¼ */ |
| | | export function planNodeOptions(nodes) { |
| | | return (nodes || []) |
| | | .map(node => ({ |
| | | name: node.name || node.workContent || "", |
| | | value: node.id, |
| | | estimatedDuration: node.estimatedDuration, |
| | | })) |
| | | .filter(item => item.value !== undefined && item.value !== null); |
| | | } |
| | | |
| | | /** 计åèç¹ id â é¶æ®µåç§°ï¼è¯¦æ
页ã项ç®é¶æ®µãTab éå端åªå idï¼éè¦åç«¯åæ¥ */ |
| | | export function stageNameFromNodes(nodes, nodeId) { |
| | | const matched = (nodes || []).find(n => String(n.id) === String(nodeId)); |
| | | return matched?.name || matched?.workContent || ""; |
| | | } |
| | | |
| | | /** |
| | | * å项ç®çå·²æ±æ¥é¶æ®µ |
| | | * è¿åç»æå端æ data.data / data.records çå¤ç§å½¢æï¼ç¨ unwrapList å
åº |
| | | */ |
| | | export async function fetchProjectStages(projectId) { |
| | | if (!projectId) return []; |
| | | try { |
| | | const res = await listStage(projectId); |
| | | const inner = res?.data?.data ?? res?.data ?? res; |
| | | return unwrapList({ data: inner }); |
| | | } catch (error) { |
| | | return []; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ ¹æ®å½å计åç¶ææ¨ç®é»è®¤éä¸ç计åèç¹ |
| | | * å端 stage åçæ¯åå
¸å¼ï¼å¯è½å½ä¸èç¹ idï¼ä¹å¯è½æ¯åºå·ï¼é½å
ä¸ä¸ |
| | | */ |
| | | export function defaultPlanNodeId(stageValue, nodes) { |
| | | const list = Array.isArray(nodes) ? nodes : []; |
| | | if (list.length === 0) return ""; |
| | | const direct = list.find(n => String(n.id) === String(stageValue)); |
| | | if (direct) return direct.id; |
| | | const index = Number(stageValue); |
| | | if (Number.isFinite(index)) { |
| | | const byIndex = list[index - 1] || list[index] || list[0]; |
| | | if (byIndex) return byIndex.id; |
| | | } |
| | | return list[0].id; |
| | | } |
| | | |
| | | /** |
| | | * è¿åº¦æ±æ¥è¡¨å â saveStage å
¥å |
| | | * @param {object} options |
| | | * @param {string|number} options.projectId |
| | | * @param {object} options.projectInfo 项ç®è¯¦æ
æ¥å£ç info èç¹ |
| | | * @param {Array} options.planNodes fetchPlanNodeList çç»æ |
| | | * @param {object} options.form è¿åº¦æ±æ¥è¡¨å |
| | | * @param {Array} options.attachments CommonUpload çæä»¶å¯¹è±¡æ°ç» |
| | | * @param {string} options.leaderId å½åç»å½ç¨æ· id |
| | | * @param {string} options.leaderName å½åç»å½ç¨æ·æµç§° |
| | | */ |
| | | export function buildStagePayload({ |
| | | projectId, |
| | | projectInfo, |
| | | planNodes, |
| | | form, |
| | | attachments, |
| | | leaderId, |
| | | leaderName, |
| | | }) { |
| | | const info = projectInfo || {}; |
| | | const node = |
| | | (planNodes || []).find(n => String(n.id) === String(form.planNodeId)) || {}; |
| | | const description = form.remark |
| | | ? `${form.reportDate || ""} ${form.remark}`.trim() |
| | | : `${form.reportDate || ""} è¿åº¦æ±æ¥`.trim(); |
| | | |
| | | // å端åªåä¸ä¸ª progressã累计è¿åº¦å¡«äºå°±ä¼å
ç¨å®ï¼ç空æéå宿è¿åº¦ ââ |
| | | // ç¨ç©ºå¼å¤æè䏿¯ `??`ï¼å¦å被åå§åæ "0" ç累计è¿åº¦ä¼æå®æè¿åº¦åæã |
| | | const totalProgress = form.totalProgress; |
| | | const progress = |
| | | totalProgress === "" || totalProgress === null || totalProgress === undefined |
| | | ? Number(form.completionProgress || 0) || 0 |
| | | : Number(totalProgress) || 0; |
| | | |
| | | return { |
| | | id: null, |
| | | projectManagementPlanNodeId: form.planNodeId, |
| | | projectManagementInfoId: projectId, |
| | | description, |
| | | // è´è´£äººå端ä¸åæ¥ï¼å端带å½åç»å½äººï¼åä¸å°åéå项ç®ç«é¡¹äºº |
| | | actualLeaderId: leaderId || info.managerId || null, |
| | | actualLeaderName: leaderName || info.managerName || "", |
| | | estimatedDuration: Number(node.estimatedDuration ?? 0) || 0, |
| | | planStartTime: form.planStartTime || info.planStartTime || null, |
| | | planEndTime: form.planEndTime || info.planEndTime || null, |
| | | actualStartTime: form.actualStartTime || null, |
| | | actualEndTime: form.actualEndTime || null, |
| | | progress, |
| | | storageBlobDTOs: Array.isArray(attachments) ? attachments : [], |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | ææ¡£å½æ¡£ |
| | | |
| | | æ°æ®æ¥èª getProjectAttachments(id)ï¼è¿åææ¨¡ååç»çéä»¶ï¼ |
| | | [{ moduleName, files: [{ name, url }] }] |
| | | ï¼å¯¹é½ç®¡ç端 components/ArchiveDialog.vue ç res.data ç»æï¼ |
| | | |
| | | éä»¶ä¸è½½å¤ç¨ utils/file.js ç openFileï¼request.ts æªéä¼ responseTypeï¼ |
| | | åªè½ downloadFile å°ä¸´æ¶æä»¶å openDocumentã |
| | | --> |
| | | <template> |
| | | <view class="project-archive"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | |
| | | <view v-if="loading" |
| | | class="state-text"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | |
| | | <view v-else-if="groups.length === 0" |
| | | class="state-text"> |
| | | <text>ææ å½æ¡£éä»¶</text> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="archive-list"> |
| | | <view v-for="(group, index) in groups" |
| | | :key="index" |
| | | class="archive-group"> |
| | | <view class="group-header" |
| | | @click="toggleGroup(index)"> |
| | | <text class="group-title"> |
| | | {{ group.moduleName || "æªå½å模å" }}ï¼{{ group.files.length }}ï¼ |
| | | </text> |
| | | <up-icon :name="isOpen(index) ? 'arrow-up' : 'arrow-down'" |
| | | size="16" |
| | | color="#909399"></up-icon> |
| | | </view> |
| | | |
| | | <view v-if="isOpen(index)" |
| | | class="file-list"> |
| | | <view v-for="(file, fileIndex) in group.files" |
| | | :key="fileIndex" |
| | | class="file-item" |
| | | @click="openFile(file.url)"> |
| | | <up-icon name="attach" |
| | | size="18" |
| | | color="#5b8ff9"></up-icon> |
| | | <text class="file-name">{{ file.name || "æªå½åæä»¶" }}</text> |
| | | <up-icon name="download" |
| | | size="18" |
| | | color="#909399"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { openFile } from "@/utils/file"; |
| | | import { getProjectAttachments } from "@/api/projectManagement/project"; |
| | | |
| | | const loading = ref(true); |
| | | const projectId = ref(""); |
| | | const projectName = ref(""); |
| | | const groups = ref([]); |
| | | const openIndex = ref([]); |
| | | |
| | | const pageTitle = computed(() => |
| | | projectName.value ? `ææ¡£å½æ¡£ - ${projectName.value}` : "ææ¡£å½æ¡£" |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const isOpen = index => openIndex.value.includes(index); |
| | | |
| | | const toggleGroup = index => { |
| | | openIndex.value = isOpen(index) |
| | | ? openIndex.value.filter(item => item !== index) |
| | | : [...openIndex.value, index]; |
| | | }; |
| | | |
| | | // å端å¯è½å files 为 nullï¼ç»ä¸ææ°ç»ï¼é¿å
模æ¿éå length æ¥é |
| | | const normalize = list => |
| | | (Array.isArray(list) ? list : []).map(group => ({ |
| | | moduleName: group?.moduleName ?? "", |
| | | files: (Array.isArray(group?.files) ? group.files : []).map(file => ({ |
| | | name: file?.name ?? "", |
| | | url: file?.url ?? "", |
| | | })), |
| | | })); |
| | | |
| | | const loadAttachments = async () => { |
| | | try { |
| | | const res = await getProjectAttachments(projectId.value); |
| | | const raw = res?.data?.data ?? res?.data ?? res; |
| | | groups.value = normalize(raw); |
| | | // å
¨é¨åç»é»è®¤å±å¼ï¼ä¸ç®¡ç端 ArchiveDialog ç activeNames è¡ä¸ºä¸è´ |
| | | openIndex.value = groups.value.map((_, index) => index); |
| | | } catch (error) { |
| | | groups.value = []; |
| | | uni.showToast({ title: "è·å彿¡£é件失败", icon: "none" }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | projectId.value = options?.id || ""; |
| | | projectName.value = options?.name ? decodeURIComponent(options.name) : ""; |
| | | if (projectId.value) { |
| | | loadAttachments(); |
| | | } else { |
| | | loading.value = false; |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .project-archive { |
| | | min-height: 100vh; |
| | | background: #f5f6fa; |
| | | } |
| | | |
| | | .state-text { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 80px 0; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .archive-list { |
| | | padding: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .archive-group { |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .group-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 14px 16px; |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | .group-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .file-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 12px 16px; |
| | | border-top: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | .file-name { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | ä¸å¡åæ°è®¾ç½®ï¼å¯¹é½ç®¡ç端 views/projectManagement/config/index.vueï¼ |
| | | |
| | | ç®ååªæä¸ä¸ªåæ°ãååå件约å®åä¼ æåæé天æ°ãï¼ç¨äºéå®å°è´¦é |
| | | ãååå件约å®åä¼ æ¥æãç临æé¢è¦ï¼days = åä¼ æ¥æ - ä»å¤©ï¼days <= N æ¥è¦ã |
| | | å端 GET /projectManagement/config è¿å { id, contractReturnReminderDays }ï¼ |
| | | PUT åå°åæ´ä½è¦çï¼æä»¥ id å¿
é¡»åæ ·å¸¦åå»ã |
| | | --> |
| | | <template> |
| | | <view class="project-config"> |
| | | <PageHeader title="ä¸å¡åæ°è®¾ç½®" |
| | | @back="goBack" /> |
| | | |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="200" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group class="form-section"> |
| | | <up-form-item label="ååå件约å®åä¼ æåæé天æ°" |
| | | prop="contractReturnReminderDays" |
| | | required> |
| | | <up-input v-model="form.contractReturnReminderDays" |
| | | type="number" |
| | | placeholder="请è¾å
¥å¤©æ°" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | |
| | | <view class="tip-text"> |
| | | <text>示ä¾ï¼å¡« 3 表示ãååå件约å®åä¼ æ¥æãè·ä»ä¸è¶³ 3 å¤©æ¶æç¤ºé¢è¦ã</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirm-text="ä¿å设置" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { |
| | | getProjectConfig, |
| | | updateProjectConfig, |
| | | } from "@/api/projectManagement/config"; |
| | | |
| | | const MIN_DAYS = 1; |
| | | const MAX_DAYS = 365; |
| | | const DEFAULT_DAYS = 3; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const form = ref({ |
| | | id: undefined, |
| | | contractReturnReminderDays: DEFAULT_DAYS, |
| | | }); |
| | | |
| | | const validateDays = (rule, value, callback) => { |
| | | const days = Number(value); |
| | | if (value === "" || value === null || value === undefined) { |
| | | callback(new Error("æåæé天æ°ä¸è½ä¸ºç©º")); |
| | | return; |
| | | } |
| | | if (!Number.isInteger(days) || days < MIN_DAYS || days > MAX_DAYS) { |
| | | callback(new Error(`天æ°é为 ${MIN_DAYS}~${MAX_DAYS} çæ´æ°`)); |
| | | return; |
| | | } |
| | | callback(); |
| | | }; |
| | | |
| | | const rules = { |
| | | contractReturnReminderDays: [ |
| | | { required: true, validator: validateDays, trigger: "blur" }, |
| | | ], |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const loadConfig = async () => { |
| | | try { |
| | | const res = await getProjectConfig(); |
| | | const data = res?.data?.data ?? res?.data ?? res; |
| | | if (!data) return; |
| | | form.value.id = data.id; |
| | | form.value.contractReturnReminderDays = |
| | | Number(data.contractReturnReminderDays) || DEFAULT_DAYS; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·åé
置失败", icon: "none" }); |
| | | } |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | loading.value = true; |
| | | updateProjectConfig({ |
| | | id: form.value.id, |
| | | contractReturnReminderDays: Number(form.value.contractReturnReminderDays), |
| | | }) |
| | | .then(() => { |
| | | uni.showToast({ title: "é
ç½®æ´æ°æå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "é
ç½®æ´æ°å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(() => { |
| | | loadConfig(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .project-config { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .tip-text { |
| | | padding: 12px 4px; |
| | | font-size: 13px; |
| | | line-height: 1.6; |
| | | color: #909399; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 项ç®è¯¦æ
|
| | | |
| | | å
个 Tabï¼åºç¡èµæ / 项ç®é¶æ®µ / è¯è®¼ä¿¡æ¯ / 䏿 ä¿¡æ¯ / éå®åå / å
¨æµç¨æ¶é´è½´ã |
| | | è¯è®¼ã䏿 ãéå®ååæ²¡æãæé¡¹ç®æ¥ãçä¸ç¨æ¥å£ï¼ç®¡çç«¯ä¹æ¯æå
¨éåæ |
| | | é¡¹ç® id / 项ç®åç§°å¹é
ï¼è§ projectDetail.vue ç matchProjectï¼ï¼è¿éç
§åã |
| | | |
| | | ãå
¨æµç¨æ¶é´è½´ãæ¯å端èåï¼é¶æ®µæ±æ¥ + è¯è®¼ç«æ¡ + æææ ææ¥æååºæ¼æ¥ï¼ |
| | | å端没æ timeline æ¥å£ï¼å°æ¥å端æä¾äºåªéæ¢æ°æ®æºï¼UI ä¸ç¨å¨ã |
| | | --> |
| | | <template> |
| | | <view class="project-detail"> |
| | | <PageHeader title="项ç®è¯¦æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading"> |
| | | <view class="detail-card"> |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ info.title || "-" }}</text> |
| | | <up-tag :text="dictLabel(auditOptions, info.reviewStatus)" |
| | | :type="dictTagType(auditOptions, info.reviewStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <view class="hero-meta"> |
| | | <text class="hero-meta-item">åæ®ç¼å·ï¼{{ info.no || "-" }}</text> |
| | | <text class="hero-meta-item">项ç®éé¢ï¼{{ formatAmount(info.orderAmount) }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="planNodes.length > 0" |
| | | class="steps-card"> |
| | | <up-steps :current="activeStep" |
| | | direction="row" |
| | | activeColor="#2979ff" |
| | | inactiveColor="#c0c4cc"> |
| | | <up-steps-item v-for="(node, index) in planNodes" |
| | | :key="node.id || index" |
| | | :title="node.name || node.workContent || ''" /> |
| | | </up-steps> |
| | | </view> |
| | | |
| | | <view class="tabs-wrap"> |
| | | <up-tabs v-model="activeTab" |
| | | :list="TABS" |
| | | :scrollable="true" |
| | | itemStyle="padding: 0 14px; height: 84rpx;" /> |
| | | </view> |
| | | |
| | | <!-- åºç¡èµæ --> |
| | | <view v-if="activeTab === 0"> |
| | | <u-cell-group title="åºæ¬ä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="åæ®ç¼å·" |
| | | :value="info.no || '-'" /> |
| | | <u-cell title="项ç®åç§°" |
| | | :value="info.title || '-'" /> |
| | | <u-cell title="客æ·åç§°" |
| | | :value="info.clientName || '-'" /> |
| | | <u-cell title="ç¶é¡¹ç®" |
| | | :value="detail.info.parentProjectName || '-'" /> |
| | | <u-cell title="项ç®ç±»å" |
| | | :value="projectTypeLabel" /> |
| | | <u-cell title="ç«é¡¹æ¥æ" |
| | | :value="info.establishTime || '-'" /> |
| | | <u-cell title="é¡¹ç®æ¥æº" |
| | | :value="info.source || '-'" /> |
| | | <u-cell title="ç«é¡¹äºº" |
| | | :value="info.managerName || '-'" /> |
| | | <u-cell title="ä¸å¡å" |
| | | :value="info.salesmanName || '-'" /> |
| | | <u-cell title="é¨é¨" |
| | | :value="info.departmentName || '-'" /> |
| | | <u-cell title="åæ®ç¶æ" |
| | | :value="dictLabel(billOptions, info.status) || '-'" /> |
| | | <u-cell title="å®¡æ ¸ç¶æ" |
| | | :value="dictLabel(auditOptions, info.reviewStatus) || '-'" /> |
| | | <u-cell title="计åç¶æ" |
| | | :value="dictLabel(stageOptions, info.stage) || '-'" /> |
| | | <u-cell title="é¢è®¡å·¥æ(天)" |
| | | :value="estimatedDays === null ? '-' : String(estimatedDays)" /> |
| | | <u-cell title="计åå¼å§æ¥æ" |
| | | :value="info.planStartTime || '-'" /> |
| | | <u-cell title="计åå®ææ¥æ" |
| | | :value="info.planEndTime || '-'" /> |
| | | <u-cell title="å®é
å¼å§æ¥æ" |
| | | :value="info.actualStartTime || '-'" /> |
| | | <u-cell title="å®é
å®ææ¥æ" |
| | | :value="info.actualEndTime || '-'" /> |
| | | <u-cell title="è®¢åæ¥æ" |
| | | :value="info.orderDate || '-'" /> |
| | | <u-cell title="项ç®éé¢" |
| | | :value="formatAmount(info.orderAmount)" /> |
| | | <u-cell title="夿³¨" |
| | | :value="info.remark || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="产åä¿¡æ¯" |
| | | class="detail-section"> |
| | | <view v-if="productList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ äº§åä¿¡æ¯</text> |
| | | </view> |
| | | <view v-for="(row, index) in productList" |
| | | v-else |
| | | :key="row.id || index" |
| | | class="sub-card"> |
| | | <view class="sub-card-header"> |
| | | <text class="sub-card-title">{{ row.productCategory || "产å" }} {{ index + 1 }}</text> |
| | | <text class="sub-card-amount">{{ formatAmount(row.taxInclusiveTotalPrice) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è§æ ¼åå·</text> |
| | | <text class="detail-value">{{ row.specificationModel || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åä½</text> |
| | | <text class="detail-value">{{ row.unit || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ°é</text> |
| | | <text class="detail-value">{{ row.quantity ?? "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç¨ç(%)</text> |
| | | <text class="detail-value">{{ row.taxRate ?? "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å«ç¨åä»·</text> |
| | | <text class="detail-value">{{ formatAmount(row.taxInclusiveUnitPrice) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å«ç¨æ»ä»·</text> |
| | | <text class="detail-value">{{ formatAmount(row.taxInclusiveTotalPrice) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä¸å«ç¨æ»ä»·</text> |
| | | <text class="detail-value">{{ formatAmount(row.taxExclusiveTotalPrice) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å票类å</text> |
| | | <text class="detail-value">{{ row.invoiceType || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="项ç®å¢é" |
| | | class="detail-section"> |
| | | <view v-if="teamList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ é¡¹ç®æå</text> |
| | | </view> |
| | | <view v-for="(row, index) in teamList" |
| | | v-else |
| | | :key="row.id || index" |
| | | class="sub-card"> |
| | | <view class="sub-card-header"> |
| | | <text class="sub-card-title">{{ row.userName || "-" }}</text> |
| | | <text class="sub-card-amount">{{ row.userRoleName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è¿å
¥æ¥æ</text> |
| | | <text class="detail-value">{{ row.joinTime || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç¦»å¼æ¥æ</text> |
| | | <text class="detail-value">{{ row.departTime || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">èç³»æ¹å¼</text> |
| | | <text class="detail-value">{{ row.contact || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ row.remark || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æ¶è´§å°å" |
| | | class="detail-section"> |
| | | <u-cell title="æ¶è´§äºº" |
| | | :value="detail.shippingAddress.consignee || '-'" /> |
| | | <u-cell title="èç³»æ¹å¼" |
| | | :value="detail.shippingAddress.contract || '-'" /> |
| | | <u-cell title="å°å" |
| | | :value="detail.shippingAddress.address || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="è系信æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="è系人" |
| | | :value="detail.contractInfo.name || '-'" /> |
| | | <u-cell title="æ§å«" |
| | | :value="detail.contractInfo.sex || '-'" /> |
| | | <u-cell title="çæ¥" |
| | | :value="detail.contractInfo.birthday || '-'" /> |
| | | <u-cell title="é¨é¨" |
| | | :value="detail.contractInfo.department || '-'" /> |
| | | <u-cell title="èå¡" |
| | | :value="detail.contractInfo.job || '-'" /> |
| | | <u-cell title="ææºå·" |
| | | :value="detail.contractInfo.phoneNumber || '-'" /> |
| | | <u-cell title="é®ç®±" |
| | | :value="detail.contractInfo.email || '-'" /> |
| | | <u-cell title="QQ" |
| | | :value="detail.contractInfo.qq || '-'" /> |
| | | <u-cell title="微信å·" |
| | | :value="detail.contractInfo.wx || '-'" /> |
| | | <u-cell title="ä¼ä¸å¾®ä¿¡" |
| | | :value="detail.contractInfo.workWechat || '-'" /> |
| | | <u-cell title="å°å" |
| | | :value="detail.contractInfo.address || '-'" /> |
| | | <u-cell title="夿³¨" |
| | | :value="detail.contractInfo.remark || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éä»¶" |
| | | class="detail-section"> |
| | | <view v-if="attachments.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ éä»¶</text> |
| | | </view> |
| | | <view v-for="(file, index) in attachments" |
| | | v-else |
| | | :key="file.id || index" |
| | | class="file-item" |
| | | @click="previewFile(file)"> |
| | | <up-icon name="attach" |
| | | size="16" |
| | | color="#2979ff"></up-icon> |
| | | <text class="file-name">{{ file.name || `éä»¶${index + 1}` }}</text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#c0c4cc"></up-icon> |
| | | </view> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <!-- 项ç®é¶æ®µ --> |
| | | <view v-if="activeTab === 1"> |
| | | <u-cell-group title="计åèç¹ï¼é¡¹ç®ç±»å模æ¿ï¼" |
| | | class="detail-section"> |
| | | <view v-if="planNodes.length === 0" |
| | | class="empty-text"> |
| | | <text>该项ç®ç±»åæªé
置计åèç¹</text> |
| | | </view> |
| | | <view v-for="(node, index) in planNodes" |
| | | v-else |
| | | :key="node.id || index" |
| | | class="sub-card"> |
| | | <view class="sub-card-header"> |
| | | <text class="sub-card-title">{{ node.name || node.workContent || "-" }}</text> |
| | | <text class="sub-card-amount">å·¥æ {{ node.estimatedDuration ?? "-" }} 天</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è´è´£äºº</text> |
| | | <text class="detail-value">{{ node.leaderName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä½ä¸å
容</text> |
| | | <text class="detail-value">{{ node.workContent || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="é¶æ®µæ±æ¥è®°å½" |
| | | class="detail-section"> |
| | | <view v-if="stageList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ é¶æ®µæ±æ¥</text> |
| | | </view> |
| | | <view v-for="(row, index) in stageList" |
| | | v-else |
| | | :key="row.id || index" |
| | | class="sub-card"> |
| | | <view class="sub-card-header"> |
| | | <text class="sub-card-title">{{ row.stageName || "-" }}</text> |
| | | <text class="sub-card-amount">{{ row.progress ?? 0 }}%</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æè¿°</text> |
| | | <text class="detail-value">{{ row.description || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®é
è´è´£äºº</text> |
| | | <text class="detail-value">{{ row.actualLeaderName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">计åèµ·æ¢</text> |
| | | <text class="detail-value">{{ row.planStartTime || "-" }} ~ {{ row.planEndTime || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®é
èµ·æ¢</text> |
| | | <text class="detail-value">{{ row.actualStartTime || "-" }} ~ {{ row.actualEndTime || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">é¢è®¡å·¥æ</text> |
| | | <text class="detail-value">{{ row.estimatedDuration ?? "-" }} 天</text> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <!-- è¯è®¼ä¿¡æ¯ --> |
| | | <view v-if="activeTab === 2"> |
| | | <view v-if="lawsuitList.length === 0" |
| | | class="no-data"> |
| | | <text>ææ è¯è®¼ä¿¡æ¯</text> |
| | | </view> |
| | | <view v-else |
| | | class="list-wrap"> |
| | | <view v-for="row in lawsuitList" |
| | | :key="row.id" |
| | | class="list-card"> |
| | | <view class="list-card-header"> |
| | | <text class="list-card-title">{{ row.caseNo || "-" }}</text> |
| | | <up-tag :text="row.lawsuitStatus || '-'" |
| | | :type="lawsuitTagType(row.lawsuitStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ååç¼å·</text> |
| | | <text class="detail-value">{{ row.contractNo || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç«æ¡æ¶é´</text> |
| | | <text class="detail-value">{{ formatDate(row.filingDate) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¼åºæ¶é´</text> |
| | | <text class="detail-value">{{ formatDate(row.hearingDate) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ çéé¢</text> |
| | | <text class="detail-value">{{ formatAmount(row.claimAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ¿åå¾å¸</text> |
| | | <text class="detail-value">{{ row.lawyer || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¤å³æè¦</text> |
| | | <text class="detail-value">{{ row.judgmentSummary || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 䏿 ä¿¡æ¯ --> |
| | | <view v-if="activeTab === 3"> |
| | | <view v-if="biddingList.length === 0" |
| | | class="no-data"> |
| | | <text>ææ æææ ä¿¡æ¯</text> |
| | | </view> |
| | | <view v-else |
| | | class="list-wrap"> |
| | | <view v-for="row in biddingList" |
| | | :key="row.id" |
| | | class="list-card"> |
| | | <view class="list-card-header"> |
| | | <text class="list-card-title">{{ row.bidDocName || "-" }}</text> |
| | | <up-tag :text="row.bidResult || '-'" |
| | | :type="biddingTagType(row.bidResult)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ åä½</text> |
| | | <text class="detail-value">{{ row.tenderUnit || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ åä½</text> |
| | | <text class="detail-value">{{ row.bidUnit || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ æ¥æ</text> |
| | | <text class="detail-value">{{ formatDate(row.tenderDate) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ æ¥æ</text> |
| | | <text class="detail-value">{{ formatDate(row.bidDate) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ 书款</text> |
| | | <text class="detail-value">{{ formatAmount(row.bidDocFee) }}ï¼{{ row.bidDocFeeStatus || "-" }}ï¼</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ ä¿è¯é</text> |
| | | <text class="detail-value">{{ formatAmount(row.bidDeposit) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- éå®åå --> |
| | | <view v-if="activeTab === 4"> |
| | | <view v-if="contractList.length === 0" |
| | | class="no-data"> |
| | | <text>ææ éå®åå</text> |
| | | </view> |
| | | <view v-else |
| | | class="list-wrap"> |
| | | <view v-for="row in contractList" |
| | | :key="row.id" |
| | | class="list-card"> |
| | | <view class="list-card-header"> |
| | | <text class="list-card-title">{{ row.salesContractNo || "-" }}</text> |
| | | <text class="list-card-meta">{{ formatDate(row.executionDate) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">客æ·åç§°</text> |
| | | <text class="detail-value">{{ row.customerName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä¸å¡å</text> |
| | | <text class="detail-value">{{ row.salesman || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ååéé¢</text> |
| | | <text class="detail-value">{{ formatAmount(row.contractAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®é
ååéé¢</text> |
| | | <text class="detail-value">{{ formatAmount(row.netContractAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">äº¤ä»æ¥æ</text> |
| | | <text class="detail-value">{{ formatDate(row.deliveryDate) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å½å
¥äºº</text> |
| | | <text class="detail-value">{{ row.entryPersonName || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å
¨æµç¨æ¶é´è½´ï¼å端èåï¼éå端æ¥å£ï¼ --> |
| | | <view v-if="activeTab === 5"> |
| | | <view v-if="timeline.length === 0" |
| | | class="no-data"> |
| | | <text>ææ æµç¨è®°å½</text> |
| | | </view> |
| | | <view v-else |
| | | class="timeline-wrap"> |
| | | <up-steps :current="-1" |
| | | direction="column" |
| | | activeColor="#2979ff" |
| | | inactiveColor="#2979ff"> |
| | | <up-steps-item v-for="(item, index) in timeline" |
| | | :key="index" |
| | | :title="item.title" |
| | | :desc="item.desc" /> |
| | | </up-steps> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | |
| | | <view v-if="!loading" |
| | | class="footer-btns"> |
| | | <up-button class="footer-btn" |
| | | text="è¿å" |
| | | @click="goBack" /> |
| | | <up-button class="footer-btn" |
| | | type="primary" |
| | | text="è¿åº¦æ±æ¥" |
| | | @click="goProgress" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad, onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { openFile } from "@/utils/file"; |
| | | import { parseDate } from "@/utils/warning"; |
| | | import { getProject } from "@/api/projectManagement/project"; |
| | | import { listPlan } from "@/api/projectManagement/projectType"; |
| | | import { lawsuitListPage } from "@/api/salesManagement/lawsuitManagement"; |
| | | import { biddingListPage } from "@/api/salesManagement/biddingManagement"; |
| | | import { ledgerListPage } from "@/api/salesManagement/salesLedger"; |
| | | import { toDetail } from "./_utils/mapper"; |
| | | import { dictLabel, dictTagType, toPickerOptions } from "./_utils/dict"; |
| | | import { |
| | | fetchPlanNodeList, |
| | | fetchProjectStages, |
| | | stageNameFromNodes, |
| | | } from "./_utils/stage"; |
| | | |
| | | const MS_PER_DAY = 24 * 60 * 60 * 1000; |
| | | |
| | | const TABS = [ |
| | | { name: "åºç¡èµæ" }, |
| | | { name: "项ç®é¶æ®µ" }, |
| | | { name: "è¯è®¼ä¿¡æ¯" }, |
| | | { name: "䏿 ä¿¡æ¯" }, |
| | | { name: "éå®åå" }, |
| | | { name: "å
¨æµç¨æ¶é´è½´" }, |
| | | ]; |
| | | |
| | | const LAWSUIT_TAG_TYPE = { |
| | | ç«æ¡ä¸: "warning", |
| | | 审çä¸: "primary", |
| | | å·²å¤å³: "success", |
| | | å·²æ¤è¯: "info", |
| | | å·²åè§£: "success", |
| | | }; |
| | | |
| | | const BIDDING_TAG_TYPE = { |
| | | åºæ : "info", |
| | | æµæ : "info", |
| | | æªä¸æ : "warning", |
| | | 䏿 å¾
åºéç¥ä¹¦: "primary", |
| | | å·²åºä¸æ éç¥ä¹¦: "success", |
| | | å·²ç¾åå: "success", |
| | | }; |
| | | |
| | | const { bill_status, project_management, plan_status } = useDict( |
| | | "bill_status", |
| | | "project_management", |
| | | "plan_status" |
| | | ); |
| | | |
| | | const billOptions = computed(() => toPickerOptions(bill_status.value)); |
| | | const auditOptions = computed(() => toPickerOptions(project_management.value)); |
| | | const stageOptions = computed(() => toPickerOptions(plan_status.value)); |
| | | |
| | | const loading = ref(true); |
| | | const projectId = ref(""); |
| | | const activeTab = ref(0); |
| | | const detail = ref({}); |
| | | const planNodes = ref([]); |
| | | const stageList = ref([]); |
| | | const projectTypeName = ref(""); |
| | | const lawsuitList = ref([]); |
| | | const biddingList = ref([]); |
| | | const contractList = ref([]); |
| | | |
| | | const info = computed(() => detail.value.info || {}); |
| | | const productList = computed(() => detail.value.productList || []); |
| | | const teamList = computed(() => detail.value.teamList || []); |
| | | const attachments = computed(() => detail.value.attachments || []); |
| | | |
| | | const projectTypeLabel = computed( |
| | | () => projectTypeName.value || String(info.value.projectManagementPlanId ?? "-") |
| | | ); |
| | | |
| | | const estimatedDays = computed(() => { |
| | | const raw = Number(info.value.estimatedDays); |
| | | if (Number.isFinite(raw) && raw > 0) return raw; |
| | | const start = parseDate(info.value.planStartTime); |
| | | const end = parseDate(info.value.planEndTime); |
| | | if (!start || !end || end < start) return null; |
| | | return Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1; |
| | | }); |
| | | |
| | | // 计åèç¹å·²æ sort æåºï¼è®¡åç¶ææåºå·æèç¹ id å½ä¸ |
| | | const activeStep = computed(() => { |
| | | const nodes = planNodes.value; |
| | | const stage = info.value.stage; |
| | | if (!nodes.length) return 0; |
| | | const byId = nodes.findIndex(n => String(n.id) === String(stage)); |
| | | if (byId >= 0) return byId; |
| | | const byLabel = nodes.findIndex( |
| | | n => String(n.name || n.workContent) === String(stage) |
| | | ); |
| | | if (byLabel >= 0) return byLabel; |
| | | const index = Number(stage); |
| | | if (Number.isFinite(index) && index > 0) return Math.min(index - 1, nodes.length - 1); |
| | | return 0; |
| | | }); |
| | | |
| | | // å
¨æµç¨æ¶é´è½´ï¼é¶æ®µ + è¯è®¼ + æææ ææ¥æååºèåï¼åç«¯æ¼æ¥ï¼å端æ timeline æ¥å£ï¼ |
| | | const timeline = computed(() => { |
| | | const events = []; |
| | | stageList.value.forEach(row => { |
| | | events.push({ |
| | | date: row.planStartTime || row.actualStartTime || "", |
| | | title: `ã项ç®é¶æ®µã${row.stageName || "é¶æ®µæ±æ¥"}`, |
| | | desc: row.description || "", |
| | | }); |
| | | }); |
| | | lawsuitList.value.forEach(row => { |
| | | events.push({ |
| | | date: row.filingDate || "", |
| | | title: `ãè¯è®¼ã${row.caseNo || ""} ${row.lawsuitStatus || ""}`.trim(), |
| | | desc: row.judgmentSummary || row.progressRemark || "", |
| | | }); |
| | | }); |
| | | biddingList.value.forEach(row => { |
| | | events.push({ |
| | | date: row.bidDate || "", |
| | | title: `ãæææ ã${row.bidDocName || ""} ${row.bidResult || ""}`.trim(), |
| | | desc: row.tenderUnit ? `ææ åä½ï¼${row.tenderUnit}` : "", |
| | | }); |
| | | }); |
| | | return events |
| | | .filter(item => item.title) |
| | | .sort((a, b) => String(b.date).localeCompare(String(a.date))); |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.navigateTo({ |
| | | url: `/pages/projectManagement/project/edit?id=${projectId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const goProgress = () => { |
| | | uni.navigateTo({ |
| | | url: `/pages/projectManagement/project/progress?id=${projectId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const formatDate = value => (value ? String(value).substring(0, 10) : "-"); |
| | | |
| | | const lawsuitTagType = status => LAWSUIT_TAG_TYPE[status] || "info"; |
| | | const biddingTagType = status => BIDDING_TAG_TYPE[status] || "info"; |
| | | |
| | | const previewFile = file => openFile(file?.url || file?.previewURL); |
| | | |
| | | const loadProjectTypeName = async () => { |
| | | const planId = info.value.projectManagementPlanId; |
| | | if (planId === undefined || planId === null || planId === "") return; |
| | | try { |
| | | const res = await listPlan({ current: 1, size: 999 }); |
| | | const matched = unwrapList(res).find( |
| | | item => String(item.id) === String(planId) |
| | | ); |
| | | projectTypeName.value = matched?.name || ""; |
| | | } catch (error) { |
| | | projectTypeName.value = ""; |
| | | } |
| | | }; |
| | | |
| | | // è¯è®¼ / æææ 没ææé¡¹ç®æ¥çæ¥å£ï¼æå
¨éåæé¡¹ç® id æåç§°å¹é
ï¼å¯¹é½ç®¡ç端 matchProjectï¼ |
| | | const matchProject = (record, id, title) => { |
| | | if (!record) return false; |
| | | if ( |
| | | record.projectId !== undefined && |
| | | record.projectId !== null && |
| | | record.projectId !== "" |
| | | ) { |
| | | if (String(record.projectId) === String(id)) return true; |
| | | } |
| | | const name = String(record.projectName || "").trim(); |
| | | const target = String(title || "").trim(); |
| | | if (name && target) { |
| | | return name === target || name.includes(target) || target.includes(name); |
| | | } |
| | | return false; |
| | | }; |
| | | |
| | | const loadRelated = async (id, title) => { |
| | | if (!id) return; |
| | | |
| | | try { |
| | | const res = await lawsuitListPage({ projectName: title, current: -1, size: -1 }); |
| | | let matched = unwrapList(res).filter(r => matchProject(r, id, title)); |
| | | if (matched.length === 0) { |
| | | const all = await lawsuitListPage({ current: -1, size: -1 }); |
| | | matched = unwrapList(all).filter(r => matchProject(r, id, title)); |
| | | } |
| | | lawsuitList.value = matched; |
| | | } catch (error) { |
| | | lawsuitList.value = []; |
| | | } |
| | | |
| | | try { |
| | | const res = await biddingListPage({ projectName: title, current: -1, size: -1 }); |
| | | let matched = unwrapList(res).filter(r => matchProject(r, id, title)); |
| | | if (matched.length === 0) { |
| | | const all = await biddingListPage({ current: -1, size: -1 }); |
| | | matched = unwrapList(all).filter(r => matchProject(r, id, title)); |
| | | } |
| | | biddingList.value = matched; |
| | | } catch (error) { |
| | | biddingList.value = []; |
| | | } |
| | | |
| | | try { |
| | | const res = await ledgerListPage({ projectId: id, current: -1, size: -1 }); |
| | | contractList.value = unwrapList(res).sort((a, b) => |
| | | String(b.executionDate || "").localeCompare(String(a.executionDate || "")) |
| | | ); |
| | | } catch (error) { |
| | | contractList.value = []; |
| | | } |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const res = await getProject(projectId.value); |
| | | const raw = res?.data?.data ?? res?.data ?? res; |
| | | detail.value = toDetail(raw || {}); |
| | | |
| | | const id = info.value.id || projectId.value; |
| | | const title = info.value.title || ""; |
| | | |
| | | const [nodes, stages] = await Promise.all([ |
| | | fetchPlanNodeList(info.value.projectManagementPlanId), |
| | | fetchProjectStages(id), |
| | | ]); |
| | | planNodes.value = nodes; |
| | | stageList.value = stages.map(row => ({ |
| | | ...row, |
| | | stageName: stageNameFromNodes(nodes, row.projectManagementPlanNodeId), |
| | | })); |
| | | |
| | | await Promise.all([loadProjectTypeName(), loadRelated(id, title)]); |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·å项ç®è¯¦æ
失败", icon: "none" }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | projectId.value = options?.id || ""; |
| | | if (!projectId.value) loading.value = false; |
| | | }); |
| | | |
| | | // ä»ãè¿åº¦æ±æ¥ãè¿åæ¶é¶æ®µå表ä¼åï¼ç¨ onShow éæï¼onShow å¨ onLoad åä¹ä¼è§¦åï¼ |
| | | onShow(() => { |
| | | if (projectId.value) loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .project-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 90px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .hero-meta { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | padding: 12px 18px 0; |
| | | } |
| | | |
| | | .hero-meta-item { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .steps-card { |
| | | margin: 12px; |
| | | padding: 16px 8px; |
| | | border-radius: 12px; |
| | | background: #ffffff; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .tabs-wrap { |
| | | margin: 12px 12px 0; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | background: #ffffff; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .detail-section { |
| | | margin: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .sub-card { |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .sub-card:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .sub-card-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .sub-card-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .sub-card-amount { |
| | | font-size: 13px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .list-wrap { |
| | | padding: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .list-card { |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | padding: 14px 16px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .list-card-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .list-card-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .list-card-meta { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .timeline-wrap { |
| | | margin: 12px; |
| | | padding: 20px 16px; |
| | | border-radius: 12px; |
| | | background: #ffffff; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .file-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .file-name { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #333; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .empty-text { |
| | | padding: 16px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .footer-btns { |
| | | position: fixed; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | display: flex; |
| | | gap: 12px; |
| | | padding: 12px 16px; |
| | | padding-bottom: calc(12px + env(safe-area-inset-bottom)); |
| | | background: #ffffff; |
| | | box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05); |
| | | z-index: 1000; |
| | | } |
| | | |
| | | .footer-btn { |
| | | flex: 1; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | é¡¹ç®æ°å¢ / ç¼è¾ |
| | | |
| | | 表åååºå¯¹é½ç®¡ç端 formDia.vueï¼åºç¡èµæ / 产åä¿¡æ¯ / 项ç®å¢é / æ¶è´§å°å / èç³»ä¿¡æ¯ / éä»¶ã |
| | | 管çç«¯è¿æä¸ä¸ªã项ç®é¶æ®µãåè¡¨ï¼æ´åæ¯æ³¨éæçä¸ä¸å¨ /save çå
¥åéï¼ |
| | | æä»¥è¿éä¹ä¸åå¯ç¼è¾é¶æ®µè¡¨ï¼åäºä¼éé»ä¸¢æ°æ®ï¼ï¼é¶æ®µéè¿è¯¦æ
页æ¥çãéè¿ãè¿åº¦æ±æ¥ãåå
¥ã |
| | | |
| | | åºç¡èµæéãé¢è®¡å·¥æãå¨ç®¡ç端å¯å¡«ä½ä¸å¨æäº¤ç»æéï¼å端æè®¡åèµ·æ¢æ¥ææ´¾çï¼ï¼ |
| | | æä»¥è¿éåæåªè¯»æ´¾çå¼ï¼é¿å
ç¨æ·å¡«äºä¸çæã |
| | | --> |
| | | <template> |
| | | <view class="project-edit"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="åºç¡èµæ" |
| | | class="form-section"> |
| | | <up-form-item v-if="projectId" |
| | | label="åæ®ç¼å·"> |
| | | <up-input :model-value="form.billNo || 'ç³»ç»çæ'" |
| | | disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="项ç®åç§°" |
| | | prop="projectName" |
| | | required> |
| | | <up-input v-model="form.projectName" |
| | | placeholder="请è¾å
¥é¡¹ç®åç§°" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="客æ·åç§°" |
| | | prop="customerName"> |
| | | <up-input v-model="form.customerName" |
| | | placeholder="请è¾å
¥å®¢æ·åç§°" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç«é¡¹æ¥æ" |
| | | prop="setupDate"> |
| | | <FormPicker v-model="form.setupDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ç«é¡¹æ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="é¡¹ç®æ¥æº" |
| | | prop="projectSource"> |
| | | <up-input v-model="form.projectSource" |
| | | placeholder="请è¾å
¥é¡¹ç®æ¥æº" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç«é¡¹äºº" |
| | | prop="creatorName"> |
| | | <up-input v-model="form.creatorName" |
| | | placeholder="请è¾å
¥ç«é¡¹äºº" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="项ç®ç±»å" |
| | | prop="projectManagementPlanId" |
| | | required> |
| | | <FormPicker v-model="form.projectManagementPlanId" |
| | | type="select" |
| | | title="鿩项ç®ç±»å" |
| | | :options="projectTypeOptions" |
| | | placeholder="è¯·éæ©é¡¹ç®ç±»å" /> |
| | | </up-form-item> |
| | | <up-form-item label="计åå¼å§æ¥æ" |
| | | prop="planStartDate"> |
| | | <FormPicker v-model="form.planStartDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©è®¡åå¼å§æ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="计åå®ææ¥æ" |
| | | prop="planEndDate"> |
| | | <FormPicker v-model="form.planEndDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©è®¡åå®ææ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="é¢è®¡å·¥æ(天)"> |
| | | <up-input :model-value="estimatedDays === null ? 'æè®¡åæ¥æèªå¨è®¡ç®' : String(estimatedDays)" |
| | | disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="项ç®éé¢" |
| | | prop="projectAmount"> |
| | | <up-input v-model="form.projectAmount" |
| | | type="number" |
| | | placeholder="请è¾å
¥é¡¹ç®éé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="å®¡æ ¸ç¶æ" |
| | | prop="auditStatus"> |
| | | <FormPicker v-model="form.auditStatus" |
| | | type="select" |
| | | title="éæ©å®¡æ ¸ç¶æ" |
| | | :options="auditOptions" |
| | | placeholder="è¯·éæ©å®¡æ ¸ç¶æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <up-textarea v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | :maxlength="100" |
| | | count |
| | | auto-height /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="产åä¿¡æ¯" |
| | | class="form-section"> |
| | | <view class="section-tools"> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="æ·»å 产å" |
| | | @click="addProduct" /> |
| | | </view> |
| | | <view v-if="productList.length === 0" |
| | | class="empty-text"> |
| | | <text>请è³å°æ·»å 䏿¡äº§åä¿¡æ¯</text> |
| | | </view> |
| | | <view v-else |
| | | class="row-list"> |
| | | <ProductRow v-for="(row, index) in productList" |
| | | :key="row.uid" |
| | | :row="row" |
| | | :index="index" |
| | | :tax-rate-options="taxRateOptions" |
| | | @remove="removeProduct(index)" /> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="项ç®å¢é" |
| | | class="form-section"> |
| | | <view class="section-tools"> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="æ·»å æå" |
| | | @click="addTeamRow" /> |
| | | </view> |
| | | <view v-if="teamList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ é¡¹ç®æå</text> |
| | | </view> |
| | | <view v-else |
| | | class="row-list"> |
| | | <TeamRow v-for="(row, index) in teamList" |
| | | :key="index" |
| | | :row="row" |
| | | :index="index" |
| | | :user-options="userOptions" |
| | | :role-options="roleOptions" |
| | | @remove="removeTeamRow(index)" /> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æ¶è´§å°å" |
| | | class="form-section"> |
| | | <view v-if="addressList.length === 0" |
| | | class="section-tools"> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="æ·»å å°å" |
| | | @click="addAddressRow" /> |
| | | </view> |
| | | <view v-else |
| | | class="row-list"> |
| | | <AddressRow v-for="(row, index) in addressList" |
| | | :key="index" |
| | | :row="row" /> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="è系信æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="è系人å§å" |
| | | prop="contactName"> |
| | | <up-input v-model="form.contactName" |
| | | placeholder="请è¾å
¥è系人å§å" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æ§å«" |
| | | prop="contactGender"> |
| | | <FormPicker v-model="form.contactGender" |
| | | type="select" |
| | | title="éæ©æ§å«" |
| | | :options="GENDER_OPTIONS" |
| | | placeholder="è¯·éæ©æ§å«" /> |
| | | </up-form-item> |
| | | <up-form-item label="çæ¥" |
| | | prop="contactBirthday"> |
| | | <FormPicker v-model="form.contactBirthday" |
| | | type="date" |
| | | placeholder="è¯·éæ©çæ¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="é¨é¨" |
| | | prop="contactDept"> |
| | | <up-input v-model="form.contactDept" |
| | | placeholder="请è¾å
¥é¨é¨" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="èå¡" |
| | | prop="contactJob"> |
| | | <up-input v-model="form.contactJob" |
| | | placeholder="请è¾å
¥èå¡" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ææºå·ç " |
| | | prop="contactMobile"> |
| | | <up-input v-model="form.contactMobile" |
| | | type="number" |
| | | placeholder="请è¾å
¥ææºå·ç " |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="é®ç®±" |
| | | prop="contactEmail"> |
| | | <up-input v-model="form.contactEmail" |
| | | placeholder="请è¾å
¥é®ç®±" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="QQ" |
| | | prop="contactQq"> |
| | | <up-input v-model="form.contactQq" |
| | | placeholder="请è¾å
¥ QQ" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="微信å·ç " |
| | | prop="contactWechat"> |
| | | <up-input v-model="form.contactWechat" |
| | | placeholder="请è¾å
¥å¾®ä¿¡å·ç " |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ä¼ä¸å¾®ä¿¡" |
| | | prop="contactWorkWechat"> |
| | | <up-input v-model="form.contactWorkWechat" |
| | | placeholder="请è¾å
¥ä¼ä¸å¾®ä¿¡" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="å°å" |
| | | prop="contactAddress"> |
| | | <up-input v-model="form.contactAddress" |
| | | placeholder="请è¾å
¥å°å" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="è系信æ¯å¤æ³¨" |
| | | prop="contactRemark"> |
| | | <up-textarea v-model="form.contactRemark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | :maxlength="200" |
| | | count |
| | | auto-height /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éä»¶" |
| | | class="form-section"> |
| | | <view class="upload-block"> |
| | | <CommonUpload v-model="attachmentFiles" |
| | | accept="all" /> |
| | | </view> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </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 FormPicker from "@/components/FormPicker.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import ProductRow from "./_components/ProductRow.vue"; |
| | | import TeamRow from "./_components/TeamRow.vue"; |
| | | import AddressRow from "./_components/AddressRow.vue"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import { parseDate } from "@/utils/warning"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { findRoleListPage } from "@/api/projectManagement/role"; |
| | | import { listPlan } from "@/api/projectManagement/projectType"; |
| | | import { getProject, saveProject } from "@/api/projectManagement/project"; |
| | | import { |
| | | GENDER_OPTIONS, |
| | | createEmptyProduct, |
| | | toForm, |
| | | toPayload, |
| | | } from "./_utils/mapper"; |
| | | import { toPickerOptions } from "./_utils/dict"; |
| | | |
| | | const MS_PER_DAY = 24 * 60 * 60 * 1000; |
| | | |
| | | const { project_management, tax_rate } = useDict( |
| | | "project_management", |
| | | "tax_rate" |
| | | ); |
| | | const auditOptions = computed(() => toPickerOptions(project_management.value)); |
| | | const taxRateOptions = computed(() => |
| | | (tax_rate.value || []).map(item => ({ label: item.label, value: item.value })) |
| | | ); |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const projectId = ref(""); |
| | | const projectTypeOptions = ref([]); |
| | | const userOptions = ref([]); |
| | | const roleOptions = ref([]); |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | billNo: "", |
| | | projectName: "", |
| | | clientId: "", |
| | | customerName: "", |
| | | parentProjectId: "", |
| | | parentProjectName: "", |
| | | projectManagementPlanId: "", |
| | | projectSource: "", |
| | | managerId: "", |
| | | creatorName: "", |
| | | salesmanId: "", |
| | | salesmanName: "", |
| | | departmentId: "", |
| | | departmentName: "", |
| | | setupDate: "", |
| | | planStartDate: "", |
| | | planEndDate: "", |
| | | actualStartDate: "", |
| | | actualEndDate: "", |
| | | orderDate: "", |
| | | projectAmount: "", |
| | | billStatus: "", |
| | | auditStatus: "", |
| | | projectStage: "", |
| | | remark: "", |
| | | contactName: "", |
| | | contactGender: "", |
| | | contactBirthday: "", |
| | | contactDept: "", |
| | | contactJob: "", |
| | | contactMobile: "", |
| | | contactEmail: "", |
| | | contactQq: "", |
| | | contactWechat: "", |
| | | contactWorkWechat: "", |
| | | contactAddress: "", |
| | | contactRemark: "", |
| | | }); |
| | | |
| | | const teamList = ref([]); |
| | | const addressList = ref([]); |
| | | const productList = ref([]); |
| | | const attachmentFiles = ref([]); |
| | | |
| | | const rules = { |
| | | projectName: [{ required: true, message: "请è¾å
¥é¡¹ç®åç§°", trigger: "blur" }], |
| | | projectManagementPlanId: [ |
| | | { required: true, message: "è¯·éæ©é¡¹ç®ç±»å", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => (projectId.value ? "ç¼è¾é¡¹ç®" : "æ°å¢é¡¹ç®")); |
| | | |
| | | // å端æè®¡åèµ·æ¢æ¥ææ´¾ç estimatedDaysï¼è¿éåªåå±ç¤ºï¼ä¸åä¸æäº¤ |
| | | const estimatedDays = computed(() => { |
| | | const start = parseDate(form.value.planStartDate); |
| | | const end = parseDate(form.value.planEndDate); |
| | | if (!start || !end || end < start) return null; |
| | | return Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1; |
| | | }); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const addProduct = () => productList.value.push(createEmptyProduct()); |
| | | |
| | | const removeProduct = index => { |
| | | productList.value.splice(index, 1); |
| | | }; |
| | | |
| | | const addTeamRow = () => |
| | | teamList.value.push({ |
| | | memberId: "", |
| | | memberName: "", |
| | | roleId: "", |
| | | roleName: "", |
| | | enterDate: "", |
| | | leaveDate: "", |
| | | phone: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const removeTeamRow = index => { |
| | | teamList.value.splice(index, 1); |
| | | }; |
| | | |
| | | const addAddressRow = () => |
| | | addressList.value.push({ receiver: "", phone: "", address: "" }); |
| | | |
| | | const loadProjectTypes = () => { |
| | | listPlan({ current: 1, size: 999 }) |
| | | .then(res => { |
| | | projectTypeOptions.value = unwrapList(res).map(item => ({ |
| | | name: item.name || item.planName || "", |
| | | value: item.id, |
| | | })); |
| | | }) |
| | | .catch(() => { |
| | | projectTypeOptions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const loadUsers = () => { |
| | | userListNoPageByTenantId() |
| | | .then(res => { |
| | | const rows = unwrapList(res); |
| | | userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({ |
| | | name: item.nickName || item.userName || "", |
| | | value: item.userId ?? item.id, |
| | | })); |
| | | }) |
| | | .catch(() => { |
| | | userOptions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const loadRoles = () => { |
| | | findRoleListPage({ pageNum: 1, pageSize: 999 }) |
| | | .then(res => { |
| | | roleOptions.value = unwrapList(res).map(item => ({ |
| | | name: item.roleName || item.name || "", |
| | | value: item.id, |
| | | })); |
| | | }) |
| | | .catch(() => { |
| | | roleOptions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | try { |
| | | const res = await getProject(projectId.value); |
| | | const detail = res?.data?.data ?? res?.data ?? res; |
| | | const mapped = toForm(detail || {}); |
| | | form.value = { ...form.value, ...mapped.form }; |
| | | teamList.value = mapped.teamList; |
| | | addressList.value = mapped.addressList; |
| | | productList.value = mapped.productList; |
| | | attachmentFiles.value = mapped.attachments; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·å项ç®è¯¦æ
失败", icon: "none" }); |
| | | } |
| | | }; |
| | | |
| | | const validateProducts = () => { |
| | | if (productList.value.length === 0) { |
| | | uni.showToast({ title: "请添å 产åä¿¡æ¯", icon: "none" }); |
| | | return false; |
| | | } |
| | | const invalid = productList.value.some(row => !row.productCategoryId); |
| | | if (invalid) { |
| | | uni.showToast({ title: "è¯·ä¸ºæ¯æ¡äº§åéæ©äº§å大类", icon: "none" }); |
| | | return false; |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | if (!validateProducts()) return; |
| | | |
| | | // CommonUpload åçæ¯ä¸ä¼ æ¥å£è¿åçå¯¹è±¡ï¼æäº¤åªè¦ id |
| | | const attachmentIds = attachmentFiles.value |
| | | .map(item => item?.id ?? item?.attachmentId) |
| | | .filter(Boolean); |
| | | |
| | | loading.value = true; |
| | | saveProject( |
| | | toPayload({ |
| | | form: form.value, |
| | | teamList: teamList.value, |
| | | addressList: addressList.value, |
| | | productList: productList.value, |
| | | attachmentIds, |
| | | }) |
| | | ) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(async options => { |
| | | projectId.value = options?.id || ""; |
| | | if (projectId.value) form.value.id = projectId.value; |
| | | |
| | | loadProjectTypes(); |
| | | loadUsers(); |
| | | loadRoles(); |
| | | |
| | | if (projectId.value) { |
| | | await loadDetail(); |
| | | } else { |
| | | // æ°å¢æ¶å
éºä¸æ¡ç©ºè¡ï¼åå°ä¸æ¬¡ç¹å»ï¼äº§åå¿
é¡»è³å°ä¸æ¡ï¼åç«¯ä¼æ ¡éª |
| | | addProduct(); |
| | | addAddressRow(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .project-edit { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .section-tools { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .row-list { |
| | | padding: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .empty-text { |
| | | padding: 16px 12px; |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .upload-block { |
| | | padding: 12px; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.projectNameOrCode" |
| | | placeholder="请è¾å
¥é¡¹ç®åç§°/ç¼å·" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="projectList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in projectList" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="grid" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.billNo || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="dictLabel(auditOptions, item.auditStatus)" |
| | | :type="dictTagType(auditOptions, item.auditStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">项ç®åç§°</text> |
| | | <text class="detail-value highlight">{{ item.projectName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">客æ·åç§°</text> |
| | | <text class="detail-value">{{ item.customerName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç«é¡¹æ¥æ</text> |
| | | <text class="detail-value">{{ item.setupDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">é¡¹ç®æ¥æº</text> |
| | | <text class="detail-value">{{ item.projectSource || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">项ç®åç±»</text> |
| | | <text class="detail-value">{{ item.projectClassification || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">计åç¶æ</text> |
| | | <text class="detail-value">{{ dictLabel(stageOptions, item.projectStage) || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">项ç®éé¢</text> |
| | | <text class="detail-value">{{ formatAmount(item.projectAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">éå®ååæ»æ°</text> |
| | | <text class="detail-value">{{ item.salesContractCount ?? 0 }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goProgress(item)">è¿åº¦æ±æ¥</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goArchive(item)">ææ¡£å½æ¡£</up-button> |
| | | <up-button v-if="canSubmit(item)" |
| | | class="action-btn" |
| | | size="small" |
| | | type="success" |
| | | @click="handleSubmitRow(item)">æäº¤</up-button> |
| | | <up-button v-if="canAudit(item)" |
| | | class="action-btn" |
| | | size="small" |
| | | type="warning" |
| | | @click="handleAuditRow(item)">å®¡æ ¸</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ é¡¹ç®æ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="项ç®çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { |
| | | auditProject, |
| | | delProject, |
| | | listProject, |
| | | submitProject, |
| | | } from "@/api/projectManagement/project"; |
| | | import { toListRow } from "./_utils/mapper"; |
| | | import { dictLabel, dictTagType, toPickerOptions } from "./_utils/dict"; |
| | | |
| | | const { bill_status, project_management, plan_status } = useDict( |
| | | "bill_status", |
| | | "project_management", |
| | | "plan_status" |
| | | ); |
| | | |
| | | const billOptions = computed(() => toPickerOptions(bill_status.value)); |
| | | const auditOptions = computed(() => toPickerOptions(project_management.value)); |
| | | const stageOptions = computed(() => toPickerOptions(plan_status.value)); |
| | | |
| | | const projectList = ref([]); |
| | | const showFilter = ref(false); |
| | | |
| | | // å端没æåæ®ç¶æï¼billStatusï¼çæ¥è¯¢åæ°ï¼ç®¡çç«¯ä¹æ¯æååè¡¨åæ¬å°è¿æ»¤ |
| | | const query = reactive({ |
| | | projectNameOrCode: "", |
| | | customerName: "", |
| | | salesperson: "", |
| | | billStatus: "", |
| | | projectStage: "", |
| | | auditStatus: "", |
| | | }); |
| | | |
| | | const filterFields = computed(() => [ |
| | | { |
| | | prop: "projectNameOrCode", |
| | | label: "项ç®åç§°/ç¼å·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥é¡¹ç®åç§°/ç¼å·", |
| | | }, |
| | | { |
| | | prop: "customerName", |
| | | label: "客æ·åç§°", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥å®¢æ·åç§°", |
| | | }, |
| | | { |
| | | prop: "salesperson", |
| | | label: "ä¸å¡äººå", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥ä¸å¡äººå", |
| | | }, |
| | | { |
| | | prop: "billStatus", |
| | | label: "åæ®ç¶æ", |
| | | type: "select", |
| | | options: billOptions.value, |
| | | }, |
| | | { |
| | | prop: "projectStage", |
| | | label: "计åç¶æ", |
| | | type: "select", |
| | | options: stageOptions.value, |
| | | }, |
| | | { |
| | | prop: "auditStatus", |
| | | label: "å®¡æ ¸ç¶æ", |
| | | type: "select", |
| | | options: auditOptions.value, |
| | | }, |
| | | ]); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("projectDetail"); |
| | | uni.navigateTo({ url: "/pages/projectManagement/project/edit" }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("projectDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/projectManagement/project/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("projectDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/projectManagement/project/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goProgress = item => { |
| | | uni.setStorageSync("projectDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/projectManagement/project/progress?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goArchive = item => { |
| | | uni.navigateTo({ |
| | | url: `/pages/projectManagement/project/archive?id=${item.id}&name=${encodeURIComponent(item.projectName || "")}`, |
| | | }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | // æäº¤ / å®¡æ ¸æ¯çº¿æ§æµç¨ï¼æ°å»º(æ ç¶æ) â æäº¤(reviewStatus 0) â å®¡æ ¸(reviewStatus 1)ã |
| | | // 管çç«¯å¦æåå®¡æ ¸ï¼ææºç«¯æçº¦å®ä¸åã |
| | | const canSubmit = item => String(item?.auditStatus ?? "") === ""; |
| | | const canAudit = item => String(item?.auditStatus) === "0"; |
| | | |
| | | const getList = () => { |
| | | listProject({ |
| | | noOrName: query.projectNameOrCode || undefined, |
| | | clientName: query.customerName || undefined, |
| | | salesmanName: query.salesperson || undefined, |
| | | reviewStatus: query.auditStatus || undefined, |
| | | stage: query.projectStage || undefined, |
| | | current: -1, |
| | | size: -1, |
| | | }) |
| | | .then(res => { |
| | | const rows = unwrapList(res).map(toListRow); |
| | | projectList.value = query.billStatus |
| | | ? rows.filter( |
| | | item => String(item.billStatus) === String(query.billStatus) |
| | | ) |
| | | : rows; |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const confirmStatusAction = (item, label, content, action) => { |
| | | uni.showModal({ |
| | | title: `${label}确认`, |
| | | content, |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | action({ id: item.id }) |
| | | .then(() => { |
| | | uni.showToast({ title: `${label}æå`, icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: `${label}失败`, icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const handleSubmitRow = item => { |
| | | if (!item?.id) return; |
| | | confirmStatusAction(item, "æäº¤", "确认æäº¤è¯¥é¡¹ç®åï¼", submitProject); |
| | | }; |
| | | |
| | | const handleAuditRow = item => { |
| | | if (!item?.id) return; |
| | | confirmStatusAction(item, "å®¡æ ¸", "ç¡®è®¤å®¡æ ¸éè¿è¯¥é¡¹ç®åï¼", auditProject); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: `确认å é¤é¡¹ç®ã${item.projectName || ""}ãåï¼`, |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delProject([item.id]) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .action-buttons { |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .action-btn { |
| | | flex: 0 0 calc(33.33% - 8px); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | è¿åº¦æ±æ¥ï¼â POST /projectManagement/info/saveStageï¼ |
| | | |
| | | åæ®µä»¥ç®¡ç端 projectDetail.vue ç handleProgressSubmitted ç»è£
åºç请æ±ä½ä¸ºåã |
| | | 管ç端 ProgressReportDialog éè¿æã䏿¬¡è¿åº¦ / è´è´£äºº / é¨é¨ãä¸ä¸ªè¾å
¥æ¡ï¼ä½å®ä»¬ |
| | | å¹¶ä¸å¨è¯·æ±ä½éï¼è´è´£äººåçæ¯å½åç»å½äººï¼ä¸æ¯è¡¨åå¼ï¼ï¼æä»¥è¿éä¸åå¯ç¼è¾æ§ä»¶ï¼ |
| | | ã䏿¬¡è¿åº¦ãç±å·²ææ±æ¥è®°å½æ¨ç®ååªè¯»å±ç¤ºï¼ãè´è´£äººãæ¾ç¤ºå°è¢«è®°å½çå½åç»å½äººï¼ |
| | | é¿å
åºç°å¡«äºä¸çæçåæ®µã |
| | | --> |
| | | <template> |
| | | <view class="project-progress"> |
| | | <PageHeader title="è¿åº¦æ±æ¥" |
| | | @back="goBack" /> |
| | | |
| | | <view v-if="projectName" |
| | | class="project-banner"> |
| | | <text class="banner-label">项ç®åç§°</text> |
| | | <text class="banner-value">{{ projectName }}</text> |
| | | </view> |
| | | |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="é¶æ®µä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="项ç®é¶æ®µ" |
| | | prop="planNodeId" |
| | | required> |
| | | <FormPicker v-model="form.planNodeId" |
| | | type="select" |
| | | title="鿩项ç®é¶æ®µ" |
| | | :options="stageOptions" |
| | | placeholder="è¯·éæ©é¡¹ç®é¶æ®µ" /> |
| | | </up-form-item> |
| | | <up-form-item label="计åå¼å§æ¶é´" |
| | | prop="planStartTime" |
| | | required> |
| | | <FormPicker v-model="form.planStartTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©è®¡åå¼å§æ¶é´" /> |
| | | </up-form-item> |
| | | <up-form-item label="计åå®å·¥æ¶é´" |
| | | prop="planEndTime" |
| | | required> |
| | | <FormPicker v-model="form.planEndTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©è®¡åå®å·¥æ¶é´" /> |
| | | </up-form-item> |
| | | <up-form-item label="å®é
å¼å·¥æ¥æ" |
| | | prop="actualStartTime"> |
| | | <FormPicker v-model="form.actualStartTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©å®é
å¼å·¥æ¥æ" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="è¿åº¦æ
åµ" |
| | | class="form-section"> |
| | | <up-form-item label="æ¬æ¬¡è¿åº¦æ¥æ" |
| | | prop="reportDate" |
| | | required> |
| | | <FormPicker v-model="form.reportDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¬æ¬¡è¿åº¦æ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="䏿¬¡è¿åº¦(%)"> |
| | | <up-input :model-value="String(lastProgress)" |
| | | disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="宿è¿åº¦(%)" |
| | | prop="completionProgress" |
| | | required> |
| | | <view class="progress-input"> |
| | | <up-input v-model="form.completionProgress" |
| | | type="number" |
| | | placeholder="请è¾å
¥å®æè¿åº¦" /> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="宿" |
| | | @click="markDone" /> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item label="累计è¿åº¦(%)" |
| | | prop="totalProgress"> |
| | | <up-input v-model="form.totalProgress" |
| | | type="number" |
| | | placeholder="ç空åå宿è¿åº¦" /> |
| | | </up-form-item> |
| | | <up-form-item label="å®é
å®å·¥æ¥æ" |
| | | prop="actualEndTime"> |
| | | <FormPicker v-model="form.actualEndTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©å®é
å®å·¥æ¥æ" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æ±æ¥ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="è´è´£äºº"> |
| | | <up-input :model-value="leaderName || 'å½åç»å½äºº'" |
| | | disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <up-textarea v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | :maxlength="200" |
| | | count |
| | | auto-height /> |
| | | </up-form-item> |
| | | <view class="upload-block"> |
| | | <CommonUpload v-model="attachmentFiles" |
| | | accept="all" /> |
| | | </view> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirm-text="æäº¤æ±æ¥" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </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 FormPicker from "@/components/FormPicker.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { getProject, saveStage } from "@/api/projectManagement/project"; |
| | | import { |
| | | buildStagePayload, |
| | | defaultPlanNodeId, |
| | | fetchPlanNodeList, |
| | | fetchProjectStages, |
| | | planNodeOptions, |
| | | } from "./_utils/stage"; |
| | | |
| | | const userStore = useUserStore(); |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const projectId = ref(""); |
| | | const info = ref({}); |
| | | const projectName = ref(""); |
| | | const planNodes = ref([]); |
| | | const lastProgress = ref(0); |
| | | const attachmentFiles = ref([]); |
| | | |
| | | const leaderName = computed(() => userStore.nickName || ""); |
| | | |
| | | const stageOptions = computed(() => planNodeOptions(planNodes.value)); |
| | | |
| | | const form = ref({ |
| | | planNodeId: "", |
| | | planStartTime: "", |
| | | planEndTime: "", |
| | | actualStartTime: "", |
| | | actualEndTime: "", |
| | | reportDate: "", |
| | | completionProgress: "", |
| | | totalProgress: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | planNodeId: [{ required: true, message: "è¯·éæ©é¡¹ç®é¶æ®µ", trigger: "change" }], |
| | | planStartTime: [ |
| | | { required: true, message: "è¯·éæ©è®¡åå¼å§æ¶é´", trigger: "change" }, |
| | | ], |
| | | planEndTime: [ |
| | | { required: true, message: "è¯·éæ©è®¡åå®å·¥æ¶é´", trigger: "change" }, |
| | | ], |
| | | reportDate: [ |
| | | { required: true, message: "è¯·éæ©æ¬æ¬¡è¿åº¦æ¥æ", trigger: "change" }, |
| | | ], |
| | | completionProgress: [ |
| | | { required: true, message: "请è¾å
¥å®æè¿åº¦", trigger: "blur" }, |
| | | ], |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | // åç«¯ä¸æ¥æ¶ã䏿¬¡è¿åº¦ãï¼ç±å·²æ±æ¥è®°å½éçæå¤§ progress æ¨ç®ï¼åªä½å¡«ååè |
| | | const deriveLastProgress = stages => |
| | | (Array.isArray(stages) ? stages : []).reduce((max, row) => { |
| | | const value = Number(row?.progress ?? 0) || 0; |
| | | return value > max ? value : max; |
| | | }, 0); |
| | | |
| | | const markDone = () => { |
| | | form.value.completionProgress = "100"; |
| | | form.value.totalProgress = "100"; |
| | | if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate; |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | try { |
| | | const res = await getProject(projectId.value); |
| | | const raw = res?.data?.data ?? res?.data ?? res; |
| | | info.value = raw?.info || {}; |
| | | projectName.value = info.value.title || ""; |
| | | |
| | | const [nodes, stages] = await Promise.all([ |
| | | fetchPlanNodeList(info.value.projectManagementPlanId), |
| | | fetchProjectStages(projectId.value), |
| | | ]); |
| | | planNodes.value = nodes; |
| | | lastProgress.value = deriveLastProgress(stages); |
| | | |
| | | form.value.planNodeId = defaultPlanNodeId(info.value.stage, nodes); |
| | | form.value.planStartTime = info.value.planStartTime || ""; |
| | | form.value.planEndTime = info.value.planEndTime || ""; |
| | | form.value.actualStartTime = info.value.actualStartTime || ""; |
| | | form.value.actualEndTime = info.value.actualEndTime || ""; |
| | | // æå岿±æ¥æé¢å¡«ç´¯è®¡è¿åº¦ï¼æ²¡æåçç©ºï¼æäº¤æ¶éåã宿è¿åº¦ã |
| | | form.value.totalProgress = lastProgress.value |
| | | ? String(lastProgress.value) |
| | | : ""; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·å项ç®è¯¦æ
失败", icon: "none" }); |
| | | } |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | loading.value = true; |
| | | saveStage( |
| | | buildStagePayload({ |
| | | projectId: projectId.value, |
| | | projectInfo: info.value, |
| | | planNodes: planNodes.value, |
| | | form: form.value, |
| | | attachments: attachmentFiles.value, |
| | | leaderId: userStore.id, |
| | | leaderName: userStore.nickName, |
| | | }) |
| | | ) |
| | | .then(() => { |
| | | uni.showToast({ title: "æ±æ¥æå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ±æ¥å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | projectId.value = options?.id || ""; |
| | | if (projectId.value) loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .project-progress { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .project-banner { |
| | | margin: 12px 12px 0; |
| | | padding: 14px 16px; |
| | | border-radius: 12px; |
| | | background: #ffffff; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .banner-label { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .banner-value { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .progress-input { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .upload-block { |
| | | padding: 12px; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ£éªé¡¹å¤å®ï¼éæ±ä¹.3ï¼ |
| | | * |
| | | * å¤å®å£å¾éåå¤å»ç®¡ç端 qualityManagement/finalInspection/components/formDia.vue |
| | | * é watch(tableData) ç飿®µé»è¾ï¼ä¸è¦èªè¡åæï¼ |
| | | * æ°åå(parameterType === 1)ï¼minValue <= æ£éªå¼ <= maxValue |
| | | * æåå(parameterType å
¶ä½)ï¼æ£éªå¼å
¨ç standardValue |
| | | * å
¨é¨é¡¹éè¿ â åæ ¼ï¼å
¨é¨é¡¹æªéè¿ â ä¸åæ ¼ï¼å
¶ä½ â é¨ååæ ¼ |
| | | * |
| | | * 注æå¤å®ç¨çæ¯æå¤§å¼/æå°å¼ï¼"å
æ§å¼(controlValue)"åªä½å±ç¤ºãä¸åä¸å¤å®ã |
| | | * 管çç«¯å¨æ£éªé¡¹ä¸ºç©ºæ¶è·³è¿å¤å®ï¼è¿é对åºè¿å空串ï¼ç±è°ç¨æ¹å³å®æ¯å¦ä¿çåå¼ã |
| | | */ |
| | | |
| | | export const TEXT_TYPE = 0; |
| | | export const NUMERIC_TYPE = 1; |
| | | |
| | | /** åè¡æ¯å¦å¤å®éè¿ */ |
| | | export function isItemPass(item) { |
| | | const val = item?.testValue; |
| | | if (val === null || val === undefined || val === "") return false; |
| | | if (item.parameterType === NUMERIC_TYPE) { |
| | | const numVal = Number(val); |
| | | const min = Number(item.minValue); |
| | | const max = Number(item.maxValue); |
| | | return !isNaN(numVal) && numVal >= min && numVal <= max; |
| | | } |
| | | return String(val) === String(item.standardValue); |
| | | } |
| | | |
| | | /** æ£éªé¡¹å表 â æ£æµç»æï¼åæ ¼ / ä¸åæ ¼ / é¨ååæ ¼ï¼ï¼ç©ºå表è¿å空串 */ |
| | | export function judgeInspectionResult(rows) { |
| | | const list = Array.isArray(rows) ? rows : []; |
| | | if (list.length === 0) return ""; |
| | | |
| | | let passCount = 0; |
| | | let failCount = 0; |
| | | list.forEach(item => { |
| | | if (isItemPass(item)) passCount++; |
| | | else failCount++; |
| | | }); |
| | | |
| | | if (failCount === 0 && passCount > 0) return "åæ ¼"; |
| | | if (passCount === 0 && failCount > 0) return "ä¸åæ ¼"; |
| | | return "é¨ååæ ¼"; |
| | | } |
| | | |
| | | /** 临æ¶è¿½å çæ£æµé¡¹è¡ï¼éæ±ä¹.2ï¼ï¼ä¸åææ æ å约æï¼ååæ®µé½å¯ç¼è¾ */ |
| | | export function createTempItem() { |
| | | return { |
| | | id: undefined, |
| | | isTemp: true, |
| | | parameterItem: "", |
| | | parameterType: NUMERIC_TYPE, |
| | | unit: "", |
| | | standardValue: "", |
| | | controlValue: "", |
| | | maxValue: null, |
| | | minValue: null, |
| | | testValue: "", |
| | | }; |
| | | } |
| | |
| | | <view class="inspection-items-container"> |
| | | <view class="steps-header"> |
| | | <text class="steps-title">æ£éªé¡¹ç®</text> |
| | | <view class="steps-header-right"> |
| | | <text class="steps-count">å
± {{ tableData.length }} 个项ç®</text> |
| | | <up-button type="primary" |
| | | size="mini" |
| | | text="è¿½å æ£æµé¡¹" |
| | | @click="addTempItem" /> |
| | | </view> |
| | | </view> |
| | | <view class="steps-list"> |
| | | <view v-for="(item, index) in tableData" |
| | |
| | | <view class="step-content"> |
| | | <view class="step-row"> |
| | | <text class="step-label">ææ ï¼</text> |
| | | <text class="step-value">{{ item.parameterItem }}</text> |
| | | <up-input v-if="item.isTemp" |
| | | v-model="item.parameterItem" |
| | | placeholder="请è¾å
¥ææ åç§°" |
| | | border-bottom |
| | | class="step-input" /> |
| | | <text v-else |
| | | class="step-value">{{ item.parameterItem }}</text> |
| | | </view> |
| | | <view class="step-row"> |
| | | <text class="step-label">åæ°ç±»åï¼</text> |
| | | <view v-if="item.isTemp" |
| | | class="type-switch"> |
| | | <text class="type-option" |
| | | :class="{ active: item.parameterType === TEXT_TYPE }" |
| | | @click="setTempType(item, TEXT_TYPE)">æå</text> |
| | | <text class="type-option" |
| | | :class="{ active: item.parameterType === NUMERIC_TYPE }" |
| | | @click="setTempType(item, NUMERIC_TYPE)">æ°å</text> |
| | | </view> |
| | | <text v-else |
| | | class="step-value">{{ isNumeric(item) ? "æ°å" : "æå" }}</text> |
| | | </view> |
| | | <!-- æ°ååæä¸ä¸éå¤å®ï¼æä¸ä¸éæåºæ¥æç徿èªå¨å¤å®ç便® --> |
| | | <template v-if="isNumeric(item)"> |
| | | <view class="step-row"> |
| | | <text class="step-label">æå¤§å¼ï¼</text> |
| | | <up-input v-if="item.isTemp" |
| | | v-model="item.maxValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå¤§å¼" |
| | | border-bottom |
| | | class="step-input" /> |
| | | <text v-else |
| | | class="step-value">{{ item.maxValue }}</text> |
| | | </view> |
| | | <view class="step-row"> |
| | | <text class="step-label">æå°å¼ï¼</text> |
| | | <up-input v-if="item.isTemp" |
| | | v-model="item.minValue" |
| | | type="number" |
| | | placeholder="请è¾å
¥æå°å¼" |
| | | border-bottom |
| | | class="step-input" /> |
| | | <text v-else |
| | | class="step-value">{{ item.minValue }}</text> |
| | | </view> |
| | | </template> |
| | | <view class="step-row"> |
| | | <text class="step-label">åä½ï¼</text> |
| | | <text class="step-value">{{ item.unit }}</text> |
| | | <up-input v-if="item.isTemp" |
| | | v-model="item.unit" |
| | | placeholder="请è¾å
¥åä½" |
| | | border-bottom |
| | | class="step-input" /> |
| | | <text v-else |
| | | class="step-value">{{ item.unit }}</text> |
| | | </view> |
| | | <view class="step-row"> |
| | | <text class="step-label">æ åå¼ï¼</text> |
| | | <text class="step-value">{{ item.standardValue }}</text> |
| | | <up-input v-if="item.isTemp" |
| | | v-model="item.standardValue" |
| | | placeholder="请è¾å
¥æ åå¼" |
| | | border-bottom |
| | | class="step-input" /> |
| | | <text v-else |
| | | class="step-value">{{ item.standardValue }}</text> |
| | | </view> |
| | | <view class="step-row"> |
| | | <text class="step-label">å
æ§å¼ï¼</text> |
| | | <text class="step-value">{{ item.controlValue }}</text> |
| | | <up-input v-if="item.isTemp" |
| | | v-model="item.controlValue" |
| | | placeholder="请è¾å
¥å
æ§å¼" |
| | | border-bottom |
| | | class="step-input" /> |
| | | <text v-else |
| | | class="step-value">{{ item.controlValue }}</text> |
| | | </view> |
| | | <view class="step-row"> |
| | | <text class="step-label">æ£éªå¼ï¼</text> |
| | | <up-input v-model="item.testValue" |
| | | :type="isNumeric(item) ? 'number' : 'text'" |
| | | placeholder="请è¾å
¥æ£éªå¼" |
| | | clearable |
| | | border-bottom |
| | | class="step-input" /> |
| | | </view> |
| | | <view class="step-row"> |
| | | <text class="step-label">å¤å®ï¼</text> |
| | | <text class="step-value judge-text" |
| | | :class="itemPassClass(item)"> |
| | | {{ judgeText(item) }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <view v-if="item.isTemp" |
| | | class="delete-btn" |
| | | @click="removeTempItem(index)"> |
| | | <up-icon name="close" |
| | | size="14" |
| | | color="#ffffff" /> |
| | | </view> |
| | | </view> |
| | | <view v-if="tableData.length === 0" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted, nextTick } from "vue"; |
| | | import { ref, computed, onMounted, nextTick, watch } from "vue"; |
| | | import { onShow, onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import dayjs from "dayjs"; |
| | |
| | | list, |
| | | } from "@/api/qualityManagement/materialInspection.js"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import { |
| | | NUMERIC_TYPE, |
| | | TEXT_TYPE, |
| | | createTempItem, |
| | | isItemPass, |
| | | judgeInspectionResult, |
| | | } from "./_utils/inspection"; |
| | | |
| | | // æ¾ç¤ºæç¤ºä¿¡æ¯ |
| | | const showToast = message => { |
| | |
| | | }); |
| | | }; |
| | | |
| | | // æ°ååæææå¤§/æå°å¼ï¼å¤å®ä¹æä¸ä¸éèµ° |
| | | const isNumeric = item => item?.parameterType === NUMERIC_TYPE; |
| | | |
| | | const judgeText = item => { |
| | | const val = item?.testValue; |
| | | if (val === null || val === undefined || val === "") return "-"; |
| | | return isItemPass(item) ? "åæ ¼" : "ä¸åæ ¼"; |
| | | }; |
| | | |
| | | const itemPassClass = item => { |
| | | const val = item?.testValue; |
| | | if (val === null || val === undefined || val === "") return ""; |
| | | return isItemPass(item) ? "pass" : "fail"; |
| | | }; |
| | | |
| | | // éæ±ä¹.3ï¼æ£éªå¼ä¸åå°±èªå¨å¤å®ï¼å£å¾è§ _utils/inspection.jsã |
| | | // ä¸ç®¡ç端ä¸è´ï¼è¿éæ æ¡ä»¶è¦ç form.checkResult ââ æå·¥éçæ£æµç»æ |
| | | // åªæ¯ä¸´æ¶å
åºï¼ä¸æ¬¡æ¹å¨æ£éªé¡¹æ¶ä¼è¢«éæ°å¤å®è¦çã |
| | | watch( |
| | | tableData, |
| | | rows => { |
| | | const result = judgeInspectionResult(rows); |
| | | if (result) form.value.checkResult = result; |
| | | }, |
| | | { deep: true } |
| | | ); |
| | | |
| | | // éæ±ä¹.2ï¼ä¸´æ¶è¿½å æ£æµé¡¹ |
| | | const addTempItem = () => { |
| | | tableData.value.push(createTempItem()); |
| | | }; |
| | | |
| | | const removeTempItem = index => { |
| | | tableData.value.splice(index, 1); |
| | | }; |
| | | |
| | | // åç±»åæ¶æ¸
æä¸åéç¨çå¼ï¼é¿å
æååæ®çä¸ä¸éãæ°ååæ®çæ åå¼ |
| | | const setTempType = (item, type) => { |
| | | if (item.parameterType === type) return; |
| | | item.parameterType = type; |
| | | if (type === NUMERIC_TYPE) { |
| | | item.standardValue = ""; |
| | | } else { |
| | | item.maxValue = null; |
| | | item.minValue = null; |
| | | } |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const submitForm = async () => { |
| | | console.log("submitForm", form.value, tableData.value); |
| | |
| | | showToast("è¯·éæ©æ£æµç»æ"); |
| | | return; |
| | | } |
| | | const missingTemp = tableData.value.find( |
| | | item => item.isTemp && !item.parameterItem |
| | | ); |
| | | if (missingTemp) { |
| | | showToast("请填åè¿½å æ£æµé¡¹çææ åç§°"); |
| | | return; |
| | | } |
| | | |
| | | loading.value = true; |
| | | |
| | | form.value.inspectType = 2; |
| | | // isTemp åªæ¯å端çè¡æ è®°ï¼ä¸è½æäº¤ç»å端 |
| | | tableData.value = tableData.value.map(({ isTemp, ...rest }) => rest); |
| | | if (!isEdit.value) { |
| | | tableData.value.forEach(item => { |
| | | delete item.id; |
| | |
| | | color: #909399; |
| | | } |
| | | |
| | | .steps-header-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | // 临æ¶è¿½å è¡çåæ°ç±»å忢 |
| | | .type-switch { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .type-option { |
| | | padding: 4px 14px; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | background: #f4f4f5; |
| | | border-radius: 12px; |
| | | } |
| | | |
| | | .type-option.active { |
| | | color: #409eff; |
| | | background: #ecf5ff; |
| | | } |
| | | |
| | | .judge-text.pass { |
| | | color: #67c23a; |
| | | } |
| | | |
| | | .judge-text.fail { |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | .steps-list { |
| | | margin-bottom: 20px; |
| | | } |
| | |
| | | <text class="item-detail-label">åä½:</text> |
| | | <text class="item-detail-value">{{ item.unit || '-' }}</text> |
| | | </view> |
| | | <!-- æ°ååæä¸ä¸éå¤å®ï¼æä¸ä¸éæåºæ¥æç徿å¤å®ä¾æ® --> |
| | | <view v-if="isNumeric(item)" |
| | | class="item-detail-row"> |
| | | <text class="item-detail-label">ä¸ä¸é:</text> |
| | | <text class="item-detail-value">{{ item.minValue }} ~ {{ item.maxValue }}</text> |
| | | </view> |
| | | <view class="item-detail-row"> |
| | | <text class="item-detail-label">æ åå¼:</text> |
| | | <text class="item-detail-value">{{ item.standardValue || '-' }}</text> |
| | |
| | | </view> |
| | | <view class="item-detail-row"> |
| | | <text class="item-detail-label">æ£éªå¼:</text> |
| | | <text class="item-detail-value">{{ item.testValue || '-' }}</text> |
| | | </view> |
| | | <view class="item-detail-row"> |
| | | <text class="item-detail-label">å¤å®:</text> |
| | | <text class="item-detail-value result" |
| | | :class="getResultClass(item.testValue, item.standardValue)"> |
| | | {{ item.testValue || '-' }} |
| | | :class="judgeClass(item)"> |
| | | {{ judgeText(item) }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- æä½æé® --> |
| | | <!-- <view class="action-buttons"> |
| | | <!-- éæ±ä¹.4ï¼ä¸è½½ PDF åæ ¼è¯ --> |
| | | <view class="action-buttons"> |
| | | <u-button type="primary" |
| | | class="action-btn" |
| | | :loading="downloading" |
| | | @click="downloadReport"> |
| | | ä¸è½½æ¥å |
| | | ä¸è½½åæ ¼è¯PDF |
| | | </u-button> |
| | | </view> --> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import dayjs from "dayjs"; |
| | | import { qualityInspectParamInfo } from "@/api/qualityManagement/materialInspection.js"; |
| | | import { downloadQualityInspectPdf } from "@/api/qualityManagement/qualityInspectFile.js"; |
| | | import { NUMERIC_TYPE, isItemPass } from "./_utils/inspection"; |
| | | |
| | | // æ¾ç¤ºæç¤ºä¿¡æ¯ |
| | | const showToast = message => { |
| | |
| | | const detailData = ref(null); |
| | | // æ£éªé¡¹ç® |
| | | const inspectionItems = ref([]); |
| | | // åæ ¼è¯PDF ä¸è½½ä¸ |
| | | const downloading = ref(false); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | |
| | | return state ? "success" : "warning"; |
| | | }; |
| | | |
| | | // è·åç»ææ ·å¼ |
| | | const getResultClass = (testValue, standardValue) => { |
| | | // ç®åçç»æå¤æé»è¾ï¼å®é
项ç®ä¸å¯è½éè¦æ´å¤æç夿 |
| | | if (testValue === "åæ ¼") { |
| | | return "result-passed"; |
| | | } else if (testValue === "ä¸åæ ¼") { |
| | | return "result-rejected"; |
| | | } |
| | | return ""; |
| | | // æ°ååææä¸ä¸é |
| | | const isNumeric = item => item?.parameterType === NUMERIC_TYPE; |
| | | |
| | | const judgeText = item => { |
| | | const val = item?.testValue; |
| | | if (val === null || val === undefined || val === "") return "-"; |
| | | return isItemPass(item) ? "åæ ¼" : "ä¸åæ ¼"; |
| | | }; |
| | | |
| | | // ä¸è½½æ¥å |
| | | const downloadReport = () => { |
| | | uni.showToast({ |
| | | title: "æ¥åä¸è½½ä¸...", |
| | | icon: "loading", |
| | | }); |
| | | const judgeClass = item => { |
| | | const val = item?.testValue; |
| | | if (val === null || val === undefined || val === "") return ""; |
| | | return isItemPass(item) ? "result-passed" : "result-rejected"; |
| | | }; |
| | | |
| | | // 模æä¸è½½ |
| | | setTimeout(() => { |
| | | uni.showToast({ |
| | | title: "æ¥åä¸è½½æå", |
| | | icon: "success", |
| | | // ä¸è½½ PDF åæ ¼è¯ |
| | | const downloadReport = () => { |
| | | if (!optionsId.value) { |
| | | showToast("åæ°é误"); |
| | | return; |
| | | } |
| | | downloading.value = true; |
| | | // downloadFileByPost å
é¨å·² toastï¼è¿éåªè´è´£æ¶å°¾ loading |
| | | downloadQualityInspectPdf(optionsId.value) |
| | | .catch(() => {}) |
| | | .finally(() => { |
| | | downloading.value = false; |
| | | }); |
| | | }, 1500); |
| | | }; |
| | | |
| | | // è·å详æ
æ°æ® |
| | |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | .action-buttons { |
| | | padding: 4px 0 8px; |
| | | } |
| | | |
| | | .action-btn { |
| | | width: 100%; |
| | | } |
| | | |
| | | // ç©ºç¶æ |
| | | .no-data { |
| | | padding: 60px 20px; |
| | |
| | | <!-- 页é¢å¤´é¨ --> |
| | | <PageHeader title="é件管ç" |
| | | @back="goBack" /> |
| | | <!-- éæ±ä¹.4ï¼ä¸è½½ PDF åæ ¼è¯ --> |
| | | <view class="pdf-bar"> |
| | | <u-button type="primary" |
| | | size="small" |
| | | :loading="downloadingPdf" |
| | | @click="downloadPdf"> |
| | | ä¸è½½åæ ¼è¯PDF |
| | | </u-button> |
| | | </view> |
| | | <!-- éä»¶å表 --> |
| | | <view class="file-list-container"> |
| | | <view v-if="fileList.length > 0" |
| | |
| | | qualityInspectFileListPage, |
| | | qualityInspectFileDel, |
| | | } from "@/api/qualityManagement/materialInspection"; |
| | | import { downloadQualityInspectPdf } from "@/api/qualityManagement/qualityInspectFile.js"; |
| | | import { openFile } from "@/utils/file"; |
| | | import { blobValidate } from "@/utils/ruoyi"; |
| | | |
| | | // éä»¶å表 |
| | | const fileList = ref([]); |
| | | // åæ ¼è¯PDF ä¸è½½ä¸ |
| | | const downloadingPdf = ref(false); |
| | | |
| | | // ä¸è½½ PDF åæ ¼è¯ï¼ä¸éä»¶åä¸ä»½ inspectIdï¼ |
| | | const downloadPdf = () => { |
| | | if (!rulesRegulationsManagementId.value) { |
| | | showToast("åæ°é误"); |
| | | return; |
| | | } |
| | | downloadingPdf.value = true; |
| | | downloadQualityInspectPdf(rulesRegulationsManagementId.value) |
| | | .catch(() => {}) |
| | | .finally(() => { |
| | | downloadingPdf.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | |
| | | }); |
| | | }); |
| | | }; |
| | | // ä¸è½½æä»¶ |
| | | // ä¸è½½å¹¶é¢è§ã |
| | | // åæ¥èµ°çæ¯ /common/download?...&delete=true ââ 伿æå¡ç«¯éä»¶å æï¼ |
| | | // ä¸åè°éå¼ç¨äºæªå®ä¹ç resolve/rejectãæ¹ä¸ºç´æ¥æ url æå¼ã |
| | | const downloadFile = file => { |
| | | var url = |
| | | config.baseUrl + |
| | | "/common/download?fileName=" + |
| | | encodeURIComponent(file.url) + |
| | | "&delete=true"; |
| | | console.log(url, "url"); |
| | | |
| | | uni |
| | | .downloadFile({ |
| | | url: url, |
| | | responseType: "blob", |
| | | header: { Authorization: "Bearer " + getToken() }, |
| | | }) |
| | | .then(res => { |
| | | let osType = uni.getStorageSync("deviceInfo").osName; |
| | | let filePath = res.tempFilePath; |
| | | if (osType === "ios") { |
| | | uni.openDocument({ |
| | | filePath: filePath, |
| | | showMenu: true, |
| | | success: res => { |
| | | resolve(res); |
| | | }, |
| | | fail: err => { |
| | | console.log("uni.openDocument--fail"); |
| | | reject(err); |
| | | }, |
| | | }); |
| | | } else { |
| | | uni.saveFile({ |
| | | tempFilePath: filePath, |
| | | success: fileRes => { |
| | | uni.showToast({ |
| | | icon: "none", |
| | | mask: true, |
| | | title: |
| | | "æä»¶å·²ä¿åï¼Android/data/uni.UNI720216F/apps/__UNI__720216F/" + |
| | | fileRes.savedFilePath, //ä¿åè·¯å¾ |
| | | duration: 3000, |
| | | }); |
| | | setTimeout(() => { |
| | | //æå¼ææ¡£æ¥ç |
| | | uni.openDocument({ |
| | | filePath: fileRes.savedFilePath, |
| | | success: function (res) { |
| | | resolve(fileRes); |
| | | }, |
| | | }); |
| | | }, 3000); |
| | | }, |
| | | 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 => { |
| | | console.error("ä¸è½½å¤±è´¥:", err); |
| | | showToast("ä¸è½½å¤±è´¥"); |
| | | }); |
| | | openFile(file?.url); |
| | | }; |
| | | |
| | | // 确认å é¤ |
| | |
| | | padding: 20rpx; |
| | | } |
| | | |
| | | .pdf-bar { |
| | | padding: 16rpx 20rpx 0; |
| | | } |
| | | |
| | | .file-list { |
| | | background: #ffffff; |
| | | border-radius: 8rpx; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="inspection-report-detail"> |
| | | <PageHeader title="æ¥å详æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view class="detail-card" |
| | | v-if="!loading"> |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ detail.reportNo || "-" }}</text> |
| | | <up-tag :text="detail.reportType || 'æªåç±»'" |
| | | type="primary" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <view class="warn-banner" |
| | | v-if="warn.text" |
| | | :style="{ background: warn.level === 'normal' ? '#f0f9eb' : '#fef0f0' }"> |
| | | <text class="warn-banner-text" |
| | | :style="{ color: warn.color }">æææç¶æï¼{{ warn.text }}ï¼å°ææ¶é´ {{ detail.expirationTime || "-" }}ï¼</text> |
| | | </view> |
| | | |
| | | <u-cell-group title="æ¥åä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="产ååç§°" |
| | | :value="detail.productName || '-'" /> |
| | | <u-cell title="产ååå·" |
| | | :value="detail.productModel || '-'" /> |
| | | <u-cell title="æ¥åç¼å·" |
| | | :value="detail.reportNo || '-'" /> |
| | | <u-cell title="æ¥åç±»å" |
| | | :value="detail.reportType || '-'" /> |
| | | <u-cell title="æ£æµåä½" |
| | | :value="detail.testingAgency || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æææ" |
| | | class="detail-section"> |
| | | <u-cell title="æ¥åæ¥æ" |
| | | :value="detail.reportDate || '-'" /> |
| | | <u-cell title="æææ(æ)" |
| | | :value="detail.validityPeriod != null ? String(detail.validityPeriod) : '-'" /> |
| | | <u-cell title="å°ææ¶é´" |
| | | :value="detail.expirationTime || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="夿³¨" |
| | | class="detail-section"> |
| | | <u-cell title="夿³¨" |
| | | :value="detail.remark || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éä»¶" |
| | | class="detail-section"> |
| | | <view v-if="attachments.length === 0" |
| | | class="empty-file"> |
| | | <text>ææ éä»¶</text> |
| | | </view> |
| | | <view v-else |
| | | class="file-list"> |
| | | <view v-for="(file, index) in attachments" |
| | | :key="index" |
| | | class="file-item" |
| | | @click="previewFile(file, index)"> |
| | | <up-icon name="file-text" |
| | | size="18" |
| | | color="#2979ff"></up-icon> |
| | | <text class="file-name">{{ fileLabel(file, index) }}</text> |
| | | <up-icon name="download" |
| | | size="16" |
| | | color="#909399"></up-icon> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { toFileList } from "@/utils/file"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { getWarnInfo } from "@/utils/warning"; |
| | | import { getInspectionReport } from "@/api/qualityManagement/inspectionReport"; |
| | | |
| | | const WARN_DAYS = 30; |
| | | |
| | | const loading = ref(true); |
| | | const reportId = ref(""); |
| | | const detail = ref({}); |
| | | |
| | | const attachments = computed(() => toFileList(detail.value.attachmentUrls)); |
| | | const warn = computed(() => getWarnInfo(detail.value.expirationTime, WARN_DAYS)); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("inspectionReportDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/qualityManagement/inspectionReport/edit?id=${reportId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const fileLabel = (file, index) => |
| | | file.name || file.originalFilename || `éä»¶${index + 1}`; |
| | | |
| | | // request.ts æªéä¼ responseTypeï¼blob ä¸è½½ä¸å¯ç¨ï¼åªè½ç¨ downloadFile + openDocument |
| | | const previewFile = (file, index) => { |
| | | const url = file.url || file.previewURL; |
| | | if (!url) return; |
| | | uni.showLoading({ title: "æå¼ä¸...", mask: true }); |
| | | uni.downloadFile({ |
| | | url, |
| | | success: res => { |
| | | if (res.statusCode !== 200) { |
| | | uni.showToast({ title: "æä»¶ä¸è½½å¤±è´¥", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.openDocument({ |
| | | filePath: res.tempFilePath, |
| | | showMenu: true, |
| | | fail: () => uni.showToast({ title: "æä¸æ¯æé¢è§è¯¥æä»¶", icon: "none" }), |
| | | }); |
| | | }, |
| | | fail: () => uni.showToast({ title: "æä»¶ä¸è½½å¤±è´¥", icon: "none" }), |
| | | complete: () => uni.hideLoading(), |
| | | }); |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const data = await loadDetailData({ |
| | | cacheKey: "inspectionReportDetail", |
| | | id: reportId.value, |
| | | fetcher: getInspectionReport, |
| | | }); |
| | | detail.value = data || {}; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·åæ¥å详æ
失败", icon: "none" }); |
| | | detail.value = {}; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | reportId.value = options?.id || ""; |
| | | if (!reportId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .inspection-report-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .warn-banner { |
| | | margin-top: 12px; |
| | | padding: 10px 14px; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | .warn-banner-text { |
| | | font-size: 13px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .empty-file { |
| | | padding: 16px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .file-list { |
| | | padding: 6px 16px 12px; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .file-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .file-name { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #333; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="110" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="æ¥åä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="产ååç§°" |
| | | prop="productName" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.productName" |
| | | placeholder="请è¾å
¥äº§ååç§°" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="产ååå·" |
| | | prop="productModel" |
| | | border-bottom> |
| | | <up-input v-model="form.productModel" |
| | | placeholder="请è¾å
¥äº§ååå·" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¥åç¼å·" |
| | | prop="reportNo" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.reportNo" |
| | | placeholder="请è¾å
¥æ¥åç¼å·" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¥åç±»å" |
| | | prop="reportType" |
| | | border-bottom> |
| | | <FormPicker v-model="form.reportType" |
| | | type="select" |
| | | title="éæ©æ¥åç±»å" |
| | | :options="reportTypeOptions" |
| | | placeholder="è¯·éæ©æ¥åç±»å" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ£æµåä½" |
| | | prop="testingAgency" |
| | | border-bottom> |
| | | <up-input v-model="form.testingAgency" |
| | | placeholder="请è¾å
¥æ£æµåä½" |
| | | clearable /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æææ" |
| | | class="form-section"> |
| | | <up-form-item label="æ¥åæ¥æ" |
| | | prop="reportDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.reportDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥åæ¥æ" |
| | | @change="calculateExpiration" /> |
| | | </up-form-item> |
| | | <up-form-item label="æææ(æ)" |
| | | prop="validityPeriod" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.validityPeriod" |
| | | type="number" |
| | | placeholder="1~120ï¼é»è®¤ 12" |
| | | clearable |
| | | @blur="calculateExpiration" /> |
| | | </up-form-item> |
| | | <up-form-item label="å°ææ¶é´" |
| | | prop="expirationTime" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.expirationTime" |
| | | placeholder="ç³»ç»èªå¨è®¡ç®" |
| | | disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨" |
| | | prop="remark"> |
| | | <up-textarea v-model="form.remark" |
| | | placeholder="请è¾å
¥å¤æ³¨å
容" |
| | | auto-height |
| | | maxlength="500" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éä»¶" |
| | | class="form-section"> |
| | | <view class="upload-wrap"> |
| | | <CommonUpload v-model="form.attachmentFiles" |
| | | accept="all" |
| | | :max-count="5" /> |
| | | </view> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </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 FormPicker from "@/components/FormPicker.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { toFileList, toUrlString } from "@/utils/file"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { addMonths } from "@/utils/warning"; |
| | | import { |
| | | addInspectionReport, |
| | | getInspectionReport, |
| | | updateInspectionReport, |
| | | } from "@/api/qualityManagement/inspectionReport"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const reportId = ref(""); |
| | | |
| | | const reportTypeOptions = [ |
| | | { name: "åå¼è¯éªæ¥å", value: "åå¼è¯éªæ¥å" }, |
| | | { name: "åºåæ£éªæ¥å", value: "åºåæ£éªæ¥å" }, |
| | | { name: "å§ææ£éªæ¥å", value: "å§ææ£éªæ¥å" }, |
| | | ]; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | productName: "", |
| | | productModel: "", |
| | | reportNo: "", |
| | | reportType: "", |
| | | testingAgency: "", |
| | | reportDate: "", |
| | | validityPeriod: 12, |
| | | expirationTime: "", |
| | | remark: "", |
| | | attachmentFiles: [], |
| | | }); |
| | | |
| | | const rules = { |
| | | productName: [ |
| | | { required: true, message: "请è¾å
¥äº§ååç§°", trigger: "blur" }, |
| | | ], |
| | | reportNo: [{ required: true, message: "请è¾å
¥æ¥åç¼å·", trigger: "blur" }], |
| | | reportDate: [ |
| | | { required: true, message: "è¯·éæ©æ¥åæ¥æ", trigger: "change" }, |
| | | ], |
| | | validityPeriod: [ |
| | | { required: true, message: "请è¾å
¥æææ", trigger: "blur" }, |
| | | ], |
| | | expirationTime: [ |
| | | { required: true, message: "å°ææ¶é´ä¸è½ä¸ºç©º", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => (reportId.value ? "ç¼è¾æ¥å" : "æ°å¢æ¥å")); |
| | | |
| | | // å°ææ¶é´ = æ¥åæ¥æ + ææææï¼åªè¯» |
| | | const calculateExpiration = () => { |
| | | const months = Number(form.value.validityPeriod); |
| | | if (form.value.reportDate && Number.isFinite(months) && months > 0) { |
| | | form.value.expirationTime = addMonths(form.value.reportDate, months); |
| | | } else { |
| | | form.value.expirationTime = ""; |
| | | } |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const loadDetail = async () => { |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "inspectionReportDetail", |
| | | id: reportId.value, |
| | | fetcher: getInspectionReport, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "æªæ¾å°æ¥åä¿¡æ¯", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | form.value = { |
| | | id: data.id, |
| | | productName: data.productName || "", |
| | | productModel: data.productModel || "", |
| | | reportNo: data.reportNo || "", |
| | | reportType: data.reportType || "", |
| | | testingAgency: data.testingAgency || "", |
| | | reportDate: (data.reportDate || "").slice(0, 10), |
| | | validityPeriod: data.validityPeriod ?? 12, |
| | | expirationTime: (data.expirationTime || "").slice(0, 10), |
| | | remark: data.remark || "", |
| | | attachmentFiles: toFileList(data.attachmentUrls), |
| | | }; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | calculateExpiration(); |
| | | const payload = { |
| | | id: form.value.id, |
| | | productName: form.value.productName, |
| | | productModel: form.value.productModel, |
| | | reportNo: form.value.reportNo, |
| | | reportType: form.value.reportType, |
| | | testingAgency: form.value.testingAgency, |
| | | reportDate: form.value.reportDate, |
| | | validityPeriod: Number(form.value.validityPeriod), |
| | | expirationTime: form.value.expirationTime, |
| | | remark: form.value.remark, |
| | | attachmentUrls: toUrlString(form.value.attachmentFiles), |
| | | }; |
| | | |
| | | loading.value = true; |
| | | const action = reportId.value ? updateInspectionReport : addInspectionReport; |
| | | action(payload) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 1000); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | reportId.value = options.id; |
| | | form.value.id = options.id; |
| | | loadDetail(); |
| | | } else { |
| | | form.value.reportDate = formatDateToYMD(Date.now()); |
| | | calculateExpiration(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .upload-wrap { |
| | | padding: 12px; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.productName" |
| | | placeholder="请è¾å
¥äº§ååç§°æç´¢" |
| | | clearable |
| | | @change="handleSearch" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | <view v-if="expiringCount > 0" |
| | | class="warn-tip"> |
| | | <text class="warn-tip-text">{{ expiringCount }} 份æ¥å已尿æå°å¨ 30 天å
å°æ</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="reportList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in reportList" |
| | | :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.reportNo || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="item.reportType || 'æªåç±»'" |
| | | type="primary" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">产ååç§°</text> |
| | | <text class="detail-value">{{ item.productName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">产ååå·</text> |
| | | <text class="detail-value">{{ item.productModel || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ£æµåä½</text> |
| | | <text class="detail-value">{{ item.testingAgency || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ¥åæ¥æ</text> |
| | | <text class="detail-value">{{ item.reportDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å°ææ¶é´</text> |
| | | <text class="detail-value">{{ item.expirationTime || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row" |
| | | v-if="warnInfo(item).text"> |
| | | <text class="detail-label">æææç¶æ</text> |
| | | <text class="detail-value" |
| | | :style="{ color: warnInfo(item).color }">{{ warnInfo(item).text }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">注é</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ æ¥åæ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="æ¥åçé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | delInspectionReport, |
| | | listInspectionReport, |
| | | } from "@/api/qualityManagement/inspectionReport"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { getWarnInfo } from "@/utils/warning"; |
| | | |
| | | // æ¥åæææé¢è¦éå¼ï¼éæ±åï¼å°æå 30 å¤©ï¼ |
| | | const WARN_DAYS = 30; |
| | | |
| | | const reportList = ref([]); |
| | | const showFilter = ref(false); |
| | | |
| | | const query = reactive({ |
| | | productName: "", |
| | | reportNo: "", |
| | | reportType: "", |
| | | }); |
| | | |
| | | const filterFields = [ |
| | | { |
| | | prop: "productName", |
| | | label: "产ååç§°", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥äº§ååç§°", |
| | | }, |
| | | { |
| | | prop: "reportNo", |
| | | label: "æ¥åç¼å·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥æ¥åç¼å·", |
| | | }, |
| | | { |
| | | prop: "reportType", |
| | | label: "æ¥åç±»å", |
| | | type: "select", |
| | | options: [ |
| | | { name: "åå¼è¯éªæ¥å", value: "åå¼è¯éªæ¥å" }, |
| | | { name: "åºåæ£éªæ¥å", value: "åºåæ£éªæ¥å" }, |
| | | { name: "å§ææ£éªæ¥å", value: "å§ææ£éªæ¥å" }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | const warnInfo = item => getWarnInfo(item.expirationTime, WARN_DAYS); |
| | | |
| | | const expiringCount = computed( |
| | | () => |
| | | reportList.value.filter(item => { |
| | | const level = warnInfo(item).level; |
| | | return level === "overdue" || level === "warning"; |
| | | }).length |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("inspectionReportDetail"); |
| | | uni.navigateTo({ url: "/pages/qualityManagement/inspectionReport/edit" }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("inspectionReportDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/qualityManagement/inspectionReport/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("inspectionReportDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/qualityManagement/inspectionReport/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | listInspectionReport({ |
| | | current: -1, |
| | | size: -1, |
| | | productName: query.productName || undefined, |
| | | reportNo: query.reportNo || undefined, |
| | | reportType: query.reportType || undefined, |
| | | }) |
| | | .then(res => { |
| | | reportList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const handleSearch = () => { |
| | | getList(); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "注é确认", |
| | | content: "该æ¥åå°è¢«æ³¨éä½åºï¼æ¯å¦ç¡®è®¤ï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delInspectionReport(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "注éæå", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "注é失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .warn-tip { |
| | | margin-top: 10px; |
| | | padding: 8px 12px; |
| | | border-radius: 8px; |
| | | background: #fff5f5; |
| | | } |
| | | |
| | | .warn-tip-text { |
| | | font-size: 13px; |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="bidding-detail"> |
| | | <PageHeader title="æææ 详æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <view class="hero-left"> |
| | | <text class="hero-title">{{ detail.bidDocName || "-" }}</text> |
| | | <text class="hero-sub">{{ detail.projectName || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="detail.bidResult || 'æªå¡«ç»æ'" |
| | | :type="resultTagType(detail.bidResult)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <view v-if="canStartContract" |
| | | class="contract-entry" |
| | | @click="startContract"> |
| | | <up-icon name="plus-circle" |
| | | size="18" |
| | | color="#2979ff"></up-icon> |
| | | <text class="contract-entry-text">åèµ·ååå½å
¥</text> |
| | | </view> |
| | | |
| | | <u-cell-group title="项ç®ä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="æå±é¡¹ç®" |
| | | :value="detail.projectName || '-'" /> |
| | | <u-cell title="æ 书åç§°" |
| | | :value="detail.bidDocName || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æææ åä½ä¸æ¥æ" |
| | | class="detail-section"> |
| | | <u-cell title="ææ åä½" |
| | | :value="detail.tenderUnit || '-'" /> |
| | | <u-cell title="ææ åä½" |
| | | :value="detail.bidUnit || '-'" /> |
| | | <u-cell title="ææ æ¥æ" |
| | | :value="detail.tenderDate || '-'" /> |
| | | <u-cell title="ææ æ¥æ" |
| | | :value="detail.bidDate || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æ 书款ä¸ä¿è¯é" |
| | | class="detail-section"> |
| | | <u-cell title="æ 书款éé¢" |
| | | :value="formatAmount(detail.bidDocFee)" /> |
| | | <u-cell title="æ ä¹¦æ¬¾ç¶æ" |
| | | :value="detail.bidDocFeeStatus || '-'" /> |
| | | <u-cell title="ç¶æåæ´æ¥æ" |
| | | :value="detail.bidDocFeeStatusChangeDate || '-'" /> |
| | | <u-cell title="ææ ä¿è¯é" |
| | | :value="formatAmount(detail.bidDeposit)" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æææ ç»æ" |
| | | class="detail-section"> |
| | | <u-cell title="æææ ç»æ" |
| | | :value="detail.bidResult || '-'" /> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { setPrefillProject } from "@/utils/prefill"; |
| | | import { getBiddingDetail } from "@/api/salesManagement/biddingManagement"; |
| | | |
| | | // éæ±ä¸.6ï¼ç»æå½ä¸è¿ä¸ç§æ¶å¯ç´æ¥åèµ·éå®ååå½å
¥ |
| | | const CONTRACT_TRIGGER_RESULTS = [ |
| | | "䏿 å¾
åºéç¥ä¹¦", |
| | | "å·²åºä¸æ éç¥ä¹¦", |
| | | "å·²ç¾åå", |
| | | ]; |
| | | |
| | | const loading = ref(true); |
| | | const biddingId = ref(""); |
| | | const detail = ref({}); |
| | | |
| | | const canStartContract = computed(() => |
| | | CONTRACT_TRIGGER_RESULTS.includes(detail.value?.bidResult) |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("biddingDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/biddingManagement/edit?id=${biddingId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const resultTagType = result => { |
| | | if (result === "å·²ç¾åå") return "success"; |
| | | if (result === "åºæ " || result === "æµæ " || result === "æªä¸æ ") { |
| | | return "error"; |
| | | } |
| | | if (result) return "warning"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const startContract = () => { |
| | | setPrefillProject({ |
| | | projectId: detail.value.projectId, |
| | | projectName: detail.value.projectName, |
| | | }); |
| | | uni.setStorageSync("operationType", "add"); |
| | | uni.removeStorageSync("editData"); |
| | | uni.navigateTo({ url: "/pages/sales/salesAccount/detail" }); |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const data = await loadDetailData({ |
| | | cacheKey: "biddingDetail", |
| | | id: biddingId.value, |
| | | fetcher: getBiddingDetail, |
| | | }); |
| | | detail.value = data || {}; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·åæææ 详æ
失败", icon: "none" }); |
| | | detail.value = {}; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | biddingId.value = options?.id || ""; |
| | | if (!biddingId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .bidding-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | max-width: 70%; |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .hero-sub { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .contract-entry { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | margin-top: 12px; |
| | | padding: 11px 0; |
| | | border-radius: 10px; |
| | | border: 1px dashed #2979ff; |
| | | background: #f4f8ff; |
| | | } |
| | | |
| | | .contract-entry-text { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="项ç®ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="æå±é¡¹ç®" |
| | | prop="projectId" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.projectId" |
| | | :display-text="form.projectName" |
| | | type="select" |
| | | title="éæ©æå±é¡¹ç®" |
| | | :options="projectOptions" |
| | | placeholder="è¯·éæ©æå±é¡¹ç®" |
| | | @change="onProjectChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ 书åç§°" |
| | | prop="bidDocName" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.bidDocName" |
| | | placeholder="请è¾å
¥æ 书åç§°" |
| | | clearable /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æææ åä½ä¸æ¥æ" |
| | | class="form-section"> |
| | | <up-form-item label="ææ åä½" |
| | | prop="tenderUnit" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.tenderUnit" |
| | | placeholder="请è¾å
¥ææ åä½" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ææ åä½" |
| | | prop="bidUnit" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.bidUnit" |
| | | placeholder="请è¾å
¥ææ åä½" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ææ æ¥æ" |
| | | prop="tenderDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.tenderDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ææ æ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="ææ æ¥æ" |
| | | prop="bidDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.bidDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ææ æ¥æ" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æ 书款ä¸ä¿è¯é" |
| | | class="form-section"> |
| | | <up-form-item label="æ 书款éé¢" |
| | | prop="bidDocFee" |
| | | border-bottom> |
| | | <up-input v-model="form.bidDocFee" |
| | | type="number" |
| | | placeholder="请è¾å
¥æ 书款éé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æ ä¹¦æ¬¾ç¶æ" |
| | | prop="bidDocFeeStatus" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.bidDocFeeStatus" |
| | | type="select" |
| | | title="éæ©æ ä¹¦æ¬¾ç¶æ" |
| | | :options="bidDocFeeStatusOptions" |
| | | placeholder="è¯·éæ©æ ä¹¦æ¬¾ç¶æ" |
| | | @change="onFeeStatusChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¶æåæ´æ¥æ" |
| | | prop="bidDocFeeStatusChangeDate" |
| | | border-bottom> |
| | | <FormPicker v-model="form.bidDocFeeStatusChangeDate" |
| | | type="date" |
| | | placeholder="ç¶æåæ´æ¶èªå¨å¡«å½åæ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="ææ ä¿è¯é" |
| | | prop="bidDeposit" |
| | | border-bottom> |
| | | <up-input v-model="form.bidDeposit" |
| | | type="number" |
| | | placeholder="请è¾å
¥ææ ä¿è¯ééé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="æææ ç»æ" |
| | | class="form-section"> |
| | | <up-form-item label="æææ ç»æ" |
| | | prop="bidResult" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.bidResult" |
| | | type="select" |
| | | title="éæ©æææ ç»æ" |
| | | :options="bidResultOptions" |
| | | placeholder="è¯·éæ©æææ ç»æ" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </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 FormPicker from "@/components/FormPicker.vue"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { loadProjectOptions, pickProjectName } from "@/utils/projectOptions"; |
| | | import { |
| | | addOrUpdateBidding, |
| | | getBiddingDetail, |
| | | } from "@/api/salesManagement/biddingManagement"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const biddingId = ref(""); |
| | | const projectOptions = ref([]); |
| | | |
| | | const bidDocFeeStatusOptions = [ |
| | | { name: "æªäº¤", value: "æªäº¤" }, |
| | | { name: "已交", value: "已交" }, |
| | | { name: "å·²é", value: "å·²é" }, |
| | | ]; |
| | | |
| | | const bidResultOptions = [ |
| | | { name: "åºæ ", value: "åºæ " }, |
| | | { name: "æµæ ", value: "æµæ " }, |
| | | { name: "æªä¸æ ", value: "æªä¸æ " }, |
| | | { name: "䏿 å¾
åºéç¥ä¹¦", value: "䏿 å¾
åºéç¥ä¹¦" }, |
| | | { name: "å·²åºä¸æ éç¥ä¹¦", value: "å·²åºä¸æ éç¥ä¹¦" }, |
| | | { name: "å·²ç¾åå", value: "å·²ç¾åå" }, |
| | | ]; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | projectId: "", |
| | | projectName: "", |
| | | bidDocName: "", |
| | | tenderUnit: "", |
| | | bidUnit: "", |
| | | tenderDate: "", |
| | | bidDate: "", |
| | | bidDocFee: "", |
| | | bidDocFeeStatus: "", |
| | | bidDocFeeStatusChangeDate: "", |
| | | bidDeposit: "", |
| | | bidResult: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | projectId: [{ required: true, message: "è¯·éæ©æå±é¡¹ç®", trigger: "change" }], |
| | | bidDocName: [{ required: true, message: "请è¾å
¥æ 书åç§°", trigger: "blur" }], |
| | | tenderUnit: [{ required: true, message: "请è¾å
¥ææ åä½", trigger: "blur" }], |
| | | bidUnit: [{ required: true, message: "请è¾å
¥ææ åä½", trigger: "blur" }], |
| | | tenderDate: [{ required: true, message: "è¯·éæ©ææ æ¥æ", trigger: "change" }], |
| | | bidDate: [{ required: true, message: "è¯·éæ©ææ æ¥æ", trigger: "change" }], |
| | | bidDocFeeStatus: [ |
| | | { required: true, message: "è¯·éæ©æ ä¹¦æ¬¾ç¶æ", trigger: "change" }, |
| | | ], |
| | | bidResult: [{ required: true, message: "è¯·éæ©æææ ç»æ", trigger: "change" }], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => |
| | | biddingId.value ? "ç¼è¾æææ " : "æ°å¢æææ " |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const numOrNull = value => |
| | | value === "" || value === null || value === undefined |
| | | ? null |
| | | : Number.isNaN(Number(value)) |
| | | ? null |
| | | : Number(value); |
| | | |
| | | const onProjectChange = (value, item) => { |
| | | form.value.projectName = pickProjectName(item); |
| | | }; |
| | | |
| | | // æ ä¹¦æ¬¾ç¶æåæ´ä¸æ¥æä¸ºç©ºæ¶èªå¨å¡«å½å¤© |
| | | const onFeeStatusChange = () => { |
| | | if (form.value.bidDocFeeStatus && !form.value.bidDocFeeStatusChangeDate) { |
| | | form.value.bidDocFeeStatusChangeDate = formatDateToYMD(Date.now()); |
| | | } |
| | | }; |
| | | |
| | | const loadOptions = async () => { |
| | | try { |
| | | projectOptions.value = await loadProjectOptions(); |
| | | } catch (error) { |
| | | projectOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "biddingDetail", |
| | | id: biddingId.value, |
| | | fetcher: getBiddingDetail, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "æªæ¾å°æææ ä¿¡æ¯", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | form.value = { |
| | | id: data.id, |
| | | projectId: data.projectId ?? "", |
| | | projectName: data.projectName || "", |
| | | bidDocName: data.bidDocName || "", |
| | | tenderUnit: data.tenderUnit || "", |
| | | bidUnit: data.bidUnit || "", |
| | | tenderDate: (data.tenderDate || "").slice(0, 10), |
| | | bidDate: (data.bidDate || "").slice(0, 10), |
| | | bidDocFee: data.bidDocFee ?? "", |
| | | bidDocFeeStatus: data.bidDocFeeStatus || "", |
| | | bidDocFeeStatusChangeDate: ( |
| | | data.bidDocFeeStatusChangeDate || "" |
| | | ).slice(0, 10), |
| | | bidDeposit: data.bidDeposit ?? "", |
| | | bidResult: data.bidResult || "", |
| | | }; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | const payload = { |
| | | id: form.value.id, |
| | | projectId: form.value.projectId, |
| | | projectName: form.value.projectName, |
| | | bidDocName: form.value.bidDocName, |
| | | tenderUnit: form.value.tenderUnit, |
| | | bidUnit: form.value.bidUnit, |
| | | tenderDate: form.value.tenderDate, |
| | | bidDate: form.value.bidDate, |
| | | bidDocFee: numOrNull(form.value.bidDocFee), |
| | | bidDocFeeStatus: form.value.bidDocFeeStatus, |
| | | bidDocFeeStatusChangeDate: form.value.bidDocFeeStatusChangeDate || null, |
| | | bidDeposit: numOrNull(form.value.bidDeposit), |
| | | bidResult: form.value.bidResult, |
| | | }; |
| | | |
| | | loading.value = true; |
| | | addOrUpdateBidding(payload) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 1000); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | loadOptions(); |
| | | if (options?.id) { |
| | | biddingId.value = options.id; |
| | | form.value.id = options.id; |
| | | loadDetail(); |
| | | } else { |
| | | form.value.tenderDate = formatDateToYMD(Date.now()); |
| | | form.value.bidDate = formatDateToYMD(Date.now()); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.projectName" |
| | | placeholder="请è¾å
¥é¡¹ç®åç§°æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="biddingList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in biddingList" |
| | | :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.bidDocName || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="item.bidResult || 'æªå¡«ç»æ'" |
| | | :type="resultTagType(item.bidResult)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æå±é¡¹ç®</text> |
| | | <text class="detail-value">{{ item.projectName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ åä½</text> |
| | | <text class="detail-value">{{ item.tenderUnit || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ åä½</text> |
| | | <text class="detail-value">{{ item.bidUnit || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ æ¥æ</text> |
| | | <text class="detail-value">{{ item.tenderDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ æ¥æ</text> |
| | | <text class="detail-value">{{ item.bidDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ 书款</text> |
| | | <text class="detail-value">{{ formatAmount(item.bidDocFee) }}ï¼{{ item.bidDocFeeStatus || "-" }}ï¼</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ææ ä¿è¯é</text> |
| | | <text class="detail-value">{{ formatAmount(item.bidDeposit) }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | <view v-if="canStartContract(item)" |
| | | class="contract-entry" |
| | | @click="startContract(item)"> |
| | | <up-icon name="plus-circle" |
| | | size="16" |
| | | color="#2979ff"></up-icon> |
| | | <text class="contract-entry-text">åèµ·ååå½å
¥</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ æææ æ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="æææ çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | biddingListPage, |
| | | delBidding, |
| | | } from "@/api/salesManagement/biddingManagement"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { setPrefillProject } from "@/utils/prefill"; |
| | | |
| | | // éæ±ä¸.6ï¼ç»æå½ä¸è¿ä¸ç§æ¶å¯ç´æ¥åèµ·éå®ååå½å
¥ |
| | | const CONTRACT_TRIGGER_RESULTS = [ |
| | | "䏿 å¾
åºéç¥ä¹¦", |
| | | "å·²åºä¸æ éç¥ä¹¦", |
| | | "å·²ç¾åå", |
| | | ]; |
| | | |
| | | const BID_RESULT_OPTIONS = [ |
| | | { name: "åºæ ", value: "åºæ " }, |
| | | { name: "æµæ ", value: "æµæ " }, |
| | | { name: "æªä¸æ ", value: "æªä¸æ " }, |
| | | { name: "䏿 å¾
åºéç¥ä¹¦", value: "䏿 å¾
åºéç¥ä¹¦" }, |
| | | { name: "å·²åºä¸æ éç¥ä¹¦", value: "å·²åºä¸æ éç¥ä¹¦" }, |
| | | { name: "å·²ç¾åå", value: "å·²ç¾åå" }, |
| | | ]; |
| | | |
| | | const biddingList = ref([]); |
| | | const showFilter = ref(false); |
| | | |
| | | const query = reactive({ |
| | | projectName: "", |
| | | tenderUnit: "", |
| | | bidResult: "", |
| | | bidDateRange: [], |
| | | }); |
| | | |
| | | const filterFields = [ |
| | | { |
| | | prop: "projectName", |
| | | label: "项ç®åç§°", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥é¡¹ç®åç§°", |
| | | }, |
| | | { |
| | | prop: "tenderUnit", |
| | | label: "ææ åä½", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥ææ åä½", |
| | | }, |
| | | { |
| | | prop: "bidResult", |
| | | label: "æææ ç»æ", |
| | | type: "select", |
| | | options: BID_RESULT_OPTIONS, |
| | | }, |
| | | { prop: "bidDateRange", label: "ææ æ¥æ", type: "daterange" }, |
| | | ]; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("biddingDetail"); |
| | | uni.navigateTo({ url: "/pages/sales/biddingManagement/edit" }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("biddingDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/biddingManagement/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("biddingDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/biddingManagement/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const canStartContract = item => |
| | | CONTRACT_TRIGGER_RESULTS.includes(item?.bidResult); |
| | | |
| | | // éæ±ä¸.6ï¼ä¸æ åç´æ¥çæä¸æ¡éå®å°è´¦ï¼å¸¦é¡¹ç®/客æ·ä¿¡æ¯è·³æ°å¢é¡µ |
| | | const startContract = item => { |
| | | setPrefillProject({ |
| | | projectId: item.projectId, |
| | | projectName: item.projectName, |
| | | }); |
| | | uni.setStorageSync("operationType", "add"); |
| | | uni.removeStorageSync("editData"); |
| | | uni.navigateTo({ url: "/pages/sales/salesAccount/detail" }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const resultTagType = result => { |
| | | if (result === "å·²ç¾åå") return "success"; |
| | | if (result === "åºæ " || result === "æµæ " || result === "æªä¸æ ") { |
| | | return "error"; |
| | | } |
| | | if (result) return "warning"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const getList = () => { |
| | | const [bidDateStart, bidDateEnd] = Array.isArray(query.bidDateRange) |
| | | ? query.bidDateRange |
| | | : []; |
| | | biddingListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | projectName: query.projectName || undefined, |
| | | tenderUnit: query.tenderUnit || undefined, |
| | | bidResult: query.bidResult || undefined, |
| | | bidDateStart: bidDateStart || undefined, |
| | | bidDateEnd: bidDateEnd || undefined, |
| | | }) |
| | | .then(res => { |
| | | biddingList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥æææ è®°å½åï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delBidding(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .contract-entry { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | margin: 0 12px 12px; |
| | | padding: 9px 0; |
| | | border-radius: 8px; |
| | | border: 1px dashed #2979ff; |
| | | background: #f4f8ff; |
| | | } |
| | | |
| | | .contract-entry-text { |
| | | font-size: 13px; |
| | | color: #2979ff; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="contract-detail"> |
| | | <PageHeader title="åå详æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ detail.contractNo || "-" }}</text> |
| | | <up-tag :text="detail.archiveStatus || 'æªå¡«ç¶æ'" |
| | | :type="archiveTagType(detail.archiveStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <u-cell-group title="åå主档" |
| | | class="detail-section"> |
| | | <u-cell title="ååç¼å·" |
| | | :value="detail.contractNo || '-'" /> |
| | | <u-cell title="ç¾è®¢æ¶é´" |
| | | :value="detail.signDate || '-'" /> |
| | | <u-cell title="ç¾çº¦åä½ç®ç§°" |
| | | :value="detail.signingUnit || '-'" /> |
| | | <u-cell title="ç¾çº¦å¨æ" |
| | | :value="detail.contractPeriod || '-'" /> |
| | | <u-cell title="åæ¡£ç¶æ" |
| | | :value="detail.archiveStatus || '-'" /> |
| | | <u-cell title="åæ¡£æ
åµè¯´æ" |
| | | :value="detail.archiveRemark || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="产åä¿¡æ¯" |
| | | class="detail-section"> |
| | | <view v-if="productList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ äº§åæç»</text> |
| | | </view> |
| | | <view v-else |
| | | class="product-list"> |
| | | <view v-for="(row, index) in productList" |
| | | :key="index" |
| | | class="product-card"> |
| | | <view class="product-head">产å {{ index + 1 }}</view> |
| | | <view class="info-item"> |
| | | <text class="info-label">产å大类</text> |
| | | <text class="info-value">{{ row.productCategory || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">è§æ ¼åå·</text> |
| | | <text class="info-value">{{ row.specificationModel || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">åå计éåä½</text> |
| | | <text class="info-value">{{ row.contractUnit || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å«ç¨åä»·</text> |
| | | <text class="info-value">{{ formatAmount(row.taxInclusiveUnitPrice) }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">约å®/æä¼°ç¨é</text> |
| | | <text class="info-value">{{ row.agreedQuantity ?? "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å·²åè´§æ°é</text> |
| | | <text class="info-value">{{ row.shippedQuantity ?? "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">产å¼</text> |
| | | <text class="info-value highlight">{{ formatAmount(row.outputValue) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="履约ä¸è´¨ä¿" |
| | | class="detail-section"> |
| | | <u-cell title="履约ä¿è¯é" |
| | | :value="formatAmount(detail.performanceBond)" /> |
| | | <u-cell title="è´¨ä¿éæ
åµ" |
| | | :value="formatAmount(detail.warrantyDeposit)" /> |
| | | <u-cell title="产å¼" |
| | | :value="formatAmount(detail.outputValue)" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="忬¾ä¸ç»ç®" |
| | | class="detail-section"> |
| | | <u-cell title="已忬¾éé¢" |
| | | :value="formatAmount(detail.receivedAmount)" /> |
| | | <u-cell title="ç»ç®æ¬¡æ°" |
| | | :value="detail.settlementCount ?? '-'" /> |
| | | <u-cell title="åºæ¶éé¢" |
| | | :value="formatAmount(detail.receivableAmount)" /> |
| | | <u-cell title="åºä»æ¯ä¾" |
| | | :value="detail.payableRatio || '-'" /> |
| | | <u-cell title="ååèç¹åºæ¶" |
| | | :value="formatAmount(detail.nodeReceivable)" /> |
| | | <u-cell title="åå约å®ä»æ¬¾èç¹" |
| | | :value="detail.paymentNodes || '-'" /> |
| | | <u-cell title="ç»ç®è®°å½" |
| | | :value="detail.settlementRecord || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å¼ç¥¨ä¸è¿çº¦" |
| | | class="detail-section"> |
| | | <u-cell title="å¼ç¥¨è¦æ±" |
| | | :value="detail.invoiceRequirement || '-'" /> |
| | | <u-cell title="æè¿ä¸æ¬¡å¼ç¥¨æ¶é´" |
| | | :value="detail.lastInvoiceDate || '-'" /> |
| | | <u-cell title="å·²å¼ç¥¨éé¢" |
| | | :value="formatAmount(detail.invoicedAmount)" /> |
| | | <u-cell title="è¿çº¦é约å®" |
| | | :value="detail.penaltyAgreement || '-'" /> |
| | | <u-cell title="æ
åµè¯´æ" |
| | | :value="detail.remark || '-'" /> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { getContractDetail } from "@/api/salesManagement/contractManagement"; |
| | | |
| | | const loading = ref(true); |
| | | const contractId = ref(""); |
| | | const detail = ref({}); |
| | | |
| | | const productList = computed(() => |
| | | Array.isArray(detail.value.productList) ? detail.value.productList : [] |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("contractDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/contractManagement/edit?id=${contractId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const archiveTagType = status => { |
| | | if (status === "已忡£") return "success"; |
| | | if (status === "å·²ç¨ç« æªå¯å") return "warning"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const data = await loadDetailData({ |
| | | cacheKey: "contractDetail", |
| | | id: contractId.value, |
| | | fetcher: getContractDetail, |
| | | }); |
| | | detail.value = data || {}; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·ååå详æ
失败", icon: "none" }); |
| | | detail.value = {}; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | contractId.value = options?.id || ""; |
| | | if (!contractId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .contract-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .empty-text { |
| | | padding: 16px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .product-list { |
| | | padding: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .product-card { |
| | | background: #f9fafc; |
| | | border-radius: 10px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .product-head { |
| | | padding: 12px 16px; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | border-bottom: 1px solid #eef2f7; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | padding: 10px 16px; |
| | | border-bottom: 1px solid #f8f8f8; |
| | | } |
| | | |
| | | .info-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .info-label { |
| | | width: 120px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .info-value { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | text-align: right; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .highlight { |
| | | color: #2979ff; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="åå主档" |
| | | class="form-section"> |
| | | <up-form-item label="ååç¼å·" |
| | | prop="contractNo" |
| | | required> |
| | | <up-input v-model="form.contractNo" |
| | | placeholder="请è¾å
¥ååç¼å·" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¾è®¢æ¶é´" |
| | | prop="signDate" |
| | | required> |
| | | <FormPicker v-model="form.signDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ç¾è®¢æ¶é´" /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¾çº¦åä½ç®ç§°" |
| | | prop="signingUnit" |
| | | required> |
| | | <up-input v-model="form.signingUnit" |
| | | placeholder="请è¾å
¥ç¾çº¦åä½ç®ç§°" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¾çº¦å¨æ" |
| | | prop="contractPeriod"> |
| | | <up-input v-model="form.contractPeriod" |
| | | placeholder="请è¾å
¥ç¾çº¦å¨æ" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="åæ¡£ç¶æ" |
| | | prop="archiveStatus" |
| | | required> |
| | | <FormPicker v-model="form.archiveStatus" |
| | | type="select" |
| | | title="鿩忡£ç¶æ" |
| | | :options="ARCHIVE_STATUS_OPTIONS" |
| | | placeholder="è¯·éæ©åæ¡£ç¶æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="åæ¡£æ
åµè¯´æ" |
| | | prop="archiveRemark"> |
| | | <up-input v-model="form.archiveRemark" |
| | | placeholder="请è¾å
¥åæ¡£æ
åµè¯´æ" |
| | | clearable /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="产åä¿¡æ¯" |
| | | class="form-section"> |
| | | <view class="section-tools"> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="æ·»å 产å" |
| | | @click="addProduct" /> |
| | | </view> |
| | | <view v-if="productList.length === 0" |
| | | class="empty-text"> |
| | | <text>ææ äº§åï¼è¯·å
æ·»å 产å</text> |
| | | </view> |
| | | <view v-else |
| | | class="product-list"> |
| | | <view v-for="(row, index) in productList" |
| | | :key="row.uid" |
| | | class="product-card"> |
| | | <view class="product-header"> |
| | | <text class="product-title">产å {{ index + 1 }}</text> |
| | | <up-icon name="trash" |
| | | color="#ee0a24" |
| | | size="18" |
| | | @click="removeProduct(index)"></up-icon> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="product-body"> |
| | | <up-form-item label="产å大类"> |
| | | <up-input v-model="row.productCategory" |
| | | placeholder="è¯·éæ©äº§å大类" |
| | | readonly |
| | | @click="openCategoryPicker(index)" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="openCategoryPicker(index)"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="è§æ ¼åå·"> |
| | | <up-input v-model="row.specificationModel" |
| | | placeholder="è¯·éæ©è§æ ¼åå·" |
| | | readonly |
| | | @click="openModelPicker(index)" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="openModelPicker(index)"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="åå计éåä½"> |
| | | <up-input v-model="row.contractUnit" |
| | | placeholder="请è¾å
¥è®¡éåä½" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="å«ç¨åä»·"> |
| | | <up-input v-model="row.taxInclusiveUnitPrice" |
| | | type="number" |
| | | placeholder="请è¾å
¥å«ç¨åä»·" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="约å®/æä¼°ç¨é"> |
| | | <up-input v-model="row.agreedQuantity" |
| | | type="number" |
| | | placeholder="请è¾å
¥çº¦å®ç¨é" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="å·²åè´§æ°é"> |
| | | <up-input v-model="row.shippedQuantity" |
| | | type="number" |
| | | placeholder="请è¾å
¥å·²åè´§æ°é" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="产å¼"> |
| | | <up-input :model-value="formatAmount(productOutputValue(row))" |
| | | disabled |
| | | placeholder="èªå¨è®¡ç®" /> |
| | | </up-form-item> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="履约ä¸è´¨ä¿" |
| | | class="form-section"> |
| | | <up-form-item label="履约ä¿è¯é"> |
| | | <up-input v-model="form.performanceBond" |
| | | type="number" |
| | | placeholder="请è¾å
¥å±¥çº¦ä¿è¯é" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="è´¨ä¿éæ
åµ"> |
| | | <up-input v-model="form.warrantyDeposit" |
| | | type="number" |
| | | placeholder="请è¾å
¥è´¨ä¿é" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="产å¼"> |
| | | <up-input :model-value="formatAmount(computedOutputValue)" |
| | | disabled |
| | | placeholder="èªå¨æ±æ»" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="忬¾ä¸ç»ç®" |
| | | class="form-section"> |
| | | <up-form-item label="已忬¾éé¢"> |
| | | <up-input v-model="form.receivedAmount" |
| | | type="number" |
| | | placeholder="请è¾å
¥å·²å款éé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç»ç®æ¬¡æ°"> |
| | | <up-input v-model="form.settlementCount" |
| | | type="number" |
| | | placeholder="请è¾å
¥ç»ç®æ¬¡æ°" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="åºæ¶éé¢"> |
| | | <up-input v-model="form.receivableAmount" |
| | | type="number" |
| | | placeholder="请è¾å
¥åºæ¶éé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="åºä»æ¯ä¾"> |
| | | <up-input v-model="form.payableRatio" |
| | | placeholder="ä¾å¦ï¼30% æ 0.3" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ååèç¹åºæ¶"> |
| | | <up-input v-model="form.nodeReceivable" |
| | | type="number" |
| | | placeholder="请è¾å
¥ååèç¹åºæ¶" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="约å®ä»æ¬¾èç¹"> |
| | | <up-input v-model="form.paymentNodes" |
| | | placeholder="请è¾å
¥åå约å®ä»æ¬¾èç¹" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç»ç®è®°å½"> |
| | | <up-textarea v-model="form.settlementRecord" |
| | | placeholder="请è¾å
¥ç»ç®è®°å½" |
| | | auto-height /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å¼ç¥¨ä¸è¿çº¦" |
| | | class="form-section"> |
| | | <up-form-item label="å¼ç¥¨è¦æ±"> |
| | | <up-input v-model="form.invoiceRequirement" |
| | | placeholder="请è¾å
¥å¼ç¥¨è¦æ±" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æè¿å¼ç¥¨æ¶é´"> |
| | | <FormPicker v-model="form.lastInvoiceDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©æè¿ä¸æ¬¡å¼ç¥¨æ¶é´" /> |
| | | </up-form-item> |
| | | <up-form-item label="å·²å¼ç¥¨éé¢"> |
| | | <up-input v-model="form.invoicedAmount" |
| | | type="number" |
| | | placeholder="请è¾å
¥å·²å¼ç¥¨éé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="è¿çº¦é约å®"> |
| | | <up-textarea v-model="form.penaltyAgreement" |
| | | placeholder="请è¾å
¥è¿çº¦é约å®" |
| | | auto-height /> |
| | | </up-form-item> |
| | | <up-form-item label="æ
åµè¯´æ"> |
| | | <up-textarea v-model="form.remark" |
| | | placeholder="请è¾å
¥æ
åµè¯´æ" |
| | | auto-height /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | |
| | | <up-action-sheet :show="showCategorySheet" |
| | | title="éæ©äº§å大类" |
| | | :actions="categoryActions" |
| | | @select="onSelectCategory" |
| | | @close="showCategorySheet = false" /> |
| | | <up-action-sheet :show="showModelSheet" |
| | | title="éæ©è§æ ¼åå·" |
| | | :actions="modelActions" |
| | | @select="onSelectModel" |
| | | @close="showModelSheet = false" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import { modelList, productTreeList } from "@/api/basicData/product"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { |
| | | addOrUpdateContract, |
| | | getContractDetail, |
| | | } from "@/api/salesManagement/contractManagement"; |
| | | |
| | | const ARCHIVE_STATUS_OPTIONS = [ |
| | | { name: "已忡£", value: "已忡£" }, |
| | | { name: "å·²ç¨ç« æªå¯å", value: "å·²ç¨ç« æªå¯å" }, |
| | | { name: "æªç¾çº¦", value: "æªç¾çº¦" }, |
| | | ]; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const contractId = ref(""); |
| | | const productList = ref([]); |
| | | const categoryOptions = ref([]); |
| | | const categoryActions = ref([]); |
| | | const modelActions = ref([]); |
| | | const showCategorySheet = ref(false); |
| | | const showModelSheet = ref(false); |
| | | const currentProductIndex = ref(-1); |
| | | |
| | | let uidSeed = 1; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | contractNo: "", |
| | | signDate: "", |
| | | signingUnit: "", |
| | | contractPeriod: "", |
| | | archiveStatus: "", |
| | | archiveRemark: "", |
| | | performanceBond: undefined, |
| | | warrantyDeposit: undefined, |
| | | outputValue: undefined, |
| | | receivedAmount: undefined, |
| | | settlementCount: undefined, |
| | | receivableAmount: undefined, |
| | | payableRatio: "", |
| | | nodeReceivable: undefined, |
| | | paymentNodes: "", |
| | | settlementRecord: "", |
| | | invoiceRequirement: "", |
| | | lastInvoiceDate: "", |
| | | invoicedAmount: undefined, |
| | | penaltyAgreement: "", |
| | | remark: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | contractNo: [{ required: true, message: "请è¾å
¥ååç¼å·", trigger: "blur" }], |
| | | signDate: [{ required: true, message: "è¯·éæ©ç¾è®¢æ¶é´", trigger: "change" }], |
| | | signingUnit: [ |
| | | { required: true, message: "请è¾å
¥ç¾çº¦åä½ç®ç§°", trigger: "blur" }, |
| | | ], |
| | | archiveStatus: [ |
| | | { required: true, message: "è¯·éæ©åæ¡£ç¶æ", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => (contractId.value ? "ç¼è¾åå" : "æ°å¢åå")); |
| | | |
| | | const productOutputValue = row => |
| | | Number( |
| | | ( |
| | | Number(row?.taxInclusiveUnitPrice || 0) * |
| | | Number(row?.shippedQuantity || 0) |
| | | ).toFixed(2) |
| | | ); |
| | | |
| | | const computedOutputValue = computed(() => |
| | | Number( |
| | | productList.value |
| | | .reduce((sum, row) => sum + productOutputValue(row), 0) |
| | | .toFixed(2) |
| | | ) |
| | | ); |
| | | |
| | | const formatAmount = amount => `Â¥${Number(amount || 0).toFixed(2)}`; |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const flattenProductTree = nodes => { |
| | | const result = []; |
| | | const walk = list => { |
| | | (list || []).forEach(item => { |
| | | if (item.children && item.children.length) { |
| | | walk(item.children); |
| | | } else { |
| | | result.push({ |
| | | label: item.label || item.productName || "", |
| | | value: item.id || item.value, |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | walk(nodes); |
| | | return result; |
| | | }; |
| | | |
| | | const findNodeIdByLabel = (nodes, label) => { |
| | | if (!label) return null; |
| | | for (let i = 0; i < nodes.length; i++) { |
| | | const node = nodes[i]; |
| | | if (node.label === label) return node.value; |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeIdByLabel(node.children, label); |
| | | if (found !== null && found !== undefined) return found; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const createEmptyProduct = () => ({ |
| | | uid: `p_${uidSeed++}`, |
| | | categoryId: "", |
| | | productCategory: "", |
| | | productModelId: "", |
| | | specificationModel: "", |
| | | contractUnit: "", |
| | | taxInclusiveUnitPrice: undefined, |
| | | agreedQuantity: undefined, |
| | | shippedQuantity: undefined, |
| | | modelOptions: [], |
| | | }); |
| | | |
| | | const addProduct = () => productList.value.push(createEmptyProduct()); |
| | | |
| | | const removeProduct = index => { |
| | | productList.value.splice(index, 1); |
| | | }; |
| | | |
| | | const fetchBaseOptions = async () => { |
| | | try { |
| | | const res = await productTreeList(); |
| | | const nodes = Array.isArray(res) ? res : res?.data || []; |
| | | categoryOptions.value = flattenProductTree(nodes); |
| | | } catch (error) { |
| | | categoryOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const fetchModelOptions = async (categoryId, row) => { |
| | | try { |
| | | const res = await modelList({ id: categoryId }); |
| | | const rows = res?.data?.records || res?.data || res?.records || res || []; |
| | | row.modelOptions = Array.isArray(rows) ? rows : []; |
| | | } catch (error) { |
| | | row.modelOptions = []; |
| | | } |
| | | }; |
| | | |
| | | const openCategoryPicker = index => { |
| | | currentProductIndex.value = index; |
| | | categoryActions.value = categoryOptions.value.map(item => ({ |
| | | name: item.label, |
| | | value: item.value, |
| | | label: item.label, |
| | | })); |
| | | showCategorySheet.value = true; |
| | | }; |
| | | |
| | | const openModelPicker = index => { |
| | | currentProductIndex.value = index; |
| | | const row = productList.value[index]; |
| | | if (!row?.categoryId) { |
| | | uni.showToast({ title: "请å
éæ©äº§å大类", icon: "none" }); |
| | | return; |
| | | } |
| | | modelActions.value = (row.modelOptions || []).map(item => ({ |
| | | name: item.model || item.specification, |
| | | value: item.id, |
| | | unit: item.unit, |
| | | })); |
| | | if (!modelActions.value.length) { |
| | | uni.showToast({ title: "ææ è§æ ¼åå·", icon: "none" }); |
| | | return; |
| | | } |
| | | showModelSheet.value = true; |
| | | }; |
| | | |
| | | const onSelectCategory = action => { |
| | | const row = productList.value[currentProductIndex.value]; |
| | | showCategorySheet.value = false; |
| | | if (!row) return; |
| | | row.categoryId = action.value; |
| | | row.productCategory = action.label; |
| | | row.productModelId = ""; |
| | | row.specificationModel = ""; |
| | | row.contractUnit = ""; |
| | | row.modelOptions = []; |
| | | fetchModelOptions(action.value, row); |
| | | }; |
| | | |
| | | const onSelectModel = action => { |
| | | const row = productList.value[currentProductIndex.value]; |
| | | showModelSheet.value = false; |
| | | if (!row) return; |
| | | row.productModelId = action.value; |
| | | row.specificationModel = action.name; |
| | | row.contractUnit = action.unit || row.contractUnit; |
| | | }; |
| | | |
| | | const normalizeProductRows = async rows => { |
| | | const normalized = await Promise.all( |
| | | (Array.isArray(rows) ? rows : []).map(async item => { |
| | | const row = { |
| | | uid: `p_${uidSeed++}`, |
| | | categoryId: |
| | | item.categoryId || |
| | | findNodeIdByLabel(categoryOptions.value, item.productCategory) || |
| | | "", |
| | | productCategory: item.productCategory || "", |
| | | productModelId: item.productModelId || "", |
| | | specificationModel: item.specificationModel || "", |
| | | contractUnit: item.contractUnit || "", |
| | | taxInclusiveUnitPrice: item.taxInclusiveUnitPrice, |
| | | agreedQuantity: item.agreedQuantity, |
| | | shippedQuantity: item.shippedQuantity, |
| | | modelOptions: [], |
| | | }; |
| | | if (row.categoryId) { |
| | | await fetchModelOptions(row.categoryId, row); |
| | | // ä»
åäºåå·åç§°æ¶ï¼ä»åå·åè¡¨åæ¥ idï¼ä¿è¯éæ©å¨è½åæ¾ |
| | | if (!row.productModelId && row.specificationModel) { |
| | | const matched = row.modelOptions.find( |
| | | m => (m.model || m.specification) === row.specificationModel |
| | | ); |
| | | if (matched) row.productModelId = matched.id; |
| | | } |
| | | } |
| | | return row; |
| | | }) |
| | | ); |
| | | productList.value = normalized; |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | if (!contractId.value) return; |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "contractDetail", |
| | | id: contractId.value, |
| | | fetcher: getContractDetail, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "è·ååå详æ
失败", icon: "none" }); |
| | | return; |
| | | } |
| | | const { productList: rows, ...rest } = data; |
| | | form.value = { ...form.value, ...rest, id: data.id }; |
| | | await normalizeProductRows(rows); |
| | | }; |
| | | |
| | | const buildPayload = () => ({ |
| | | ...form.value, |
| | | outputValue: computedOutputValue.value, |
| | | productList: productList.value.map(row => ({ |
| | | productCategory: row.productCategory, |
| | | specificationModel: row.specificationModel, |
| | | productModelId: row.productModelId, |
| | | contractUnit: row.contractUnit, |
| | | taxInclusiveUnitPrice: Number(row.taxInclusiveUnitPrice || 0), |
| | | agreedQuantity: Number(row.agreedQuantity || 0), |
| | | shippedQuantity: Number(row.shippedQuantity || 0), |
| | | outputValue: productOutputValue(row), |
| | | })), |
| | | }); |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | loading.value = true; |
| | | addOrUpdateContract(buildPayload()) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | contractId.value = options.id; |
| | | form.value.id = options.id; |
| | | } |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | await fetchBaseOptions(); |
| | | if (contractId.value) await loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .section-tools { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .product-list { |
| | | padding: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .product-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 0 12px 12px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .product-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .product-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .empty-text { |
| | | padding: 16px 12px; |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.contractNo" |
| | | placeholder="请è¾å
¥ååç¼å·æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="contractList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in contractList" |
| | | :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.contractNo || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="item.archiveStatus || 'æªå¡«ç¶æ'" |
| | | :type="archiveTagType(item.archiveStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç¾çº¦æ¥æ</text> |
| | | <text class="detail-value">{{ item.signDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç¾çº¦åä½</text> |
| | | <text class="detail-value">{{ item.signingUnit || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç¾çº¦å¨æ</text> |
| | | <text class="detail-value">{{ item.contractPeriod || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">产å¼</text> |
| | | <text class="detail-value highlight">{{ formatAmount(item.outputValue) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å·²æ¶æ¬¾</text> |
| | | <text class="detail-value">{{ formatAmount(item.receivedAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºæ¶éé¢</text> |
| | | <text class="detail-value">{{ formatAmount(item.receivableAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">履约ä¿è¯é</text> |
| | | <text class="detail-value">{{ formatAmount(item.performanceBond) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è´¨ä¿é</text> |
| | | <text class="detail-value">{{ formatAmount(item.warrantyDeposit) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç»ç®åæ°é</text> |
| | | <text class="detail-value">{{ item.settlementCount ?? "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºæ¶æ¬¾æ¯ä¾</text> |
| | | <text class="detail-value">{{ item.payableRatio || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æè¿å¼ç¥¨æ¥æ</text> |
| | | <text class="detail-value">{{ item.lastInvoiceDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å·²å¼ç¥¨éé¢</text> |
| | | <text class="detail-value">{{ formatAmount(item.invoicedAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row" |
| | | v-if="item.remark"> |
| | | <text class="detail-label">æ
åµè¯´æ</text> |
| | | <text class="detail-value">{{ item.remark }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ ååæ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="ååçé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | contractListPage, |
| | | delContract, |
| | | } from "@/api/salesManagement/contractManagement"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | |
| | | const ARCHIVE_STATUS_OPTIONS = [ |
| | | { name: "已忡£", value: "已忡£" }, |
| | | { name: "å·²ç¨ç« æªå¯å", value: "å·²ç¨ç« æªå¯å" }, |
| | | { name: "æªç¾çº¦", value: "æªç¾çº¦" }, |
| | | ]; |
| | | |
| | | const contractList = ref([]); |
| | | const showFilter = ref(false); |
| | | |
| | | const query = reactive({ |
| | | contractNo: "", |
| | | signingUnit: "", |
| | | archiveStatus: "", |
| | | signDateRange: [], |
| | | }); |
| | | |
| | | const filterFields = [ |
| | | { |
| | | prop: "contractNo", |
| | | label: "ååç¼å·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥ååç¼å·", |
| | | }, |
| | | { |
| | | prop: "signingUnit", |
| | | label: "ç¾çº¦åä½", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥ç¾çº¦åä½", |
| | | }, |
| | | { |
| | | prop: "archiveStatus", |
| | | label: "åæ¡£ç¶æ", |
| | | type: "select", |
| | | options: ARCHIVE_STATUS_OPTIONS, |
| | | }, |
| | | { prop: "signDateRange", label: "ç¾çº¦æ¥æ", type: "daterange" }, |
| | | ]; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("contractDetail"); |
| | | uni.navigateTo({ url: "/pages/sales/contractManagement/edit" }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("contractDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/contractManagement/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("contractDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/contractManagement/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const archiveTagType = status => { |
| | | if (status === "已忡£") return "success"; |
| | | if (status === "å·²ç¨ç« æªå¯å") return "warning"; |
| | | if (status === "æªç¾çº¦") return "info"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const getList = () => { |
| | | const [signDateStart, signDateEnd] = Array.isArray(query.signDateRange) |
| | | ? query.signDateRange |
| | | : []; |
| | | contractListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | contractNo: query.contractNo || undefined, |
| | | signingUnit: query.signingUnit || undefined, |
| | | archiveStatus: query.archiveStatus || undefined, |
| | | signDateStart: signDateStart || undefined, |
| | | signDateEnd: signDateEnd || undefined, |
| | | }) |
| | | .then(res => { |
| | | contractList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥åååï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delContract(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="delivery-detail"> |
| | | <PageHeader title="å货详æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text v-if="!locked" |
| | | class="header-action" |
| | | @click="goEdit">è¡¥å
ä¿¡æ¯</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <view class="hero-left"> |
| | | <text class="hero-title">{{ detail.shippingNo || detail.salesContractNo || "-" }}</text> |
| | | <text class="hero-sub">{{ detail.projectName || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="statusText(detail.status)" |
| | | :type="statusTagType(detail.status)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <view v-if="locked" |
| | | class="lock-banner"> |
| | | <text class="lock-banner-text">å½åç¶æå·²è¿å
¥æ£å¼æµç¨ï¼åè´§ä¿¡æ¯ä¸å¯ä¿®æ¹</text> |
| | | </view> |
| | | |
| | | <u-cell-group title="ååä¸å®¢æ·" |
| | | class="detail-section"> |
| | | <u-cell title="éå®ååå·" |
| | | :value="detail.salesContractNo || '-'" /> |
| | | <u-cell title="åè´§åå·" |
| | | :value="detail.shippingNo || '-'" /> |
| | | <u-cell title="æå±é¡¹ç®" |
| | | :value="detail.projectName || '-'" /> |
| | | <u-cell title="客æ·åç§°" |
| | | :value="detail.customerName || '-'" /> |
| | | <u-cell title="产ååç§°" |
| | | :value="detail.productName || '-'" /> |
| | | <u-cell title="è§æ ¼åå·" |
| | | :value="detail.specificationModel || '-'" /> |
| | | <u-cell title="åè´§æ°é" |
| | | :value="detail.totalQuantity != null ? String(detail.totalQuantity) : '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="åè´§ä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="åè´§ç±»å" |
| | | :value="detail.type || '-'" /> |
| | | <u-cell title="åè´§æ¥æ" |
| | | :value="detail.shippingDate || '-'" /> |
| | | <u-cell title="å货车çå·" |
| | | :value="detail.shippingCarNumber || '-'" /> |
| | | <u-cell title="å¿«éå
¬å¸" |
| | | :value="detail.expressCompany || '-'" /> |
| | | <u-cell title="å¿«éåå·" |
| | | :value="detail.expressNumber || '-'" /> |
| | | <u-cell title="åºåºæ¹æ¬¡" |
| | | :value="detail.outboundBatches || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="åæ®ä¸å½æ¡£" |
| | | class="detail-section"> |
| | | <u-cell title="åè´§åæ®" |
| | | :value="detail.shippingDocument || '-'" /> |
| | | <u-cell title="æ¯å¦å¯¹è´¦" |
| | | :value="yesNo(detail.isReconciled)" /> |
| | | <u-cell v-if="Number(detail.isReconciled) === 1" |
| | | title="对账å彿¡£æ
åµ" |
| | | :value="detail.statementArchivingStatus || '-'" /> |
| | | <u-cell title="æ¯å¦ç»ç®" |
| | | :value="yesNo(detail.isSettled)" /> |
| | | <u-cell v-if="Number(detail.isSettled) === 1" |
| | | title="ç»ç®å彿¡£æ
åµ" |
| | | :value="detail.settlementArchivingStatus || '-'" /> |
| | | <u-cell title="æ¯å¦å·²å¼ç¥¨" |
| | | :value="yesNo(detail.isInvoiced)" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å货产åæç»" |
| | | class="detail-section"> |
| | | <view v-if="products.length === 0" |
| | | class="empty-block"> |
| | | <text>ææ äº§åæç»</text> |
| | | </view> |
| | | <view v-for="(row, index) in products" |
| | | :key="index" |
| | | class="product-row"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">产ååç§°</text> |
| | | <text class="detail-value">{{ row.productName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è§æ ¼åå·</text> |
| | | <text class="detail-value">{{ row.specificationModel || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ¹æ¬¡å·</text> |
| | | <text class="detail-value">{{ row.batchNo || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åè´§æ°é</text> |
| | | <text class="detail-value"> |
| | | {{ row.deliveryQuantity === "" ? "-" : row.deliveryQuantity }}{{ row.unit ? ` ${row.unit}` : "" }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="åè´§å¾ç" |
| | | class="detail-section"> |
| | | <view v-if="files.length === 0" |
| | | class="empty-block"> |
| | | <text>ææ å¾ç</text> |
| | | </view> |
| | | <view v-else |
| | | class="file-list"> |
| | | <view v-for="(file, index) in files" |
| | | :key="index" |
| | | class="file-item" |
| | | @click="previewFile(file)"> |
| | | <up-icon name="photo" |
| | | size="18" |
| | | color="#2979ff"></up-icon> |
| | | <text class="file-name">{{ fileLabel(file, index) }}</text> |
| | | <up-icon name="download" |
| | | size="16" |
| | | color="#909399"></up-icon> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éè´§å" |
| | | class="detail-section"> |
| | | <view v-if="returnNos.length === 0" |
| | | class="empty-block"> |
| | | <text>ææ éè´§å</text> |
| | | </view> |
| | | <view v-for="(no, index) in returnNos" |
| | | :key="index" |
| | | class="file-item"> |
| | | <up-icon name="arrow-return-left" |
| | | size="18" |
| | | color="#ff9900"></up-icon> |
| | | <text class="file-name">{{ no }}</text> |
| | | </view> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { |
| | | resolveDeliveryFiles, |
| | | resolveDeliveryProducts, |
| | | resolveReturnNos, |
| | | } from "@/utils/delivery"; |
| | | import { getDeliveryDetail } from "@/api/salesManagement/deliveryLedger"; |
| | | |
| | | // è¿ä¸¤ç§ç¶æå·²è¿å
¥æ£å¼æµç¨ï¼ä¸å
è®¸åæ¹åè´§ä¿¡æ¯ |
| | | const LOCKED_STATUS = ["å®¡æ ¸éè¿", "å·²åè´§"]; |
| | | |
| | | const STATUS_TEXT = { |
| | | 0: "å¾
å®¡æ ¸", |
| | | 1: "å®¡æ ¸ä¸", |
| | | 2: "å®¡æ ¸æç»", |
| | | 3: "å®¡æ ¸éè¿", |
| | | }; |
| | | |
| | | const loading = ref(true); |
| | | const deliveryId = ref(""); |
| | | const detail = ref({}); |
| | | |
| | | const products = computed(() => resolveDeliveryProducts(detail.value)); |
| | | const files = computed(() => resolveDeliveryFiles(detail.value)); |
| | | const returnNos = computed(() => resolveReturnNos(detail.value.returnNoList)); |
| | | const locked = computed(() => LOCKED_STATUS.includes(detail.value?.status)); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | if (locked.value) { |
| | | uni.showToast({ title: "è¯¥ç¶æçæ°æ®ä¸å¯ç¼è¾åè´§ä¿¡æ¯", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.setStorageSync("deliveryDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/deliveryLedger/edit?id=${deliveryId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const statusText = status => { |
| | | if (status === null || status === undefined || status === "") return "-"; |
| | | return STATUS_TEXT[status] || String(status); |
| | | }; |
| | | |
| | | const statusTagType = status => { |
| | | if (status === "å·²åè´§" || status === 3 || status === "3") return "success"; |
| | | if (status === 1 || status === "1") return "warning"; |
| | | if (status === 2 || status === "2") return "error"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const yesNo = value => { |
| | | if (value === null || value === undefined || value === "") return "-"; |
| | | return Number(value) === 1 ? "æ¯" : "å¦"; |
| | | }; |
| | | |
| | | const fileLabel = (file, index) => |
| | | file.name || file.originalFilename || `å¾ç${index + 1}`; |
| | | |
| | | // request.ts æªéä¼ responseTypeï¼blob ä¸è½½ä¸å¯ç¨ï¼åªè½ç¨ downloadFile + openDocument |
| | | const previewFile = file => { |
| | | const url = file.url || file.previewURL; |
| | | if (!url) return; |
| | | uni.showLoading({ title: "æå¼ä¸...", mask: true }); |
| | | uni.downloadFile({ |
| | | url, |
| | | success: res => { |
| | | if (res.statusCode !== 200) { |
| | | uni.showToast({ title: "æä»¶ä¸è½½å¤±è´¥", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.openDocument({ |
| | | filePath: res.tempFilePath, |
| | | showMenu: true, |
| | | fail: () => uni.showToast({ title: "æä¸æ¯æé¢è§è¯¥æä»¶", icon: "none" }), |
| | | }); |
| | | }, |
| | | fail: () => uni.showToast({ title: "æä»¶ä¸è½½å¤±è´¥", icon: "none" }), |
| | | complete: () => uni.hideLoading(), |
| | | }); |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const data = await loadDetailData({ |
| | | cacheKey: "deliveryDetail", |
| | | id: deliveryId.value, |
| | | fetcher: getDeliveryDetail, |
| | | }); |
| | | detail.value = data || {}; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·åå货详æ
失败", icon: "none" }); |
| | | detail.value = {}; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | deliveryId.value = options?.id || ""; |
| | | if (!deliveryId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .delivery-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | max-width: 70%; |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .hero-sub { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .lock-banner { |
| | | margin-top: 12px; |
| | | padding: 10px 14px; |
| | | border-radius: 10px; |
| | | background: #f4f4f5; |
| | | } |
| | | |
| | | .lock-banner-text { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .empty-block { |
| | | padding: 16px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .product-row { |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .product-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .detail-row { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .detail-label { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .detail-value { |
| | | font-size: 13px; |
| | | color: #333; |
| | | max-width: 70%; |
| | | text-align: right; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .file-list { |
| | | padding: 6px 16px 12px; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 10px 16px; |
| | | } |
| | | |
| | | .file-name { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #333; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader title="è¡¥å
åè´§ä¿¡æ¯" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="åè´§ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="åè´§ç±»å" |
| | | prop="type" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.type" |
| | | type="select" |
| | | title="éæ©åè´§ç±»å" |
| | | :options="typeOptions" |
| | | placeholder="è¯·éæ©åè´§ç±»å" |
| | | @change="onTypeChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="åè´§æ¥æ" |
| | | prop="shippingDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.shippingDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©åè´§æ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.type === '货车'" |
| | | label="å货车çå·" |
| | | prop="shippingCarNumber" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.shippingCarNumber" |
| | | placeholder="请è¾å
¥å货车çå·" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.type === 'å¿«é'" |
| | | label="å¿«éå
¬å¸" |
| | | prop="expressCompany" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.expressCompany" |
| | | placeholder="请è¾å
¥å¿«éå
¬å¸" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.type === 'å¿«é'" |
| | | label="å¿«éåå·" |
| | | prop="expressNumber"> |
| | | <up-input v-model="form.expressNumber" |
| | | placeholder="请è¾å
¥å¿«éåå·" |
| | | clearable /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="åæ®ä¸å½æ¡£" |
| | | class="form-section"> |
| | | <up-form-item label="åè´§åæ®" |
| | | prop="shippingDocument" |
| | | border-bottom> |
| | | <up-input v-model="form.shippingDocument" |
| | | placeholder="请è¾å
¥åè´§åæ®" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯å¦å¯¹è´¦" |
| | | prop="isReconciled" |
| | | border-bottom> |
| | | <up-switch v-model="form.isReconciled" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.isReconciled === 1" |
| | | label="对账å彿¡£" |
| | | prop="statementArchivingStatus" |
| | | border-bottom> |
| | | <up-textarea v-model="form.statementArchivingStatus" |
| | | placeholder="请填å对账å彿¡£æ
åµ" |
| | | auto-height |
| | | maxlength="200" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯å¦ç»ç®" |
| | | prop="isSettled" |
| | | border-bottom> |
| | | <up-switch v-model="form.isSettled" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.isSettled === 1" |
| | | label="ç»ç®å彿¡£" |
| | | prop="settlementArchivingStatus" |
| | | border-bottom> |
| | | <up-textarea v-model="form.settlementArchivingStatus" |
| | | placeholder="请填åç»ç®å彿¡£æ
åµ" |
| | | auto-height |
| | | maxlength="200" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯å¦å·²å¼ç¥¨" |
| | | prop="isInvoiced"> |
| | | <up-switch v-model="form.isInvoiced" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="åè´§å¾ç" |
| | | class="form-section"> |
| | | <view class="upload-wrap"> |
| | | <CommonUpload v-model="form.deliveryFiles" |
| | | accept="image" |
| | | :max-count="6" /> |
| | | </view> |
| | | </u-cell-group> |
| | | </up-form> |
| | | |
| | | <u-cell-group title="å货产åæç»" |
| | | class="form-section"> |
| | | <view v-if="products.length === 0" |
| | | class="empty-block"> |
| | | <text>ææ äº§åæç»</text> |
| | | </view> |
| | | <view v-for="(row, index) in products" |
| | | :key="index" |
| | | class="product-row"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">产ååç§°</text> |
| | | <text class="detail-value">{{ row.productName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è§æ ¼åå·</text> |
| | | <text class="detail-value">{{ row.specificationModel || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ¹æ¬¡å·</text> |
| | | <text class="detail-value">{{ row.batchNo || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åè´§æ°é</text> |
| | | <text class="detail-value"> |
| | | {{ row.deliveryQuantity === "" ? "-" : row.deliveryQuantity }}{{ row.unit ? ` ${row.unit}` : "" }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { |
| | | resolveDeliveryFiles, |
| | | resolveDeliveryProducts, |
| | | } from "@/utils/delivery"; |
| | | import { |
| | | deductStock, |
| | | getDeliveryDetail, |
| | | } from "@/api/salesManagement/deliveryLedger"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const deliveryId = ref(""); |
| | | const products = ref([]); |
| | | |
| | | const typeOptions = [ |
| | | { name: "货车", value: "货车" }, |
| | | { name: "å¿«é", value: "å¿«é" }, |
| | | ]; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | type: "", |
| | | shippingDate: "", |
| | | shippingCarNumber: "", |
| | | expressCompany: "", |
| | | expressNumber: "", |
| | | shippingDocument: "", |
| | | isReconciled: 0, |
| | | statementArchivingStatus: "", |
| | | isSettled: 0, |
| | | settlementArchivingStatus: "", |
| | | isInvoiced: 0, |
| | | deliveryFiles: [], |
| | | }); |
| | | |
| | | // 车çä¸å¿«éå
¬å¸æåè´§ç±»åäºéä¸å¿
å¡« |
| | | const rules = { |
| | | type: [{ required: true, message: "è¯·éæ©åè´§ç±»å", trigger: "change" }], |
| | | shippingDate: [ |
| | | { required: true, message: "è¯·éæ©åè´§æ¥æ", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const onTypeChange = value => { |
| | | if (value === "货车") { |
| | | form.value.expressCompany = ""; |
| | | form.value.expressNumber = ""; |
| | | } else { |
| | | form.value.shippingCarNumber = ""; |
| | | } |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "deliveryDetail", |
| | | id: deliveryId.value, |
| | | fetcher: getDeliveryDetail, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "æªæ¾å°åè´§ä¿¡æ¯", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | form.value = { |
| | | id: data.id, |
| | | type: data.type || "", |
| | | shippingDate: (data.shippingDate || "").slice(0, 10), |
| | | shippingCarNumber: data.shippingCarNumber || "", |
| | | expressCompany: data.expressCompany || "", |
| | | expressNumber: data.expressNumber || "", |
| | | shippingDocument: data.shippingDocument || "", |
| | | isReconciled: data.isReconciled ?? 0, |
| | | statementArchivingStatus: data.statementArchivingStatus || "", |
| | | isSettled: data.isSettled ?? 0, |
| | | settlementArchivingStatus: data.settlementArchivingStatus || "", |
| | | isInvoiced: data.isInvoiced ?? 0, |
| | | deliveryFiles: resolveDeliveryFiles(data), |
| | | }; |
| | | products.value = resolveDeliveryProducts(data); |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | if (form.value.type === "货车" && !form.value.shippingCarNumber) { |
| | | uni.showToast({ title: "请è¾å
¥å货车çå·", icon: "none" }); |
| | | return; |
| | | } |
| | | if (form.value.type === "å¿«é" && !form.value.expressCompany) { |
| | | uni.showToast({ title: "请è¾å
¥å¿«éå
¬å¸", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | // éæ±å
ï¼åè´§å°è´¦è¦æ±è¡¥å
åæ®ä¸å¯¹è´¦/ç»ç®/å¼ç¥¨æ
åµã |
| | | // åèå®ç°ï¼ç®¡ç端ï¼åªæäº¤å 7 ä¸ªåæ®µï¼è¿éæéæ±ä¸å¹¶æäº¤ï¼ |
| | | // åç«¯è¥æªæ¥æ¶è¿å ä¸ªåæ®µä¼è¢«å¿½ç¥ï¼ä¸å½±åå货主æµç¨ã |
| | | const payload = { |
| | | id: form.value.id, |
| | | type: form.value.type, |
| | | shippingDate: form.value.shippingDate, |
| | | shippingCarNumber: |
| | | form.value.type === "货车" ? form.value.shippingCarNumber : "", |
| | | expressCompany: |
| | | form.value.type === "å¿«é" ? form.value.expressCompany : "", |
| | | expressNumber: form.value.type === "å¿«é" ? form.value.expressNumber : "", |
| | | storageBlobDTOs: (form.value.deliveryFiles || []) |
| | | .filter(file => file?.url) |
| | | .map(file => ({ url: file.url, name: file.name || "" })), |
| | | shippingDocument: form.value.shippingDocument, |
| | | isReconciled: form.value.isReconciled, |
| | | statementArchivingStatus: |
| | | form.value.isReconciled === 1 |
| | | ? form.value.statementArchivingStatus |
| | | : "", |
| | | isSettled: form.value.isSettled, |
| | | settlementArchivingStatus: |
| | | form.value.isSettled === 1 ? form.value.settlementArchivingStatus : "", |
| | | isInvoiced: form.value.isInvoiced, |
| | | }; |
| | | |
| | | loading.value = true; |
| | | deductStock(payload) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 1200); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | deliveryId.value = options.id; |
| | | form.value.id = options.id; |
| | | loadDetail(); |
| | | } else { |
| | | form.value.shippingDate = formatDateToYMD(Date.now()); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .upload-wrap { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .empty-block { |
| | | padding: 16px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .product-row { |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .product-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .detail-row { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .detail-label { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .detail-value { |
| | | font-size: 13px; |
| | | color: #333; |
| | | max-width: 70%; |
| | | text-align: right; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.salesContractNo" |
| | | placeholder="请è¾å
¥éå®ååå·æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="deliveryList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in deliveryList" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="car" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">{{ item.shippingNo || item.salesContractNo || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="statusText(item.status)" |
| | | :type="statusTagType(item.status)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">éå®ååå·</text> |
| | | <text class="detail-value">{{ item.salesContractNo || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æå±é¡¹ç®</text> |
| | | <text class="detail-value">{{ item.projectName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">客æ·åç§°</text> |
| | | <text class="detail-value">{{ item.customerName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">产ååç§°</text> |
| | | <text class="detail-value">{{ item.productName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è§æ ¼åå·</text> |
| | | <text class="detail-value">{{ item.specificationModel || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åè´§æ¥æ</text> |
| | | <text class="detail-value">{{ item.shippingDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åè´§æ°é</text> |
| | | <text class="detail-value">{{ item.totalQuantity ?? "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å货车çå·</text> |
| | | <text class="detail-value">{{ item.shippingCarNumber || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¿«éå
¬å¸</text> |
| | | <text class="detail-value">{{ item.expressCompany || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¿«éåå·</text> |
| | | <text class="detail-value">{{ item.expressNumber || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºåºæ¹æ¬¡</text> |
| | | <text class="detail-value">{{ item.outboundBatches || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="returnNos(item).length > 0" |
| | | class="return-entry" |
| | | @click="showReturnNos(item)"> |
| | | <up-icon name="arrow-return-left" |
| | | size="15" |
| | | color="#ff9900"></up-icon> |
| | | <text class="return-entry-text">æ¥ç {{ returnNos(item).length }} 个éè´§å</text> |
| | | </view> |
| | | |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | :disabled="isLocked(item)" |
| | | @click="goEdit(item)">è¡¥å
åè´§ä¿¡æ¯</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | :disabled="isLocked(item)" |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ åè´§å°è´¦æ°æ®</text> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="åè´§å°è´¦çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | deliveryLedgerListPage, |
| | | delDeliveryLedger, |
| | | } from "@/api/salesManagement/deliveryLedger"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | import { loadProjectOptions } from "@/utils/projectOptions"; |
| | | |
| | | // è¿ä¸¤ç§ç¶æå·²è¿å
¥æ£å¼æµç¨ï¼ä¸å
è®¸åæ¹åè´§ä¿¡æ¯æå é¤ |
| | | const LOCKED_STATUS = ["å®¡æ ¸éè¿", "å·²åè´§"]; |
| | | |
| | | const STATUS_TEXT = { |
| | | 0: "å¾
å®¡æ ¸", |
| | | 1: "å®¡æ ¸ä¸", |
| | | 2: "å®¡æ ¸æç»", |
| | | 3: "å®¡æ ¸éè¿", |
| | | }; |
| | | |
| | | const deliveryList = ref([]); |
| | | const showFilter = ref(false); |
| | | const projectOptions = ref([]); |
| | | |
| | | const query = reactive({ |
| | | salesContractNo: "", |
| | | shippingCarNumber: "", |
| | | expressNumber: "", |
| | | projectId: "", |
| | | shippingDateRange: [], |
| | | }); |
| | | |
| | | const filterFields = computed(() => [ |
| | | { |
| | | prop: "salesContractNo", |
| | | label: "éå®ååå·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥éå®ååå·", |
| | | }, |
| | | { |
| | | prop: "shippingCarNumber", |
| | | label: "å货车çå·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥å货车çå·", |
| | | }, |
| | | { |
| | | prop: "expressNumber", |
| | | label: "å¿«éåå·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥å¿«éåå·", |
| | | }, |
| | | { prop: "projectId", label: "æå±é¡¹ç®", type: "select", options: projectOptions.value }, |
| | | { prop: "shippingDateRange", label: "åè´§æ¥æ", type: "daterange" }, |
| | | ]); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const statusText = status => { |
| | | if (status === null || status === undefined || status === "") return "-"; |
| | | return STATUS_TEXT[status] || String(status); |
| | | }; |
| | | |
| | | const statusTagType = status => { |
| | | if (status === "å·²åè´§" || status === 3 || status === "3") return "success"; |
| | | if (status === 1 || status === "1") return "warning"; |
| | | if (status === 2 || status === "2") return "error"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const isLocked = item => LOCKED_STATUS.includes(item?.status); |
| | | |
| | | const returnNos = item => |
| | | Array.isArray(item?.returnNoList) ? item.returnNoList : []; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("deliveryDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/deliveryLedger/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | if (isLocked(item)) { |
| | | uni.showToast({ title: "è¯¥ç¶æçæ°æ®ä¸å¯ç¼è¾åè´§ä¿¡æ¯", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.setStorageSync("deliveryDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/deliveryLedger/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | // éæ±å
.4ï¼éè´§ç»è®¡ï¼å¼¹çªå举éè´§åå· |
| | | const showReturnNos = item => { |
| | | const list = returnNos(item); |
| | | if (list.length === 0) return; |
| | | uni.showModal({ |
| | | title: "éè´§åå·", |
| | | content: list.join("\n"), |
| | | showCancel: false, |
| | | confirmText: "ç¥éäº", |
| | | }); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | const [beginShippingDate, endShippingDate] = Array.isArray( |
| | | query.shippingDateRange |
| | | ) |
| | | ? query.shippingDateRange |
| | | : []; |
| | | deliveryLedgerListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | salesContractNo: query.salesContractNo || undefined, |
| | | shippingCarNumber: query.shippingCarNumber || undefined, |
| | | expressNumber: query.expressNumber || undefined, |
| | | projectId: query.projectId || undefined, |
| | | beginShippingDate: beginShippingDate || undefined, |
| | | endShippingDate: endShippingDate || undefined, |
| | | }) |
| | | .then(res => { |
| | | deliveryList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | if (isLocked(item)) { |
| | | uni.showToast({ title: "è¯¥ç¶æçæ°æ®ä¸å¯å é¤", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥åè´§å°è´¦åï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delDeliveryLedger(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | let projectsLoaded = false; |
| | | |
| | | const loadProjects = () => { |
| | | if (projectsLoaded) return; |
| | | projectsLoaded = true; |
| | | loadProjectOptions() |
| | | .then(options => { |
| | | projectOptions.value = options; |
| | | }) |
| | | .catch(() => { |
| | | projectsLoaded = false; |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | loadProjects(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .return-entry { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | margin: 0 12px 10px; |
| | | padding: 8px 10px; |
| | | border-radius: 8px; |
| | | background: #fffaf0; |
| | | } |
| | | |
| | | .return-entry-text { |
| | | font-size: 13px; |
| | | color: #ff9900; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="lawsuit-detail"> |
| | | <PageHeader title="è¯è®¼è¯¦æ
" |
| | | @back="goBack"> |
| | | <template #right> |
| | | <text class="header-action" |
| | | @click="goEdit">ç¼è¾</text> |
| | | </template> |
| | | </PageHeader> |
| | | |
| | | <view v-if="!loading" |
| | | class="detail-card"> |
| | | <view class="hero"> |
| | | <text class="hero-title">{{ detail.caseNo || "-" }}</text> |
| | | <up-tag :text="detail.lawsuitStatus || 'æªå¡«ç¶æ'" |
| | | :type="statusTagType(detail.lawsuitStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | |
| | | <u-cell-group title="æ¡ä»¶ä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="æ¡ä»¶ç¼å·" |
| | | :value="detail.caseNo || '-'" /> |
| | | <u-cell title="è¯è®¼ç¶æ" |
| | | :value="detail.lawsuitStatus || '-'" /> |
| | | <u-cell title="è¯è®¼æ çéé¢" |
| | | :value="formatAmount(detail.claimAmount)" /> |
| | | <u-cell title="ç«æ¡æ¶é´" |
| | | :value="detail.filingDate || '-'" /> |
| | | <u-cell title="å¼åºæ¶é´" |
| | | :value="detail.hearingDate || '-'" /> |
| | | <u-cell title="æ¿åå¾å¸" |
| | | :value="detail.lawyer || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å
³èä¿¡æ¯" |
| | | class="detail-section"> |
| | | <u-cell title="æå±é¡¹ç®" |
| | | :value="detail.projectName || '-'" /> |
| | | <u-cell title="å
³èéå®åå" |
| | | :value="detail.contractNo || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="è¿å±ä¸ç»æ" |
| | | class="detail-section"> |
| | | <u-cell title="å¤å³ç»ææè¦" |
| | | :value="detail.judgmentSummary || '-'" /> |
| | | <u-cell title="è¿å±å¤æ³¨" |
| | | :value="detail.progressRemark || '-'" /> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éä»¶" |
| | | class="detail-section"> |
| | | <view v-if="attachments.length === 0" |
| | | class="empty-file"> |
| | | <text>ææ éä»¶</text> |
| | | </view> |
| | | <view v-else |
| | | class="file-list"> |
| | | <view v-for="(file, index) in attachments" |
| | | :key="index" |
| | | class="file-item" |
| | | @click="previewFile(file, index)"> |
| | | <up-icon name="file-text" |
| | | size="18" |
| | | color="#2979ff"></up-icon> |
| | | <text class="file-name">{{ fileLabel(file, index) }}</text> |
| | | <up-icon name="download" |
| | | size="16" |
| | | color="#909399"></up-icon> |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | </view> |
| | | |
| | | <view v-else |
| | | class="loading-tip"> |
| | | <text>å è½½ä¸...</text> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { toFileList } from "@/utils/file"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { getLawsuitDetail } from "@/api/salesManagement/lawsuitManagement"; |
| | | |
| | | const loading = ref(true); |
| | | const lawsuitId = ref(""); |
| | | const detail = ref({}); |
| | | |
| | | const attachments = computed(() => |
| | | toFileList( |
| | | detail.value.storageBlobVOs || |
| | | detail.value.attachmentList || |
| | | detail.value.fileList |
| | | ) |
| | | ); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goEdit = () => { |
| | | uni.setStorageSync("lawsuitDetail", detail.value || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/lawsuitManagement/edit?id=${lawsuitId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const statusTagType = status => { |
| | | if (status === "å·²å¤å³" || status === "å·²åè§£") return "success"; |
| | | if (status === "å·²æ¤è¯") return "info"; |
| | | if (status) return "warning"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const fileLabel = (file, index) => |
| | | file.name || file.originalFilename || `éä»¶${index + 1}`; |
| | | |
| | | // request.ts æªéä¼ responseTypeï¼blob ä¸è½½ä¸å¯ç¨ï¼åªè½ç¨ downloadFile + openDocument |
| | | const previewFile = file => { |
| | | const url = file.url || file.previewURL; |
| | | if (!url) return; |
| | | uni.showLoading({ title: "æå¼ä¸...", mask: true }); |
| | | uni.downloadFile({ |
| | | url, |
| | | success: res => { |
| | | if (res.statusCode !== 200) { |
| | | uni.showToast({ title: "æä»¶ä¸è½½å¤±è´¥", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.openDocument({ |
| | | filePath: res.tempFilePath, |
| | | showMenu: true, |
| | | fail: () => uni.showToast({ title: "æä¸æ¯æé¢è§è¯¥æä»¶", icon: "none" }), |
| | | }); |
| | | }, |
| | | fail: () => uni.showToast({ title: "æä»¶ä¸è½½å¤±è´¥", icon: "none" }), |
| | | complete: () => uni.hideLoading(), |
| | | }); |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const data = await loadDetailData({ |
| | | cacheKey: "lawsuitDetail", |
| | | id: lawsuitId.value, |
| | | fetcher: getLawsuitDetail, |
| | | }); |
| | | detail.value = data || {}; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·åè¯è®¼è¯¦æ
失败", icon: "none" }); |
| | | detail.value = {}; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | lawsuitId.value = options?.id || ""; |
| | | if (!lawsuitId.value) { |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loadDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .lawsuit-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 24px; |
| | | } |
| | | |
| | | .header-action { |
| | | font-size: 14px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .hero { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 18px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #fff7ea 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(230, 162, 60, 0.1); |
| | | } |
| | | |
| | | .hero-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .empty-file { |
| | | padding: 16px; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .file-list { |
| | | padding: 6px 16px 12px; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #f2f4f8; |
| | | } |
| | | |
| | | .file-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .file-name { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #333; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .loading-tip { |
| | | padding: 80px 0; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="æ¡ä»¶ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="æ¡ä»¶ç¼å·" |
| | | prop="caseNo" |
| | | required |
| | | border-bottom> |
| | | <up-input v-model="form.caseNo" |
| | | placeholder="请è¾å
¥æ¡ä»¶ç¼å·" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="è¯è®¼ç¶æ" |
| | | prop="lawsuitStatus" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.lawsuitStatus" |
| | | type="select" |
| | | title="éæ©è¯è®¼ç¶æ" |
| | | :options="lawsuitStatusOptions" |
| | | placeholder="è¯·éæ©è¯è®¼ç¶æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="è¯è®¼æ çéé¢" |
| | | prop="claimAmount" |
| | | border-bottom> |
| | | <up-input v-model="form.claimAmount" |
| | | type="number" |
| | | placeholder="请è¾å
¥è¯è®¼æ çéé¢" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="ç«æ¡æ¶é´" |
| | | prop="filingDate" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.filingDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ç«æ¡æ¶é´" /> |
| | | </up-form-item> |
| | | <up-form-item label="å¼åºæ¶é´" |
| | | prop="hearingDate" |
| | | border-bottom> |
| | | <FormPicker v-model="form.hearingDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©å¼åºæ¶é´" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="å
³èä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="æå±é¡¹ç®" |
| | | prop="projectId" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.projectId" |
| | | :display-text="form.projectName" |
| | | type="select" |
| | | title="éæ©æå±é¡¹ç®" |
| | | :options="projectOptions" |
| | | placeholder="è¯·éæ©æå±é¡¹ç®" |
| | | @change="onProjectChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="å
³èéå®åå" |
| | | prop="contractId" |
| | | required |
| | | border-bottom> |
| | | <FormPicker v-model="form.contractId" |
| | | :display-text="form.contractNo" |
| | | type="select" |
| | | title="éæ©å
³èéå®åå" |
| | | :options="contractOptions" |
| | | placeholder="è¯·éæ©å
³èéå®åå" |
| | | @change="onContractChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¿åå¾å¸" |
| | | prop="lawyer" |
| | | border-bottom> |
| | | <up-input v-model="form.lawyer" |
| | | placeholder="请è¾å
¥æ¿åå¾å¸/æ³å¡è´è´£äºº" |
| | | clearable /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="è¿å±ä¸ç»æ" |
| | | class="form-section"> |
| | | <up-form-item label="å¤å³ç»ææè¦" |
| | | prop="judgmentSummary" |
| | | border-bottom> |
| | | <up-textarea v-model="form.judgmentSummary" |
| | | placeholder="请è¾å
¥å¤å³ç»ææè¦" |
| | | auto-height |
| | | maxlength="500" /> |
| | | </up-form-item> |
| | | <up-form-item label="è¿å±å¤æ³¨" |
| | | prop="progressRemark"> |
| | | <up-textarea v-model="form.progressRemark" |
| | | placeholder="请è¾å
¥è¯è®¼è¿å±å¤æ³¨" |
| | | auto-height |
| | | maxlength="500" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <u-cell-group title="éä»¶" |
| | | class="form-section"> |
| | | <view class="upload-wrap"> |
| | | <CommonUpload v-model="form.attachmentFiles" |
| | | accept="all" |
| | | :max-count="5" /> |
| | | </view> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | |
| | | <FooterButtons :loading="loading" |
| | | confirmText="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | </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 FormPicker from "@/components/FormPicker.vue"; |
| | | import CommonUpload from "@/components/CommonUpload.vue"; |
| | | import { toFileList } from "@/utils/file"; |
| | | import { loadDetailData } from "@/utils/detailCache"; |
| | | import { loadProjectOptions, pickProjectName } from "@/utils/projectOptions"; |
| | | import { |
| | | addOrUpdateLawsuit, |
| | | getLawsuitDetail, |
| | | } from "@/api/salesManagement/lawsuitManagement"; |
| | | import { contractListNoPage } from "@/api/salesManagement/contractManagement"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const lawsuitId = ref(""); |
| | | const projectOptions = ref([]); |
| | | const contractOptions = ref([]); |
| | | |
| | | const lawsuitStatusOptions = [ |
| | | { name: "ç«æ¡ä¸", value: "ç«æ¡ä¸" }, |
| | | { name: "审çä¸", value: "审çä¸" }, |
| | | { name: "å·²å¤å³", value: "å·²å¤å³" }, |
| | | { name: "å·²æ¤è¯", value: "å·²æ¤è¯" }, |
| | | { name: "å·²åè§£", value: "å·²åè§£" }, |
| | | ]; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | caseNo: "", |
| | | lawsuitStatus: "", |
| | | claimAmount: "", |
| | | filingDate: "", |
| | | hearingDate: "", |
| | | lawyer: "", |
| | | projectId: "", |
| | | projectName: "", |
| | | contractId: "", |
| | | contractNo: "", |
| | | judgmentSummary: "", |
| | | progressRemark: "", |
| | | attachmentFiles: [], |
| | | }); |
| | | |
| | | const rules = { |
| | | caseNo: [{ required: true, message: "请è¾å
¥æ¡ä»¶ç¼å·", trigger: "blur" }], |
| | | lawsuitStatus: [ |
| | | { required: true, message: "è¯·éæ©è¯è®¼ç¶æ", trigger: "change" }, |
| | | ], |
| | | filingDate: [{ required: true, message: "è¯·éæ©ç«æ¡æ¶é´", trigger: "change" }], |
| | | projectId: [{ required: true, message: "è¯·éæ©æå±é¡¹ç®", trigger: "change" }], |
| | | contractId: [ |
| | | { required: true, message: "è¯·éæ©å
³èéå®åå", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | const pageTitle = computed(() => (lawsuitId.value ? "ç¼è¾è¯è®¼" : "æ°å¢è¯è®¼")); |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const numOrNull = value => |
| | | value === "" || value === null || value === undefined |
| | | ? null |
| | | : Number.isNaN(Number(value)) |
| | | ? null |
| | | : Number(value); |
| | | |
| | | const onProjectChange = (value, item) => { |
| | | form.value.projectName = pickProjectName(item); |
| | | }; |
| | | |
| | | const onContractChange = (value, item) => { |
| | | form.value.contractNo = item?.contractNo || ""; |
| | | }; |
| | | |
| | | const loadOptions = async () => { |
| | | try { |
| | | projectOptions.value = await loadProjectOptions(); |
| | | } catch (error) { |
| | | projectOptions.value = []; |
| | | } |
| | | try { |
| | | const res = await contractListNoPage(); |
| | | const records = res?.data?.records || res?.records || res?.data || []; |
| | | contractOptions.value = (Array.isArray(records) ? records : []).map( |
| | | item => ({ |
| | | name: item.signingUnit |
| | | ? `${item.contractNo}ï¼${item.signingUnit}ï¼` |
| | | : item.contractNo, |
| | | value: item.id, |
| | | raw: item, |
| | | }) |
| | | ); |
| | | } catch (error) { |
| | | contractOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const loadDetail = async () => { |
| | | let data = null; |
| | | try { |
| | | data = await loadDetailData({ |
| | | cacheKey: "lawsuitDetail", |
| | | id: lawsuitId.value, |
| | | fetcher: getLawsuitDetail, |
| | | }); |
| | | } catch (error) { |
| | | data = null; |
| | | } |
| | | if (!data) { |
| | | uni.showToast({ title: "æªæ¾å°è¯è®¼ä¿¡æ¯", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | form.value = { |
| | | id: data.id, |
| | | caseNo: data.caseNo || "", |
| | | lawsuitStatus: data.lawsuitStatus || "", |
| | | claimAmount: data.claimAmount ?? "", |
| | | filingDate: (data.filingDate || "").slice(0, 10), |
| | | hearingDate: (data.hearingDate || "").slice(0, 10), |
| | | lawyer: data.lawyer || "", |
| | | projectId: data.projectId ?? "", |
| | | projectName: data.projectName || "", |
| | | contractId: data.contractId ?? "", |
| | | contractNo: data.contractNo || "", |
| | | judgmentSummary: data.judgmentSummary || "", |
| | | progressRemark: data.progressRemark || "", |
| | | attachmentFiles: toFileList( |
| | | data.storageBlobVOs || data.attachmentList || data.fileList |
| | | ), |
| | | }; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | const payload = { |
| | | id: form.value.id, |
| | | caseNo: form.value.caseNo, |
| | | lawsuitStatus: form.value.lawsuitStatus, |
| | | claimAmount: numOrNull(form.value.claimAmount), |
| | | filingDate: form.value.filingDate, |
| | | hearingDate: form.value.hearingDate || null, |
| | | lawyer: form.value.lawyer, |
| | | projectId: form.value.projectId, |
| | | projectName: form.value.projectName, |
| | | contractId: form.value.contractId, |
| | | contractNo: form.value.contractNo, |
| | | judgmentSummary: form.value.judgmentSummary, |
| | | progressRemark: form.value.progressRemark, |
| | | storageBlobDTOs: (form.value.attachmentFiles || []) |
| | | .filter(file => file?.url) |
| | | .map(file => ({ url: file.url, name: file.name || "" })), |
| | | }; |
| | | |
| | | loading.value = true; |
| | | addOrUpdateLawsuit(payload) |
| | | .then(() => { |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 1000); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "ä¿å失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | loadOptions(); |
| | | if (options?.id) { |
| | | lawsuitId.value = options.id; |
| | | form.value.id = options.id; |
| | | loadDetail(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .upload-wrap { |
| | | padding: 12px; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | </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" |
| | | v-model="query.caseNo" |
| | | placeholder="请è¾å
¥æ¡ä»¶ç¼å·æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="lawsuitList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in lawsuitList" |
| | | :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.caseNo || "-" }}</text> |
| | | </view> |
| | | <up-tag :text="item.lawsuitStatus || 'æªå¡«ç¶æ'" |
| | | :type="statusTagType(item.lawsuitStatus)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æå±é¡¹ç®</text> |
| | | <text class="detail-value">{{ item.projectName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å
³èåå</text> |
| | | <text class="detail-value">{{ item.contractNo || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">è¯è®¼æ ç</text> |
| | | <text class="detail-value">{{ formatAmount(item.claimAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç«æ¡æ¶é´</text> |
| | | <text class="detail-value">{{ item.filingDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¼åºæ¶é´</text> |
| | | <text class="detail-value">{{ item.hearingDate || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ¿åå¾å¸</text> |
| | | <text class="detail-value">{{ item.lawyer || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¤å³ç»æ</text> |
| | | <text class="detail-value">{{ item.judgmentSummary || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click="goEdit(item)">ç¼è¾</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click="goDetail(item)">详æ
</up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click="handleDelete(item)">å é¤</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ è¯è®¼æ°æ®</text> |
| | | </view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="è¯è®¼çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | lawsuitListPage, |
| | | delLawsuit, |
| | | } from "@/api/salesManagement/lawsuitManagement"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | |
| | | const LAWSUIT_STATUS_OPTIONS = [ |
| | | { name: "ç«æ¡ä¸", value: "ç«æ¡ä¸" }, |
| | | { name: "审çä¸", value: "审çä¸" }, |
| | | { name: "å·²å¤å³", value: "å·²å¤å³" }, |
| | | { name: "å·²æ¤è¯", value: "å·²æ¤è¯" }, |
| | | { name: "å·²åè§£", value: "å·²åè§£" }, |
| | | ]; |
| | | |
| | | const lawsuitList = ref([]); |
| | | const showFilter = ref(false); |
| | | |
| | | const query = reactive({ |
| | | caseNo: "", |
| | | projectName: "", |
| | | contractNo: "", |
| | | lawsuitStatus: "", |
| | | filingDateRange: [], |
| | | }); |
| | | |
| | | const filterFields = [ |
| | | { |
| | | prop: "caseNo", |
| | | label: "æ¡ä»¶ç¼å·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥æ¡ä»¶ç¼å·", |
| | | }, |
| | | { |
| | | prop: "projectName", |
| | | label: "æå±é¡¹ç®", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥é¡¹ç®åç§°", |
| | | }, |
| | | { |
| | | prop: "contractNo", |
| | | label: "ååç¼å·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥ååç¼å·", |
| | | }, |
| | | { |
| | | prop: "lawsuitStatus", |
| | | label: "è¯è®¼ç¶æ", |
| | | type: "select", |
| | | options: LAWSUIT_STATUS_OPTIONS, |
| | | }, |
| | | { prop: "filingDateRange", label: "ç«æ¡æ¶é´", type: "daterange" }, |
| | | ]; |
| | | |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync("lawsuitDetail"); |
| | | uni.navigateTo({ url: "/pages/sales/lawsuitManagement/edit" }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | uni.setStorageSync("lawsuitDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/lawsuitManagement/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | uni.setStorageSync("lawsuitDetail", item || {}); |
| | | uni.navigateTo({ |
| | | url: `/pages/sales/lawsuitManagement/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const formatAmount = amount => |
| | | amount === null || amount === undefined || amount === "" |
| | | ? "-" |
| | | : `Â¥${Number(amount).toFixed(2)}`; |
| | | |
| | | const statusTagType = status => { |
| | | if (status === "å·²å¤å³" || status === "å·²åè§£") return "success"; |
| | | if (status === "å·²æ¤è¯") return "info"; |
| | | if (status) return "warning"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const getList = () => { |
| | | const [filingDateStart, filingDateEnd] = Array.isArray(query.filingDateRange) |
| | | ? query.filingDateRange |
| | | : []; |
| | | lawsuitListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | caseNo: query.caseNo || undefined, |
| | | projectName: query.projectName || undefined, |
| | | contractNo: query.contractNo || undefined, |
| | | lawsuitStatus: query.lawsuitStatus || undefined, |
| | | filingDateStart: filingDateStart || undefined, |
| | | filingDateEnd: filingDateEnd || undefined, |
| | | }) |
| | | .then(res => { |
| | | lawsuitList.value = unwrapList(res); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: "确认å é¤è¯¥è¯è®¼è®°å½åï¼", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å¤çä¸...", mask: true }); |
| | | delLawsuit(item.id) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .detail-value { |
| | | max-width: 70%; |
| | | word-break: break-all; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <view class="receipt-payment-detail"> |
| | | <!-- 使ç¨éç¨é¡µé¢å¤´é¨ç»ä»¶ --> |
| | | <PageHeader title="客æ·å¾æ¥è¯¦æ
" |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <!-- ç»è®¡ä¿¡æ¯ --> |
| | | <view class="summary-info" |
| | | v-if="tableData.length > 0"> |
| | | |
| | | <!-- 第äºçº§ï¼é¡¹ç®ä¸ç客æ·å¾æ¥ --> |
| | | <template v-if="level === 'customer'"> |
| | | <!-- 项ç®åºæ¶æè¦ï¼éæ±ä¸ï¼åºæ¶è´¦æ¬¾ç®¡çéç¹ä¿¡æ¯æè¦ï¼ --> |
| | | <view class="summary-info"> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">æ»è®°å½æ°</text> |
| | | <text class="summary-value">{{ tableData.length }}</text> |
| | | <text class="summary-label">å®¢æ·æ°</text> |
| | | <text class="summary-value">{{ customerTotal }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">å¼ç¥¨æ»éé¢</text> |
| | | <text class="summary-value">{{ formatAmount(invoiceTotal) }}</text> |
| | | <text class="summary-label">ç¾çº¦æ»é¢</text> |
| | | <text class="summary-value">{{ fmtMoney(sumOf("contractAmounts")) }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">忬¾æ»éé¢</text> |
| | | <text class="summary-value highlight">{{ formatAmount(receiptTotal) }}</text> |
| | | <text class="summary-label">已忬¾</text> |
| | | <text class="summary-value highlight">{{ fmtMoney(sumOf("receiptPaymentAmount")) }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">åºæ¶æ»éé¢</text> |
| | | <text class="summary-value danger">{{ formatAmount(unReceiptTotal) }}</text> |
| | | <text class="summary-label">åºæ¶æ»é¢</text> |
| | | <text class="summary-value danger">{{ fmtMoney(sumOf("receiptableAmount")) }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">è¶
æåå</text> |
| | | <text class="summary-value danger">{{ overdueCount }} 份</text> |
| | | </view> |
| | | </view> |
| | | <!-- 忬¾è®°å½æç»å表 --> |
| | | |
| | | <view class="detail-list" |
| | | v-if="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" |
| | | v-if="customerList.length > 0"> |
| | | <view v-for="item in customerList" |
| | | :key="item.customerId" |
| | | class="detail-item" |
| | | @click="goContracts(item)"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="record-icon"> |
| | | <up-icon name="account" |
| | | size="16" |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="item-index">{{ item.customerName || "æªå½å客æ·" }}</text> |
| | | </view> |
| | | <up-tag :text="item.overdueCount > 0 ? `${item.overdueCount} 份è¶
æ` : 'æ£å¸¸å±¥çº¦'" |
| | | :type="item.overdueCount > 0 ? 'error' : 'success'" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç¾çº¦æ»é¢</text> |
| | | <text class="detail-value">{{ fmtMoney(item.contractAmounts) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">已忬¾</text> |
| | | <text class="detail-value highlight">{{ fmtMoney(item.receiptPaymentAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºæ¶éé¢</text> |
| | | <text class="detail-value danger">{{ fmtMoney(item.receiptableAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">éè´§éé¢</text> |
| | | <text class="detail-value">{{ fmtMoney(item.returnAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">忬¾è¿åº¦</text> |
| | | <text class="detail-value" |
| | | :class="{ danger: item.overdueCount > 0 }"> |
| | | {{ calcRate(item.receiptPaymentAmount, item.contractAmounts) }}% |
| | | </text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ååæ°</text> |
| | | <text class="detail-value">{{ item.contractCount || 0 }} 份</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>该项ç®ä¸ææ 客æ·å¾æ¥æ°æ®</text> |
| | | </view> |
| | | </template> |
| | | |
| | | <!-- 第ä¸çº§ï¼å®¢æ·ä¸çååæç» --> |
| | | <template v-else> |
| | | <view class="detail-list" |
| | | v-if="contractList.length > 0"> |
| | | <view v-for="(item, index) in contractList" |
| | | :key="index" |
| | | class="detail-item"> |
| | | <view class="item-header"> |
| | |
| | | <view class="record-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="item-index">{{ index + 1 }}</text> |
| | | <text class="item-index">{{ item.contractName || item.salesContractNo || "æªå½ååå" }}</text> |
| | | </view> |
| | | <view class="item-date">{{ item.happenTime }}</view> |
| | | <up-tag :text="contractStatusText(item)" |
| | | :type="contractStatusType(item)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¼ç¥¨éé¢(å
)</text> |
| | | <text class="detail-value">{{ formatAmount(item.invoiceTotal) }}</text> |
| | | <text class="detail-label">ååç¼å·</text> |
| | | <text class="detail-value">{{ item.salesContractNo || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">忬¾éé¢(å
)</text> |
| | | <text class="detail-value highlight">{{ formatAmount(item.receiptPaymentAmount) }}</text> |
| | | <text class="detail-label">ååæ»é¢</text> |
| | | <text class="detail-value">{{ fmtMoney(item.contractAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºæ¶éé¢(å
)</text> |
| | | <text class="detail-value danger">{{ formatAmount(item.unReceiptPaymentAmount) }}</text> |
| | | <text class="detail-label">已忬¾</text> |
| | | <text class="detail-value" |
| | | :class="{ danger: item.isPaymentTimeout }"> |
| | | {{ fmtMoney(item.receiptPaymentAmount) }} |
| | | ({{ calcRate(item.receiptPaymentAmount, item.contractAmount) }}%) |
| | | </text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åçæ¥æ</text> |
| | | <text class="detail-value">{{ item.receiptPaymentDate }}</text> |
| | | <text class="detail-label">åºæ¶éé¢</text> |
| | | <text class="detail-value danger">{{ fmtMoney(item.receiptableAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">éè´§éé¢</text> |
| | | <text class="detail-value">{{ fmtMoney(item.returnAmount) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ åæ¬¾è®°å½</text> |
| | | <text>该客æ·ä¸ææ ååæç»</text> |
| | | </view> |
| | | </template> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { customerInteractions } from "@/api/salesManagement/receiptPayment.js"; |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { |
| | | customewTransactions, |
| | | customewTransactionsDetails, |
| | | } from "@/api/salesManagement/indicatorStats"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | |
| | | // 客æ·ä¿¡æ¯ |
| | | // level: customer = 项ç®ä¸çå®¢æ·æ±æ»ï¼contract = 客æ·ä¸çååæç» |
| | | const level = ref("customer"); |
| | | const projectId = ref(""); |
| | | const projectName = ref(""); |
| | | const customerId = ref(""); |
| | | const customerName = ref(""); |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = ref([]); |
| | | const customerList = ref([]); |
| | | const contractList = ref([]); |
| | | const customerTotal = ref(0); |
| | | |
| | | const invoiceTotal = computed(() => { |
| | | return tableData.value.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.invoiceTotal) || 0); |
| | | }, 0); |
| | | const pageTitle = computed(() => { |
| | | if (level.value === "customer") return projectName.value || "客æ·å¾æ¥"; |
| | | return customerName.value || "ååæç»"; |
| | | }); |
| | | |
| | | const receiptTotal = computed(() => { |
| | | return tableData.value.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.receiptPaymentAmount) || 0); |
| | | }, 0); |
| | | }); |
| | | const sumOf = field => |
| | | customerList.value.reduce((sum, item) => sum + (Number(item[field]) || 0), 0); |
| | | |
| | | const unReceiptTotal = computed(() => { |
| | | return tableData.value.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.unReceiptPaymentAmount) || 0); |
| | | }, 0); |
| | | }); |
| | | const overdueCount = computed(() => |
| | | customerList.value.reduce((sum, item) => sum + (Number(item.overdueCount) || 0), 0) |
| | | ); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.removeStorageSync("customerId"); |
| | | uni.navigateBack(); |
| | | const loadCustomers = () => { |
| | | if (!projectId.value) return; |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | customewTransactions({ projectId: projectId.value, current: -1, size: -1 }) |
| | | .then(res => { |
| | | customerList.value = unwrapList(res); |
| | | customerTotal.value = Number(res?.data?.total ?? customerList.value.length); |
| | | uni.hideLoading(); |
| | | }) |
| | | .catch(() => { |
| | | uni.hideLoading(); |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // è·å页é¢åæ° |
| | | const getPageParams = () => { |
| | | // 仿¬å°åå¨è·å客æ·ID |
| | | const storedCustomerId = uni.getStorageSync("customerId"); |
| | | if (storedCustomerId) { |
| | | customerId.value = storedCustomerId; |
| | | } |
| | | }; |
| | | |
| | | // æ¥è¯¢å表 |
| | | const getList = () => { |
| | | if (!customerId.value) { |
| | | uni.showToast({ |
| | | title: "客æ·ä¿¡æ¯ç¼ºå¤±", |
| | | icon: "error", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | showLoadingToast("å è½½ä¸..."); |
| | | const param = { |
| | | const loadContracts = () => { |
| | | if (!projectId.value || !customerId.value) return; |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | customewTransactionsDetails({ |
| | | projectId: projectId.value, |
| | | customerId: customerId.value, |
| | | current: -1, |
| | | size: -1, |
| | | }; |
| | | |
| | | customerInteractions(param) |
| | | }) |
| | | .then(res => { |
| | | tableData.value = res.data; |
| | | closeToast(); |
| | | contractList.value = unwrapList(res); |
| | | uni.hideLoading(); |
| | | }) |
| | | .catch(() => { |
| | | closeToast(); |
| | | uni.showToast({ |
| | | title: "æ¥è¯¢å¤±è´¥", |
| | | icon: "error", |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // æ ¼å¼åéé¢ |
| | | const formatAmount = amount => { |
| | | return amount ? parseFloat(amount).toFixed(2) : "0.00"; |
| | | }; |
| | | |
| | | // æ¾ç¤ºå è½½æç¤º |
| | | const showLoadingToast = message => { |
| | | uni.showLoading({ |
| | | title: message, |
| | | mask: true, |
| | | }); |
| | | }; |
| | | |
| | | // å
³éæç¤º |
| | | const closeToast = () => { |
| | | uni.hideLoading(); |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | // 页颿¾ç¤ºæ¶è·ååæ°å¹¶å·æ°å表 |
| | | getPageParams(); |
| | | getList(); |
| | | }); |
| | | const goContracts = item => { |
| | | customerId.value = item.customerId; |
| | | customerName.value = item.customerName || ""; |
| | | level.value = "contract"; |
| | | loadContracts(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | // 页é¢å è½½æ¶è·ååæ°å¹¶å·æ°å表 |
| | | getPageParams(); |
| | | getList(); |
| | | // è¿åï¼ç¬¬ä¸çº§éå第äºçº§ï¼ç¬¬äºçº§æçæ£éåºé¡µé¢ |
| | | const goBack = () => { |
| | | if (level.value === "contract") { |
| | | level.value = "customer"; |
| | | contractList.value = []; |
| | | return; |
| | | } |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const contractStatusText = item => { |
| | | if (item.isPaymentTimeout) return "è¶
ææªå"; |
| | | const amount = Number(item.contractAmount) || 0; |
| | | if (amount > 0 && (Number(item.receiptPaymentAmount) || 0) >= amount) return "å·²ç»æ¸
"; |
| | | return "å±¥è¡ä¸"; |
| | | }; |
| | | |
| | | const contractStatusType = item => { |
| | | if (item.isPaymentTimeout) return "error"; |
| | | const amount = Number(item.contractAmount) || 0; |
| | | if (amount > 0 && (Number(item.receiptPaymentAmount) || 0) >= amount) return "success"; |
| | | return "primary"; |
| | | }; |
| | | |
| | | const fmtMoney = value => { |
| | | const num = Number(value); |
| | | if (!Number.isFinite(num)) return "Â¥ 0.00"; |
| | | return "Â¥ " + num.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | }; |
| | | |
| | | const calcRate = (paid, amount) => { |
| | | const total = Number(amount); |
| | | if (!Number.isFinite(total) || total === 0) return "0.0"; |
| | | return (((Number(paid) || 0) / total) * 100).toFixed(1); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | projectId.value = options?.projectId || ""; |
| | | projectName.value = options?.projectName ? decodeURIComponent(options.projectName) : ""; |
| | | if (!projectId.value) { |
| | | uni.showToast({ title: "缺å°é¡¹ç®åæ°", icon: "none" }); |
| | | return; |
| | | } |
| | | loadCustomers(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .record-icon { |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .item-index { |
| | | font-size: 14px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .item-date { |
| | | font-size: 12px; |
| | | color: #666; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .item-details { |
| | |
| | | <template> |
| | | <view class="sales-account"> |
| | | <!-- 使ç¨éç¨é¡µé¢å¤´é¨ç»ä»¶ --> |
| | | <PageHeader title="客æ·å¾æ¥" |
| | | @back="goBack" /> |
| | | |
| | | <!-- ç»è®¡æ¡ï¼éæ±ä¸ï¼åºæ¶æè¦ï¼ --> |
| | | <view class="summary-strip"> |
| | | <view class="summary-cell"> |
| | | <text class="summary-num">{{ summary.totalProjects || 0 }}</text> |
| | | <text class="summary-cap">项ç®</text> |
| | | </view> |
| | | <view class="summary-cell"> |
| | | <text class="summary-num">{{ summary.totalCustomers || 0 }}</text> |
| | | <text class="summary-cap">客æ·</text> |
| | | </view> |
| | | <view class="summary-cell"> |
| | | <text class="summary-num">{{ summary.totalContracts || 0 }}</text> |
| | | <text class="summary-cap">åå</text> |
| | | </view> |
| | | <view class="summary-cell"> |
| | | <text class="summary-num">{{ fmtMoney(summary.totalAmount) }}</text> |
| | | <text class="summary-cap">ç¾çº¦æ»é¢</text> |
| | | </view> |
| | | <view class="summary-cell overdue"> |
| | | <text class="summary-num">{{ summary.totalOverdueCount || 0 }}</text> |
| | | <text class="summary-cap">è¶
30天æªå款</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æç´¢åºå --> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input class="search-text" |
| | | placeholder="请è¾å
¥å®¢æ·åç§°" |
| | | v-model="searchForm.searchText" |
| | | @change="searchChange" |
| | | clearable /> |
| | | <up-input v-model="query.projectName" |
| | | class="search-text" |
| | | placeholder="请è¾å
¥é¡¹ç®åç§°æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="search-button" |
| | | @click="handleQuery"> |
| | | <up-icon name="search" |
| | | <view class="filter-button" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | color="#999" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- 客æ·å表 --> |
| | | <view class="customer-list-container"> |
| | | <view class="customer-list" |
| | | v-if="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" |
| | | :key="item.id" |
| | | class="customer-item" |
| | | @click="rowClickMethod(item)"> |
| | | |
| | | <!-- 项ç®å表 --> |
| | | <view v-if="projectList.length > 0" |
| | | class="ledger-list"> |
| | | <view v-for="item in projectList" |
| | | :key="item.projectId" |
| | | class="ledger-item" |
| | | @click="goDetail(item)"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="customer-icon"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="customer-name">{{ item.customerName }}</text> |
| | | <text class="item-id">{{ item.projectName || "-" }}</text> |
| | | </view> |
| | | <view class="item-right"> |
| | | <up-icon name="arrow-right" |
| | | size="16" |
| | | color="#999"></up-icon> |
| | | </view> |
| | | <up-tag :text="overdueText(item.overdueCount)" |
| | | :type="item.overdueCount > 0 ? 'error' : 'success'" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å¼ç¥¨éé¢(å
)</text> |
| | | <text class="detail-value">{{ formattedNumber(item.invoiceTotal) }}</text> |
| | | <text class="detail-label">项ç®ç¼å·</text> |
| | | <text class="detail-value">{{ item.projectCode || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">忬¾éé¢(å
)</text> |
| | | <text class="detail-value">{{ formattedNumber(item.receiptPaymentAmount) }}</text> |
| | | <text class="detail-label">ç¾çº¦æ»é¢</text> |
| | | <text class="detail-value highlight">{{ fmtMoney(item.contractAmounts) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºæ¶éé¢(å
)</text> |
| | | <text class="detail-value highlight danger">{{ formattedNumber(item.unReceiptPaymentAmount) }}</text> |
| | | <text class="detail-label">å¼ç¥¨éé¢</text> |
| | | <text class="detail-value">{{ fmtMoney(item.invoiceAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">忬¾éé¢</text> |
| | | <text class="detail-value highlight">{{ fmtMoney(item.receiptPaymentAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åºæ¶éé¢</text> |
| | | <text class="detail-value danger">{{ fmtMoney(item.receiptableAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">éè´§éé¢</text> |
| | | <text class="detail-value">{{ fmtMoney(item.returnAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">忬¾è¿åº¦</text> |
| | | <text class="detail-value" |
| | | :class="{ danger: item.overdueCount > 0 }"> |
| | | {{ calcRate(item.receiptPaymentAmount, item.contractAmounts) }}% |
| | | </text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å®¢æ· / åå</text> |
| | | <text class="detail-value">{{ item.customerCount || 0 }} 家 / {{ item.contractCount || 0 }} 份</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click.stop="goDetail(item)">æ¥ç客æ·å¾æ¥</up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>ææ å®¢æ·æ°æ®</text> |
| | | <text>ææ é¡¹ç®å¾æ¥æ°æ®</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="客æ·å¾æ¥çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref, reactive, toRefs } from "vue"; |
| | | import { reactive, ref, onMounted } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { invoiceLedgerSalesAccount } from "@/api/salesManagement/invoiceLedger"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | projectTransactions, |
| | | transactionsSummary, |
| | | } from "@/api/salesManagement/indicatorStats"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | |
| | | const tableData = ref([]); |
| | | const projectList = ref([]); |
| | | const summary = ref({}); |
| | | const showFilter = ref(false); |
| | | |
| | | const page = reactive({ |
| | | current: -1, |
| | | size: -1, |
| | | const query = reactive({ |
| | | projectName: "", |
| | | customerName: "", |
| | | contractName: "", |
| | | }); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | searchText: "", |
| | | invoiceDate: "", |
| | | }, |
| | | }); |
| | | const filterFields = [ |
| | | { prop: "customerName", label: "客æ·åç§°", type: "input", placeholder: "请è¾å
¥å®¢æ·åç§°" }, |
| | | { prop: "contractName", label: "åååç§°", type: "input", placeholder: "请è¾å
¥åååç§°" }, |
| | | ]; |
| | | |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | // æç´¢æ¡ååæ¶è§¦å |
| | | const searchChange = val => { |
| | | searchForm.value.searchText = val; |
| | | getList(); |
| | | }; |
| | | // æ¥è¯¢å表 |
| | | const handleQuery = () => { |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | showLoadingToast("å è½½ä¸..."); |
| | | invoiceLedgerSalesAccount({ ...searchForm.value, ...page }) |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | const params = { ...query, current: -1, size: -1 }; |
| | | projectTransactions(params) |
| | | .then(res => { |
| | | tableData.value = res.data.records; |
| | | closeToast(); |
| | | projectList.value = unwrapList(res); |
| | | uni.hideLoading(); |
| | | }) |
| | | .catch(() => { |
| | | closeToast(); |
| | | uni.showToast({ |
| | | title: "æ¥è¯¢å¤±è´¥", |
| | | icon: "error", |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const formattedNumber = value => { |
| | | return parseFloat(value || 0).toFixed(2); |
| | | }; |
| | | |
| | | // æ¾ç¤ºå è½½æç¤º |
| | | const showLoadingToast = message => { |
| | | uni.showLoading({ |
| | | title: message, |
| | | mask: true, |
| | | }); |
| | | }; |
| | | |
| | | // å
³éæç¤º |
| | | const closeToast = () => { |
| | | uni.hideLoading(); |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const rowClickMethod = row => { |
| | | // ä½¿ç¨ uni.setStorageSync åå¨å®¢æ·ä¿¡æ¯ |
| | | uni.setStorageSync("customerId", row.id); |
| | | // 跳转å°åæ¬¾è®°å½æç»é¡µé¢ |
| | | const getSummary = () => { |
| | | transactionsSummary() |
| | | .then(res => { |
| | | summary.value = res.data || {}; |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values || {}); |
| | | getList(); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | if (!item?.projectId) return; |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/receiptPaymentLedger/detail", |
| | | url: `/pages/sales/receiptPaymentLedger/detail?projectId=${item.projectId}` |
| | | + `&projectName=${encodeURIComponent(item.projectName || "")}`, |
| | | }); |
| | | }; |
| | | |
| | | const overdueText = count => (count > 0 ? `${count} 份è¶
æ` : "æ£å¸¸å±¥çº¦"); |
| | | |
| | | const fmtMoney = value => { |
| | | const num = Number(value); |
| | | if (!Number.isFinite(num)) return "Â¥ 0.00"; |
| | | return "Â¥ " + num.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | }; |
| | | |
| | | const calcRate = (paid, amount) => { |
| | | const total = Number(amount); |
| | | if (!Number.isFinite(total) || total === 0) return "0.0"; |
| | | return (((Number(paid) || 0) / total) * 100).toFixed(1); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | // 页颿¾ç¤ºæ¶å·æ°å表 |
| | | getList(); |
| | | getSummary(); |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | getSummary(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | // 客æ·å¾æ¥ç¹ææ ·å¼ |
| | | .summary-strip { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | background: #ffffff; |
| | | margin: 12px 12px 0; |
| | | border-radius: 12px; |
| | | padding: 12px 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .summary-cell { |
| | | flex: 1 0 33%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | padding: 6px 0; |
| | | } |
| | | |
| | | .summary-num { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333333; |
| | | } |
| | | |
| | | .summary-cap { |
| | | font-size: 11px; |
| | | color: #909399; |
| | | margin-top: 2px; |
| | | } |
| | | |
| | | .summary-cell.overdue .summary-num { |
| | | color: #ff4757; |
| | | } |
| | | |
| | | .detail-value.danger { |
| | | color: #ff4757; // ä¸å
Œ
±æ ·å¼ä¸ç #ee0a24 ä¸å |
| | | color: #ff4757; |
| | | font-weight: 500; |
| | | } |
| | | </style> |
| | |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="项ç®åç§°" |
| | | prop="projectName" |
| | | prop="projectId" |
| | | required> |
| | | <up-input v-model="form.projectName" |
| | | placeholder="请è¾å
¥é¡¹ç®åç§°" /> |
| | | <FormPicker v-model="form.projectId" |
| | | type="select" |
| | | title="鿩项ç®" |
| | | :options="projectOptions" |
| | | placeholder="è¯·éæ©é¡¹ç®" |
| | | @change="onProjectChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¾è®¢æ¥æ" |
| | | prop="executionDate" |
| | | required> |
| | | <up-input v-model="form.executionDate" |
| | | readonly |
| | | placeholder="ç¹å»éæ©æ¶é´" |
| | | @click="showDatePicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showDatePicker = true"></up-icon> |
| | | </template> |
| | | <FormPicker v-model="form.executionDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ç¾è®¢æ¥æ" |
| | | @change="onExecutionDateChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="年份"> |
| | | <up-input :model-value="form.year === '' ? 'èªå¨åç¾è®¢æ¥æ' : String(form.year)" |
| | | disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="仿¬¾æ¹å¼" |
| | | prop="paymentMethod"> |
| | | <up-input v-model="form.paymentMethod" |
| | | placeholder="请è¾å
¥ä»æ¬¾æ¹å¼" /> |
| | | <FormPicker v-model="form.paymentMethod" |
| | | type="select" |
| | | title="鿩仿¬¾æ¹å¼" |
| | | :display-text="paymentDisplayText" |
| | | :options="paymentOptions" |
| | | placeholder="è¯·éæ©ä»æ¬¾æ¹å¼" /> |
| | | </up-form-item> |
| | | <up-form-item label="äº¤è´§æ¥æ" |
| | | prop="deliveryDate"> |
| | | <FormPicker v-model="form.deliveryDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©äº¤è´§æ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="ç¾ååä½" |
| | | prop="signingUnitId"> |
| | | <FormPicker v-model="form.signingUnitId" |
| | | type="select" |
| | | title="éæ©ç¾ååä½" |
| | | :options="deptOptions" |
| | | placeholder="è¯·éæ©ç¾ååä½" /> |
| | | </up-form-item> |
| | | <up-form-item label="åäºå¤" |
| | | prop="officeId"> |
| | | <FormPicker v-model="form.officeId" |
| | | type="select" |
| | | title="éæ©åäºå¤" |
| | | :options="deptOptions" |
| | | placeholder="è¯·éæ©åäºå¤" /> |
| | | </up-form-item> |
| | | <up-form-item label="ç»è´è´£äºº" |
| | | prop="groupLeaderId"> |
| | | <FormPicker v-model="form.groupLeaderId" |
| | | type="select" |
| | | title="éæ©ç»è´è´£äºº" |
| | | :options="userOptions" |
| | | placeholder="è¯·éæ©ç»è´è´£äºº" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯å¦å¼å票"> |
| | | <view class="switch-row"> |
| | | <up-switch v-model="form.isInvoice" |
| | | :activeValue="1" |
| | | :inactiveValue="0" /> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯å¦ä¸æ "> |
| | | <view class="switch-row"> |
| | | <up-switch v-model="form.isBid" |
| | | :activeValue="1" |
| | | :inactiveValue="0" /> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.isBid === 1" |
| | | label="䏿 价"> |
| | | <up-input v-model="form.bidPrice" |
| | | type="number" |
| | | placeholder="请è¾å
¥ä¸æ ä»·" /> |
| | | </up-form-item> |
| | | <up-form-item v-else |
| | | label="æªä¸æ åå "> |
| | | <up-input v-model="form.unbidReason" |
| | | placeholder="请è¾å
¥æªä¸æ åå " /> |
| | | </up-form-item> |
| | | <up-form-item label="ååå件约å®åä¼ æ¥æ"> |
| | | <FormPicker v-model="form.contractOriginalReturnDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ååå件约å®åä¼ æ¥æ" /> |
| | | </up-form-item> |
| | | <up-form-item label="夿³¨"> |
| | | <up-textarea v-model="form.remarks" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | :maxlength="200" |
| | | count |
| | | auto-height /> |
| | | </up-form-item> |
| | | <up-form-item label="å½å
¥äºº" |
| | | prop="entryPersonName"> |
| | |
| | | title="éæ©ä¸å¡å" |
| | | @select="onSalesmanSelect" |
| | | @close="showPicker = false" /> |
| | | <!-- æ¥æéæ© --> |
| | | <up-popup :show="showDatePicker" |
| | | mode="bottom" |
| | | @close="showDatePicker = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="pickerDateValue" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" |
| | | mode="date" /> |
| | | </up-popup> |
| | | <!-- 客æ·éæ© --> |
| | | <up-action-sheet :show="showCustomerPicker" |
| | | :actions="customerActionList" |
| | |
| | | </up-form-item> |
| | | </view> |
| | | </view> |
| | | <!-- éæ±äº.4ï¼ç¾çº¦éé¢ = å«ç¨åä»· à æ°éï¼å³å«ç¨æ»ä»·ï¼æäº§åè¡æ±æ» --> |
| | | <view v-if="productData.length > 0" |
| | | class="product-summary"> |
| | | <view class="summary-row"> |
| | | <text class="summary-label">ç¾çº¦éé¢å计(å
)</text> |
| | | <text class="summary-value highlight">{{ productTotals.taxInclusive }}</text> |
| | | </view> |
| | | <view class="summary-row"> |
| | | <text class="summary-label">ä¸å«ç¨æ»ä»·å计(å
)</text> |
| | | <text class="summary-value">{{ productTotals.taxExclusive }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </up-form> |
| | | <!-- 使ç¨å
Œ
±åºé¨æé®ç»ä»¶ --> |
| | |
| | | <script setup> |
| | | import { onMounted, ref, computed } from "vue"; |
| | | import { userListNoPage } from "@/api/system/user"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { |
| | | addOrUpdateSalesLedger, |
| | | customerList, |
| | |
| | | modelList, |
| | | productTreeList, |
| | | } from "@/api/salesManagement/salesLedger"; |
| | | import { getProject } from "@/api/projectManagement/project"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { calculateTaxExclusiveTotalPrice } from "@/utils/summarizeTable"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import { loadProjectOptions } from "@/utils/projectOptions"; |
| | | import { takePrefillProject } from "@/utils/prefill"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | |
| | | const { checkout_payment } = useDict("checkout_payment"); |
| | | const paymentOptions = computed(() => |
| | | (checkout_payment.value || []).map(item => ({ |
| | | label: item.label, |
| | | value: item.value, |
| | | })) |
| | | ); |
| | | |
| | | // æ¹å¨å仿¬¾æ¹å¼æ¯èªç±ææ¬ï¼èåæ®çå¼å¯è½ä¸å¨åå
¸éï¼æ¤æ¶åæ¾åå¼è䏿¯ç©ºç½ |
| | | const paymentDisplayText = computed(() => { |
| | | const value = form.value.paymentMethod; |
| | | if (!value) return ""; |
| | | const matched = paymentOptions.value.find( |
| | | item => String(item.value) === String(value) |
| | | ); |
| | | return matched ? matched.label : String(value); |
| | | }); |
| | | |
| | | // è·å页é¢åæ° |
| | | const operationType = ref(""); |
| | |
| | | const form = ref({ |
| | | id: "", |
| | | salesContractNo: "", |
| | | autoGenerateContractNo: true, |
| | | customerId: "", |
| | | customerName: "", |
| | | projectId: "", |
| | | projectName: "", |
| | | executionDate: "", |
| | | year: "", |
| | | paymentMethod: "", |
| | | deliveryDate: "", |
| | | signingUnitId: "", |
| | | officeId: "", |
| | | groupLeaderId: "", |
| | | isInvoice: 0, |
| | | isBid: 0, |
| | | bidPrice: "", |
| | | unbidReason: "", |
| | | contractOriginalReturnDate: "", |
| | | remarks: "", |
| | | entryPerson: "", |
| | | entryPersonName: "", |
| | | entryDate: "", |
| | | }); |
| | | const showPicker = ref(false); |
| | | const showDatePicker = ref(false); |
| | | const pickerDateValue = ref(Date.now()); |
| | | const showCustomerPicker = ref(false); |
| | | const userList = ref([]); |
| | | const customerOption = ref([]); |
| | | const projectOptions = ref([]); |
| | | const deptOptions = ref([]); |
| | | const userOptions = ref([]); |
| | | const userActionList = computed(() => { |
| | | return userList.value.map(user => ({ |
| | | name: user.text, |
| | | value: user.value, |
| | | })); |
| | | }); |
| | | const formatter = (type, value) => { |
| | | if (type === "year") { |
| | | return `${value}`; |
| | | } |
| | | if (type === "month") { |
| | | return `${value}`; |
| | | } |
| | | if (type === "day") { |
| | | return `${value}`; |
| | | } |
| | | return value; |
| | | }; |
| | | const customerActionList = computed(() => { |
| | | return customerOption.value.map(customer => ({ |
| | | name: customer.text, |
| | | value: customer.value, |
| | | })); |
| | | }); |
| | | |
| | | // æ¥æéæ©å表已移é¤ï¼æ¹ç¨ up-datetime-picker |
| | | |
| | | // 产åå¤§ç±»éæ©å表 |
| | | const categoryActionList = computed(() => { |
| | |
| | | customerName: [ |
| | | { required: true, message: "è¯·éæ©å®¢æ·åç§°", trigger: "change" }, |
| | | ], |
| | | projectName: [{ required: true, message: "请è¾å
¥é¡¹ç®åç§°", trigger: "blur" }], |
| | | projectId: [{ required: true, message: "è¯·éæ©é¡¹ç®", trigger: "change" }], |
| | | executionDate: [ |
| | | { required: true, message: "è¯·éæ©ç¾è®¢æ¥æ", trigger: "change" }, |
| | | ], |
| | |
| | | form.value.salesman = item.name; |
| | | }; |
| | | |
| | | // æ¥æç¡®è®¤äºä»¶ |
| | | const onDateConfirm = e => { |
| | | form.value.executionDate = formatDateToYMD(e.value); |
| | | // ä¿æpickerDateValue为æ¶é´æ³æ ¼å¼ï¼è䏿¯æ ¼å¼åçå符串 |
| | | pickerDateValue.value = e.value; |
| | | showDatePicker.value = false; |
| | | // ç¾è®¢æ¥æ â 年份èªå¨å年份ï¼ç®¡çç«¯åæ¤é»è¾ï¼å¹´ä»½åªè¯»ï¼ |
| | | const onExecutionDateChange = value => { |
| | | form.value.year = value ? Number(String(value).substring(0, 4)) : ""; |
| | | }; |
| | | |
| | | // 客æ·éæ©äºä»¶ |
| | | const onCustomerSelect = item => { |
| | | form.value.customerName = item.name; |
| | | form.value.customerId = item.value; |
| | | }; |
| | | |
| | | // éæ±äº.4ï¼ç¾çº¦éé¢ = å«ç¨åä»· à æ°éï¼éè¡æ±æ» |
| | | const productTotals = computed(() => { |
| | | const sumBy = key => |
| | | (productData.value || []) |
| | | .reduce((sum, row) => sum + (Number(row?.[key]) || 0), 0) |
| | | .toFixed(2); |
| | | return { |
| | | taxInclusive: sumBy("taxInclusiveTotalPrice"), |
| | | taxExclusive: sumBy("taxExclusiveTotalPrice"), |
| | | }; |
| | | }); |
| | | |
| | | // éæ±äº.1ï¼é项ç®å带åºå®¢æ·ä¸äº§åæç»ï¼ç®¡ç端 fetchAndPopulateProject åé»è¾ï¼ |
| | | const applyProject = async id => { |
| | | const matched = projectOptions.value.find( |
| | | item => String(item.value) === String(id) |
| | | ); |
| | | form.value.projectName = matched?.name || ""; |
| | | |
| | | if (!id) { |
| | | productData.value = []; |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | const res = await getProject(id); |
| | | const detail = res?.data?.data ?? res?.data ?? res; |
| | | const info = detail?.info || {}; |
| | | if (info.clientId) form.value.customerId = info.clientId; |
| | | if (info.clientName) form.value.customerName = info.clientName; |
| | | |
| | | const products = detail?.salesLedgerProductList; |
| | | productData.value = Array.isArray(products) |
| | | ? products.map(item => ({ |
| | | ...item, |
| | | id: undefined, |
| | | salesLedgerId: undefined, |
| | | })) |
| | | : []; |
| | | } catch (error) { |
| | | uni.showToast({ title: "è·å项ç®ä¿¡æ¯å¤±è´¥", icon: "none" }); |
| | | } |
| | | }; |
| | | |
| | | // v-model å·²å
åå
¥ projectIdï¼åæ¶æ¶è¦åæ»å°ä¸ä¸ä¸ªé¡¹ç® |
| | | const previousProjectId = ref(""); |
| | | |
| | | const confirmProjectChange = id => { |
| | | previousProjectId.value = id; |
| | | applyProject(id); |
| | | }; |
| | | |
| | | const onProjectChange = id => { |
| | | if (productData.value && productData.value.length > 0) { |
| | | uni.showModal({ |
| | | title: "æç¤º", |
| | | content: "å½åå·²å½å
¥äº§åæç»ï¼åæ¢é¡¹ç®å°è¢«è¦çï¼æ¯å¦ç»§ç»ï¼", |
| | | success: res => { |
| | | if (res.confirm) { |
| | | confirmProjectChange(id); |
| | | } else { |
| | | form.value.projectId = previousProjectId.value; |
| | | } |
| | | }, |
| | | }); |
| | | return; |
| | | } |
| | | confirmProjectChange(id); |
| | | }; |
| | | |
| | | // åæçç¡®è®¤æ¹æ³å·²è¢«æ°çaction-sheetéæ©æ¹æ³æ¿ä»£ |
| | |
| | | const month = String(today.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(today.getDate()).padStart(2, "0"); |
| | | form.value.entryDate = `${year}-${month}-${day}`; |
| | | // è®¾ç½®æ¥æéæ©å¨é»è®¤å¼ä¸ºä»å¤© |
| | | pickerDateValue.value = `${year}-${month}-${day}`; |
| | | }; |
| | | // å¡«å
è¡¨åæ°æ®ï¼ç¼è¾æ¨¡å¼ï¼ |
| | | const fillFormData = () => { |
| | | if (!editData.value) return; |
| | | getSalesLedgerWithProducts({ id: editData.value.id, type: 1 }).then(res => { |
| | | productData.value = res.productData; |
| | | }); |
| | | console.log(editData.value); |
| | | // å¡«å
åºæ¬ä¿¡æ¯ |
| | | form.value.salesContractNo = editData.value.salesContractNo || ""; |
| | | form.value.customerName = editData.value.customerName || ""; |
| | | form.value.projectName = editData.value.projectName || ""; |
| | | form.value.executionDate = editData.value.executionDate || ""; |
| | | form.value.paymentMethod = editData.value.paymentMethod || ""; |
| | | form.value.salesman = editData.value.salesman || ""; |
| | | form.value.entryPerson = editData.value.entryPerson || ""; |
| | | form.value.entryPersonName = editData.value.entryPersonName || ""; |
| | | form.value.entryDate = editData.value.entryDate || ""; |
| | | form.value.id = editData.value.id || ""; |
| | | form.value.customerId = editData.value.customerId || ""; |
| | | const row = editData.value; |
| | | |
| | | // è®¾ç½®æ¥æéæ©å¨çå¼ |
| | | if (editData.value.executionDate) { |
| | | pickerDateValue.value = editData.value.executionDate; |
| | | // å
ç¨å表è¡åæ¥éºä¸éï¼ä¿è¯è¯·æ±è¿åå表å䏿¯ç©ºç |
| | | form.value.salesContractNo = row.salesContractNo || ""; |
| | | form.value.customerName = row.customerName || ""; |
| | | form.value.customerId = row.customerId || ""; |
| | | form.value.projectId = row.projectId ?? ""; |
| | | form.value.projectName = row.projectName || ""; |
| | | form.value.executionDate = row.executionDate || ""; |
| | | form.value.paymentMethod = row.paymentMethod || ""; |
| | | form.value.deliveryDate = row.deliveryDate || ""; |
| | | form.value.signingUnitId = row.signingUnitId ?? ""; |
| | | form.value.officeId = row.officeId ?? ""; |
| | | form.value.groupLeaderId = row.groupLeaderId ?? ""; |
| | | form.value.isInvoice = row.isInvoice ?? 0; |
| | | form.value.isBid = row.isBid ?? 0; |
| | | form.value.bidPrice = row.bidPrice ?? ""; |
| | | form.value.unbidReason = row.unbidReason || ""; |
| | | form.value.contractOriginalReturnDate = |
| | | row.contractOriginalReturnDate || ""; |
| | | form.value.remarks = row.remarks || ""; |
| | | form.value.salesman = row.salesman || ""; |
| | | form.value.entryPerson = row.entryPerson || ""; |
| | | form.value.entryPersonName = row.entryPersonName || ""; |
| | | form.value.entryDate = row.entryDate || ""; |
| | | form.value.year = row.year || ""; |
| | | form.value.id = row.id || ""; |
| | | previousProjectId.value = form.value.projectId; |
| | | |
| | | // 详æ
æ¥å£å段æ¯å表è¡å
¨ï¼åæ´ä½è¦ç䏿¬¡è¡¥é½æ°å¢å段 |
| | | getSalesLedgerWithProducts({ id: row.id, type: 1 }) |
| | | .then(res => { |
| | | const data = res?.data ?? res ?? {}; |
| | | productData.value = data.productData || []; |
| | | form.value = { ...form.value, ...data, id: row.id }; |
| | | previousProjectId.value = form.value.projectId ?? ""; |
| | | if (form.value.executionDate) { |
| | | form.value.year = Number( |
| | | String(form.value.executionDate).substring(0, 4) |
| | | ); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "è·åå°è´¦è¯¦æ
失败", icon: "none" }); |
| | | }); |
| | | }; |
| | | const getUserList = () => { |
| | | userListNoPage().then(res => { |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 项ç®ä¸æï¼éæ±äº.1 çèå¨å
¥å£ï¼ |
| | | const getProjectOptions = () => { |
| | | loadProjectOptions() |
| | | .then(options => { |
| | | projectOptions.value = options; |
| | | }) |
| | | .catch(() => { |
| | | projectOptions.value = []; |
| | | }); |
| | | }; |
| | | |
| | | // ç¾ååä½ / åäºå¤å
±ç¨é¨é¨ä¸æï¼ç»è´è´£äººç¨å工䏿 |
| | | const getOrgOptions = async () => { |
| | | const [depts, users] = await Promise.all([ |
| | | getDept().catch(() => ({})), |
| | | userListNoPageByTenantId().catch(() => ({})), |
| | | ]); |
| | | deptOptions.value = (depts?.data || []).map(item => ({ |
| | | name: item.deptName, |
| | | value: item.deptId, |
| | | })); |
| | | const rows = users?.data || []; |
| | | userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({ |
| | | name: item.nickName || item.userName || `ç¨æ·${item.userId}`, |
| | | value: item.userId, |
| | | })); |
| | | }; |
| | | |
| | | // éæ±ä¸.6ï¼æææ ä¸æ å带项ç®ä¿¡æ¯è·³è¿æ¥ï¼utils/prefill.js åå
¥ï¼ |
| | | const applyPrefillProject = () => { |
| | | const prefill = takePrefillProject(); |
| | | if (!prefill) return; |
| | | form.value.projectId = prefill.projectId ?? ""; |
| | | form.value.projectName = prefill.projectName || ""; |
| | | if (prefill.customerId) form.value.customerId = prefill.customerId; |
| | | if (prefill.customerName) form.value.customerName = prefill.customerName; |
| | | previousProjectId.value = form.value.projectId; |
| | | if (form.value.projectId) applyProject(form.value.projectId); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | // è·å页é¢åæ° |
| | | operationType.value = uni.getStorageSync("operationType") || ""; |
| | |
| | | getCustomerList(); |
| | | // è·å产å大类treeæ°æ® |
| | | getProductOptions(); |
| | | // è·åé¡¹ç® / é¨é¨ / å工䏿 |
| | | getProjectOptions(); |
| | | getOrgOptions(); |
| | | // èµå¼é»è®¤ä¿¡æ¯ |
| | | if (operationType.value === "add") { |
| | | setUserInfo(); |
| | | applyPrefillProject(); |
| | | } |
| | | |
| | | // è·åç¼è¾æ°æ®å¹¶å¡«å
表å |
| | |
| | | |
| | | <style lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .switch-row { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | width: 100%; |
| | | } |
| | | |
| | | .product-summary { |
| | | margin: 12px 0 0; |
| | | padding: 12px 14px; |
| | | background: #f8fbff; |
| | | border-radius: 10px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .summary-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .summary-label { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .summary-value { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | |
| | | .summary-value.highlight { |
| | | color: #2979ff; |
| | | } |
| | | </style> |
| | |
| | | </view> |
| | | </view> |
| | | </u-cell-group> |
| | | <!-- åæ®ä¸å½æ¡£ï¼éæ±å
ï¼ --> |
| | | <u-cell-group title="åæ®ä¸å½æ¡£" |
| | | class="form-section"> |
| | | <up-form-item label="åè´§åæ®" |
| | | prop="shippingDocument"> |
| | | <up-input v-model="form.shippingDocument" |
| | | placeholder="请è¾å
¥åè´§åæ®" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯å¦å¯¹è´¦" |
| | | prop="isReconciled"> |
| | | <up-switch v-model="form.isReconciled" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.isReconciled === 1" |
| | | label="对账å彿¡£" |
| | | prop="statementArchivingStatus"> |
| | | <up-textarea v-model="form.statementArchivingStatus" |
| | | placeholder="请填å对账å彿¡£æ
åµ" |
| | | auto-height |
| | | maxlength="200" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯å¦ç»ç®" |
| | | prop="isSettled"> |
| | | <up-switch v-model="form.isSettled" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </up-form-item> |
| | | <up-form-item v-if="form.isSettled === 1" |
| | | label="ç»ç®å彿¡£" |
| | | prop="settlementArchivingStatus"> |
| | | <up-textarea v-model="form.settlementArchivingStatus" |
| | | placeholder="请填åç»ç®å彿¡£æ
åµ" |
| | | auto-height |
| | | maxlength="200" /> |
| | | </up-form-item> |
| | | <up-form-item label="æ¯å¦å·²å¼ç¥¨" |
| | | prop="isInvoiced"> |
| | | <up-switch v-model="form.isInvoiced" |
| | | :active-value="1" |
| | | :inactive-value="0" /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | <!-- åè´§å¾ç --> |
| | | <u-cell-group title="åè´§å¾ç" |
| | | class="form-section"> |
| | |
| | | shippingCarNumber: "", |
| | | expressCompany: "", |
| | | expressNumber: "", |
| | | shippingDocument: "", |
| | | isReconciled: 0, |
| | | statementArchivingStatus: "", |
| | | isSettled: 0, |
| | | settlementArchivingStatus: "", |
| | | isInvoiced: 0, |
| | | }); |
| | | |
| | | const rules = { |
| | |
| | | quantity: parseFloat(b.deliveryQuantity), |
| | | productModelId: goOutData.value.productModelId, |
| | | })), |
| | | // éæ±å
ï¼åè´§æ¶åæ¥ç»è®°åæ®ä¸å¯¹è´¦/ç»ç®/å¼ç¥¨æ
åµï¼ |
| | | // åç«¯æªæ¥æ¶æ¶ä¼è¢«å¿½ç¥ï¼ä¸å½±åå货主æµç¨ |
| | | shippingDocument: form.shippingDocument, |
| | | isReconciled: form.isReconciled, |
| | | statementArchivingStatus: |
| | | form.isReconciled === 1 ? form.statementArchivingStatus : "", |
| | | isSettled: form.isSettled, |
| | | settlementArchivingStatus: |
| | | form.isSettled === 1 ? form.settlementArchivingStatus : "", |
| | | isInvoiced: form.isInvoiced, |
| | | }; |
| | | |
| | | try { |
| | |
| | | @click.stop="openOut(item)"> |
| | | åè´§ç¶æ |
| | | </u-button> |
| | | <!-- <u-button class="detail-button" |
| | | <u-button class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | @click.stop="handleInfo('edit', item)"> |
| | |
| | | </u-button> |
| | | <u-button class="detail-button" |
| | | size="small" |
| | | type="primary" |
| | | plain |
| | | @click.stop="openOut(item)"> |
| | | åè´§ç¶æ |
| | | </u-button> --> |
| | | <!-- <u-button class="detail-button" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click.stop="handleDelete(item)"> |
| | | å é¤ |
| | | </u-button> --> |
| | | @click.stop="openDetail(item)"> |
| | | 详æ
|
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | <text>ææ éå®å°è´¦æ°æ®</text> |
| | | </view> |
| | | <!-- æµ®å¨æä½æé® --> |
| | | <!-- <view class="fab-button" |
| | | <view class="fab-button" |
| | | @click="handleInfo('add')"> |
| | | <up-icon name="plus" |
| | | size="24" |
| | | color="#ffffff"></up-icon> |
| | | </view> --> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | }); |
| | | }; |
| | | |
| | | // åªè¯»è¯¦æ
ï¼view.vue 伿 id éæ°æä¸é宿´å°è´¦ï¼å表è¡åªä½å ä½ |
| | | const openDetail = item => { |
| | | uni.setStorageSync("editData", JSON.stringify(item)); |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/salesAccount/view", |
| | | }); |
| | | }; |
| | | |
| | | // å é¤åæ¡éå®å°è´¦ |
| | | const handleDelete = async row => { |
| | | if (!row || !row.id) return; |
| | |
| | | <text class="info-value">{{ form.executionDate }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">年份</text> |
| | | <text class="info-value">{{ form.year || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">仿¬¾æ¹å¼</text> |
| | | <text class="info-value">{{ form.paymentMethod }}</text> |
| | | <text class="info-value">{{ dictLabel(form.paymentMethod) }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">äº¤è´§æ¥æ</text> |
| | | <text class="info-value">{{ form.deliveryDate || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">ç¾ååä½</text> |
| | | <text class="info-value">{{ deptName(form.signingUnitId) }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">åäºå¤</text> |
| | | <text class="info-value">{{ deptName(form.officeId) }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">ç»è´è´£äºº</text> |
| | | <text class="info-value">{{ userName(form.groupLeaderId) }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">æ¯å¦å¼å票</text> |
| | | <text class="info-value">{{ form.isInvoice === 1 ? "æ¯" : "å¦" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">æ¯å¦ä¸æ </text> |
| | | <text class="info-value">{{ form.isBid === 1 ? "æ¯" : "å¦" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">{{ form.isBid === 1 ? "䏿 ä»·" : "æªä¸æ åå " }}</text> |
| | | <text class="info-value"> |
| | | {{ form.isBid === 1 ? form.bidPrice ?? "-" : form.unbidReason || "-" }} |
| | | </text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">ååå件约å®åä¼ æ¥æ</text> |
| | | <text class="info-value">{{ form.contractOriginalReturnDate || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å½å
¥äºº</text> |
| | |
| | | <view class="info-item"> |
| | | <text class="info-label">å½å
¥æ¥æ</text> |
| | | <text class="info-value">{{ form.entryDate }}</text> |
| | | </view> |
| | | <view v-if="form.remarks" |
| | | class="info-item remarks-item"> |
| | | <text class="info-label">夿³¨</text> |
| | | <text class="info-value">{{ form.remarks }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | class="no-product"> |
| | | <text>ææ äº§åä¿¡æ¯</text> |
| | | </view> |
| | | <!-- éæ±äº.4ï¼ç¾çº¦éé¢ = å«ç¨åä»· à æ°éï¼æäº§åè¡æ±æ»ï¼å¯¹é½ç®¡ç端 summarizeMainTableï¼ --> |
| | | <view v-if="productData.length > 0" |
| | | class="product-summary"> |
| | | <view class="summary-row"> |
| | | <text class="summary-label">ç¾çº¦éé¢å计(å
)</text> |
| | | <text class="summary-value highlight">{{ productTotals.taxInclusive }}</text> |
| | | </view> |
| | | <view class="summary-row"> |
| | | <text class="summary-label">ä¸å«ç¨æ»ä»·å计(å
)</text> |
| | | <text class="summary-value">{{ productTotals.taxExclusive }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref } from "vue"; |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { getSalesLedgerWithProducts } from "@/api/salesManagement/salesLedger"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { useDict } from "@/utils/dict"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | |
| | | const { checkout_payment } = useDict("checkout_payment"); |
| | | |
| | | // è·å页é¢åæ° |
| | | const editData = ref(null); |
| | |
| | | |
| | | // äº§åæ°æ® |
| | | const productData = ref([]); |
| | | const deptOptions = ref([]); |
| | | const userOptions = ref([]); |
| | | |
| | | const dictLabel = value => { |
| | | if (value === undefined || value === null || value === "") return "-"; |
| | | const matched = (checkout_payment.value || []).find( |
| | | item => String(item.value) === String(value) |
| | | ); |
| | | return matched ? matched.label : String(value); |
| | | }; |
| | | |
| | | // å端åªå idï¼åªè¯»é¡µéè¦èªå·±åæ¥é¨é¨ / åå·¥åç§° |
| | | const deptName = id => |
| | | deptOptions.value.find(item => String(item.value) === String(id))?.name || |
| | | "-"; |
| | | const userName = id => |
| | | userOptions.value.find(item => String(item.value) === String(id))?.name || |
| | | "-"; |
| | | |
| | | const productTotals = computed(() => { |
| | | const sumBy = key => |
| | | (productData.value || []) |
| | | .reduce((sum, row) => sum + (Number(row?.[key]) || 0), 0) |
| | | .toFixed(2); |
| | | return { |
| | | taxInclusive: sumBy("taxInclusiveTotalPrice"), |
| | | taxExclusive: sumBy("taxExclusiveTotalPrice"), |
| | | }; |
| | | }); |
| | | |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | |
| | | }); |
| | | }; |
| | | |
| | | const loadOrgOptions = async () => { |
| | | const [depts, users] = await Promise.all([ |
| | | getDept().catch(() => ({})), |
| | | userListNoPageByTenantId().catch(() => ({})), |
| | | ]); |
| | | deptOptions.value = (depts?.data || []).map(item => ({ |
| | | name: item.deptName, |
| | | value: item.deptId, |
| | | })); |
| | | const rows = users?.data || []; |
| | | userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({ |
| | | name: item.nickName || item.userName || `ç¨æ·${item.userId}`, |
| | | value: item.userId, |
| | | })); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | loadOrgOptions(); |
| | | // è·åç¼è¾æ°æ®å¹¶å¡«å
表å |
| | | const editDataStr = uni.getStorageSync("editData"); |
| | | if (editDataStr) { |
| | |
| | | color: #999; |
| | | font-size: 0.875rem; |
| | | } |
| | | |
| | | .remarks-item { |
| | | grid-column: 1 / -1; |
| | | } |
| | | |
| | | .product-summary { |
| | | background: #fff; |
| | | margin: 1rem; |
| | | padding: 1rem; |
| | | border-radius: 0.5rem; |
| | | box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.04); |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 0.75rem; |
| | | } |
| | | |
| | | .summary-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .summary-label { |
| | | font-size: 0.8125rem; |
| | | color: #666; |
| | | } |
| | | |
| | | .summary-value { |
| | | font-size: 0.875rem; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .summary-value.highlight { |
| | | color: #2979ff; |
| | | } |
| | | </style> |
| | |
| | | <text class="info-value">{{ detailData.customer || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">æå±é¡¹ç®</text> |
| | | <text class="info-value">{{ detailData.projectName || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">ä¸å¡å</text> |
| | | <text class="info-value">{{ detailData.salesperson || "-" }}</text> |
| | | </view> |
| | |
| | | @click="showCustomerSheet = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="æå±é¡¹ç®" |
| | | prop="projectId"> |
| | | <FormPicker v-model="form.projectId" |
| | | :display-text="form.projectName" |
| | | type="select" |
| | | title="éæ©æå±é¡¹ç®" |
| | | :options="projectOptions" |
| | | placeholder="è¯·éæ©æå±é¡¹ç®" |
| | | @change="onProjectChange" /> |
| | | </up-form-item> |
| | | <up-form-item label="ä¸å¡å" |
| | | prop="salesperson" |
| | | required> |
| | |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FormPicker from "@/components/FormPicker.vue"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { loadProjectOptions, pickProjectName } from "@/utils/projectOptions"; |
| | | import { modelList, productTreeList } from "@/api/basicData/product"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { |
| | |
| | | const salespersonList = ref([]); |
| | | const productList = ref([]); |
| | | const modelActions = ref([]); |
| | | const projectOptions = ref([]); |
| | | |
| | | let uidSeed = 1; |
| | | |
| | |
| | | quotationNo: "", |
| | | customerId: undefined, |
| | | customer: "", |
| | | projectId: "", |
| | | projectName: "", |
| | | salesperson: "", |
| | | quotationDate: "", |
| | | validDate: "", |
| | |
| | | ); |
| | | }; |
| | | |
| | | const onProjectChange = (value, item) => { |
| | | form.value.projectName = pickProjectName(item); |
| | | }; |
| | | |
| | | const loadProjectList = async () => { |
| | | try { |
| | | projectOptions.value = await loadProjectOptions(); |
| | | } catch (error) { |
| | | projectOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | // æ¥ä»·ååªå¸¦ projectId ä¸å¸¦åç§°æ¶ï¼ä»ä¸æéé¡¹åæ¥è¡¥å
¨ï¼é¿å
ä¿åæ¶æåç§°æ¸
空 |
| | | const resolveProjectName = () => { |
| | | if (form.value.projectName) return; |
| | | if (!form.value.projectId) { |
| | | form.value.projectName = ""; |
| | | return; |
| | | } |
| | | const matched = projectOptions.value.find( |
| | | option => String(option.value) === String(form.value.projectId) |
| | | ); |
| | | form.value.projectName = matched ? matched.name : ""; |
| | | }; |
| | | |
| | | // æ ¹æ®åç§°åæ¥èç¹ idï¼ä¾¿äºä»
ååç§°æ¶çåæ¾ |
| | | const findNodeIdByLabel = (nodes, label) => { |
| | | if (!label) return null; |
| | |
| | | quotationNo: data.quotationNo || "", |
| | | customerId: data.customerId, |
| | | customer: data.customer || "", |
| | | projectId: data.projectId ?? "", |
| | | projectName: data.projectName || "", |
| | | salesperson: data.salesperson || "", |
| | | quotationDate: data.quotationDate || "", |
| | | validDate: data.validDate || "", |
| | |
| | | // åæ¥ææ°çæ»é¢ |
| | | form.value.totalAmount = totalAmount.value; |
| | | form.value.subtotal = totalAmount.value; |
| | | resolveProjectName(); |
| | | |
| | | const payload = { |
| | | ...form.value, |
| | |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | await fetchBaseOptions(); |
| | | await Promise.all([fetchBaseOptions(), loadProjectList()]); |
| | | if (quotationId.value) { |
| | | await loadDetail(); |
| | | } |
| | |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input class="search-text" |
| | | v-model="quotationNo" |
| | | v-model="query.quotationNo" |
| | | placeholder="请è¾å
¥æ¥ä»·åå·æç´¢" |
| | | clearable |
| | | @change="getList" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="getList"> |
| | | <up-icon name="search" |
| | | @click="showFilter = true"> |
| | | <up-icon name="list" |
| | | size="24" |
| | | color="#999"></up-icon> |
| | | </view> |
| | |
| | | <view class="detail-row"> |
| | | <text class="detail-label">客æ·åç§°</text> |
| | | <text class="detail-value">{{ item.customer || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æå±é¡¹ç®</text> |
| | | <text class="detail-value">{{ item.projectName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ä¸å¡å</text> |
| | |
| | | size="28" |
| | | color="#ffffff"></up-icon> |
| | | </view> |
| | | |
| | | <FilterPanel v-model:show="showFilter" |
| | | :model-value="query" |
| | | title="æ¥ä»·çé" |
| | | :fields="filterFields" |
| | | @search="onFilterSearch" |
| | | @reset="onFilterSearch" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FilterPanel from "@/components/FilterPanel.vue"; |
| | | import { |
| | | deleteQuotation, |
| | | getQuotationList, |
| | | } from "@/api/salesManagement/salesQuotation"; |
| | | import { loadProjectOptions } from "@/utils/projectOptions"; |
| | | |
| | | const quotationNo = ref(""); |
| | | const quotationList = ref([]); |
| | | const showFilter = ref(false); |
| | | const projectOptions = ref([]); |
| | | |
| | | const query = reactive({ |
| | | quotationNo: "", |
| | | projectId: "", |
| | | }); |
| | | |
| | | const filterFields = computed(() => [ |
| | | { |
| | | prop: "quotationNo", |
| | | label: "æ¥ä»·åå·", |
| | | type: "input", |
| | | placeholder: "请è¾å
¥æ¥ä»·åå·", |
| | | }, |
| | | { |
| | | prop: "projectId", |
| | | label: "æå±é¡¹ç®", |
| | | type: "select", |
| | | options: projectOptions.value, |
| | | }, |
| | | ]); |
| | | |
| | | const tabList = reactive([ |
| | | { name: "å
¨é¨", value: "" }, |
| | |
| | | uni.showLoading({ title: "å è½½ä¸...", mask: true }); |
| | | getQuotationList({ |
| | | ...page, |
| | | quotationNo: quotationNo.value, |
| | | quotationNo: query.quotationNo, |
| | | projectId: query.projectId || undefined, |
| | | status: getCurrentStatus(), |
| | | }) |
| | | .then(res => { |
| | |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | // FilterPanel åªååçéå¼ï¼åå query åç»ä¸æ¥è¯¢ |
| | | const onFilterSearch = values => { |
| | | Object.assign(query, values); |
| | | getList(); |
| | | }; |
| | | |
| | | let projectsLoaded = false; |
| | | |
| | | const loadProjects = () => { |
| | | if (projectsLoaded) return; |
| | | projectsLoaded = true; |
| | | loadProjectOptions() |
| | | .then(options => { |
| | | projectOptions.value = options; |
| | | }) |
| | | .catch(() => { |
| | | projectsLoaded = false; |
| | | }); |
| | | }; |
| | | |
| | |
| | | |
| | | onShow(() => { |
| | | getList(); |
| | | loadProjects(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | </up-grid> |
| | | </view> |
| | | </view> |
| | | <!-- 项ç®ç®¡ç模å --> |
| | | <view class="common-module project-module" |
| | | v-if="hasProjectItems"> |
| | | <view class="module-header"> |
| | | <view class="module-title-container"> |
| | | <text class="module-title">项ç®ç®¡ç</text> |
| | | </view> |
| | | </view> |
| | | <view class="module-content"> |
| | | <up-grid :border="false" |
| | | col="4"> |
| | | <up-grid-item v-for="(item, index) in projectItems" |
| | | :key="index" |
| | | @click="handleCommonItemClick(item)"> |
| | | <view class="icon-container"> |
| | | <image :src="item.icon" |
| | | class="item-icon"></image> |
| | | </view> |
| | | <text class="item-label">{{item.label}}</text> |
| | | </up-grid-item> |
| | | </up-grid> |
| | | </view> |
| | | </view> |
| | | <!-- è¥éç®¡çæ¨¡å --> |
| | | <view class="common-module marketing-module" |
| | | v-if="hasMarketingItems"> |
| | |
| | | </up-grid> |
| | | </view> |
| | | </view> |
| | | <!-- 车è¾ç®¡ç模å --> |
| | | <view class="common-module vehicle-module" |
| | | v-if="hasVehicleItems"> |
| | | <view class="module-header"> |
| | | <view class="module-title-container"> |
| | | <text class="module-title">车è¾ç®¡ç</text> |
| | | </view> |
| | | </view> |
| | | <view class="module-content"> |
| | | <up-grid :border="false" |
| | | col="4"> |
| | | <up-grid-item v-for="(item, index) in vehicleItems" |
| | | :key="index" |
| | | @click="handleCommonItemClick(item)"> |
| | | <view class="icon-container"> |
| | | <image :src="item.icon" |
| | | class="item-icon"></image> |
| | | </view> |
| | | <text class="item-label">{{item.label}}</text> |
| | | </up-grid-item> |
| | | </up-grid> |
| | | </view> |
| | | </view> |
| | | <!-- å®å
¨ç产模å --> |
| | | <view class="common-module collaboration-module" |
| | | v-if="hasSafetyItems"> |
| | |
| | | }, |
| | | ]); |
| | | |
| | | // 项ç®ç®¡çåè½æ°æ® |
| | | const projectItems = reactive([ |
| | | { |
| | | icon: "/static/images/icon/xiangmuguanli.svg", |
| | | label: "项ç®ç®¡ç", |
| | | }, |
| | | ]); |
| | | |
| | | // 车è¾ç®¡çåè½æ°æ® |
| | | const vehicleItems = reactive([ |
| | | { |
| | | icon: "/static/images/icon/cheliangguanli.svg", |
| | | label: "车è¾ç®¡ç", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/jiayoukaguanli.svg", |
| | | label: "å æ²¹å¡ç®¡ç", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/jiayoujilu.svg", |
| | | label: "å æ²¹è®°å½", |
| | | }, |
| | | ]); |
| | | |
| | | // è¥é管çåè½æ°æ® |
| | | const marketingItems = reactive([ |
| | | { |
| | |
| | | { |
| | | icon: "/static/images/icon/kehuwanglai.svg", |
| | | label: "客æ·å¾æ¥", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/zhaotoubiaoguanli.svg", |
| | | label: "æææ 管ç", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/susongguanli.svg", |
| | | label: "è¯è®¼ç®¡ç", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/fahuotaizhang.svg", |
| | | label: "åè´§å°è´¦", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/hetongguanli.svg", |
| | | label: "éå®åå", |
| | | }, |
| | | ]); |
| | | |
| | |
| | | icon: "/static/images/icon/chuchangjianyan.svg", |
| | | label: "åºåæ£éª", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/baobiaoguanli.svg", |
| | | label: "æ¥è¡¨ç®¡ç", |
| | | }, |
| | | ]); |
| | | const safetyItems = reactive([ |
| | | { |
| | |
| | | icon: "/static/images/icon/baoxiaoguanli.svg", |
| | | label: "è´¹ç¨æ¥é", |
| | | path: "/pages/oa/ReimburseManage/cost-reimburse/index", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/kuandaiyutongxin.svg", |
| | | label: "宽带ä¸é信管ç", |
| | | }, |
| | | { |
| | | icon: "/static/images/icon/fangwuzulin.svg", |
| | | label: "æ¿å±ç§èµç®¡ç", |
| | | }, |
| | | ]); |
| | | |
| | |
| | | case "客æ·å¾æ¥": |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/receiptPaymentLedger/index", |
| | | }); |
| | | break; |
| | | case "项ç®ç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/projectManagement/project/index", |
| | | }); |
| | | break; |
| | | case "æææ 管ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/biddingManagement/index", |
| | | }); |
| | | break; |
| | | case "è¯è®¼ç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/lawsuitManagement/index", |
| | | }); |
| | | break; |
| | | case "åè´§å°è´¦": |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/deliveryLedger/index", |
| | | }); |
| | | break; |
| | | case "éå®åå": |
| | | uni.navigateTo({ |
| | | url: "/pages/sales/contractManagement/index", |
| | | }); |
| | | break; |
| | | case "æ¥è¡¨ç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/qualityManagement/inspectionReport/index", |
| | | }); |
| | | break; |
| | | case "车è¾ç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/inventoryManagement/vehicleInfo/index", |
| | | }); |
| | | break; |
| | | case "å æ²¹å¡ç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/inventoryManagement/fuelCard/index", |
| | | }); |
| | | break; |
| | | case "å æ²¹è®°å½": |
| | | uni.navigateTo({ |
| | | url: "/pages/inventoryManagement/fuelRecord/index", |
| | | }); |
| | | break; |
| | | case "宽带ä¸é信管ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/cooperativeOffice/broadbandCommunication/index", |
| | | }); |
| | | break; |
| | | case "æ¿å±ç§èµç®¡ç": |
| | | uni.navigateTo({ |
| | | url: "/pages/cooperativeOffice/houseLeaseManagement/index", |
| | | }); |
| | | break; |
| | | case "éè´å°è´¦": |
| | |
| | | specialMapping: { è§ç« å¶åº¦: "è§ç« å¶åº¦ç®¡ç" }, |
| | | }, |
| | | purchase: { specialMapping: { ä¾åºåæ¡£æ¡: "ä¾åºå管ç" } }, |
| | | // ææºç«¯äººäºæ¨¡åå·²å ç¨ãåå管çãlabelï¼èµ° /staff/staffContractï¼ï¼ |
| | | // éå®ååæ¹ç¨ãéå®ååãlabelï¼æ¤å¤æ å°åå端çèåæ é¢ |
| | | marketing: { specialMapping: { éå®åå: "åå管ç" } }, |
| | | }; |
| | | console.log(allowedMenuTitles); |
| | | // éç¨è¿æ»¤å½æ° |
| | |
| | | }; |
| | | |
| | | // è¿æ»¤å个模å |
| | | filterArray(marketingItems); |
| | | filterArray(marketingItems, menuMapping.marketing.specialMapping); |
| | | filterArray(designItems); |
| | | filterArray(purchaseItems, menuMapping.purchase.specialMapping); |
| | | filterArray(financeManagementItems); |
| | |
| | | filterArray(equipmentItems); |
| | | filterArray(archiveManagementItems); |
| | | filterArray(afterSalesServiceItems); |
| | | filterArray(projectItems); |
| | | filterArray(vehicleItems); |
| | | }; |
| | | |
| | | // æ£æ¥æ¨¡åæ¯å¦æèå项éè¦æ¾ç¤º |
| | |
| | | const hasArchiveManagementItems = computed( |
| | | () => archiveManagementItems.length > 0 |
| | | ); |
| | | const hasProjectItems = computed(() => projectItems.length > 0); |
| | | const hasVehicleItems = computed(() => vehicleItems.length > 0); |
| | | |
| | | onMounted(() => { |
| | | // æ¯æ¬¡è¿å
¥é¦é¡µé½å¼ºå¶å·æ°ç¨æ·ä¿¡æ¯åè·¯ç±æéï¼ä¸åæ¬å°ç¼å夿 |
| | |
| | | --module-color: #9c27b0; |
| | | } |
| | | |
| | | .project-module { |
| | | --module-color: #0066ff; |
| | | } |
| | | |
| | | .vehicle-module { |
| | | --module-color: #ff8a3d; |
| | | } |
| | | |
| | | .module-header { |
| | | margin-bottom: 10px; |
| | | display: flex; |
| | |
| | | --module-color: #9c27b0; |
| | | } |
| | | |
| | | .project-module { |
| | | --module-color: #0066ff; |
| | | } |
| | | |
| | | .vehicle-module { |
| | | --module-color: #ff8a3d; |
| | | } |
| | | |
| | | .module-header { |
| | | margin-bottom: 18px; |
| | | padding-left: 18px; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#19BE6B"/> |
| | | <rect x="8.4" y="14" width="2.8" height="5.4" rx="0.8" fill="#A0E8C0"/> |
| | | <rect x="12.6" y="10.5" width="2.8" height="8.9" rx="0.8" fill="#FFFFFF"/> |
| | | <rect x="16.8" y="12.8" width="2.8" height="6.6" rx="0.8" fill="#A0E8C0"/> |
| | | <rect x="7" y="19.4" width="14" height="1.7" rx="0.85" fill="#FFFFFF"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#2F7BE0"/> |
| | | <path d="M9.2 13.2V11.6A1.8 1.8 0 0 1 11 9.8h6a1.8 1.8 0 0 1 1.8 1.8v1.6z" fill="#99D9FC"/> |
| | | <rect x="6" y="13.2" width="16" height="5.4" rx="2" fill="#FFFFFF"/> |
| | | <rect x="10.4" y="10.6" width="2.6" height="1.6" rx="0.5" fill="#2F7BE0"/> |
| | | <rect x="15" y="10.6" width="2.6" height="1.6" rx="0.5" fill="#2F7BE0"/> |
| | | <rect x="17.8" y="14.6" width="2.4" height="1.7" rx="0.85" fill="#99D9FC"/> |
| | | <circle cx="9.6" cy="18.6" r="1.9" fill="#FFFFFF"/> |
| | | <circle cx="9.6" cy="18.6" r="0.85" fill="#2F7BE0"/> |
| | | <circle cx="18.4" cy="18.6" r="1.9" fill="#FFFFFF"/> |
| | | <circle cx="18.4" cy="18.6" r="0.85" fill="#2F7BE0"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#4C9AFF"/> |
| | | <path d="M13.2 5.9a1.2 1.2 0 0 1 1.6 0l8.2 7.2a1.1 1.1 0 0 1-.7 1.9H5.7a1.1 1.1 0 0 1-.7-1.9z" fill="#FFFFFF"/> |
| | | <rect x="7.4" y="14.4" width="13.2" height="8" rx="1.4" fill="#FFFFFF"/> |
| | | <rect x="18.4" y="15.4" width="1.4" height="6" fill="#4C9AFF"/> |
| | | <rect x="8.2" y="15.4" width="1.4" height="6" fill="#4C9AFF"/> |
| | | <rect x="12.4" y="17" width="3.2" height="5.4" rx="0.8" fill="#4C9AFF"/> |
| | | <rect x="16.2" y="16.4" width="2.6" height="2.4" rx="0.6" fill="#BFDBFF"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#12B7A0"/> |
| | | <rect x="6.5" y="7" width="10.5" height="15" rx="2.2" fill="#FFFFFF"/> |
| | | <rect x="8.8" y="9.4" width="6" height="4.2" rx="1" fill="#A7F0E4"/> |
| | | <rect x="8.8" y="15.2" width="6" height="1.6" rx="0.8" fill="#12B7A0"/> |
| | | <rect x="8.8" y="18" width="4.2" height="1.6" rx="0.8" fill="#12B7A0"/> |
| | | <path d="M20.2 9.4h1.8a1.8 1.8 0 0 1 1.8 1.8v5.6a1.8 1.8 0 1 1-3.6 0" fill="none" stroke="#FFFFFF" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#FF8A3D"/> |
| | | <rect x="4.6" y="8.4" width="18.8" height="11.6" rx="2.2" fill="#FFFFFF"/> |
| | | <rect x="4.6" y="11.2" width="18.8" height="2.6" fill="#FFD9B3"/> |
| | | <rect x="7.4" y="15.4" width="3.6" height="2.8" rx="0.9" fill="#FFB870"/> |
| | | <rect x="14.4" y="15.4" width="6.6" height="1.5" rx="0.75" fill="#FFD9B3"/> |
| | | <rect x="14.4" y="18" width="4.2" height="1.5" rx="0.75" fill="#FFD9B3"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#7C5CFC"/> |
| | | <path d="M6.6 12.2a10.4 10.4 0 0 1 14.8 0" fill="none" stroke="#C9BCFF" stroke-width="1.8" stroke-linecap="round"/> |
| | | <path d="M9.9 15.4a5.8 5.8 0 0 1 8.2 0" fill="none" stroke="#FFFFFF" stroke-width="1.8" stroke-linecap="round"/> |
| | | <circle cx="14" cy="19" r="1.9" fill="#FFFFFF"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#E6A23C"/> |
| | | <rect x="13.2" y="8" width="1.6" height="12" rx="0.8" fill="#FFFFFF"/> |
| | | <rect x="6.8" y="10.2" width="14.4" height="1.6" rx="0.8" fill="#FFFFFF"/> |
| | | <rect x="9.6" y="19.4" width="8.8" height="1.7" rx="0.85" fill="#FFFFFF"/> |
| | | <rect x="8" y="11.6" width="1" height="3.2" rx="0.5" fill="#FFE7A3"/> |
| | | <rect x="19" y="11.6" width="1" height="3.2" rx="0.5" fill="#FFE7A3"/> |
| | | <ellipse cx="8.5" cy="15.4" rx="2.6" ry="1.2" fill="#FFE7A3"/> |
| | | <ellipse cx="19.5" cy="15.4" rx="2.6" ry="1.2" fill="#FFE7A3"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#0066FF"/> |
| | | <rect x="8" y="8.5" width="12" height="13.5" rx="2.5" fill="#FFFFFF"/> |
| | | <rect x="11" y="6" width="6" height="4" rx="2" fill="#7BF1FF"/> |
| | | <rect x="10.5" y="13" width="7" height="1.6" rx="0.8" fill="#0066FF"/> |
| | | <rect x="10.5" y="16.5" width="4.5" height="1.6" rx="0.8" fill="#0066FF"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none"> |
| | | <rect x="2" y="2" width="24" height="24" rx="7" fill="#3462CC"/> |
| | | <g transform="rotate(-45 14 14)"> |
| | | <rect x="8.5" y="7.6" width="11" height="5" rx="1.6" fill="#FFFFFF"/> |
| | | <rect x="12.25" y="12.6" width="3.5" height="8" rx="1.6" fill="#99D9FC"/> |
| | | </g> |
| | | <circle cx="20.4" cy="20.4" r="3.6" fill="#3462CC"/> |
| | | <circle cx="20.4" cy="20.4" r="2.1" fill="#FFFFFF"/> |
| | | </svg> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * åè´§å°è´¦è¯¦æ
è§£å
|
| | | * |
| | | * å端 /shippingInfo/getDateil/{id} ç产åæç»æ°ç»æå¨å段åå¨ä¸åæµç¨ä¸ä¸ä¸è´ |
| | | * ï¼å®¡æ¹è¯¦æ
èµ° shippingProductDetailDtoListï¼åè´§å°è´¦èµ° batchNoDetailList çï¼ï¼ |
| | | * è¡å
çæ¹æ¬¡å·/æ°é乿å¤å¥å½åãéä¸å¨è¿éæ¢æµï¼é¡µé¢éä¸åºç°è£¸å段åã |
| | | */ |
| | | import { toFileList } from "@/utils/file"; |
| | | |
| | | const LIST_KEYS = [ |
| | | "batchNoDetailList", |
| | | "batchNoList", |
| | | "shippingBatchList", |
| | | "shippingInfoDetailList", |
| | | "shippingProductDetailDtoList", |
| | | "detailList", |
| | | "batchDetailList", |
| | | "rows", |
| | | "records", |
| | | "list", |
| | | ]; |
| | | |
| | | function pickList(source) { |
| | | if (!source) return []; |
| | | for (const key of LIST_KEYS) { |
| | | if (Array.isArray(source[key])) return source[key]; |
| | | } |
| | | return []; |
| | | } |
| | | |
| | | const pick = (row, keys) => { |
| | | for (const key of keys) { |
| | | const value = row?.[key]; |
| | | if (value !== undefined && value !== null && value !== "") return value; |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | /** |
| | | * å½ä¸å产åæç»è¡ |
| | | * @returns {Array<{batchNo, productName, specificationModel, deliveryQuantity, unit, remark}>} |
| | | */ |
| | | export function resolveDeliveryProducts(data) { |
| | | return pickList(data).map(row => ({ |
| | | batchNo: pick(row, ["batchNo", "batchNumber"]), |
| | | productName: pick(row, ["productName", "product"]), |
| | | specificationModel: pick(row, ["specificationModel", "model", "specification"]), |
| | | deliveryQuantity: pick(row, ["deliveryQuantity", "quantity", "shippingQuantity"]), |
| | | unit: pick(row, ["unit", "measureUnit"]), |
| | | remark: pick(row, ["remark", "remarks"]), |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * åè´§å¾çåæ¾ï¼deliveryFileList / storageBlobVOs 两ç§å½å |
| | | */ |
| | | export function resolveDeliveryFiles(data) { |
| | | return toFileList( |
| | | data?.deliveryFileList || data?.storageBlobVOs || data?.fileList |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * éè´§åå·å表ï¼å
¼å®¹å符串æ°ç»ä¸ [{returnNo}] 两ç§ç»æ |
| | | */ |
| | | export function resolveReturnNos(source) { |
| | | if (!Array.isArray(source)) return []; |
| | | return source |
| | | .map(item => |
| | | typeof item === "string" || typeof item === "number" |
| | | ? String(item) |
| | | : item?.returnNo || item?.returnOrderNo || "" |
| | | ) |
| | | .filter(Boolean); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * 详æ
æ°æ®å è½½ï¼æ¥å£ä¼å
ï¼ç¼åå
åº |
| | | * |
| | | * å表页跳转æ¶ä¼ `uni.setStorageSync('xxxDetail', row)`ï¼ä½å表è¡åªæå表ååæ®µï¼ |
| | | * éä»¶ãå表ã彿¡£æ
åµè¿äºåªæè¯¦æ
æ¥å£ææãè¥ç¼è¾é¡µå
读ç¼åå°±å填表åï¼ |
| | | * æäº¤æ¶ä¼æç¼ºå¤±çéä»¶ä¸å¹¶æ¸
ç©ºï¼æ°æ®ä¸¢å¤±ï¼ã |
| | | * |
| | | * æä»¥ç»ä¸èµ°ãå
请æ±è¯¦æ
æ¥å£ï¼å¤±è´¥æ¶æç¨ç¼åå
åºãï¼ç¼ååªç¨äºæ¥å£ä¸å¯ç¨æ¶çé级ã |
| | | */ |
| | | |
| | | /** |
| | | * @param {object} options |
| | | * @param {string} options.cacheKey å表页åå
¥ç storage key |
| | | * @param {string} options.id å½åè®°å½ id |
| | | * @param {Function} options.fetcher æ¥æ¶ idãè¿å Promise ç详æ
æ¥å£ |
| | | * @returns {Promise<object|null>} 详æ
对象ï¼åä¸å°è¿å null |
| | | */ |
| | | export async function loadDetailData({ cacheKey, id, fetcher }) { |
| | | const cached = cacheKey ? uni.getStorageSync(cacheKey) : null; |
| | | const fallback = |
| | | cached && String(cached.id) === String(id) ? cached : null; |
| | | |
| | | try { |
| | | const res = await fetcher(id); |
| | | return res?.data || fallback || null; |
| | | } catch (error) { |
| | | if (fallback) return fallback; |
| | | throw error; |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * éä»¶åæ®µå
¼å®¹å·¥å
· |
| | | * |
| | | * åç«¯åæ¨¡åçéä»¶åæ®µåä¸å½¢æä¸ç»ä¸ï¼ |
| | | * 对象æ°ç»ï¼storageBlobVOs(åæ¾) / attachmentList / fileListï¼é¡¹å« url||previewURL||fileUrl||path |
| | | * éå·å符串ï¼attachmentUrls / attachmentIds |
| | | * 页é¢ç»ä¸è½¬æ [{url}] äº¤ç» CommonUploadï¼æäº¤æ¶åæå模å约å®è½¬åã |
| | | */ |
| | | import config from "@/config"; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | /** |
| | | * ä»»æå½¢æçéä»¶æ°æ® â 对象æ°ç»ï¼æ¯é¡¹ä¿è¯æå¯æ¸²æç urlï¼ |
| | | */ |
| | | export function toFileList(source) { |
| | | if (!source) return []; |
| | | if (Array.isArray(source)) { |
| | | return source |
| | | .map(item => { |
| | | if (typeof item === "string") return item ? { url: item } : null; |
| | | const url = item?.url || item?.previewURL || item?.fileUrl || item?.path; |
| | | return url ? { ...item, url } : null; |
| | | }) |
| | | .filter(Boolean); |
| | | } |
| | | if (typeof source === "string") { |
| | | return source |
| | | .split(",") |
| | | .map(part => part.trim()) |
| | | .filter(Boolean) |
| | | .map(url => ({ url })); |
| | | } |
| | | return []; |
| | | } |
| | | |
| | | /** |
| | | * 对象æ°ç» / å符串æ°ç» â éå·åéå符串ï¼attachmentUrls è¿ç±»åç«¯åæ®µè¦çå½¢æï¼ |
| | | */ |
| | | export function toUrlString(fileList) { |
| | | return (fileList || []) |
| | | .map(item => |
| | | typeof item === "string" ? item : item?.url || item?.previewURL || "" |
| | | ) |
| | | .filter(Boolean) |
| | | .join(","); |
| | | } |
| | | |
| | | /** |
| | | * éä»¶ url â å¯ä¸è½½ç宿´å°å |
| | | * éä»¶æ¥å£åçå¸¸æ¯ /profile/upload/... è¿ç±»å端ç¸å¯¹è·¯å¾ï¼ç´æ¥ä¸¢ç» uni.downloadFile ä¼å¤±è´¥ï¼ |
| | | * æç®¡ç端 getRealUrl çåæ³è¡¥ä¸ baseUrlã |
| | | */ |
| | | export function resolveFileUrl(url) { |
| | | const target = String(url ?? "").trim(); |
| | | if (!target) return ""; |
| | | if (/^https?:\/\//i.test(target)) return target; |
| | | return `${config.baseUrl}${target.startsWith("/") ? "" : "/"}${target}`; |
| | | } |
| | | |
| | | /** |
| | | * æå¼éä»¶ï¼request.ts æªéä¼ responseTypeï¼blob ä¸è½½ä¸å¯ç¨ï¼ |
| | | * åªè½ downloadFile å°ä¸´æ¶æä»¶å openDocumentã |
| | | */ |
| | | export function openFile(url) { |
| | | const target = resolveFileUrl(url); |
| | | if (!target) { |
| | | uni.showToast({ title: "æä»¶å°åæ æ", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.showLoading({ title: "æå¼ä¸...", mask: true }); |
| | | uni.downloadFile({ |
| | | url: target, |
| | | header: { Authorization: "Bearer " + getToken() }, |
| | | success: res => { |
| | | if (res.statusCode !== 200) { |
| | | uni.showToast({ title: "æä»¶ä¸è½½å¤±è´¥", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.openDocument({ |
| | | filePath: res.tempFilePath, |
| | | showMenu: true, |
| | | fail: () => uni.showToast({ title: "æä¸æ¯æé¢è§è¯¥æä»¶", icon: "none" }), |
| | | }); |
| | | }, |
| | | fail: () => uni.showToast({ title: "æä»¶ä¸è½½å¤±è´¥", icon: "none" }), |
| | | complete: () => uni.hideLoading(), |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * åç«¯çææä»¶ï¼PDF/Wordï¼å¹¶ä¸è½½æå¼ã |
| | | * |
| | | * è¿ç±»æ¥å£æ¯ POST + bodyï¼è uni.downloadFile åªæ¯æ GETï¼æä»¥åªè½å
ç¨ |
| | | * uni.request æ¿ arraybufferï¼åè½å° USER_DATA_PATH å openDocumentã |
| | | * uni.getFileSystemManager å¨ H5 ä¸åå¨ï¼H5 ä¸ä¼æç¤ºä¸æ¯æã |
| | | * |
| | | * @param {string} path æ¥å£è·¯å¾ï¼å¦ /quality/qualityInspect/downPdf |
| | | * @param {object} data 请æ±ä½ |
| | | * @param {string} fileName è½çæä»¶åï¼å«åç¼ï¼ |
| | | */ |
| | | export function downloadFileByPost(path, data = {}, fileName = "download.pdf") { |
| | | uni.showLoading({ title: "çæä¸...", mask: true }); |
| | | return new Promise((resolve, reject) => { |
| | | uni.request({ |
| | | url: `${config.baseUrl}${path}`, |
| | | method: "POST", |
| | | data, |
| | | responseType: "arraybuffer", |
| | | header: { |
| | | Authorization: "Bearer " + getToken(), |
| | | "Content-Type": "application/json", |
| | | }, |
| | | success: res => { |
| | | const contentType = String( |
| | | res.header?.["Content-Type"] || res.header?.["content-type"] || "" |
| | | ); |
| | | // å端åºéæ¶åçæ¯ JSONï¼code/msgï¼ï¼ç
§åæ ·åæ pdf ä¼å¾å°ä¸ä¸ªæä¸å¼çæä»¶ |
| | | if (res.statusCode !== 200 || contentType.includes("application/json")) { |
| | | uni.showToast({ title: "çææä»¶å¤±è´¥", icon: "none" }); |
| | | reject(new Error("çææä»¶å¤±è´¥")); |
| | | return; |
| | | } |
| | | const fs = uni.getFileSystemManager && uni.getFileSystemManager(); |
| | | if (!fs) { |
| | | uni.showToast({ title: "å½åç¯å¢ä¸æ¯æä¸è½½æä»¶", icon: "none" }); |
| | | reject(new Error("unsupported")); |
| | | return; |
| | | } |
| | | const filePath = `${uni.env.USER_DATA_PATH}/${fileName}`; |
| | | fs.writeFile({ |
| | | filePath, |
| | | data: res.data, |
| | | encoding: "binary", |
| | | success: () => { |
| | | uni.openDocument({ |
| | | filePath, |
| | | showMenu: true, |
| | | fail: () => |
| | | uni.showToast({ title: "æä¸æ¯æé¢è§è¯¥æä»¶", icon: "none" }), |
| | | }); |
| | | resolve(filePath); |
| | | }, |
| | | fail: () => { |
| | | uni.showToast({ title: "æä»¶ä¿å失败", icon: "none" }); |
| | | reject(new Error("æä»¶ä¿å失败")); |
| | | }, |
| | | }); |
| | | }, |
| | | fail: () => { |
| | | uni.showToast({ title: "çææä»¶å¤±è´¥", icon: "none" }); |
| | | reject(new Error("çææä»¶å¤±è´¥")); |
| | | }, |
| | | complete: () => uni.hideLoading(), |
| | | }); |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * å表ååºè§£å
å·¥å
· |
| | | * |
| | | * å端å controller çå页è¿åç»æä¸ç»ä¸ï¼å®æµè³å°æè¿å ç§ï¼ |
| | | * { code, data: { records, total } } |
| | | * { code, data: [ ... ] } ï¼çº¯æ°ç»ï¼ |
| | | * { code, records, total } ï¼ruoyi 飿 ¼ï¼records å¨å¤å±ï¼ |
| | | * { code, rows, total } ï¼ruoyi 飿 ¼ï¼ |
| | | * éä¸å¨è¿éå
åºï¼é¿å
æ¯ä¸ªé¡µé¢ååä¸é `res?.data?.records || res?.records || []`ã |
| | | */ |
| | | |
| | | function pickArray(source) { |
| | | if (!source) return null; |
| | | if (Array.isArray(source)) return source; |
| | | const candidates = ["records", "list", "rows", "data", "content", "items"]; |
| | | for (const key of candidates) { |
| | | if (Array.isArray(source[key])) return source[key]; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * ä»ä»»æç»æçå页ååºéååºå表æ°ç»ï¼åä¸å°è¿å [] |
| | | */ |
| | | export function unwrapList(res) { |
| | | if (!res) return []; |
| | | return pickArray(res.data) || pickArray(res) || []; |
| | | } |
| | | |
| | | /** |
| | | * ååºæ»æ¡æ°ï¼åä¸å°è¿å 0 |
| | | */ |
| | | export function unwrapTotal(res) { |
| | | if (!res) return 0; |
| | | const outer = Number(res.total); |
| | | if (Number.isFinite(outer) && outer > 0) return outer; |
| | | const inner = Number(res.data?.total); |
| | | if (Number.isFinite(inner) && inner > 0) return inner; |
| | | return 0; |
| | | } |
| | | |
| | | /** |
| | | * ååºåæ¡è¯¦æ
ï¼åä¸å°è¿å null |
| | | */ |
| | | export function unwrapData(res) { |
| | | if (!res) return null; |
| | | return res.data ?? null; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ç¼´è´¹å¨æä¸ç¼´è´¹æ¹å¼é项 |
| | | * |
| | | * 宽带ä¸é信管çãæ¿å±ç§èµç®¡çå
±ç¨åä¸å¥åå
¸ï¼ |
| | | * ç¼´è´¹å¨æ paymentCycle ââ å Numberï¼1 æä» / 3 å£ä» / 6 åå¹´ä» / 12 å¹´ä»ï¼ï¼å端ä¹ç¨å®æ¨ç®ä¸æ¬¡ç¼´è´¹æ¥æ |
| | | * ç¼´è´¹æ¹å¼ paymentMethod ââ å䏿å符串ï¼ç°é / é¶è¡è½¬è´¦ / æ¯ä»å® / å¾®ä¿¡ï¼ |
| | | */ |
| | | |
| | | export const PAYMENT_CYCLE_OPTIONS = [ |
| | | { name: "æä»", value: 1 }, |
| | | { name: "å£ä»", value: 3 }, |
| | | { name: "åå¹´ä»", value: 6 }, |
| | | { name: "å¹´ä»", value: 12 }, |
| | | ]; |
| | | |
| | | export const PAYMENT_METHOD_OPTIONS = [ |
| | | { name: "ç°é", value: "ç°é" }, |
| | | { name: "é¶è¡è½¬è´¦", value: "é¶è¡è½¬è´¦" }, |
| | | { name: "æ¯ä»å®", value: "æ¯ä»å®" }, |
| | | { name: "微信", value: "微信" }, |
| | | ]; |
| | | |
| | | const CYCLE_TEXT = { |
| | | 1: "æä»", |
| | | 3: "å£ä»", |
| | | 6: "åå¹´ä»", |
| | | 12: "å¹´ä»", |
| | | }; |
| | | |
| | | export function cycleText(cycle) { |
| | | if (cycle === null || cycle === undefined || cycle === "") return "-"; |
| | | return CYCLE_TEXT[Number(cycle)] || "-"; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * 跨模åãåèµ·ååå½å
¥ãçé¢å¡«åæ° |
| | | * |
| | | * æææ 䏿 åï¼éæ±ä¸.6ï¼éè¦å¸¦ç项ç®ä¿¡æ¯è·³å°éå®å°è´¦æ°å¢é¡µã |
| | | * ææºç«¯éå®å°è´¦æ°å¢èµ° pages/sales/salesAccount/detail.vueï¼ |
| | | * è¯¥é¡µå¨ onMounted è¯»åæ¬ key 宿项ç®/客æ·åå¡«ã |
| | | * 项ç®å段åä¸é¡¹ç®ç®¡çåç«¯ä¿æä¸è´ï¼title=项ç®åç§°ãclientId/clientName=客æ·ï¼ï¼ |
| | | * å¨ setPrefillProject éç»ä¸å½ä¸åï¼è°ç¨æ¹ä¼ åå§è¡å³å¯ã |
| | | */ |
| | | |
| | | export const PREFILL_PROJECT_KEY = "prefillProject"; |
| | | |
| | | export function setPrefillProject(project) { |
| | | uni.setStorageSync(PREFILL_PROJECT_KEY, { |
| | | projectId: project?.projectId ?? project?.id ?? "", |
| | | projectName: project?.projectName ?? project?.title ?? "", |
| | | customerId: project?.customerId ?? project?.clientId ?? "", |
| | | customerName: project?.customerName ?? project?.clientName ?? "", |
| | | }); |
| | | } |
| | | |
| | | export function takePrefillProject() { |
| | | const data = uni.getStorageSync(PREFILL_PROJECT_KEY); |
| | | uni.removeStorageSync(PREFILL_PROJECT_KEY); |
| | | return data || null; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * 项ç®ä¸æé项 |
| | | * |
| | | * æææ ãè¯è®¼ãé宿¥ä»·ãéå®å°è´¦çå¤å¤éè¦ãéæ©æå±é¡¹ç®ãï¼ |
| | | * å端项ç®ç®¡çè¿åçåæ®µåä¸ç»ä¸ï¼é¡¹ç®åç§°å¨ title ä¸ï¼id å¨ id ä¸ï¼ï¼ |
| | | * ç»ä¸å¨è¿éå½ä¸åæ FormPicker 认è¯ç {name, value}ï¼å¹¶ä¿ç raw ä¾å填使ç¨ã |
| | | */ |
| | | import { listProject } from "@/api/projectManagement/project"; |
| | | |
| | | const pickProjectName = project => |
| | | project?.title ?? project?.projectName ?? project?.name ?? ""; |
| | | |
| | | export function normalizeProjectOptions(records = []) { |
| | | return (records || []).map(project => ({ |
| | | name: pickProjectName(project), |
| | | value: project.id ?? project.projectId, |
| | | raw: project, |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * æå项ç®å表ï¼ä¸åé¡µï¼ |
| | | * @returns {Promise<Array<{name, value, raw}>>} |
| | | */ |
| | | export async function loadProjectOptions(params = {}) { |
| | | const res = await listProject({ current: -1, size: -1, ...params }); |
| | | const records = res?.data?.records || res?.records || res?.data || []; |
| | | return normalizeProjectOptions(Array.isArray(records) ? records : []); |
| | | } |
| | | |
| | | export { pickProjectName }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * å°æé¢è¦è®¡ç®å·¥å
· |
| | | * |
| | | * åä¸å¡æ¨¡åçé¢è¦éå¼ä¸åï¼ |
| | | * 车è¾å¹´æ£/ä¿é©ãæ¿å±ç§èµååãè®¾å¤æ ¡åãæ¥è¡¨æææ â 30 天 |
| | | * 宽带ä¸éä¿¡ä¸æ¬¡ç¼´è´¹ â 15 天 |
| | | * éå®å°è´¦ååå件约å®åä¼ æ¥æ â æå N 天ï¼ç±é¡¹ç®ç®¡çé
ç½®å³å®ï¼ |
| | | * ç»ä¸ç¨ warnDays ä¼ å
¥ï¼é¿å
å页é¢ååä¸å¥æ¥æå·®é»è¾ã |
| | | */ |
| | | |
| | | const MS_PER_DAY = 24 * 60 * 60 * 1000; |
| | | |
| | | /** |
| | | * è§£æå端è¿åçæ¥æï¼å
¼å®¹æ¶é´æ³ / Date / 'YYYY-MM-DD' / 'YYYY/MM/DD' / 'YYYY-MM-DD HH:mm:ss' |
| | | */ |
| | | export function parseDate(value) { |
| | | if (value === undefined || value === null || value === "") return null; |
| | | if (value instanceof Date) { |
| | | return Number.isNaN(value.getTime()) ? null : new Date(value.getTime()); |
| | | } |
| | | if (typeof value === "number") { |
| | | const d = new Date(value); |
| | | return Number.isNaN(d.getTime()) ? null : d; |
| | | } |
| | | const matched = String(value) |
| | | .trim() |
| | | .replace(/\//g, "-") |
| | | .replace("T", " ") |
| | | .match(/^(\d{4})-(\d{1,2})-(\d{1,2})/); |
| | | if (!matched) return null; |
| | | const date = new Date( |
| | | Number(matched[1]), |
| | | Number(matched[2]) - 1, |
| | | Number(matched[3]) |
| | | ); |
| | | return Number.isNaN(date.getTime()) ? null : date; |
| | | } |
| | | |
| | | /** |
| | | * è·ç¦»å°ææ¥çå©ä½å¤©æ° |
| | | * æ£æ°=è¿æ N 天ï¼0=ä»å¤©å°æï¼è´æ°=已龿 N å¤©ï¼æ æ³è§£æè¿å null |
| | | */ |
| | | export function remainDays(dateStr) { |
| | | const target = parseDate(dateStr); |
| | | if (!target) return null; |
| | | const today = new Date(); |
| | | today.setHours(0, 0, 0, 0); |
| | | target.setHours(0, 0, 0, 0); |
| | | return Math.round((target.getTime() - today.getTime()) / MS_PER_DAY); |
| | | } |
| | | |
| | | /** |
| | | * é¢è¦çº§å«ï¼overdue 已龿 / warning å³å°å°æ / normal æ£å¸¸ / none æ æ¥æ |
| | | */ |
| | | export function warnLevel(dateStr, warnDays = 30) { |
| | | const days = remainDays(dateStr); |
| | | if (days === null) return "none"; |
| | | if (days < 0) return "overdue"; |
| | | if (days <= warnDays) return "warning"; |
| | | return "normal"; |
| | | } |
| | | |
| | | export function warnColor(level) { |
| | | const colors = { |
| | | overdue: "#f56c6c", |
| | | warning: "#ff9900", |
| | | normal: "#19be6b", |
| | | none: "#909399", |
| | | }; |
| | | return colors[level] || colors.none; |
| | | } |
| | | |
| | | export function warnText(level, days) { |
| | | if (level === "overdue") return `已龿${Math.abs(days)}天`; |
| | | if (level === "warning") return days === 0 ? "ä»å¤©å°æ" : `å³å°å°æ(${days}天)`; |
| | | if (level === "normal") return "æ£å¸¸"; |
| | | return ""; |
| | | } |
| | | |
| | | /** |
| | | * 䏿¬¡æ¿å°æ¸²æé¢è¦æ è®°æéçå
¨é¨ä¿¡æ¯ |
| | | * @returns {{days: number|null, level: string, color: string, text: string}} |
| | | */ |
| | | export function getWarnInfo(dateStr, warnDays = 30) { |
| | | const days = remainDays(dateStr); |
| | | const level = warnLevel(dateStr, warnDays); |
| | | return { |
| | | days, |
| | | level, |
| | | color: warnColor(level), |
| | | text: warnText(level, days), |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * æ¯å¦å·²é¾ææå³å°å°æï¼ç¨äºå表çéãé«äº®ï¼ |
| | | */ |
| | | export function isExpiringSoon(dateStr, warnDays = 30) { |
| | | const level = warnLevel(dateStr, warnDays); |
| | | return level === "overdue" || level === "warning"; |
| | | } |
| | | |
| | | /** |
| | | * æå¤©/ææ¨ç®æ¥æï¼ç¨äº"䏿¬¡ç¼´è´¹æ¥æ = å®è£
æ¥æ + ç¼´è´¹å¨ææ"è¿ç±»èªå¨è®¡ç® |
| | | * @returns {string} 'YYYY-MM-DD'ï¼æ æ³è®¡ç®è¿å '' |
| | | */ |
| | | export function addMonths(dateStr, months) { |
| | | const base = parseDate(dateStr); |
| | | if (!base || !Number.isFinite(Number(months))) return ""; |
| | | const day = base.getDate(); |
| | | base.setDate(1); |
| | | base.setMonth(base.getMonth() + Number(months)); |
| | | // å¤ç 1/31 + 1 æè¿ç±»æº¢åºï¼åéå°å½ææåä¸å¤© |
| | | const lastDay = new Date(base.getFullYear(), base.getMonth() + 1, 0).getDate(); |
| | | base.setDate(Math.min(day, lastDay)); |
| | | const pad = n => String(n).padStart(2, "0"); |
| | | return `${base.getFullYear()}-${pad(base.getMonth() + 1)}-${pad(base.getDate())}`; |
| | | } |
| | | |
| | | /** |
| | | * çæ [start, end] ä¹é´çæä»½å表ï¼å«é¦å°¾ï¼ï¼ç¨äºæ¿å±ç§èµçæåº¦è´¹ç¨å表 |
| | | * @returns {string[]} ['2026-01', '2026-02', ...] |
| | | */ |
| | | export function monthRange(startStr, endStr) { |
| | | const start = parseDate(startStr); |
| | | const end = parseDate(endStr); |
| | | if (!start || !end || end < start) return []; |
| | | const months = []; |
| | | const cursor = new Date(start.getFullYear(), start.getMonth(), 1); |
| | | const last = new Date(end.getFullYear(), end.getMonth(), 1); |
| | | while (cursor <= last && months.length < 600) { |
| | | const pad = n => String(n).padStart(2, "0"); |
| | | months.push(`${cursor.getFullYear()}-${pad(cursor.getMonth() + 1)}`); |
| | | cursor.setMonth(cursor.getMonth() + 1); |
| | | } |
| | | return months; |
| | | } |