Explorar o código

feat(miniprogram): add invoice thumb helper utilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong hai 1 mes
pai
achega
4201d656b1

+ 53 - 0
miniprogram/scripts/test-bx-invoice-thumb.mjs

@@ -0,0 +1,53 @@
+import assert from 'node:assert/strict'
+import {
+  isImageFileType,
+  fileExtLabel,
+  formatInvoiceDateYmd,
+  buildAttachItemsFromInvoiceRows,
+  mergeInvoicePicksByDraftKeys
+} from '../utils/bxInvoiceThumb.js'
+
+assert.equal(isImageFileType('image/jpeg'), true)
+assert.equal(isImageFileType('application/pdf'), false)
+assert.equal(fileExtLabel('a.PDF'), 'PDF')
+assert.equal(formatInvoiceDateYmd('2026-09-16T12:00:00'), '2026-09-16')
+assert.equal(formatInvoiceDateYmd(''), '—')
+
+const rows = [
+  {
+    id: 'inv1',
+    amount: 100,
+    createTime: '2026-09-01T10:00:00',
+    attachments: [
+      { id: 'a1', fileName: 'x.jpg', fileType: 'image/jpeg' },
+      { id: 'a2', fileName: 'y.pdf', fileType: 'application/pdf' }
+    ]
+  }
+]
+const items = buildAttachItemsFromInvoiceRows(rows)
+assert.equal(items.length, 2)
+assert.equal(items[0].key, 'inv1:a1')
+assert.equal(items[1].fileName, 'y.pdf')
+
+const prev = [
+  {
+    key: 'inv9:a9',
+    invoiceId: 'inv9',
+    attachmentId: 'a9',
+    amount: 9,
+    createTime: '2026-08-01',
+    fileName: 'old.jpg',
+    fileType: 'image/jpeg',
+    previewUrl: 'http://old'
+  }
+]
+const merged = mergeInvoicePicksByDraftKeys(
+  ['inv9:a9', 'inv1:a1'],
+  items,
+  prev
+)
+assert.equal(merged.length, 2)
+assert.ok(merged.some((p) => p.key === 'inv9:a9' && p.previewUrl === 'http://old'))
+assert.ok(merged.some((p) => p.key === 'inv1:a1'))
+
+console.log('bxInvoiceThumb ok')

+ 94 - 0
miniprogram/utils/bxInvoiceThumb.js

@@ -0,0 +1,94 @@
+export function isImageFileType(fileType) {
+  return !!(fileType && String(fileType).startsWith('image/'))
+}
+
+export function fileExtLabel(name) {
+  const ext = String(name || '').split('.').pop()
+  return ext ? ext.toUpperCase() : 'FILE'
+}
+
+/** 展示用 YYYY-MM-DD;无有效日期返回 — */
+export function formatInvoiceDateYmd(value) {
+  if (value == null || value === '') return '—'
+  const s = String(value).trim()
+  const m = s.match(/^(\d{4}-\d{2}-\d{2})/)
+  if (m) return m[1]
+  const d = new Date(s)
+  if (Number.isNaN(d.getTime())) return '—'
+  const y = d.getFullYear()
+  const mo = String(d.getMonth() + 1).padStart(2, '0')
+  const day = String(d.getDate()).padStart(2, '0')
+  return `${y}-${mo}-${day}`
+}
+
+function invoiceFileList(inv) {
+  const raw = Array.isArray(inv?.attachments) ? inv.attachments : []
+  if (raw.length) {
+    return raw.map((a) => ({
+      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 []
+}
+
+/**
+ * 发票列表行 → 附件粒度选项
+ * @returns {{ key, invoiceId, attachmentId, amount, createTime, fileName, fileType, previewUrl, previewLoading }[]}
+ */
+export function buildAttachItemsFromInvoiceRows(rows) {
+  const items = []
+  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: '',
+        previewLoading: false
+      })
+    }
+  }
+  return items
+}
+
+/**
+ * 确定勾选:已加载项优先;draft 中仍勾选但不在已加载列表的旧已选项保留(防分页冲掉)
+ */
+export function mergeInvoicePicksByDraftKeys(draftKeys, attachItems, previousPicks) {
+  const set = new Set(draftKeys || [])
+  const fromLoaded = (attachItems || []).filter((i) => set.has(i.key))
+  const loadedKeys = new Set(fromLoaded.map((i) => i.key))
+  const fromPrev = (previousPicks || []).filter(
+    (p) => set.has(p.key) && !loadedKeys.has(p.key)
+  )
+  const byKey = new Map()
+  for (const p of fromPrev) byKey.set(p.key, { ...p })
+  for (const i of fromLoaded) {
+    const prev = byKey.get(i.key)
+    byKey.set(i.key, {
+      ...i,
+      previewUrl: i.previewUrl || prev?.previewUrl || ''
+    })
+  }
+  return Array.from(set)
+    .map((k) => byKey.get(k))
+    .filter(Boolean)
+}