gaoluyang
2026-07-01 3b430f42c1412422aa32412463533385bf4dd44e
银川
1.联调产品维护页面
2.添加采购申请页面
已添加16个文件
已修改11个文件
2680 ■■■■■ 文件已修改
src/api/erp/purchase/in/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/order/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/request/index.ts 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/statistics/index.ts 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/supplier/index.ts 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/unit/index.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/unit/data.ts 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/unit/index.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/unit/modules/form.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/in/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/in/index.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/in/modules/inspection-form.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/data.ts 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/form.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/item-form.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/process-select-modal.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/data.ts 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/index.vue 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/form.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/item-form.vue 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/statistics/index.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/statistics/price.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/statistics/supplier.vue 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/statistics/uncompleted.vue 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/supplier/data.ts 127 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/in/index.ts
@@ -21,6 +21,10 @@
    otherPrice?: number; // å…¶ä»–费用
    totalProductPrice?: number; // åˆè®¡å•†å“é‡‘额
    taxPrice?: number; // åˆè®¡ç¨Žé¢
    inspectionStatus?: number; // éªŒæ”¶çŠ¶æ€ï¼ˆ0:待验收 1:已验收 2:验收不合格)
    inspectionTime?: string; // éªŒæ”¶æ—¶é—´
    inspectorId?: number; // éªŒæ”¶äººå‘˜ç¼–号
    inspectionResult?: string; // éªŒæ”¶ç»“æžœ
    items?: PurchaseInItem[]; // é‡‡è´­å…¥åº“明细
  }
