已添加7个文件
已删除8个文件
已重命名1个文件
已修改51个文件
4117 ■■■■■ 文件已修改
company.config.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/aftersales/return.ts 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/aftersales/ticket.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/oa/notice/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/bidBond/index.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/request/index.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/statement/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/resignation/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/statistics/index.ts 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/checkplan/machinery/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/checkrecord/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/machinery/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/maintenrecord/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/repair/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/workstation/machine/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/arrivalnotice/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/returnvendor/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/bidBond/index.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/notice/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/aftersales.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/return/data.ts 318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/return/detail.vue 356 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/return/index.vue 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/return/modules/form.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/data.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/detail.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/modules/form.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/oa/notice/data.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/oa/notice/detail.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/oa/notice/modules/form.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/bidBond/data.ts 290 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/bidBond/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/bidBond/modules/form.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/components/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/components/payment-date-picker.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/data.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/form.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/item-form.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/item-form.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/order-form.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/statement/index.vue 531 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/data.ts 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/modules/form.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/statistics/chartOptions.ts 310 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/statistics/index.vue 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/checkplan/modules/machinery-list.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/checkrecord/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/components/select.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/data.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/maintenrecord/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/workstation/modules/machine-list.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/components/qc-attachments.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/bidBond/data.ts 255 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/bidBond/modules/form.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/notice/data.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/notice/modules/form.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/arrivalnotice/data.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/modules/form.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/modules/line-form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/modules/line-list.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts
@@ -23,70 +23,10 @@
    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',
  },
};
src/api/aftersales/return.ts
ÎļþÒÑɾ³ý
src/api/aftersales/ticket.ts
@@ -20,6 +20,22 @@
    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;
@@ -35,6 +51,18 @@
    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;
@@ -61,8 +89,10 @@
    updateTime?: string;
    /** é™„ä»¶ blobId åˆ—表(保存时传入) */
    blobIds?: number[];
    /** é™„件列表(详情时返回) */
    /** é€šç”¨é™„件列表(历史数据) */
    attachmentList?: { id: number; name?: string; url?: string }[];
    /** æµè½¬è®°å½•列表(详情时返回,按时间正序) */
    flowList?: FlowRecord[];
    items?: TicketItem[];
  }
}
@@ -97,13 +127,6 @@
    '/aftersales/ticket/page',
    { params },
  );
}
/** åˆ†é…å¤„理人 */
export function assignTicket(id: number, handlerUserId: number) {
  return requestClient.post<boolean>('/aftersales/ticket/assign', null, {
    params: { id, handlerUserId },
  });
}
/** é—®é¢˜åˆ¤å®š */
src/api/bpm/oa/notice/index.ts
@@ -1,6 +1,7 @@
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 {
@@ -15,6 +16,8 @@
    publishTime: string;
    createTime: string;
    userIds?: number[];
    /** é™„件列表(详情回显) */
    attachmentList?: SystemStorageApi.StorageBlob[];
  }
  export interface NoticeSaveReqVO {
@@ -26,6 +29,8 @@
    sendScope: number;
    isTop?: number;
    userIds?: number[];
    /** é™„ä»¶ blobId åˆ—表 */
    blobIds?: number[];
  }
}
src/api/crm/bidBond/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
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 } },
  );
}
src/api/erp/purchase/request/index.ts
@@ -76,8 +76,8 @@
  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 } });
}
src/api/erp/purchase/statement/index.ts
@@ -42,6 +42,19 @@
    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;
@@ -52,6 +65,18 @@
  }
}
/** æŸ¥è¯¢å„供应商往来汇总(左侧供应商总览),口径与单个供应商详情一致 */
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;
src/api/hrm/employee/index.ts
@@ -87,6 +87,8 @@
    name: string;
    deptName?: string;
    postName?: string;
    /** å‘˜å·¥çŠ¶æ€ï¼š1 åœ¨èŒã€2 è¯•用、3 ç¦»èŒ */
    employeeStatus?: number;
  }
  /** å‘˜å·¥å¯¼å…¥ç»“æžœ */
