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 }}
+
-
请先在「我的发票夹」上传发票;提交时按合计扣减余额。
+
请先在「我的发票夹」上传发票;提交时按合计扣减余额。选择对象为附件文件。
-
选择发票
-
-
-
-
- ¥{{ money(inv.amount) }} · {{ inv.remark || '发票' }}
-
-
-
-
-
-
-
+
选择发票附件
+
+
+
+ ¥{{ money(item.amount) }} · {{ item.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