From 0127fd119fe2a349fbfee92b375a5680e10d3275 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期四, 27 八月 2026 14:30:33 +0800
Subject: [PATCH] feat(erp): 销售订单审批接入协同办公前端支持

---
 src/api/erp/sale/order/index.ts                |   16 ++
 src/views/erp/sale/order/index.vue             |   61 +++++++----
 src/views/erp/sale/order/modules/item-form.vue |    4 
 src/views/bpm/processInstance/detail/index.vue |   12 ++
 src/views/erp/sale/order/detail/index.vue      |  154 ++++++++++++++++++++++++++++++
 src/views/erp/sale/order/data.ts               |    4 
 6 files changed, 221 insertions(+), 30 deletions(-)

diff --git a/src/api/erp/sale/order/index.ts b/src/api/erp/sale/order/index.ts
index 1e6366c..3961aca 100644
--- a/src/api/erp/sale/order/index.ts
+++ b/src/api/erp/sale/order/index.ts
@@ -16,6 +16,7 @@
     totalPrice: number; // 鍚堣閲戦锛屽崟浣嶏細鍏�
     status: number; // 鐘舵��
     remark: string; // 澶囨敞
+    processInstanceId?: string; // BPM 娴佺▼瀹炰緥缂栧彿锛堟彁浜ゅ崗鍚屽姙鍏鎵瑰悗鐢熸垚锛�
     outCount: number; // 閿�鍞嚭搴撴暟閲�
     outStatus?: number; // 鍑哄簱鐘舵�侊細0-鏈嚭搴擄紝1-閮ㄥ垎鍑哄簱锛�2-鍏ㄩ儴鍑哄簱
     attachmentList?: { id: number; name?: string; url?: string }[]; // 闄勪欢鍒楄〃
@@ -96,13 +97,20 @@
   return requestClient.put('/erp/sale-order/update', data);
 }
 
