liu
2 天以前 fe2d6627b8f1002af7a31b08f6f43bec034d04ee
src/views/finance/reimbursement/modules/form.vue
@@ -1,11 +1,13 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { FinanceReimbursementApi } from '#/api/finance/reimbursement';
import type { SystemStorageApi } from '#/api/system/storage';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useUserStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
@@ -21,6 +23,14 @@
import ItemForm from './item-form.vue';
const emit = defineEmits(['success']);
const userStore = useUserStore();
/** 当前登录用户 ID(用于本人发起时回填申请人) */
const currentUserId = computed(() => {
  const id = Number(userStore.userInfo?.userId);
  return Number.isFinite(id) && id > 0 ? id : undefined;
});
const formData = ref<FinanceReimbursementApi.Reimbursement>({
  title: '',
@@ -74,6 +84,47 @@
  formApi.setValues({ totalAmount }, false);
}
/**
 * OCR 识别时上传的发票文件同步到「发票附件」,保证识别过的发票随单保存。
 * 按 blobId 去重,避免同一张发票被重复识别时出现重复附件。
 */
async function handleOcrBlobs(blobs: SystemStorageApi.StorageBlob[]) {
  if (blobs.length === 0) {
    return;
  }
  const values = await formApi.getValues();
  const existing = (values.blobIds ?? []) as Array<
    | number
    | { id?: number; name?: string; status?: string; uid?: string; url?: string }
  >;
  const existingFiles = existing.map((item) =>
    typeof item === 'number'
      ? { uid: String(item), status: 'done', id: item }
      : item,
  );
  const existingIds = new Set(
    existingFiles
      .map((item) => item.id)
      .filter((id): id is number => typeof id === 'number'),
  );
  const appended = blobs
    .filter((blob) => !existingIds.has(blob.id))
    .map((blob) => ({
      uid: String(blob.id),
      name: blob.name || blob.originalFilename || '',
      url: blob.url || blob.previewURL || '',
      status: 'done',
      id: blob.id,
    }));
  if (appended.length === 0) {
    return;
  }
  await formApi.setValues(
    { blobIds: [...existingFiles, ...appended] },
    false,
  );
}
/** 创建或更新报销单 */
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
@@ -91,12 +142,24 @@
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as FinanceReimbursementApi.Reimbursement;
    const data = (await formApi.getValues()) as FinanceReimbursementApi.Reimbursement & {
      proxyFlag?: boolean;
    };
    // 代他人发起必须选择实际申请人;本人发起时以当前登录用户为准
    if (data.proxyFlag) {
      if (!data.applicantId) {
        message.error('代他人发起时,请选择实际申请人');
        return;
      }
    } else {
      data.applicantId = currentUserId.value;
    }
    // proxyFlag 仅用于前端交互,不提交给后端
    delete data.proxyFlag;
    // 发票附件为全量替换语义:清空全部发票时必须显式传空数组,
    // 传 null 后端会视为「不动附件」,导致删除操作不生效
    data.blobIds = data.blobIds ?? [];
    modalApi.lock();
    try {
      await (formType.value === 'create'
        ? createReimbursement(data)
@@ -120,9 +183,14 @@
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      initialItems.value = [];
      // 新建时默认申请日期为今天
      // 新建时默认本人发起:申请人展示当前登录用户,申请日期为今天
      await formApi.setValues(
        { applyDate: dayjs().format('YYYY-MM-DD') },
        {
          applicantId: currentUserId.value,
          applicantName: userStore.userInfo?.nickname ?? '',
          proxyFlag: false,
          applyDate: dayjs().format('YYYY-MM-DD'),
        },
        false,
      );
      return;
@@ -140,9 +208,15 @@
        status: 'done',
        id: item.id,
      }));
      // 实际申请人与当前登录用户不一致时,视为代他人发起,回显申请人选择器
      const isProxy =
        formType.value === 'edit' &&
        formData.value.applicantId != null &&
        formData.value.applicantId !== currentUserId.value;
      await formApi.setValues(
        {
          ...formData.value,
          proxyFlag: isProxy,
          // 表单里的 items 只用于提交,必须剥掉行 id(后端「先删后增」会与软删行主键冲突)
          items: toSubmitItems(formData.value.items),
          blobIds,
@@ -170,6 +244,7 @@
          :items="initialItems"
          :disabled="formType === 'detail'"
          class="w-full"
          @ocr-blobs="handleOcrBlobs"
          @update:items="handleUpdateItems"
          @update:total-amount="handleUpdateTotalAmount"
        />