费用优化
This commit is contained in:
@@ -6,7 +6,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { listPlatformTenants } from '@/modules/iam'
|
||||
import * as api from '../api/finance'
|
||||
import type { FinanceAccountSummary, FinanceRechargeRequest } from '../types'
|
||||
import type { FinanceAccountSummary, FinanceLedgerEntry, FinanceRechargeRequest } from '../types'
|
||||
import FinanceCustomerAccountsView from './FinanceCustomerAccountsView.vue'
|
||||
|
||||
const { default: ElementPlus, ElDialog } = createRequire(import.meta.url)('element-plus') as typeof import('element-plus')
|
||||
@@ -17,7 +17,7 @@ vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
||||
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
|
||||
vi.mock('../api/finance', () => ({
|
||||
listFinanceAccounts: vi.fn(), listFinanceAccountLedger: vi.fn(), listFinanceRechargeRequests: vi.fn(),
|
||||
reviewFinanceRechargeRequest: vi.fn(), adjustFinanceAccount: vi.fn(), updateFinanceCreditLimit: vi.fn(),
|
||||
reviewFinanceRechargeRequest: vi.fn(), adjustFinanceAccount: vi.fn(), refundFinanceConsumption: vi.fn(), updateFinanceCreditLimit: vi.fn(),
|
||||
downloadFinanceRechargeVoucher: vi.fn(), getFinanceCustomerTerms: vi.fn(), updateFinanceCustomerTerms: vi.fn(),
|
||||
}))
|
||||
const account: FinanceAccountSummary = {
|
||||
@@ -287,3 +287,105 @@ describe('customer wallet native dialogs', () => {
|
||||
expect(activeDialog(wrapper).find('.voucher-preview__image').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
const consumption: FinanceLedgerEntry = {
|
||||
ledgerId: '61', businessNo: 'AC-CONSUME-61', changeType: 'CONSUMPTION', amount: '-80.00',
|
||||
sourceNo: 'RELABEL-1', occurredAt: '', businessType: 'ADJUSTMENT', documentType: '', documentId: '', documentNo: '',
|
||||
receivableChange: 0, payableChange: 0, outstandingReceivableAfter: 0, operatorName: '', remark: '',
|
||||
}
|
||||
async function openRefundDialog() {
|
||||
vi.mocked(api.listFinanceAccountLedger).mockResolvedValue({ records: [consumption], total: 1, pages: 1 })
|
||||
const wrapper = await mountPage()
|
||||
await click(wrapper, '退款', '.ledger-table button')
|
||||
return { wrapper, dialog: activeDialog(wrapper) }
|
||||
}
|
||||
describe('linked original consumption refunds', () => {
|
||||
it.each(['', '0', '-1', '80.01', '1.001', 'NaN'])('rejects invalid refund %s', async amount => {
|
||||
const { dialog } = await openRefundDialog()
|
||||
await dialog.get('input[placeholder="请输入退款金额"]').setValue(amount)
|
||||
await dialog.get('textarea').setValue('测试退款')
|
||||
await click(dialog, '确认退款')
|
||||
expect(api.refundFinanceConsumption).not.toHaveBeenCalled()
|
||||
})
|
||||
it('requires a reason and keeps the original consumption visible', async () => {
|
||||
const { dialog } = await openRefundDialog()
|
||||
expect(dialog.text()).toContain('AC-CONSUME-61')
|
||||
expect(dialog.text()).toContain('RELABEL-1')
|
||||
await dialog.get('input[placeholder="请输入退款金额"]').setValue('10')
|
||||
await click(dialog, '确认退款')
|
||||
expect(api.refundFinanceConsumption).not.toHaveBeenCalled()
|
||||
})
|
||||
it('locks a pending refund, preserves failures, and retries the identical payload with the same key', async () => {
|
||||
const pending = deferred<Record<string, unknown>>()
|
||||
vi.mocked(api.refundFinanceConsumption).mockReturnValueOnce(pending.promise).mockResolvedValueOnce({})
|
||||
const { wrapper, dialog } = await openRefundDialog()
|
||||
await dialog.get('input[placeholder="请输入退款金额"]').setValue('12.3')
|
||||
await dialog.get('textarea').setValue(' 原单多扣 ')
|
||||
await click(dialog, '确认退款')
|
||||
expect(dialog.props()).toMatchObject({ showClose: false, closeOnPressEscape: false, closeOnClickModal: false })
|
||||
expect(dialog.get('textarea').attributes('disabled')).toBeDefined()
|
||||
await click(dialog, '取消')
|
||||
expect(dialog.props('modelValue')).toBe(true)
|
||||
pending.reject(failure())
|
||||
await flushPromises()
|
||||
expect(dialog.text()).toContain('测试操作未成功')
|
||||
const first = vi.mocked(api.refundFinanceConsumption).mock.calls[0]!
|
||||
expect(first).toEqual([account.customerId, '61', { amount: '12.30', reason: '原单多扣' }, expect.stringMatching(/^finance-refund-/)])
|
||||
await click(dialog, '确认退款')
|
||||
expect(vi.mocked(api.refundFinanceConsumption).mock.calls[1]).toEqual(first)
|
||||
expect(api.listFinanceAccountLedger).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.text()).toContain('账户流水')
|
||||
})
|
||||
it('creates a new key for changed amount after a rejected request', async () => {
|
||||
vi.mocked(api.refundFinanceConsumption).mockRejectedValue(failure())
|
||||
const { dialog } = await openRefundDialog()
|
||||
await dialog.get('input[placeholder="请输入退款金额"]').setValue('10')
|
||||
await dialog.get('textarea').setValue('退款')
|
||||
await click(dialog, '确认退款')
|
||||
await dialog.get('input[placeholder="请输入退款金额"]').setValue('11')
|
||||
await click(dialog, '确认退款')
|
||||
const calls = vi.mocked(api.refundFinanceConsumption).mock.calls
|
||||
expect(calls[0]![3]).not.toBe(calls[1]![3])
|
||||
})
|
||||
it('hides refunds without adjustment permission and labels refund original references', async () => {
|
||||
can.mockImplementation(permission => permission !== 'platform:finance:customer-account:adjust')
|
||||
vi.mocked(api.listFinanceAccountLedger).mockResolvedValue({ records: [consumption, { ...consumption, ledgerId: '62', changeType: 'REFUND', amount: '10.00', referenceNo: 'AC-CONSUME-61' }], total: 2, pages: 1 })
|
||||
const wrapper = await mountPage()
|
||||
expect(wrapper.findAll('.ledger-table button').some(button => button.text() === '退款')).toBe(false)
|
||||
expect(wrapper.text()).toContain('消费退款')
|
||||
expect(wrapper.text()).toContain('原消费:')
|
||||
})
|
||||
})
|
||||
|
||||
describe('customer wallet detail error recovery', () => {
|
||||
it.each(['ACCOUNT', 'LEDGER'] as const)('reloads the current detail after %s fails without same-route navigation', async failurePoint => {
|
||||
if (failurePoint === 'ACCOUNT') vi.mocked(api.listFinanceAccounts).mockRejectedValueOnce(failure())
|
||||
else vi.mocked(api.listFinanceAccountLedger).mockRejectedValueOnce(failure())
|
||||
const wrapper = await mountPage()
|
||||
expect(wrapper.get('.load-error-state').text()).toContain('测试操作未成功')
|
||||
const pending = deferred<Awaited<ReturnType<typeof api.listFinanceAccounts>>>()
|
||||
vi.mocked(api.listFinanceAccounts).mockReturnValueOnce(pending.promise)
|
||||
await click(wrapper, '重新加载', '.load-error-state button')
|
||||
expect(api.listFinanceAccounts).toHaveBeenCalledTimes(2)
|
||||
pending.resolve({ records: [{ ...account, actualBalance: '90.00' }], total: 1, pages: 1 })
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.load-error-state').exists()).toBe(false)
|
||||
expect(wrapper.get('[aria-label="客户资金账户余额"]').text()).toContain('¥90.00')
|
||||
expect(wrapper.find('.ledger-table').exists()).toBe(true)
|
||||
expect(api.listFinanceAccountLedger).toHaveBeenCalledTimes(failurePoint === 'ACCOUNT' ? 1 : 2)
|
||||
expect(api.listFinanceRechargeRequests).toHaveBeenCalledTimes(failurePoint === 'ACCOUNT' ? 1 : 2)
|
||||
})
|
||||
})
|
||||
|
||||
it('enforces the refund contract reason limit before submitting', async () => {
|
||||
const { wrapper, dialog } = await openRefundDialog()
|
||||
expect(dialog.get('textarea').attributes('maxlength')).toBe('500')
|
||||
await dialog.get('input[placeholder="请输入退款金额"]').setValue('10')
|
||||
const vm = wrapper.vm as unknown as { refundForm: { reason: string } }
|
||||
vm.refundForm.reason = '退'.repeat(501)
|
||||
await click(dialog, '确认退款')
|
||||
expect(api.refundFinanceConsumption).not.toHaveBeenCalled()
|
||||
vm.refundForm.reason = '退'.repeat(500)
|
||||
await click(dialog, '确认退款')
|
||||
expect(api.refundFinanceConsumption).toHaveBeenCalledWith(account.customerId, consumption.ledgerId, { amount: '10.00', reason: '退'.repeat(500) }, expect.any(String))
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ vi.mock('../api/finance', () => ({
|
||||
listFinanceAccountLedger: vi.fn(),
|
||||
listFinanceRechargeRequests: vi.fn(),
|
||||
reviewFinanceRechargeRequest: vi.fn(),
|
||||
adjustFinanceAccount: vi.fn(),
|
||||
adjustFinanceAccount: vi.fn(), refundFinanceConsumption: vi.fn(),
|
||||
updateFinanceCreditLimit: vi.fn(),
|
||||
downloadFinanceRechargeVoucher: vi.fn(),
|
||||
getFinanceCustomerTerms: vi.fn(),
|
||||
|
||||
@@ -22,7 +22,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
|
||||
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
||||
import { adjustFinanceAccount, downloadFinanceRechargeVoucher, getFinanceCustomerTerms, listFinanceAccountLedger, listFinanceAccounts, listFinanceRechargeRequests, reviewFinanceRechargeRequest, updateFinanceCreditLimit, updateFinanceCustomerTerms } from '../api/finance'
|
||||
import { refundFinanceConsumption, adjustFinanceAccount, downloadFinanceRechargeVoucher, getFinanceCustomerTerms, listFinanceAccountLedger, listFinanceAccounts, listFinanceRechargeRequests, reviewFinanceRechargeRequest, updateFinanceCreditLimit, updateFinanceCustomerTerms } from '../api/finance'
|
||||
import { financeBusinessTypeLabel } from '../status'
|
||||
import type { FinanceAccountSummary, FinanceCustomerTerms, FinanceLedgerEntry, FinanceRechargeRequest } from '../types'
|
||||
|
||||
@@ -45,6 +45,7 @@ const saving = ref(false)
|
||||
const selected = ref<FinanceAccountSummary | null>(null)
|
||||
const ledger = ref<FinanceLedgerEntry[]>([])
|
||||
const ledgerLoading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const ledgerError = ref<ApiErrorDetails | null>(null)
|
||||
const selectedTerms = ref<FinanceCustomerTerms | null>(null)
|
||||
const creditDays = ref(30)
|
||||
@@ -57,6 +58,12 @@ const rechargeLoading = ref(false)
|
||||
const rechargeError = ref<ApiErrorDetails | null>(null)
|
||||
const accountExportBusy = ref(false)
|
||||
const ledgerExportBusy = ref(false)
|
||||
const refundOpen = ref(false)
|
||||
const refundSaving = ref(false)
|
||||
const refundSource = ref<{ customerId: string; entry: FinanceLedgerEntry } | null>(null)
|
||||
const refundForm = reactive({ amount: '', reason: '' })
|
||||
const refundError = ref<ApiErrorDetails | null>(null)
|
||||
let refundAttempt = { payload: '', key: '' }
|
||||
const adjustmentOpen = ref(false)
|
||||
const adjustmentSaving = ref(false)
|
||||
const adjustmentAccount = ref<FinanceAccountSummary | null>(null)
|
||||
@@ -114,6 +121,7 @@ const ledgerQueryFields: QueryField[] = [
|
||||
{ key: 'occurredRange', type: 'daterange', label: '发生日期', width: 280 },
|
||||
]
|
||||
const ledgerColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'ledgerId', label: '流水号', width: 180, slotName: 'ledgerId' }, { key: 'occurredAt', label: '发生时间', width: 170, slotName: 'occurredAt' }, { key: 'businessType', label: '业务类型', width: 140, slotName: 'businessType' },
|
||||
{ key: 'documentNo', label: '关联单据', width: 220, slotName: 'documentNo' }, { key: 'amount', label: '余额变动(CNY)', width: 145, slotName: 'amount' },
|
||||
{ key: 'paymentMethod', label: '支付方式', width: 130, slotName: 'ledgerPaymentMethod' },
|
||||
@@ -204,7 +212,15 @@ async function refreshWalletDetail() {
|
||||
if (!selected.value) return
|
||||
await Promise.all([loadRechargeRequests(), loadLedger()])
|
||||
}
|
||||
async function retryDrawer() { if (selected.value) await openAccount(selected.value as unknown as Record<string, unknown>, editing.value) }
|
||||
async function retryDrawer() {
|
||||
if (!canView.value || detailLoading.value) return
|
||||
if (editing.value) {
|
||||
if (selected.value) await openAccount(selected.value as unknown as Record<string, unknown>, true)
|
||||
return
|
||||
}
|
||||
const customerId = String(route.params.customerId || selected.value?.customerId || '')
|
||||
if (customerId) await restoreAccountDetail(customerId)
|
||||
}
|
||||
async function saveTerms() {
|
||||
if (!selected.value || !Number.isInteger(creditDays.value) || creditDays.value < 0 || creditDays.value > 3650) { ElMessage.warning('账期必须是 0 到 3650 的整数天'); return }
|
||||
saving.value = true
|
||||
@@ -237,7 +253,41 @@ function paymentMethodLabel(value: unknown) {
|
||||
return ({ BANK_TRANSFER: '银行转账', WECHAT: '微信', ALIPAY: '支付宝', CASH: '现金', OTHER: '其他' } as Record<string, string>)[String(value)] || '其他'
|
||||
}
|
||||
function walletBusinessType(value: unknown) {
|
||||
return ({ RECHARGE: '充值', DEDUCTION: '扣款', FREEZE: '冻结', UNFREEZE: '解冻', CONSUMPTION: '消费结算', CREDIT_LIMIT: '授信调整' } as Record<string, string>)[String(value)] || ledgerBusinessType(value)
|
||||
return ({ RECHARGE: '充值', DEDUCTION: '扣款', FREEZE: '冻结', UNFREEZE: '解冻', CONSUMPTION: '消费结算', REFUND: '消费退款', CREDIT_LIMIT: '授信调整' } as Record<string, string>)[String(value)] || ledgerBusinessType(value)
|
||||
}
|
||||
function canRefund(entry: FinanceLedgerEntry) {
|
||||
return canAdjust.value && entry.changeType === 'CONSUMPTION' && !!entry.ledgerId && Number(entry.amount) < 0
|
||||
}
|
||||
function openRefund(entry: FinanceLedgerEntry) {
|
||||
if (!selected.value || !canRefund(entry) || refundSaving.value) return
|
||||
refundSource.value = { customerId: selected.value.customerId, entry: { ...entry } }
|
||||
Object.assign(refundForm, { amount: '', reason: '' })
|
||||
refundError.value = null
|
||||
refundAttempt = { payload: '', key: '' }
|
||||
refundOpen.value = true
|
||||
}
|
||||
async function submitRefund() {
|
||||
const source = refundSource.value
|
||||
if (refundSaving.value || !source || !canRefund(source.entry) || selected.value?.customerId !== source.customerId) return
|
||||
const raw = refundForm.amount.trim()
|
||||
const amount = Number(raw)
|
||||
if (!/^\d+(?:\.\d{1,2})?$/.test(raw) || !Number.isFinite(amount) || amount <= 0 || amount > Math.abs(Number(source.entry.amount))) {
|
||||
ElMessage.warning('退款金额必须大于 0、最多两位小数,且不能超过原消费金额'); return
|
||||
}
|
||||
const reason = refundForm.reason.trim()
|
||||
if (!reason || reason.length > 500) { ElMessage.warning('请填写退款说明(最多 500 字)'); return }
|
||||
const payload = { amount: amount.toFixed(2), reason }
|
||||
const signature = JSON.stringify([source.customerId, source.entry.ledgerId, payload])
|
||||
if (refundAttempt.payload !== signature) refundAttempt = { payload: signature, key: 'finance-refund-' + crypto.randomUUID() }
|
||||
refundSaving.value = true
|
||||
refundError.value = null
|
||||
try {
|
||||
await refundFinanceConsumption(source.customerId, source.entry.ledgerId, payload, refundAttempt.key)
|
||||
refundOpen.value = false
|
||||
ElMessage.success('消费退款已入账')
|
||||
if (selected.value?.customerId === source.customerId) await refreshWalletDetail()
|
||||
} catch (error) { refundError.value = getApiErrorDetails(error, '退款失败,请重试') }
|
||||
finally { refundSaving.value = false }
|
||||
}
|
||||
function openAdjustment(account: FinanceAccountSummary, type: 'RECHARGE' | 'DEDUCTION') {
|
||||
adjustmentAccount.value = account
|
||||
@@ -425,12 +475,15 @@ async function closeDetail() {
|
||||
if (route.name === 'finance-customer-account-detail') await router.push({ name: 'finance-customer-accounts' })
|
||||
}
|
||||
async function restoreAccountDetail(customerId: string) {
|
||||
if (!canView.value || detailLoading.value) return
|
||||
detailLoading.value = true
|
||||
detailOpen.value = true; editing.value = false; ledgerError.value = null
|
||||
try {
|
||||
selected.value = (await listFinanceAccounts({ page: 1, size: 1, customerId })).records.find(row => row.customerId === customerId) || null
|
||||
if (!selected.value) { ledgerError.value = { message: '未找到该客户账户,可能已停用或无权访问' }; return }
|
||||
await Promise.all([loadLedger(), loadRechargeRequests()])
|
||||
} catch (error) { ledgerError.value = getApiErrorDetails(error, '客户账户详情加载失败,请重试') }
|
||||
finally { detailLoading.value = false }
|
||||
}
|
||||
onMounted(() => {
|
||||
const customerId = String(route.params.customerId || '')
|
||||
@@ -468,7 +521,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
<DetailPageLayout class="finance-account-detail__layout finance-density-detail" :aria-label="editing ? '维护客户账期' : '客户账户详情'">
|
||||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="closeDetail"><ArrowLeft :size="16" />返回客户账户</el-button><h1 class="density-detail-title">{{ selected ? customerName(selected.customerId, selected.customerName) : '客户账户' }}</h1><span class="finance-detail-context">{{ editing ? '维护客户账期' : 'CNY 资金账户' }}</span></div><div v-if="selected && !editing" class="finance-detail-actions"><el-button v-if="canAdjust" type="primary" plain @click="openAdjustment(selected, 'RECHARGE')"><PlusCircle :size="16" />手工充值</el-button><el-button v-if="canAdjust" type="danger" plain @click="openAdjustment(selected, 'DEDUCTION')"><MinusCircle :size="16" />手工扣款</el-button><el-button v-if="canAdjust" plain @click="openCreditLimit(selected)"><CreditCard :size="16" />设置授信</el-button></div></div></template>
|
||||
<template v-if="selected && !ledgerError" #summary><dl class="density-detail-overview account-metrics" aria-label="客户资金账户余额"><div class="account-metric is-actual"><dt>实际余额</dt><dd :class="{ 'is-negative': Number(selected.actualBalance) < 0 }">{{ money(selected.actualBalance) }} <small>CNY</small></dd></div><div class="account-metric is-frozen"><dt>冻结金额</dt><dd>{{ money(selected.frozenBalance) }} <small>CNY</small></dd></div><div class="account-metric is-available"><dt>总可用余额</dt><dd>{{ money(selected.availableBalance) }} <small>CNY</small></dd></div><div class="account-metric"><dt>授信额度</dt><dd>{{ money(selected.creditLimit) }} <small>{{ creditExpiryLabel(selected) }}</small></dd></div><div class="account-metric is-frozen"><dt>已用授信</dt><dd>{{ money(selected.creditUsed) }} <small>CNY</small></dd></div><div class="account-metric"><dt>可用授信</dt><dd>{{ money(selected.availableCredit) }} <small>CNY</small></dd></div></dl></template>
|
||||
<LoadErrorState v-if="ledgerError" :title="editing ? '客户账期加载失败' : '账户流水加载失败'" :message="ledgerError.message" :loading="ledgerLoading" @retry="retryDrawer"><template #details><ApiErrorMeta :details="ledgerError" /></template></LoadErrorState>
|
||||
<LoadErrorState v-if="ledgerError" :title="editing ? '客户账期加载失败' : '账户流水加载失败'" :message="ledgerError.message" :loading="ledgerLoading || detailLoading" @retry="retryDrawer"><template #details><ApiErrorMeta :details="ledgerError" /></template></LoadErrorState>
|
||||
<template v-else-if="selected">
|
||||
<FormSection class="customer-profile-section density-form-section" title="客户信息" :bordered="false"><el-descriptions border class="density-descriptions" :column="3"><el-descriptions-item label="客户编号">{{ customerField(selected.customerCode) }}</el-descriptions-item><el-descriptions-item label="联系人">{{ customerField(selected.contactName) }}</el-descriptions-item><el-descriptions-item label="联系电话">{{ customerField(selected.contactPhone) }}</el-descriptions-item></el-descriptions></FormSection>
|
||||
<FormSection v-if="!editing" class="recharge-section density-form-section" title="充值申请" :bordered="false">
|
||||
@@ -483,7 +536,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</FormSection>
|
||||
<FormSection v-if="!editing" class="ledger-section density-form-section" title="账户流水" :bordered="false">
|
||||
<QueryToolbar v-model="ledgerFilters" class="ledger-query workbench-query" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; void loadLedger($event)" />
|
||||
<div class="detail-table-scroll workbench-table"><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" class="ledger-table" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #toolbar-actions><el-button v-if="canExport" :loading="ledgerExportBusy" :disabled="ledgerLoading || ledgerExportBusy" @click="exportLedger"><Download :size="16" />导出流水</el-button></template><template #pagination-left>共 {{ ledgerTotal }} 条</template><template #ledgerId="{ row }"><span class="ledger-identifier" :title="displayLedgerId(row.businessNo || row.ledgerId)">{{ displayLedgerId(row.businessNo || row.ledgerId) }}</span></template><template #occurredAt="{ row }">{{ row.occurredDate || formatDateTime(row.createdAt || row.occurredAt) }}</template><template #businessType="{ row }">{{ walletBusinessType(row.changeType || row.businessType) }}</template><template #documentNo="{ row }"><span class="ledger-document" :title="displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo)">{{ displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo) }}</span></template><template #amount="{ row }">{{ money(row.amount) }}</template><template #ledgerPaymentMethod="{ row }">{{ row.paymentMethod ? paymentMethodLabel(row.paymentMethod) : '—' }}</template><template #actualBalanceAfter="{ row }">{{ money(row.actualBalanceAfter) }}</template><template #frozenBalanceAfter="{ row }">{{ money(row.frozenBalanceAfter) }}</template><template #availableBalanceAfter="{ row }">{{ money(row.availableBalanceAfter) }}</template><template #creditUsedAfter="{ row }">{{ money(row.creditUsedAfter) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></div>
|
||||
<div class="detail-table-scroll workbench-table"><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" class="ledger-table" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #actions="{ row }"><el-button v-if="canRefund(row as FinanceLedgerEntry)" link type="primary" @click="openRefund(row as FinanceLedgerEntry)">退款</el-button><span v-else>—</span></template><template #toolbar-actions><el-button v-if="canExport" :loading="ledgerExportBusy" :disabled="ledgerLoading || ledgerExportBusy" @click="exportLedger"><Download :size="16" />导出流水</el-button></template><template #pagination-left>共 {{ ledgerTotal }} 条</template><template #ledgerId="{ row }"><span class="ledger-identifier" :title="displayLedgerId(row.businessNo || row.ledgerId)">{{ displayLedgerId(row.businessNo || row.ledgerId) }}</span></template><template #occurredAt="{ row }">{{ row.occurredDate || formatDateTime(row.createdAt || row.occurredAt) }}</template><template #businessType="{ row }">{{ walletBusinessType(row.changeType || row.businessType) }}</template><template #documentNo="{ row }"><span v-if="row.changeType === 'REFUND'">原消费:</span><span class="ledger-document" :title="displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo)">{{ displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo) }}</span></template><template #amount="{ row }">{{ money(row.amount) }}</template><template #ledgerPaymentMethod="{ row }">{{ row.paymentMethod ? paymentMethodLabel(row.paymentMethod) : '—' }}</template><template #actualBalanceAfter="{ row }">{{ money(row.actualBalanceAfter) }}</template><template #frozenBalanceAfter="{ row }">{{ money(row.frozenBalanceAfter) }}</template><template #availableBalanceAfter="{ row }">{{ money(row.availableBalanceAfter) }}</template><template #creditUsedAfter="{ row }">{{ money(row.creditUsedAfter) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></div>
|
||||
</FormSection>
|
||||
<FormSection v-else class="term-section density-form-section" title="账期设置" :bordered="false"><div class="term-editor"><span class="term-label">账期天数</span><div class="term-control"><el-input-number v-model="creditDays" :min="0" :max="3650" :step="1" placeholder="请输入账期天数" /><span class="term-unit">天</span></div></div></FormSection>
|
||||
</template>
|
||||
@@ -491,6 +544,21 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</DetailPageLayout>
|
||||
</section>
|
||||
</AdminPage>
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="refundOpen" title="原消费退款" width="min(620px, calc(100vw - 28px))" fullscreenable confirm-text="确认退款" :confirm-loading="refundSaving" @confirm="submitRefund">
|
||||
<div class="density-dialog-sections">
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="source" aria-label="原消费资料"><h3 class="density-dialog-section__title">原消费(CNY)</h3><table class="density-table wallet-summary"><tbody>
|
||||
<tr><th scope="row">原消费流水</th><td>{{ refundSource?.entry.businessNo || refundSource?.entry.ledgerId }}</td></tr>
|
||||
<tr><th scope="row">关联业务单据</th><td>{{ refundSource?.entry.sourceNo || refundSource?.entry.documentNo || '—' }}</td></tr>
|
||||
<tr><th scope="row">原消费金额</th><td>{{ money(refundSource?.entry.amount) }}</td></tr>
|
||||
</tbody></table></section>
|
||||
<el-alert title="退款会生成独立流水并关联原消费;累计可退金额以服务端校验为准。" type="info" :closable="false" />
|
||||
<div v-if="refundError"><el-alert :title="refundError.message" type="error" :closable="false" /><ApiErrorMeta :details="refundError" /></div>
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="退款登记"><el-form class="density-form" label-position="left" label-width="100px" :disabled="refundSaving"><table class="density-table density-table--editable"><tbody>
|
||||
<tr><td><el-form-item label="退款金额" required><el-input v-model="refundForm.amount" inputmode="decimal" placeholder="请输入退款金额" /></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label="退款说明" required><el-input v-model="refundForm.reason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请填写退款说明" /></el-form-item></td></tr>
|
||||
</tbody></table></el-form></section>
|
||||
</div>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="adjustmentOpen" :title="`${adjustmentForm.type === 'RECHARGE' ? '手工充值' : '手工扣款'} · ${adjustmentAccount ? customerName(adjustmentAccount.customerId, adjustmentAccount.customerName) : '—'}`" width="min(720px, calc(100vw - 28px))" fullscreenable :confirm-text="adjustmentForm.type === 'RECHARGE' ? '确认充值' : '确认扣款'" :confirm-type="adjustmentForm.type === 'RECHARGE' ? 'primary' : 'danger'" :confirm-loading="adjustmentSaving" @confirm="submitAdjustment">
|
||||
<div class="density-dialog-sections">
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="summary" aria-label="当前账户资料">
|
||||
|
||||
Reference in New Issue
Block a user