-/** 鏇存柊閿�鍞鍗曠殑鐘舵�� */
-export function updateSaleOrderStatus(id: number, status: number) {
-  return requestClient.put('/erp/sale-order/update-status', null, {
-    params: { id, status },
+/** 鎻愪氦閿�鍞鍗曞鎵癸紙鍗忓悓鍔炲叕锛� */
+export function submitSaleOrder(id: number, processDefinitionKey: string) {
+  return requestClient.post('/erp/sale-order/submit', null, {
+    params: { id, processDefinitionKey },
   });
 }
 
+/** 鑾峰彇閿�鍞鍗曞鎵规祦绋嬪畾涔夊垪琛� */
+export function getSaleOrderApproveProcessList() {
+  return requestClient.get<
+    { id: string; key: string; name: string }[]
+  >('/erp/sale-order/approve-process-list');
+}
+
 /** 鍒犻櫎閿�鍞鍗� */
 export function deleteSaleOrder(ids: number[]) {
   return requestClient.delete('/erp/sale-order/delete', {
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index f4d8288..4ac3c9e 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -4,6 +4,8 @@
 
 import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
 
+import { useRoute } from 'vue-router';
+
 import { Page, useVbenModal } from '@vben/common-ui';
 import {
   BpmFieldPermissionType,
@@ -41,11 +43,16 @@
 
 defineOptions({ name: 'BpmProcessInstanceDetail' });
 
+const route = useRoute();
+
 const props = defineProps<{
   activityId?: string; // 娴佺▼娲诲姩缂栧彿锛岀敤浜庢妱閫佹煡鐪�
   id: string; // 娴佺▼瀹炰緥鐨勭紪鍙�
   taskId?: string; // 浠诲姟缂栧彿
 }>();
+
+/** 鍙鏌ョ湅妯″紡锛氫粠涓氬姟妯″潡锛堝閿�鍞鍗曞鎵硅繘搴︼級璺宠浆鏃堕殣钘忓鎵规搷浣滄寜閽紝浠呮煡鐪� */
+const viewOnly = route.query.viewOnly === '1';
 
 const processInstanceLoading = ref(false); // 娴佺▼瀹炰緥鐨勫姞杞戒腑
 const processInstance = ref<BpmProcessInstanceApi.ProcessInstance>(); // 娴佺▼瀹炰緥
@@ -362,7 +369,7 @@
       </div>
 
       <template #actions>
-        <div class="px-4">
+        <div v-if="!viewOnly" class="px-4">
           <ProcessInstanceOperationButton
             ref="operationButtonRef"
             :process-instance="processInstance"
@@ -374,6 +381,9 @@
             @success="refresh"
           />
         </div>
+        <div v-else class="px-4 py-2 text-sm text-muted-foreground">
+          <span>鍙鏌ョ湅妯″紡锛氬鎵规搷浣滆鍦ㄣ�屽崗鍚屽姙鍏� 鈫� 寰呭姙浠诲姟銆嶄腑澶勭悊</span>
+        </div>
       </template>
     </Card>
     <!-- 鎵撳嵃瀵硅瘽妗� -->
diff --git a/src/views/erp/sale/order/data.ts b/src/views/erp/sale/order/data.ts
index b61597e..ffec496 100644
--- a/src/views/erp/sale/order/data.ts
+++ b/src/views/erp/sale/order/data.ts
@@ -378,8 +378,10 @@
       component: 'Select',
       componentProps: {
         options: [
-          { label: '鏈鏍�', value: 10 },
+          { label: '鏈彁浜�', value: 0 },
+          { label: '瀹℃壒涓�', value: 15 },
           { label: '宸插鏍�', value: 20 },
+          { label: '宸查┏鍥�', value: 35 },
           { label: '宸蹭綔搴�', value: 30 },
         ],
         placeholder: '璇烽�夋嫨鐘舵��',
diff --git a/src/views/erp/sale/order/detail/index.vue b/src/views/erp/sale/order/detail/index.vue
new file mode 100644
index 0000000..673f4a4
--- /dev/null
+++ b/src/views/erp/sale/order/detail/index.vue
@@ -0,0 +1,154 @@
+<script lang="ts" setup>
+import type { ErpSaleOrderApi } from '#/api/erp/sale/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 { getSaleOrder } from '#/api/erp/sale/order';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+import ItemForm from '../modules/item-form.vue';
+
+defineOptions({ name: 'ErpSaleOrderDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<ErpSaleOrderApi.SaleOrder>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+  return val === 0 || val ? String(val) : '-';
+}
+
+/** 鍑哄簱鐘舵�佹覆鏌� */
+function renderOutStatus(val?: number) {
+  const map: Record<number, { text: string; color: string }> = {
+    0: { text: '鏈嚭搴�', color: 'default' },
+    1: { text: '宸查鐣�', color: 'processing' },
+    2: { text: '閮ㄥ垎鍑哄簱', color: 'warning' },
+    3: { text: '鍏ㄩ儴鍑哄簱', color: 'success' },
+  };
+  const item = map[val ?? 0]!;
+  return h(Tag, { color: item.color }, () => item.text);
+}
+
+/** 鐢熶骇鐘舵�佹覆鏌� */
+function renderProductionStatus(val?: number) {
+  if (formData.value?.needProduction !== 1) {
+    return h(Tag, { color: 'default' }, () => '鏃犻渶鐢熶骇');
+  }
+  const map: Record<number, { text: string; color: string }> = {
+    0: { text: '鏈畬鎴�', color: 'warning' },
+    1: { 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', render: textRender },
+    { label: '瀹㈡埛鍚嶇О', field: 'customerName', render: textRender },
+    { label: '鍏宠仈鍚堝悓', field: 'contractNo', 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: 'outStatus',
+      render: (val) => renderOutStatus(val as number),
+    },
+    {
+      label: '鐢熶骇鐘舵��',
+      field: 'productionStatus',
+      render: (val) => renderProductionStatus(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 getSaleOrder(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/sale/order/index.vue b/src/views/erp/sale/order/index.vue
index 6124ec7..925be2a 100644
--- a/src/views/erp/sale/order/index.vue
+++ b/src/views/erp/sale/order/index.vue
@@ -16,8 +16,9 @@
   deleteSaleOrder,
   enableSaleOrder,
   exportSaleOrder,
+  getSaleOrderApproveProcessList,
   getSaleOrderPage,
-  updateSaleOrderStatus,
+  submitSaleOrder,
 } from '#/api/erp/sale/order';
 import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice';
 import { $t } from '#/locales';
@@ -79,21 +80,34 @@
   }
 }
 
-/** 瀹℃壒/鍙嶅鎵规搷浣� */
-async function handleUpdateStatus(
-  row: ErpSaleOrderApi.SaleOrder,
-  status: number,
-) {
+/** 鎻愪氦閿�鍞鍗曞鎵癸紙鍗忓悓鍔炲叕锛� */
+async function handleSubmit(row: ErpSaleOrderApi.SaleOrder) {
+  // 鑾峰彇鍙敤鐨勫鎵规祦绋嬪畾涔�
+  const processList = await getSaleOrderApproveProcessList();
+  if (isEmpty(processList)) {
+    message.error('閿�鍞鍗曞鎵规祦绋嬫湭閰嶇疆锛岃鍏堝湪銆屽崗鍚屽姙鍏啋娴佺▼绠$悊銆嶄腑鍒涘缓骞跺彂甯冮攢鍞鍗曞鎵规祦绋�');
+    return;
+  }
   const hideLoading = message.loading({
-    content: `纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヨ鍗曞悧锛焋,
+    content: '姝e湪鎻愪氦瀹℃壒...',
     duration: 0,
   });
   try {
-    await updateSaleOrderStatus(row.id!, status);
-    message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`);
+    await submitSaleOrder(row.id!, processList[0].key);
+    message.success('鎻愪氦瀹℃壒鎴愬姛锛岃鍒板崗鍚屽姙鍏腑澶勭悊');
     handleRefresh();
   } finally {
     hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒杩涘害锛堣烦杞崗鍚屽姙鍏祦绋嬭鎯咃紝鍙妯″紡锛屼笉鏄剧ず瀹℃壒鎿嶄綔锛� */
+function handleViewApproval(row: ErpSaleOrderApi.SaleOrder) {
+  if (row.processInstanceId) {
+    router.push({
+      name: 'BpmProcessInstanceDetail',
+      query: { id: row.processInstanceId, viewOnly: '1' },
+    });
   }
 }
 
@@ -278,8 +292,10 @@
         <span v-else>-</span>
       </template>
       <template #status="{ row }">
-        <Tag v-if="row.status === 10" color="warning">鏈鏍�</Tag>
+        <Tag v-if="row.status === 0" color="default">鏈彁浜�</Tag>
+        <Tag v-if="row.status === 15" color="processing">瀹℃壒涓�</Tag>
         <Tag v-if="row.status === 20" color="success">宸插鏍�</Tag>
+        <Tag v-if="row.status === 35" color="warning">宸查┏鍥�</Tag>
         <Tag v-if="row.status === 30" color="error">宸蹭綔搴�</Tag>
       </template>
       <template #needProduction="{ row }">
@@ -323,30 +339,27 @@
               type: 'link',
               icon: ACTION_ICON.EDIT,
               auth: ['erp:sale-order:update'],
-              ifShow: () => row.status === 10,
+              ifShow: () => row.status === 0 || row.status === 35,
               onClick: handleEdit.bind(null, row),
             },
             {
-              label: '瀹℃壒',
+              label: '鎻愪氦瀹℃壒',
               type: 'link',
               icon: ACTION_ICON.AUDIT,
               auth: ['erp:sale-order:update-status'],
-              ifShow: () => row.status === 10,
+              ifShow: () => row.status === 0 || row.status === 35,
               popConfirm: {
-                title: `纭瀹℃壒${row.no}鍚楋紵`,
-                confirm: handleUpdateStatus.bind(null, row, 20),
+                title: `纭鎻愪氦${row.no}鐨勫鎵瑰悧锛熸彁浜ゅ悗瀹℃壒灏嗗湪鍗忓悓鍔炲叕涓畬鎴恅,
+                confirm: handleSubmit.bind(null, row),
               },
             },
             {
-              label: '鍙嶅鎵�',
+              label: '瀹℃壒杩涘害',
               type: 'link',
-              icon: ACTION_ICON.AUDIT,
-              auth: ['erp:sale-order:update-status'],
-              ifShow: () => row.status === 20,
-              popConfirm: {
-                title: `纭鍙嶅鎵�${row.no}鍚楋紵`,
-                confirm: handleUpdateStatus.bind(null, row, 10),
-              },
+              icon: 'lucide:file-clock',
+              auth: ['erp:sale-order:query'],
+              ifShow: () => row.status === 15 && !!row.processInstanceId,
+              onClick: handleViewApproval.bind(null, row),
             },
             {
               label: '浣滃簾',
@@ -384,7 +397,7 @@
               danger: true,
               icon: ACTION_ICON.DELETE,
               auth: ['erp:sale-order:delete'],
-              ifShow: () => row.status === 10,
+              ifShow: () => row.status === 0 || row.status === 35,
               popConfirm: {
                 title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                 confirm: handleDelete.bind(null, [row.id!]),
diff --git a/src/views/erp/sale/order/modules/item-form.vue b/src/views/erp/sale/order/modules/item-form.vue
index ffd3207..eea4f3c 100644
--- a/src/views/erp/sale/order/modules/item-form.vue
+++ b/src/views/erp/sale/order/modules/item-form.vue
@@ -63,6 +63,10 @@
     minHeight: 250,
     autoResize: true,
     border: true,
+    // 鏄庣粏鍒楄緝澶氾紝鍚敤妯悜婊氬姩锛岄伩鍏嶅垪琚帇缂╁鑷存樉绀轰笉瀹屾暣锛堝崗鍚屽姙鍏鎵硅鎯呯瓑绐勫鍣ㄥ満鏅級
+    scroll: {
+      x: 'auto',
+    },
     rowConfig: {
       keyField: 'seq',
       isHover: true,

--
Gitblit v1.9.3