云
2026-09-29 07aab8bb335132d5dc56f2b000301457079e8539
feat(research): 完善经费拨付流程支持财务报销审批

- 添加报销审批中和已驳回状态定义
- 在表格中增加财务报销单号和驳回原因字段显示
- 移除拨付状态下拉选择,改为自动管理状态流转
- 强制凭证必传,确保报销流程完整性
- 更新保存成功提示信息,明确财务审批状态
- 添加驳回原因显示和重新发起报销功能
- 增加顶部状态提醒,展示审批进度信息
- 优化标签显示逻辑,统一凭证状态标识
- 修改API注释,明确状态码含义和操作说明
已修改4个文件
114 ■■■■■ 文件已修改
src/api/research/fund/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/research/application/data.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/research/application/modules/fund-form.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/research/application/modules/fund-panel.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/research/fund/index.ts
@@ -11,7 +11,11 @@
    amount: number; // 拨付金额(元)
    payDate?: string; // 拨付日期
    payee?: string; // 收款单位/账户名
    status?: number; // 拨付状态,字典 research_fund_status
    status?: number; // 拨付状态,字典 research_fund_status(0-待拨付 15-报销审批中 20-已拨付 30-已驳回)
    reimbursementId?: number; // 关联财务报销单 ID
    reimbursementNo?: string; // 关联财务报销单号
    rejectReason?: string; // 财务报销驳回原因(BPM 回写)
    rejectTime?: string; // 财务报销驳回时间
    remark?: string; // 备注
    creator?: string; // 创建人 ID
    creatorName?: string; // 创建人姓名
@@ -30,12 +34,12 @@
  );
}
/** 新增经费拨付记录 */
/** 登记经费拨付记录(登记后自动生成并提交财务报销审批) */
export function createFundRecord(data: ResearchFundRecordApi.FundRecord) {
  return requestClient.post('/research/fund-record/create', data);
}
/** 修改经费拨付记录(仅待拨付可改) */
/** 修改并重新发起报销(仅「待拨付」「已驳回」可改) */
export function updateFundRecord(data: ResearchFundRecordApi.FundRecord) {
  return requestClient.put('/research/fund-record/update', data);
}
src/views/research/application/data.ts
@@ -28,7 +28,9 @@
/** 经费拨付状态(对应字典 research_fund_status) */
export const FundStatus = {
  PENDING: 0, // 待拨付
  APPROVING: 15, // 报销审批中
  PAID: 20, // 已拨付
  REJECTED: 30, // 已驳回
} as const;
/** 课题申报书表单配置 */
@@ -516,13 +518,25 @@
    {
      field: 'status',
      title: '拨付状态',
      minWidth: 100,
      minWidth: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.RESEARCH_FUND_STATUS },
      },
    },
    {
      field: 'reimbursementNo',
      title: '财务报销单号',
      minWidth: 160,
      formatter: dashIfEmpty,
    },
    {
      field: 'rejectReason',
      title: '驳回原因',
      minWidth: 200,
      formatter: dashIfEmpty,
    },
    {
      field: 'hasAttachment',
      title: '拨付凭证',
      minWidth: 100,
src/views/research/application/modules/fund-form.vue
@@ -3,9 +3,6 @@
import { computed, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useVbenModal } from '@vben/common-ui';
import { Alert, message } from 'ant-design-vue';
@@ -22,6 +19,8 @@
const isEdit = ref(false);
/** 申请经费减去已登记拨付金额后的可用余额 */
const remainder = ref<number>();
/** 编辑「已驳回」记录时展示的财务驳回原因 */
const rejectReason = ref('');
const alertMessage = computed(() => {
  if (remainder.value === undefined) {
@@ -98,17 +97,6 @@
      },
    },
    {
      fieldName: 'status',
      label: '拨付状态',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.RESEARCH_FUND_STATUS, 'number'),
        placeholder: '请选择拨付状态',
      },
      rules: 'selectRequired',
      help: '选择「已拨付」时必须上传拨付凭证附件',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
@@ -128,6 +116,8 @@
        multiple: true,
        showDescription: true,
      },
      rules: 'required',
      help: '必填:凭证将随自动生成的财务报销单一起送交财务审批',
      formItemClass: 'col-span-2',
    },
  ],
