liu
2 天以前 31f8c28b502a3f963a2651194112584371448e37
feat(srm): 供应商准入审核接入协同办公(BPM)

- 移除采购/质量/财务审核按钮,改为提交时选择协同办公审批流程
- 新增审批流程选择弹窗、查看审批(只读)跳转
- 新增协同办公审批详情业务组件 /srm/supplierApply/detail/index.vue
- 状态简化为 5 态,表单支持只读详情模式
- 接口新增 getApproveProcessList,submit 携带 processDefinitionKey

Co-Authored-By: Claude <noreply@anthropic.com>
已添加2个文件
已修改5个文件
455 ■■■■■ 文件已修改
src/api/srm/supplierApply/index.ts 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/enums.ts 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/data.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/detail/index.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/index.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/modules/form.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/supplierApply/modules/process-select-modal.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/supplierApply/index.ts
@@ -8,27 +8,32 @@
    applyNo?: string;
    supplierId?: number;
    supplierName: string;
    contactName?: string;
    contactMobile?: string;
    contactEmail?: string;
    supplierType?: string;
    companyNature?: string;
    registeredCapital?: number;
    businessLicenseNo?: string;
    taxNo?: string;
    address?: string;
    bankName?: string;
    bankAccount?: string;
    taxNo?: string;
    businessLicense?: string;
    taxCertificate?: string;
    orgCodeCertificate?: string;
    contactName?: string;
    contactMobile?: string;
    contactEmail?: string;
    applyUserName?: string;
    applyTime?: string;
    applyStatus?: number;
    applyStatusLabel?: string;
    auditComment?: string;
    processInstanceId?: string;
    remark?: string;
    createTime?: string;
  }
  export interface ReviewVO {
    id: number;
    passed: boolean;
    comment?: string;
  /** ååŒåŠžå…¬å®¡æ‰¹æµç¨‹å®šä¹‰ */
  export interface ProcessDefinition {
    id: string;
    key: string;
    name: string;
  }
}
@@ -57,22 +62,16 @@
  return requestClient.delete(`/srm/supplier-apply/delete?id=${id}`);
}
/** æäº¤å®¡æ ¸ */
export function submitApply(id: number) {
  return requestClient.put(`/srm/supplier-apply/submit?id=${id}`);
/** èŽ·å–å®¡æ‰¹æµç¨‹åˆ—è¡¨ï¼ˆååŒåŠžå…¬å·²å‘å¸ƒæµç¨‹ï¼‰ */
export function getApproveProcessList() {
  return requestClient.get<SrmSupplierApplyApi.ProcessDefinition[]>(
    '/srm/supplier-apply/approve-process-list',
  );
}
/** é‡‡è´­å®¡æ ¸ */
export function reviewPurchase(data: SrmSupplierApplyApi.ReviewVO) {
  return requestClient.put('/srm/supplier-apply/review-purchase', data);
}
/** è´¨é‡å®¡æ ¸ */
export function reviewQuality(data: SrmSupplierApplyApi.ReviewVO) {
  return requestClient.put('/srm/supplier-apply/review-quality', data);
}
/** è´¢åŠ¡å®¡æ ¸ */
export function reviewFinance(data: SrmSupplierApplyApi.ReviewVO) {
  return requestClient.put('/srm/supplier-apply/review-finance', data);
/** æäº¤å‡†å…¥ç”³è¯·å®¡æ‰¹ï¼ˆå¯åŠ¨ååŒåŠžå…¬æµç¨‹ï¼‰ */
export function submitApply(id: number, processDefinitionKey: string) {
  return requestClient.put('/srm/supplier-apply/submit', null, {
    params: { id, processDefinitionKey },
  });
}
src/views/srm/enums.ts
@@ -62,36 +62,35 @@
  labels: { '1': '生产型', '2': '贸易型', '3': '服务型', '4': '其他' },
};
// ========== å‡†å…¥ç”³è¯· â€” å¯¹é½ SupplierApplyStatusEnum (0/10/20/30/40/50) ==========
// ========== å‡†å…¥ç”³è¯· â€” å¯¹é½ SupplierApplyStatusEnum (0/10/30/40/50)
// æŽ¥å…¥ååŒåŠžå…¬åŽç®€åŒ–ä¸º 5 æ€ï¼šå¾…提交 / å®¡æ‰¹ä¸­ / å·²å–消 / å·²é€šè¿‡ / å·²é©³å›ž
// åŽŸå¾…é‡‡è´­å®¡æ ¸(10)/待质量审核(20)/待财务审核(30)已废弃 ==========
export const APPLY_STATUS = {
  DRAFT: 0,
  PENDING_PURCHASE: 10,
  PENDING_QUALITY: 20,
  PENDING_FINANCE: 30,
  PROCESS: 10,
  CANCEL: 30,
  APPROVED: 40,
  REJECTED: 50,
} as const;
export const APPLY_STATUS_OPTIONS: EnumOption[] = [
  { label: '待提交', value: 0 },
  { label: '待采购审核', value: 10 },
  { label: '待质量审核', value: 20 },
  { label: '待财务审核', value: 30 },
  { label: '审批中', value: 10 },
  { label: '已取消', value: 30 },
  { label: '已通过', value: 40 },
  { label: '已拒绝', value: 50 },
  { label: '已驳回', value: 50 },
];
export const APPLY_STATUS_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
  data: [
    { value: 0, color: 'gray' },
    { value: 10, color: 'blue' },
    { value: 20, color: 'orange' },
    { value: 30, color: 'purple' },
    { value: 40, color: 'green' },
    { value: 50, color: 'red' },
  ],
  labels: { 0: '待提交', 10: '待采购审核', 20: '待质量审核', 30: '待财务审核', 40: '已通过', 50: '已拒绝' },
  labels: { 0: '待提交', 10: '审批中', 30: '已取消', 40: '已通过', 50: '已驳回' },
};
// ========== èµ„质证书 â€” å¯¹é½ CertificateTypeEnum (1=营业执照 2=认证证书 3=检测报告 4=其他) ==========
src/views/srm/supplierApply/data.ts
@@ -10,6 +10,9 @@
import { APPLY_STATUS_CELLTAG, APPLY_STATUS_OPTIONS } from '../enums';
/** è¡¨å•类型:新增 / ç¼–辑 / è¯¦æƒ…(只读) */
export type FormType = 'create' | 'edit' | 'detail';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
  return [
src/views/srm/supplierApply/detail/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Spin, Tag } from 'ant-design-vue';
import { getApply } from '#/api/srm/supplierApply';
import { useDescription } from '#/components/description';
import { APPLY_STATUS_CELLTAG } from '../../enums';
defineOptions({ name: 'SrmSupplierApplyDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<SrmSupplierApplyApi.ApplyVO>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** ç”³è¯·çŠ¶æ€æ ‡ç­¾æ¸²æŸ“ */
function statusRender(val: unknown) {
  const num = Number(val);
  const item = APPLY_STATUS_CELLTAG.data.find((i) => Number(i.value) === num);
  const label = APPLY_STATUS_CELLTAG.labels[num] || '-';
  return h(Tag, { color: item?.color }, { default: () => label });
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '申请编号', field: 'applyNo', render: textRender },
    { label: '申请状态', field: 'applyStatus', render: statusRender },
    { label: '申请人', field: 'applyUserName', render: textRender },
    {
      label: '申请时间',
      field: 'applyTime',
      render: (val) => formatDateTime(val) as string,
    },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    { label: '供应商类型', field: 'supplierType', render: textRender },
    { label: '企业性质', field: 'companyNature', render: textRender },
    {
      label: '注册资金(万元)',
      field: 'registeredCapital',
      render: textRender,
    },
    { label: '营业执照编号', field: 'businessLicenseNo', render: textRender },
    { label: '税号', field: 'taxNo', render: textRender },
    { label: '开户银行', field: 'bankName', render: textRender },
    { label: '银行账号', field: 'bankAccount', render: textRender },
    { label: '联系人', field: 'contactName', render: textRender },
    { label: '联系电话', field: 'contactMobile', render: textRender },
    { label: '邮箱', field: 'contactEmail', render: textRender },
    { label: '公司地址', field: 'address', render: textRender },
    {
      label: '备注',
      field: 'remark',
      render: textRender,
      span: 2,
    },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getApply(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <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>
    </Spin>
  </ContentWrap>
</template>
src/views/srm/supplierApply/index.vue
@@ -2,118 +2,38 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteApply,
  getApproveProcessList,
  getApplyPage,
  submitApply,
  reviewPurchase,
  reviewQuality,
  reviewFinance,
} from '#/api/srm/supplierApply';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import ApplyForm from './modules/form.vue';
import ProcessSelectModal from './modules/process-select-modal.vue';
import { APPLY_STATUS } from '../enums';
defineOptions({ name: 'SrmSupplierApply' });
// ---------- reviewModal ----------
const reviewFormData = ref({
  applyId: 0,
  reviewType: '' as 'purchase' | 'quality' | 'finance',
  passed: true,
  comment: '',
});
const reviewTitle = computed(() => {
  const labels: Record<string, string> = {
    purchase: '采购审核',
    quality: '质量审核',
    finance: '财务审核',
  };
  return labels[reviewFormData.value.reviewType] || '审核';
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: ApplyForm,
  destroyOnClose: true,
});
const [ReviewForm, reviewFormApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-1',
  layout: 'horizontal',
  schema: [
    {
      fieldName: 'passed',
      label: '审核结果',
      component: 'RadioGroup',
      rules: 'required',
      componentProps: {
        options: [
          { label: '通过', value: true },
          { label: '拒绝', value: false },
        ],
        buttonStyle: 'solid',
        optionType: 'button',
      },
    },
    {
      fieldName: 'comment',
      label: '审核意见',
      component: 'Textarea',
      componentProps: { placeholder: '请输入审核意见', rows: 3 },
    },
  ],
  showDefaultActions: false,
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
const [ReviewModal, reviewModalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await reviewFormApi.validate();
    if (!valid) {
      return;
    }
    reviewModalApi.lock();
    const data = (await reviewFormApi.getValues()) as { passed: boolean; comment?: string };
    try {
      const payload: SrmSupplierApplyApi.ReviewVO = {
        id: reviewFormData.value.applyId,
        passed: data.passed,
        comment: data.comment,
      };
      if (reviewFormData.value.reviewType === 'purchase') {
        await reviewPurchase(payload);
      } else if (reviewFormData.value.reviewType === 'quality') {
        await reviewQuality(payload);
      } else if (reviewFormData.value.reviewType === 'finance') {
        await reviewFinance(payload);
      }
      await reviewModalApi.close();
      handleRefresh();
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      reviewModalApi.unlock();
    }
  },
});
function openReviewModal(row: SrmSupplierApplyApi.ApplyVO, reviewType: 'purchase' | 'quality' | 'finance') {
  reviewFormData.value = {
    applyId: row.id!,
    reviewType,
    passed: true,
    comment: '',
  };
  reviewFormApi.resetForm();
  reviewModalApi.open();
}
// ---------- grid ----------
function handleRefresh() {
@@ -121,11 +41,16 @@
}
function handleCreate() {
  formModalApi.setData(null).open();
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: SrmSupplierApplyApi.ApplyVO) {
  formModalApi.setData(row).open();
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
/** æŸ¥çœ‹è¯¦æƒ…(只读) */
function handleDetail(row: SrmSupplierApplyApi.ApplyVO) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
async function handleDelete(row: SrmSupplierApplyApi.ApplyVO) {
@@ -142,21 +67,38 @@
  }
}
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–ååŒåŠžå…¬å®¡æ‰¹æµç¨‹åˆ—è¡¨ */
async function handleSubmit(row: SrmSupplierApplyApi.ApplyVO) {
  const hideLoading = message.loading({ content: '提交中...', duration: 0 });
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    await submitApply(row.id!);
    message.success($t('ui.actionMessage.operationSuccess'));
    handleRefresh();
  } finally {
    const processList = await getApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先在协同办公配置审批流程');
      return;
    }
    // æ‰“开流程选择弹窗
    processModalApi.setData({ id: row.id, processList }).open();
  } catch {
    hideLoading();
  }
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: ApplyForm,
  destroyOnClose: true,