src/api/hrm/resignation/index.ts
@@ -9,14 +9,20 @@
    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;
src/api/hrm/statistics/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
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',
  );
}
src/api/mes/dv/checkplan/machinery/index.ts
@@ -6,7 +6,7 @@
    id?: number; // å…³è”编号
    planId?: number; // æ–¹æ¡ˆç¼–号
    machineryId?: number; // è®¾å¤‡ç¼–号
    machineryCode?: string; // è®¾å¤‡ç¼–码
    machineryCode?: string; // è®¾å¤‡ç¼–号
    machineryName?: string; // è®¾å¤‡åç§°
    machineryBrand?: string; // å“ç‰Œ
    machinerySpecification?: string; // è§„格型号
src/api/mes/dv/checkrecord/index.ts
@@ -9,7 +9,7 @@
    planId?: number; // ç‚¹æ£€è®¡åˆ’编号
    planName?: string; // è®¡åˆ’名称
    machineryId?: number; // è®¾å¤‡ç¼–号
    machineryCode?: string; // è®¾å¤‡ç¼–码
    machineryCode?: string; // è®¾å¤‡ç¼–号
    machineryName?: string; // è®¾å¤‡åç§°
    machineryBrand?: string; // å“ç‰Œ
    machinerySpecification?: string; // è§„格型号
src/api/mes/dv/machinery/index.ts
@@ -6,7 +6,7 @@
  /** MES è®¾å¤‡å°è´¦ */
  export interface Machinery {
    id?: number; // è®¾å¤‡ç¼–号
    code?: string; // è®¾å¤‡ç¼–码
    code?: string; // è®¾å¤‡ç¼–号
    name?: string; // è®¾å¤‡åç§°
    brand?: string; // å“ç‰Œ
    specification?: string; // è§„格型号
@@ -27,9 +27,9 @@
  /** è®¾å¤‡å¯¼å…¥ç»“æžœ */
  export interface MachineryImportRespVO {
    createCodes?: string[]; // æ–°å¢žæˆåŠŸçš„è®¾å¤‡ç¼–ç 
    updateCodes?: string[]; // æ›´æ–°æˆåŠŸçš„è®¾å¤‡ç¼–ç 
    failureCodes?: Record<string, string>; // å¯¼å…¥å¤±è´¥çš„设备编码及原因
    createCodes?: string[]; // æ–°å¢žæˆåŠŸçš„è®¾å¤‡ç¼–å·
    updateCodes?: string[]; // æ›´æ–°æˆåŠŸçš„è®¾å¤‡ç¼–å·
    failureCodes?: Record<string, string>; // å¯¼å…¥å¤±è´¥çš„设备编号及原因
  }
}
src/api/mes/dv/maintenrecord/index.ts
@@ -9,7 +9,7 @@
    planId?: number; // è®¡åˆ’编号
    planName?: string; // è®¡åˆ’名称
    machineryId?: number; // è®¾å¤‡ç¼–号
    machineryCode?: string; // è®¾å¤‡ç¼–码
    machineryCode?: string; // è®¾å¤‡ç¼–号
    machineryName?: string; // è®¾å¤‡åç§°
    machineryBrand?: string; // å“ç‰Œ
    machinerySpecification?: string; // è§„格型号
src/api/mes/dv/repair/index.ts
@@ -9,7 +9,7 @@
    code?: string; // ç»´ä¿®è®¢å•编码
    name?: string; // ç»´ä¿®è®¢å•名称
    machineryId?: number; // è®¾å¤‡ç¼–号
    machineryCode?: string; // è®¾å¤‡ç¼–码
    machineryCode?: string; // è®¾å¤‡ç¼–号
    machineryName?: string; // è®¾å¤‡åç§°
    machineryBrand?: string; // å“ç‰Œ
    machinerySpecification?: string; // è§„格型号
