|
|
@@ -161,42 +161,40 @@
|
|
|
<button type="button" class="btn btn-primary btn-sm" @click="openInvoicePicker">
|
|
|
从发票夹选择
|
|
|
</button>
|
|
|
- <div v-if="selectedInvoiceIds.length" class="invoice-picked">
|
|
|
+ <div v-if="selectedInvoicePicksSorted.length" class="invoice-picked">
|
|
|
<div
|
|
|
- v-for="inv in selectedInvoiceRows"
|
|
|
- :key="inv.id"
|
|
|
+ v-for="pick in selectedInvoicePicksSorted"
|
|
|
+ :key="pick.key"
|
|
|
class="invoice-picked__item"
|
|
|
>
|
|
|
- <span>¥{{ money(inv.amount) }} · {{ inv.remark || '发票' }}</span>
|
|
|
- <button type="button" class="del" @click="removeInvoice(inv.id)">×</button>
|
|
|
+ <span>¥{{ money(pick.amount) }} · {{ pick.fileName }}</span>
|
|
|
+ <button type="button" class="del" @click="removeInvoicePick(pick.key)">×</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <p class="form-tip">请先在「我的发票夹」上传发票;提交时按合计扣减余额。</p>
|
|
|
+ <p class="form-tip">请先在「我的发票夹」上传发票;提交时按合计扣减余额。选择对象为附件文件。</p>
|
|
|
</div>
|
|
|
|
|
|
<van-popup v-model:show="showInvoicePicker" position="bottom" round :style="{ height: '70%' }">
|
|
|
<div class="invoice-picker">
|
|
|
- <div class="invoice-picker__title">选择发票</div>
|
|
|
- <van-checkbox-group v-model="pickerCheckedIds">
|
|
|
- <van-cell-group>
|
|
|
- <van-cell
|
|
|
- v-for="inv in walletInvoices"
|
|
|
- :key="inv.id"
|
|
|
- clickable
|
|
|
- @click="togglePickerId(inv.id)"
|
|
|
- >
|
|
|
- <template #title>
|
|
|
- ¥{{ money(inv.amount) }} · {{ inv.remark || '发票' }}
|
|
|
- </template>
|
|
|
- <template #right-icon>
|
|
|
- <van-checkbox :name="String(inv.id)" @click.stop />
|
|
|
- </template>
|
|
|
- </van-cell>
|
|
|
- </van-cell-group>
|
|
|
- </van-checkbox-group>
|
|
|
+ <div class="invoice-picker__title">选择发票附件</div>
|
|
|
+ <van-cell-group>
|
|
|
+ <van-cell
|
|
|
+ v-for="item in attachItems"
|
|
|
+ :key="item.key"
|
|
|
+ clickable
|
|
|
+ @click="togglePickerKey(item.key)"
|
|
|
+ >
|
|
|
+ <template #title>
|
|
|
+ ¥{{ money(item.amount) }} · {{ item.fileName }}
|
|
|
+ </template>
|
|
|
+ <template #right-icon>
|
|
|
+ <span v-if="pickerDraftKeys.includes(item.key)">✓</span>
|
|
|
+ </template>
|
|
|
+ </van-cell>
|
|
|
+ </van-cell-group>
|
|
|
<div class="invoice-picker__footer">
|
|
|
<button type="button" class="btn btn-primary btn-block" @click="confirmInvoicePicker">
|
|
|
- 确定(已选 {{ pickerCheckedIds.length }})
|
|
|
+ 确定(已选 {{ pickerDraftKeys.length }})
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -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<any[]>([])
|
|
|
-const selectedInvoiceIds = ref<string[]>([])
|
|
|
+const selectedInvoicePicks = ref<InvoiceAttachPick[]>([])
|
|
|
+const pickerDraftKeys = ref<string[]>([])
|
|
|
+const attachItems = ref<InvoiceAttachPick[]>([])
|
|
|
const showInvoicePicker = ref(false)
|
|
|
-const pickerCheckedIds = ref<string[]>([])
|
|
|
|
|
|
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<string, InvoiceAttachPick[]>()
|
|
|
+ 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
|