/** æŸ¥çœ‹å®¡æ‰¹è¿›åº¦ï¼ˆè·³è½¬ååŒåŠžå…¬æµç¨‹å®žä¾‹è¯¦æƒ…ï¼Œåªè¯»æŸ¥çœ‹ï¼‰ */
function handleViewProcess(row: SrmSupplierApplyApi.ApplyVO) {
  if (!row.processInstanceId) {
    message.warning('该申请尚未提交审批');
    return;
  }
  // viewOnly=1:隐藏审批操作按钮,仅查看审批进度
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId, viewOnly: '1' },
});
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
@@ -186,9 +128,7 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ReviewModal :title="reviewTitle" class="w-1/3">
      <ReviewForm class="mx-4" />
    </ReviewModal>
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="准入申请列表">
      <template #toolbar-tools>
        <TableAction
@@ -207,43 +147,34 @@
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:supplier-apply:update'],
              onClick: handleEdit.bind(null, row),
              icon: ACTION_ICON.VIEW,
              auth: ['srm:supplier-apply:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              icon: ACTION_ICON.CHECK,
              icon: ACTION_ICON.AUDIT,
              auth: ['srm:supplier-apply:update'],
              ifShow: row.applyStatus === APPLY_STATUS.DRAFT,
              ifShow: () => row.applyStatus === APPLY_STATUS.DRAFT || row.applyStatus === APPLY_STATUS.REJECTED,
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '采购审核',
              label: '查看审批',
              type: 'link',
              icon: ACTION_ICON.CHECK,
              auth: ['srm:supplier-apply:update'],
              ifShow: row.applyStatus === APPLY_STATUS.PENDING_PURCHASE,
              onClick: openReviewModal.bind(null, row, 'purchase'),
              icon: ACTION_ICON.VIEW,
              ifShow: () => !!row.processInstanceId,
              onClick: handleViewProcess.bind(null, row),
            },
            {
              label: '质量审核',
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.CHECK,
              icon: ACTION_ICON.EDIT,
              auth: ['srm:supplier-apply:update'],
              ifShow: row.applyStatus === APPLY_STATUS.PENDING_QUALITY,
              onClick: openReviewModal.bind(null, row, 'quality'),
            },
            {
              label: '财务审核',
              type: 'link',
              icon: ACTION_ICON.CHECK,
              auth: ['srm:supplier-apply:update'],
              ifShow: row.applyStatus === APPLY_STATUS.PENDING_FINANCE,
              onClick: openReviewModal.bind(null, row, 'finance'),
              ifShow: () => row.applyStatus === APPLY_STATUS.DRAFT || row.applyStatus === APPLY_STATUS.REJECTED,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
@@ -251,6 +182,7 @@
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:supplier-apply:delete'],
              ifShow: () => row.applyStatus === APPLY_STATUS.DRAFT || row.applyStatus === APPLY_STATUS.REJECTED,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.supplierName]),
                confirm: handleDelete.bind(null, row),
