liu
2026-09-29 7a6351a1fa5fc6e67b55c475bcd253d6d3e5d68f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
<script lang="ts" setup>
import type { FormType } from '../data';
import type { FinanceReimbursementApi } from '#/api/finance/reimbursement';
 
import { computed, ref } from 'vue';
 
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
 
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
 
import { useVbenForm } from '#/adapter/form';
import {
  createReimbursement,
  getReimbursement,
  updateReimbursement,
} from '#/api/finance/reimbursement';
 
import { toSubmitItems, useFormSchema } from '../data';
import ItemForm from './item-form.vue';
 
const emit = defineEmits(['success']);
 
const formData = ref<FinanceReimbursementApi.Reimbursement>({
  title: '',
  totalAmount: 0,
  applyDate: '',
  remark: '',
  items: [],
});
 
const formType = ref<FormType>('create'); // 表单类型:'create' | 'edit' | 'detail'
const itemFormRef = ref<InstanceType<typeof ItemForm>>();
 
/**
 * 明细表格的回显数据源:只在打开弹窗时赋值(编辑回显 / 新增置空)。
 * 用户在表格里的增删改只通过 emit 单向流到表单值,绝不回写到这里,
 * 否则 prop 变化会让表格反复重载(且与子组件 watcher 形成死循环)。
 */
const initialItems = ref<FinanceReimbursementApi.ReimbursementItem[]>([]);
 
const getTitle = computed(() => {
  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({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
 
/** 同步明细行到表单值,保证随主表一次性提交 */
function handleUpdateItems(items: FinanceReimbursementApi.ReimbursementItem[]) {
  // 第二个参数为 false:跳过合并算法,否则数组类型的值会被过滤掉
  formApi.setValues({ items }, false);
}
 
/** 明细金额变化时回填主表报销总金额(用户仍可手动修改) */
function handleUpdateTotalAmount(totalAmount: number) {
  formApi.setValues({ totalAmount }, false);
}
 
/** 创建或更新报销单 */
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const itemFormInstance = Array.isArray(itemFormRef.value)
      ? itemFormRef.value[0]
      : itemFormRef.value;
    try {
      itemFormInstance?.validate();
    } catch (error: any) {
      message.error(error.message || '报销明细验证失败');
      return;
    }
 
    modalApi.lock();
    const data =
      (await formApi.getValues()) as FinanceReimbursementApi.Reimbursement;
    // 发票附件为全量替换语义:清空全部发票时必须显式传空数组,
    // 传 null 后端会视为「不动附件」,导致删除操作不生效
    data.blobIds = data.blobIds ?? [];
    try {
      await (formType.value === 'create'
        ? createReimbursement(data)
        : updateReimbursement(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = {} as FinanceReimbursementApi.Reimbursement;
      initialItems.value = [];
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      initialItems.value = [];
      // 新建时默认申请日期为今天
      await formApi.setValues(
        { applyDate: dayjs().format('YYYY-MM-DD') },
        false,
      );
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getReimbursement(data.id);
      // 明细表格的行 key 用数据库 id,故回显数据源保留原 id
      initialItems.value = formData.value.items ?? [];
      // 已有发票转成上传组件可识别的文件对象,提交时按 valueKey=id 提取为 blobId
      const blobIds = (formData.value.attachmentList ?? []).map((item) => ({
        uid: String(item.id),
        name: item.name || '',
        url: item.url || '',
        status: 'done',
        id: item.id,
      }));
      await formApi.setValues(
        {
          ...formData.value,
          // 表单里的 items 只用于提交,必须剥掉行 id(后端「先删后增」会与软删行主键冲突)
          items: toSubmitItems(formData.value.items),
          blobIds,
        },
        false,
      );
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
 
<template>
  <Modal
    :title="getTitle"
    class="w-3/4"
    :close-on-click-modal="false"
    :show-confirm-button="formType !== 'detail'"
  >
    <Form class="mx-3">
      <template #items>
        <ItemForm
          ref="itemFormRef"
          :items="initialItems"
          :disabled="formType === 'detail'"
          class="w-full"
          @update:items="handleUpdateItems"
          @update:total-amount="handleUpdateTotalAmount"
        />
      </template>
    </Form>
  </Modal>
</template>