已添加7个文件
已删除8个文件
已重命名1个文件
已修改51个文件
| | |
| | | favicon: '/logos/TEST.ico', |
| | | apiUrl: 'http://36.213.90.123:9501/admin-api', |
| | | }, |
| | | JHHG: { |
| | | name: 'å®å¤ä¹æ³åå·¥', |
| | | logo: '/logos/TEST.png', |
| | | favicon: '/logos/TEST.ico', |
| | | apiUrl: 'http://42.63.70.90:9001/admin-api', |
| | | }, |
| | | YZFX: { |
| | | name: 'å®å¤è±æ³½é²é', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://42.63.70.231:9001/admin-api', |
| | | }, |
| | | WTXC: { |
| | | name: 'å®å¤ä¸éæ°æ', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://42.63.71.140:9001/admin-api', |
| | | }, |
| | | YSJX: { |
| | | name: 'å®å¤çå±±æºæ¢°', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://42.63.71.140:9004/admin-api', |
| | | }, |
| | | XLZB: { |
| | | name: 'å®å¤ååè£
å¤', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://42.63.71.140:9004/admin-api', |
| | | }, |
| | | ZSJC: { |
| | | name: 'å®å¤ä¸ç建æç§ææéå
¬å¸', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://116.172.73.239:9001/admin-api', |
| | | }, |
| | | KYZN: { |
| | | name: 'åº·ææºè½', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9006/admin-api', |
| | | }, |
| | | CHDL: { |
| | | name: 'é¿è¾çµå', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9004/admin-api', |
| | | }, |
| | | HM: { |
| | | name: 'æ ç§', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9013/admin-api', |
| | | }, |
| | | TRT: { |
| | | name: 'åéèç©çº¿ç¼', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9015/admin-api', |
| | | }, |
| | | HSC: { |
| | | name: '好水å·é£åæéå
¬å¸', |
| | | logo: '/logos/YZFXLogo.png', |
| | | favicon: '/logos/YZFXLogo.ico', |
| | | apiUrl: 'http://36.213.150.237:9540/admin-api', |
| | | }, |
| | | }; |
| | |
| | | remark?: string; |
| | | } |
| | | |
| | | /** å·¥åæµè½¬è®°å½ */ |
| | | export interface FlowRecord { |
| | | id?: number; |
| | | ticketId?: number; |
| | | action?: string; |
| | | actionName?: string; |
| | | fromStatus?: number; |
| | | fromStatusName?: string; |
| | | toStatus?: number; |
| | | toStatusName?: string; |
| | | content?: string; |
| | | operatorId?: number; |
| | | operatorName?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** å®åå·¥å */ |
| | | export interface Ticket { |
| | | id?: number; |
| | |
| | | issueType?: number; |
| | | issueCategory?: string; |
| | | issueDescription?: string; |
| | | /** 客æ·è¯æ±åé¦æè¿° */ |
| | | feedbackDescription?: string; |
| | | /** 客æ·è¯æ±éä»¶ blobId å表ï¼ä¿åæ¶ä¼ å
¥ï¼ */ |
| | | feedbackBlobIds?: number[]; |
| | | /** 客æ·è¯æ±éä»¶å表ï¼è¯¦æ
æ¶è¿åï¼ */ |
| | | feedbackAttachmentList?: { id: number; name?: string; url?: string }[]; |
| | | /** å¤çç»ææè¿° */ |
| | | resultDescription?: string; |
| | | /** å¤çç»æéä»¶ blobId å表ï¼ä¿åæ¶ä¼ å
¥ï¼ */ |
| | | resultBlobIds?: number[]; |
| | | /** å¤çç»æéä»¶å表ï¼è¯¦æ
æ¶è¿åï¼ */ |
| | | resultAttachmentList?: { id: number; name?: string; url?: string }[]; |
| | | severityLevel?: number; |
| | | status?: number; |
| | | ownerUserId?: number; |
| | |
| | | updateTime?: string; |
| | | /** éä»¶ blobId å表ï¼ä¿åæ¶ä¼ å
¥ï¼ */ |
| | | blobIds?: number[]; |
| | | /** éä»¶å表ï¼è¯¦æ
æ¶è¿åï¼ */ |
| | | /** éç¨éä»¶å表ï¼å岿°æ®ï¼ */ |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; |
| | | /** æµè½¬è®°å½å表ï¼è¯¦æ
æ¶è¿åï¼ææ¶é´æ£åºï¼ */ |
| | | flowList?: FlowRecord[]; |
| | | items?: TicketItem[]; |
| | | } |
| | | } |
| | |
| | | '/aftersales/ticket/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** åé
å¤ç人 */ |
| | | export function assignTicket(id: number, handlerUserId: number) { |
| | | return requestClient.post<boolean>('/aftersales/ticket/assign', null, { |
| | | params: { id, handlerUserId }, |
| | | }); |
| | | } |
| | | |
| | | /** é®é¢å¤å® */ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | export namespace OaNoticeApi { |
| | | export interface NoticeVO { |
| | |
| | | publishTime: string; |
| | | createTime: string; |
| | | userIds?: number[]; |
| | | /** éä»¶å表ï¼è¯¦æ
åæ¾ï¼ */ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; |
| | | } |
| | | |
| | | export interface NoticeSaveReqVO { |
| | |
| | | sendScope: number; |
| | | isTop?: number; |
| | | userIds?: number[]; |
| | | /** éä»¶ blobId å表 */ |
| | | blobIds?: number[]; |
| | | } |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace CrmBidBondApi { |
| | | /** ææ ä¿è¯é */ |
| | | export interface BidBond { |
| | | id?: number; |
| | | no?: string; |
| | | customerId?: number; |
| | | customerName?: string; |
| | | projectName?: string; |
| | | tenderNo?: string; |
| | | saleOrderId?: number; |
| | | saleOrderNo?: string; |
| | | saleOrderAmount?: number; |
| | | bondAmount?: number; |
| | | payDate?: string; |
| | | /** 䏿 ç»æï¼0 å¾
弿 ã1 已䏿 ã2 æªä¸æ */ |
| | | bidResult?: number; |
| | | /** éåç¶æï¼0 æªéåã1 é¨åéåã2 å·²éåã3 æ ééå */ |
| | | refundStatus?: number; |
| | | refundAmount?: number; |
| | | refundDate?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** å¯å
³èçéå®è®¢åï¼éå®å°è´¦ï¼ */ |
| | | export interface SaleOrderOption { |
| | | id: number; |
| | | no: string; |
| | | customerId: number; |
| | | status: number; |
| | | statusName?: string; |
| | | totalPrice?: number; |
| | | contractNo?: string; |
| | | } |
| | | } |
| | | |
| | | /** å页æ¥è¯¢ææ ä¿è¯é */ |
| | | export function getBidBondPage(params: PageParam) { |
| | | return requestClient.get<PageResult<CrmBidBondApi.BidBond>>( |
| | | '/crm/bid-bond/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢ææ ä¿è¯é详æ
*/ |
| | | export function getBidBond(id: number) { |
| | | return requestClient.get<CrmBidBondApi.BidBond>( |
| | | `/crm/bid-bond/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** åå»ºææ ä¿è¯é */ |
| | | export function createBidBond(data: CrmBidBondApi.BidBond) { |
| | | return requestClient.post<number>('/crm/bid-bond/create', data); |
| | | } |
| | | |
| | | /** æ´æ°ææ ä¿è¯é */ |
| | | export function updateBidBond(data: CrmBidBondApi.BidBond) { |
| | | return requestClient.put('/crm/bid-bond/update', data); |
| | | } |
| | | |
| | | /** å 餿æ ä¿è¯é */ |
| | | export function deleteBidBond(id: number) { |
| | | return requestClient.delete('/crm/bid-bond/delete', { params: { id } }); |
| | | } |
| | | |
| | | /** æ¥è¯¢å®¢æ·ä¸å¯å
³èçéå®è®¢åï¼ä»
å·²å®¡æ ¸ï¼ */ |
| | | export function getSaleOrderOptions(customerId: number) { |
| | | return requestClient.get<CrmBidBondApi.SaleOrderOption[]>( |
| | | '/crm/bid-bond/sale-order-list', |
| | | { params: { customerId } }, |
| | | ); |
| | | } |
| | |
| | | return requestClient.put('/erp/purchase-request/reset-status', null, { params: { id } }); |
| | | } |
| | | |
| | | /** çæéè´è®¢å */ |
| | | export function generatePurchaseOrder(id: number, supplierId: number) { |
| | | /** çæéè´è®¢åï¼ä¾åºåé»è®¤æ²¿ç¨éè´ç³è¯·ä¸çéæ©ï¼æ ééå¤ä¼ å
¥ï¼ */ |
| | | export function generatePurchaseOrder(id: number, supplierId?: number) { |
| | | return requestClient.post<number>('/erp/purchase-request/generate-order', null, { params: { id, supplierId } }); |
| | | } |
| | | |
| | |
| | | invoiceNo?: string; |
| | | } |
| | | |
| | | export interface SupplierSummary { |
| | | supplierId?: number; |
| | | supplierCode?: string; |
| | | supplierName?: string; |
| | | orderCount?: number; |
| | | invoiceCount?: number; |
| | | paymentCount?: number; |
| | | totalPurchaseAmount?: number; |
| | | totalInvoiceAmount?: number; |
| | | totalPaymentAmount?: number; |
| | | unpaidAmount?: number; |
| | | } |
| | | |
| | | export interface Statement { |
| | | supplierId?: number; |
| | | supplierName?: string; |
| | |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢åä¾åºå徿¥æ±æ»ï¼å·¦ä¾§ä¾åºåæ»è§ï¼ï¼å£å¾ä¸å个ä¾åºå详æ
ä¸è´ */ |
| | | export function getSupplierStatementSummaryList(params: { |
| | | beginTime?: string; |
| | | endTime?: string; |
| | | includeUnapprovedOrder?: boolean; |
| | | }) { |
| | | return requestClient.get<ErpSupplierStatementApi.SupplierSummary[]>( |
| | | '/erp/supplier-statement/summary-list', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢ä¾åºå徿¥å¯¹è´¦ */ |
| | | export function getSupplierStatement(params: { |
| | | supplierId: number; |
| | |
| | | name: string; |
| | | deptName?: string; |
| | | postName?: string; |
| | | /** åå·¥ç¶æï¼1 å¨èã2 è¯ç¨ã3 离è */ |
| | | employeeStatus?: number; |
| | | } |
| | | |
| | | /** å工导å
¥ç»æ */ |
| | |
| | | id?: number; |
| | | employeeId?: number; |
| | | employeeName?: string; |
| | | deptName?: string; |
| | | userId?: number; |
| | | userName?: string; |
| | | postName?: string; |
| | | deptName?: string; |
| | | resignationType?: number; |
| | | expectedDate?: string; |
| | | reason?: string; |
| | | handoverEmployeeId?: number; |
| | | handoverUserId?: number; |
| | | handoverUserName?: string; |
| | | handoverStatus?: number; |
| | | handoverDataStatus?: number; |
| | | handoverDataStatusName?: string; |
| | | actualDate?: string; |
| | | blobIds?: number[]; |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; |
| | | status?: number; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace HrmStatisticsApi { |
| | | /** åå¸é¡¹ */ |
| | | export interface Item { |
| | | name: string; |
| | | value: number; |
| | | } |
| | | |
| | | /** é¨é¨å·¥é¾ç»è®¡ */ |
| | | export interface DeptTenure { |
| | | deptId?: number; |
| | | deptName: string; |
| | | count: number; |
| | | averageTenureYears: number; |
| | | } |
| | | |
| | | /** åå·¥å·¥é¾æç» */ |
| | | export interface TenureEmployee { |
| | | id: number; |
| | | name: string; |
| | | deptName: string; |
| | | hireDate: string; |
| | | tenureYears: number; |
| | | } |
| | | |
| | | /** å
¥è / 离èè¶å¿é¡¹ */ |
| | | export interface Trend { |
| | | month: string; // yyyy-MM |
| | | hireCount: number; // å
¥èäººæ° |
| | | resignCount: number; // 离èäººæ° |
| | | } |
| | | |
| | | /** å工人åç»è®¡ */ |
| | | export interface EmployeeStatistics { |
| | | totalCount: number; // å¨å²äººæ°ï¼å¨è + è¯ç¨ï¼ |
| | | activeCount: number; // å¨èäººæ° |
| | | probationCount: number; // è¯ç¨äººæ° |
| | | resignedCount: number; // 离èäººæ° |
| | | averageTenureYears: number; // å¹³åå·¥é¾ï¼å¹´ï¼ |
| | | averageAge: number; // å¹³åå¹´é¾ï¼å²ï¼ |
| | | thisYearHireCount: number; // æ¬å¹´å
¥èäººæ° |
| | | thisYearResignCount: number; // æ¬å¹´ç¦»èäººæ° |
| | | tenureDistribution: Item[]; // å·¥é¾åå¸ |
| | | ageDistribution: Item[]; // å¹´é¾åå¸ |
| | | educationDistribution: Item[]; // å¦ååå¸ |
| | | genderDistribution: Item[]; // æ§å«åå¸ |
| | | statusDistribution: Item[]; // 人åç¶æææï¼å¨è / è¯ç¨ / 离èï¼ |
| | | deptDistribution: Item[]; // é¨é¨äººæ°åå¸ |
| | | postDistribution: Item[]; // å²ä½åå¸ |
| | | marriageDistribution: Item[]; // å©å§»ç¶åµåå¸ |
| | | politicalDistribution: Item[]; // æ¿æ²»é¢è²åå¸ |
| | | hireResignTrend: Trend[]; // è¿ 12 个æå
¥è / 离èè¶å¿ |
| | | contractWarning: Item[]; // ååå°æé¢è¦ |
| | | noContractCount: number; // å¨å²æªç¾ææååäººæ° |
| | | deptTenureList: DeptTenure[]; // é¨é¨å·¥é¾ç»è®¡ |
| | | topTenureEmployees: TenureEmployee[]; // å·¥é¾æè¡ |
| | | } |
| | | } |
| | | |
| | | /** è·å¾å工人åç»è®¡ */ |
| | | export function getTenureStatistics() { |
| | | return requestClient.get<HrmStatisticsApi.EmployeeStatistics>( |
| | | '/hrm/employee/tenure-statistics', |
| | | ); |
| | | } |
| | |
| | | id?: number; // å
³èç¼å· |
| | | planId?: number; // æ¹æ¡ç¼å· |
| | | machineryId?: number; // 设å¤ç¼å· |
| | | machineryCode?: string; // 设å¤ç¼ç |
| | | machineryCode?: string; // 设å¤ç¼å· |
| | | machineryName?: string; // 设å¤åç§° |
| | | machineryBrand?: string; // åç |
| | | machinerySpecification?: string; // è§æ ¼åå· |
| | |
| | | planId?: number; // ç¹æ£è®¡åç¼å· |
| | | planName?: string; // 计ååç§° |
| | | machineryId?: number; // 设å¤ç¼å· |
| | | machineryCode?: string; // 设å¤ç¼ç |
| | | machineryCode?: string; // 设å¤ç¼å· |
| | | machineryName?: string; // 设å¤åç§° |
| | | machineryBrand?: string; // åç |
| | | machinerySpecification?: string; // è§æ ¼åå· |
| | |
| | | /** MES 设å¤å°è´¦ */ |
| | | export interface Machinery { |
| | | id?: number; // 设å¤ç¼å· |
| | | code?: string; // 设å¤ç¼ç |
| | | code?: string; // 设å¤ç¼å· |
| | | name?: string; // 设å¤åç§° |
| | | brand?: string; // åç |
| | | specification?: string; // è§æ ¼åå· |
| | |
| | | |
| | | /** 设å¤å¯¼å
¥ç»æ */ |
| | | export interface MachineryImportRespVO { |
| | | createCodes?: string[]; // æ°å¢æåç设å¤ç¼ç |
| | | updateCodes?: string[]; // æ´æ°æåç设å¤ç¼ç |
| | | failureCodes?: Record<string, string>; // 导å
¥å¤±è´¥ç设å¤ç¼ç ååå |
| | | createCodes?: string[]; // æ°å¢æåç设å¤ç¼å· |
| | | updateCodes?: string[]; // æ´æ°æåç设å¤ç¼å· |
| | | failureCodes?: Record<string, string>; // 导å
¥å¤±è´¥ç设å¤ç¼å·ååå |
| | | } |
| | | } |
| | | |
| | |
| | | planId?: number; // 计åç¼å· |
| | | planName?: string; // 计ååç§° |
| | | machineryId?: number; // 设å¤ç¼å· |
| | | machineryCode?: string; // 设å¤ç¼ç |
| | | machineryCode?: string; // 设å¤ç¼å· |
| | | machineryName?: string; // 设å¤åç§° |
| | | machineryBrand?: string; // åç |
| | | machinerySpecification?: string; // è§æ ¼åå· |
| | |
| | | code?: string; // 维修订åç¼ç |
| | | name?: string; // 维修订ååç§° |
| | | machineryId?: number; // 设å¤ç¼å· |
| | | machineryCode?: string; // 设å¤ç¼ç |
| | | machineryCode?: string; // 设å¤ç¼å· |
| | | machineryName?: string; // 设å¤åç§° |
| | | machineryBrand?: string; // åç |
| | | machinerySpecification?: string; // è§æ ¼åå· |
| | |
| | | id?: number; // èµæºç¼å· |
| | | workstationId?: number; // å·¥ä½ç«ç¼å· |
| | | machineryId?: number; // 设å¤ç¼å· |
| | | machineryCode?: string; // 设å¤ç¼ç |
| | | machineryCode?: string; // 设å¤ç¼å· |
| | | machineryName?: string; // 设å¤åç§° |
| | | quantity?: number; // æ°é |
| | | remark?: string; // 夿³¨ |
| | |
| | | id?: number; // éç¥åç¼å· |
| | | code?: string; // éç¥åç¼å· |
| | | name?: string; // éç¥ååç§° |
| | | purchaseOrderId?: number; // éè´è®¢åç¼å· |
| | | purchaseOrderCode?: string; // éè´è®¢åç¼å· |
| | | purchaseOrderNo?: string; // éè´è®¢åå· |
| | | contractNo?: string; // éè´ååå· |
| | | vendorId?: number; // ä¾åºåç¼å· |
| | | vendorCode?: string; // ä¾åºåç¼ç |
| | | vendorName?: string; // ä¾åºååç§° |
| | |
| | | ); |
| | | } |
| | | |
| | | /** ä»å
³èçéè´è®¢å鿰另å
¥å¯éç©æï¼å¹çï¼å·²åå¨çç©æè¡ä¸ä¼éå¤å¸¦å
¥ï¼è¿åæ¬æ¬¡æ°å¢è¡æ°ï¼ */ |
| | | export function importReturnVendorPurchaseOrderItems(id: number) { |
| | | return requestClient.post<number>( |
| | | '/mes/wm/return-vendor/import-purchase-order-items', |
| | | null, |
| | | { params: { id } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ§è¡æ£è´§ */ |
| | | export function stockReturnVendor(id: number) { |
| | | return requestClient.put(`/mes/wm/return-vendor/stock?id=${id}`); |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | import type { SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | export namespace SystemNoticeApi { |
| | | /** å
¬åä¿¡æ¯ */ |
| | |
| | | remark: string; |
| | | creator?: string; |
| | | createTime?: Date; |
| | | /** éä»¶ blobId åè¡¨ï¼æäº¤æ¶ä½¿ç¨ï¼ */ |
| | | blobIds?: number[]; |
| | | /** éä»¶å表ï¼è¯¦æ
åæ¾ï¼ */ |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; |
| | | } |
| | | } |
| | | |
| | |
| | | }, |
| | | component: () => import('#/views/aftersales/ticket/detail.vue'), |
| | | }, |
| | | { |
| | | path: 'return/detail/:id', |
| | | name: 'AftersalesReturnDetail', |
| | | meta: { |
| | | title: 'éè´§ç³è¯·è¯¦æ
', |
| | | activePath: '/aftersales/return', |
| | | }, |
| | | component: () => import('#/views/aftersales/return/detail.vue'), |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | |
| | | }, |
| | | { |
| | | fieldName: 'feedbackDescription', |
| | | label: '客æ·è¯æ±åé¦', |
| | | label: '客æ·è¯æ±åé¦æè¿°', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-3', |
| | | componentProps: { placeholder: '请è¾å
¥å®¢æ·è¯æ±ååé¦æè¿°', rows: 3 }, |
| | | }, |
| | | { |
| | | fieldName: 'feedbackBlobIds', |
| | | label: '客æ·è¯æ±éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-3', |
| | | }, |
| | | { |
| | | fieldName: 'resultDescription', |
| | |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-3', |
| | | componentProps: { placeholder: '请è¾å
¥å¤çç»æ', rows: 3 }, |
| | | }, |
| | | { |
| | | fieldName: 'resultBlobIds', |
| | | label: 'å¤çç»æéä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-3', |
| | | }, |
| | | { |
| | | fieldName: 'issueType', |
| | |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | rows: 3, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | disabled: readonly, |
| | | }, |
| | | formItemClass: 'col-span-3', |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | import { useRoute, useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum } from '@vben/constants'; |
| | | import { AftersaleTicketStatusEnum } from '@vben/constants'; |
| | | |
| | | import { |
| | | Button, |
| | |
| | | Select, |
| | | Switch, |
| | | Tag, |
| | | Timeline, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | assignTicket, |
| | | cancelTicket, |
| | | classifyTicket, |
| | | closeTicket, |
| | |
| | | 2: 'è¿ç¨', |
| | | 3: 'å
¶ä»', |
| | | }; |
| | | |
| | | // åé
å¤ç人 |
| | | const showAssignInput = ref(false); |
| | | const assignUserId = ref<number>(); |
| | | |
| | | // é®é¢å¤å® |
| | | const showClassifyInput = ref(false); |
| | |
| | | await Promise.all([loadVisits(), loadTicket()]); |
| | | } |
| | | |
| | | function toggleAssignInput() { |
| | | showAssignInput.value = !showAssignInput.value; |
| | | assignUserId.value = undefined; |
| | | } |
| | | |
| | | async function handleAssign() { |
| | | if (!assignUserId.value) { |
| | | message.warning('请è¾å
¥å¤ç人ID'); |
| | | return; |
| | | } |
| | | await assignTicket(ticketId.value, assignUserId.value); |
| | | message.success('åé
æå'); |
| | | showAssignInput.value = false; |
| | | await loadTicket(); |
| | | } |
| | | |
| | | function toggleClassifyInput() { |
| | | showClassifyInput.value = !showClassifyInput.value; |
| | | if (showClassifyInput.value) { |
| | |
| | | await cancelTicket(ticketId.value); |
| | | message.success('åæ¶æå'); |
| | | await loadTicket(); |
| | | } |
| | | |
| | | function handleCreateReturn() { |
| | | router.push({ |
| | | name: 'AftersalesReturnDetail', |
| | | params: { id: 'new' }, |
| | | query: { ticketId: ticketId.value }, |
| | | }); |
| | | } |
| | | |
| | | function goBack() { |
| | |
| | | <Descriptions.Item label="é®é¢æè¿°" :span="3"> |
| | | {{ ticket.issueDescription || '-' }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="客æ·è¯æ±åé¦æè¿°" :span="3"> |
| | | {{ ticket.feedbackDescription || '-' }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="å¤çç»æ" :span="3"> |
| | | {{ ticket.resultDescription || '-' }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="夿³¨" :span="3"> |
| | | {{ ticket.remark || '-' }} |
| | | </Descriptions.Item> |
| | |
| | | </div> |
| | | </Card> |
| | | |
| | | <!-- éä»¶ --> |
| | | <Card v-if="ticket?.attachmentList?.length" title="éä»¶" class="mb-4"> |
| | | <!-- 客æ·è¯æ±éä»¶ / å¤çç»æéä»¶ï¼åå¼ä¸ä¼ ãåå¼å±ç¤ºï¼ --> |
| | | <Card v-if="ticket?.feedbackAttachmentList?.length" title="客æ·è¯æ±éä»¶" class="mb-4"> |
| | | <div class="flex flex-wrap gap-3"> |
| | | <a |
| | | v-for="file in ticket.feedbackAttachmentList" |
| | | :key="file.id" |
| | | :href="file.url" |
| | | target="_blank" |
| | | class="text-blue-500 hover:underline" |
| | | > |
| | | {{ file.name || 'éä»¶-' + file.id }} |
| | | </a> |
| | | </div> |
| | | </Card> |
| | | <Card v-if="ticket?.resultAttachmentList?.length" title="å¤çç»æéä»¶" class="mb-4"> |
| | | <div class="flex flex-wrap gap-3"> |
| | | <a |
| | | v-for="file in ticket.resultAttachmentList" |
| | | :key="file.id" |
| | | :href="file.url" |
| | | target="_blank" |
| | | class="text-blue-500 hover:underline" |
| | | > |
| | | {{ file.name || 'éä»¶-' + file.id }} |
| | | </a> |
| | | </div> |
| | | </Card> |
| | | <!-- åå²éç¨éä»¶ï¼æ§æ°æ®ï¼ --> |
| | | <Card v-if="ticket?.attachmentList?.length" title="å
¶ä»éä»¶" class="mb-4"> |
| | | <div class="flex flex-wrap gap-3"> |
| | | <a |
| | | v-for="file in ticket.attachmentList" |
| | |
| | | {{ file.name || 'éä»¶-' + file.id }} |
| | | </a> |
| | | </div> |
| | | </Card> |
| | | |
| | | <!-- æµè½¬æµç¨è®°å½ --> |
| | | <Card v-if="ticket" title="æµè½¬æµç¨è®°å½" class="mb-4"> |
| | | <div v-if="!ticket.flowList?.length" class="text-sm text-gray-400"> |
| | | ææ æµè½¬è®°å½ |
| | | </div> |
| | | <Timeline v-else> |
| | | <Timeline.Item v-for="flow in ticket.flowList" :key="flow.id"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <span class="text-sm font-medium">{{ flow.actionName }}</span> |
| | | <span class="text-xs text-gray-400">{{ flow.createTime }}</span> |
| | | <span v-if="flow.operatorName" class="text-xs text-gray-500"> |
| | | æä½äººï¼{{ flow.operatorName }} |
| | | </span> |
| | | </div> |
| | | <div class="text-sm text-gray-700">{{ flow.content }}</div> |
| | | <div v-if="flow.fromStatusName || flow.toStatusName" class="text-xs text-gray-400"> |
| | | ç¶æï¼{{ flow.fromStatusName || 'â' }} â {{ flow.toStatusName || 'â' }} |
| | | </div> |
| | | </Timeline.Item> |
| | | </Timeline> |
| | | </Card> |
| | | |
| | | <!-- åè®¿è®°å½ --> |
| | |
| | | |
| | | <template #extra> |
| | | <div class="flex gap-2"> |
| | | <!-- å建éè´§ç³è¯· --> |
| | | <Button v-if="isProcessing && ticket?.issueType === AftersaleIssueTypeEnum.RETURN" type="primary" @click="handleCreateReturn"> |
| | | å建éè´§ç³è¯· |
| | | </Button> |
| | | |
| | | <!-- åé
å¤ç人 --> |
| | | <template v-if="isDraft || isProcessing"> |
| | | <template v-if="!showAssignInput"> |
| | | <Button @click="toggleAssignInput">åé
å¤ç人</Button> |
| | | </template> |
| | | <template v-else> |
| | | <Input |
| | | v-model:value="assignUserId" |
| | | placeholder="å¤ç人ID" |
| | | type="number" |
| | | style="width: 120px" |
| | | @pressEnter="handleAssign" |
| | | /> |
| | | <Button size="small" type="primary" @click="handleAssign">确认</Button> |
| | | <Button size="small" @click="toggleAssignInput">åæ¶</Button> |
| | | </template> |
| | | </template> |
| | | |
| | | <!-- é®é¢å¤å® --> |
| | | <Button v-if="isProcessing && !showClassifyInput" @click="toggleClassifyInput">é®é¢å¤å®</Button> |
| | | |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['aftersales:ticket:update'], |
| | | ifShow: row.status === AftersaleTicketStatusEnum.DRAFT, |
| | | ifShow: |
| | | row.status === AftersaleTicketStatusEnum.DRAFT || |
| | | row.status === AftersaleTicketStatusEnum.PROCESSING, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { computed, nextTick, ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum } from '@vben/constants'; |
| | |
| | | defineOptions({ name: 'TicketForm' }); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const router = useRouter(); |
| | | |
| | | const TS = AftersaleTicketStatusEnum; |
| | | const IT = AftersaleIssueTypeEnum; |
| | |
| | | const isPendingReturn = computed(() => formData.value?.status === TS.PENDING_RETURN); |
| | | const isPendingRefund = computed(() => formData.value?.status === TS.PENDING_REFUND); |
| | | const isResolved = computed(() => formData.value?.status === TS.RESOLVED); |
| | | const isReturnIssue = computed(() => formData.value?.issueType === IT.RETURN); |
| | | |
| | | const title = computed(() => { |
| | | if (formType.value === 'detail') return $t('ui.actionTitle.view', ['å®åå·¥å']); |
| | |
| | | items.value = formData.value?.items || []; |
| | | await refreshItemsGrid(); |
| | | await formApi.setValues(formData.value as Record<string, unknown>); |
| | | // åæ¾éä»¶ |
| | | if (formData.value?.attachmentList?.length) { |
| | | const blobIds = formData.value.attachmentList.map((item) => ({ |
| | | // åæ¾éä»¶ï¼å®¢æ·è¯æ±éä»¶ãå¤çç»æéä»¶åå«åæ¾ |
| | | if (formData.value?.feedbackAttachmentList?.length) { |
| | | const feedbackFiles = formData.value.feedbackAttachmentList.map((item) => ({ |
| | | uid: String(item.id), name: item.name || '', url: item.url || '', |
| | | status: 'done', id: item.id, |
| | | })); |
| | | await formApi.setFieldValue('blobIds', blobIds); |
| | | await formApi.setFieldValue('feedbackBlobIds', feedbackFiles); |
| | | } |
| | | if (formData.value?.resultAttachmentList?.length) { |
| | | const resultFiles = formData.value.resultAttachmentList.map((item) => ({ |
| | | uid: String(item.id), name: item.name || '', url: item.url || '', |
| | | status: 'done', id: item.id, |
| | | })); |
| | | await formApi.setFieldValue('resultBlobIds', resultFiles); |
| | | } |
| | | if (formData.value?.saleOrderId) { |
| | | const order = await getSaleOrder(formData.value.saleOrderId); |
| | |
| | | try { await cancelTicket(formData.value.id); message.success('åæ¶æå'); await modalApi.close(); emit('success'); } |
| | | finally { submitting.value = false; } |
| | | } |
| | | function toCreateReturn() { |
| | | if (!formData.value?.id) return; |
| | | router.push({ name: 'AftersalesReturnDetail', params: { id: 'new' }, query: { ticketId: formData.value.id } }); |
| | | modalApi.close(); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | <div v-if="isEdit" class="flex flex-auto items-center gap-2"> |
| | | <!-- é®é¢å¤å®: å¤çä¸ --> |
| | | <Button v-if="isProcessing && !showClassify" @click="toggleClassify">é®é¢å¤å®</Button> |
| | | <!-- å建éè´§: å¤çä¸ + éè´§é®é¢ --> |
| | | <Button v-if="isProcessing && isReturnIssue" type="primary" @click="toCreateReturn">å建éè´§ç³è¯·</Button> |
| | | <!-- å®ç»: å¤çä¸/å¾
éè´§/å¾
鿬¾ --> |
| | | <template v-if="isProcessing || isPendingReturn || isPendingRefund"> |
| | | <Popconfirm title="确认å®ç»è¯¥å·¥åï¼" @confirm="doResolve"><Button type="primary" :loading="submitting">å®ç»</Button></Popconfirm> |
| | |
| | | placeholder: '请è¾å
¥å
容', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | placeholder: 'å¯ä¸ä¼ éç¥å
¬åç¸å
³éä»¶', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | |
| | | </Descriptions> |
| | | </Card> |
| | | |
| | | <Card size="small" title="å
容"> |
| | | <Card size="small" title="å
容" class="mb-3"> |
| | | <div |
| | | class="min-h-40 rounded bg-gray-50 p-4" |
| | | v-dompurify-html="formData.content" |
| | | /> |
| | | </Card> |
| | | |
| | | <Card |
| | | v-if="formData.attachmentList?.length" |
| | | size="small" |
| | | title="éä»¶" |
| | | > |
| | | <div class="flex flex-col gap-2"> |
| | | <a |
| | | v-for="item in formData.attachmentList" |
| | | :key="item.id" |
| | | :href="item.downloadURL || item.url || item.previewURL" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="flex items-center gap-2 text-blue-600 hover:underline" |
| | | > |
| | | <IconifyIcon icon="ant-design:paper-clip-outlined" /> |
| | | <span>{{ item.name || `éä»¶${item.id}` }}</span> |
| | | </a> |
| | | </div> |
| | | </Card> |
| | | </div> |
| | | </template> |
| | | </Spin> |
| | |
| | | try { |
| | | formData.value = await getNotice(data.id); |
| | | await formApi.setValues(formData.value); |
| | | // éä»¶åæ¾ |
| | | await formApi.setFieldValue( |
| | | 'blobIds', |
| | | (formData.value.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })), |
| | | ); |
| | | selectedUserIds.value = formData.value.userIds ?? []; |
| | | } finally { |
| | | modalApi.unlock(); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { getCustomerSimpleList } from '#/api/crm/customer'; |
| | | import { getSaleOrderOptions } from '#/api/crm/bidBond'; |
| | | |
| | | export const BID_RESULT_OPTIONS = [ |
| | | { label: 'å¾
弿 ', value: 0, color: 'processing' }, |
| | | { label: '已䏿 ', value: 1, color: 'success' }, |
| | | { label: 'æªä¸æ ', value: 2, color: 'default' }, |
| | | ]; |
| | | |
| | | export const REFUND_STATUS_OPTIONS = [ |
| | | { label: 'æªéå', value: 0, color: 'warning' }, |
| | | { label: 'é¨åéå', value: 1, color: 'processing' }, |
| | | { label: 'å·²éå', value: 2, color: 'success' }, |
| | | { label: 'æ ééå', value: 3, color: 'default' }, |
| | | ]; |
| | | |
| | | export function getBidResultLabel(result?: number) { |
| | | return BID_RESULT_OPTIONS.find((item) => item.value === result)?.label ?? '-'; |
| | | } |
| | | |
| | | export function getBidResultColor(result?: number) { |
| | | return BID_RESULT_OPTIONS.find((item) => item.value === result)?.color ?? 'default'; |
| | | } |
| | | |
| | | export function getRefundStatusLabel(status?: number) { |
| | | return REFUND_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? '-'; |
| | | } |
| | | |
| | | export function getRefundStatusColor(status?: number) { |
| | | return REFUND_STATUS_OPTIONS.find((item) => item.value === status)?.color ?? 'default'; |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹ææ ä¿è¯éç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'customerId', |
| | | label: '客æ·ï¼ææ æ¹ï¼', |
| | | component: 'ApiSelect', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getCustomerSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'projectName', |
| | | label: '项ç®åç§°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥æ¬æ¬¡ææ 项ç®åç§°' }, |
| | | }, |
| | | { |
| | | fieldName: 'tenderNo', |
| | | label: 'ææ ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ææ æ¹åå¸çææ ç¼å·' }, |
| | | }, |
| | | { |
| | | fieldName: 'saleOrderId', |
| | | label: 'å
³èéå®è®¢å', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | placeholder: '请å
鿩客æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | labelField: 'no', |
| | | valueField: 'id', |
| | | optionFilterProp: 'label', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['customerId'], |
| | | componentProps: (values) => { |
| | | const customerId = values.customerId as number | undefined; |
| | | return { |
| | | placeholder: customerId |
| | | ? 'è¯·éæ©è¯¥å®¢æ·å·²å®¡æ ¸çéå®è®¢åï¼éå®å°è´¦ï¼' |
| | | : '请å
鿩客æ·', |
| | | disabled: !customerId, |
| | | api: customerId |
| | | ? () => getSaleOrderOptions(customerId) |
| | | : () => Promise.resolve([]), |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'bondAmount', |
| | | label: 'ä¿è¯ééé¢', |
| | | component: 'InputNumber', |
| | | rules: 'required', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | placeholder: '请è¾å
¥ç¼´çº³çä¿è¯ééé¢', |
| | | min: 0, |
| | | precision: 2, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'payDate', |
| | | label: 'ç¼´çº³æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: 'w-full', |
| | | placeholder: 'è¯·éæ©å客æ·ç¼´çº³ä¿è¯éçæ¥æ', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'bidResult', |
| | | label: '䏿 ç»æ', |
| | | component: 'Select', |
| | | rules: 'required', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ä¸æ ç»æ', |
| | | options: BID_RESULT_OPTIONS.map(({ label, value }) => ({ |
| | | label, |
| | | value, |
| | | })), |
| | | }, |
| | | defaultValue: 0, |
| | | }, |
| | | { |
| | | fieldName: 'refundStatus', |
| | | label: 'éåç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'æªä¸æ æ¶è·è¸ªä¿è¯éæ¯å¦éå', |
| | | options: REFUND_STATUS_OPTIONS.map(({ label, value }) => ({ |
| | | label, |
| | | value, |
| | | })), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'refundAmount', |
| | | label: 'éåéé¢', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | placeholder: 'å·²éåæ¶é¡»çäºä¿è¯ééé¢ï¼é¨åéåæ¶é¡»å°äºä¿è¯ééé¢', |
| | | min: 0, |
| | | precision: 2, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'refundDate', |
| | | label: 'é忥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: 'w-full', |
| | | placeholder: 'è¯·éæ©å®¢æ·å®é
éåä¿è¯éçæ¥æ', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨', rows: 2 }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'no', |
| | | label: 'ä¿è¯éåå·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ä¿è¯éåå·', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'customerId', |
| | | label: '客æ·', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | allowClear: true, |
| | | showSearch: true, |
| | | api: getCustomerSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'projectName', |
| | | label: '项ç®åç§°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥é¡¹ç®åç§°', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'saleOrderNo', |
| | | label: 'éå®è®¢åå·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥å
³èéå®è®¢åå·', allowClear: true }, |
| | | }, |
| | | { |
| | | fieldName: 'bidResult', |
| | | label: '䏿 ç»æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©', |
| | | allowClear: true, |
| | | options: BID_RESULT_OPTIONS.map(({ label, value }) => ({ |
| | | label, |
| | | value, |
| | | })), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'refundStatus', |
| | | label: 'éåç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©', |
| | | allowClear: true, |
| | | options: REFUND_STATUS_OPTIONS.map(({ label, value }) => ({ |
| | | label, |
| | | value, |
| | | })), |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** è¡¨æ ¼å */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { field: 'no', title: 'ä¿è¯éåå·', minWidth: 150 }, |
| | | { field: 'customerName', title: '客æ·ï¼ææ æ¹ï¼', minWidth: 160 }, |
| | | { field: 'projectName', title: '项ç®åç§°', minWidth: 180 }, |
| | | { field: 'tenderNo', title: 'ææ ç¼å·', minWidth: 140 }, |
| | | { |
| | | field: 'saleOrderNo', |
| | | title: 'å
³èéå®è®¢å', |
| | | minWidth: 160, |
| | | slots: { default: 'saleOrderNo' }, |
| | | }, |
| | | { |
| | | field: 'saleOrderAmount', |
| | | title: '订åéé¢', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'bondAmount', |
| | | title: 'ä¿è¯ééé¢', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { field: 'payDate', title: 'ç¼´çº³æ¥æ', minWidth: 110 }, |
| | | { |
| | | field: 'bidResult', |
| | | title: '䏿 ç»æ', |
| | | minWidth: 100, |
| | | slots: { default: 'bidResult' }, |
| | | }, |
| | | { |
| | | field: 'refundStatus', |
| | | title: 'éåç¶æ', |
| | | minWidth: 110, |
| | | slots: { default: 'refundStatus' }, |
| | | }, |
| | | { |
| | | field: 'refundAmount', |
| | | title: 'éåéé¢', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { field: 'refundDate', title: 'é忥æ', minWidth: 110 }, |
| | | { field: 'remark', title: '夿³¨', minWidth: 150 }, |
| | | { |
| | | title: 'æä½', |
| | | width: 130, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ÎļþÃû´Ó src/views/srm/bidBond/index.vue ÐÞ¸Ä |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { SrmBidBondApi } from '#/api/srm/bidBond'; |
| | | import type { CrmBidBondApi } from '#/api/crm/bidBond'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteBidBond, |
| | | getBidBondPage, |
| | | } from '#/api/srm/bidBond'; |
| | | import { deleteBidBond, getBidBondPage } from '#/api/crm/bidBond'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { |
| | | getBidResultColor, |
| | | getBidResultLabel, |
| | | getRefundStatusColor, |
| | | getRefundStatusLabel, |
| | | useGridColumns, |
| | |
| | | } from './data'; |
| | | import BidBondForm from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'SrmBidBond' }); |
| | | defineOptions({ name: 'CrmBidBond' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: BidBondForm, |
| | |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | function handleEdit(row: SrmBidBondApi.BidBond) { |
| | | function handleEdit(row: CrmBidBondApi.BidBond) { |
| | | formModalApi.setData({ id: row.id }).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: SrmBidBondApi.BidBond) { |
| | | async function handleDelete(row: CrmBidBondApi.BidBond) { |
| | | await deleteBidBond(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.no])); |
| | | handleRefresh(); |
| | |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<SrmBidBondApi.BidBond>, |
| | | } as VxeTableGridOptions<CrmBidBondApi.BidBond>, |
| | | }); |
| | | </script> |
| | | |
| | |
| | | label: $t('ui.actionTitle.create', ['ææ ä¿è¯é']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['srm:bid-bond:create'], |
| | | auth: ['crm:bid-bond:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #won="{ row }"> |
| | | <Tag :color="row.won ? 'success' : 'default'"> |
| | | {{ row.won ? '䏿 ' : 'æªä¸æ ' }} |
| | | <template #saleOrderNo="{ row }"> |
| | | <span v-if="row.saleOrderNo">{{ row.saleOrderNo }}</span> |
| | | <span v-else class="text-gray-400">æªå
³è</span> |
| | | </template> |
| | | <template #bidResult="{ row }"> |
| | | <Tag :color="getBidResultColor(row.bidResult)"> |
| | | {{ getBidResultLabel(row.bidResult) }} |
| | | </Tag> |
| | | </template> |
| | | <template #refundStatus="{ row }"> |
| | |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['srm:bid-bond:update'], |
| | | auth: ['crm:bid-bond:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['srm:bid-bond:delete'], |
| | | auth: ['crm:bid-bond:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.no]), |
| | | confirm: handleDelete.bind(null, row), |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { CrmBidBondApi } from '#/api/crm/bidBond'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createBidBond, |
| | | getBidBond, |
| | | updateBidBond, |
| | | } from '#/api/crm/bidBond'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<CrmBidBondApi.BidBond>(); |
| | | /** æå¼å¼¹çªåå¡«æ°æ®æ¶ä¸è§¦ååæ®µèå¨ï¼é¿å
åå¡«æå·²ééå®è®¢åæ¸
空 */ |
| | | const loading = ref(false); |
| | | |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['ææ ä¿è¯é']) |
| | | : $t('ui.actionTitle.create', ['ææ ä¿è¯é']); |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | handleValuesChange: async (values, changedFields) => { |
| | | if (loading.value) { |
| | | return; |
| | | } |
| | | // æ¢å®¢æ·åï¼åéå®è®¢åä¸å®ä¸åå±äºè¯¥å®¢æ·ï¼å¿
é¡»æ¸
空éé |
| | | if (changedFields.includes('customerId') && values.saleOrderId) { |
| | | await formApi.setValues({ saleOrderId: undefined }); |
| | | return; |
| | | } |
| | | // éäºéå®è®¢å说æå·²ä¸æ ï¼æ¸
空订ååéåå¾
弿 |
| | | if (changedFields.includes('saleOrderId')) { |
| | | if (values.saleOrderId && values.bidResult !== 1) { |
| | | await formApi.setValues({ bidResult: 1 }); |
| | | return; |
| | | } |
| | | if (!values.saleOrderId && values.bidResult === 1) { |
| | | await formApi.setValues({ bidResult: 0 }); |
| | | return; |
| | | } |
| | | } |
| | | // æªä¸æ ä¸å¯è½æéå®è®¢åï¼æ¸
空订åé¿å
åç«¯æ ¡éªå¤±è´¥ |
| | | if (changedFields.includes('bidResult') && values.bidResult === 2) { |
| | | if (values.saleOrderId) { |
| | | await formApi.setValues({ saleOrderId: undefined }); |
| | | } |
| | | if (values.refundStatus === 3) { |
| | | await formApi.setValues({ refundStatus: 0 }); |
| | | } |
| | | return; |
| | | } |
| | | // 已䏿 ä¸åå¨éæ¬¾åºæ¯ |
| | | if (changedFields.includes('bidResult') && values.bidResult === 1) { |
| | | if (values.refundStatus !== 3) { |
| | | await formApi.setValues({ |
| | | refundStatus: 3, |
| | | refundAmount: undefined, |
| | | refundDate: undefined, |
| | | }); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | const data = (await formApi.getValues()) as CrmBidBondApi.BidBond; |
| | | // æªéå / æ ééåä¸å
è®¸å¸¦éæ¬¾éé¢ä¸æ¥æï¼é¿å
åç«¯ç¶æä¸éé¢å²çªæ ¡éª |
| | | if ( |
| | | data.refundStatus === 0 || |
| | | data.refundStatus === 3 || |
| | | data.refundStatus === undefined || |
| | | data.refundStatus === null |
| | | ) { |
| | | data.refundAmount = undefined; |
| | | data.refundDate = undefined; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | await (formData.value?.id |
| | | ? updateBidBond({ ...data, id: formData.value.id }) |
| | | : createBidBond(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id?: number }>(); |
| | | if (!data?.id) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | loading.value = true; |
| | | try { |
| | | formData.value = await getBidBond(data.id); |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | loading.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | defineExpose({ modalApi }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/4"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | import ErpPurchaseOrderSelect from './select.vue'; |
| | | import ErpPurchaseOrderSelectDialog from './select-dialog.vue'; |
| | | import PaymentDatePicker from './payment-date-picker.vue'; |
| | | |
| | | export { ErpPurchaseOrderSelect, ErpPurchaseOrderSelectDialog }; |
| | | export { |
| | | ErpPurchaseOrderSelect, |
| | | ErpPurchaseOrderSelectDialog, |
| | | PaymentDatePicker, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { Dayjs } from 'dayjs'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { DatePicker } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | DEFAULT_ACCOUNT_PERIOD_DAYS, |
| | | getAccountPeriodDays, |
| | | isOverdue, |
| | | } from '#/utils/overdue'; |
| | | |
| | | defineOptions({ name: 'PaymentDatePicker' }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | modelValue?: string; |
| | | /** éè´æ¶é´ï¼è´¦æèµ·ç®æ¥ï¼ */ |
| | | orderTime?: Date | string; |
| | | placeholder?: string; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | modelValue: undefined, |
| | | orderTime: undefined, |
| | | placeholder: 'è¯·éæ©ä»æ¬¾æ¥æ', |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: [value?: string]; |
| | | 'update:modelValue': [value?: string]; |
| | | }>(); |
| | | |
| | | /** è´¦æå¤©æ°ï¼ä»æ¬¾æ¥æ - éè´æ¶é´ */ |
| | | const periodDays = computed(() => |
| | | getAccountPeriodDays(props.orderTime, props.modelValue), |
| | | ); |
| | | |
| | | /** è´¦æè¶
è¿ 30 天æ¶çº¢è²æç¤º */ |
| | | const overdue = computed(() => isOverdue(props.orderTime, props.modelValue)); |
| | | |
| | | const overdueTip = computed(() => { |
| | | if (!overdue.value || periodDays.value === null) { |
| | | return ''; |
| | | } |
| | | return `è´¦æ ${periodDays.value} 天ï¼è¶
è¿ ${DEFAULT_ACCOUNT_PERIOD_DAYS} 天ï¼è¯·ç¡®è®¤ä»æ¬¾è®¡å`; |
| | | }); |
| | | |
| | | function handleUpdate(value?: Dayjs | string) { |
| | | // é
ç½®äº value-format æ¶ DatePicker åä¼ å符串ï¼è¿éå䏿¬¡å
åº |
| | | const next = typeof value === 'string' ? value : undefined; |
| | | emit('update:modelValue', next); |
| | | emit('change', next); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="w-full"> |
| | | <DatePicker |
| | | class="w-full" |
| | | :disabled="disabled" |
| | | :placeholder="placeholder" |
| | | :status="overdue ? 'error' : undefined" |
| | | :value="modelValue" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | @update:value="handleUpdate" |
| | | /> |
| | | <div v-if="overdueTip" class="mt-1 text-xs text-red-500"> |
| | | â {{ overdueTip }} |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | import { getAccountSimpleList } from '#/api/erp/finance/account'; |
| | | import { getSupplierSimpleList } from '#/api/srm/supplier'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps, getOverdueTip, withoutFixedColumns } from '#/utils'; |
| | | import { getRangePickerDefaultProps, withoutFixedColumns } from '#/utils'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | |
| | | import { PaymentDatePicker } from './components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | |
| | | { |
| | | fieldName: 'paymentDate', |
| | | label: '仿¬¾æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | component: PaymentDatePicker, |
| | | componentProps: (values) => ({ |
| | | orderTime: values.orderTime, |
| | | placeholder: 'è¯·éæ©ä»æ¬¾æ¥æ', |
| | | showTime: false, |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | }), |
| | | rules: 'required', |
| | | help: (values: { |
| | | orderTime?: Date | string; |
| | | paymentDate?: Date | string; |
| | | }) => { |
| | | const tip = getOverdueTip(values.orderTime, values.paymentDate); |
| | | return tip ? `â ${tip}ï¼è¯·ç¡®è®¤ä»æ¬¾è®¡å` : ''; |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'paymentMethod', |
| | |
| | | } |
| | | } |
| | | |
| | | /** æäº¤è稿订åå®¡æ¹ */ |
| | | /** æäº¤è稿订å */ |
| | | async function handleSubmit(row: ErpPurchaseOrderApi.PurchaseOrder) { |
| | | const hideLoading = message.loading({ |
| | | content: 'æäº¤å®¡æ¹ä¸...', |
| | | content: 'æäº¤ä¸...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await submitPurchaseOrder(row.id!); |
| | | message.success(`éè´å°è´¦ ${row.no} å·²æäº¤å®¡æ¹`); |
| | | message.success(`éè´å°è´¦ ${row.no} å·²æäº¤`); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æäº¤å®¡æ¹', |
| | | label: 'æäº¤', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['erp:purchase-order:update-status'], |
| | | ifShow: () => row.status === 0, |
| | | popConfirm: { |
| | | title: `æäº¤éè´å°è´¦${row.no}审æ¹åï¼`, |
| | | title: `æäº¤éè´å°è´¦${row.no}åï¼`, |
| | | confirm: handleSubmit.bind(null, row), |
| | | }, |
| | | }, |
| | |
| | | taxPrice: saleItem.taxPrice, |
| | | totalPrice: saleItem.totalPrice, |
| | | remark: saleItem.remark, |
| | | qcCheckFlag: false, |
| | | qcCheckFlag: true, |
| | | })), |
| | | ); |
| | | } |
| | |
| | | count: bomItem.quantity ?? 1, |
| | | taxPercent: 0, |
| | | remark: bomItem.remark, |
| | | qcCheckFlag: false, |
| | | qcCheckFlag: true, |
| | | })), |
| | | ); |
| | | message.info('å·²æå·¥å BOM 带å
¥ç©æï¼è¯·è¡¥å¡«å«ç¨åä»·'); |
| | |
| | | taxPrice: undefined, |
| | | totalPrice: undefined, |
| | | remark: undefined, |
| | | qcCheckFlag: false, |
| | | // ä¸éè´ç³è¯·ä¿æä¸è´ï¼æ°å¢æç»é»è®¤è´¨æ£ |
| | | qcCheckFlag: true, |
| | | }; |
| | | tableData.value.push(newRow); |
| | | // éç¥ç¶ç»ä»¶æ´æ° |
| | |
| | | |
| | | /** çæéè´è®¢å */ |
| | | function handleGenerateOrder(row: ErpPurchaseRequestApi.PurchaseRequest) { |
| | | orderFormModalApi.setData({ id: row.id, supplierId: row.supplierId }).open(); |
| | | // ä¾åºå以ç³è¯·å为åï¼çæè®¢åæ¶ç´æ¥æ²¿ç¨ï¼ä¸åè®©ç¨æ·éå¤éæ© |
| | | orderFormModalApi |
| | | .setData({ |
| | | id: row.id, |
| | | supplierId: row.supplierId, |
| | | supplierName: row.supplierName, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | |
| | | demandTime: undefined, |
| | | remark: undefined, |
| | | totalPrice: 0, |
| | | qcCheckFlag: false, |
| | | // éè´ç³è¯·ç产åæç»é»è®¤éè¦è´¨æ£ |
| | | qcCheckFlag: true, |
| | | }; |
| | | tableData.value.push(newRow); |
| | | emit('update:items', [...tableData.value]); |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request'; |
| | | |
| | | import { ref, computed } from 'vue'; |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Select } from 'ant-design-vue'; |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { generatePurchaseOrder } from '#/api/erp/purchase/request'; |
| | | import { getSupplierSimpleList } from '#/api/srm/supplier'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const requestId = ref<number>(); |
| | | const supplierId = ref<number>(); |
| | | const supplierOptions = ref<{ id: number; name: string }[]>([]); |
| | | |
| | | const supplierSelectOptions = computed(() => { |
| | | return supplierOptions.value.map((s) => ({ |
| | | label: s.name, |
| | | value: s.id, |
| | | })); |
| | | }); |
| | | const supplierName = ref<string>(); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!supplierId.value) { |
| | | message.warning('è¯·éæ©ä¾åºå'); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const orderId = await generatePurchaseOrder(requestId.value!, supplierId.value!); |
| | | // ä¾åºå沿ç¨éè´ç³è¯·æäº¤æ¶éæ©çä¾åºåï¼æ é忬¡éæ© |
| | | await generatePurchaseOrder(requestId.value!, supplierId.value); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success(`çæéè´è®¢åæåï¼è®¢åç¼å·ï¼${orderId}`); |
| | | message.success('çæéè´è®¢åæå'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | if (!isOpen) { |
| | | requestId.value = undefined; |
| | | supplierId.value = undefined; |
| | | supplierName.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number; supplierId?: number }>(); |
| | | const data = modalApi.getData<{ |
| | | id: number; |
| | | supplierId?: number; |
| | | supplierName?: string; |
| | | }>(); |
| | | requestId.value = data.id; |
| | | supplierId.value = data.supplierId; |
| | | // è·åä¾åºåå表 |
| | | supplierOptions.value = await getSupplierSimpleList(); |
| | | supplierName.value = data.supplierName; |
| | | }, |
| | | }); |
| | | </script> |
| | |
| | | <template> |
| | | <Modal title="çæéè´è®¢å" class="w-1/3"> |
| | | <div class="mb-4"> |
| | | <span class="mr-2">éæ©ä¾åºåï¼</span> |
| | | <Select |
| | | v-model:value="supplierId" |
| | | :options="supplierSelectOptions" |
| | | style="width: 200px" |
| | | placeholder="è¯·éæ©ä¾åºå" |
| | | show-search |
| | | :filter-option="(input: string, option: any) => option.label.toLowerCase().includes(input.toLowerCase())" |
| | | /> |
| | | <span class="mr-2">ä¾åºåï¼</span> |
| | | <span class="font-medium"> |
| | | {{ supplierName || (supplierId ? `ID=${supplierId}` : 'æªæå®') }} |
| | | </span> |
| | | </div> |
| | | <div class="text-sm text-muted-foreground"> |
| | | ä¾åºå沿ç¨éè´ç³è¯·æäº¤æ¶éæ©çä¾åºåï¼ç¡®è®¤åå°æç³è¯·æç»çæéè´è®¢åã |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | | </template> |
| | |
| | | import type { Dayjs } from 'dayjs'; |
| | | |
| | | import type { ErpSupplierStatementApi } from '#/api/erp/purchase/statement'; |
| | | import type { SrmSupplierApi } from '#/api/srm/supplier'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { |
| | | Button, |
| | | Card, |
| | | Checkbox, |
| | | Col, |
| | | Empty, |
| | | Input, |
| | | RangePicker, |
| | | Row, |
| | | Spin, |
| | | Table, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { getSupplierStatement } from '#/api/erp/purchase/statement'; |
| | | import { getSupplierSimpleList } from '#/api/srm/supplier'; |
| | | import { |
| | | getSupplierStatement, |
| | | getSupplierStatementSummaryList, |
| | | } from '#/api/erp/purchase/statement'; |
| | | |
| | | defineOptions({ name: 'ErpSupplierStatement' }); |
| | | |
| | | const supplierId = ref<number>(); |
| | | const dateRange = ref<[Dayjs, Dayjs]>(); |
| | | const supplierOptions = ref<SrmSupplierApi.SupplierVO[]>([]); |
| | | const includeUnapproved = ref(false); |
| | | const keyword = ref(''); |
| | | |
| | | /** 左侧ï¼åä¾åºå徿¥æ±æ» */ |
| | | const summaries = ref<ErpSupplierStatementApi.SupplierSummary[]>([]); |
| | | const listLoading = ref(false); |
| | | /** å½åéä¸çä¾åºå */ |
| | | const selectedId = ref<number>(); |
| | | /** å³ä¾§ï¼å½åä¾åºå徿¥æç» */ |
| | | const statement = ref<ErpSupplierStatementApi.Statement>(); |
| | | const loading = ref(false); |
| | | const detailLoading = ref(false); |
| | | |
| | | const filteredSummaries = computed(() => { |
| | | const kw = keyword.value.trim().toLowerCase(); |
| | | if (!kw) { |
| | | return summaries.value; |
| | | } |
| | | return summaries.value.filter( |
| | | (item) => |
| | | (item.supplierName ?? '').toLowerCase().includes(kw) || |
| | | (item.supplierCode ?? '').toLowerCase().includes(kw), |
| | | ); |
| | | }); |
| | | |
| | | const selectedSummary = computed(() => |
| | | summaries.value.find((item) => item.supplierId === selectedId.value), |
| | | ); |
| | | |
| | | const totalUnpaid = computed(() => |
| | | summaries.value.reduce((sum, item) => sum + (item.unpaidAmount ?? 0), 0), |
| | | ); |
| | | |
| | | const orderColumns = [ |
| | | { title: 'éè´è®¢åå·', dataIndex: 'no', key: 'no', width: 170 }, |
| | |
| | | key: 'remainingPayable', |
| | | width: 120, |
| | | }, |
| | | { title: 'éè´è®¢å', dataIndex: 'purchaseOrderNo', key: 'purchaseOrderNo', width: 160 }, |
| | | { |
| | | title: 'éè´è®¢å', |
| | | dataIndex: 'purchaseOrderNo', |
| | | key: 'purchaseOrderNo', |
| | | width: 160, |
| | | }, |
| | | ]; |
| | | |
| | | const paymentColumns = [ |
| | |
| | | ]; |
| | | |
| | | function formatMoney(value?: number) { |
| | | return value === undefined || value === null ? '-' : Number(value).toFixed(2); |
| | | if (value === undefined || value === null) { |
| | | return '-'; |
| | | } |
| | | return Number(value).toLocaleString('zh-CN', { |
| | | maximumFractionDigits: 2, |
| | | minimumFractionDigits: 2, |
| | | }); |
| | | } |
| | | |
| | | function getStatusLabel(status?: number) { |
| | |
| | | } |
| | | |
| | | function getPaymentStatusLabel(status?: number) { |
| | | return ( |
| | | ['æªä»æ¬¾', 'é¨å仿¬¾', 'å
¨é¨ä»æ¬¾'][status ?? 0] ?? '-' |
| | | ); |
| | | return ['æªä»æ¬¾', 'é¨å仿¬¾', 'å
¨é¨ä»æ¬¾'][status ?? 0] ?? '-'; |
| | | } |
| | | |
| | | async function handleQuery() { |
| | | if (!supplierId.value) { |
| | | message.warning('è¯·éæ©ä¾åºå'); |
| | | /** ç»è£
æ¶é´èå´åæ° */ |
| | | function buildTimeParams() { |
| | | const params: { |
| | | beginTime?: string; |
| | | endTime?: string; |
| | | } = {}; |
| | | if (dateRange.value?.length === 2) { |
| | | params.beginTime = dateRange.value[0]! |
| | | .startOf('day') |
| | | .format('YYYY-MM-DD HH:mm:ss'); |
| | | params.endTime = dateRange.value[1]! |
| | | .endOf('day') |
| | | .format('YYYY-MM-DD HH:mm:ss'); |
| | | } |
| | | return params; |
| | | } |
| | | |
| | | /** å 载左侧ä¾åºåæ±æ»ï¼é»è®¤éä¸ç¬¬ä¸ä¸ªä¾åºå */ |
| | | async function loadSummaries() { |
| | | listLoading.value = true; |
| | | try { |
| | | summaries.value = |
| | | (await getSupplierStatementSummaryList({ |
| | | ...buildTimeParams(), |
| | | includeUnapprovedOrder: includeUnapproved.value, |
| | | })) ?? []; |
| | | const stillExists = summaries.value.some( |
| | | (item) => item.supplierId === selectedId.value, |
| | | ); |
| | | if (!stillExists) { |
| | | selectedId.value = summaries.value[0]?.supplierId; |
| | | } |
| | | const targetId = selectedId.value; |
| | | if (targetId === undefined) { |
| | | statement.value = undefined; |
| | | return; |
| | | } |
| | | await loadDetail(targetId); |
| | | } finally { |
| | | listLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** ç¹å»å·¦ä¾§ä¾åºåï¼å è½½å³ä¾§æç» */ |
| | | async function handleSelect(supplierId: number) { |
| | | if (selectedId.value === supplierId && statement.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | selectedId.value = supplierId; |
| | | await loadDetail(supplierId); |
| | | } |
| | | |
| | | /** å è½½æå®ä¾åºåç徿¥æç» */ |
| | | async function loadDetail(supplierId: number) { |
| | | detailLoading.value = true; |
| | | try { |
| | | const params: { |
| | | supplierId: number; |
| | | beginTime?: string; |
| | | endTime?: string; |
| | | } = { supplierId: supplierId.value }; |
| | | if (dateRange.value?.length === 2) { |
| | | params.beginTime = dateRange.value[0]!.startOf('day').format('YYYY-MM-DD HH:mm:ss'); |
| | | params.endTime = dateRange.value[1]!.endOf('day').format('YYYY-MM-DD HH:mm:ss'); |
| | | } |
| | | statement.value = await getSupplierStatement(params); |
| | | statement.value = await getSupplierStatement({ |
| | | ...buildTimeParams(), |
| | | includeUnapprovedOrder: includeUnapproved.value, |
| | | supplierId, |
| | | }); |
| | | } finally { |
| | | loading.value = false; |
| | | detailLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | supplierOptions.value = (await getSupplierSimpleList()) ?? []; |
| | | }); |
| | | onMounted(loadSummaries); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="p-4" :class="{ 'pointer-events-none opacity-60': loading }"> |
| | | <div class="mb-4 flex flex-wrap items-center gap-3"> |
| | | <span class="text-sm text-gray-500">ä¾åºå</span> |
| | | <a-select |
| | | v-model:value="supplierId" |
| | | show-search |
| | | option-filter-prop="label" |
| | | placeholder="è¯·éæ©ä¾åºå" |
| | | style="width: 260px" |
| | | :options=" |
| | | supplierOptions.map((item) => ({ |
| | | label: item.name, |
| | | value: item.id, |
| | | })) |
| | | " |
| | | /> |
| | | <a-range-picker v-model:value="dateRange" /> |
| | | <a-button type="primary" :loading="loading" @click="handleQuery"> |
| | | æ¥è¯¢ |
| | | </a-button> |
| | | <span v-if="statement?.supplierName" class="text-sm text-gray-600"> |
| | | å½åä¾åºåï¼{{ statement.supplierName }} |
| | | </span> |
| | | <div class="flex h-full gap-4"> |
| | | <!-- 左侧ï¼ä¾åºåæ»è§ï¼ç¹å»æ¥çå³ä¾§æç» --> |
| | | <div class="flex w-[360px] shrink-0 flex-col rounded-lg border"> |
| | | <div class="border-b p-3"> |
| | | <Input |
| | | v-model:value="keyword" |
| | | allow-clear |
| | | placeholder="æç´¢ä¾åºååç§° / ç¼ç " |
| | | /> |
| | | <div class="mt-2 flex items-center gap-2"> |
| | | <RangePicker |
| | | v-model:value="dateRange" |
| | | class="min-w-0 flex-1" |
| | | size="small" |
| | | /> |
| | | </div> |
| | | <div class="mt-2 flex items-center justify-between"> |
| | | <Checkbox v-model:checked="includeUnapproved"> |
| | | 嫿ªå®¡æ ¸éè´è®¢å |
| | | </Checkbox> |
| | | <Button |
| | | size="small" |
| | | type="primary" |
| | | :loading="listLoading" |
| | | @click="loadSummaries" |
| | | > |
| | | æ¥è¯¢ |
| | | </Button> |
| | | </div> |
| | | <div class="mt-2 text-xs text-gray-500"> |
| | | å
± {{ filteredSummaries.length }} 个ä¾åºåï¼æªä»å计 |
| | | <span class="font-medium text-red-600"> |
| | | ¥{{ formatMoney(totalUnpaid) }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div class="min-h-0 flex-1 overflow-y-auto p-2"> |
| | | <Spin :spinning="listLoading"> |
| | | <div |
| | | v-if="filteredSummaries.length === 0" |
| | | class="p-6 text-center text-sm text-gray-500" |
| | | > |
| | | {{ |
| | | summaries.length > 0 |
| | | ? '没æå¹é
çä¾åºå' |
| | | : 'æéæ¶é´èå´å
没æä¾åºå徿¥' |
| | | }} |
| | | </div> |
| | | <div |
| | | v-for="item in filteredSummaries" |
| | | :key="item.supplierId" |
| | | class="mb-2 cursor-pointer rounded-lg border p-3 transition-colors" |
| | | :class=" |
| | | item.supplierId === selectedId |
| | | ? 'border-blue-500 bg-blue-50' |
| | | : 'hover:border-blue-300 hover:bg-gray-50' |
| | | " |
| | | @click="handleSelect(item.supplierId!)" |
| | | > |
| | | <div class="flex items-start justify-between gap-2"> |
| | | <div class="min-w-0"> |
| | | <div class="truncate text-sm font-medium"> |
| | | {{ item.supplierName }} |
| | | </div> |
| | | <div class="text-xs text-gray-400"> |
| | | {{ item.supplierCode || '-' }} |
| | | </div> |
| | | </div> |
| | | <div class="shrink-0 text-right"> |
| | | <div class="text-xs text-gray-500">æªä»ä½é¢</div> |
| | | <div |
| | | class="text-sm font-semibold" |
| | | :class=" |
| | | (item.unpaidAmount ?? 0) > 0 |
| | | ? 'text-red-600' |
| | | : 'text-gray-700' |
| | | " |
| | | > |
| | | {{ formatMoney(item.unpaidAmount) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="mt-2 grid grid-cols-3 gap-1 text-xs text-gray-500"> |
| | | <span>éè´ {{ formatMoney(item.totalPurchaseAmount) }}</span> |
| | | <span>å¼ç¥¨ {{ formatMoney(item.totalInvoiceAmount) }}</span> |
| | | <span>仿¬¾ {{ formatMoney(item.totalPaymentAmount) }}</span> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | </div> |
| | | </div> |
| | | |
| | | <a-row :gutter="16" class="mb-4"> |
| | | <a-col :span="6"> |
| | | <a-card size="small"> |
| | | <div class="text-gray-500">累计éè´é¢</div> |
| | | <div class="text-2xl font-semibold"> |
| | | {{ formatMoney(statement?.summary?.totalPurchaseAmount) }} |
| | | </div> |
| | | </a-card> |
| | | </a-col> |
| | | <a-col :span="6"> |
| | | <a-card size="small"> |
| | | <div class="text-gray-500">累计å¼ç¥¨é¢</div> |
| | | <div class="text-2xl font-semibold text-blue-600"> |
| | | {{ formatMoney(statement?.summary?.totalInvoiceAmount) }} |
| | | </div> |
| | | </a-card> |
| | | </a-col> |
| | | <a-col :span="6"> |
| | | <a-card size="small"> |
| | | <div class="text-gray-500">ç´¯è®¡ä»æ¬¾é¢</div> |
| | | <div class="text-2xl font-semibold text-green-600"> |
| | | {{ formatMoney(statement?.summary?.totalPaymentAmount) }} |
| | | </div> |
| | | </a-card> |
| | | </a-col> |
| | | <a-col :span="6"> |
| | | <a-card size="small"> |
| | | <div class="text-gray-500">æªä»ä½é¢ï¼å¼ç¥¨ - 仿¬¾ï¼</div> |
| | | <div class="text-2xl font-semibold text-red-600"> |
| | | {{ formatMoney(statement?.summary?.unpaidAmount) }} |
| | | </div> |
| | | </a-card> |
| | | </a-col> |
| | | </a-row> |
| | | <!-- å³ä¾§ï¼å½åä¾åºå徿¥æç» --> |
| | | <div class="min-w-0 flex-1 overflow-y-auto"> |
| | | <Empty |
| | | v-if="!selectedId" |
| | | class="mt-20" |
| | | description="请å¨å·¦ä¾§éæ©ä¾åºåï¼æ¥ç徿¥æç»" |
| | | /> |
| | | <Spin v-else :spinning="detailLoading"> |
| | | <div class="mb-4 flex flex-wrap items-center gap-2"> |
| | | <span class="text-lg font-semibold"> |
| | | {{ statement?.supplierName ?? selectedSummary?.supplierName }} |
| | | </span> |
| | | <span class="text-sm text-gray-500"> |
| | | {{ selectedSummary?.supplierCode || '-' }} |
| | | </span> |
| | | <span class="text-xs text-gray-400"> |
| | | éè´ {{ selectedSummary?.orderCount ?? 0 }} å / æ¥ç¥¨ |
| | | {{ selectedSummary?.invoiceCount ?? 0 }} å¼ / 仿¬¾ |
| | | {{ selectedSummary?.paymentCount ?? 0 }} ç¬ |
| | | </span> |
| | | </div> |
| | | |
| | | <a-row :gutter="16"> |
| | | <a-col :span="12"> |
| | | <a-card title="éè´è®¢åæµæ°´ï¼å«å¼ç¥¨éé¢ï¼" size="small"> |
| | | <a-table |
| | | :columns="orderColumns" |
| | | :data-source="statement?.orders ?? []" |
| | | :pagination="false" |
| | | size="small" |
| | | bordered |
| | | :scroll="{ x: 800 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'orderAmount'"> |
| | | {{ formatMoney(record.orderAmount) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'invoicedAmount'"> |
| | | <span class="text-blue-600"> |
| | | {{ formatMoney(record.invoicedAmount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'uninvoicedAmount'"> |
| | | <span class="text-red-600"> |
| | | {{ formatMoney(record.uninvoicedAmount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | {{ getStatusLabel(record.status) }} |
| | | </template> |
| | | </template> |
| | | </a-table> |
| | | </a-card> |
| | | </a-col> |
| | | <a-col :span="12"> |
| | | <a-card title="éè´æ¥ç¥¨æµæ°´ï¼å¼ç¥¨éé¢ / å·²ä» / å©ä½åºä»ï¼" size="small"> |
| | | <a-table |
| | | :columns="invoiceColumns" |
| | | :data-source="statement?.invoices ?? []" |
| | | :pagination="false" |
| | | size="small" |
| | | bordered |
| | | :scroll="{ x: 900 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'invoiceAmount'"> |
| | | <span class="text-blue-600"> |
| | | {{ formatMoney(record.invoiceAmount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'paidAmount'"> |
| | | {{ formatMoney(record.paidAmount) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'remainingPayable'"> |
| | | <span class="text-red-600"> |
| | | {{ formatMoney(record.remainingPayable) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'paymentStatus'"> |
| | | {{ getPaymentStatusLabel(record.paymentStatus) }} |
| | | </template> |
| | | </template> |
| | | </a-table> |
| | | </a-card> |
| | | <a-card title="仿¬¾æµæ°´" size="small" class="mt-4"> |
| | | <a-table |
| | | :columns="paymentColumns" |
| | | :data-source="statement?.payments ?? []" |
| | | :pagination="false" |
| | | size="small" |
| | | bordered |
| | | :scroll="{ x: 700 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'paymentAmount'"> |
| | | <span class="text-green-600"> |
| | | {{ formatMoney(record.paymentAmount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | {{ getStatusLabel(record.status) }} |
| | | </template> |
| | | </template> |
| | | </a-table> |
| | | </a-card> |
| | | </a-col> |
| | | </a-row> |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Col :span="6"> |
| | | <Card size="small"> |
| | | <div class="text-gray-500">累计éè´é¢</div> |
| | | <div class="text-2xl font-semibold"> |
| | | {{ formatMoney(statement?.summary?.totalPurchaseAmount) }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card size="small"> |
| | | <div class="text-gray-500">累计å¼ç¥¨é¢</div> |
| | | <div class="text-2xl font-semibold text-blue-600"> |
| | | {{ formatMoney(statement?.summary?.totalInvoiceAmount) }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card size="small"> |
| | | <div class="text-gray-500">ç´¯è®¡ä»æ¬¾é¢</div> |
| | | <div class="text-2xl font-semibold text-green-600"> |
| | | {{ formatMoney(statement?.summary?.totalPaymentAmount) }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="6"> |
| | | <Card size="small"> |
| | | <div class="text-gray-500">æªä»ä½é¢ï¼å¼ç¥¨ - 仿¬¾ï¼</div> |
| | | <div class="text-2xl font-semibold text-red-600"> |
| | | {{ formatMoney(statement?.summary?.unpaidAmount) }} |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | </Row> |
| | | |
| | | <Row :gutter="16"> |
| | | <Col :span="12"> |
| | | <Card title="éè´è®¢åæµæ°´ï¼å«å¼ç¥¨éé¢ï¼" size="small"> |
| | | <Table |
| | | :columns="orderColumns" |
| | | :data-source="statement?.orders ?? []" |
| | | :pagination="false" |
| | | bordered |
| | | size="small" |
| | | :scroll="{ x: 800 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'orderAmount'"> |
| | | {{ formatMoney(record.orderAmount) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'invoicedAmount'"> |
| | | <span class="text-blue-600"> |
| | | {{ formatMoney(record.invoicedAmount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'uninvoicedAmount'"> |
| | | <span class="text-red-600"> |
| | | {{ formatMoney(record.uninvoicedAmount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | {{ getStatusLabel(record.status) }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | </Col> |
| | | <Col :span="12"> |
| | | <Card |
| | | title="éè´æ¥ç¥¨æµæ°´ï¼å¼ç¥¨éé¢ / å·²ä» / å©ä½åºä»ï¼" |
| | | size="small" |
| | | > |
| | | <Table |
| | | :columns="invoiceColumns" |
| | | :data-source="statement?.invoices ?? []" |
| | | :pagination="false" |
| | | bordered |
| | | size="small" |
| | | :scroll="{ x: 900 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'invoiceAmount'"> |
| | | <span class="text-blue-600"> |
| | | {{ formatMoney(record.invoiceAmount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'paidAmount'"> |
| | | {{ formatMoney(record.paidAmount) }} |
| | | </template> |
| | | <template v-else-if="column.key === 'remainingPayable'"> |
| | | <span class="text-red-600"> |
| | | {{ formatMoney(record.remainingPayable) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'paymentStatus'"> |
| | | {{ getPaymentStatusLabel(record.paymentStatus) }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | <Card class="mt-4" size="small" title="仿¬¾æµæ°´"> |
| | | <Table |
| | | :columns="paymentColumns" |
| | | :data-source="statement?.payments ?? []" |
| | | :pagination="false" |
| | | bordered |
| | | size="small" |
| | | :scroll="{ x: 700 }" |
| | | > |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'paymentAmount'"> |
| | | <span class="text-green-600"> |
| | | {{ formatMoney(record.paymentAmount) }} |
| | | </span> |
| | | </template> |
| | | <template v-else-if="column.key === 'status'"> |
| | | {{ getStatusLabel(record.status) }} |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | </Col> |
| | | </Row> |
| | | </Spin> |
| | | </div> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { getEmployeeSimpleList } from '#/api/hrm/employee'; |
| | | import { getEmployeePage, getEmployeeSimpleList } from '#/api/hrm/employee'; |
| | | import type { HrmEmployeeApi } from '#/api/hrm/employee'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç¦»èç³è¯·ç表å */ |
| | | export function useFormSchema(formType: FormType): VbenFormSchema[] { |
| | | export function useFormSchema( |
| | | formType: FormType, |
| | | onApplicantChange?: (option?: HrmEmployeeApi.Employee) => void, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'userId', |
| | | label: 'ç³è¯·äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | // éæ©åå·¥åï¼value åå
³èçç³»ç»ç¨æ· IDï¼å端æ userId å
³èå工档æ¡ï¼ |
| | | api: getEmployeePage, |
| | | // å页æ¥å£ä¸é 200ï¼è¶
åºä¼æ¥âæ¯é¡µæ¡æ°æå¤§å¼ä¸º 200â |
| | | params: { pageNo: 1, pageSize: 200 }, |
| | | resultField: 'list', |
| | | labelField: 'name', |
| | | valueField: 'userId', |
| | | // æªå
³èç³»ç»è´¦å·æå·²ç¦»èçåå·¥ä¸è½ä½ä¸ºç³è¯·äºº |
| | | afterFetch: (res: { list?: HrmEmployeeApi.Employee[] }) => ({ |
| | | list: (res?.list ?? []).filter( |
| | | (item) => item.userId != null && item.employeeStatus !== 3, |
| | | ), |
| | | }), |
| | | placeholder: 'è¯·éæ©ç³è¯·äººï¼ä¸éé»è®¤ä¸ºæ¬äººï¼', |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | // ä»
æ°å¢æ¶å¯éæ©ï¼ä¿®æ¹æ¶ç³è¯·äººéå® |
| | | disabled: formType !== 'create', |
| | | onChange: (_value: unknown, option: unknown) => { |
| | | onApplicantChange?.(option as HrmEmployeeApi.Employee); |
| | | }, |
| | | }, |
| | | help: |
| | | formType === 'create' |
| | | ? 'ä¸éæ©æ¶é»è®¤ç³è¯·äººä¸ºå½åç»å½ç¨æ·ï¼éæ©åèªå¨å¸¦åºå²ä½' |
| | | : undefined, |
| | | }, |
| | | { |
| | | fieldName: 'postName', |
| | | label: 'å²ä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | disabled: true, |
| | | placeholder: 'éæ©ç³è¯·äººåèªå¨å¸¦åº', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'handoverUserId', |
| | | fieldName: 'handoverEmployeeId', |
| | | label: '交æ¥äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getEmployeeSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | // 已离èåå·¥ä¸è½ä½ä¸ºäº¤æ¥äººï¼ç´æ¥æé¤ |
| | | afterFetch: (list: HrmEmployeeApi.EmployeeSimple[]) => |
| | | (list ?? []).filter((item) => item.employeeStatus !== 3), |
| | | placeholder: 'è¯·éæ©äº¤æ¥äºº', |
| | | showSearch: true, |
| | | optionFilterProp: 'label', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'handoverEmployeeId', |
| | | label: '交æ¥åå·¥ID', |
| | | component: 'InputNumber', |
| | | componentProps: { class: '!w-full', min: 1, precision: 0, placeholder: '请è¾å
¥äº¤æ¥åå·¥ID' }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'reason', |
| | |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** æ´æ°äº¤æ¥ç¶æ */ |
| | | /** 宿工ä½äº¤æ¥ï¼å®¡æ¹éè¿å䏿¬¡ç¹å»ç´æ¥å®æï¼äº¤æ¥äººéå³è·å¾ç¦»è人çä¸å¡æ°æ® */ |
| | | async function handleUpdateHandover(row: HrmResignationApi.Resignation) { |
| | | // 交æ¥ç¶æï¼0-æªå¼å§ï¼10-è¿è¡ä¸ï¼20-已宿 |
| | | const nextStatus = row.handoverStatus === 0 ? 10 : row.handoverStatus === 10 ? 20 : 0; |
| | | const statusText = nextStatus === 10 ? 'è¿è¡ä¸' : nextStatus === 20 ? '已宿' : 'æªå¼å§'; |
| | | await updateHandoverStatus(row.id!, nextStatus); |
| | | message.success(`交æ¥ç¶æå·²æ´æ°ä¸ºï¼${statusText}`); |
| | | await updateHandoverStatus(row.id!, 20); |
| | | message.success('交æ¥å·²å®æï¼ç¦»è人çä¸å¡æ°æ®å·²è½¬ç»äº¤æ¥äºº'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | |
| | | onClick: handleCancel.bind(null, row), |
| | | }, |
| | | { |
| | | label: '交æ¥', |
| | | label: 'å®æäº¤æ¥', |
| | | type: 'link', |
| | | auth: ['hrm:resignation:update'], |
| | | ifShow: row.status === 20 && row.handoverStatus !== 20, |
| | | popConfirm: { |
| | | title: row.handoverStatus === 0 ? '确认å¼å§äº¤æ¥åï¼' : 'ç¡®è®¤å®æäº¤æ¥åï¼', |
| | | title: |
| | | 'ç¡®è®¤å®æäº¤æ¥åï¼ç¡®è®¤å离è人çä¸å¡æ°æ®å°è½¬ç»äº¤æ¥äººï¼ä¸ä¸å¯åé', |
| | | confirm: handleUpdateHandover.bind(null, row), |
| | | }, |
| | | }, |
| | |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setState({ schema: useFormSchema(data.formType) }); |
| | | formApi.setState({ |
| | | schema: useFormSchema(data.formType, (option) => { |
| | | // éå®ç³è¯·äººåèªå¨å¸¦åºå²ä½ |
| | | formApi.setFieldValue('postName', option?.postNames ?? ''); |
| | | }), |
| | | }); |
| | | formApi.setDisabled(data.formType === 'detail'); |
| | | modalApi.setState({ showConfirmButton: data.formType !== 'detail' }); |
| | | if (!data?.id) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { EChartsOption } from '@vben/plugins/echarts'; |
| | | |
| | | import type { HrmStatisticsApi } from '#/api/hrm/statistics'; |
| | | |
| | | /** ç»ä¸è²æ¿ï¼ä¿è¯åä¸é¡µé¢çå¾è¡¨è§æä¸è´ */ |
| | | const PALETTE = [ |
| | | '#3b82f6', |
| | | '#10b981', |
| | | '#f59e0b', |
| | | '#ef4444', |
| | | '#8b5cf6', |
| | | '#06b6d4', |
| | | '#ec4899', |
| | | '#84cc16', |
| | | ]; |
| | | |
| | | const BASE_TEXT_STYLE = { |
| | | fontSize: 12, |
| | | }; |
| | | |
| | | interface AxisTooltipParams { |
| | | dataIndex: number; |
| | | } |
| | | |
| | | const TITLE_STYLE = { |
| | | textStyle: { fontSize: 14, fontWeight: 'bold' as const }, |
| | | left: 'center' as const, |
| | | }; |
| | | |
| | | /** æ±ç¶å¾ï¼éç¨äºåºå®åºé´åå¸ï¼å·¥é¾ãå¹´é¾ï¼ */ |
| | | function getBarOption(title: string, data: HrmStatisticsApi.Item[]): EChartsOption { |
| | | return { |
| | | title: { text: title, ...TITLE_STYLE }, |
| | | color: PALETTE, |
| | | grid: { bottom: 40, left: 50, right: 20, top: 60 }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | formatter: '{b}<br/>{a}ï¼{c} 人', |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: data.map((item) => item.name), |
| | | axisLabel: { ...BASE_TEXT_STYLE, interval: 0 }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '人æ°', |
| | | minInterval: 1, |
| | | axisLabel: BASE_TEXT_STYLE, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: title, |
| | | type: 'bar', |
| | | barMaxWidth: 42, |
| | | itemStyle: { borderRadius: [6, 6, 0, 0] }, |
| | | label: { show: true, position: 'top', ...BASE_TEXT_STYLE }, |
| | | data: data.map((item) => item.value), |
| | | }, |
| | | ], |
| | | }; |
| | | } |
| | | |
| | | /** æ¨ªåæ¡å½¢å¾ï¼éç¨äºäººæ°æåç§°æåï¼é¨é¨ãå²ä½ï¼ */ |
| | | function getRankingBarOption( |
| | | title: string, |
| | | data: HrmStatisticsApi.Item[], |
| | | unit: string, |
| | | seriesName: string, |
| | | ): EChartsOption { |
| | | return { |
| | | title: { text: title, ...TITLE_STYLE }, |
| | | color: PALETTE, |
| | | grid: { bottom: 30, left: 110, right: 50, top: 60 }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | formatter: `{b}<br/>${seriesName}ï¼{c} ${unit}`, |
| | | }, |
| | | xAxis: { |
| | | type: 'value', |
| | | name: unit, |
| | | axisLabel: BASE_TEXT_STYLE, |
| | | }, |
| | | yAxis: { |
| | | type: 'category', |
| | | inverse: true, |
| | | data: data.map((item) => item.name), |
| | | axisLabel: BASE_TEXT_STYLE, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: seriesName, |
| | | type: 'bar', |
| | | barMaxWidth: 26, |
| | | itemStyle: { borderRadius: [0, 6, 6, 0] }, |
| | | label: { |
| | | show: true, |
| | | position: 'right', |
| | | formatter: `{c} ${unit}`, |
| | | ...BASE_TEXT_STYLE, |
| | | }, |
| | | data: data.map((item) => item.value), |
| | | }, |
| | | ], |
| | | }; |
| | | } |
| | | |
| | | /** ç¯å½¢å¾ï¼éç¨äºå æ¯ç±»åå¸ï¼å¦åãæ§å«ãç¶æãå©å§»ãæ¿æ²»é¢è²ï¼ */ |
| | | function getPieOption(title: string, data: HrmStatisticsApi.Item[]): EChartsOption { |
| | | return { |
| | | title: { text: title, ...TITLE_STYLE }, |
| | | color: PALETTE, |
| | | tooltip: { trigger: 'item', formatter: '{b}ï¼{c} 人ï¼{d}%ï¼' }, |
| | | legend: { |
| | | type: 'scroll', |
| | | orient: 'vertical', |
| | | left: 10, |
| | | top: 50, |
| | | textStyle: BASE_TEXT_STYLE, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: title, |
| | | type: 'pie', |
| | | radius: ['42%', '68%'], |
| | | center: ['58%', '58%'], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 }, |
| | | label: { show: true, formatter: '{b}\n{c} 人' }, |
| | | labelLine: { show: true }, |
| | | data: data.map((item) => ({ name: item.name, value: item.value })), |
| | | }, |
| | | ], |
| | | }; |
| | | } |
| | | |
| | | /** ååå°æé¢è¦ï¼æç´§æ¥ç¨åº¦çè² */ |
| | | function getContractWarningOption( |
| | | data: HrmStatisticsApi.Item[], |
| | | ): EChartsOption { |
| | | const colorMap: Record<string, string> = { |
| | | 已尿: '#ef4444', |
| | | '30天å
å°æ': '#f59e0b', |
| | | '31-90天尿': '#3b82f6', |
| | | '90天以ä¸': '#10b981', |
| | | æ åºå®æé: '#9ca3af', |
| | | }; |
| | | return { |
| | | title: { text: 'ååå°æé¢è¦', ...TITLE_STYLE }, |
| | | grid: { bottom: 40, left: 50, right: 20, top: 60 }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | formatter: '{b}<br/>{c} 份', |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: data.map((item) => item.name), |
| | | axisLabel: { ...BASE_TEXT_STYLE, interval: 0 }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: 'ååæ°', |
| | | minInterval: 1, |
| | | axisLabel: BASE_TEXT_STYLE, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'ååå°æé¢è¦', |
| | | type: 'bar', |
| | | barMaxWidth: 42, |
| | | label: { show: true, position: 'top', ...BASE_TEXT_STYLE }, |
| | | data: data.map((item) => ({ |
| | | name: item.name, |
| | | value: item.value, |
| | | itemStyle: { |
| | | color: colorMap[item.name] ?? '#3b82f6', |
| | | borderRadius: [6, 6, 0, 0], |
| | | }, |
| | | })), |
| | | }, |
| | | ], |
| | | }; |
| | | } |
| | | |
| | | export function getTenureDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getBarOption('å·¥é¾åå¸', data); |
| | | } |
| | | |
| | | export function getAgeDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getBarOption('å¹´é¾åå¸', data); |
| | | } |
| | | |
| | | export function getStatusDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getPieOption('人åç¶æææ', data); |
| | | } |
| | | |
| | | export function getDeptDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getRankingBarOption('é¨é¨äººæ°åå¸', data, '人', 'å¨å²äººæ°'); |
| | | } |
| | | |
| | | export function getPostDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getRankingBarOption('å²ä½åå¸', data, '人', 'å¨å²äººæ°'); |
| | | } |
| | | |
| | | export function getDeptTenureOption(data: HrmStatisticsApi.DeptTenure[]) { |
| | | return { |
| | | title: { text: 'é¨é¨å¹³åå·¥é¾', ...TITLE_STYLE }, |
| | | color: PALETTE, |
| | | grid: { bottom: 30, left: 110, right: 50, top: 60 }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | formatter: (params: AxisTooltipParams | AxisTooltipParams[]) => { |
| | | const item = Array.isArray(params) ? params[0] : params; |
| | | if (!item) { |
| | | return ''; |
| | | } |
| | | const dept = data[item.dataIndex]; |
| | | return `${dept?.deptName ?? ''}<br/>å¹³åå·¥é¾ï¼${dept?.averageTenureYears ?? 0} å¹´<br/>å¨å²äººæ°ï¼${dept?.count ?? 0} 人`; |
| | | }, |
| | | }, |
| | | xAxis: { |
| | | type: 'value', |
| | | name: 'å¹´', |
| | | axisLabel: BASE_TEXT_STYLE, |
| | | }, |
| | | yAxis: { |
| | | type: 'category', |
| | | inverse: true, |
| | | data: data.map((item) => item.deptName), |
| | | axisLabel: BASE_TEXT_STYLE, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'å¹³åå·¥é¾', |
| | | type: 'bar', |
| | | barMaxWidth: 26, |
| | | itemStyle: { borderRadius: [0, 6, 6, 0] }, |
| | | label: { |
| | | show: true, |
| | | position: 'right', |
| | | formatter: '{c} å¹´', |
| | | ...BASE_TEXT_STYLE, |
| | | }, |
| | | data: data.map((item) => item.averageTenureYears), |
| | | }, |
| | | ], |
| | | } as EChartsOption; |
| | | } |
| | | |
| | | export function getEducationDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getPieOption('å¦ååå¸', data); |
| | | } |
| | | |
| | | export function getGenderDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getPieOption('æ§å«åå¸', data); |
| | | } |
| | | |
| | | export function getMarriageDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getPieOption('å©å§»ç¶åµåå¸', data); |
| | | } |
| | | |
| | | export function getPoliticalDistributionOption(data: HrmStatisticsApi.Item[]) { |
| | | return getPieOption('æ¿æ²»é¢è²åå¸', data); |
| | | } |
| | | |
| | | export function getHireResignTrendOption(data: HrmStatisticsApi.Trend[]) { |
| | | return { |
| | | title: { text: 'è¿ 12 个æå
¥è / 离èè¶å¿', ...TITLE_STYLE }, |
| | | color: ['#10b981', '#ef4444'], |
| | | grid: { bottom: 50, left: 50, right: 30, top: 70 }, |
| | | tooltip: { trigger: 'axis', formatter: '{b}<br/>{a}ï¼{c} 人' }, |
| | | legend: { top: 30, data: ['å
¥è', '离è'] }, |
| | | xAxis: { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: data.map((item) => item.month.slice(2)), |
| | | axisLabel: { ...BASE_TEXT_STYLE, rotate: 30 }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '人æ°', |
| | | minInterval: 1, |
| | | axisLabel: BASE_TEXT_STYLE, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'å
¥è', |
| | | type: 'line', |
| | | smooth: true, |
| | | symbolSize: 6, |
| | | data: data.map((item) => item.hireCount), |
| | | }, |
| | | { |
| | | name: '离è', |
| | | type: 'line', |
| | | smooth: true, |
| | | symbolSize: 6, |
| | | data: data.map((item) => item.resignCount), |
| | | }, |
| | | ], |
| | | } as EChartsOption; |
| | | } |
| | | |
| | | export function getContractWarningChartOption(data: HrmStatisticsApi.Item[]) { |
| | | return getContractWarningOption(data); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | |
| | | import type { HrmStatisticsApi } from '#/api/hrm/statistics'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { ContentWrap, Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | |
| | | import { Spin, Table } from 'ant-design-vue'; |
| | | |
| | | import { getTenureStatistics } from '#/api/hrm/statistics'; |
| | | |
| | | import { |
| | | getAgeDistributionOption, |
| | | getContractWarningChartOption, |
| | | getDeptDistributionOption, |
| | | getDeptTenureOption, |
| | | getEducationDistributionOption, |
| | | getGenderDistributionOption, |
| | | getHireResignTrendOption, |
| | | getMarriageDistributionOption, |
| | | getPoliticalDistributionOption, |
| | | getPostDistributionOption, |
| | | getStatusDistributionOption, |
| | | getTenureDistributionOption, |
| | | } from './chartOptions'; |
| | | |
| | | defineOptions({ name: 'HrmHumanStatistics' }); |
| | | |
| | | const loading = ref(true); |
| | | const statistics = ref<HrmStatisticsApi.EmployeeStatistics>(); |
| | | |
| | | const statusChartRef = ref<EchartsUIType>(); |
| | | const trendChartRef = ref<EchartsUIType>(); |
| | | const tenureChartRef = ref<EchartsUIType>(); |
| | | const ageChartRef = ref<EchartsUIType>(); |
| | | const deptDistChartRef = ref<EchartsUIType>(); |
| | | const deptTenureChartRef = ref<EchartsUIType>(); |
| | | const postChartRef = ref<EchartsUIType>(); |
| | | const educationChartRef = ref<EchartsUIType>(); |
| | | const genderChartRef = ref<EchartsUIType>(); |
| | | const marriageChartRef = ref<EchartsUIType>(); |
| | | const politicalChartRef = ref<EchartsUIType>(); |
| | | const contractChartRef = ref<EchartsUIType>(); |
| | | |
| | | const { renderEcharts: renderStatusChart } = useEcharts(statusChartRef); |
| | | const { renderEcharts: renderTrendChart } = useEcharts(trendChartRef); |
| | | const { renderEcharts: renderTenureChart } = useEcharts(tenureChartRef); |
| | | const { renderEcharts: renderAgeChart } = useEcharts(ageChartRef); |
| | | const { renderEcharts: renderDeptDistChart } = useEcharts(deptDistChartRef); |
| | | const { renderEcharts: renderDeptTenureChart } = useEcharts(deptTenureChartRef); |
| | | const { renderEcharts: renderPostChart } = useEcharts(postChartRef); |
| | | const { renderEcharts: renderEducationChart } = useEcharts(educationChartRef); |
| | | const { renderEcharts: renderGenderChart } = useEcharts(genderChartRef); |
| | | const { renderEcharts: renderMarriageChart } = useEcharts(marriageChartRef); |
| | | const { renderEcharts: renderPoliticalChart } = useEcharts(politicalChartRef); |
| | | const { renderEcharts: renderContractChart } = useEcharts(contractChartRef); |
| | | |
| | | /** 顶鍿æ å¡ */ |
| | | const summaryCards = computed(() => { |
| | | const data = statistics.value; |
| | | if (!data) { |
| | | return []; |
| | | } |
| | | return [ |
| | | { |
| | | label: 'å¨å²äººæ°', |
| | | value: data.totalCount ?? 0, |
| | | unit: '人', |
| | | desc: `å¨è ${data.activeCount ?? 0} · è¯ç¨ ${data.probationCount ?? 0}`, |
| | | }, |
| | | { |
| | | label: 'å¹³åå·¥é¾', |
| | | value: data.averageTenureYears ?? 0, |
| | | unit: 'å¹´', |
| | | desc: 'ä»
ç»è®¡å¨å²åå·¥', |
| | | }, |
| | | { |
| | | label: 'å¹³åå¹´é¾', |
| | | value: data.averageAge ?? 0, |
| | | unit: 'å²', |
| | | desc: 'ä»
ç»è®¡å¨å²åå·¥', |
| | | }, |
| | | { |
| | | label: 'æ¬å¹´å
¥è', |
| | | value: data.thisYearHireCount ?? 0, |
| | | unit: '人', |
| | | desc: 'æå
¥èæ¥æç»è®¡', |
| | | }, |
| | | { |
| | | label: 'æ¬å¹´ç¦»è', |
| | | value: data.thisYearResignCount ?? 0, |
| | | unit: '人', |
| | | desc: 'æç¦»èæ¥æç»è®¡', |
| | | }, |
| | | { |
| | | label: '离èåå·¥', |
| | | value: data.resignedCount ?? 0, |
| | | unit: '人', |
| | | desc: 'å工档æ¡ç´¯è®¡', |
| | | }, |
| | | ]; |
| | | }); |
| | | |
| | | /** 工龿è¡è¡¨æ ¼å */ |
| | | const topTenureColumns = [ |
| | | { title: 'æå', dataIndex: 'rank', width: 80 }, |
| | | { title: 'å§å', dataIndex: 'name' }, |
| | | { title: 'é¨é¨', dataIndex: 'deptName' }, |
| | | { title: 'å
¥èæ¥æ', dataIndex: 'hireDate', width: 140 }, |
| | | { title: 'å·¥é¾ï¼å¹´ï¼', dataIndex: 'tenureYears', width: 120 }, |
| | | ]; |
| | | |
| | | /** å·¥é¾æè¡æ°æ®ï¼è¡¥å
æåå */ |
| | | const topTenureData = computed(() => |
| | | (statistics.value?.topTenureEmployees ?? []).map((item, index) => ({ |
| | | ...item, |
| | | rank: index + 1, |
| | | })), |
| | | ); |
| | | |
| | | async function loadStatistics() { |
| | | loading.value = true; |
| | | try { |
| | | const data = await getTenureStatistics(); |
| | | statistics.value = data; |
| | | await Promise.all([ |
| | | renderStatusChart(getStatusDistributionOption(data.statusDistribution ?? [])), |
| | | renderTrendChart(getHireResignTrendOption(data.hireResignTrend ?? [])), |
| | | renderTenureChart( |
| | | getTenureDistributionOption(data.tenureDistribution ?? []), |
| | | ), |
| | | renderAgeChart(getAgeDistributionOption(data.ageDistribution ?? [])), |
| | | renderDeptDistChart( |
| | | getDeptDistributionOption(data.deptDistribution ?? []), |
| | | ), |
| | | renderDeptTenureChart(getDeptTenureOption(data.deptTenureList ?? [])), |
| | | renderPostChart(getPostDistributionOption(data.postDistribution ?? [])), |
| | | renderEducationChart( |
| | | getEducationDistributionOption(data.educationDistribution ?? []), |
| | | ), |
| | | renderGenderChart( |
| | | getGenderDistributionOption(data.genderDistribution ?? []), |
| | | ), |
| | | renderMarriageChart( |
| | | getMarriageDistributionOption(data.marriageDistribution ?? []), |
| | | ), |
| | | renderPoliticalChart( |
| | | getPoliticalDistributionOption(data.politicalDistribution ?? []), |
| | | ), |
| | | renderContractChart( |
| | | getContractWarningChartOption(data.contractWarning ?? []), |
| | | ), |
| | | ]); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadStatistics(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <Spin :spinning="loading" tip="å è½½ä¸..."> |
| | | <div class="flex flex-col gap-4"> |
| | | <!-- ææ å¡ --> |
| | | <div class="grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-6"> |
| | | <div |
| | | v-for="card in summaryCards" |
| | | :key="card.label" |
| | | class="rounded-lg border border-border bg-background p-4" |
| | | > |
| | | <div class="text-sm text-muted-foreground">{{ card.label }}</div> |
| | | <div class="mt-1 flex items-baseline gap-1"> |
| | | <span class="text-2xl font-semibold">{{ card.value }}</span> |
| | | <span class="text-sm text-muted-foreground">{{ card.unit }}</span> |
| | | </div> |
| | | <div class="mt-1 text-xs text-muted-foreground">{{ card.desc }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- äººåæ¦åµï¼ç¶æææ + è¿ 12 个æå
¥ç¦»èè¶å¿ --> |
| | | <ContentWrap> |
| | | <div class="grid grid-cols-1 gap-4 xl:grid-cols-3"> |
| | | <EchartsUI ref="statusChartRef" height="320px" /> |
| | | <div class="xl:col-span-2"> |
| | | <EchartsUI ref="trendChartRef" height="320px" /> |
| | | </div> |
| | | </div> |
| | | </ContentWrap> |
| | | |
| | | <!-- 人åç»æï¼å·¥é¾ / å¹´é¾ / é¨é¨äººæ° --> |
| | | <ContentWrap> |
| | | <div class="grid grid-cols-1 gap-4 xl:grid-cols-3"> |
| | | <EchartsUI ref="tenureChartRef" height="320px" /> |
| | | <EchartsUI ref="ageChartRef" height="320px" /> |
| | | <EchartsUI ref="deptDistChartRef" height="320px" /> |
| | | </div> |
| | | </ContentWrap> |
| | | |
| | | <!-- é¨é¨å¹³åå·¥é¾ + å²ä½åå¸ --> |
| | | <ContentWrap> |
| | | <div class="grid grid-cols-1 gap-4 xl:grid-cols-2"> |
| | | <EchartsUI ref="deptTenureChartRef" height="340px" /> |
| | | <EchartsUI ref="postChartRef" height="340px" /> |
| | | </div> |
| | | </ContentWrap> |
| | | |
| | | <!-- 人åç»åï¼å¦å / æ§å« / å©å§» / æ¿æ²»é¢è² --> |
| | | <ContentWrap> |
| | | <div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"> |
| | | <EchartsUI ref="educationChartRef" height="320px" /> |
| | | <EchartsUI ref="genderChartRef" height="320px" /> |
| | | <EchartsUI ref="marriageChartRef" height="320px" /> |
| | | <EchartsUI ref="politicalChartRef" height="320px" /> |
| | | </div> |
| | | </ContentWrap> |
| | | |
| | | <!-- ååé£é© --> |
| | | <ContentWrap> |
| | | <div class="grid grid-cols-1 gap-4 xl:grid-cols-3"> |
| | | <div class="xl:col-span-2"> |
| | | <EchartsUI ref="contractChartRef" height="320px" /> |
| | | </div> |
| | | <div class="flex flex-col justify-center gap-3"> |
| | | <div class="rounded-lg border border-border bg-background p-4"> |
| | | <div class="text-sm text-muted-foreground">å¨å²æªç¾ææåå</div> |
| | | <div class="mt-1 flex items-baseline gap-1"> |
| | | <span class="text-2xl font-semibold"> |
| | | {{ statistics?.noContractCount ?? 0 }} |
| | | </span> |
| | | <span class="text-sm text-muted-foreground">人</span> |
| | | </div> |
| | | <div class="mt-1 text-xs text-muted-foreground"> |
| | | å¨å²å工䏿²¡æå½åçæååçåå·¥æ° |
| | | </div> |
| | | </div> |
| | | <div class="text-xs text-muted-foreground"> |
| | | å£å¾ï¼ä»
ç»è®¡å¨å²åå·¥çå½åçæååï¼å·²è§£é¤/å·²ç»æ¢ååä¸è®¡å
¥ï¼æ åºå®æéååä¸è®¡å
¥å°æå¤©æ°ã |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </ContentWrap> |
| | | |
| | | <!-- å·¥é¾æè¡ --> |
| | | <ContentWrap title="å·¥é¾æè¡ TOP 10"> |
| | | <Table |
| | | :columns="topTenureColumns" |
| | | :data-source="topTenureData" |
| | | :pagination="false" |
| | | row-key="id" |
| | | size="small" |
| | | /> |
| | | </ContentWrap> |
| | | </div> |
| | | </Spin> |
| | | </Page> |
| | | </template> |
| | |
| | | autoResize: true, |
| | | border: true, |
| | | columns: [ |
| | | { field: 'machineryCode', title: '设å¤ç¼ç ', minWidth: 140 }, |
| | | { field: 'machineryCode', title: '设å¤ç¼å·', minWidth: 140 }, |
| | | { field: 'machineryName', title: '设å¤åç§°', minWidth: 150 }, |
| | | { field: 'machineryBrand', title: 'åç', minWidth: 120 }, |
| | | { field: 'machinerySpecification', title: 'è§æ ¼åå·', minWidth: 140 }, |
| | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesDvCheckRecordApi.CheckRecord>['columns'] { |
| | | return [ |
| | | { field: 'machineryCode', title: '设å¤ç¼ç ', minWidth: 140 }, |
| | | { field: 'machineryCode', title: '设å¤ç¼å·', minWidth: 140 }, |
| | | { |
| | | field: 'machineryName', |
| | | title: '设å¤åç§°', |
| | |
| | | <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false"> |
| | | <template #title> |
| | | <div v-if="selectedItem" class="leading-6"> |
| | | <div>设å¤ç¼ç ï¼{{ selectedItem.code || '-' }}</div> |
| | | <div>设å¤ç¼å·ï¼{{ selectedItem.code || '-' }}</div> |
| | | <div>设å¤åç§°ï¼{{ selectedItem.name || '-' }}</div> |
| | | <div v-if="selectedItem.brand">åçï¼{{ selectedItem.brand }}</div> |
| | | <div v-if="selectedItem.specification"> |
| | |
| | | }, |
| | | { |
| | | fieldName: 'code', |
| | | label: '设å¤ç¼ç ', |
| | | label: '设å¤ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è®¾å¤ç¼ç ', |
| | | placeholder: '坿å¨å¡«åï¼æç¹å»å³ä¾§âèªå¨çæâ', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['id'], |
| | | componentProps: (values) => ({ disabled: !!values.id }), |
| | | }, |
| | | rules: 'required', |
| | | help: 'æ¯ææå¨å¡«åï¼ä¹å¯ç¹å»å³ä¾§æé®ç±ç³»ç»æç¼ç è§åèªå¨çæ', |
| | | suffix: |
| | | formType === 'detail' |
| | | ? undefined |
| | |
| | | await formApi?.setFieldValue('code', code); |
| | | }, |
| | | }, |
| | | { default: () => 'çæ' }, |
| | | { default: () => 'èªå¨çæ' }, |
| | | ), |
| | | }, |
| | | { |
| | |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: '设å¤ç¼ç ', |
| | | label: '设å¤ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥è®¾å¤ç¼ç ', |
| | | placeholder: '请è¾å
¥è®¾å¤ç¼å·', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | return [ |
| | | { |
| | | field: 'code', |
| | | title: '设å¤ç¼ç ', |
| | | title: '设å¤ç¼å·', |
| | | minWidth: 140, |
| | | slots: { |
| | | default: 'code', |
| | |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: '设å¤ç¼ç ', |
| | | label: '设å¤ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥è®¾å¤ç¼ç ', |
| | | placeholder: '请è¾å
¥è®¾å¤ç¼å·', |
| | | }, |
| | | }, |
| | | { |
| | |
| | | ): VxeTableGridOptions<MesDvMachineryApi.Machinery>['columns'] { |
| | | return [ |
| | | { type: multiple ? 'checkbox' : 'radio', width: 50 }, |
| | | { field: 'code', title: '设å¤ç¼ç ', width: 120 }, |
| | | { field: 'code', title: '设å¤ç¼å·', width: 120 }, |
| | | { field: 'name', title: '设å¤åç§°', minWidth: 120 }, |
| | | { field: 'brand', title: 'åç', minWidth: 120 }, |
| | | { field: 'specification', title: 'è§æ ¼åå·', minWidth: 120 }, |
| | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesDvMaintenRecordApi.MaintenRecord>['columns'] { |
| | | return [ |
| | | { field: 'machineryCode', title: '设å¤ç¼ç ', minWidth: 140 }, |
| | | { field: 'machineryCode', title: '设å¤ç¼å·', minWidth: 140 }, |
| | | { |
| | | field: 'machineryName', |
| | | title: '设å¤åç§°', |
| | |
| | | columns: [ |
| | | { |
| | | field: 'machineryCode', |
| | | title: '设å¤ç¼ç ', |
| | | title: '设å¤ç¼å·', |
| | | width: 140, |
| | | }, |
| | | { |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | import { uploadFile, type SystemStorageApi } from '#/api/system/storage'; |
| | | |
| | | const props = defineProps<{ qcType: 'iqc' | 'ipqc' | 'oqc' | 'rqc'; qcId: number; readonly?: boolean }>(); |
| | | const props = defineProps<{ qcType: 'iqc' | 'ipqc' | 'oqc' | 'rqc' | 'patrol'; qcId: number; readonly?: boolean }>(); |
| | | const { hasAccessByCodes, hasAccessByRoles } = useAccess(); |
| | | const canEdit = computed(() => !props.readonly && (hasAccessByRoles(['super_admin']) || hasAccessByCodes([`mes:qc-${props.qcType}:update`]))); |
| | | const files = ref<SystemStorageApi.StorageBlob[]>([]); |
| | |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'blobIds', |
| | | label: 'éä»¶', |
| | | component: 'FileUpload', |
| | | componentProps: { |
| | | valueKey: 'id', |
| | | maxNumber: 10, |
| | | multiple: true, |
| | | placeholder: 'å¯ä¸ä¼ å
¬åç¸å
³éä»¶', |
| | | }, |
| | | help: 'æ¯æå¤éï¼å个æä»¶å¤§å°åç³»ç»ä¸ä¼ é
ç½®éå¶', |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'å
¬åç¶æ', |
| | | component: 'RadioGroup', |
| | |
| | | formData.value = await getNotice(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | // éä»¶åæ¾ï¼FileUpload éè¦ { id, name, url, status } |
| | | await formApi.setFieldValue( |
| | | 'blobIds', |
| | | (formData.value.attachmentList ?? []).map((item) => ({ |
| | | uid: String(item.id), |
| | | name: item.name || '', |
| | | url: item.url || '', |
| | | status: 'done', |
| | | id: item.id, |
| | | })), |
| | | ); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | formApi.setFieldValue('vendorId', item.supplierId); |
| | | formApi.setFieldValue('vendorName', item.supplierName); |
| | | } |
| | | if (formApi) { |
| | | formApi.setFieldValue('contractNo', item?.contractNo); |
| | | } |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractNo', |
| | | label: 'éè´ååå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'éæ©éè´è®¢ååèªå¨å¸¦å
¥', |
| | | disabled: true, |
| | | }, |
| | | help: 'æ¢è´§éæ°å
¥åºæ¶æ²¿ç¨åéè´§åçéè´ååå·ï¼ä¾¿äºè¿½æº¯', |
| | | }, |
| | | { |
| | | fieldName: 'vendorId', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractNo', |
| | | label: 'éè´ååå·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥éè´ååå·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'vendorName', |
| | | label: 'ä¾åºå', |
| | | component: 'Input', |
| | |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'contractNo', |
| | | title: 'éè´ååå·', |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'vendorName', |
| | | title: 'ä¾åºååç§°', |
| | | minWidth: 120, |
| | |
| | | } |
| | | }, |
| | | }, |
| | | help: 'ä¿ååèªå¨å¸¦å
¥ä¾åºåãéè´ååå·ï¼ä»¥å订åä¸å¯éçç©æè¡ï¼ç©æä¸å¯æå·¥æ´æ¢ï¼é¿å
ä¸éè´è®¢å对ä¸ä¸å·ã', |
| | | }, |
| | | { |
| | | fieldName: 'contractNo', |
| | |
| | | export function useLineFormSchema( |
| | | vendorId?: number, |
| | | formApi?: VbenFormApi, |
| | | lockItem = false, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©äº§åç©æ', |
| | | disabled: lockItem, |
| | | }, |
| | | help: lockItem |
| | | ? 'ç©æç±æééè´è®¢åèªå¨å¸¦å
¥ï¼ä¸å¯ä¿®æ¹ï¼å¦éæ´æ¢ç©æï¼è¯·ä¿®æ¹éè´§åçéè´è®¢åã' |
| | | : undefined, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | |
| | | /** æäº¤å®¡æ¹ï¼BPMï¼éè´ -> è´¨é -> ä»åºï¼ */ |
| | | async function handleSubmitApproval(row: MesWmReturnVendorApi.ReturnVendor) { |
| | | // è´¨éé¨åèµ·éè´§å¿
é¡»ä¸ä¼ éä»¶ï¼ä¼å
ç¨å端已带åçéä»¶å¿«ç
§ï¼é¿å
夿䏿¬¡æ¥å£ |
| | | if (!row.attachmentList || row.attachmentList.length === 0) { |
| | | message.warning( |
| | | `éè´§å ${row.code} æªä¸ä¼ éä»¶ï¼è´¨éé¨åèµ·éè´§å¿
é¡»ä¸ä¼ è´¨æ£æ¥åãä¸åæ ¼ç
§ççéä»¶åæè½æäº¤å®¡æ¹ï¼è¯·å
ç¼è¾åæ®ä¸ä¼ éä»¶`, |
| | | ); |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: 'æäº¤å®¡æ¹ä¸...', |
| | | duration: 0, |
| | |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¿åº¦ï¼BPM æµç¨å®ä¾ï¼ */ |
| | | function handleProcessDetail(row: MesWmReturnVendorApi.ReturnVendor) { |
| | | if (!row.processInstanceId) { |
| | | message.warning( |
| | | `éè´§å ${row.code} æ²¡ææµç¨å®ä¾ï¼å¯è½å¼å¯äºå
¨å±å
审æ¹ï¼æå°æªæäº¤å®¡æ¹`, |
| | | ); |
| | | return; |
| | | } |
| | | router.push({ |
| | | name: 'BpmProcessInstanceDetail', |
| | | query: { id: row.processInstanceId }, |
| | | }); |
| | | } |
| | | |
| | | /** æ§è¡æ£è´§ */ |
| | |
| | | }, |
| | | }, |
| | | { |
| | | label: '审æ¹è¿åº¦', |
| | | type: 'link', |
| | | ifShow: row.status === MesWmReturnVendorStatusEnum.BPM_APPROVING, |
| | | onClick: handleProcessDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'æ§è¡æ£è´§', |
| | | type: 'link', |
| | | auth: ['mes:wm-return-vendor:update'], |
| | |
| | | formType.value === 'update' && |
| | | formData.value?.status === MesWmReturnVendorStatusEnum.PREPARE, |
| | | ); |
| | | /** 宿éè´§äºæ¬¡ç¡®è®¤ææ¡ï¼æå¤çæ¹å¼è¯´æä¼è§¦åçè´¢å¡å²çº¢ / æ¢è´§éå
¥åº */ |
| | | const finishConfirmTitle = computed(() => { |
| | | if (formData.value?.handleType === 1) { |
| | | const amount = formData.value?.refundAmount; |
| | | return `ç¡®è®¤å®æéè´§ï¼å®æåç³»ç»å°èªå¨çæè´æ°å²çº¢ä»æ¬¾å${ |
| | | amount !== null && amount !== undefined ? `ï¼é款éé¢ ${amount} å
ï¼` : '' |
| | | }ï¼æµç¨ä¸å¯æ¤éã`; |
| | | } |
| | | if (formData.value?.handleType === 2) { |
| | | return 'ç¡®è®¤å®æéè´§ï¼å®æåç³»ç»å°èªå¨çææ¢è´§å°è´§éç¥åå¹¶éæ°åèµ·æ¥æè´¨æ£ï¼å°è´§éç¥å沿ç¨åéè´ååå·ã'; |
| | | } |
| | | return 'ç¡®è®¤å®æè¯¥éè´§åå¹¶æ§è¡éè´§ï¼'; |
| | | }); |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'detail') { |
| | | return $t('ui.actionTitle.view', ['ä¾åºåéè´§å']); |
| | |
| | | async function handleSubmit() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid || !formData.value?.id) { |
| | | return; |
| | | } |
| | | // è´¨éé¨åèµ·éè´§å¿
é¡»ä¸ä¼ éä»¶ï¼é¿å
审æ¹äººçä¸å°éè´§ä¾æ® |
| | | const values = await formApi.getValues(); |
| | | if (!values.blobIds || values.blobIds.length === 0) { |
| | | message.warning( |
| | | 'æªä¸ä¼ éè´§éä»¶ï¼è´¨éé¨åèµ·éè´§å¿
é¡»ä¸ä¼ è´¨æ£æ¥åãä¸åæ ¼ç
§ççéä»¶åæè½æäº¤å®¡æ¹', |
| | | ); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | await finishReturnVendor(formData.value.id); |
| | | message.success('宿æå'); |
| | | message.success( |
| | | formData.value.handleType === 1 |
| | | ? '宿éè´§æåï¼å·²çæè´æ°å²çº¢ä»æ¬¾åï¼å¯å¨è´¢å¡-仿¬¾åæåå·æ¥è¯¢' |
| | | : '宿éè´§æåï¼å·²çææ¢è´§å°è´§éç¥åå¹¶éæ°åèµ·è´¨æ£ï¼è¯·å°ãå°è´§éç¥ãæ¥ç', |
| | | ); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | |
| | | <div class="mx-4"> |
| | | <LineList |
| | | :form-type="formType" |
| | | :purchase-order-id="formData.purchaseOrderId" |
| | | :return-id="formData.id" |
| | | :vendor-id="formData.vendorId" |
| | | /> |
| | |
| | | <div class="flex flex-auto items-center gap-2"> |
| | | <Popconfirm |
| | | v-if="canSubmit" |
| | | title="确认æäº¤è¯¥éè´§åï¼ãæäº¤åå°ä¸è½ä¿®æ¹ã" |
| | | :title="`确认æäº¤å®¡æ¹ï¼æµç¨ï¼éè´å®¡æ ¸ â è´¨éå®¡æ ¸ â ä»åºå®¡æ ¸ï¼æäº¤åå°ä¸è½ä¿®æ¹ï¼æªä¸ä¼ éä»¶å°æ æ³æäº¤`" |
| | | @confirm="handleSubmit" |
| | | > |
| | | <Button type="primary">æäº¤</Button> |
| | |
| | | </Button> |
| | | <Popconfirm |
| | | v-if="isFinish" |
| | | title="ç¡®è®¤å®æè¯¥éè´§åå¹¶æ§è¡éè´§ï¼" |
| | | :title="finishConfirmTitle" |
| | | @confirm="handleFinish" |
| | | > |
| | | <Button type="primary">宿éè´§</Button> |
| | |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | purchaseOrderId?: number; |
| | | returnId: number; |
| | | vendorId?: number; |
| | | }>(); |
| | | returnId.value = data.returnId; |
| | | formApi.setState({ schema: useLineFormSchema(data.vendorId, formApi) }); |
| | | // å
³èéè´è®¢åæ¶ï¼ç©æåªè½æ¥èªè®¢åæç»ï¼ç©æéæ©ç¦ç¨ï¼åªå
è®¸æ¹æ°é/æ¹æ¬¡/夿³¨ |
| | | formApi.setState({ |
| | | schema: useLineFormSchema(data.vendorId, formApi, !!data.purchaseOrderId), |
| | | }); |
| | | if (!data.id) { |
| | | return; |
| | | } |
| | |
| | | import type { MesWmReturnVendorDetailApi } from '#/api/mes/wm/returnvendor/detail'; |
| | | import type { MesWmReturnVendorLineApi } from '#/api/mes/wm/returnvendor/line'; |
| | | |
| | | import { computed, reactive } from 'vue'; |
| | | import { computed, reactive, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getReturnVendorDetailListByLineId } from '#/api/mes/wm/returnvendor/detail'; |
| | | import { importReturnVendorPurchaseOrderItems } from '#/api/mes/wm/returnvendor'; |
| | | import { |
| | | deleteReturnVendorLine, |
| | | getReturnVendorLinePage, |
| | |
| | | |
| | | const props = defineProps<{ |
| | | formType: FormType; |
| | | purchaseOrderId?: number; |
| | | returnId: number; |
| | | vendorId?: number; |
| | | }>(); |
| | | |
| | | /** æ¯å¦å·²å
³èéè´è®¢åï¼ç©æç±è®¢åèªå¨å¸¦å
¥ï¼ä¸å
许æå·¥æ°å¢/æ¹æ */ |
| | | const isLinkedToPurchaseOrder = computed(() => !!props.purchaseOrderId); |
| | | /** 已带å
¥çç©æè¡æ°ï¼null è¡¨ç¤ºå°æªæ¥è¯¢ï¼ */ |
| | | const lineCount = ref<null | number>(null); |
| | | |
| | | const isEditable = computed(() => |
| | | // æ¯å¦å¯ç¼è¾æç»è¡ |
| | |
| | | |
| | | /** æ·»å ç©æ */ |
| | | function handleCreate() { |
| | | if (isLinkedToPurchaseOrder.value) { |
| | | message.warning('å·²å
³èéè´è®¢åï¼ç©æç±è®¢åæç»èªå¨å¸¦å
¥ï¼ä¸è½æå·¥æ°å¢'); |
| | | return; |
| | | } |
| | | lineFormModalApi |
| | | .setData({ returnId: props.returnId, vendorId: props.vendorId }) |
| | | .open(); |
| | | } |
| | | |
| | | /** ä»éè´è®¢å鿰另å
¥å¯éç©æï¼å
¥åºåçå¨éè´§åä¿åä¹åæ¶ï¼éè¦æå¨è§¦å䏿¬¡ */ |
| | | async function handleImportItems() { |
| | | if (!props.returnId) { |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: 'æ£å¨ä»éè´è®¢å带å
¥å¯éç©æ...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const count = await importReturnVendorPurchaseOrderItems(props.returnId); |
| | | if (count > 0) { |
| | | message.success(`å·²ä»éè´è®¢å带å
¥ ${count} æ¡å¯éç©æ`); |
| | | } else { |
| | | message.info( |
| | | 'éè´è®¢åææ å¯éç©æï¼æç»å°æªå
¥åºï¼æå·²å
¨é¨éå®ã请å
å¨éè´å
¥åº/è´¨æ£æµç¨å®æå
¥åºåå带å
¥', |
| | | ); |
| | | } |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | | function handleEdit(row: MesWmReturnVendorLineApi.ReturnVendorLine) { |
| | | lineFormModalApi |
| | | .setData({ id: row.id, returnId: props.returnId, vendorId: props.vendorId }) |
| | | .setData({ |
| | | id: row.id, |
| | | purchaseOrderId: props.purchaseOrderId, |
| | | returnId: props.returnId, |
| | | vendorId: props.vendorId, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | |
| | | ajax: { |
| | | query: async ({ page }) => { |
| | | if (!props.returnId) { |
| | | lineCount.value = 0; |
| | | return { list: [], total: 0 }; |
| | | } |
| | | return await getReturnVendorLinePage({ |
| | | const result = await getReturnVendorLinePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | returnId: props.returnId, |
| | | }); |
| | | lineCount.value = result.list?.length ?? 0; |
| | | return result; |
| | | }, |
| | | }, |
| | | }, |
| | |
| | | <div> |
| | | <LineFormModal @success="handleRefresh" /> |
| | | <DetailFormModal @success="handleDetailSuccess" /> |
| | | <div |
| | | v-if="isEditable && isLinkedToPurchaseOrder && lineCount === 0" |
| | | class="mb-2 text-xs text-red-500" |
| | | > |
| | | 该éè´è®¢åææ å¯éç©æï¼æç»å°æªå
¥åºï¼æå·²å
¨é¨éå®ã请å
å¨éè´å
¥åº/è´¨æ£æµç¨å®æå
¥åºï¼å
¥åºåç¹å»ã鿰另å
¥è®¢åç©æãï¼å¦ä»æ ç©æï¼è¯·æ´æ¢éè´è®¢åã |
| | | </div> |
| | | <div |
| | | v-else-if="isEditable && isLinkedToPurchaseOrder" |
| | | class="mb-2 text-xs text-gray-500" |
| | | > |
| | | ç©æç±æééè´è®¢åèªå¨å¸¦å
¥ï¼ä¸å¯æå·¥æ°å¢ææ´æ¢ï¼éè´å
¥åºåå¯ç¨ã鿰另å
¥è®¢åç©æã补带æ°å
¥åºçç©æï¼å
¶ä½å¯ä¿®æ¹éè´§æ°éææ´è¡å é¤ä¸éè¦éçç©æã |
| | | </div> |
| | | <div |
| | | v-else-if="isEditable && !isLinkedToPurchaseOrder" |
| | | class="mb-2 text-xs text-gray-500" |
| | | > |
| | | æªå
³èéè´è®¢åæ¶å¯æå·¥æ·»å ç©æï¼å»ºè®®å
éæ©éè´è®¢åï¼ç±è®¢åæç»èªå¨å¸¦å
¥ï¼é¿å
ç©æå¯¹ä¸ä¸å·ã |
| | | </div> |
| | | <Grid table-title="ç©æä¿¡æ¯"> |
| | | <template v-if="isEditable" #toolbar-tools> |
| | | <template |
| | | v-if="isEditable && !isLinkedToPurchaseOrder" |
| | | #toolbar-tools |
| | | > |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template |
| | | v-else-if="isEditable && isLinkedToPurchaseOrder" |
| | | #toolbar-tools |
| | | > |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: '鿰另å
¥è®¢åç©æ', |
| | | type: 'primary', |
| | | onClick: handleImportItems, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #detail="{ row }"> |
| | | <DetailList |
| | | :details="getExpandedDetails(row)" |