liu
3 天以前 0127fd119fe2a349fbfee92b375a5680e10d3275
feat(erp): 销售订单审批接入协同办公前端支持

- 列表页移除审批/反审批,新增提交审批、审批进度(只读查看模式)
- 状态支持 未提交/审批中/已驳回
- 新增销售订单协同办公审批详情组件 detail/index.vue
- 明细表格启用横向滚动,避免列被压缩显示不完整

Co-Authored-By: Claude <noreply@anthropic.com>
已添加1个文件
已修改5个文件
251 ■■■■ 文件已修改
src/api/erp/sale/order/index.ts 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/detail/index.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/index.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/modules/item-form.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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', {
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>
    <!-- æ‰“印对话框 -->
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: '请选择状态',
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>
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: '正在提交审批...',
    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!]),
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,