feat(web-admin): add receive account snapshot value helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-30 13:46:22 +08:00
1 parent 947e6ae6f8
commit d946b91dd1
2 files changed
+187

No files matched your search

@@ -0,0 +1,68 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
ACCOUNT_TYPE_LABEL,
formatReceiveAccountDisplay,
formatReceiveAccountLine,
isReceiveAccountSnapshot,
normalizeReceiveAccountModelValue,
type ReceiveAccountSnapshot
} from './oa-receive-account-value'
const alipay: ReceiveAccountSnapshot = {
id: '1',
accountType: 'alipay',
accountName: '张三',
accountNo: '13800138000',
bankName: '',
bankProvince: '',
bankCity: '',
bankBranch: '',
remark: '日常'
}
const personal: ReceiveAccountSnapshot = {
id: '2',
accountType: 'personal',
accountName: '李四',
accountNo: '6222',
bankName: '工行',
bankProvince: '安徽省',
bankCity: '合肥市',
bankBranch: '长江中路支行',
remark: ''
}
describe('oa-receive-account-value', () => {
it('type labels', () => {
assert.equal(ACCOUNT_TYPE_LABEL.alipay, '支付宝')
assert.equal(ACCOUNT_TYPE_LABEL.personal, '个人银行卡')
assert.equal(ACCOUNT_TYPE_LABEL.corporate, '对公银行账户')
})
it('isReceiveAccountSnapshot', () => {
assert.equal(isReceiveAccountSnapshot(alipay), true)
assert.equal(isReceiveAccountSnapshot({ id: '1' }), false)
})
it('normalize always array', () => {
assert.deepEqual(normalizeReceiveAccountModelValue(null), [])
assert.deepEqual(normalizeReceiveAccountModelValue(alipay), [alipay])
assert.deepEqual(normalizeReceiveAccountModelValue([alipay, personal]), [alipay, personal])
})
it('format line alipay omits bank', () => {
const line = formatReceiveAccountLine(alipay)
assert.ok(line.includes('支付宝'))
assert.ok(line.includes('张三'))
assert.ok(!line.includes('工行'))
assert.ok(line.includes('日常'))
})
it('format line personal full', () => {
const line = formatReceiveAccountLine(personal)
assert.ok(line.includes('个人银行卡'))
assert.ok(line.includes('安徽省 / 合肥市'))
assert.ok(line.includes('长江中路支行'))
})
it('format display multiline', () => {
const text = formatReceiveAccountDisplay([alipay, personal])
assert.ok(text.includes('\n'))
})
})
@@ -0,0 +1,119 @@
export type ReceiveAccountType = 'alipay' | 'personal' | 'corporate'
export type ReceiveAccountSnapshot = {
id: string
accountType: ReceiveAccountType
accountName: string
accountNo: string
bankName: string
bankProvince: string
bankCity: string
bankBranch: string
remark: string
}
export const ACCOUNT_TYPE_LABEL: Record<ReceiveAccountType, string> = {
alipay: '支付宝',
personal: '个人银行卡',
corporate: '对公银行账户'
}
const SNAPSHOT_FIELDS: (keyof ReceiveAccountSnapshot)[] = [
'id',
'accountType',
'accountName',
'accountNo',
'bankName',
'bankProvince',
'bankCity',
'bankBranch',
'remark'
]
function isReceiveAccountType(v: unknown): v is ReceiveAccountType {
return v === 'alipay' || v === 'personal' || v === 'corporate'
}
export function isReceiveAccountSnapshot(v: unknown): v is ReceiveAccountSnapshot {
if (!v || typeof v !== 'object') return false
const o = v as Record<string, unknown>
if (!isReceiveAccountType(o.accountType)) return false
for (const key of SNAPSHOT_FIELDS) {
if (typeof o[key] !== 'string') return false
}
return String(o.id || '').trim() !== ''
}
function trimSnapshot(v: ReceiveAccountSnapshot): ReceiveAccountSnapshot {
return {
id: String(v.id).trim(),
accountType: v.accountType,
accountName: String(v.accountName).trim(),
accountNo: String(v.accountNo).trim(),
bankName: String(v.bankName).trim(),
bankProvince: String(v.bankProvince).trim(),
bankCity: String(v.bankCity).trim(),
bankBranch: String(v.bankBranch).trim(),
remark: String(v.remark).trim()
}
}
export function normalizeReceiveAccountModelValue(
raw: unknown
): ReceiveAccountSnapshot[] {
if (Array.isArray(raw)) {
return raw.filter(isReceiveAccountSnapshot).map(trimSnapshot)
}
if (isReceiveAccountSnapshot(raw)) {
return [trimSnapshot(raw)]
}
return []
}
export function formatReceiveAccountLine(item: ReceiveAccountSnapshot): string {
const parts: string[] = [
ACCOUNT_TYPE_LABEL[item.accountType],
item.accountName,
item.accountNo
]
if (item.accountType === 'personal' || item.accountType === 'corporate') {
if (item.bankName) parts.push(item.bankName)
if (item.bankProvince || item.bankCity) {
parts.push(`${item.bankProvince} / ${item.bankCity}`)
}
if (item.bankBranch) parts.push(item.bankBranch)
}
if (item.remark) parts.push(item.remark)
return parts.filter(Boolean).join(' · ')
}
export function formatReceiveAccountDisplay(raw: unknown): string {
return normalizeReceiveAccountModelValue(raw)
.map(formatReceiveAccountLine)
.join('\n')
}
export function optionLabelFromRow(row: ReceiveAccountSnapshot): string {
return [
ACCOUNT_TYPE_LABEL[row.accountType],
row.accountName,
row.accountNo
]
.filter(Boolean)
.join(' · ')
}
export function snapshotFromRow(row: any): ReceiveAccountSnapshot {
const accountType = row?.accountType
return {
id: String(row?.id ?? '').trim(),
accountType: isReceiveAccountType(accountType) ? accountType : 'personal',
accountName: String(row?.accountName ?? '').trim(),
accountNo: String(row?.accountNo ?? '').trim(),
bankName: String(row?.bankName ?? '').trim(),
bankProvince: String(row?.bankProvince ?? '').trim(),
bankCity: String(row?.bankCity ?? '').trim(),
bankBranch: String(row?.bankBranch ?? '').trim(),
remark: String(row?.remark ?? '').trim()
}
}