@@ -99,3 +103,12 @@
    params,
  });
}
/** éªŒæ”¶é‡‡è´­å…¥åº“ */
export function inspectPurchaseIn(data: {
  id: number;
  inspectionStatus: number;
  inspectionResult?: string;
}) {
  return requestClient.put('/erp/purchase-in/inspect', data);
}
src/api/erp/purchase/order/index.ts
@@ -96,3 +96,16 @@
export function exportPurchaseOrder(params: any) {
  return requestClient.download('/erp/purchase-order/export-excel', { params });
}
/** èŽ·å–é‡‡è´­å®¡æ ¸æµç¨‹åˆ—è¡¨ */
export function getPurchaseApproveProcessList() {
  return requestClient.get<any[]>('/erp/purchase-order/approve-process-list');
}
/** èŽ·å–é‡‡è´­å®¡æ ¸æµç¨‹åˆ—è¡¨å“åº” */
export interface ProcessDefinition {
  id: string; // æµç¨‹å®šä¹‰ID
  key: string; // æµç¨‹Key
  name: string; // æµç¨‹åç§°
  version: number; // ç‰ˆæœ¬å·
}
src/api/erp/purchase/request/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace ErpPurchaseRequestApi {
  /** é‡‡è´­ç”³è¯·ä¿¡æ¯ */
  export interface PurchaseRequest {
    id?: number; // ç¼–号
    no?: string; // ç”³è¯·ç¼–号
    supplierId?: number; // ä¾›åº”商编号
    supplierName?: string; // ä¾›åº”商名称
    requestTime: string; // ç”³è¯·æ—¶é—´
    requesterId?: number; // ç”³è¯·äººç¼–号
    requesterName?: string; // ç”³è¯·äººåç§°
    reason?: string; // ç”³è¯·ç†ç”±
    estimatedAmount?: number; // é¢„估金额
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
    items?: PurchaseRequestItem[]; // ç”³è¯·æ˜Žç»†
    createTime?: string; // åˆ›å»ºæ—¶é—´
  }
  /** é‡‡è´­ç”³è¯·æ˜Žç»† */
  export interface PurchaseRequestItem {
    id?: number; // ç¼–号
    productId: number; // äº§å“ç¼–号
    productName?: string; // äº§å“åç§°
    productUnitName?: string; // äº§å“å•位
    productBarCode?: string; // äº§å“æ¡ç 
    requestCount: number; // ç”³è¯·æ•°é‡
    estimatedPrice?: number; // é¢„估单价
    requiredDate?: string; // éœ€æ±‚日期
    remark?: string; // å¤‡æ³¨
  }
}
/** åˆ›å»ºé‡‡è´­ç”³è¯· */
export function createPurchaseRequest(data: ErpPurchaseRequestApi.PurchaseRequest) {
  return requestClient.post('/erp/purchase-request/create', data);
}
/** æ›´æ–°é‡‡è´­ç”³è¯· */
export function updatePurchaseRequest(data: ErpPurchaseRequestApi.PurchaseRequest) {
  return requestClient.put('/erp/purchase-request/update', data);
}
/** å®¡æ‰¹é‡‡è´­ç”³è¯· */
export function approvePurchaseRequest(params: {
  id: number;
  status: number;
  remark?: string;
}) {
  return requestClient.put('/erp/purchase-request/approve', null, { params });
}
/** è½¬ä¸ºé‡‡è´­è®¢å• */
export function convertToPurchaseOrder(id: number) {
  return requestClient.put('/erp/purchase-request/convert', null, { params: { id } });
}
/** åˆ é™¤é‡‡è´­ç”³è¯· */
export function deletePurchaseRequest(ids: number[]) {
  return requestClient.delete('/erp/purchase-request/delete', { params: { ids: ids.join(',') } });
}
/** èŽ·å–é‡‡è´­ç”³è¯·è¯¦æƒ… */
export function getPurchaseRequest(id: number) {
  return requestClient.get<ErpPurchaseRequestApi.PurchaseRequest>(
    `/erp/purchase-request/get?id=${id}`,
  );
}
/** èŽ·å–é‡‡è´­ç”³è¯·åˆ†é¡µ */
export function getPurchaseRequestPage(params: PageParam) {
  return requestClient.get<PageResult<ErpPurchaseRequestApi.PurchaseRequest>>(
    '/erp/purchase-request/page',
    { params },
  );
}
/** å¯¼å‡ºé‡‡è´­ç”³è¯· Excel */
export function exportPurchaseRequest(params: any) {
  return requestClient.download('/erp/purchase-request/export-excel', { params });
}
src/api/erp/purchase/statistics/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
import type { PageParam, PageResult } from '../../../../packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace ErpPurchaseStatisticsApi {
  /** é‡‡è´­æ±‡æ€»ç»Ÿè®¡ */
  export interface Summary {
    totalOrderCount?: number; // æ€»è®¢å•æ•°
    totalOrderAmount?: number; // æ€»è®¢å•金额
    totalInCount?: number; // æ€»å…¥åº“æ•°
    totalInAmount?: number; // æ€»å…¥åº“金额
    totalReturnCount?: number; // æ€»é€€è´§æ•°
    totalReturnAmount?: number; // æ€»é€€è´§é‡‘额
  }
  /** ä¾›åº”商供货统计 */
  export interface SupplierStatistics {
    supplierId?: number; // ä¾›åº”商编号
    supplierName?: string; // ä¾›åº”商名称
    orderCount?: number; // è®¢å•æ•°
    orderAmount?: number; // è®¢å•金额
    inCount?: number; // å…¥åº“æ•°
    inAmount?: number; // å…¥åº“金额
    returnCount?: number; // é€€è´§æ•°
    returnAmount?: number; // é€€è´§é‡‘额
  }
  /** é‡‡è´­ä»·æ ¼åˆ†æž */
  export interface PriceAnalysis {
    productId?: number; // äº§å“ç¼–号
    productName?: string; // äº§å“åç§°
    productBarCode?: string; // äº§å“æ¡ç 
    avgPrice?: number; // å¹³å‡ä»·æ ¼
    minPrice?: number; // æœ€ä½Žä»·æ ¼
    maxPrice?: number; // æœ€é«˜ä»·æ ¼
    latestPrice?: number; // æœ€è¿‘ä»·æ ¼
    priceChangeRate?: number; // ä»·æ ¼å˜åŒ–率
  }
  /** æœªå®Œæˆè®¢å•跟踪 */
  export interface UncompletedOrder {
    orderId?: number; // è®¢å•编号
    orderNo?: string; // è®¢å•号
    supplierName?: string; // ä¾›åº”商名称
    orderTime?: string; // è®¢å•æ—¶é—´
    productNames?: string; // äº§å“åç§°
    orderAmount?: number; // è®¢å•金额
    inAmount?: number; // å…¥åº“金额
    uncompletedAmount?: number; // æœªå®Œæˆé‡‘额
    daysOverdue?: number; // è¶…期天数
  }
}
/** é‡‡è´­æ±‡æ€»ç»Ÿè®¡ */
export function getPurchaseSummary(params?: any) {
  return requestClient.get<ErpPurchaseStatisticsApi.Summary>(
    '/erp/purchase-statistics/summary',
    { params },
  );
}
/** ä¾›åº”商供货统计 */
export function getSupplierStatistics(params: PageParam) {
  return requestClient.get<PageResult<ErpPurchaseStatisticsApi.SupplierStatistics>>(
    '/erp/purchase-statistics/supplier',
    { params },
  );
}
/** é‡‡è´­ä»·æ ¼åˆ†æž */
export function getPriceAnalysis(params: PageParam) {
  return requestClient.get<PageResult<ErpPurchaseStatisticsApi.PriceAnalysis>>(
    '/erp/purchase-statistics/price',
    { params },
  );
}
/** æœªå®Œæˆè®¢å•跟踪 */
export function getUncompletedOrders(params: PageParam) {
  return requestClient.get<PageResult<ErpPurchaseStatisticsApi.UncompletedOrder>>(
    '/erp/purchase-statistics/uncompleted',
    { params },
  );
}
src/api/erp/purchase/supplier/index.ts
@@ -7,19 +7,24 @@
  export interface Supplier {
    id?: number; // ä¾›åº”商编号
    name: string; // ä¾›åº”商名称
    creditCode?: string; // ç»Ÿä¸€ç¤¾ä¼šä¿¡ç”¨ä»£ç 
    contact: string; // è”系人
    mobile: string; // æ‰‹æœºå·ç 
    telephone: string; // è”系电话
    phone?: string; // è”系电话(备用)
    email: string; // ç”µå­é‚®ç®±
    fax: string; // ä¼ çœŸ
    address?: string; // åœ°å€
    remark: string; // å¤‡æ³¨
    status: number; // å¼€å¯çŠ¶æ€
    sort: number; // æŽ’序
    taxNo: string; // çº³ç¨Žäººè¯†åˆ«å·
    taxPercent: number; // ç¨Žçއ
    bankName: string; // å¼€æˆ·è¡Œ
    bankAccount: string; // å¼€æˆ·è´¦å·
    bankName: string; // å¼€æˆ·é“¶è¡Œ
    bankAccount: string; // é“¶è¡Œè´¦å·
    bankAddress: string; // å¼€æˆ·åœ°å€
    qualification?: string; // èµ„质信息
    cooperationTime?: string; // åˆä½œå¼€å§‹æ—¶é—´
  }
}
src/api/mdm/unit/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace MdmUnitApi {
  /** è®¡é‡å•位信息 */
  export interface Unit {
    id?: number; // å•位编号
    code: string; // å•位编码
    name: string; // å•位名称
    primaryFlag?: boolean; // æ˜¯å¦ä¸»å•位
    primaryId?: number; // ä¸»å•位编号
    changeRate?: number; // ä¸Žä¸»å•位换算比例
    status: number; // çŠ¶æ€ï¼ˆ0:启用, 1:禁用)
    remark?: string; // å¤‡æ³¨
  }
}
/** æŸ¥è¯¢è®¡é‡å•位分页 */
export function getUnitPage(params: PageParam) {
  return requestClient.get<PageResult<MdmUnitApi.Unit>>('/mdm/unit/page', {
    params,
  });
}
/** æŸ¥è¯¢è®¡é‡å•位列表(根据ID) */
export function getUnitListByIds(ids: string) {
  return requestClient.get<MdmUnitApi.Unit[]>(`/mdm/unit/list-by-ids?ids=${ids}`);
}
/** æŸ¥è¯¢è®¡é‡å•位详情 */
export function getUnit(id: number) {
  return requestClient.get<MdmUnitApi.Unit>(`/mdm/unit/get?id=${id}`);
}
/** æ–°å¢žè®¡é‡å•位 */
export function createUnit(data: MdmUnitApi.Unit) {
  return requestClient.post('/mdm/unit/create', data);
}
/** ä¿®æ”¹è®¡é‡å•位 */
export function updateUnit(data: MdmUnitApi.Unit) {
  return requestClient.put('/mdm/unit/update', data);
}
/** åˆ é™¤è®¡é‡å•位 */
export function deleteUnit(id: number) {
  return requestClient.delete(`/mdm/unit/delete?id=${id}`);
}
src/views/basicData/mdm/data.ts
@@ -5,6 +5,7 @@
import { getDictOptions } from '#/packages/effects/hooks/src';
import { z } from '#/adapter/form';
import { getUnitPage } from '#/api/mdm/unit';
/** ç‰©æ–™ç±»åž‹é€‰é¡¹ */
export const ITEM_TYPE_OPTIONS = [
@@ -13,6 +14,22 @@
  { label: '成品', value: 3 },
  { label: '辅料', value: 4 },
];
/** ç‰©æ–™ç±»åž‹æ˜ å°„ */
export const ITEM_TYPE_MAP: Record<number, string> = {
  1: '原料',
  2: '半成品',
  3: '成品',
  4: '辅料',
};
/** ç‰©æ–™ç±»åž‹é¢œè‰²æ˜ å°„ */
export const ITEM_TYPE_COLOR_MAP: Record<number, string> = {
  1: 'green',
  2: 'orange',
  3: 'blue',
  4: 'purple',
};
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
@@ -44,63 +61,39 @@
      rules: 'required',
    },
    {
      component: 'RadioGroup',
      fieldName: 'itemType',
      label: '物料类型',
      componentProps: {
        options: ITEM_TYPE_OPTIONS,
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: 'required',
    },
    {
      component: 'ApiSelect',
      fieldName: 'unitMeasureId',
      label: '计量单位',
      rules: 'required',
      componentProps: {
        placeholder: '请选择计量单位',
        allowClear: true,
        api: async () => {
          const res = await getUnitPage({ pageNo: 1, pageSize: 100, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      component: 'Input',
      fieldName: 'barCode',
      label: '条码',
      componentProps: {
        placeholder: '请输入条码',
      },
    },
    {
      component: 'Input',
      fieldName: 'specification',
      label: '规格型号',
      componentProps: {
        placeholder: '请输入规格型号',
      },
    },
    {
      component: 'TreeSelect',
      fieldName: 'categoryId',
      label: '物料分类',
      componentProps: {
        placeholder: '请选择物料分类',
        allowClear: true,
        treeData: [],
        fieldNames: { label: 'name', value: 'id', children: 'children' },
      },
    },
    {
      component: 'Select',
      fieldName: 'unitMeasureId',
      label: '计量单位',
      componentProps: {
        placeholder: '请选择计量单位',
        allowClear: true,
        options: [],
        fieldNames: { label: 'name', value: 'id' },
      },
    },
    {
      component: 'Select',
      fieldName: 'brandId',
      label: '品牌',
      componentProps: {
        placeholder: '请选择品牌',
        allowClear: true,
        options: [],
        fieldNames: { label: 'name', value: 'id' },
      },
    },
    {
      component: 'Select',
      fieldName: 'itemType',
      label: '物料类型',
      componentProps: {
        placeholder: '请选择物料类型',
        options: ITEM_TYPE_OPTIONS,
      },
      rules: 'required',
    },
    {
      component: 'InputNumber',
@@ -177,7 +170,7 @@
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
      rules: z.boolean().default(true),
    },
    {
      component: 'InputNumber',
@@ -235,17 +228,6 @@
      },
    },
    {
      fieldName: 'categoryId',
      label: '物料分类',
      component: 'TreeSelect',
      componentProps: {
        placeholder: '请选择物料分类',
        allowClear: true,
        treeData: [],
        fieldNames: { label: 'name', value: 'id', children: 'children' },
      },
    },
    {
      fieldName: 'itemType',
      label: '物料类型',
      component: 'Select',
@@ -288,11 +270,6 @@
      minWidth: 150,
    },
    {
      field: 'categoryName',
      title: '分类',
      minWidth: 100,
    },
    {
      field: 'unitMeasureName',
      title: '单位',
      minWidth: 80,
@@ -301,10 +278,7 @@
      field: 'itemType',
      title: '物料类型',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MDM_ITEM_TYPE },
      },
      slots: { default: 'itemType' },
    },
    {
      field: 'purchasePrice',
@@ -350,4 +324,4 @@
      slots: { default: 'actions' },
    },
  ];
}
}
src/views/basicData/mdm/index.vue
@@ -19,7 +19,7 @@
} from '#/api/mdm/item';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import { useGridColumns, useGridFormSchema, ITEM_TYPE_MAP, ITEM_TYPE_COLOR_MAP } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'MdmItem' });
@@ -175,6 +175,12 @@
          ]"
        />
      </template>
      <template #itemType="{ row }">
        <Tag v-if="row.itemType === 1" color="green">原料</Tag>
        <Tag v-else-if="row.itemType === 2" color="orange">半成品</Tag>
        <Tag v-else-if="row.itemType === 3" color="blue">成品</Tag>
        <Tag v-else-if="row.itemType === 4" color="purple">辅料</Tag>
      </template>
      <template #isBatchManaged="{ row }">
        <Tag v-if="row.isBatchManaged" color="success">是</Tag>
        <Tag v-else>否</Tag>