src/views/srm/supplierApply/modules/form.vue
@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
import type { FormType } from '../data';
import { computed, ref } from 'vue';
@@ -19,10 +20,16 @@
const emit = defineEmits(['success']);
const formData = ref<SrmSupplierApplyApi.ApplyVO>();
const formType = ref<FormType>('create');
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['准入申请'])
    : $t('ui.actionTitle.create', ['准入申请']);
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['准入申请']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['准入申请']);
  } else {
    return '准入申请详情';
  }
});
const [Form, formApi] = useVbenForm({
@@ -60,7 +67,10 @@
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<SrmSupplierApplyApi.ApplyVO>();
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data?.formType ?? 'create';
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formApi));
    if (!data || !data.id) {
      return;
    }
@@ -78,7 +88,11 @@
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
  <Modal
    :title="getTitle"
    class="w-1/2"
    :show-confirm-button="formType !== 'detail'"
  >
    <Form class="mx-4" />
  </Modal>
</template>
src/views/srm/supplierApply/modules/process-select-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
<script lang="ts" setup>
import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Select } from 'ant-design-vue';
import { submitApply } from '#/api/srm/supplierApply';
const emit = defineEmits(['success']);
const applyId = ref<number>();
const processList = ref<SrmSupplierApplyApi.ProcessDefinition[]>([]);
const selectedProcessKey = ref<string>('');
const processOptions = computed(() => {
  return processList.value.map((p) => ({
    label: p.name,
    value: p.key,
  }));
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!selectedProcessKey.value) {
      message.warning('请选择审批流程');
      return;
    }
    modalApi.lock();
    try {
      await submitApply(applyId.value!, selectedProcessKey.value);
      await modalApi.close();
      emit('success');
      message.success('提交成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      applyId.value = undefined;
      selectedProcessKey.value = '';
      processList.value = [];
      return;
    }
    const data = modalApi.getData<{
      id: number;
      processList: SrmSupplierApplyApi.ProcessDefinition[];
    }>();
    applyId.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: 220px"
        placeholder="请选择审批流程"
      />
    </div>
  </Modal>
</template>