@@ -142,12 +132,9 @@
    }
    const values =
      (await formApi.getValues()) as ResearchFundRecordApi.FundRecord;
    // 「已拨付」必须有凭证,先在前端拦一道,避免用户白等一次请求
    if (
      values.status === FundStatus.PAID &&
      (!values.blobIds || values.blobIds.length === 0)
    ) {
      message.warning('拨付状态为「已拨付」时,请先上传拨付凭证附件');
    // 凭证必填,先在前端拦一道,避免用户白等一次请求
    if (!values.blobIds || values.blobIds.length === 0) {
      message.warning('请先上传本次拨付凭证,凭证将随财务报销单送交财务审批');
      return;
    }
    modalApi.lock();
@@ -159,15 +146,16 @@
      amount: values.amount,
      payDate: values.payDate,
      payee: values.payee,
      status: values.status,
      remark: values.remark,
      blobIds: values.blobIds ?? [],
      blobIds: values.blobIds,
    } as ResearchFundRecordApi.FundRecord;
    try {
      await (data.id ? updateFundRecord(data) : createFundRecord(data));
      await modalApi.close();
      emit('success');
      message.success('保存成功');
      message.success(
        data.id ? '已重新发起财务报销审批' : '登记成功,已自动提交财务报销审批',
      );
    } finally {
      modalApi.unlock();
    }
@@ -176,6 +164,7 @@
    if (!isOpen) {
      isEdit.value = false;
      remainder.value = undefined;
      rejectReason.value = '';
      return;
    }
    const data = modalApi.getData<{
@@ -185,6 +174,10 @@
    }>();
    isEdit.value = !!data.record;
    remainder.value = data.remainder;
    rejectReason.value =
      data.record?.status === FundStatus.REJECTED
        ? data.record.rejectReason || '财务未填写驳回原因'
        : '';
    await formApi.resetForm();
    if (data.record) {
      const blobIds = (data.record.attachmentList ?? []).map((item) => ({
@@ -206,7 +199,6 @@
      await formApi.setValues(
        {
          applicationId: data.applicationId,
          status: FundStatus.PENDING,
        },
        false,
      );
@@ -217,11 +209,18 @@
<template>
  <Modal
    :title="isEdit ? '编辑拨款记录' : '登记拨款记录'"
    :title="isEdit ? '修改并重新发起报销' : '登记拨款(登记后自动提交财务报销审批)'"
    class="w-1/2"
    :close-on-click-modal="false"
  >
    <Alert
      v-if="rejectReason"
      class="mx-3 mb-3"
      type="error"
      show-icon
      :message="`财务报销已驳回:${rejectReason}`"
    />
    <Alert
      v-if="alertMessage"
      class="mx-3 mb-3"
      type="info"
src/views/research/application/modules/fund-panel.vue
@@ -39,6 +39,10 @@
  () => (Number(application.value?.budgetAmount) || 0) - registeredAmount.value,
);
/** 各状态记录数量,用于顶部提示(0 条时不提示) */
const statusCount = (status: number) =>
  records.value.filter((item) => item.status === status).length;
const [FundFormModal, fundFormModalApi] = useVbenModal({
  connectedComponent: FundForm,
  destroyOnClose: true,
@@ -176,6 +180,25 @@
        </div>
      </div>
      <Alert
        v-if="statusCount(FundStatus.APPROVING) > 0"
        type="info"
        show-icon
        :message="`有 ${statusCount(FundStatus.APPROVING)} 条拨付记录正在报销审批中,暂不可修改;财务审批结果将自动回写本记录`"
      />
      <Alert
        v-if="statusCount(FundStatus.PAID) > 0"
        type="success"
        show-icon
        :message="`有 ${statusCount(FundStatus.PAID)} 条拨付记录财务已通过,拨款完成`"
      />
      <Alert
        v-if="statusCount(FundStatus.REJECTED) > 0"
        type="error"
        show-icon
        :message="`有 ${statusCount(FundStatus.REJECTED)} 条拨付记录财务已驳回,请修改金额或凭证后点「重新发起报销」`"
      />
      <div v-if="editable">
        <TableAction
          :actions="[
@@ -193,13 +216,7 @@
      <Grid>
        <template #hasAttachment="{ row }">
          <Tag :color="row.hasAttachment ? 'success' : 'warning'">
            {{
              row.hasAttachment
                ? '已上传'
                : row.status === FundStatus.PAID
                  ? '缺失'
                  : '不需要'
            }}
            {{ row.hasAttachment ? '已上传' : '缺失' }}
          </Tag>
        </template>
        <template #actions="{ row }">
@@ -215,6 +232,14 @@
                onClick: handleEdit.bind(null, row),
              },
              {
                label: '重新发起报销',
                type: 'link',
                icon: ACTION_ICON.REFRESH,
                auth: ['research:fund:update'],
                ifShow: () => row.status === FundStatus.REJECTED,
                onClick: handleEdit.bind(null, row),
              },
              {
                label: '删除',
                type: 'link',
                danger: true,