@@ -188,12 +194,6 @@
              icon: ACTION_ICON.EDIT,
              auth: ['mdm:item:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: row.status === 0 ? '停用' : '启用',
              type: 'link',
              auth: ['mdm:item:update'],
              onClick: handleStatusChange.bind(null, row, row.status === 0),
            },
            {
              label: $t('common.delete'),
src/views/basicData/mdm/unit/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,198 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { z } from '#/adapter/form';
import { getUnitPage } from '#/api/mdm/unit';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      component: 'Input',
      fieldName: 'id',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      component: 'Input',
      fieldName: 'code',
      label: '单位编码',
      rules: 'required',
      componentProps: {
        placeholder: '请输入单位编码',
      },
    },
    {
      component: 'Input',
      fieldName: 'name',
      label: '单位名称',
      rules: 'required',
      componentProps: {
        placeholder: '请输入单位名称',
      },
    },
    {
      component: 'Switch',
      fieldName: 'primaryFlag',
      label: '是否主单位',
      componentProps: {
        class: 'w-auto',
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
    },
    {
      component: 'ApiSelect',
      fieldName: 'primaryId',
      label: '主单位',
      dependencies: {
        triggerFields: ['primaryFlag'],
        show: (values) => {
          return values.primaryFlag === false;
        },
        required: (values) => {
          return values.primaryFlag === false;
        },
      },
      componentProps: {
        placeholder: '请选择主单位',
        allowClear: true,
        api: async () => {
          const res = await getUnitPage({ pageNo: 1, pageSize: 100, primaryFlag: true, status: 0 });
          return res.list || [];
        },
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'changeRate',
      label: '换算比例',
      dependencies: {
        triggerFields: ['primaryFlag'],
        show: (values) => {
          return values.primaryFlag === false;
        },
        required: (values) => {
          return values.primaryFlag === false;
        },
      },
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 4,
        placeholder: '请输入换算比例',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      component: 'Textarea',
      fieldName: 'remark',
      label: '备注',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '单位编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入单位编码',
        allowClear: true,
      },
    },
    {
      fieldName: 'name',
      label: '单位名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入单位名称',
        allowClear: true,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择状态',
        allowClear: true,
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 40 },
    {
      field: 'code',
      title: '单位编码',
      minWidth: 120,
    },
    {
      field: 'name',
      title: '单位名称',
      minWidth: 150,
    },
    {
      field: 'primaryFlag',
      title: '主单位',
      minWidth: 80,
      slots: { default: 'primaryFlag' },
    },
    {
      field: 'changeRate',
      title: '换算比例',
      minWidth: 100,
    },
    {
      field: 'status',
      title: '状态',
      minWidth: 80,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.COMMON_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      minWidth: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/basicData/mdm/unit/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MdmUnitApi } from '#/api/mdm/unit';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteUnit,
  getUnitPage,
} from '#/api/mdm/unit';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'MdmUnit' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºè®¡é‡å•位 */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑计量单位 */
function handleEdit(row: MdmUnitApi.Unit) {
  formModalApi.setData(row).open();
}
/** åˆ é™¤è®¡é‡å•位 */
async function handleDelete(row: MdmUnitApi.Unit) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteUnit(row.id as number);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getUnitPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MdmUnitApi.Unit>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="计量单位列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['计量单位']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mdm:unit:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #primaryFlag="{ row }">
        <Tag v-if="row.primaryFlag" color="success">是</Tag>
        <Tag v-else>否</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mdm:unit:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mdm:unit:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/basicData/mdm/unit/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
