diff --git a/bx-mobile/src/views/reimburse/ReimburseApplyPage.vue b/bx-mobile/src/views/reimburse/ReimburseApplyPage.vue index 0061ec877..fd986c95a 100644 --- a/bx-mobile/src/views/reimburse/ReimburseApplyPage.vue +++ b/bx-mobile/src/views/reimburse/ReimburseApplyPage.vue @@ -161,42 +161,40 @@ -
+
- ¥{{ money(inv.amount) }} · {{ inv.remark || '发票' }} - + ¥{{ money(pick.amount) }} · {{ pick.fileName }} +
-

请先在「我的发票夹」上传发票;提交时按合计扣减余额。

+

请先在「我的发票夹」上传发票;提交时按合计扣减余额。选择对象为附件文件。

-
选择发票
- - - - - - - - +
选择发票附件
+ + + + + +
@@ -296,7 +294,7 @@ import { ref, reactive, computed, watch, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { showToast, showDialog } from 'vant' -import { invoiceApi, invoiceWalletApi, reimburseApi } from '@/api' +import { attachmentApi, invoiceApi, invoiceWalletApi, reimburseApi } from '@/api' import { BizError } from '@/api/client' import { useExpenseItemTree, @@ -361,6 +359,7 @@ function onApplicantConfirm({ selectedOptions }: any) { const opt = selectedOptions?.[0] if (opt?.value) { form.applicantId = String(opt.value) + selectedInvoicePicks.value = [] refreshTitle() refreshWallet() } @@ -382,11 +381,23 @@ async function refreshTitle() { } } +type InvoiceAttachPick = { + key: string + invoiceId: string + attachmentId: string + amount: number | string + createTime: string + fileName: string + fileType: string + previewUrl: string +} + const walletBalance = ref(0) const walletInvoices = ref([]) -const selectedInvoiceIds = ref([]) +const selectedInvoicePicks = ref([]) +const pickerDraftKeys = ref([]) +const attachItems = ref([]) const showInvoicePicker = ref(false) -const pickerCheckedIds = ref([]) const detailTotalNumber = computed(() => form.details.reduce((sum: number, d) => sum + Number(d.amount || 0), 0), @@ -403,8 +414,37 @@ const balanceInsufficient = computed( () => detailTotalNumber.value > 0 && walletBalance.value < detailTotalNumber.value, ) -const selectedInvoiceRows = computed(() => - walletInvoices.value.filter((inv) => selectedInvoiceIds.value.includes(String(inv.id))), +const pickerMonthGroups = computed(() => { + const map = new Map() + for (const item of attachItems.value) { + const k = monthKey(item.createTime) + const list = map.get(k) || [] + list.push(item) + map.set(k, list) + } + return Array.from(map.entries()) + .sort((a, b) => b[0].localeCompare(a[0])) + .map(([key, items]) => { + const sorted = items + .slice() + .sort((a, b) => String(b.createTime).localeCompare(String(a.createTime))) + return { + monthKey: key, + monthLabel: monthLabel(key), + items: sorted.map((item, idx) => ({ ...item, serial: idx + 1 })), + } + }) +}) + +const selectedInvoicePicksSorted = computed(() => + selectedInvoicePicks.value + .slice() + .sort((a, b) => { + const ta = String(a.createTime || '') + const tb = String(b.createTime || '') + if (ta !== tb) return tb.localeCompare(ta) + return String(b.attachmentId).localeCompare(String(a.attachmentId)) + }), ) function money(v: unknown) { @@ -414,6 +454,89 @@ function money(v: unknown) { }) } +function isImageFileType(ft: string) { + return String(ft || '').startsWith('image/') +} + +function monthKey(raw: string) { + const text = String(raw || '').trim() + const m = text.match(/^(\d{4})[-/]?(\d{2})/) + if (m) return `${m[1]}-${m[2]}` + return 'unknown' +} + +function monthLabel(key: string) { + if (key === 'unknown') return '未知月份' + const [y, m] = key.split('-') + return `${y}年${Number(m)}月` +} + +function invoiceFileList(inv: any): Array<{ id: string; fileName: string; fileType: string }> { + const raw = Array.isArray(inv?.attachments) ? inv.attachments : [] + if (raw.length) { + return raw.map((a: any) => ({ + id: String(a.id || a.attachmentId), + fileName: a.fileName || a.file_name || '发票附件', + fileType: a.fileType || a.file_type || '', + })) + } + const aid = inv?.attachmentId || inv?.attachment_id + if (aid) { + return [ + { + id: String(aid), + fileName: inv.fileName || inv.file_name || '发票附件', + fileType: inv.fileType || inv.file_type || '', + }, + ] + } + return [] +} + +function buildAttachItems(rows: any[]): InvoiceAttachPick[] { + const items: InvoiceAttachPick[] = [] + for (const row of rows || []) { + const createTime = row.createTime || row.create_time || '' + for (const file of invoiceFileList(row)) { + if (!file.id) continue + items.push({ + key: `${row.id}:${file.id}`, + invoiceId: String(row.id), + attachmentId: file.id, + amount: row.amount, + createTime, + fileName: file.fileName, + fileType: file.fileType, + previewUrl: '', + }) + } + } + return items +} + +function buildInvoicePayload() { + const invoiceLinks = selectedInvoicePicks.value.map((p) => ({ + invoiceId: String(p.invoiceId), + attachmentId: String(p.attachmentId), + })) + const invoiceIds = Array.from(new Set(invoiceLinks.map((p) => p.invoiceId))) + return { invoiceIds, invoiceLinks } +} + +function kickAttachPreviews(items: InvoiceAttachPick[]) { + for (const item of items) { + if (!isImageFileType(item.fileType) || item.previewUrl) continue + attachmentApi + .preview(item.attachmentId) + .then((pRes: any) => { + item.previewUrl = pRes?.data?.url || '' + }) + .catch(() => { + /* optional */ + }) + } +} + async function refreshWallet() { try { const owner = form.applicantId || undefined @@ -424,37 +547,34 @@ async function refreshWallet() { walletBalance.value = Number(sumRes?.data?.balance ?? 0) const d = invRes?.data walletInvoices.value = d?.rows ?? d?.items ?? d?.list ?? (Array.isArray(d) ? d : []) + attachItems.value = buildAttachItems(walletInvoices.value) + kickAttachPreviews(attachItems.value) } catch { /* ignore */ } } function openInvoicePicker() { - pickerCheckedIds.value = [...selectedInvoiceIds.value] + pickerDraftKeys.value = selectedInvoicePicks.value.map((p) => p.key) showInvoicePicker.value = true } -function togglePickerId(id: string | number) { - const sid = String(id) - const i = pickerCheckedIds.value.indexOf(sid) - if (i >= 0) pickerCheckedIds.value.splice(i, 1) - else pickerCheckedIds.value.push(sid) +function togglePickerKey(key: string) { + const i = pickerDraftKeys.value.indexOf(key) + if (i >= 0) pickerDraftKeys.value.splice(i, 1) + else pickerDraftKeys.value.push(key) } function confirmInvoicePicker() { - selectedInvoiceIds.value = [...pickerCheckedIds.value] + const set = new Set(pickerDraftKeys.value) + selectedInvoicePicks.value = attachItems.value + .filter((i) => set.has(i.key)) + .map((i) => ({ ...i })) showInvoicePicker.value = false } -function removeInvoice(id: string | number) { - const sid = String(id) - selectedInvoiceIds.value = selectedInvoiceIds.value.filter((x) => x !== sid) -} - -function buildInvoicePayload() { - const invoiceIds = [...selectedInvoiceIds.value] - const invoiceLinks = invoiceIds.map((invoiceId) => ({ invoiceId })) - return { invoiceIds, invoiceLinks } +function removeInvoicePick(key: string) { + selectedInvoicePicks.value = selectedInvoicePicks.value.filter((p) => p.key !== key) } function lineExpenseDisplay(id: number | string): string { @@ -664,7 +784,7 @@ async function loadEditData() { const id = route.query.id if (!id) { editingId.value = null - selectedInvoiceIds.value = [] + selectedInvoicePicks.value = [] return } editingId.value = String(id) @@ -685,8 +805,38 @@ async function loadEditData() { form.details = [{ expenseItemId: '', amount: '', remark: '' }] } const linked = data.invoices || [] - selectedInvoiceIds.value = linked.map((x: any) => String(x.id || x.invoiceId)).filter(Boolean) await refreshWallet() + const picks: InvoiceAttachPick[] = [] + for (const inv of linked) { + const invoiceId = String(inv.id || inv.invoiceId || '') + if (!invoiceId) continue + let attachmentId = String(inv.attachmentId || inv.attachment_id || '') + let fileName = inv.fileName || inv.file_name || '发票附件' + let fileType = inv.fileType || inv.file_type || '' + const createTime = inv.createTime || inv.create_time || '' + if (!attachmentId) { + const src = walletInvoices.value.find((w) => String(w.id) === invoiceId) + const first = src ? invoiceFileList(src)[0] : undefined + if (first) { + attachmentId = first.id + fileName = first.fileName + fileType = first.fileType + } + } + if (!attachmentId) continue + picks.push({ + key: `${invoiceId}:${attachmentId}`, + invoiceId, + attachmentId, + amount: inv.amount, + createTime, + fileName, + fileType, + previewUrl: '', + }) + } + selectedInvoicePicks.value = picks + kickAttachPreviews(selectedInvoicePicks.value) } catch (e: any) { showToast(e?.message || '加载数据失败') } @@ -697,7 +847,7 @@ watch( async (id, prev) => { if (!id) { editingId.value = null - selectedInvoiceIds.value = [] + selectedInvoicePicks.value = [] return } if (String(id) === String(prev ?? '')) return