10 天以前 bdaa472dc215775e5d306b7046af0bc1111c718d
feat(bpm): 添加业务附件配置功能

- 在 package.json 中添加 cross-env 和 vue-tsc 依赖
- 更新 pnpm-lock.yaml 以同步依赖变更
- 新增业务附件配置的数据结构定义文件 data.ts
- 创建业务附件配置的表单组件 modules/form.vue
- 实现业务附件配置的 API 接口 index.ts
- 开发业务附件配置的主页面 index.vue
- 添加 CRM 合同、发票、回款的审批详情页面
- 添加 ERP 财务付款的审批详情页面
已修改3个文件
已添加15个文件
1848 ■■■■■ 文件已修改
package.json 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-workspace.yaml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/businessAttach/index.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/businessAttach/data.ts 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/businessAttach/index.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/businessAttach/modules/form.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/detail/approve/index.vue 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/invoice/detail/approve/index.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/detail/approve/index.vue 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/detail/approve/index.vue 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/detail/approve/index.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/detail/index.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/detail/index.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/exception/detail/approve/index.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/detail/approve/index.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/resignation/detail/approve/index.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/transfer/detail/approve/index.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -81,11 +81,13 @@
    "@vben/tailwind-config": "workspace:*",
    "@vben/tsconfig": "workspace:*",
    "@vben/vite-config": "workspace:*",
    "cross-env": "catalog:",
    "jiti": "^2.7.0",
    "tsdown": "^0.22.3",
    "tsx": "^4.22.4",
    "typescript": "catalog:",
    "vite": "catalog:"
    "vite": "catalog:",
    "vue-tsc": "catalog:"
  },
  "packageManager": "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38"
}
pnpm-lock.yaml
@@ -231,6 +231,9 @@
    cropperjs:
      specifier: ^1.6.2
      version: 1.6.2
    cross-env:
      specifier: ^10.1.0
      version: 10.1.0
    crypto-js:
      specifier: ^4.2.0
      version: 4.2.0
@@ -504,6 +507,9 @@
    vue-tippy:
      specifier: ^6.7.1
      version: 6.7.1
    vue-tsc:
      specifier: ^3.3.9
      version: 3.3.9
    vue3-print-nb:
      specifier: ^0.1.4
      version: 0.1.4
@@ -693,12 +699,15 @@
      '@vben/vite-config':
        specifier: workspace:*
        version: link:internal/vite-config
      cross-env:
        specifier: 'catalog:'
        version: 10.1.0
      jiti:
        specifier: ^2.7.0
        version: 2.7.0
      tsdown:
        specifier: ^0.22.3
        version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))
        version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3))
      tsx:
        specifier: ^4.22.4
        version: 4.22.4
@@ -708,6 +717,9 @@
      vite:
        specifier: 'catalog:'
        version: 8.0.10(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
      vue-tsc:
        specifier: 'catalog:'
        version: 3.3.9(typescript@6.0.3)
  internal/lint-configs/commitlint-config:
    dependencies:
@@ -2552,6 +2564,9 @@
  '@emotion/unitless@0.8.1':
    resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
  '@epic-web/invariant@1.0.0':
    resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
  '@es-joy/jsdoccomment@0.84.0':
    resolution: {integrity: sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==}
@@ -4754,6 +4769,9 @@
  '@vue/devtools-shared@8.1.3':
    resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==}
  '@vue/language-core@3.3.9':
    resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==}
  '@vue/reactivity@3.5.38':
    resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==}
@@ -4914,6 +4932,9 @@
  ajv@8.20.0:
    resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
  alien-signals@3.2.1:
    resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
  ansi-regex@5.0.1:
    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@@ -5527,6 +5548,11 @@
  cropperjs@1.6.2:
    resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
  cross-env@10.1.0:
    resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
    engines: {node: '>=20'}
    hasBin: true
  cross-spawn@7.0.6:
    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
@@ -9595,6 +9621,12 @@
    peerDependencies:
      vue: ^3.2.0
  vue-tsc@3.3.9:
    resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==}
    hasBin: true
    peerDependencies:
      typescript: '>=5.0.0'
  vue-types@3.0.0:
    resolution: {integrity: sha512-xzq2D7zLGbzn3bPQ0CWWYx4W78JWdGYZAFfRkzxz1aTm9DaNoH0qxYDiLXBofBEQ5X78l32uIq/G9INQ2E8X/Q==}
    engines: {node: '>=10.15.0'}