src/api/mes/md/workstation/machine/index.ts
@@ -6,7 +6,7 @@
    id?: number; // èµ„源编号
    workstationId?: number; // å·¥ä½œç«™ç¼–号
    machineryId?: number; // è®¾å¤‡ç¼–号
    machineryCode?: string; // è®¾å¤‡ç¼–码
    machineryCode?: string; // è®¾å¤‡ç¼–号
    machineryName?: string; // è®¾å¤‡åç§°
    quantity?: number; // æ•°é‡
    remark?: string; // å¤‡æ³¨
src/api/mes/wm/arrivalnotice/index.ts
@@ -8,7 +8,10 @@
    id?: number; // é€šçŸ¥å•编号
    code?: string; // é€šçŸ¥å•编号
    name?: string; // é€šçŸ¥å•名称
    purchaseOrderId?: number; // é‡‡è´­è®¢å•编号
    purchaseOrderCode?: string; // é‡‡è´­è®¢å•编号
    purchaseOrderNo?: string; // é‡‡è´­è®¢å•号
    contractNo?: string; // é‡‡è´­åˆåŒå·
    vendorId?: number; // ä¾›åº”商编号
    vendorCode?: string; // ä¾›åº”商编码
    vendorName?: string; // ä¾›åº”商名称
src/api/mes/wm/returnvendor/index.ts
@@ -103,6 +103,15 @@
  );
}
/** ä»Žå…³è”的采购订单重新带入可退物料(幂等:已存在的物料行不会重复带入,返回本次新增行数) */
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}`);
src/api/srm/bidBond/index.ts
ÎļþÒÑɾ³ý
src/api/system/notice/index.ts
@@ -1,6 +1,7 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
import type { SystemStorageApi } from '#/api/system/storage';
export namespace SystemNoticeApi {
  /** å…¬å‘Šä¿¡æ¯ */
@@ -13,6 +14,10 @@
    remark: string;
    creator?: string;
    createTime?: Date;
    /** é™„ä»¶ blobId åˆ—表(提交时使用) */
    blobIds?: number[];
    /** é™„件列表(详情回显) */
    attachmentList?: SystemStorageApi.StorageBlob[];
  }
}
src/router/routes/modules/aftersales.ts
@@ -20,15 +20,6 @@
        },
        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'),
      },
    ],
  },
];
src/views/aftersales/return/data.ts
ÎļþÒÑɾ³ý
src/views/aftersales/return/detail.vue
ÎļþÒÑɾ³ý
src/views/aftersales/return/index.vue
ÎļþÒÑɾ³ý
src/views/aftersales/return/modules/form.vue
ÎļþÒÑɾ³ý
src/views/aftersales/ticket/data.ts
@@ -144,10 +144,22 @@
    },
    {
      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',
@@ -155,6 +167,18 @@
      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',
@@ -195,18 +219,6 @@
        placeholder: '请输入备注',
        rows: 3,
      },
    },
    {
      fieldName: 'blobIds',
      label: '附件',
      component: 'FileUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        disabled: readonly,
      },
      formItemClass: 'col-span-3',
    },
  ];
}
src/views/aftersales/ticket/detail.vue
@@ -5,7 +5,7 @@
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,
@@ -17,10 +17,10 @@
  Select,
  Switch,
  Tag,
  Timeline,
} from 'ant-design-vue';
import {
  assignTicket,
  cancelTicket,
  classifyTicket,
  closeTicket,
@@ -73,10 +73,6 @@
  2: '远程',
  3: '其他',
};
// åˆ†é…å¤„理人
const showAssignInput = ref(false);
const assignUserId = ref<number>();
// é—®é¢˜åˆ¤å®š
const showClassifyInput = ref(false);
@@ -153,22 +149,6 @@
  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) {
@@ -210,14 +190,6 @@
  await cancelTicket(ticketId.value);
  message.success('取消成功');
  await loadTicket();
}
function handleCreateReturn() {
  router.push({
    name: 'AftersalesReturnDetail',
    params: { id: 'new' },
    query: { ticketId: ticketId.value },
  });
}
function goBack() {
@@ -272,6 +244,12 @@
        <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>
@@ -315,8 +293,35 @@
      </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"
@@ -328,6 +333,28 @@
          {{ 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>
    <!-- å›žè®¿è®°å½• -->
@@ -457,29 +484,6 @@
    <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>
src/views/aftersales/ticket/index.vue
@@ -180,7 +180,9 @@
              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),
            },
            {
src/views/aftersales/ticket/modules/form.vue
@@ -6,7 +6,6 @@
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';
@@ -32,7 +31,6 @@
defineOptions({ name: 'TicketForm' });
const emit = defineEmits(['success']);
const router = useRouter();
const TS = AftersaleTicketStatusEnum;
const IT = AftersaleIssueTypeEnum;
@@ -67,7 +65,6 @@
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', ['售后工单']);
@@ -145,13 +142,20 @@
        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);
@@ -248,11 +252,6 @@
  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>
@@ -294,8 +293,6 @@
      <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>
src/views/bpm/oa/notice/data.ts
@@ -137,6 +137,17 @@
        placeholder: '请输入内容',
      },
    },
    {
      fieldName: 'blobIds',
      label: '附件',
      component: 'FileUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        placeholder: '可上传通知公告相关附件',
      },
    },
  ];
}
src/views/bpm/oa/notice/detail.vue
@@ -101,12 +101,32 @@
            </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>
src/views/bpm/oa/notice/modules/form.vue
@@ -82,6 +82,17 @@
    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();
src/views/crm/bidBond/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,290 @@
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/crm/bidBond/index.vue
ÎļþÃû´Ó src/views/srm/bidBond/index.vue ÐÞ¸Ä
@@ -1,19 +1,18 @@
<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,
@@ -21,7 +20,7 @@
} from './data';
import BidBondForm from './modules/form.vue';
defineOptions({ name: 'SrmBidBond' });
defineOptions({ name: 'CrmBidBond' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: BidBondForm,
@@ -36,11 +35,11 @@
  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();
@@ -67,7 +66,7 @@
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmBidBondApi.BidBond>,
  } as VxeTableGridOptions<CrmBidBondApi.BidBond>,
});
</script>
@@ -82,15 +81,19 @@
              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 }">
@@ -105,7 +108,7 @@
              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),
            },
            {
@@ -113,7 +116,7 @@
              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),
src/views/crm/bidBond/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<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>
src/views/erp/purchase/order/components/index.ts
@@ -1,4 +1,9 @@
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,
};
src/views/erp/purchase/order/components/payment-date-picker.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
<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>
src/views/erp/purchase/order/data.ts
@@ -11,10 +11,12 @@
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';
@@ -116,21 +118,12 @@
    {
      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',
src/views/erp/purchase/order/index.vue
@@ -82,15 +82,15 @@
  }
}
/** æäº¤è‰ç¨¿è®¢å•审批 */
/** æäº¤è‰ç¨¿è®¢å• */
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();
@@ -252,13 +252,13 @@
              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),
              },
            },
src/views/erp/purchase/order/modules/form.vue
@@ -76,7 +76,7 @@
          taxPrice: saleItem.taxPrice,
          totalPrice: saleItem.totalPrice,
          remark: saleItem.remark,
          qcCheckFlag: false,
          qcCheckFlag: true,
        })),
      );
    }
@@ -97,7 +97,7 @@
          count: bomItem.quantity ?? 1,
          taxPercent: 0,
          remark: bomItem.remark,
          qcCheckFlag: false,
          qcCheckFlag: true,
        })),
      );
      message.info('已按工单 BOM å¸¦å…¥ç‰©æ–™ï¼Œè¯·è¡¥å¡«å«ç¨Žå•ä»·');
src/views/erp/purchase/order/modules/item-form.vue
@@ -131,7 +131,8 @@
    taxPrice: undefined,
    totalPrice: undefined,
    remark: undefined,
    qcCheckFlag: false,
    // ä¸Žé‡‡è´­ç”³è¯·ä¿æŒä¸€è‡´ï¼šæ–°å¢žæ˜Žç»†é»˜è®¤è´¨æ£€
    qcCheckFlag: true,
  };
  tableData.value.push(newRow);
  // é€šçŸ¥çˆ¶ç»„ä»¶æ›´æ–°
src/views/erp/purchase/request/index.vue
@@ -124,7 +124,14 @@
/** ç”Ÿæˆé‡‡è´­è®¢å• */
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[]>([]);
src/views/erp/purchase/request/modules/item-form.vue
@@ -96,7 +96,8 @@
    demandTime: undefined,
    remark: undefined,
    totalPrice: 0,
    qcCheckFlag: false,
    // é‡‡è´­ç”³è¯·çš„产品明细默认需要质检
    qcCheckFlag: true,
  };
  tableData.value.push(newRow);
  emit('update:items', [...tableData.value]);
src/views/erp/purchase/request/modules/order-form.vue
@@ -1,40 +1,27 @@
<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();
    }
@@ -43,13 +30,17 @@
    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>
@@ -57,15 +48,13 @@
<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>
src/views/erp/purchase/statement/index.vue
@@ -2,25 +2,65 @@
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 },
@@ -58,7 +98,12 @@
    key: 'remainingPayable',
    width: 120,
  },
  { title: '采购订单', dataIndex: 'purchaseOrderNo', key: 'purchaseOrderNo', width: 160 },
  {
    title: '采购订单',
    dataIndex: 'purchaseOrderNo',
    key: 'purchaseOrderNo',
    width: 160,
  },
];
const paymentColumns = [
@@ -75,7 +120,13 @@
];
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) {
@@ -86,185 +137,319 @@
}
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>
src/views/hrm/resignation/data.ts
@@ -5,13 +5,17 @@
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',
@@ -19,6 +23,47 @@
      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: '选择申请人后自动带出',
      },
    },
    {
@@ -44,21 +89,22 @@
      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',
src/views/hrm/resignation/index.vue
@@ -73,13 +73,10 @@
  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();
}
@@ -161,12 +158,13 @@
              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),
              },
            },
src/views/hrm/resignation/modules/form.vue
@@ -70,7 +70,12 @@
    }
    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) {
src/views/hrm/statistics/chartOptions.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,310 @@
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);
}
src/views/hrm/statistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,263 @@
<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>
src/views/mes/dv/checkplan/modules/machinery-list.vue
@@ -72,7 +72,7 @@
    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 },
src/views/mes/dv/checkrecord/data.ts
@@ -132,7 +132,7 @@
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesDvCheckRecordApi.CheckRecord>['columns'] {
  return [
    { field: 'machineryCode', title: '设备编码', minWidth: 140 },
    { field: 'machineryCode', title: '设备编号', minWidth: 140 },
    {
      field: 'machineryName',
      title: '设备名称',
src/views/mes/dv/machinery/components/select.vue
@@ -113,7 +113,7 @@
    <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">
src/views/mes/dv/machinery/data.ts
@@ -38,16 +38,17 @@
    },
    {
      fieldName: 'code',
      label: '设备编码',
      label: '设备编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入设备编码',
        placeholder: '可手动填写,或点击右侧“自动生成”',
      },
      dependencies: {
        triggerFields: ['id'],
        componentProps: (values) => ({ disabled: !!values.id }),
      },
      rules: 'required',
      help: '支持手动填写;也可点击右侧按钮由系统按编码规则自动生成',
      suffix:
        formType === 'detail'
          ? undefined
@@ -63,7 +64,7 @@
                    await formApi?.setFieldValue('code', code);
                  },
                },
                { default: () => '生成' },
                { default: () => '自动生成' },
              ),
    },
    {
@@ -193,11 +194,11 @@
  return [
    {
      fieldName: 'code',
      label: '设备编码',
      label: '设备编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入设备编码',
        placeholder: '请输入设备编号',
      },
    },
    {
@@ -236,7 +237,7 @@
  return [
    {
      field: 'code',
      title: '设备编码',
      title: '设备编号',
      minWidth: 140,
      slots: {
        default: 'code',
@@ -316,11 +317,11 @@
  return [
    {
      fieldName: 'code',
      label: '设备编码',
      label: '设备编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入设备编码',
        placeholder: '请输入设备编号',
      },
    },
    {
@@ -350,7 +351,7 @@
): 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 },
src/views/mes/dv/maintenrecord/data.ts
@@ -132,7 +132,7 @@
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesDvMaintenRecordApi.MaintenRecord>['columns'] {
  return [
    { field: 'machineryCode', title: '设备编码', minWidth: 140 },
    { field: 'machineryCode', title: '设备编号', minWidth: 140 },
    {
      field: 'machineryName',
      title: '设备名称',
src/views/mes/md/workstation/modules/machine-list.vue
@@ -94,7 +94,7 @@
    columns: [
      {
        field: 'machineryCode',
        title: '设备编码',
        title: '设备编号',
        width: 140,
      },
      {
src/views/mes/qc/components/qc-attachments.vue
@@ -5,7 +5,7 @@
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[]>([]);
src/views/srm/bidBond/data.ts
ÎļþÒÑɾ³ý
src/views/srm/bidBond/modules/form.vue
ÎļþÒÑɾ³ý
src/views/system/notice/data.ts
@@ -44,6 +44,18 @@
      rules: 'required',
    },
    {
      fieldName: 'blobIds',
      label: '附件',
      component: 'FileUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        placeholder: '可上传公告相关附件',
      },
      help: '支持多选,单个文件大小受系统上传配置限制',
    },
    {
      fieldName: 'status',
      label: '公告状态',
      component: 'RadioGroup',
src/views/system/notice/modules/form.vue
@@ -68,6 +68,17 @@
      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();
    }
src/views/wls/arrivalnotice/data.ts
@@ -96,8 +96,21 @@
            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',
@@ -189,6 +202,15 @@
      },
    },
    {
      fieldName: 'contractNo',
      label: '采购合同号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入采购合同号',
      },
    },
    {
      fieldName: 'vendorName',
      label: '供应商',
      component: 'Input',
@@ -229,6 +251,11 @@
      minWidth: 140,
    },
    {
      field: 'contractNo',
      title: '采购合同号',
      minWidth: 140,
    },
    {
      field: 'vendorName',
      title: '供应商名称',
      minWidth: 120,
src/views/wls/returnvendor/data.ts
@@ -110,6 +110,7 @@
          }
        },
      },
      help: '保存后自动带入供应商、采购合同号,以及订单中可退的物料行;物料不可手工更换,避免与采购订单对不上号。',
    },
    {
      fieldName: 'contractNo',
@@ -425,6 +426,7 @@
export function useLineFormSchema(
  vendorId?: number,
  formApi?: VbenFormApi,
  lockItem = false,
): VbenFormSchema[] {
  return [
    {
@@ -433,7 +435,11 @@
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择产品物料',
        disabled: lockItem,
      },
      help: lockItem
        ? '物料由所选采购订单自动带入,不可修改;如需更换物料,请修改退货单的采购订单。'
        : undefined,
      rules: 'selectRequired',
    },
    {
src/views/wls/returnvendor/index.vue
@@ -7,6 +7,8 @@
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 {
@@ -20,6 +22,8 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -48,6 +52,13 @@
/** æäº¤å®¡æ‰¹ï¼ˆ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,
@@ -59,6 +70,20 @@
  } finally {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¿›åº¦ï¼ˆBPM æµç¨‹å®žä¾‹ï¼‰ */
function handleProcessDetail(row: MesWmReturnVendorApi.ReturnVendor) {
  if (!row.processInstanceId) {
    message.warning(
      `退货单 ${row.code} æ²¡æœ‰æµç¨‹å®žä¾‹ï¼šå¯èƒ½å¼€å¯äº†å…¨å±€å…å®¡æ‰¹ï¼Œæˆ–尚未提交审批`,
    );
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
/** æ‰§è¡Œæ‹£è´§ */
@@ -193,6 +218,12 @@
              },
            },
            {
              label: '审批进度',
              type: 'link',
              ifShow: row.status === MesWmReturnVendorStatusEnum.BPM_APPROVING,
              onClick: handleProcessDetail.bind(null, row),
            },
            {
              label: '执行拣货',
              type: 'link',
              auth: ['mes:wm-return-vendor:update'],
src/views/wls/returnvendor/modules/form.vue
@@ -43,6 +43,19 @@
    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', ['供应商退货单']);
@@ -87,6 +100,14 @@
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();
@@ -139,7 +160,11 @@
  modalApi.lock();
  try {
    await finishReturnVendor(formData.value.id);
    message.success('完成成功');
    message.success(
      formData.value.handleType === 1
        ? '完成退货成功,已生成负数冲红付款单,可在财务-付款单按单号查询'
        : '完成退货成功,已生成换货到货通知单并重新发起质检,请到「到货通知」查看',
    );
    await modalApi.close();
    emit('success');
  } finally {
@@ -239,6 +264,7 @@
      <div class="mx-4">
        <LineList
          :form-type="formType"
          :purchase-order-id="formData.purchaseOrderId"
          :return-id="formData.id"
          :vendor-id="formData.vendorId"
        />
@@ -248,7 +274,7 @@
      <div class="flex flex-auto items-center gap-2">
        <Popconfirm
          v-if="canSubmit"
          title="确认提交该退货单?【提交后将不能修改】"
          :title="`确认提交审批?流程:采购审核 â†’ è´¨é‡å®¡æ ¸ â†’ ä»“库审核,提交后将不能修改;未上传附件将无法提交`"
          @confirm="handleSubmit"
        >
          <Button type="primary">提交</Button>
@@ -258,7 +284,7 @@
        </Button>
        <Popconfirm
          v-if="isFinish"
          title="确认完成该退货单并执行退货?"
          :title="finishConfirmTitle"
          @confirm="handleFinish"
        >
          <Button type="primary">完成退货</Button>
src/views/wls/returnvendor/modules/line-form.vue
@@ -72,11 +72,15 @@
    // åŠ è½½æ•°æ®
    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;
    }
src/views/wls/returnvendor/modules/line-list.vue
@@ -5,7 +5,7 @@
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';
@@ -14,6 +14,7 @@
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,
@@ -28,9 +29,15 @@
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(() =>
  // æ˜¯å¦å¯ç¼–辑明细行
@@ -61,15 +68,48 @@
/** æ·»åŠ ç‰©æ–™ */
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();
}
@@ -138,13 +178,16 @@
      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;
        },
      },
    },
@@ -174,8 +217,29 @@
  <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="[
            {
@@ -187,6 +251,20 @@
          ]"
        />
      </template>
      <template
        v-else-if="isEditable && isLinkedToPurchaseOrder"
        #toolbar-tools
      >
        <TableAction
          :actions="[
            {
              label: '重新带入订单物料',
              type: 'primary',
              onClick: handleImportItems,
            },
          ]"
        />
      </template>
      <template #detail="{ row }">
        <DetailList
          :details="getExpandedDetails(row)"