From e99df7745f202936ad9fa59982e52327fd90e763 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期三, 16 九月 2026 14:29:48 +0800
Subject: [PATCH] refactor(approval): 审批配置页 + 采购计划/采购订单改为状态机审核
---
src/api/system/approval-config/index.ts | 62 +++
src/views/erp/purchase/order/index.vue | 119 ++++++-
/dev/null | 79 ----
src/views/system/approvalConfig/data.ts | 111 ++++++
src/views/erp/purchase/plan/index.vue | 97 ++++-
src/views/erp/purchase/order/detail/index.vue | 12
src/views/system/approvalConfig/modules/config-form.vue | 79 ++++
src/api/erp/purchase/plan/index.ts | 43 ++
src/views/erp/purchase/order/modules/audit-modal.vue | 99 ++++++
src/views/system/approvalConfig/index.vue | 93 +++++
src/api/erp/purchase/order/index.ts | 42 ++
src/views/erp/purchase/plan/modules/audit-modal.vue | 99 ++++++
12 files changed, 797 insertions(+), 138 deletions(-)
diff --git a/src/api/erp/purchase/order/index.ts b/src/api/erp/purchase/order/index.ts
index 758b8dc..c085891 100644
--- a/src/api/erp/purchase/order/index.ts
+++ b/src/api/erp/purchase/order/index.ts
@@ -28,6 +28,10 @@
productNames?: string; // 浜у搧鍚嶇О鍒楄〃
creatorName?: string; // 鍒涘缓浜哄悕绉�
createTime?: Date; // 鍒涘缓鏃堕棿
+ reviewerId?: number; // 瀹℃牳浜虹紪鍙�
+ reviewerName?: string; // 瀹℃牳浜哄鍚�
+ reviewTime?: Date | string; // 瀹℃牳鏃堕棿
+ reviewRemark?: string; // 瀹℃牳鎰忚锛堜笉閫氳繃鏃朵负椹冲洖鍘熷洜锛�
items?: PurchaseOrderItem[]; // 璁㈠崟椤瑰垪琛�
}
@@ -98,17 +102,37 @@
return requestClient.download('/purchase/order/export-excel', { params });
}
-/** 鑾峰彇閲囪喘瀹℃牳娴佺▼鍒楄〃 */
-export function getPurchaseApproveProcessList() {
- return requestClient.get<any[]>('/purchase/order/approve-process-list');
+/**
+ * 鎻愪氦閲囪喘璁㈠崟瀹℃壒锛堟湭鎻愪氦/瀹℃牳涓嶉�氳繃 鈫� 瀹℃壒涓級
+ *
+ * 鏀逛负杞婚噺绾х姸鎬佹満瀹℃壒鍚庯紝鍚庣鎸夊鎵归厤缃牎楠屽鎵逛汉锛屽墠绔棤闇�鍐嶉�夋嫨 BPM 娴佺▼銆�
+ */
+export function submitPurchaseOrder(id: number) {
+ return requestClient.put('/purchase/order/submit', null, {
+ params: { id },
+ });
}
-/** 鑾峰彇閲囪喘瀹℃牳娴佺▼鍒楄〃鍝嶅簲 */
-export interface ProcessDefinition {
- id: string; // 娴佺▼瀹氫箟ID
- key: string; // 娴佺▼Key
- name: string; // 娴佺▼鍚嶇О
- version: number; // 鐗堟湰鍙�
+/**
+ * 瀹℃牳閲囪喘璁㈠崟锛堝鎵逛腑 鈫� 瀹℃牳閫氳繃/瀹℃牳涓嶉�氳繃锛�
+ *
+ * 浠呭鎵归厤缃腑鎸囧畾鐨勫鎵逛汉鏈汉鍙皟鐢紱pass=false 鏃� reviewRemark锛堜笉閫氳繃鍘熷洜锛夊繀濉��
+ */
+export function auditPurchaseOrder(
+ id: number,
+ pass: boolean,
+ reviewRemark?: string,
+) {
+ return requestClient.put('/purchase/order/audit', {
+ id,
+ pass,
+ reviewRemark,
+ });
+}
+
+/** 鑾峰彇閲囪喘璁㈠崟鐨勫鎵逛汉缂栧彿闆嗗悎锛屼緵鍓嶇鍒ゆ柇鏄惁灞曠ず瀹℃牳鎸夐挳 */
+export function getPurchaseOrderApproverUserIds() {
+ return requestClient.get<number[]>('/purchase/order/approver-ids');
}
/** 纭鏀惰揣 */
diff --git a/src/api/erp/purchase/plan/index.ts b/src/api/erp/purchase/plan/index.ts
index 1e6f166..7f0e68c 100644
--- a/src/api/erp/purchase/plan/index.ts
+++ b/src/api/erp/purchase/plan/index.ts
@@ -9,7 +9,11 @@
no?: string; // 璁″垝鍗曞彿
name?: string; // 璁″垝鍚嶇О
status?: number; // 鐘舵�侊細0鏈彁浜� 10瀹℃壒涓� 20瀹℃牳閫氳繃 30瀹℃牳涓嶉�氳繃 40宸插彇娑�
- processInstanceId?: string; // 宸ヤ綔娴佺紪鍙�
+ processInstanceId?: string; // 宸ヤ綔娴佺紪鍙凤紙鍘嗗彶瀛楁锛屾敼閫犲悗涓嶅啀鍐欏叆锛�
+ reviewerId?: number; // 瀹℃牳浜虹紪鍙�
+ reviewerName?: string; // 瀹℃牳浜哄鍚�
+ reviewTime?: string; // 瀹℃牳鏃堕棿
+ reviewRemark?: string; // 瀹℃牳鎰忚锛堥�氳繃鏃剁殑鎰忚 / 涓嶉�氳繃鏃剁殑鍘熷洜锛�
demandSource?: number; // 闇�姹傛潵婧愶細1搴撳瓨棰勮 2杩愮淮/鐢熶骇璁″垝
demandSourceName?: string; // 闇�姹傛潵婧愬悕绉�
planDate?: string; // 璁″垝鏃ユ湡
@@ -50,11 +54,37 @@
return requestClient.put('/purchase/plan/update', data);
}
-/** 鎻愪氦閲囪喘璁″垝瀹℃壒 */
-export function submitPurchasePlan(id: number, processDefinitionKey: string) {
+/**
+ * 鎻愪氦閲囪喘璁″垝瀹℃牳锛堣崏绋�/瀹℃牳涓嶉�氳繃 鈫� 瀹℃壒涓級
+ *
+ * 鎻愪氦鍓嶇敱鍚庣鏍¢獙瀹℃壒閰嶇疆宸插惎鐢ㄤ笖閰嶇疆浜嗘湁鏁堝鎵逛汉锛屾棤闇�鍓嶇浼犳祦绋嬪畾涔夈��
+ */
+export function submitPurchasePlan(id: number) {
return requestClient.put('/purchase/plan/submit', null, {
- params: { id, processDefinitionKey },
+ params: { id },
});
+}
+
+/**
+ * 瀹℃牳閲囪喘璁″垝锛堝鎵逛腑 鈫� 瀹℃牳閫氳繃/瀹℃牳涓嶉�氳繃锛�
+ *
+ * 浠呭鎵归厤缃腑鎸囧畾鐨勫鎵逛汉鏈汉鍙皟鐢紱pass=false 鏃� reviewRemark锛堜笉閫氳繃鍘熷洜锛夊繀濉��
+ */
+export function auditPurchasePlan(
+ id: number,
+ pass: boolean,
+ reviewRemark?: string,
+) {
+ return requestClient.put('/purchase/plan/audit', {
+ id,
+ pass,
+ reviewRemark,
+ });
+}
+
+/** 鑾峰彇閲囪喘璁″垝鐨勫鎵逛汉缂栧彿闆嗗悎锛岀敤浜庡垽鏂槸鍚﹀睍绀哄鏍告寜閽� */
+export function getPurchasePlanApproverUserIds() {
+ return requestClient.get<number[]>('/purchase/plan/approver-ids');
}
/** 鏍规嵁搴撳瓨棰勮鐢熸垚閲囪喘璁″垝 */
@@ -101,9 +131,4 @@
/** 瀵煎嚭閲囪喘璁″垝 Excel */
export function exportPurchasePlan(params: any) {
return requestClient.download('/purchase/plan/export-excel', { params });
-}
-
-/** 鑾峰彇瀹℃壒娴佺▼鍒楄〃 */
-export function getApproveProcessList(id: number) {
- return requestClient.get(`/purchase/plan/approve-process-list`, { params: { id } });
}
diff --git a/src/api/system/approval-config/index.ts b/src/api/system/approval-config/index.ts
new file mode 100644
index 0000000..adb5b4d
--- /dev/null
+++ b/src/api/system/approval-config/index.ts
@@ -0,0 +1,62 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace SystemApprovalConfigApi {
+ /** 瀹℃壒閰嶇疆 */
+ export interface ApprovalConfig {
+ id?: number; // 涓婚敭ID
+ bizType?: string; // 涓氬姟绫诲瀷缂栫爜
+ bizName?: string; // 涓氬姟绫诲瀷鍚嶇О
+ approvalEnabled?: boolean; // 鏄惁鍚敤瀹℃壒
+ userIds?: number[]; // 瀹℃壒浜虹敤鎴稩D鍒楄〃
+ userNames?: string[]; // 瀹℃壒浜哄鍚嶅垪琛�
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+}
+
+/** 鏌ヨ瀹℃壒閰嶇疆鍒嗛〉 */
+export function getApprovalConfigPage(params: PageParam) {
+ return requestClient.get<PageResult<SystemApprovalConfigApi.ApprovalConfig>>(
+ '/system/approval-config/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ瀹℃壒閰嶇疆璇︽儏 */
+export function getApprovalConfig(id: number) {
+ return requestClient.get<SystemApprovalConfigApi.ApprovalConfig>(
+ `/system/approval-config/get?id=${id}`,
+ );
+}
+
+/**
+ * 淇濆瓨瀹℃壒閰嶇疆锛堟柊澧炴垨淇敼锛�
+ *
+ * 瀹℃壒浜轰负鏁翠綋瑕嗙洊锛氫紶鍏ョ殑 userIds 鍗充负鏈�缁堢粨鏋滐紝鏈紶鍏ョ殑浼氳绉婚櫎銆�
+ */
+export function saveApprovalConfig(
+ data: SystemApprovalConfigApi.ApprovalConfig,
+) {
+ return requestClient.post('/system/approval-config/save', data);
+}
+
+/** 鍒犻櫎瀹℃壒閰嶇疆 */
+export function deleteApprovalConfig(id: number) {
+ return requestClient.delete(`/system/approval-config/delete?id=${id}`);
+}
+
+/** 鏌ヨ鎸囧畾涓氬姟绫诲瀷鐨勫鎵逛汉缂栧彿闆嗗悎 */
+export function getApproverUserIds(bizType: string) {
+ return requestClient.get<number[]>('/system/approval-config/approver-ids', {
+ params: { bizType },
+ });
+}
+
+/** 鍒ゆ柇褰撳墠鐧诲綍鐢ㄦ埛鏄惁涓烘寚瀹氫笟鍔$被鍨嬬殑瀹℃壒浜� */
+export function isApprover(bizType: string) {
+ return requestClient.get<boolean>('/system/approval-config/is-approver', {
+ params: { bizType },
+ });
+}
diff --git a/src/views/erp/purchase/order/detail/index.vue b/src/views/erp/purchase/order/detail/index.vue
index fadc835..80dacef 100644
--- a/src/views/erp/purchase/order/detail/index.vue
+++ b/src/views/erp/purchase/order/detail/index.vue
@@ -85,6 +85,18 @@
field: 'inStatus',
render: (val) => renderInStatus(val as number),
},
+ { label: '瀹℃牳浜�', field: 'reviewerName', render: textRender },
+ {
+ label: '瀹℃牳鏃堕棿',
+ field: 'reviewTime',
+ render: (val) => (val ? (formatDateTime(val) as string) : '-'),
+ },
+ {
+ label: '瀹℃牳鎰忚',
+ field: 'reviewRemark',
+ render: (val, data) =>
+ data?.status === 30 && val ? `涓嶉�氳繃鍘熷洜锛�${val}` : (val || '-'),
+ },
{ label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
{
label: '鍒涘缓鏃堕棿',
diff --git a/src/views/erp/purchase/order/index.vue b/src/views/erp/purchase/order/index.vue
index c56c84d..e260992 100644
--- a/src/views/erp/purchase/order/index.vue
+++ b/src/views/erp/purchase/order/index.vue
@@ -7,6 +7,7 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+import { useUserStore } from '@vben/stores';
import { message, Tag } from 'ant-design-vue';
@@ -15,21 +16,34 @@
confirmReceipt,
deletePurchaseOrder,
exportPurchaseOrder,
+ getPurchaseOrderApproverUserIds,
getPurchaseOrderPage,
- updatePurchaseOrderStatus,
+ submitPurchaseOrder,
} from '#/api/erp/purchase/order';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
+import AuditModal from './modules/audit-modal.vue';
import Form from './modules/form.vue';
/** ERP 閲囪喘璁㈠崟鍒楄〃 */
defineOptions({ name: 'ErpPurchaseOrder' });
const route = useRoute();
+const userStore = useUserStore();
+
+/** 褰撳墠涓氬姟绫诲瀷鐨勫鎵逛汉缂栧彿闆嗗悎锛岀敤浜庡垽鏂槸鍚﹀睍绀哄鏍告寜閽� */
+const approverIds = ref<number[]>([]);
+/** 褰撳墠鐧诲綍鐢ㄦ埛缂栧彿 */
+const currentUserId = userStore.userInfo?.id;
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [AuditModalComp, auditModalApi] = useVbenModal({
+ connectedComponent: AuditModal,
destroyOnClose: true,
});
@@ -69,21 +83,60 @@
}
}
-/** 瀹℃壒/鍙嶅鎵规搷浣� */
-async function handleUpdateStatus(
- row: ErpPurchaseOrderApi.PurchaseOrder,
- status: number,
-) {
+/** 鎻愪氦瀹℃牳 - 鍚庣鎸夊鎵归厤缃牎楠屽鎵逛汉锛屾棤闇�鍓嶇閫夋嫨娴佺▼ */
+async function handleSubmit(row: ErpPurchaseOrderApi.PurchaseOrder) {
const hideLoading = message.loading({
- content: `纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヨ鍗曞悧锛焋,
+ content: '姝e湪鎻愪氦瀹℃牳...',
duration: 0,
});
try {
- await updatePurchaseOrderStatus(row.id!, status);
- message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`);
+ await submitPurchaseOrder(row.id!);
+ message.success('鎻愪氦鎴愬姛锛岀瓑寰呭鎵逛汉瀹℃牳');
handleRefresh();
} finally {
hideLoading();
+ }
+}
+
+/** 瀹℃牳 - 閫氳繃 / 涓嶉�氳繃 */
+function handleAudit(row: ErpPurchaseOrderApi.PurchaseOrder, pass: boolean) {
+ auditModalApi.setData({ pass, row }).open();
+}
+
+/** 褰撳墠鐢ㄦ埛鏄惁涓鸿涓氬姟绫诲瀷鐨勫鎵逛汉 */
+function canAudit(row: ErpPurchaseOrderApi.PurchaseOrder) {
+ return (
+ row.status === 10 && approverIds.value.includes(currentUserId as number)
+ );
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 - 鏀归�犱负杞婚噺绾у鎵瑰悗灞曠ず瀹℃牳璁板綍锛屼笉鍐嶈烦杞� BPM 娴佺▼璇︽儏 */
+function handleViewProcess(row: ErpPurchaseOrderApi.PurchaseOrder) {
+ const parts: string[] = [];
+ if (row.reviewerName) {
+ parts.push(`瀹℃牳浜猴細${row.reviewerName}`);
+ }
+ if (row.reviewTime) {
+ parts.push(`瀹℃牳鏃堕棿锛�${row.reviewTime}`);
+ }
+ if (row.reviewRemark) {
+ parts.push(
+ `${row.status === 30 ? '涓嶉�氳繃鍘熷洜' : '瀹℃牳鎰忚'}锛�${row.reviewRemark}`,
+ );
+ }
+ if (parts.length === 0) {
+ message.info('璇ヨ鍗曟殏鏃犲鏍歌褰�');
+ return;
+ }
+ message.info(parts.join('銆�'));
+}
+
+/** 鍔犺浇褰撳墠涓氬姟绫诲瀷鐨勫鎵逛汉缂栧彿闆嗗悎 */
+async function loadApproverIds() {
+ try {
+ approverIds.value = (await getPurchaseOrderApproverUserIds()) ?? [];
+ } catch {
+ approverIds.value = [];
}
}
@@ -151,6 +204,7 @@
});
onMounted(() => {
+ loadApproverIds();
const id = route.query.id;
if (id) {
formModalApi.setData({ formType: 'detail', id: Number(id) }).open();
@@ -159,7 +213,9 @@
</script>
<template>
- <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <AuditModalComp @success="handleRefresh" />
<Grid table-title="閲囪喘璁㈠崟鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -213,22 +269,43 @@
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['erp:purchase-order:update'],
- ifShow: () => row.status !== 20,
+ // 瀹℃壒涓笉鍙慨鏀癸細闇�鍏堛�屽鏍镐笉閫氳繃銆嶉��鍥烇紱宸插鏍镐笉鍙慨鏀�
+ ifShow: () => row.status === 0 || row.status === 30,
onClick: handleEdit.bind(null, row),
},
{
- label: row.status === 10 ? '瀹℃壒' : '鍙嶅鎵�',
+ label: '鎻愪氦',
+ type: 'link',
+ auth: ['erp:purchase-order:update-status'],
+ // 鏈彁浜や笌瀹℃牳涓嶉�氳繃鍧囧彲鎻愪氦锛屽鏍镐笉閫氳繃闇�淇敼鍚庨噸鏂版彁浜�
+ ifShow: () => row.status === 0 || row.status === 30,
+ onClick: handleSubmit.bind(null, row),
+ },
+ {
+ label: '瀹℃牳',
type: 'link',
icon: ACTION_ICON.AUDIT,
- auth: ['erp:purchase-order:update-status'],
- popConfirm: {
- title: `纭${row.status === 10 ? '瀹℃壒' : '鍙嶅鎵�'}${row.no}鍚楋紵`,
- confirm: handleUpdateStatus.bind(
- null,
- row,
- row.status === 10 ? 20 : 10,
- ),
- },
+ auth: ['erp:purchase-order:audit'],
+ // 浠呫�屽鎵逛腑 + 褰撳墠鐧诲綍鐢ㄦ埛鏄鎵逛汉銆嶆墠鏄剧ず锛屽叾浠栦汉鐪嬩笉鍒拌鎸夐挳
+ ifShow: canAudit(row),
+ onClick: handleAudit.bind(null, row, true),
+ },
+ {
+ label: '瀹℃牳涓嶉�氳繃',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.AUDIT,
+ auth: ['erp:purchase-order:audit'],
+ ifShow: canAudit(row),
+ onClick: handleAudit.bind(null, row, false),
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['erp:purchase-order:query'],
+ ifShow: () => row.status === 20 || row.status === 30,
+ onClick: handleViewProcess.bind(null, row),
},
{
label: '纭鏀惰揣',
diff --git a/src/views/erp/purchase/order/modules/audit-modal.vue b/src/views/erp/purchase/order/modules/audit-modal.vue
new file mode 100644
index 0000000..4227b61
--- /dev/null
+++ b/src/views/erp/purchase/order/modules/audit-modal.vue
@@ -0,0 +1,99 @@
+<script lang="ts" setup>
+ import type { VbenFormSchema } from '#/adapter/form';
+ import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
+
+ import { computed, ref } from 'vue';
+
+ import { useVbenModal } from '@vben/common-ui';
+
+ import { message } from 'ant-design-vue';
+
+ import { useVbenForm } from '#/adapter/form';
+ import { auditPurchaseOrder } from '#/api/erp/purchase/order';
+
+ const emit = defineEmits(['success']);
+
+ const pass = ref(true); // true=瀹℃牳閫氳繃 / false=瀹℃牳涓嶉�氳繃
+ const rowData = ref<ErpPurchaseOrderApi.PurchaseOrder>();
+ const getTitle = computed(() => (pass.value ? '瀹℃牳閫氳繃' : '瀹℃牳涓嶉�氳繃'));
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 130,
+ },
+ wrapperClass: 'grid-cols-1',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ });
+
+ /** 瀹℃牳琛ㄥ崟瀛楁锛氫笉閫氳繃鏃跺繀椤诲~鍐欏師鍥� */
+ function useAuditSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'reviewRemark',
+ label: '瀹℃牳鎰忚',
+ component: 'Textarea',
+ formItemClass: 'col-span-1',
+ componentProps: {
+ rows: 3,
+ placeholder: pass.value
+ ? '璇疯緭鍏ュ鏍告剰瑙侊紙鍙�夛級'
+ : '璇疯緭鍏ヤ笉閫氳繃鍘熷洜锛堝繀濉級',
+ },
+ rules: pass.value ? '' : 'required',
+ },
+ ];
+ }
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const values = await formApi.getValues();
+ try {
+ await auditPurchaseOrder(
+ rowData.value!.id!,
+ pass.value,
+ values.reviewRemark,
+ );
+ await modalApi.close();
+ emit('success');
+ message.success(pass.value ? '瀹℃牳閫氳繃' : '宸查��鍥烇紝鍙慨鏀瑰悗閲嶆柊鎻愪氦');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ rowData.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{
+ pass: boolean;
+ row: ErpPurchaseOrderApi.PurchaseOrder;
+ }>();
+ rowData.value = data.row;
+ pass.value = data.pass;
+ formApi.setState({ schema: useAuditSchema() });
+ formApi.resetForm();
+ },
+ });
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/5">
+ <div v-if="rowData" class="mx-4 mb-4 text-sm text-gray-600">
+ <div>璁㈠崟鍗曞彿锛歿{ rowData.no }}</div>
+ <div>渚涘簲鍟嗭細{{ rowData.supplierName || '-' }}</div>
+ </div>
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/erp/purchase/plan/index.vue b/src/views/erp/purchase/plan/index.vue
index 671a09f..916cb2a 100644
--- a/src/views/erp/purchase/plan/index.vue
+++ b/src/views/erp/purchase/plan/index.vue
@@ -2,11 +2,12 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchasePlanApi } from '#/api/erp/purchase/plan';
-import { ref } from 'vue';
+import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { confirm, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+import { useUserStore } from '@vben/stores';
import { message, Tag } from 'ant-design-vue';
@@ -16,27 +17,34 @@
exportPurchasePlan,
generatePlanFromStockAlert,
generatePurchaseRequests,
- getApproveProcessList,
+ getPurchasePlanApproverUserIds,
getPurchasePlanPage,
+ submitPurchasePlan,
} from '#/api/erp/purchase/plan';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
+import AuditModal from './modules/audit-modal.vue';
import Form from './modules/form.vue';
-import ProcessSelectModal from './modules/process-select-modal.vue';
/** ERP 閲囪喘璁″垝鍒楄〃 */
defineOptions({ name: 'ErpPurchasePlan' });
const router = useRouter();
+const userStore = useUserStore();
+
+/** 褰撳墠涓氬姟绫诲瀷鐨勫鎵逛汉缂栧彿闆嗗悎锛岀敤浜庡垽鏂槸鍚﹀睍绀哄鏍告寜閽� */
+const approverIds = ref<number[]>([]);
+/** 褰撳墠鐧诲綍鐢ㄦ埛缂栧彿 */
+const currentUserId = userStore.userInfo?.id;
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
-const [ProcessModal, processModalApi] = useVbenModal({
- connectedComponent: ProcessSelectModal,
+const [AuditModalComp, auditModalApi] = useVbenModal({
+ connectedComponent: AuditModal,
destroyOnClose: true,
});
@@ -92,23 +100,29 @@
}
}
-/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+/** 鎻愪氦瀹℃牳 - 鍚庣鎸夊鎵归厤缃牎楠屽鎵逛汉锛屾棤闇�鍓嶇閫夋嫨娴佺▼ */
async function handleSubmit(row: ErpPurchasePlanApi.PurchasePlan) {
const hideLoading = message.loading({
- content: '鑾峰彇瀹℃壒娴佺▼...',
+ content: '姝e湪鎻愪氦瀹℃牳...',
duration: 0,
});
try {
- const processList = await getApproveProcessList(row.id!);
- hideLoading();
- if (!processList || processList.length === 0) {
- message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
- return;
- }
- processModalApi.setData({ id: row.id, processList }).open();
- } catch {
+ await submitPurchasePlan(row.id!);
+ message.success('鎻愪氦鎴愬姛锛岀瓑寰呭鎵逛汉瀹℃牳');
+ handleRefresh();
+ } finally {
hideLoading();
}
+}
+
+/** 瀹℃牳 - 閫氳繃 / 涓嶉�氳繃 */
+function handleAudit(row: ErpPurchasePlanApi.PurchasePlan, pass: boolean) {
+ auditModalApi.setData({ pass, row }).open();
+}
+
+/** 褰撳墠鐢ㄦ埛鏄惁涓鸿涓氬姟绫诲瀷鐨勫鎵逛汉 */
+function canAudit(row: ErpPurchasePlanApi.PurchasePlan) {
+ return row.status === 10 && approverIds.value.includes(currentUserId as number);
}
/** 鐢熸垚閲囪喘鐢宠 */
@@ -145,14 +159,37 @@
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
-/** 鏌ョ湅瀹℃壒璇︽儏 */
+/** 鏌ョ湅瀹℃壒璇︽儏 - 鏀归�犱负杞婚噺绾у鎵瑰悗灞曠ず瀹℃牳璁板綍锛屼笉鍐嶈烦杞� BPM 娴佺▼璇︽儏 */
function handleViewProcess(row: ErpPurchasePlanApi.PurchasePlan) {
- if (!row.processInstanceId) {
- message.warning('璇ヨ鍒掑皻鏈彁浜ゅ鎵�');
+ const parts: string[] = [];
+ if (row.reviewerName) {
+ parts.push(`瀹℃牳浜猴細${row.reviewerName}`);
+ }
+ if (row.reviewTime) {
+ parts.push(`瀹℃牳鏃堕棿锛�${row.reviewTime}`);
+ }
+ if (row.reviewRemark) {
+ parts.push(`${row.status === 30 ? '涓嶉�氳繃鍘熷洜' : '瀹℃牳鎰忚'}锛�${row.reviewRemark}`);
+ }
+ if (parts.length === 0) {
+ message.info('璇ヨ鍒掓殏鏃犲鏍歌褰�');
return;
}
- router.push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } });
+ message.info(parts.join('銆�'));
}
+
+/** 鍔犺浇褰撳墠涓氬姟绫诲瀷鐨勫鎵逛汉缂栧彿闆嗗悎 */
+async function loadApproverIds() {
+ try {
+ approverIds.value = (await getPurchasePlanApproverUserIds()) ?? [];
+ } catch {
+ approverIds.value = [];
+ }
+}
+
+onMounted(() => {
+ loadApproverIds();
+});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
@@ -192,7 +229,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
- <ProcessModal @success="handleRefresh" />
+ <AuditModalComp @success="handleRefresh" />
<Grid table-title="閲囪喘璁″垝鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -264,10 +301,30 @@
{
label: '鎻愪氦',
type: 'link',
+ auth: ['erp:purchase-plan:submit'],
+ // 鑽夌涓庡鏍镐笉閫氳繃鍧囧彲鎻愪氦锛屽鏍镐笉閫氳繃闇�淇敼鍚庨噸鏂版彁浜�
ifShow: () => row.status === 0 || row.status === 30,
onClick: handleSubmit.bind(null, row),
},
{
+ label: '瀹℃牳',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['erp:purchase-plan:audit'],
+ // 浠呫�屽鎵逛腑 + 褰撳墠鐧诲綍鐢ㄦ埛鏄鎵逛汉銆嶆墠鏄剧ず锛屽叾浠栦汉鐪嬩笉鍒拌鎸夐挳
+ ifShow: canAudit(row),
+ onClick: handleAudit.bind(null, row, true),
+ },
+ {
+ label: '瀹℃牳涓嶉�氳繃',
+ type: 'link',
+ danger: true,
+ auth: ['erp:purchase-plan:audit'],
+ // 鍚屼笂锛屼笌銆屽鏍搞�嶆垚瀵瑰嚭鐜帮紝浠呭鎵逛汉鍙
+ ifShow: canAudit(row),
+ onClick: handleAudit.bind(null, row, false),
+ },
+ {
label: '鐢熸垚鐢宠',
type: 'link',
auth: ['erp:purchase-plan:generate-request'],
diff --git a/src/views/erp/purchase/plan/modules/audit-modal.vue b/src/views/erp/purchase/plan/modules/audit-modal.vue
new file mode 100644
index 0000000..57d7108
--- /dev/null
+++ b/src/views/erp/purchase/plan/modules/audit-modal.vue
@@ -0,0 +1,99 @@
+<script lang="ts" setup>
+ import type { VbenFormSchema } from '#/adapter/form';
+ import type { ErpPurchasePlanApi } from '#/api/erp/purchase/plan';
+
+ import { computed, ref } from 'vue';
+
+ import { useVbenModal } from '@vben/common-ui';
+
+ import { message } from 'ant-design-vue';
+
+ import { useVbenForm } from '#/adapter/form';
+ import { auditPurchasePlan } from '#/api/erp/purchase/plan';
+
+ const emit = defineEmits(['success']);
+
+ const pass = ref(true); // true=瀹℃牳閫氳繃 / false=瀹℃牳涓嶉�氳繃
+ const rowData = ref<ErpPurchasePlanApi.PurchasePlan>();
+ const getTitle = computed(() => (pass.value ? '瀹℃牳閫氳繃' : '瀹℃牳涓嶉�氳繃'));
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 130,
+ },
+ wrapperClass: 'grid-cols-1',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ });
+
+ /** 瀹℃牳琛ㄥ崟瀛楁锛氫笉閫氳繃鏃跺繀椤诲~鍐欏師鍥� */
+ function useAuditSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'reviewRemark',
+ label: '瀹℃牳鎰忚',
+ component: 'Textarea',
+ formItemClass: 'col-span-1',
+ componentProps: {
+ rows: 3,
+ placeholder: pass.value
+ ? '璇疯緭鍏ュ鏍告剰瑙侊紙鍙�夛級'
+ : '璇疯緭鍏ヤ笉閫氳繃鍘熷洜锛堝繀濉級',
+ },
+ rules: pass.value ? '' : 'required',
+ },
+ ];
+ }
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const values = await formApi.getValues();
+ try {
+ await auditPurchasePlan(
+ rowData.value!.id!,
+ pass.value,
+ values.reviewRemark,
+ );
+ await modalApi.close();
+ emit('success');
+ message.success(pass.value ? '瀹℃牳閫氳繃' : '宸查��鍥烇紝鍙慨鏀瑰悗閲嶆柊鎻愪氦');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ rowData.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{
+ pass: boolean;
+ row: ErpPurchasePlanApi.PurchasePlan;
+ }>();
+ rowData.value = data.row;
+ pass.value = data.pass;
+ formApi.setState({ schema: useAuditSchema() });
+ formApi.resetForm();
+ },
+ });
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/5">
+ <div v-if="rowData" class="mx-4 mb-4 text-sm text-gray-600">
+ <div>璁″垝鍗曞彿锛歿{ rowData.no }}</div>
+ <div>璁″垝鍚嶇О锛歿{ rowData.name }}</div>
+ </div>
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/erp/purchase/plan/modules/process-select-modal.vue b/src/views/erp/purchase/plan/modules/process-select-modal.vue
deleted file mode 100644
index dd7a2a8..0000000
--- a/src/views/erp/purchase/plan/modules/process-select-modal.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<script lang="ts" setup>
-import { computed, ref } from 'vue';
-
-import { useVbenModal } from '@vben/common-ui';
-
-import { message, Select } from 'ant-design-vue';
-
-import { submitPurchasePlan } from '#/api/erp/purchase/plan';
-
-interface ProcessDefinition {
- id: string;
- name: string;
- version: number;
- key: string;
-}
-
-const emit = defineEmits(['success']);
-
-const planId = ref<number>();
-const processList = ref<ProcessDefinition[]>([]);
-const selectedProcessKey = ref<string>('');
-
-const processOptions = computed(() => {
- return processList.value.map((p) => ({
- label: `${p.name} (V${p.version})`,
- value: p.key,
- }));
-});
-
-const [Modal, modalApi] = useVbenModal({
- async onConfirm() {
- if (!selectedProcessKey.value) {
- message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
- return;
- }
- modalApi.lock();
- try {
- await submitPurchasePlan(planId.value!, selectedProcessKey.value);
- await modalApi.close();
- emit('success');
- message.success('鎻愪氦鎴愬姛');
- } finally {
- modalApi.unlock();
- }
- },
- async onOpenChange(isOpen: boolean) {
- if (!isOpen) {
- planId.value = undefined;
- selectedProcessKey.value = '';
- processList.value = [];
- return;
- }
- const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
- if (!data) {
- return;
- }
- planId.value = data.id;
- processList.value = data.processList || [];
- // 榛樿閫夋嫨绗竴涓祦绋�
- if (processList.value.length > 0) {
- selectedProcessKey.value = processList.value[0]?.key ?? '';
- }
- },
-});
-</script>
-
-<template>
- <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
- <div class="mb-4">
- <span class="mr-2">瀹℃壒娴佺▼锛�</span>
- <Select
- v-model:value="selectedProcessKey"
- :options="processOptions"
- style="width: 200px"
- placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
- />
- </div>
- </Modal>
-</template>
diff --git a/src/views/system/approvalConfig/data.ts b/src/views/system/approvalConfig/data.ts
new file mode 100644
index 0000000..1d1b06d
--- /dev/null
+++ b/src/views/system/approvalConfig/data.ts
@@ -0,0 +1,111 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'bizName',
+ label: '涓氬姟绫诲瀷',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヤ笟鍔$被鍨嬪悕绉�',
+ },
+ },
+ {
+ fieldName: 'approvalEnabled',
+ label: '鏄惁鍚敤',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '宸插惎鐢�', value: true },
+ { label: '鏈惎鐢�', value: false },
+ ],
+ placeholder: '璇烽�夋嫨鏄惁鍚敤',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { field: 'bizName', title: '涓氬姟绫诲瀷', minWidth: 160 },
+ { field: 'bizType', title: '涓氬姟缂栫爜', minWidth: 200 },
+ {
+ field: 'approvalEnabled',
+ title: '鏄惁鍚敤瀹℃壒',
+ width: 120,
+ align: 'center',
+ slots: { default: 'approvalEnabled' },
+ },
+ {
+ field: 'userNames',
+ title: '瀹℃壒浜�',
+ minWidth: 200,
+ slots: { default: 'userNames' },
+ },
+ { field: 'remark', title: '澶囨敞', minWidth: 200 },
+ { title: '鎿嶄綔', width: 140, fixed: 'right', slots: { default: 'actions' } },
+ ];
+}
+
+/** 閰嶇疆琛ㄥ崟锛氫笟鍔$被鍨� + 鍚敤寮�鍏� + 瀹℃壒浜� */
+export function useConfigFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'bizName',
+ label: '涓氬姟绫诲瀷',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'bizType',
+ label: '涓氬姟缂栫爜',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'approvalEnabled',
+ label: '鍚敤瀹℃壒',
+ component: 'Switch',
+ defaultValue: false,
+ componentProps: {
+ checkedChildren: '鍚敤',
+ unCheckedChildren: '鍋滅敤',
+ },
+ help: '鍋滅敤鍚庤涓氬姟绫诲瀷灏嗘棤娉曟彁浜ゅ鏍�',
+ },
+ {
+ fieldName: 'userIds',
+ label: '瀹℃壒浜�',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ mode: 'multiple',
+ allowClear: true,
+ placeholder: '璇烽�夋嫨瀹℃壒浜�',
+ },
+ help: '鍙厤缃涓鎵逛汉锛屽涓鎵逛汉涔嬮棿涓恒�屾垨绛俱�嶏紝鍗充换鎰忎竴浜哄鏍搁�氳繃鍗充负閫氳繃',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ },
+ },
+ ];
+}
diff --git a/src/views/system/approvalConfig/index.vue b/src/views/system/approvalConfig/index.vue
new file mode 100644
index 0000000..c2a3e96
--- /dev/null
+++ b/src/views/system/approvalConfig/index.vue
@@ -0,0 +1,93 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+ import type { SystemApprovalConfigApi } from '#/api/system/approval-config';
+
+ import { Page, useVbenModal } from '@vben/common-ui';
+
+ import { Tag } from 'ant-design-vue';
+
+ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+ import { getApprovalConfigPage } from '#/api/system/approval-config';
+
+ import { useGridColumns, useGridFormSchema } from './data';
+ import ConfigForm from './modules/config-form.vue';
+
+ defineOptions({ name: 'SystemApprovalConfig' });
+
+ const [ConfigFormModal, configFormModalApi] = useVbenModal({
+ connectedComponent: ConfigForm,
+ destroyOnClose: true,
+ });
+
+ /** 鍒锋柊琛ㄦ牸 */
+ function handleRefresh() {
+ gridApi.query();
+ }
+
+ /** 閰嶇疆瀹℃壒浜� */
+ function handleConfig(row: SystemApprovalConfigApi.ApprovalConfig) {
+ configFormModalApi.setData(row).open();
+ }
+
+ const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getApprovalConfigPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<SystemApprovalConfigApi.ApprovalConfig>,
+ });
+</script>
+
+<template>
+ <Page auto-content-height>
+ <ConfigFormModal @success="handleRefresh" />
+
+ <Grid table-title="瀹℃壒閰嶇疆">
+ <template #approvalEnabled="{ row }">
+ <Tag :color="row.approvalEnabled ? 'success' : 'default'">
+ {{ row.approvalEnabled ? '宸插惎鐢�' : '鏈惎鐢�' }}
+ </Tag>
+ </template>
+ <template #userNames="{ row }">
+ <span v-if="row.userNames && row.userNames.length > 0">
+ {{ row.userNames.join('銆�') }}
+ </span>
+ <span v-else class="text-gray-400">鏈厤缃�</span>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '閰嶇疆瀹℃壒浜�',
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['system:approval-config:update'],
+ onClick: handleConfig.bind(null, row),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/system/approvalConfig/modules/config-form.vue b/src/views/system/approvalConfig/modules/config-form.vue
new file mode 100644
index 0000000..d8455ac
--- /dev/null
+++ b/src/views/system/approvalConfig/modules/config-form.vue
@@ -0,0 +1,79 @@
+<script lang="ts" setup>
+ import type { SystemApprovalConfigApi } from '#/api/system/approval-config';
+
+ import { ref } from 'vue';
+
+ import { useVbenModal } from '@vben/common-ui';
+
+ import { message } from 'ant-design-vue';
+
+ import { useVbenForm } from '#/adapter/form';
+ import { saveApprovalConfig } from '#/api/system/approval-config';
+
+ import { useConfigFormSchema } from '../data';
+
+ const emit = defineEmits(['success']);
+
+ const currentRow = ref<SystemApprovalConfigApi.ApprovalConfig>();
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ wrapperClass: 'grid-cols-1',
+ layout: 'horizontal',
+ schema: useConfigFormSchema(),
+ showDefaultActions: false,
+ });
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const values = await formApi.getValues();
+ try {
+ await saveApprovalConfig({
+ id: currentRow.value?.id,
+ bizType: values.bizType,
+ bizName: values.bizName,
+ approvalEnabled: values.approvalEnabled,
+ userIds: values.userIds ?? [],
+ remark: values.remark,
+ });
+ await modalApi.close();
+ emit('success');
+ message.success('淇濆瓨鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ currentRow.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<SystemApprovalConfigApi.ApprovalConfig>();
+ currentRow.value = data;
+ await formApi.setValues({
+ bizName: data.bizName,
+ bizType: data.bizType,
+ approvalEnabled: data.approvalEnabled ?? false,
+ userIds: data.userIds ?? [],
+ remark: data.remark,
+ });
+ },
+ });
+</script>
+
+<template>
+ <Modal title="瀹℃壒閰嶇疆" class="w-2/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
--
Gitblit v1.9.3