@@ -10976,6 +11008,8 @@
  '@emotion/hash@0.9.2': {}
  '@emotion/unitless@0.8.1': {}
  '@epic-web/invariant@1.0.0': {}
  '@es-joy/jsdoccomment@0.84.0':
    dependencies:
@@ -12903,6 +12937,16 @@
  '@vue/devtools-shared@8.1.3': {}
  '@vue/language-core@3.3.9':
    dependencies:
      '@volar/language-core': 2.4.28
      '@vue/compiler-dom': 3.5.38
      '@vue/shared': 3.5.38
      alien-signals: 3.2.1
      muggle-string: 0.4.1
      path-browserify: 1.0.1
      picomatch: 4.0.4
  '@vue/reactivity@3.5.38':
    dependencies:
      '@vue/shared': 3.5.38
@@ -13069,6 +13113,8 @@
      fast-uri: 3.1.2
      json-schema-traverse: 1.0.0
      require-from-string: 2.0.2
  alien-signals@3.2.1: {}
  ansi-regex@5.0.1: {}
@@ -13722,6 +13768,11 @@
  croner@10.0.1: {}
  cropperjs@1.6.2: {}
  cross-env@10.1.0:
    dependencies:
      '@epic-web/invariant': 1.0.0
      cross-spawn: 7.0.6
  cross-spawn@7.0.6:
    dependencies:
@@ -16668,7 +16719,7 @@
  robust-predicates@3.0.3: {}
  rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3):
  rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.9(typescript@6.0.3)):
    dependencies:
      '@babel/generator': 8.0.0
      '@babel/helper-validator-identifier': 8.0.2
@@ -16681,6 +16732,7 @@
      rolldown: 1.1.2
    optionalDependencies:
      typescript: 6.0.3
      vue-tsc: 3.3.9(typescript@6.0.3)
    transitivePeerDependencies:
      - oxc-resolver
@@ -17573,7 +17625,7 @@
      minimist: 1.2.8
      strip-bom: 3.0.0
  tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)):
  tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3)):
    dependencies:
      ansis: 4.3.1
      cac: 7.0.0
@@ -17584,7 +17636,7 @@
      obug: 2.1.3
      picomatch: 4.0.4
      rolldown: 1.1.2
      rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3)
      rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.9(typescript@6.0.3))
      semver: 7.8.5
      tinyexec: 1.2.4
      tinyglobby: 0.2.17
@@ -18092,6 +18144,12 @@
      tippy.js: 6.3.7
      vue: 3.5.38(typescript@6.0.3)
  vue-tsc@3.3.9(typescript@6.0.3):
    dependencies:
      '@volar/typescript': 2.4.28
      '@vue/language-core': 3.3.9
      typescript: 6.0.3
  vue-types@3.0.0(vue@3.5.38(typescript@6.0.3)):
    dependencies:
      is-plain-object: 3.0.1
pnpm-workspace.yaml
@@ -193,7 +193,7 @@
  vue-json-pretty: ^2.6.0
  vue-router: ^5.1.0
  vue-tippy: ^6.7.1
  vue-tsc: ^3.3.3
  vue-tsc: ^3.3.9
  vue3-print-nb: ^0.1.4
  vue3-signature: ^0.4.4
  vuedraggable: ^4.1.0
