Sfoglia il codice sorgente

refactor(bx-mobile): use attachment-level invoice picks in reimburse apply logic

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 mesi fa
parent
commit
9f92012e43
1 ha cambiato i file con 199 aggiunte e 49 eliminazioni
  1. 199 49
      bx-mobile/src/views/reimburse/ReimburseApplyPage.vue

+ 199 - 49
bx-mobile/src/views/reimburse/ReimburseApplyPage.vue

@@ -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