采购申请批量提交、订单导入重复弹框;修复考勤异常申请字段不匹配
- 采购订单导入:支持 overwrite 参数,检测到重复订单时弹框提醒,用户确认后再覆盖
- 采购申请:列表新增批量提交按钮,按 50 张分批调用批量提交接口,失败明细弹框展示
- 考勤异常申请:表单/列表/审批详情字段改为 exceptionDate、type,异常类型选项与后端枚举一致
- 补充提交图标
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | }); |
| | | } |
| | | |
| | | /** 采购订单导入重复项 */ |
| | | export interface ImportDuplicate { |
| | | /** 导入文件中的采购单编号;编号为空时为行号 */ |
| | | key: string; |
| | | /** 系统内已存在的采购订单编号 */ |
| | | existingNo: string; |
| | | /** 系统内已存在的采购订单编号 */ |
| | | existingId: number; |
| | | /** 重复原因 */ |
| | | reason: string; |
| | | } |
| | | |
| | | /** 采购订单导入结果 */ |
| | | export interface ImportResult { |
| | | /** 创建成功的采购单编号 */ |
| | |
| | | updateList?: string[]; |
| | | /** 导入失败集合,key 为采购单编号或行号,value 为失败原因 */ |
| | | failureList?: Record<string, string>; |
| | | /** 检测到的重复订单;未开启覆盖时这些订单不会导入 */ |
| | | duplicateList?: ImportDuplicate[]; |
| | | } |
| | | |
| | | /** 获取采购审核流程列表响应 */ |
| | |
| | | } |
| | | |
| | | /** 导入采购订单 */ |
| | | export function importPurchaseOrder(file: File, directApprove: boolean) { |
| | | export function importPurchaseOrder( |
| | | file: File, |
| | | directApprove: boolean, |
| | | overwrite = false, |
| | | ) { |
| | | return requestClient.upload<ImportResult>( |
| | | '/erp/purchase-order/import', |
| | | { file, directApprove }, |
| | | { file, directApprove, overwrite }, |
| | | ); |
| | | } |
| | |
| | | return requestClient.put('/erp/purchase-request/submit', null, { params: { id, processDefinitionKey } }); |
| | | } |
| | | |
| | | /** 采购申请批量提交结果 */ |
| | | export interface SubmitBatchResult { |
| | | /** 提交成功的采购申请编号 */ |
| | | successIds?: number[]; |
| | | /** 提交失败集合,key 为采购申请单号(取不到单号时为编号),value 为失败原因 */ |
| | | failureMap?: Record<string, string>; |
| | | } |
| | | |
| | | /** 批量提交采购申请审批 */ |
| | | export function submitPurchaseRequestBatch( |
| | | ids: number[], |
| | | processDefinitionKey: string, |
| | | ) { |
| | | return requestClient.put<SubmitBatchResult>( |
| | | '/erp/purchase-request/submit-batch', |
| | | null, |
| | | { params: { ids: ids.join(','), processDefinitionKey } }, |
| | | ); |
| | | } |
| | | |
| | | /** 重置被驳回的采购申请为草稿状态 */ |
| | | export function resetPurchaseRequestStatus(id: number) { |
| | | return requestClient.put('/erp/purchase-request/reset-status', null, { params: { id } }); |
| | |
| | | userId?: number; |
| | | userName?: string; |
| | | deptName?: string; |
| | | date?: string; |
| | | exceptionType?: number; |
| | | exceptionTypeName?: string; |
| | | exceptionDate?: string; |
| | | type?: number; |
| | | reason?: string; |
| | | blobIds?: number[]; |
| | | attachmentList?: SystemStorageApi.StorageBlob[]; |
| | |
| | | CLOSE: 'lucide:x', |
| | | BOOK: 'lucide:book', |
| | | AUDIT: 'lucide:file-check', |
| | | SUBMIT: 'lucide:send', |
| | | }; |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { ImportResult } from '#/api/erp/purchase/order'; |
| | | import type { |
| | | ImportDuplicate, |
| | | ImportResult, |
| | | } from '#/api/erp/purchase/order'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | import { computed, h, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { downloadFileFromBlobPart } from '#/packages/utils/src'; |
| | | |
| | | import { Button, message, Progress, Switch, Upload } from 'ant-design-vue'; |
| | | import { |
| | | Button, |
| | | message, |
| | | Modal as AntModal, |
| | | Progress, |
| | | Switch, |
| | | Upload, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | importPurchaseOrder, |
| | |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await importPurchaseOrder( |
| | | fileList.value[0] as File, |
| | | directApprove.value, |
| | | ); |
| | | // 完成:进度条走满 |
| | | // 1. 先按“不覆盖”导入:重复订单不落库,返回给前端弹框确认 |
| | | let result = await runImport(false); |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | |
| | | // 2. 检测到重复时弹框提醒,用户确认后才以覆盖模式重新导入 |
| | | const duplicates = result.duplicateList ?? []; |
| | | if (duplicates.length > 0) { |
| | | const overwrite = await askOverwrite(duplicates); |
| | | if (overwrite) { |
| | | startMockProgress(); |
| | | result = await runImport(true); |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | } |
| | | } |
| | | |
| | | importResult.value = result; |
| | | await delay(300); |
| | | emit('success'); |
| | | const failedCount = Object.keys(importResult.value?.failureList ?? {}).length; |
| | | const failedCount = Object.keys(result.failureList ?? {}).length; |
| | | const duplicateCount = result.duplicateList?.length ?? 0; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `导入完成:新增 ${createCount.value} 条,失败 ${failedCount} 条,请查看下方失败明细`, |
| | | `导入完成:新增 ${createCount.value} 张,覆盖更新 ${updateCount.value} 张,失败 ${failedCount} 张,请查看下方失败明细`, |
| | | ); |
| | | } else if (duplicateCount > 0) { |
| | | message.warning( |
| | | `导入完成:新增 ${createCount.value} 张;${duplicateCount} 张重复订单未导入,可在下方选择覆盖`, |
| | | ); |
| | | } else { |
| | | message.success('导入完成'); |
| | |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | function runImport(overwrite: boolean) { |
| | | return importPurchaseOrder( |
| | | fileList.value[0] as File, |
| | | directApprove.value, |
| | | overwrite, |
| | | ); |
| | | } |
| | | |
| | | /** 重复订单弹框:确认后覆盖系统内已有订单 */ |
| | | function askOverwrite(duplicates: ImportDuplicate[]) { |
| | | return new Promise<boolean>((resolve) => { |
| | | const lines = duplicates.slice(0, 10).map((item) => |
| | | h( |
| | | 'div', |
| | | { class: 'text-sm leading-6' }, |
| | | `${item.key} → 已存在订单 ${item.existingNo}(${item.reason})`, |
| | | ), |
| | | ); |
| | | if (duplicates.length > 10) { |
| | | lines.push( |
| | | h( |
| | | 'div', |
| | | { class: 'text-sm leading-6 text-gray-500' }, |
| | | `……另有 ${duplicates.length - 10} 张重复订单`, |
| | | ), |
| | | ); |
| | | } |
| | | AntModal.confirm({ |
| | | title: `检测到 ${duplicates.length} 张重复采购订单`, |
| | | width: 560, |
| | | zIndex: 1100, |
| | | content: h('div', [ |
| | | h( |
| | | 'p', |
| | | { class: 'mb-2' }, |
| | | '以下订单在系统中已存在,本次未导入。覆盖会用文件内容更新系统内已有订单;已入库、已退货、已关联发票或采购申请、已提交审批流程的订单不会覆盖。如确认是正常的新采购(不是重复上传),请在文件中填写一个系统中不存在的采购单编号后重新导入。', |
| | | ), |
| | | ...lines, |
| | | ]), |
| | | okText: '覆盖重复订单', |
| | | cancelText: '不覆盖,仅保留非重复项', |
| | | maskClosable: false, |
| | | onOk: () => resolve(true), |
| | | onCancel: () => resolve(false), |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | /** 导入结果中的“覆盖重复订单”按钮 */ |
| | | async function handleOverwrite() { |
| | | if (importing.value || fileList.value.length === 0) return; |
| | | modalApi.lock(); |
| | | importing.value = true; |
| | | startMockProgress(); |
| | | try { |
| | | importResult.value = await runImport(true); |
| | | stopMockProgress(); |
| | | progressPercent.value = 100; |
| | | emit('success'); |
| | | const failedCount = Object.keys(importResult.value?.failureList ?? {}).length; |
| | | if (failedCount > 0) { |
| | | message.warning( |
| | | `覆盖完成:更新 ${updateCount.value} 张,失败 ${failedCount} 张,请查看下方失败明细`, |
| | | ); |
| | | } else { |
| | | message.success(`覆盖完成:更新 ${updateCount.value} 张采购订单`); |
| | | } |
| | | } finally { |
| | | stopMockProgress(); |
| | | importing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | /** 模拟进度条:请求期间从 0 走到 90% */ |
| | | function startMockProgress() { |
| | |
| | | const failureCount = computed( |
| | | () => Object.keys(importResult.value?.failureList ?? {}).length, |
| | | ); |
| | | const duplicateCount = computed( |
| | | () => importResult.value?.duplicateList?.length ?? 0, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | </div> |
| | | <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"> |
| | | <p>仅允许导入 xls、xlsx 格式文件;模板中“供应商名称”按名称(或编码)填写,需与系统内供应商一致;“产品编码”按物料编码填写(“产品名称”列仅供对照,匹配以产品编码为准);“采购时间”格式如 2026-01-01 10:00:00,留空取当前时间。</p> |
| | | <p>一张采购单可含多条明细:相同“采购单编号”的多行会合并为一张订单(主表字段取该组首行);“采购单编号”留空时每一行各生成一张新订单。采购单编号已存在时会导入失败。</p> |
| | | <p>一张采购单可含多条明细:相同“采购单编号”的多行会合并为一张订单(主表字段取该组首行);“采购单编号”留空时每一行各生成一张新订单。</p> |
| | | <p>导入时会自动检测重复订单(采购单编号已存在,或供应商、明细、数量、单价、金额与已有订单完全一致)。检测到重复会弹框提醒,由您决定是否覆盖;已入库、已退货、已关联发票或采购申请的订单不会被覆盖。</p> |
| | | <p>订单状态:勾选上方开关 = 直接置为“已审核”;不勾选 = “未审核”。导入不会自动提交审批流程。</p> |
| | | </div> |
| | | <!-- 导入进度条 --> |
| | |
| | | <div class="mb-2 flex gap-4"> |
| | | <span class="text-green-600">新增:{{ createCount }} 张</span> |
| | | <span class="text-blue-600">覆盖更新:{{ updateCount }} 张</span> |
| | | <span class="text-amber-600">重复未导入:{{ duplicateCount }} 张</span> |
| | | <span class="text-red-600">失败:{{ failureCount }} 张</span> |
| | | </div> |
| | | <div v-if="duplicateCount > 0" class="mb-2"> |
| | | <div class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="item in importResult.duplicateList" |
| | | :key="`${item.key}-${item.existingId}`" |
| | | class="text-sm leading-6 text-amber-600" |
| | | > |
| | | {{ item.key }} → 已存在订单 {{ item.existingNo }}({{ item.reason }}) |
| | | </div> |
| | | </div> |
| | | <Button |
| | | class="mt-2" |
| | | type="primary" |
| | | danger |
| | | :loading="importing" |
| | | @click="handleOverwrite" |
| | | > |
| | | 覆盖重复订单 |
| | | </Button> |
| | | </div> |
| | | <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> |
| | | <div |
| | | v-for="(reason, name) in importResult.failureList" |
| | |
| | | |
| | | /** 提交审批 - 先获取流程列表 */ |
| | | async function handleSubmit(row: ErpPurchaseRequestApi.PurchaseRequest) { |
| | | await openProcessModal([row.id!]); |
| | | } |
| | | |
| | | /** 打开审批流程选择弹窗(支持单张/批量) */ |
| | | async function openProcessModal(ids: number[]) { |
| | | if (ids.length === 0) return; |
| | | const hideLoading = message.loading({ |
| | | content: '获取审批流程...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const processList = await getApproveProcessList(row.id!); |
| | | const processList = await getApproveProcessList(ids[0]!); |
| | | hideLoading(); |
| | | if (!processList || processList.length === 0) { |
| | | message.warning('暂无可用审批流程,请先配置 BPM 流程'); |
| | | return; |
| | | } |
| | | // 打开流程选择弹窗 |
| | | processModalApi.setData({ id: row.id, processList }).open(); |
| | | processModalApi.setData({ ids, processList }).open(); |
| | | } catch { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** 批量提交:只提交可提交(未提交/审批不通过)的申请 */ |
| | | function handleBatchSubmit() { |
| | | const eligible = checkedRows.value.filter( |
| | | (row) => row.status === 0 || Number(row.status) === 30, |
| | | ); |
| | | if (eligible.length === 0) { |
| | | message.warning( |
| | | '所选采购申请中没有可提交的单据,只有“未提交”或“审批不通过”的申请可以提交', |
| | | ); |
| | | return; |
| | | } |
| | | const skipped = checkedRows.value.length - eligible.length; |
| | | if (skipped > 0) { |
| | | message.info( |
| | | `已自动跳过 ${skipped} 张不可提交的申请,本次提交 ${eligible.length} 张`, |
| | | ); |
| | | } |
| | | openProcessModal(eligible.map((row) => row.id!)); |
| | | } |
| | | |
| | | /** 生成采购订单 */ |
| | |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | const checkedRows = ref<ErpPurchaseRequestApi.PurchaseRequest[]>([]); |
| | | function handleRowCheckboxChange({ |
| | | records, |
| | | }: { |
| | | records: ErpPurchaseRequestApi.PurchaseRequest[]; |
| | | }) { |
| | | checkedRows.value = records; |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: '批量提交', |
| | | type: 'primary', |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.SUBMIT, |
| | | auth: ['erp:purchase-request:submit'], |
| | | onClick: handleBatchSubmit, |
| | | }, |
| | | { |
| | | label: '批量删除', |
| | | type: 'primary', |
| | | danger: true, |
| | |
| | | <script lang="ts" setup> |
| | | import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request'; |
| | | |
| | | import { ref, computed } from 'vue'; |
| | | import { ref, computed, h } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Select } from 'ant-design-vue'; |
| | | import { message, Modal as AntModal, Select } from 'ant-design-vue'; |
| | | |
| | | import { submitPurchaseRequest } from '#/api/erp/purchase/request'; |
| | | import { |
| | | submitPurchaseRequest, |
| | | submitPurchaseRequestBatch, |
| | | } from '#/api/erp/purchase/request'; |
| | | |
| | | interface ProcessDefinition { |
| | | id: string; |
| | |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const requestId = ref<number>(); |
| | | const requestIds = ref<number[]>([]); |
| | | const processList = ref<ProcessDefinition[]>([]); |
| | | const selectedProcessKey = ref<string>(''); |
| | | |
| | |
| | | })); |
| | | }); |
| | | |
| | | const isBatch = computed(() => requestIds.value.length > 1); |
| | | const modalTitle = computed(() => |
| | | isBatch.value |
| | | ? `批量提交采购申请(${requestIds.value.length} 张)` |
| | | : '选择审批流程', |
| | | ); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!selectedProcessKey.value) { |
| | | message.warning('请选择审批流程'); |
| | | return; |
| | | } |
| | | if (requestIds.value.length === 0) return; |
| | | modalApi.lock(); |
| | | try { |
| | | await submitPurchaseRequest(requestId.value!, selectedProcessKey.value); |
| | | if (!isBatch.value) { |
| | | await submitPurchaseRequest( |
| | | requestIds.value[0]!, |
| | | selectedProcessKey.value, |
| | | ); |
| | | message.success('提交成功'); |
| | | } else { |
| | | // 分批提交,避免一次性提交过多导致请求超时 |
| | | const batchSize = 50; |
| | | const total = requestIds.value.length; |
| | | let successCount = 0; |
| | | const failures: [string, string][] = []; |
| | | const loadingKey = 'purchase-request-batch-submit'; |
| | | message.open({ |
| | | key: loadingKey, |
| | | type: 'loading', |
| | | content: `正在提交采购申请:0/${total}`, |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | for (let i = 0; i < total; i += batchSize) { |
| | | const chunk = requestIds.value.slice(i, i + batchSize); |
| | | const result = await submitPurchaseRequestBatch( |
| | | chunk, |
| | | selectedProcessKey.value, |
| | | ); |
| | | successCount += result.successIds?.length ?? 0; |
| | | failures.push(...Object.entries(result.failureMap ?? {})); |
| | | message.open({ |
| | | key: loadingKey, |
| | | type: 'loading', |
| | | content: `正在提交采购申请:${Math.min( |
| | | i + batchSize, |
| | | total, |
| | | )}/${total}`, |
| | | duration: 0, |
| | | }); |
| | | } |
| | | } finally { |
| | | message.destroy(loadingKey); |
| | | } |
| | | |
| | | if (failures.length > 0) { |
| | | AntModal.warning({ |
| | | title: `批量提交完成:成功 ${successCount} 张,失败 ${failures.length} 张`, |
| | | width: 600, |
| | | zIndex: 1100, |
| | | content: h( |
| | | 'div', |
| | | { class: 'max-h-80 overflow-y-auto' }, |
| | | failures.map(([name, reason]) => |
| | | h( |
| | | 'div', |
| | | { class: 'text-sm leading-6' }, |
| | | `${name}:${reason}`, |
| | | ), |
| | | ), |
| | | ), |
| | | okText: '知道了', |
| | | }); |
| | | } else { |
| | | message.success(`批量提交成功,共 ${successCount} 张`); |
| | | } |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('提交成功'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | requestId.value = undefined; |
| | | requestIds.value = []; |
| | | selectedProcessKey.value = ''; |
| | | processList.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>(); |
| | | requestId.value = data.id; |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | ids?: number[]; |
| | | processList: ProcessDefinition[]; |
| | | }>(); |
| | | requestIds.value = data.ids ?? (data.id ? [data.id] : []); |
| | | processList.value = data.processList || []; |
| | | // 默认选择第一个流程 |
| | | if (processList.value.length > 0) { |
| | | selectedProcessKey.value = processList.value[0].key; |
| | | selectedProcessKey.value = processList.value[0]!.key; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="选择审批流程" class="w-1/3"> |
| | | <Modal :title="modalTitle" class="w-1/3"> |
| | | <div class="mb-4"> |
| | | <span class="mr-2">审批流程:</span> |
| | | <Select |
| | |
| | | /** 表单类型 */ |
| | | export type FormType = 'create' | 'detail'; |
| | | |
| | | /** 考勤异常类型,与后端 HrmAttendanceExceptionTypeEnum 保持一致 */ |
| | | const EXCEPTION_TYPE_OPTIONS = [ |
| | | { label: '补卡', value: 1 }, |
| | | { label: '加班', value: 2 }, |
| | | { label: '出差', value: 3 }, |
| | | ]; |
| | | |
| | | /** 考勤异常类型名称 */ |
| | | export function formatExceptionType(value?: number) { |
| | | return EXCEPTION_TYPE_OPTIONS.find((item) => item.value === value)?.label ?? '-'; |
| | | } |
| | | |
| | | /** 新增考勤异常申请的表单 */ |
| | | export function useFormSchema(formType: FormType): VbenFormSchema[] { |
| | | return [ |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'date', |
| | | label: '考勤日期', |
| | | fieldName: 'exceptionDate', |
| | | label: '异常日期', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | placeholder: '请选择考勤日期', |
| | | placeholder: '请选择异常日期', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | style: { width: '100%' }, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'exceptionType', |
| | | fieldName: 'type', |
| | | label: '异常类型', |
| | | component: 'Select', |
| | | componentProps: { |
| | | placeholder: '请选择异常类型', |
| | | options: [ |
| | | { label: '漏打卡', value: 1 }, |
| | | { label: '迟到', value: 2 }, |
| | | { label: '早退', value: 3 }, |
| | | { label: '其他', value: 4 }, |
| | | ], |
| | | options: EXCEPTION_TYPE_OPTIONS, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | return [ |
| | | { field: 'userName', title: '申请人', minWidth: 100 }, |
| | | { field: 'deptName', title: '部门', minWidth: 120 }, |
| | | { field: 'date', title: '考勤日期', width: 110 }, |
| | | { field: 'exceptionTypeName', title: '异常类型', width: 100 }, |
| | | { field: 'exceptionDate', title: '异常日期', width: 110 }, |
| | | { |
| | | field: 'type', |
| | | title: '异常类型', |
| | | width: 100, |
| | | formatter: ({ cellValue }: { cellValue: number }) => |
| | | formatExceptionType(cellValue), |
| | | }, |
| | | { field: 'reason', title: '异常原因', minWidth: 200 }, |
| | | { |
| | | field: 'status', |
| | |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | import { formatExceptionType } from '../../data'; |
| | | |
| | | defineOptions({ name: 'HrmAttendanceExceptionApproveDetail' }); |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | |
| | | { label: '所属部门', field: 'deptName', render: textRender }, |
| | | { |
| | | label: '异常日期', |
| | | field: 'date', |
| | | field: 'exceptionDate', |
| | | render: (val) => formatDateTime(val) as string, |
| | | }, |
| | | { label: '异常类型', field: 'exceptionTypeName', render: textRender }, |
| | | { |
| | | label: '异常类型', |
| | | field: 'type', |
| | | render: (val) => formatExceptionType(val as number), |
| | | }, |
| | | { label: '异常说明', field: 'reason', render: textRender }, |
| | | { |
| | | label: '审批状态', |