src/api/bpm/businessAttach/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace BpmBusinessAttachApi {
  /** ä¸šåŠ¡é™„ä»¶é…ç½® */
  export interface BusinessAttach {
    id: number;
    processDefinitionKey: string;
    businessType?: string;
    componentPath: string;
    status: number;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢ä¸šåŠ¡é™„ä»¶é…ç½®åˆ†é¡µ */
export async function getBusinessAttachPage(params: PageParam) {
  return requestClient.get<PageResult<BpmBusinessAttachApi.BusinessAttach>>(
    '/bpm/business-attach/page',
    { params },
  );
}
/** æŸ¥è¯¢ä¸šåŠ¡é™„ä»¶é…ç½®è¯¦æƒ… */
export async function getBusinessAttach(id: number) {
  return requestClient.get<BpmBusinessAttachApi.BusinessAttach>(
    `/bpm/business-attach/get?id=${id}`,
  );
}
/** æ–°å¢žä¸šåŠ¡é™„ä»¶é…ç½® */
export async function createBusinessAttach(
  data: BpmBusinessAttachApi.BusinessAttach,
) {
  return requestClient.post<number>('/bpm/business-attach/create', data);
}
/** ä¿®æ”¹ä¸šåŠ¡é™„ä»¶é…ç½® */
export async function updateBusinessAttach(
  data: BpmBusinessAttachApi.BusinessAttach,
) {
  return requestClient.put<boolean>('/bpm/business-attach/update', data);
}
/** åˆ é™¤ä¸šåŠ¡é™„ä»¶é…ç½® */
export async function deleteBusinessAttach(id: number) {
  return requestClient.delete<boolean>(
    `/bpm/business-attach/delete?id=${id}`,
  );
}
src/views/bpm/businessAttach/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,163 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { getRangePickerDefaultProps } from '#/utils';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'processDefinitionKey',
      label: '流程定义 Key',
      component: 'Input',
      componentProps: {
        placeholder: '请输入流程定义 Key,如 purchase_approve',
      },
      rules: 'required',
    },
    {
      fieldName: 'businessType',
      label: '业务类型',
      component: 'Input',
      componentProps: {
        placeholder: '请输入业务类型,如 erp_purchase_order',
      },
    },
    {
      fieldName: 'componentPath',
      label: '业务详情组件路径',
      component: 'Input',
      componentProps: {
        placeholder:
          '请输入业务详情组件路径,如 /erp/purchase/order/detail/index.vue',
      },
      rules: 'required',
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        buttonStyle: 'solid',
        optionType: 'button',
      },
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'processDefinitionKey',
      label: '流程定义 Key',
      component: 'Input',
      componentProps: {
        placeholder: '请输入流程定义 Key',
        allowClear: true,
      },
    },
    {
      fieldName: 'businessType',
      label: '业务类型',
      component: 'Input',
      componentProps: {
        placeholder: '请输入业务类型',
        allowClear: true,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        placeholder: '请选择状态',
        allowClear: true,
      },
    },
    {
      fieldName: 'createTime',
      label: '创建时间',
      component: 'RangePicker',
      componentProps: {
        ...getRangePickerDefaultProps(),
        allowClear: true,
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    {
      field: 'id',
      title: '编号',
      minWidth: 80,
    },
    {
      field: 'processDefinitionKey',
      title: '流程定义 Key',
      minWidth: 180,
    },
    {
      field: 'businessType',
      title: '业务类型',
      minWidth: 160,
    },
    {
      field: 'componentPath',
      title: '业务详情组件路径',
      minWidth: 260,
    },
    {
      field: 'status',
      title: '状态',
      minWidth: 80,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.COMMON_STATUS },
      },
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 160,
    },
    {
      field: 'createTime',
      title: '创建时间',
      minWidth: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 160,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/bpm/businessAttach/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { BpmBusinessAttachApi } from '#/api/bpm/businessAttach';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteBusinessAttach,
  getBusinessAttachPage,
} from '#/api/bpm/businessAttach';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºä¸šåŠ¡é™„ä»¶é…ç½® */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑业务附件配置 */
function handleEdit(row: BpmBusinessAttachApi.BusinessAttach) {
  formModalApi.setData(row).open();
}
/** åˆ é™¤ä¸šåŠ¡é™„ä»¶é…ç½® */
async function handleDelete(row: BpmBusinessAttachApi.BusinessAttach) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.processDefinitionKey]),
    duration: 0,
  });
  try {
    await deleteBusinessAttach(row.id as number);
    message.success($t('ui.actionMessage.deleteSuccess'));
    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 getBusinessAttachPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<BpmBusinessAttachApi.BusinessAttach>,
});
</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: ['bpm:business-attach:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['bpm:business-attach:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['bpm:business-attach:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  row.processDefinitionKey,
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/bpm/businessAttach/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { BpmBusinessAttachApi } from '#/api/bpm/businessAttach';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createBusinessAttach,
  getBusinessAttach,
  updateBusinessAttach,
} from '#/api/bpm/businessAttach';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<BpmBusinessAttachApi.BusinessAttach>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['业务附件配置'])
    : $t('ui.actionTitle.create', ['业务附件配置']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-2',
    labelWidth: 140,
  },
  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 BpmBusinessAttachApi.BusinessAttach;
    try {
      await (formData.value?.id
        ? updateBusinessAttach(data)
        : createBusinessAttach(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<BpmBusinessAttachApi.BusinessAttach>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getBusinessAttach(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/3">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/crm/contract/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,190 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmContractApi } from '#/api/crm/contract';
import type { DescriptionItemSchema } from '#/components/description';
import { h, nextTick, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import {
  erpCountInputFormatter,
  erpPriceInputFormatter,
  formatDateTime,
} from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getContract } from '#/api/crm/contract';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'CrmContractApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<CrmContractApi.Contract>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** åˆåŒäº§å“æ˜Žç»†ï¼ˆå®¡æ‰¹è¯¦æƒ…只读列) */
function useProductColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'seq', title: '序号', width: 60 },
    { field: 'itemName', title: '产品名称', minWidth: 180 },
    { field: 'itemCode', title: '物料编码', minWidth: 120 },
    { field: 'itemUnitName', title: '单位', minWidth: 80, formatter: ({ cellValue }) => textRender(cellValue) },
    {
      field: 'contractPrice',
      title: '合同价格(元)',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'count',
      title: '数量',
      minWidth: 100,
      formatter: ({ cellValue }) => erpCountInputFormatter(cellValue) || '-',
    },
    {
      field: 'totalPrice',
      title: '合计金额(元)',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
  ];
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useProductColumns(),
    data: [],
    minHeight: 200,
    autoResize: true,
    border: true,
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  },
});
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '合同编号', field: 'no', render: textRender },
    { label: '合同名称', field: 'name', render: textRender },
    { label: '客户名称', field: 'customerName', render: textRender },
    { label: '商机名称', field: 'businessName', render: textRender },
    {
      label: '合同金额(元)',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '定金金额(元)',
      field: 'depositPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '回款金额(元)',
      field: 'totalReceivablePrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '已开票金额(元)',
      field: 'totalInvoicePrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '未开票金额(元)',
      field: 'unInvoicePrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '下单时间',
      field: 'orderDate',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合同开始时间',
      field: 'startTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合同结束时间',
      field: 'endTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '客户签约人', field: 'signContactName', render: textRender },
    { label: '公司签约人', field: 'signUserName', render: textRender },
    { label: '销售订单', field: 'orderNo', render: textRender },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合同状态',
      field: 'auditStatus',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getContract(Number(props.id));
    if (formData.value?.products?.length) {
      await nextTick();
      await gridApi.grid.reloadData(formData.value.products);
    }
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- åˆåŒåŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">合同基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
        <!-- äº§å“æ˜Žç»† -->
        <div
          v-if="formData?.products?.length"
          class="rounded-lg border border-border bg-background p-4"
        >
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">产品明细</span>
          </div>
          <Grid />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/crm/invoice/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import type { CrmInvoiceApi } from '#/api/crm/invoice';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getInvoice } from '#/api/crm/invoice';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'CrmInvoiceApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<CrmInvoiceApi.Invoice>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '开票编号', field: 'no', render: textRender },
    { label: '客户名称', field: 'customerName', render: textRender },
    {
      label: '合同编号',
      field: 'contract',
      render: (_val, data) => (data?.contract?.no ? data.contract.no : '-'),
    },
    {
      label: '发票类型',
      field: 'invoiceType',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.CRM_INVOICE_TYPE, value: val }),
    },
    { label: '发票号码', field: 'invoiceNo', render: textRender },
    { label: '发票抬头', field: 'invoiceTitle', render: textRender },
    {
      label: '开票金额(元)',
      field: 'price',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '剩余可回款金额(元)',
      field: 'remainingInvoicePrice',
      render: (val) => erpPriceInputFormatter(val ?? 0) || '-',
    },
    {
      label: '开票日期',
      field: 'invoiceTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '审批状态',
      field: 'auditStatus',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
    },
    { label: '负责人', field: 'ownerUserName', render: textRender },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getInvoice(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- å¼€ç¥¨åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">开票基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/crm/receivable/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
<script lang="ts" setup>
import type { CrmReceivableApi } from '#/api/crm/receivable';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getReceivable } from '#/api/crm/receivable';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'CrmReceivableApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<CrmReceivableApi.Receivable>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** å…³è”开票展示 */
function invoiceRender(_val: unknown, data?: CrmReceivableApi.Receivable) {
  if (!data?.invoice?.no) {
    return '-';
  }
  return `${data.invoice.no} / ${data.invoice.invoiceNo ?? '-'} / Â¥${
    data.invoice.price ?? 0
  }`;
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '回款编号', field: 'no', render: textRender },
    { label: '客户名称', field: 'customerName', render: textRender },
    {
      label: '合同编号',
      field: 'contract',
      render: (_val, data) => (data?.contract?.no ? data.contract.no : '-'),
    },
    { label: '关联开票', field: 'invoice', render: invoiceRender },
    {
      label: '回款日期',
      field: 'returnTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '回款金额(元)',
      field: 'price',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '回款方式',
      field: 'returnType',
      render: (val) =>
        h(DictTag, {
          type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
          value: val,
        }),
    },
    {
      label: '审批状态',
      field: 'auditStatus',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
    },
    { label: '负责人', field: 'ownerUserName', render: textRender },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getReceivable(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- å›žæ¬¾åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">回款基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/erp/finance/payment/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,172 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment';
import type { DescriptionItemSchema } from '#/components/description';
import { h, nextTick, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getFinancePayment } from '#/api/erp/finance/payment';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'ErpFinancePaymentApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<ErpFinancePaymentApi.FinancePayment>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** ä»˜æ¬¾æ˜Žç»†ï¼ˆå®¡æ‰¹è¯¦æƒ…只读列) */
function usePaymentItemColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'seq', title: '序号', width: 60 },
    { field: 'bizNo', title: '来票编号', minWidth: 200 },
    {
      field: 'totalPrice',
      title: '应付金额',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'paidPrice',
      title: '已付金额',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'paymentPrice',
      title: '本次付款',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 150,
      formatter: ({ cellValue }) => textRender(cellValue),
    },
  ];
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: usePaymentItemColumns(),
    data: [],
    minHeight: 200,
    autoResize: true,
    border: true,
    rowConfig: {
      keyField: 'bizId',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  },
});
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '付款单号', field: 'no', render: textRender },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '关联来票',
      field: 'invoice',
      render: (_val, data) => (data?.invoice?.no ? data.invoice.no : '-'),
    },
    {
      label: '付款时间',
      field: 'paymentTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合计金额(元)',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '优惠金额(元)',
      field: 'discountPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '实际付款金额(元)',
      field: 'paymentPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    { label: '付款账户', field: 'accountName', render: textRender },
    { label: '财务人员', field: 'financeUserName', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
    },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getFinancePayment(Number(props.id));
    if (formData.value?.items?.length) {
      await nextTick();
      await gridApi.grid.reloadData(formData.value.items);
    }
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- ä»˜æ¬¾å•基本信息 -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">付款单基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
        <!-- ä»˜æ¬¾æ˜Žç»† -->
        <div
          v-if="formData?.items?.length"
          class="rounded-lg border border-border bg-background p-4"
        >
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">付款明细</span>
          </div>
          <Grid />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/erp/purchase/invoice/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
import type { DescriptionItemSchema } from '#/components/description';
import { onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getPurchaseInvoice } from '#/api/erp/purchase/invoice';
import { useDescription } from '#/components/description';
defineOptions({ name: 'ErpPurchaseInvoiceApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '来票编号', field: 'no', render: textRender },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '采购订单号',
      field: 'order',
      render: (_val, data) => (data?.order?.no ? data.order.no : '-'),
    },
    { label: '发票号码', field: 'invoiceNo', render: textRender },
    { label: '发票抬头', field: 'invoiceTitle', render: textRender },
    {
      label: '来票金额(元)',
      field: 'price',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '剩余可付金额(元)',
      field: 'remainingInvoicePrice',
      render: (val) => erpPriceInputFormatter(val ?? 0) || '-',
    },
    {
      label: '开票日期',
      field: 'invoiceTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getPurchaseInvoice(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- æ¥ç¥¨åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">来票基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/erp/purchase/order/detail/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
<script lang="ts" setup>
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import {
  erpCountInputFormatter,
  erpPriceInputFormatter,
  formatDateTime,
} from '@vben/utils';
import { Spin, Tag } from 'ant-design-vue';
import { getPurchaseOrder } from '#/api/erp/purchase/order';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
import ItemForm from '../modules/item-form.vue';
defineOptions({ name: 'ErpPurchaseOrderDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** å…¥åº“状态渲染 */
function renderInStatus(val?: number) {
  const map: Record<number, { text: string; color: string }> = {
    0: { text: '未入库', color: 'default' },
    1: { text: '部分入库', color: 'warning' },
    2: { text: '全部入库', color: 'success' },
  };
  const item = map[val ?? 0]!;
  return h(Tag, { color: item.color }, () => item.text);
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '采购订单号', field: 'no' },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '订单时间',
      field: 'orderTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '合计数量',
      field: 'totalCount',
      render: (val) => erpCountInputFormatter(val) || '-',
    },
    {
      label: '合计金额',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '优惠金额',
      field: 'discountPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '定金金额',
      field: 'depositPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
    },
    {
      label: '入库状态',
      field: 'inStatus',
      render: (val) => renderInStatus(val as number),
    },
    { label: '创建人', field: 'creatorName', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getPurchaseOrder(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
        <!-- é‡‡è´­æ˜Žç»† -->
        <div
          v-if="formData?.items?.length"
          class="rounded-lg border border-border bg-background p-4"
        >
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">采购明细</span>
          </div>
          <ItemForm :items="formData.items" :disabled="true" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/erp/purchase/request/detail/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
import type { DescriptionItemSchema } from '#/components/description';
import { h, nextTick, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import {
  erpCountInputFormatter,
  erpPriceInputFormatter,
  formatDateTime,
} from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getPurchaseRequest } from '#/api/erp/purchase/request';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'ErpPurchaseRequestDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<ErpPurchaseRequestApi.PurchaseRequest>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** é‡‡è´­ç”³è¯·æ˜Žç»†ï¼ˆå®¡æ‰¹è¯¦æƒ…只读列) */
function useDetailItemColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'seq', title: '序号', width: 60 },
    { field: 'productName', title: '产品名称', minWidth: 180 },
    { field: 'productUnitName', title: '单位', minWidth: 80 },
    {
      field: 'count',
      title: '数量',
      minWidth: 100,
      formatter: ({ cellValue }) => erpCountInputFormatter(cellValue) || '-',
    },
    {
      field: 'productPrice',
      title: '单价(参考价)',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'totalPrice',
      title: '预估金额',
      minWidth: 120,
      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
    },
    {
      field: 'taxPercent',
      title: '税率(%)',
      minWidth: 90,
      formatter: ({ cellValue }) => (cellValue ? `${cellValue}%` : '-'),
    },
    { field: 'demandTime', title: '需求日期', minWidth: 110 },
    {
      field: 'remark',
      title: '备注',
      minWidth: 160,
      showOverflow: 'tooltip',
    },
    {
      field: 'qcCheckFlag',
      title: '是否质检',
      minWidth: 80,
      formatter: ({ cellValue }) => (cellValue ? '是' : '否'),
    },
  ];
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useDetailItemColumns(),
    data: [],
    minHeight: 200,
    autoResize: true,
    border: true,
    rowConfig: {
      keyField: 'seq',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      enabled: false,
    },
  },
});
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '申请编号', field: 'no' },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
    },
    { label: '申请人', field: 'requestUserName', render: textRender },
    { label: '申请部门', field: 'requestDeptName', render: textRender },
    {
      label: '申请时间',
      field: 'requestTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '合计数量',
      field: 'totalCount',
      render: (val) => erpCountInputFormatter(val) || '-',
    },
    {
      label: '合计金额',
      field: 'totalPrice',
      render: (val) => erpPriceInputFormatter(val) || '-',
    },
    { label: '申请理由', field: 'requestReason', render: textRender },
    { label: '生成采购订单', field: 'orderNo', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getPurchaseRequest(Number(props.id));
    if (formData.value?.items?.length) {
      await nextTick();
      await gridApi.grid.reloadData(formData.value.items);
    }
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
        <!-- ç”³è¯·æ˜Žç»† -->
        <div
          v-if="formData?.items?.length"
          class="rounded-lg border border-border bg-background p-4"
        >
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">申请明细</span>
          </div>
          <Grid />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/attendance/exception/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getAttendanceException } from '#/api/hrm/attendance/exception';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'HrmAttendanceExceptionApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmAttendanceExceptionApi.AttendanceException>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'userName', render: textRender },
    { label: '所属部门', field: 'deptName', render: textRender },
    {
      label: '异常日期',
      field: 'date',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '异常类型', field: 'exceptionTypeName', render: textRender },
    { label: '异常说明', field: 'reason', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getAttendanceException(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- è€ƒå‹¤å¼‚常基本信息 -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">考勤异常基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/leave/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
<script lang="ts" setup>
import type { HrmLeaveApi } from '#/api/hrm/leave';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getLeave } from '#/api/hrm/leave';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'HrmLeaveApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmLeaveApi.Leave>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'employeeName', render: textRender },
    { label: '所属部门', field: 'deptName', render: textRender },
    {
      label: '请假类型',
      field: 'leaveType',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_LEAVE_TYPE, value: val }),
    },
    {
      label: '开始时间',
      field: 'startTime',
      render: (val) => formatDateTime(val) as string,
    },
    {
      label: '结束时间',
      field: 'endTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '请假时长', field: 'duration', render: textRender },
    { label: '请假事由', field: 'reason', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getLeave(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- è¯·å‡åŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">请假基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/resignation/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { HrmResignationApi } from '#/api/hrm/resignation';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getResignation } from '#/api/hrm/resignation';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'HrmResignationApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmResignationApi.Resignation>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'employeeName', render: textRender },
    { label: '所属部门', field: 'deptName', render: textRender },
    { label: '职位', field: 'postName', render: textRender },
    {
      label: '离职类型',
      field: 'resignationType',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_RESIGNATION_TYPE, value: val }),
    },
    {
      label: '预计离职日期',
      field: 'expectedDate',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '离职原因', field: 'reason', render: textRender },
    { label: '交接人', field: 'handoverUserName', render: textRender },
    {
      label: '交接状态',
      field: 'handoverStatus',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_HANDOVER_STATUS, value: val }),
    },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getResignation(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- ç¦»èŒåŸºæœ¬ä¿¡æ¯ -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">离职基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/hrm/transfer/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { HrmTransferApi } from '#/api/hrm/transfer';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { Spin } from 'ant-design-vue';
import { getTransfer } from '#/api/hrm/transfer';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
defineOptions({ name: 'HrmTransferApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<HrmTransferApi.Transfer>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '员工姓名', field: 'employeeName', render: textRender },
    { label: '当前部门', field: 'currentDeptName', render: textRender },
    { label: '当前职位', field: 'currentPostName', render: textRender },
    { label: '目标部门', field: 'targetDeptName', render: textRender },
    { label: '目标职位', field: 'targetPostName', render: textRender },
    {
      label: '调岗类型',
      field: 'transferType',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_TRANSFER_TYPE, value: val }),
    },
    {
      label: '生效日期',
      field: 'effectiveDate',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '调岗原因', field: 'reason', render: textRender },
    {
      label: '审批状态',
      field: 'status',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
    },
    { label: '备注', field: 'remark', render: textRender },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => formatDateTime(val) as string,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getTransfer(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="space-y-4">
        <!-- è°ƒå²—基本信息 -->
        <div class="rounded-lg border border-border bg-background p-4">
          <div class="mb-4 flex items-center gap-2">
            <span class="inline-block h-4 w-1 rounded bg-primary" />
            <span class="text-base font-semibold">调岗基本信息</span>
          </div>
          <Descriptions :data="formData" />
        </div>
      </div>
    </Spin>
  </ContentWrap>
</template>