From bdaa472dc215775e5d306b7046af0bc1111c718d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 14 八月 2026 15:58:06 +0800
Subject: [PATCH] feat(bpm): 添加业务附件配置功能
---
src/views/hrm/leave/detail/approve/index.vue | 93 +++
src/views/erp/finance/payment/detail/approve/index.vue | 172 +++++
pnpm-workspace.yaml | 2
src/views/erp/purchase/order/detail/index.vue | 134 ++++
src/views/crm/invoice/detail/approve/index.vue | 105 +++
pnpm-lock.yaml | 66 ++
src/views/crm/receivable/detail/approve/index.vue | 112 +++
src/views/hrm/attendance/exception/detail/approve/index.vue | 82 ++
src/views/hrm/resignation/detail/approve/index.vue | 95 +++
src/views/erp/purchase/invoice/detail/approve/index.vue | 90 ++
src/views/crm/contract/detail/approve/index.vue | 190 ++++++
src/views/erp/purchase/request/detail/index.vue | 181 +++++
src/views/hrm/transfer/detail/approve/index.vue | 90 ++
src/api/bpm/businessAttach/index.ts | 52 +
package.json | 4
src/views/bpm/businessAttach/modules/form.vue | 88 ++
src/views/bpm/businessAttach/index.vue | 129 ++++
src/views/bpm/businessAttach/data.ts | 163 +++++
18 files changed, 1,842 insertions(+), 6 deletions(-)
diff --git a/package.json b/package.json
index b7c7fbb..f8cce41 100644
--- a/package.json
+++ b/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"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b02d913..81f3c62 100644
--- a/pnpm-lock.yaml
+++ b/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
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5df4a75..6105520 100644
--- a/pnpm-workspace.yaml
+++ b/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
diff --git a/src/api/bpm/businessAttach/index.ts b/src/api/bpm/businessAttach/index.ts
new file mode 100644
index 0000000..4dd1aa9
--- /dev/null
+++ b/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}`,
+ );
+}
diff --git a/src/views/bpm/businessAttach/data.ts b/src/views/bpm/businessAttach/data.ts
new file mode 100644
index 0000000..f642965
--- /dev/null
+++ b/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' },
+ },
+ ];
+}
diff --git a/src/views/bpm/businessAttach/index.vue b/src/views/bpm/businessAttach/index.vue
new file mode 100644
index 0000000..10c506d
--- /dev/null
+++ b/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>
diff --git a/src/views/bpm/businessAttach/modules/form.vue b/src/views/bpm/businessAttach/modules/form.vue
new file mode 100644
index 0000000..62e57dc
--- /dev/null
+++ b/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>
diff --git a/src/views/crm/contract/detail/approve/index.vue b/src/views/crm/contract/detail/approve/index.vue
new file mode 100644
index 0000000..adc3157
--- /dev/null
+++ b/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>
diff --git a/src/views/crm/invoice/detail/approve/index.vue b/src/views/crm/invoice/detail/approve/index.vue
new file mode 100644
index 0000000..ec1530c
--- /dev/null
+++ b/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>
diff --git a/src/views/crm/receivable/detail/approve/index.vue b/src/views/crm/receivable/detail/approve/index.vue
new file mode 100644
index 0000000..cc3c795
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/finance/payment/detail/approve/index.vue b/src/views/erp/finance/payment/detail/approve/index.vue
new file mode 100644
index 0000000..c098816
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/purchase/invoice/detail/approve/index.vue b/src/views/erp/purchase/invoice/detail/approve/index.vue
new file mode 100644
index 0000000..993efbc
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/purchase/order/detail/index.vue b/src/views/erp/purchase/order/detail/index.vue
new file mode 100644
index 0000000..fadc835
--- /dev/null
+++ b/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>
diff --git a/src/views/erp/purchase/request/detail/index.vue b/src/views/erp/purchase/request/detail/index.vue
new file mode 100644
index 0000000..62c383e
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/attendance/exception/detail/approve/index.vue b/src/views/hrm/attendance/exception/detail/approve/index.vue
new file mode 100644
index 0000000..e0fef3a
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/leave/detail/approve/index.vue b/src/views/hrm/leave/detail/approve/index.vue
new file mode 100644
index 0000000..59248e4
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/resignation/detail/approve/index.vue b/src/views/hrm/resignation/detail/approve/index.vue
new file mode 100644
index 0000000..4e3a587
--- /dev/null
+++ b/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>
diff --git a/src/views/hrm/transfer/detail/approve/index.vue b/src/views/hrm/transfer/detail/approve/index.vue
new file mode 100644
index 0000000..5d983ef
--- /dev/null
+++ b/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>
--
Gitblit v1.9.3