gaoluyang
3 天以前 f3cac4aa2db56d6017c1d1dc035bcfeaec76c3e2
feat(crm): 投标保证金迁移到销售管理并接入销售台账

- 新增销售管理 → 投标保证金页面:客户(招标方)筛选与登记,展示中标结果、
退回状态、退回金额与日期,可关联该客户已审核的销售订单并显示订单金额
- 选中销售订单自动置为已中标/无需退回,改未中标自动清空订单与退款字段,
换客户清空已选订单;提交前清理无效退款金额与日期
- 删除原供应商管理下的投标保证金页面与接口定义,权限串改为 crm:bid-bond:*

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加3个文件
已删除3个文件
已重命名1个文件
968 ■■■■■ 文件已修改
src/api/crm/bidBond/index.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/bidBond/index.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/bidBond/data.ts 290 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/bidBond/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/bidBond/modules/form.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/bidBond/data.ts 255 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/bidBond/modules/form.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/bidBond/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmBidBondApi {
  /** æŠ•标保证金 */
  export interface BidBond {
    id?: number;
    no?: string;
    customerId?: number;
    customerName?: string;
    projectName?: string;
    tenderNo?: string;
    saleOrderId?: number;
    saleOrderNo?: string;
    saleOrderAmount?: number;
    bondAmount?: number;
    payDate?: string;
    /** ä¸­æ ‡ç»“果:0 å¾…开标、1 å·²ä¸­æ ‡ã€2 æœªä¸­æ ‡ */
    bidResult?: number;
    /** é€€å›žçŠ¶æ€ï¼š0 æœªé€€å›žã€1 éƒ¨åˆ†é€€å›žã€2 å·²é€€å›žã€3 æ— éœ€é€€å›ž */
    refundStatus?: number;
    refundAmount?: number;
    refundDate?: string;
    remark?: string;
    createTime?: string;
  }
  /** å¯å…³è”的销售订单(销售台账) */
  export interface SaleOrderOption {
    id: number;
    no: string;
    customerId: number;
    status: number;
    statusName?: string;
    totalPrice?: number;
    contractNo?: string;
  }
}
/** åˆ†é¡µæŸ¥è¯¢æŠ•标保证金 */
export function getBidBondPage(params: PageParam) {
  return requestClient.get<PageResult<CrmBidBondApi.BidBond>>(
    '/crm/bid-bond/page',
    { params },
  );
}
/** æŸ¥è¯¢æŠ•标保证金详情 */
export function getBidBond(id: number) {
  return requestClient.get<CrmBidBondApi.BidBond>(
    `/crm/bid-bond/get?id=${id}`,
  );
}
/** åˆ›å»ºæŠ•标保证金 */
export function createBidBond(data: CrmBidBondApi.BidBond) {
  return requestClient.post<number>('/crm/bid-bond/create', data);
}
/** æ›´æ–°æŠ•标保证金 */
export function updateBidBond(data: CrmBidBondApi.BidBond) {
  return requestClient.put('/crm/bid-bond/update', data);
}
/** åˆ é™¤æŠ•标保证金 */
export function deleteBidBond(id: number) {
  return requestClient.delete('/crm/bid-bond/delete', { params: { id } });
}
/** æŸ¥è¯¢å®¢æˆ·ä¸‹å¯å…³è”的销售订单(仅已审核) */
export function getSaleOrderOptions(customerId: number) {
  return requestClient.get<CrmBidBondApi.SaleOrderOption[]>(
    '/crm/bid-bond/sale-order-list',
    { params: { customerId } },
  );
}
src/api/srm/bidBond/index.ts
ÎļþÒÑɾ³ý
src/views/crm/bidBond/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,290 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getCustomerSimpleList } from '#/api/crm/customer';
import { getSaleOrderOptions } from '#/api/crm/bidBond';
export const BID_RESULT_OPTIONS = [
  { label: '待开标', value: 0, color: 'processing' },
  { label: '已中标', value: 1, color: 'success' },
  { label: '未中标', value: 2, color: 'default' },
];
export const REFUND_STATUS_OPTIONS = [
  { label: '未退回', value: 0, color: 'warning' },
  { label: '部分退回', value: 1, color: 'processing' },
  { label: '已退回', value: 2, color: 'success' },
  { label: '无需退回', value: 3, color: 'default' },
];
export function getBidResultLabel(result?: number) {
  return BID_RESULT_OPTIONS.find((item) => item.value === result)?.label ?? '-';
}
export function getBidResultColor(result?: number) {
  return BID_RESULT_OPTIONS.find((item) => item.value === result)?.color ?? 'default';
}
export function getRefundStatusLabel(status?: number) {
  return REFUND_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? '-';
}
export function getRefundStatusColor(status?: number) {
  return REFUND_STATUS_OPTIONS.find((item) => item.value === status)?.color ?? 'default';
}
/** æ–°å¢ž/修改投标保证金的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'customerId',
      label: '客户(招标方)',
      component: 'ApiSelect',
      rules: 'required',
      componentProps: {
        placeholder: '请选择客户',
        allowClear: true,
        showSearch: true,
        api: getCustomerSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      fieldName: 'projectName',
      label: '项目名称',
      component: 'Input',
      componentProps: { placeholder: '请输入本次投标项目名称' },
    },
    {
      fieldName: 'tenderNo',
      label: '招标编号',
      component: 'Input',
      componentProps: { placeholder: '请输入招标方发布的招标编号' },
    },
    {
      fieldName: 'saleOrderId',
      label: '关联销售订单',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请先选择客户',
        allowClear: true,
        showSearch: true,
        labelField: 'no',
        valueField: 'id',
        optionFilterProp: 'label',
      },
      dependencies: {
        triggerFields: ['customerId'],
        componentProps: (values) => {
          const customerId = values.customerId as number | undefined;
          return {
            placeholder: customerId
              ? '请选择该客户已审核的销售订单(销售台账)'
              : '请先选择客户',
            disabled: !customerId,
            api: customerId
              ? () => getSaleOrderOptions(customerId)
              : () => Promise.resolve([]),
          };
        },
      },
    },
    {
      fieldName: 'bondAmount',
      label: '保证金金额',
      component: 'InputNumber',
      rules: 'required',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入缴纳的保证金金额',
        min: 0,
        precision: 2,
      },
    },
    {
      fieldName: 'payDate',
      label: '缴纳日期',
      component: 'DatePicker',
      componentProps: {
        class: 'w-full',
        placeholder: '请选择向客户缴纳保证金的日期',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'bidResult',
      label: '中标结果',
      component: 'Select',
      rules: 'required',
      componentProps: {
        placeholder: '请选择中标结果',
        options: BID_RESULT_OPTIONS.map(({ label, value }) => ({
          label,
          value,
        })),
      },
      defaultValue: 0,
    },
    {
      fieldName: 'refundStatus',
      label: '退回状态',
      component: 'Select',
      componentProps: {
        placeholder: '未中标时跟踪保证金是否退回',
        options: REFUND_STATUS_OPTIONS.map(({ label, value }) => ({
          label,
          value,
        })),
      },
    },
    {
      fieldName: 'refundAmount',
      label: '退回金额',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        placeholder: '已退回时须等于保证金金额;部分退回时须小于保证金金额',
        min: 0,
        precision: 2,
      },
    },
    {
      fieldName: 'refundDate',
      label: '退回日期',
      component: 'DatePicker',
      componentProps: {
        class: 'w-full',
        placeholder: '请选择客户实际退回保证金的日期',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: { placeholder: '请输入备注', rows: 2 },
      formItemClass: 'col-span-2',
    },
  ];
}
/** æœç´¢è¡¨å• */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '保证金单号',
      component: 'Input',
      componentProps: { placeholder: '请输入保证金单号', allowClear: true },
    },
    {
      fieldName: 'customerId',
      label: '客户',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择客户',
        allowClear: true,
        showSearch: true,
        api: getCustomerSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
    },
    {
      fieldName: 'projectName',
      label: '项目名称',
      component: 'Input',
      componentProps: { placeholder: '请输入项目名称', allowClear: true },
    },
    {
      fieldName: 'saleOrderNo',
      label: '销售订单号',
      component: 'Input',
      componentProps: { placeholder: '请输入关联销售订单号', allowClear: true },
    },
    {
      fieldName: 'bidResult',
      label: '中标结果',
      component: 'Select',
      componentProps: {
        placeholder: '请选择',
        allowClear: true,
        options: BID_RESULT_OPTIONS.map(({ label, value }) => ({
          label,
          value,
        })),
      },
    },
    {
      fieldName: 'refundStatus',
      label: '退回状态',
      component: 'Select',
      componentProps: {
        placeholder: '请选择',
        allowClear: true,
        options: REFUND_STATUS_OPTIONS.map(({ label, value }) => ({
          label,
          value,
        })),
      },
    },
  ];
}
/** è¡¨æ ¼åˆ— */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'no', title: '保证金单号', minWidth: 150 },
    { field: 'customerName', title: '客户(招标方)', minWidth: 160 },
    { field: 'projectName', title: '项目名称', minWidth: 180 },
    { field: 'tenderNo', title: '招标编号', minWidth: 140 },
    {
      field: 'saleOrderNo',
      title: '关联销售订单',
      minWidth: 160,
      slots: { default: 'saleOrderNo' },
    },
    {
      field: 'saleOrderAmount',
      title: '订单金额',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    {
      field: 'bondAmount',
      title: '保证金金额',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    { field: 'payDate', title: '缴纳日期', minWidth: 110 },
    {
      field: 'bidResult',
      title: '中标结果',
      minWidth: 100,
      slots: { default: 'bidResult' },
    },
    {
      field: 'refundStatus',
      title: '退回状态',
      minWidth: 110,
      slots: { default: 'refundStatus' },
    },
    {
      field: 'refundAmount',
      title: '退回金额',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    { field: 'refundDate', title: '退回日期', minWidth: 110 },
    { field: 'remark', title: '备注', minWidth: 150 },
    {
      title: '操作',
      width: 130,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/crm/bidBond/index.vue
ÎļþÃû´Ó src/views/srm/bidBond/index.vue ÐÞ¸Ä
@@ -1,19 +1,18 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmBidBondApi } from '#/api/srm/bidBond';
import type { CrmBidBondApi } from '#/api/crm/bidBond';
import { Page, useVbenModal } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteBidBond,
  getBidBondPage,
} from '#/api/srm/bidBond';
import { deleteBidBond, getBidBondPage } from '#/api/crm/bidBond';
import { $t } from '#/locales';
import {
  getBidResultColor,
  getBidResultLabel,
  getRefundStatusColor,
  getRefundStatusLabel,
  useGridColumns,
@@ -21,7 +20,7 @@
} from './data';
import BidBondForm from './modules/form.vue';
defineOptions({ name: 'SrmBidBond' });
defineOptions({ name: 'CrmBidBond' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: BidBondForm,
@@ -36,11 +35,11 @@
  formModalApi.setData(null).open();
}
function handleEdit(row: SrmBidBondApi.BidBond) {
function handleEdit(row: CrmBidBondApi.BidBond) {
  formModalApi.setData({ id: row.id }).open();
}
async function handleDelete(row: SrmBidBondApi.BidBond) {
async function handleDelete(row: CrmBidBondApi.BidBond) {
  await deleteBidBond(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
  handleRefresh();
@@ -67,7 +66,7 @@
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SrmBidBondApi.BidBond>,
  } as VxeTableGridOptions<CrmBidBondApi.BidBond>,
});
</script>
@@ -82,15 +81,19 @@
              label: $t('ui.actionTitle.create', ['投标保证金']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['srm:bid-bond:create'],
              auth: ['crm:bid-bond:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #won="{ row }">
        <Tag :color="row.won ? 'success' : 'default'">
          {{ row.won ? '中标' : '未中标' }}
      <template #saleOrderNo="{ row }">
        <span v-if="row.saleOrderNo">{{ row.saleOrderNo }}</span>
        <span v-else class="text-gray-400">未关联</span>
      </template>
      <template #bidResult="{ row }">
        <Tag :color="getBidResultColor(row.bidResult)">
          {{ getBidResultLabel(row.bidResult) }}
        </Tag>
      </template>
      <template #refundStatus="{ row }">
@@ -105,7 +108,7 @@
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:bid-bond:update'],
              auth: ['crm:bid-bond:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
@@ -113,7 +116,7 @@
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['srm:bid-bond:delete'],
              auth: ['crm:bid-bond:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                confirm: handleDelete.bind(null, row),
src/views/crm/bidBond/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<script lang="ts" setup>
import type { CrmBidBondApi } from '#/api/crm/bidBond';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createBidBond,
  getBidBond,
  updateBidBond,
} from '#/api/crm/bidBond';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<CrmBidBondApi.BidBond>();
/** æ‰“开弹窗回填数据时不触发字段联动,避免回填把已选销售订单清空 */
const loading = ref(false);
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['投标保证金'])
    : $t('ui.actionTitle.create', ['投标保证金']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
  handleValuesChange: async (values, changedFields) => {
    if (loading.value) {
      return;
    }
    // æ¢å®¢æˆ·åŽï¼ŒåŽŸé”€å”®è®¢å•ä¸€å®šä¸å†å±žäºŽè¯¥å®¢æˆ·ï¼Œå¿…é¡»æ¸…ç©ºé‡é€‰
    if (changedFields.includes('customerId') && values.saleOrderId) {
      await formApi.setValues({ saleOrderId: undefined });
      return;
    }
    // é€‰äº†é”€å”®è®¢å•说明已中标;清空订单则退回待开标
    if (changedFields.includes('saleOrderId')) {
      if (values.saleOrderId && values.bidResult !== 1) {
        await formApi.setValues({ bidResult: 1 });
        return;
      }
      if (!values.saleOrderId && values.bidResult === 1) {
        await formApi.setValues({ bidResult: 0 });
        return;
      }
    }
    // æœªä¸­æ ‡ä¸å¯èƒ½æœ‰é”€å”®è®¢å•,清空订单避免后端校验失败
    if (changedFields.includes('bidResult') && values.bidResult === 2) {
      if (values.saleOrderId) {
        await formApi.setValues({ saleOrderId: undefined });
      }
      if (values.refundStatus === 3) {
        await formApi.setValues({ refundStatus: 0 });
      }
      return;
    }
    // å·²ä¸­æ ‡ä¸å­˜åœ¨é€€æ¬¾åœºæ™¯
    if (changedFields.includes('bidResult') && values.bidResult === 1) {
      if (values.refundStatus !== 3) {
        await formApi.setValues({
          refundStatus: 3,
          refundAmount: undefined,
          refundDate: undefined,
        });
      }
    }
  },
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = (await formApi.getValues()) as CrmBidBondApi.BidBond;
    // æœªé€€å›ž / æ— éœ€é€€å›žä¸å…è®¸å¸¦é€€æ¬¾é‡‘额与日期,避免后端状态与金额冲突校验
    if (
      data.refundStatus === 0 ||
      data.refundStatus === 3 ||
      data.refundStatus === undefined ||
      data.refundStatus === null
    ) {
      data.refundAmount = undefined;
      data.refundDate = undefined;
    }
    modalApi.lock();
    try {
      await (formData.value?.id
        ? updateBidBond({ ...data, id: formData.value.id })
        : createBidBond(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<{ id?: number }>();
    if (!data?.id) {
      formData.value = undefined;
      return;
    }
    modalApi.lock();
    loading.value = true;
    try {
      formData.value = await getBidBond(data.id);
      await formApi.setValues(formData.value);
    } finally {
      loading.value = false;
      modalApi.unlock();
    }
  },
});
defineExpose({ modalApi });
</script>
<template>
  <Modal :title="getTitle" class="w-3/4">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/srm/bidBond/data.ts
ÎļþÒÑɾ³ý
src/views/srm/bidBond/modules/form.vue
ÎļþÒÑɾ³ý