<script lang="ts" setup>
import type { MdmUnitApi } from '#/api/mdm/unit';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createUnit,
  getUnit,
  updateUnit,
} from '#/api/mdm/unit';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MdmUnitApi.Unit>();
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: 100,
  },
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MdmUnitApi.Unit;
    try {
      await (formData.value?.id
        ? updateUnit(data)
        : createUnit(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<MdmUnitApi.Unit>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getUnit(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal class="w-[500px]" :title="getTitle">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/erp/purchase/in/data.ts
@@ -503,6 +503,12 @@
      minWidth: 120,
    },
    {
      field: 'inspectionStatus',
      title: '验收状态',
      minWidth: 120,
      slots: { default: 'inspectionStatus' },
    },
    {
      field: 'status',
      title: '审批状态',
      minWidth: 120,
src/views/erp/purchase/in/index.vue
@@ -7,25 +7,32 @@
import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
import { downloadFileFromBlobPart, isEmpty } from '../../../../packages/utils/src';
import { message } from 'ant-design-vue';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deletePurchaseIn,
  exportPurchaseIn,
  getPurchaseInPage,
  inspectPurchaseIn,
  updatePurchaseInStatus,
} from '#/api/erp/purchase/in';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import InspectionForm from './modules/inspection-form.vue';
/** ERP é‡‡è´­å…¥åº“列表 */
defineOptions({ name: 'ErpPurchaseIn' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [InspectionModal, inspectionModalApi] = useVbenModal({
  connectedComponent: InspectionForm,
  destroyOnClose: true,
});
@@ -83,6 +90,11 @@
  }
}
/** éªŒæ”¶æ“ä½œ */
function handleInspection(row: ErpPurchaseInApi.PurchaseIn) {
  inspectionModalApi.setData({ id: row.id }).open();
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
@@ -133,7 +145,9 @@
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <InspectionModal @success="handleRefresh" />
    <Grid table-title="采购入库列表">
      <template #toolbar-tools>
        <TableAction
@@ -167,6 +181,11 @@
          ]"
        />
      </template>
      <template #inspectionStatus="{ row }">
        <Tag v-if="row.inspectionStatus === 0" color="warning">待验收</Tag>
        <Tag v-else-if="row.inspectionStatus === 1" color="success">已验收</Tag>
        <Tag v-else-if="row.inspectionStatus === 2" color="error">验收不合格</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
@@ -186,6 +205,13 @@
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '验收',
              type: 'link',
              auth: ['erp:purchase-in:inspect'],
              ifShow: () => row.inspectionStatus === 0 && row.status === 20,
              onClick: handleInspection.bind(null, row),
            },
            {
              label: row.status === 10 ? '审批' : '反审批',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
src/views/erp/purchase/in/modules/inspection-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { inspectPurchaseIn } from '#/api/erp/purchase/in';
import { $t } from '#/locales';
const emit = defineEmits(['success']);
const purchaseInId = ref<number>();
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-1',
  layout: 'horizontal',
  schema: [
    {
      fieldName: 'inspectionStatus',
      label: '验收状态',
      component: 'RadioGroup',
      componentProps: {
        options: [
          { label: '验收合格', value: 1 },
          { label: '验收不合格', value: 2 },
        ],
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: 'required',
    },
    {
      fieldName: 'inspectionResult',
      label: '验收结果',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入验收结果',
        rows: 3,
      },
    },
  ],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    if (!purchaseInId.value) {
      message.error('入库单ID不存在');
      return;
    }
    modalApi.lock();
    const data = await formApi.getValues();
    try {
      await inspectPurchaseIn({
        id: purchaseInId.value,
        inspectionStatus: data.inspectionStatus,
        inspectionResult: data.inspectionResult,
      });
      await modalApi.close();
      emit('success');
      message.success('验收成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      purchaseInId.value = undefined;
      await formApi.resetForm();
      return;
    }
    const data = modalApi.getData<{ id: number }>();
    if (!data || !data.id) {
      return;
    }
    purchaseInId.value = data.id;
    // é»˜è®¤é€‰æ‹©éªŒæ”¶åˆæ ¼
    await formApi.setValues({ inspectionStatus: 1 });
  },
});
</script>
<template>
  <Modal title="采购入库验收" class="w-1/3">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/erp/purchase/order/data.ts
@@ -62,6 +62,18 @@
      rules: 'required',
    },
    {
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入支付订金',
        precision: 2,
        min: 0,
      },
      fieldName: 'depositPrice',
      label: '支付订金',
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
@@ -150,18 +162,6 @@
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入支付订金',
        precision: 2,
        min: 0,
      },
      fieldName: 'depositPrice',
      label: '支付订金',
      rules: z.number().min(0).optional(),
    },
  ];
}
src/views/erp/purchase/order/modules/form.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import type { ErpPurchaseOrderApi, ProcessDefinition } from '#/api/erp/purchase/order';
import { computed, ref } from 'vue';
@@ -13,6 +13,7 @@
import { getAccountSimpleList } from '#/api/erp/finance/account';
import {
  createPurchaseOrder,
  getPurchaseApproveProcessList,
  getPurchaseOrder,
  updatePurchaseOrder,
} from '#/api/erp/purchase/order';
@@ -20,11 +21,13 @@
import { useFormSchema } from '../data';
import PurchaseOrderItemForm from './item-form.vue';
import ProcessSelectModal from './process-select-modal.vue';
const emit = defineEmits(['success']);
const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>();
const formType = ref<FormType>('create'); // è¡¨å•类型:'create' | 'edit' | 'detail'
const formType = ref<FormType>('create');
const itemFormRef = ref<InstanceType<typeof PurchaseOrderItemForm>>();
const selectedProcessId = ref<string>('');
const getTitle = computed(() => {
  if (formType.value === 'create') {
@@ -48,11 +51,15 @@
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
  handleValuesChange: (values, changedFields) => {
    // ç›®çš„:同步到 item-form ç»„件,触发整体的价格计算
    if (formData.value && changedFields.includes('discountPercent')) {
      formData.value.discountPercent = values.discountPercent;
    }
  },
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
/** æ›´æ–°é‡‡è´­è®¢å•项 */
@@ -78,6 +85,33 @@
  });
}
/** æµç¨‹é€‰æ‹©æˆåŠŸ */
function handleProcessSelectSuccess(processId: string) {
  selectedProcessId.value = processId;
  submitOrder();
}
/** æäº¤è®¢å• */
async function submitOrder() {
  const data = (await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
  data.items = formData.value?.items?.map((item) => ({
    ...item,
    id: undefined,
  }));
  if (data.fileUrl && Array.isArray(data.fileUrl)) {
    data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
  }
  if (selectedProcessId.value) {
    data.processDefinitionId = selectedProcessId.value;
  }
  await (formType.value === 'create'
    ? createPurchaseOrder(data)
    : updatePurchaseOrder(data));
  await modalApi.close();
  emit('success');
  message.success($t('ui.actionMessage.operationSuccess'));
}
/** åˆ›å»ºæˆ–更新采购订单 */
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
@@ -96,42 +130,42 @@
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data =
      (await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
    data.items = formData.value?.items?.map((item) => ({
      ...item,
      // è§£å†³æ–°å¢žé‡‡è´­è®¢å•报错
      id: undefined,
    }));
    // å°†æ–‡ä»¶æ•°ç»„转换为字符串
    if (data.fileUrl && Array.isArray(data.fileUrl)) {
      data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
    }
    try {
      await (formType.value === 'create'
        ? createPurchaseOrder(data)
        : updatePurchaseOrder(data));
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    if (formType.value === 'create') {
      try {
        const processes = await getPurchaseApproveProcessList();
        if (processes.length === 1) {
          selectedProcessId.value = processes[0].id;
          await submitOrder();
        } else if (processes.length > 1) {
          processModalApi.setData(processes).open();
        } else {
          message.error('采购审核分类下没有可用的流程模型,请先创建并发布流程');
        }
      } catch (error: any) {
        message.error(error.message || '获取流程列表失败');
      } finally {
        modalApi.unlock();
      }
    } else {
      try {
        await submitOrder();
      } finally {
        modalApi.unlock();
      }
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      selectedProcessId.value = '';
      return;
    }
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      // æ–°å¢žæ—¶ï¼Œé»˜è®¤é€‰ä¸­è´¦æˆ·
      const accountList = await getAccountSimpleList();
      const defaultAccount = accountList.find((item) => item.defaultStatus);
      if (defaultAccount) {
@@ -142,7 +176,6 @@
    modalApi.lock();
    try {
      formData.value = await getPurchaseOrder(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
@@ -157,6 +190,7 @@
    class="w-3/4"
    :show-confirm-button="formType !== 'detail'"
  >
    <ProcessModal @success="handleProcessSelectSuccess" />
    <Form class="mx-3">
      <template #items>
        <PurchaseOrderItemForm
src/views/erp/purchase/order/modules/item-form.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ErpProductApi } from '#/api/erp/product/product';
import type { MdmItemApi } from '#/api/mdm/item';
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
@@ -13,7 +13,7 @@
import { Input, InputNumber, Select } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getItemPage } from '#/api/mdm/item';
import { getStockCount } from '#/api/erp/stock/stock';
import { useFormItemColumns } from '../data';
@@ -37,7 +37,7 @@
]);
const tableData = ref<ErpPurchaseOrderApi.PurchaseOrderItem[]>([]); // è¡¨æ ¼æ•°æ®
const productOptions = ref<ErpProductApi.Product[]>([]); // äº§å“ä¸‹æ‹‰é€‰é¡¹
const productOptions = ref<MdmItemApi.Item[]>([]); // äº§å“ä¸‹æ‹‰é€‰é¡¹
/** èŽ·å–è¡¨æ ¼åˆè®¡æ•°æ® */
const summaries = computed(() => {
@@ -158,9 +158,9 @@
    return;
  }
  row.productId = productId;
  row.productUnitId = product.unitId;
  row.productUnitId = product.unitMeasureId;
  row.productBarCode = product.barCode;
  row.productUnitName = product.unitName;
  row.productUnitName = product.unitMeasureName;
  row.productName = product.name;
  row.stockCount = (await getStockCount(productId)) || 0;
  row.productPrice = product.purchasePrice || 0;
@@ -213,7 +213,8 @@
/** åˆå§‹åŒ– */
onMounted(async () => {
  productOptions.value = await getProductSimpleList();
  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
  productOptions.value = res.list || [];
  // ç›®çš„:新增时,默认添加一行
  if (tableData.value.length === 0) {
    handleAdd();
@@ -225,6 +226,7 @@
  <Grid class="w-full">
    <template #productId="{ row }">
      <Select
        v-if="!disabled"
        v-model:value="row.productId"
        :options="productOptions"
        :field-names="{ label: 'name', value: 'id' }"
@@ -233,6 +235,7 @@
        show-search
        @change="handleProductChange($event, row)"
      />
      <span v-else>{{ row.productName || '-' }}</span>
    </template>
    <template #count="{ row }">
      <InputNumber
src/views/erp/purchase/order/modules/process-select-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import type { ProcessDefinition } from '#/api/erp/purchase/order';
import { ref, computed } from 'vue';
import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
import { Select } from 'ant-design-vue';
const emit = defineEmits(['success']);
const processList = ref<ProcessDefinition[]>([]);
const selectedProcessId = ref<string>('');
const processOptions = computed(() => {
  return processList.value.map((p) => ({
    label: `${p.name} (V${p.version})`,
    value: p.id,
  }));
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!selectedProcessId.value) {
      return;
    }
    await modalApi.close();
    emit('success', selectedProcessId.value);
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      selectedProcessId.value = '';
      processList.value = [];
      return;
    }
    const data = modalApi.getData<ProcessDefinition[]>();
    if (data && data.length > 0) {
      processList.value = data;
    }
  },
});
</script>
<template>
  <Modal title="选择审批流程" class="w-1/3">
    <Select
      v-model:value="selectedProcessId"
      :options="processOptions"
      style="width: 100%"
      placeholder="请选择审批流程"
    />
  </Modal>
</template>
src/views/erp/purchase/request/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,293 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '../../../../packages/constants/src';
import { getDictOptions } from '../../../../packages/effects/hooks/src';
import { erpPriceInputFormatter } from '../../../../packages/utils/src';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** çŠ¶æ€å¸¸é‡ */
export const PURCHASE_REQUEST_STATUS = {
  PENDING: 0, // å¾…审批
  APPROVED: 1, // å·²å®¡æ‰¹
  REJECTED: 2, // å·²æ‹’绝
  CONVERTED: 3, // å·²è½¬è®¢å•
};
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'no',
      label: '申请编号',
      component: 'Input',
      componentProps: {
        placeholder: '系统自动生成',
        disabled: true,
      },
    },
    {
      fieldName: 'requestTime',
      label: '申请时间',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择申请时间',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'x',
        disabled: formType === 'detail',
      },
      rules: 'required',
    },
    {
      label: '供应商',
      fieldName: 'supplierId',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
        disabled: formType === 'detail',
      },
    },
    {
      fieldName: 'reason',
      label: '申请理由',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入申请理由',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: formType === 'detail',
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: formType === 'detail',
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'items',
      label: '申请明细',
      component: 'Input',
      formItemClass: 'col-span-3',
    },
  ];
}
/** è¡¨å•的明细表格列 */
export function useFormItemColumns(
  disabled: boolean,
): VxeTableGridOptions['columns'] {
  return [
    { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
    {
      field: 'productId',
      title: '产品名称',
      minWidth: 200,
      slots: { default: 'productId' },
    },
    {
      field: 'productBarCode',
      title: '条码',
      minWidth: 120,
    },
    {
      field: 'productUnitName',
      title: '单位',
      minWidth: 80,
    },
    {
      field: 'requestCount',
      title: '申请数量',
      minWidth: 120,
      fixed: 'right',
      slots: { default: 'requestCount' },
    },
    {
      field: 'estimatedPrice',
      title: '预估单价',
      minWidth: 120,
      fixed: 'right',
      slots: { default: 'estimatedPrice' },
    },
    {
      field: 'totalPrice',
      title: '预估金额',
      minWidth: 120,
      fixed: 'right',
      formatter: 'formatAmount2',
    },
    {
      field: 'requiredDate',
      title: '需求日期',
      minWidth: 140,
      fixed: 'right',
      slots: { default: 'requiredDate' },
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 150,
      slots: { default: 'remark' },
    },
    {
      title: '操作',
      width: 50,
      fixed: 'right',
      slots: { default: 'actions' },
      visible: !disabled,
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '申请编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入申请编号',
        allowClear: true,
      },
    },
    {
      fieldName: 'supplierId',
      label: '供应商',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      fieldName: 'requestTime',
      label: '申请时间',
      component: 'RangePicker',
      componentProps: {
        ...getRangePickerDefaultProps(),
        allowClear: true,
      },
    },
    {
      fieldName: 'requesterId',
      label: '申请人',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择申请人',
        allowClear: true,
        showSearch: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        valueField: 'id',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        options: [
          { label: '待审批', value: 0 },
          { label: '已审批', value: 1 },
          { label: '已拒绝', value: 2 },
          { label: '已转订单', value: 3 },
        ],
        placeholder: '请选择状态',
        allowClear: true,
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    {
      type: 'checkbox',
      width: 50,
      fixed: 'left',
    },
    {
      field: 'no',
      title: '申请编号',
      width: 200,
      fixed: 'left',
    },
    {
      field: 'supplierName',
      title: '供应商',
      minWidth: 120,
    },
    {
      field: 'requestTime',
      title: '申请时间',
      width: 160,
      formatter: 'formatDate',
    },
    {
      field: 'requesterName',
      title: '申请人',
      minWidth: 120,
    },
    {
      field: 'estimatedAmount',
      title: '预估金额',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
    {
      field: 'status',
      title: '状态',
      minWidth: 120,
      slots: { default: 'status' },
    },
    {
      field: 'reason',
      title: '申请理由',
      minWidth: 150,
      showOverflow: 'tooltip',
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 160,
      formatter: 'formatDate',
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/erp/purchase/request/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,256 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
import { ref } from 'vue';
import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
import { downloadFileFromBlobPart, isEmpty } from '../../../../packages/utils/src';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  approvePurchaseRequest,
  convertToPurchaseOrder,
  deletePurchaseRequest,
  exportPurchaseRequest,
  getPurchaseRequestPage,
} from '#/api/erp/purchase/request';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema, PURCHASE_REQUEST_STATUS } from './data';
import Form from './modules/form.vue';
/** ERP é‡‡è´­ç”³è¯·åˆ—表 */
defineOptions({ name: 'ErpPurchaseRequest' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportPurchaseRequest(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '采购申请.xls', source: data });
}
/** æ–°å¢žé‡‡è´­ç”³è¯· */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑采购申请 */
function handleEdit(row: ErpPurchaseRequestApi.PurchaseRequest) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
/** åˆ é™¤é‡‡è´­ç”³è¯· */
async function handleDelete(ids: number[]) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting'),
    duration: 0,
  });
  try {
    await deletePurchaseRequest(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å®¡æ‰¹é‡‡è´­ç”³è¯· */
async function handleApprove(row: ErpPurchaseRequestApi.PurchaseRequest, status: number) {
  const hideLoading = message.loading({
    content: `确定${status === 1 ? '审批通过' : '审批拒绝'}该申请吗?`,
    duration: 0,
  });
  try {
    await approvePurchaseRequest({ id: row.id!, status, remark: '' });
    message.success(`${status === 1 ? '审批通过' : '审批拒绝'}成功`);
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** è½¬ä¸ºé‡‡è´­è®¢å• */
async function handleConvert(row: ErpPurchaseRequestApi.PurchaseRequest) {
  const hideLoading = message.loading({
    content: '正在转为采购订单...',
    duration: 0,
  });
  try {
    await convertToPurchaseOrder(row.id!);
    message.success('已成功转为采购订单');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: ErpPurchaseRequestApi.PurchaseRequest[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
/** æŸ¥çœ‹è¯¦æƒ… */
function handleDetail(row: ErpPurchaseRequestApi.PurchaseRequest) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getPurchaseRequestPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<ErpPurchaseRequestApi.PurchaseRequest>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="采购申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['采购申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['erp:purchase-request:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['erp:purchase-request:export'],
              onClick: handleExport,
            },
            {
              label: '批量删除',
              type: 'primary',
              danger: true,
              disabled: isEmpty(checkedIds),
              icon: ACTION_ICON.DELETE,
              auth: ['erp:purchase-request:delete'],
              popConfirm: {
                title: `是否删除所选中数据?`,
                confirm: handleDelete.bind(null, checkedIds),
              },
            },
          ]"
        />
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === 0" color="warning">待审批</Tag>
        <Tag v-else-if="row.status === 1" color="success">已审批</Tag>
        <Tag v-else-if="row.status === 2" color="error">已拒绝</Tag>
        <Tag v-else-if="row.status === 3" color="default">已转订单</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['erp:purchase-request:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['erp:purchase-request:update'],
              ifShow: () => row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '审批通过',
              type: 'link',
              auth: ['erp:purchase-request:approve'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: '确认审批通过该申请吗?',
                confirm: handleApprove.bind(null, row, 1),
              },
            },
            {
              label: '审批拒绝',
              type: 'link',
              danger: true,
              auth: ['erp:purchase-request:approve'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: '确认审批拒绝该申请吗?',
                confirm: handleApprove.bind(null, row, 2),
              },
            },
            {
              label: '转订单',
              type: 'link',
              auth: ['erp:purchase-request:convert'],
              ifShow: () => row.status === 1,
              popConfirm: {
                title: '确认转为采购订单吗?',
                confirm: handleConvert.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['erp:purchase-request:delete'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                confirm: handleDelete.bind(null, [row.id!]),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/erp/purchase/request/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,135 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
import { computed, ref } from 'vue';
import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
import { message, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createPurchaseRequest,
  getPurchaseRequest,
  updatePurchaseRequest,
} from '#/api/erp/purchase/request';
import { $t } from '#/locales';
import { useFormSchema, PURCHASE_REQUEST_STATUS } from '../data';
import PurchaseRequestItemForm from './item-form.vue';
const emit = defineEmits(['success']);
const formData = ref<ErpPurchaseRequestApi.PurchaseRequest>();
const formType = ref<FormType>('create');
const itemFormRef = ref<InstanceType<typeof PurchaseRequestItemForm>>();
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['采购申请']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['采购申请']);
  } else {
    return '采购申请详情';
  }
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
/** æ›´æ–°é‡‡è´­ç”³è¯·é¡¹ */
function handleUpdateItems(items: ErpPurchaseRequestApi.PurchaseRequestItem[]) {
  formData.value = modalApi.getData<ErpPurchaseRequestApi.PurchaseRequest>();
  formData.value.items = items;
  formApi.setValues({
    items,
  });
}
/** åˆ›å»ºæˆ–更新采购申请 */
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const itemFormInstance = Array.isArray(itemFormRef.value)
      ? itemFormRef.value[0]
      : itemFormRef.value;
    try {
      itemFormInstance.validate();
    } catch (error: any) {
      message.error(error.message || '子表单验证失败');
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as ErpPurchaseRequestApi.PurchaseRequest;
    data.items = formData.value?.items?.map((item) => ({
      ...item,
      id: undefined,
    }));
    try {
      await (formType.value === 'create'
        ? createPurchaseRequest(data)
        : updatePurchaseRequest(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<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getPurchaseRequest(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal
    :title="getTitle"
    class="w-3/4"
    :show-confirm-button="formType !== 'detail'"
  >
    <Form class="mx-3">
      <template #items>
        <PurchaseRequestItemForm
          ref="itemFormRef"
          :items="formData?.items ?? []"
          :disabled="formType === 'detail'"
          @update:items="handleUpdateItems"
        />
      </template>
    </Form>
  </Modal>
</template>
src/views/erp/purchase/request/modules/item-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,267 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import {
  erpCountInputFormatter,
  erpPriceInputFormatter,
  erpPriceMultiply,
} from '../../../../../packages/utils/src';
import { Input, InputNumber, Select, DatePicker } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getItemPage } from '#/api/mdm/item';
import { useFormItemColumns } from '../data';
interface Props {
  items?: ErpPurchaseRequestApi.PurchaseRequestItem[];
  disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
  items: () => [],
  disabled: false,
});
const emit = defineEmits(['update:items']);
const tableData = ref<ErpPurchaseRequestApi.PurchaseRequestItem[]>([]);
const productOptions = ref<MdmItemApi.Item[]>([]);
/** èŽ·å–è¡¨æ ¼åˆè®¡æ•°æ® */
const summaries = computed(() => {
  return {
    count: tableData.value.reduce((sum, item) => sum + (item.requestCount || 0), 0),
    totalPrice: tableData.value.reduce(
      (sum, item) => {
        const price = erpPriceMultiply(item.estimatedPrice || 0, item.requestCount || 0) ?? 0;
        return sum + price;
      },
      0,
    ),
  };
});
/** è¡¨æ ¼é…ç½® */
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useFormItemColumns(props.disabled),
    data: tableData.value,
    minHeight: 250,
    autoResize: true,
    border: true,
    rowConfig: {
      keyField: 'seq',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  },
});
/** ç›‘听外部传入的列数据 */
watch(
  () => props.items,
  async (items) => {
    if (!items) {
      return;
    }
    items.forEach((item) => initRow(item));
    tableData.value = [...items];
    await nextTick();
    await gridApi.grid.reloadData(tableData.value);
  },
  {
    immediate: true,
  },
);
/** å¤„理新增 */
function handleAdd() {
  const newRow = {
    id: undefined,
    productId: undefined,
    productUnitName: undefined,
    productBarCode: undefined,
    requestCount: 1,
    estimatedPrice: undefined,
    requiredDate: undefined,
    remark: undefined,
    totalPrice: 0,
  };
  tableData.value.push(newRow);
  emit('update:items', [...tableData.value]);
}
/** å¤„理删除 */
function handleDelete(row: ErpPurchaseRequestApi.PurchaseRequestItem) {
  const index = tableData.value.findIndex((item) => item.seq === row.seq);
  if (index !== -1) {
    tableData.value.splice(index, 1);
  }
  emit('update:items', [...tableData.value]);
}
/** å¤„理产品变更 */
async function handleProductChange(productId: any, row: any) {
  const product = productOptions.value.find((p) => p.id === productId);
  if (!product) {
    return;
  }
  row.productId = productId;
  row.productBarCode = product.barCode;
  row.productUnitName = product.unitMeasureName;
  row.productName = product.name;
  row.estimatedPrice = product.purchasePrice || 0;
  row.requestCount = row.requestCount || 1;
  handleRowChange(row);
}
/** å¤„理行数据变更 */
function handleRowChange(row: any) {
  const index = tableData.value.findIndex((item) => item.seq === row.seq);
  if (index === -1) {
    tableData.value.push(row);
  } else {
    tableData.value[index] = row;
  }
  // è®¡ç®—预估金额
  if (row.estimatedPrice && row.requestCount) {
    row.totalPrice = erpPriceMultiply(row.estimatedPrice, row.requestCount) ?? 0;
  }
  emit('update:items', [...tableData.value]);
}
/** åˆå§‹åŒ–行数据 */
function initRow(row: ErpPurchaseRequestApi.PurchaseRequestItem) {
  if (row.estimatedPrice && row.requestCount) {
    row.totalPrice = erpPriceMultiply(row.estimatedPrice, row.requestCount) ?? 0;
  }
}
/** è¡¨å•校验 */
function validate() {
  for (let i = 0; i < tableData.value.length; i++) {
    const item = tableData.value[i];
    if (item) {
      if (!item.productId) {
        throw new Error(`第 ${i + 1} è¡Œï¼šäº§å“ä¸èƒ½ä¸ºç©º`);
      }
      if (!item.requestCount || item.requestCount <= 0) {
        throw new Error(`第 ${i + 1} è¡Œï¼šç”³è¯·æ•°é‡ä¸èƒ½ä¸ºç©º`);
      }
    }
  }
}
defineExpose({
  validate,
});
/** åˆå§‹åŒ– */
onMounted(async () => {
  const res = await getItemPage({ pageNo: 1, pageSize: 100, status: 0 });
  productOptions.value = res.list || [];
  if (tableData.value.length === 0) {
    handleAdd();
  }
});
</script>
<template>
  <Grid class="w-full">
    <template #productId="{ row }">
      <Select
        v-if="!disabled"
        v-model:value="row.productId"
        :options="productOptions"
        :field-names="{ label: 'name', value: 'id' }"
        class="w-full"
        placeholder="请选择产品"
        show-search
        @change="handleProductChange($event, row)"
      />
      <span v-else>{{ row.productName || '-' }}</span>
    </template>
    <template #requestCount="{ row }">
      <InputNumber
        v-if="!disabled"
        v-model:value="row.requestCount"
        :min="0"
        :precision="3"
        @change="handleRowChange(row)"
      />
      <span v-else>{{ erpCountInputFormatter(row.requestCount) || '-' }}</span>
    </template>
    <template #estimatedPrice="{ row }">
      <InputNumber
        v-if="!disabled"
        v-model:value="row.estimatedPrice"
        :min="0"
        :precision="2"
        @change="handleRowChange(row)"
      />
      <span v-else>{{ erpPriceInputFormatter(row.estimatedPrice) || '-' }}</span>
    </template>
    <template #requiredDate="{ row }">
      <DatePicker
        v-if="!disabled"
        v-model:value="row.requiredDate"
        valueFormat="YYYY-MM-DD"
        class="w-full"
        @change="handleRowChange(row)"
      />
      <span v-else>{{ row.requiredDate || '-' }}</span>
    </template>
    <template #remark="{ row }">
      <Input v-if="!disabled" v-model:value="row.remark" class="w-full" @change="handleRowChange(row)" />
      <span v-else>{{ row.remark || '-' }}</span>
    </template>
    <template #actions="{ row }">
      <TableAction
        :actions="[
          {
            label: '删除',
            type: 'link',
            danger: true,
            popConfirm: {
              title: '确认删除该产品吗?',
              confirm: handleDelete.bind(null, row),
            },
          },
        ]"
      />
    </template>
    <template #bottom>
      <div class="mt-2 rounded border border-border bg-muted p-2">
        <div class="flex justify-between text-sm text-muted-foreground">
          <span class="font-medium text-foreground">合计:</span>
          <div class="flex space-x-4">
            <span>申请数量:{{ erpCountInputFormatter(summaries.count) }}</span>
            <span>预估金额:{{ erpPriceInputFormatter(summaries.totalPrice) }}</span>
          </div>
        </div>
      </div>
      <TableAction
        v-if="!disabled"
        class="mt-2 flex justify-center"
        :actions="[
          {
            label: '添加申请产品',
            type: 'default',
            onClick: handleAdd,
          },
        ]"
      />
    </template>
  </Grid>
</template>
src/views/erp/purchase/statistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Page } from '../../../../packages/effects/common-ui/src';
import { erpPriceInputFormatter } from '../../../../packages/utils/src';
import { Card, Col, Row, Statistic } from 'ant-design-vue';
import { getPurchaseSummary } from '#/api/erp/purchase/statistics';
import type { ErpPurchaseStatisticsApi } from '#/api/erp/purchase/statistics';
defineOptions({ name: 'ErpPurchaseStatistics' });
const loading = ref(false);
const summary = ref<ErpPurchaseStatisticsApi.Summary>({});
/** èŽ·å–ç»Ÿè®¡æ•°æ® */
async function fetchSummary() {
  loading.value = true;
  try {
    summary.value = await getPurchaseSummary();
  } finally {
    loading.value = false;
  }
}
onMounted(() => {
  fetchSummary();
});
</script>
<template>
  <Page auto-content-height>
    <div class="p-4">
      <Card title="采购汇总统计" :loading="loading">
        <Row :gutter="16">
          <Col :span="6">
            <Statistic
              title="总订单数"
              :value="summary.totalOrderCount || 0"
              suffix="单"
            />
          </Col>
          <Col :span="6">
            <Statistic
              title="总订单金额"
              :value="erpPriceInputFormatter(summary.totalOrderAmount) || '0.00'"
              suffix="元"
            />
          </Col>
          <Col :span="6">
            <Statistic
              title="总入库数"
              :value="summary.totalInCount || 0"
              suffix="单"
            />
          </Col>
          <Col :span="6">
            <Statistic
              title="总入库金额"
              :value="erpPriceInputFormatter(summary.totalInAmount) || '0.00'"
              suffix="元"
            />
          </Col>
        </Row>
        <Row :gutter="16" class="mt-4">
          <Col :span="6">
            <Statistic
              title="总退货数"
              :value="summary.totalReturnCount || 0"
              suffix="单"
            />
          </Col>
          <Col :span="6">
            <Statistic
              title="总退货金额"
              :value="erpPriceInputFormatter(summary.totalReturnAmount) || '0.00'"
              suffix="元"
            />
          </Col>
        </Row>
      </Card>
    </div>
  </Page>
</template>
src/views/erp/purchase/statistics/price.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,133 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchaseStatisticsApi } from '#/api/erp/purchase/statistics';
import { Page } from '../../../../packages/effects/common-ui/src';
import { Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getPriceAnalysis } from '#/api/erp/purchase/statistics';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getRangePickerDefaultProps } from '#/utils';
import type { VbenFormSchema } from '#/adapter/form';
defineOptions({ name: 'ErpPurchaseStatisticsPrice' });
/** æœç´¢è¡¨å• */
const formSchema: VbenFormSchema[] = [
  {
    fieldName: 'productId',
    label: '产品',
    component: 'ApiSelect',
    componentProps: {
      placeholder: '请选择产品',
      allowClear: true,
      showSearch: true,
      api: getProductSimpleList,
      labelField: 'name',
      valueField: 'id',
    },
  },
  {
    fieldName: 'orderTime',
    label: '采购时间',
    component: 'RangePicker',
    componentProps: {
      ...getRangePickerDefaultProps(),
      allowClear: true,
    },
  },
];
/** åˆ—表字段 */
const columns: VxeTableGridOptions['columns'] = [
  {
    field: 'productName',
    title: '产品名称',
    minWidth: 150,
  },
  {
    field: 'productBarCode',
    title: '产品条码',
    minWidth: 120,
  },
  {
    field: 'avgPrice',
    title: '平均价格',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'minPrice',
    title: '最低价格',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'maxPrice',
    title: '最高价格',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'latestPrice',
    title: '最近价格',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'priceChangeRate',
    title: '价格变化率',
    minWidth: 120,
    slots: { default: 'priceChangeRate' },
  },
];
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: formSchema,
  },
  gridOptions: {
    columns,
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getPriceAnalysis({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'productId',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<ErpPurchaseStatisticsApi.PriceAnalysis>,
});
</script>
<template>
  <Page auto-content-height>
    <Grid table-title="采购价格分析">
      <template #priceChangeRate="{ row }">
        <Tag v-if="row.priceChangeRate > 0" color="red">
          +{{ row.priceChangeRate }}%
        </Tag>
        <Tag v-else-if="row.priceChangeRate < 0" color="green">
          {{ row.priceChangeRate }}%
        </Tag>
        <Tag v-else color="default">{{ row.priceChangeRate }}%</Tag>
      </template>
    </Grid>
  </Page>
</template>
src/views/erp/purchase/statistics/supplier.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,122 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchaseStatisticsApi } from '#/api/erp/purchase/statistics';
import { Page } from '../../../../packages/effects/common-ui/src';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSupplierStatistics } from '#/api/erp/purchase/statistics';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getRangePickerDefaultProps } from '#/utils';
import type { VbenFormSchema } from '#/adapter/form';
defineOptions({ name: 'ErpPurchaseStatisticsSupplier' });
/** æœç´¢è¡¨å• */
const formSchema: VbenFormSchema[] = [
  {
    fieldName: 'supplierId',
    label: '供应商',
    component: 'ApiSelect',
    componentProps: {
      placeholder: '请选择供应商',
      allowClear: true,
      showSearch: true,
      api: getSupplierSimpleList,
      labelField: 'name',
      valueField: 'id',
    },
  },
  {
    fieldName: 'orderTime',
    label: '订单时间',
    component: 'RangePicker',
    componentProps: {
      ...getRangePickerDefaultProps(),
      allowClear: true,
    },
  },
];
/** åˆ—表字段 */
const columns: VxeTableGridOptions['columns'] = [
  {
    field: 'supplierName',
    title: '供应商',
    minWidth: 150,
  },
  {
    field: 'orderCount',
    title: '订单数',
    minWidth: 100,
    formatter: 'formatAmount3',
  },
  {
    field: 'orderAmount',
    title: '订单金额',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'inCount',
    title: '入库数',
    minWidth: 100,
    formatter: 'formatAmount3',
  },
  {
    field: 'inAmount',
    title: '入库金额',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'returnCount',
    title: '退货数',
    minWidth: 100,
    formatter: 'formatAmount3',
  },
  {
    field: 'returnAmount',
    title: '退货金额',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
];
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: formSchema,
  },
  gridOptions: {
    columns,
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getSupplierStatistics({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'supplierId',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<ErpPurchaseStatisticsApi.SupplierStatistics>,
});
</script>
<template>
  <Page auto-content-height>
    <Grid table-title="供应商供货统计" />
  </Page>
</template>
src/views/erp/purchase/statistics/uncompleted.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,145 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchaseStatisticsApi } from '#/api/erp/purchase/statistics';
import { Page } from '../../../../packages/effects/common-ui/src';
import { Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getUncompletedOrders } from '#/api/erp/purchase/statistics';
import { getSupplierSimpleList } from '#/api/erp/purchase/supplier';
import { getRangePickerDefaultProps } from '#/utils';
import type { VbenFormSchema } from '#/adapter/form';
defineOptions({ name: 'ErpPurchaseStatisticsUncompleted' });
/** æœç´¢è¡¨å• */
const formSchema: VbenFormSchema[] = [
  {
    fieldName: 'orderNo',
    label: '订单号',
    component: 'Input',
    componentProps: {
      placeholder: '请输入订单号',
      allowClear: true,
    },
  },
  {
    fieldName: 'supplierId',
    label: '供应商',
    component: 'ApiSelect',
    componentProps: {
      placeholder: '请选择供应商',
      allowClear: true,
      showSearch: true,
      api: getSupplierSimpleList,
      labelField: 'name',
      valueField: 'id',
    },
  },
  {
    fieldName: 'orderTime',
    label: '订单时间',
    component: 'RangePicker',
    componentProps: {
      ...getRangePickerDefaultProps(),
      allowClear: true,
    },
  },
];
/** åˆ—表字段 */
const columns: VxeTableGridOptions['columns'] = [
  {
    field: 'orderNo',
    title: '订单号',
    minWidth: 150,
  },
  {
    field: 'supplierName',
    title: '供应商',
    minWidth: 150,
  },
  {
    field: 'orderTime',
    title: '订单时间',
    minWidth: 160,
    formatter: 'formatDate',
  },
  {
    field: 'productNames',
    title: '产品信息',
    minWidth: 150,
    showOverflow: 'tooltip',
  },
  {
    field: 'orderAmount',
    title: '订单金额',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'inAmount',
    title: '入库金额',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'uncompletedAmount',
    title: '未完成金额',
    minWidth: 120,
    formatter: 'formatAmount2',
  },
  {
    field: 'daysOverdue',
    title: '超期天数',
    minWidth: 100,
    slots: { default: 'daysOverdue' },
  },
];
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: formSchema,
  },
  gridOptions: {
    columns,
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getUncompletedOrders({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'orderId',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<ErpPurchaseStatisticsApi.UncompletedOrder>,
});
</script>
<template>
  <Page auto-content-height>
    <Grid table-title="未完成订单跟踪">
      <template #daysOverdue="{ row }">
        <Tag v-if="row.daysOverdue > 0" color="error">
          å·²è¶…期 {{ row.daysOverdue }} å¤©
        </Tag>
        <Tag v-else color="success">未超期</Tag>
      </template>
    </Grid>
  </Page>
</template>
src/views/erp/purchase/supplier/data.ts
@@ -27,6 +27,22 @@
      },
    },
    {
      fieldName: 'creditCode',
      label: '统一社会信用代码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入统一社会信用代码',
      },
    },
    {
      fieldName: 'taxNo',
      label: '税号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入税号',
      },
    },
    {
      fieldName: 'contact',
      label: '联系人',
      component: 'Input',
@@ -35,15 +51,7 @@
      },
    },
    {
      fieldName: 'mobile',
      label: '手机号码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入手机号码',
      },
    },
    {
      fieldName: 'telephone',
      fieldName: 'phone',
      label: '联系电话',
      component: 'Input',
      componentProps: {
@@ -52,83 +60,70 @@
    },
    {
      fieldName: 'email',
      label: '电子邮箱',
      label: '邮箱',
      component: 'Input',
      componentProps: {
        placeholder: '请输入电子邮箱',
      },
    },
    {
      fieldName: 'fax',
      label: '传真',
      component: 'Input',
      componentProps: {
        placeholder: '请输入传真',
      },
    },
    {
      fieldName: 'status',
      label: '开启状态',
      component: 'RadioGroup',
      componentProps: {
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      fieldName: 'sort',
      label: '排序',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入排序',
      },
      rules: 'required',
    },
    {
      fieldName: 'taxNo',
      label: '纳税人识别号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入纳税人识别号',
      },
    },
    {
      fieldName: 'taxPercent',
      label: '税率(%)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入税率',
        min: 0,
        precision: 2,
        placeholder: '请输入邮箱',
      },
    },
    {
      fieldName: 'bankName',
      label: '开户行',
      label: '开户银行',
      component: 'Input',
      componentProps: {
        placeholder: '请输入开户行',
        placeholder: '请输入开户银行',
      },
    },
    {
      fieldName: 'bankAccount',
      label: '开户账号',
      label: '银行账号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入开户账号',
        placeholder: '请输入银行账号',
      },
    },
    {
      fieldName: 'bankAddress',
      label: '开户地址',
      fieldName: 'cooperationTime',
      label: '合作开始时间',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择合作开始时间',
        class: '!w-full',
        valueFormat: 'x',
      },
    },
    {
      fieldName: 'address',
      label: '地址',
      component: 'Input',
      componentProps: {
        placeholder: '请输入开户地址',
        placeholder: '请输入地址',
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        options: [
          { label: '启用', value: 0 },
          { label: '禁用', value: 1 },
        ],
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: z.number().default(0),
    },
    {
      fieldName: 'qualification',
      label: '资质信息',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入资质信息',
        rows: 3,
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'remark',