748 lines
48 KiB
Vue
748 lines
48 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminWorkbench.css'
|
||
import '@/styles/adminDensity.css'
|
||
import '../financeDensity.css'
|
||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ArrowLeft, CreditCard, Download, MinusCircle, PlusCircle, Save } from '@lucide/vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { createPlatformExportTask } from '@/modules/downloads'
|
||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import InlineHelp from '@/shared/components/InlineHelp.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||
import FormSection from '@/shared/components/FormSection.vue'
|
||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||
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 { financeBusinessTypeLabel } from '../status'
|
||
import type { FinanceAccountSummary, FinanceCustomerTerms, FinanceLedgerEntry, FinanceRechargeRequest } from '../types'
|
||
|
||
const access = useAccessPreviewStore()
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const filters = ref<QueryModel>({ customerId: '', status: '' })
|
||
const customers = ref<PlatformTenantResource[]>([])
|
||
const customersLoading = ref(false)
|
||
const customersError = ref<ApiErrorDetails | null>(null)
|
||
const rows = ref<FinanceAccountSummary[]>([])
|
||
const loading = ref(false)
|
||
const loadError = ref<ApiErrorDetails | null>(null)
|
||
const page = ref(1)
|
||
const pageSize = ref(20)
|
||
const total = ref(0)
|
||
const detailOpen = ref(false)
|
||
const editing = ref(false)
|
||
const saving = ref(false)
|
||
const selected = ref<FinanceAccountSummary | null>(null)
|
||
const ledger = ref<FinanceLedgerEntry[]>([])
|
||
const ledgerLoading = ref(false)
|
||
const ledgerError = ref<ApiErrorDetails | null>(null)
|
||
const selectedTerms = ref<FinanceCustomerTerms | null>(null)
|
||
const creditDays = ref(30)
|
||
const ledgerFilters = ref<QueryModel>({ businessType: '', occurredRange: [] })
|
||
const ledgerPage = ref(1)
|
||
const ledgerPageSize = ref(20)
|
||
const ledgerTotal = ref(0)
|
||
const rechargeRequests = ref<FinanceRechargeRequest[]>([])
|
||
const rechargeLoading = ref(false)
|
||
const rechargeError = ref<ApiErrorDetails | null>(null)
|
||
const accountExportBusy = ref(false)
|
||
const ledgerExportBusy = ref(false)
|
||
const adjustmentOpen = ref(false)
|
||
const adjustmentSaving = ref(false)
|
||
const adjustmentAccount = ref<FinanceAccountSummary | null>(null)
|
||
const adjustmentForm = reactive({ type: 'RECHARGE' as 'RECHARGE' | 'DEDUCTION', amount: '', occurredDate: '', paymentMethod: 'BANK_TRANSFER', referenceNo: '', remark: '' })
|
||
const creditLimitOpen = ref(false)
|
||
const creditLimitSaving = ref(false)
|
||
const creditLimitForm = reactive({ amount: '', expiresOn: '', remark: '' })
|
||
const reviewOpen = ref(false)
|
||
const reviewSaving = ref(false)
|
||
const reviewRequest = ref<FinanceRechargeRequest | null>(null)
|
||
const reviewForm = reactive({ decision: 'APPROVE' as 'APPROVE' | 'REJECT', remark: '' })
|
||
const voucherPreviewOpen = ref(false)
|
||
const voucherPreviewLoading = ref(false)
|
||
const voucherPreviewUrl = ref('')
|
||
const voucherPreviewName = ref('付款凭证')
|
||
let voucherPreviewRequest = 0
|
||
watch(voucherPreviewOpen, value => { if (!value) clearVoucherPreview() })
|
||
|
||
const canView = computed(() => access.can('platform:finance:customer-account:list'))
|
||
const canListCustomers = computed(() => canView.value || access.can('platform:customer:list'))
|
||
const canViewTerms = computed(() => access.can('platform:finance:customer-terms:view'))
|
||
const canUpdateTerms = computed(() => canViewTerms.value && access.can('platform:finance:customer-terms:update'))
|
||
const canExport = computed(() => access.can('platform:finance:customer-account:export') && access.can('platform:download:list'))
|
||
const canAdjust = computed(() => access.can('platform:finance:customer-account:adjust'))
|
||
const canReviewRecharge = computed(() => access.can('platform:finance:recharge:review'))
|
||
const stateError = computed(() => customersError.value || loadError.value)
|
||
const queryFields = computed<QueryField[]>(() => [
|
||
{ key: 'customerId', type: 'select', label: '客户名称', width: 260, placeholder: '全部客户', props: { 'aria-label': '客户名称', filterable: true, clearable: true, loading: customersLoading.value }, options: customers.value.filter((item) => item.status === 'ACTIVE').map((item) => ({ label: item.tenantName, value: String(item.id) })) },
|
||
{ key: 'status', type: 'select', label: '状态', width: 150, placeholder: '全部状态', options: [{ label: '正常', value: 'ACTIVE' }, { label: '停用', value: 'DISABLED' }] },
|
||
])
|
||
const columns: DataTableColumn[] = [
|
||
{ key: 'actions', label: '操作', width: 290, fixed: 'left', slotName: 'actions' },
|
||
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' },
|
||
{ key: 'customerCode', label: '客户编号', width: 150, slotName: 'customerCode' },
|
||
{ key: 'contactName', label: '联系人', width: 130, slotName: 'contactName' },
|
||
{ key: 'contactPhone', label: '联系电话', width: 150, slotName: 'contactPhone' },
|
||
{ key: 'actualBalance', label: '实际余额(CNY)', width: 155, slotName: 'actualBalance' },
|
||
{ key: 'frozenBalance', label: '冻结金额(CNY)', width: 150, slotName: 'frozenBalance' },
|
||
{ key: 'availableBalance', label: '总可用余额(CNY)', width: 155, slotName: 'availableBalance' },
|
||
{ key: 'creditLimit', label: '授信额度(CNY)', width: 155, slotName: 'creditLimit' },
|
||
{ key: 'creditExpiresOn', label: '授信有效期', width: 140, slotName: 'creditExpiresOn' },
|
||
{ key: 'creditUsed', label: '已用授信(CNY)', width: 150, slotName: 'creditUsed' },
|
||
{ key: 'pendingRechargeCount', label: '待审核充值', width: 120, slotName: 'pendingRechargeCount' },
|
||
{ key: 'status', label: '客户状态', width: 110, slotName: 'status' },
|
||
{ key: 'lastChangedAt', label: '最近变动', width: 170, slotName: 'lastChangedAt' },
|
||
]
|
||
const ledgerQueryFields: QueryField[] = [
|
||
{ key: 'businessType', type: 'select', label: '流水类型', width: 180, placeholder: '全部类型', options: [
|
||
{ label: '生成对账单', value: 'RECONCILIATION_CREATED' }, { label: '结算单生效', value: 'SETTLEMENT_ISSUED' },
|
||
{ label: '收款', value: 'RECEIPT' },
|
||
{ label: '供应商线下直付', value: 'SUPPLIER_DIRECT_PAID' },
|
||
{ label: '供应商账期付款', value: 'SUPPLIER_ON_ACCOUNT_PAYMENT' },
|
||
{ label: '账务调整', value: 'ADJUSTMENT' }, { label: '作废', value: 'VOID' },
|
||
] },
|
||
{ key: 'occurredRange', type: 'daterange', label: '发生日期', width: 280 },
|
||
]
|
||
const ledgerColumns: DataTableColumn[] = [
|
||
{ 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' },
|
||
{ key: 'actualBalanceAfter', label: '变动后实际余额', width: 160, slotName: 'actualBalanceAfter' },
|
||
{ key: 'frozenBalanceAfter', label: '变动后冻结金额', width: 160, slotName: 'frozenBalanceAfter' },
|
||
{ key: 'availableBalanceAfter', label: '变动后可用余额', width: 160, slotName: 'availableBalanceAfter' },
|
||
{ key: 'creditUsedAfter', label: '变动后已用授信', width: 160, slotName: 'creditUsedAfter' },
|
||
{ key: 'operatorName', label: '操作人', width: 140, slotName: 'operatorName' }, { key: 'remark', label: '备注', minWidth: 180 },
|
||
]
|
||
const rechargeColumns: DataTableColumn[] = [
|
||
{ key: 'actions', label: '操作', width: 150, fixed: 'left', slotName: 'actions' },
|
||
{ key: 'requestNo', label: '申请单号', minWidth: 190 }, { key: 'createdAt', label: '提交时间', width: 170, slotName: 'createdAt' },
|
||
{ key: 'amount', label: '充值金额(CNY)', width: 150, slotName: 'amount' },
|
||
{ key: 'voucherFileName', label: '付款凭证', minWidth: 170, slotName: 'voucherFileName' }, { key: 'submittedByName', label: '提交人', width: 130 },
|
||
{ key: 'status', label: '状态', width: 110, slotName: 'status' }, { key: 'reviewedByName', label: '审核人', width: 130 },
|
||
{ key: 'reviewRemark', label: '审核备注', minWidth: 180 }, { key: 'reviewedAt', label: '审核时间', width: 170, slotName: 'reviewedAt' },
|
||
]
|
||
const sortedRechargeRequests = computed(() => [...rechargeRequests.value].sort((left, right) => Number(right.status === 'PENDING') - Number(left.status === 'PENDING')))
|
||
|
||
async function load(model: QueryModel = filters.value) {
|
||
Object.assign(filters.value, model)
|
||
if (!canView.value) return
|
||
loading.value = true
|
||
loadError.value = null
|
||
try {
|
||
const result = await listFinanceAccounts({ page: page.value, size: pageSize.value, customerId: String(filters.value.customerId || '').trim() || undefined, status: String(filters.value.status || '') })
|
||
rows.value = result.records
|
||
total.value = result.total
|
||
} catch (error) { loadError.value = getApiErrorDetails(error, '客户账户加载失败,请重试') }
|
||
finally { loading.value = false }
|
||
}
|
||
async function loadCustomers() {
|
||
customersLoading.value = true; customersError.value = null
|
||
try {
|
||
const records: PlatformTenantResource[] = []; let currentPage = 1; let pages = 1
|
||
do { const result = await listPlatformTenants({ page: currentPage, size: 200, status: 'ACTIVE' }); records.push(...result.records); pages = Math.max(1, result.pages); currentPage += 1 } while (currentPage <= pages)
|
||
customers.value = records
|
||
} catch (error) { customersError.value = getApiErrorDetails(error, '客户列表加载失败') }
|
||
finally { customersLoading.value = false }
|
||
}
|
||
function search(model: QueryModel) { page.value = 1; void load(model) }
|
||
async function openAccount(row: Record<string, unknown>, edit: boolean) {
|
||
selected.value = row as unknown as FinanceAccountSummary
|
||
editing.value = edit
|
||
if (!edit) {
|
||
await router.push({ name: 'finance-customer-account-detail', params: { customerId: selected.value.customerId } })
|
||
return
|
||
}
|
||
detailOpen.value = true
|
||
ledger.value = []
|
||
ledgerError.value = null
|
||
selectedTerms.value = null
|
||
if (edit) {
|
||
if (!canViewTerms.value) return
|
||
try { selectedTerms.value = await getFinanceCustomerTerms(selected.value.customerId); creditDays.value = selectedTerms.value.creditDays }
|
||
catch (error) { ledgerError.value = getApiErrorDetails(error, '客户账期加载失败,请重试') }
|
||
return
|
||
}
|
||
await loadLedger()
|
||
}
|
||
async function loadLedger(model: QueryModel = ledgerFilters.value) {
|
||
if (!selected.value) return
|
||
Object.assign(ledgerFilters.value, model)
|
||
ledgerLoading.value = true; ledgerError.value = null
|
||
const range = Array.isArray(ledgerFilters.value.occurredRange) ? ledgerFilters.value.occurredRange.map(String) : []
|
||
try {
|
||
const result = await listFinanceAccountLedger(selected.value.customerId, { page: ledgerPage.value, size: ledgerPageSize.value, businessType: String(ledgerFilters.value.businessType || ''), startDate: range[0], endDate: range[1] })
|
||
ledger.value = result.records
|
||
ledgerTotal.value = result.total
|
||
} catch (error) { ledgerError.value = getApiErrorDetails(error, '客户账户流水加载失败,请重试') }
|
||
finally { ledgerLoading.value = false }
|
||
}
|
||
async function loadRechargeRequests() {
|
||
if (!selected.value) return
|
||
rechargeLoading.value = true; rechargeError.value = null
|
||
try { rechargeRequests.value = (await listFinanceRechargeRequests(selected.value.customerId, { page: 1, size: 200 })).records }
|
||
catch (error) { rechargeRequests.value = []; rechargeError.value = getApiErrorDetails(error, '充值申请加载失败,请重试') }
|
||
finally { rechargeLoading.value = false }
|
||
}
|
||
async function refreshSelectedAccount() {
|
||
if (!selected.value) return
|
||
const customerId = selected.value.customerId
|
||
const result = await listFinanceAccounts({ page: 1, size: 1, customerId })
|
||
selected.value = result.records.find(row => row.customerId === customerId) || null
|
||
}
|
||
async function refreshWalletDetail() {
|
||
await refreshSelectedAccount()
|
||
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 saveTerms() {
|
||
if (!selected.value || !Number.isInteger(creditDays.value) || creditDays.value < 0 || creditDays.value > 3650) { ElMessage.warning('账期必须是 0 到 3650 的整数天'); return }
|
||
saving.value = true
|
||
ledgerError.value = null
|
||
try { selectedTerms.value = await updateFinanceCustomerTerms(selected.value.customerId, { customerName: selected.value.customerName, creditDays: creditDays.value }); ElMessage.success('客户账期已保存') }
|
||
catch (error) { ledgerError.value = getApiErrorDetails(error, '客户账期保存失败,请重试'); ElMessage.error(ledgerError.value.message) }
|
||
finally { saving.value = false }
|
||
}
|
||
function money(value: unknown) { const amount = String(value ?? '').trim(); return amount ? `¥${amount}` : '—' }
|
||
function amountInput(value: string) {
|
||
const amount = Number(value)
|
||
return Number.isFinite(amount) && amount > 0 ? amount.toFixed(2) : null
|
||
}
|
||
function today() {
|
||
const date = new Date()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
return `${date.getFullYear()}-${month}-${day}`
|
||
}
|
||
function disableCreditExpiry(date: Date) {
|
||
const boundary = new Date()
|
||
boundary.setHours(0, 0, 0, 0)
|
||
return date.getTime() <= boundary.getTime()
|
||
}
|
||
function creditExpiryLabel(account: FinanceAccountSummary) {
|
||
if (!account.creditExpiresOn) return '未设置'
|
||
return account.creditActive ? `${account.creditExpiresOn} 到期` : `${account.creditExpiresOn} 已到期`
|
||
}
|
||
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)
|
||
}
|
||
function openAdjustment(account: FinanceAccountSummary, type: 'RECHARGE' | 'DEDUCTION') {
|
||
adjustmentAccount.value = account
|
||
Object.assign(adjustmentForm, { type, amount: '', occurredDate: today(), paymentMethod: 'BANK_TRANSFER', referenceNo: '', remark: '' })
|
||
adjustmentOpen.value = true
|
||
}
|
||
async function submitAdjustment() {
|
||
if (adjustmentSaving.value || !adjustmentAccount.value) return
|
||
const amount = amountInput(adjustmentForm.amount)
|
||
if (!amount || !adjustmentForm.occurredDate || !adjustmentForm.paymentMethod) { ElMessage.warning('请填写有效金额、发生日期和付款方式'); return }
|
||
adjustmentSaving.value = true
|
||
try {
|
||
await adjustFinanceAccount(adjustmentAccount.value.customerId, {
|
||
type: adjustmentForm.type, amount, occurredDate: adjustmentForm.occurredDate, paymentMethod: adjustmentForm.paymentMethod,
|
||
referenceNo: adjustmentForm.referenceNo.trim() || undefined, remark: adjustmentForm.remark.trim() || undefined,
|
||
expectedAccountVersion: adjustmentAccount.value.accountVersion,
|
||
})
|
||
adjustmentOpen.value = false
|
||
ElMessage.success(adjustmentForm.type === 'RECHARGE' ? '手工充值已入账' : '手工扣款已入账')
|
||
if (selected.value?.customerId === adjustmentAccount.value.customerId) await refreshWalletDetail()
|
||
else await load()
|
||
} catch (error) { ElMessage.error(getApiErrorMessage(error, adjustmentForm.type === 'RECHARGE' ? '手工充值失败' : '手工扣款失败')) }
|
||
finally { adjustmentSaving.value = false }
|
||
}
|
||
function openCreditLimit(account: FinanceAccountSummary) {
|
||
adjustmentAccount.value = account
|
||
Object.assign(creditLimitForm, {
|
||
amount: Number(account.creditLimit || 0).toFixed(2),
|
||
expiresOn: account.creditActive ? account.creditExpiresOn || '' : '',
|
||
remark: '',
|
||
})
|
||
creditLimitOpen.value = true
|
||
}
|
||
function handleAccountMore(command: string, account: FinanceAccountSummary) {
|
||
if (command === 'credit') openCreditLimit(account)
|
||
if (command === 'terms') void openAccount(account as unknown as Record<string, unknown>, true)
|
||
}
|
||
function accountActions(account: FinanceAccountSummary): RowActionItem[] {
|
||
return [
|
||
{ key: 'detail', label: '详情', priority: 300 },
|
||
{ key: 'recharge', label: '充值', priority: 250, visible: canAdjust.value },
|
||
{ key: 'deduction', label: '扣款', priority: 200, tone: 'danger', visible: canAdjust.value },
|
||
{ key: 'credit', label: '授信', priority: 100, visible: canAdjust.value },
|
||
{ key: 'terms', label: '账期', visible: canUpdateTerms.value, divided: canAdjust.value },
|
||
]
|
||
}
|
||
function handleAccountAction(command: string, account: FinanceAccountSummary) {
|
||
if (command === 'detail') void openAccount(account as unknown as Record<string, unknown>, false)
|
||
else if (command === 'recharge') openAdjustment(account, 'RECHARGE')
|
||
else if (command === 'deduction') openAdjustment(account, 'DEDUCTION')
|
||
else handleAccountMore(command, account)
|
||
}
|
||
async function submitCreditLimit() {
|
||
if (creditLimitSaving.value || !adjustmentAccount.value) return
|
||
const raw = creditLimitForm.amount.trim()
|
||
const amount = Number(raw)
|
||
if (!Number.isFinite(amount) || amount < 0 || !/^\d+(?:\.\d{1,2})?$/.test(raw)) {
|
||
ElMessage.warning('授信额度必须是大于或等于 0 且最多两位小数的金额')
|
||
return
|
||
}
|
||
if (!creditLimitForm.expiresOn || creditLimitForm.expiresOn <= today()) {
|
||
ElMessage.warning('授信到期日必须晚于今天')
|
||
return
|
||
}
|
||
creditLimitSaving.value = true
|
||
try {
|
||
await updateFinanceCreditLimit(adjustmentAccount.value.customerId, {
|
||
creditLimit: amount.toFixed(2), creditExpiresOn: creditLimitForm.expiresOn,
|
||
remark: creditLimitForm.remark.trim() || undefined,
|
||
expectedAccountVersion: adjustmentAccount.value.accountVersion,
|
||
})
|
||
creditLimitOpen.value = false
|
||
ElMessage.success('授信额度已更新')
|
||
if (selected.value?.customerId === adjustmentAccount.value.customerId) await refreshWalletDetail()
|
||
else await load()
|
||
} catch (error) { ElMessage.error(getApiErrorMessage(error, '授信额度更新失败')) }
|
||
finally { creditLimitSaving.value = false }
|
||
}
|
||
function openReview(request: FinanceRechargeRequest) {
|
||
reviewRequest.value = request
|
||
Object.assign(reviewForm, { decision: 'APPROVE', remark: '' })
|
||
reviewOpen.value = true
|
||
}
|
||
async function submitReview() {
|
||
if (reviewSaving.value || !reviewRequest.value) return
|
||
reviewSaving.value = true
|
||
try {
|
||
await reviewFinanceRechargeRequest(reviewRequest.value.requestId, {
|
||
decision: reviewForm.decision, remark: reviewForm.remark.trim() || undefined, expectedVersion: reviewRequest.value.version,
|
||
})
|
||
reviewOpen.value = false
|
||
ElMessage.success(reviewForm.decision === 'APPROVE' ? '充值申请已通过并入账' : '充值申请已驳回')
|
||
await refreshWalletDetail()
|
||
} catch (error) { ElMessage.error(getApiErrorMessage(error, '充值申请审核失败')) }
|
||
finally { reviewSaving.value = false }
|
||
}
|
||
function clearVoucherPreview() {
|
||
voucherPreviewRequest++
|
||
if (voucherPreviewUrl.value) URL.revokeObjectURL(voucherPreviewUrl.value)
|
||
voucherPreviewUrl.value = ''
|
||
}
|
||
async function previewVoucher(request: FinanceRechargeRequest) {
|
||
clearVoucherPreview()
|
||
const requestSequence = ++voucherPreviewRequest
|
||
voucherPreviewName.value = request.voucherFileName || '付款凭证'
|
||
voucherPreviewOpen.value = true
|
||
voucherPreviewLoading.value = true
|
||
try {
|
||
const blob = await downloadFinanceRechargeVoucher(request.requestId)
|
||
if (requestSequence !== voucherPreviewRequest || !voucherPreviewOpen.value) return
|
||
voucherPreviewUrl.value = URL.createObjectURL(blob)
|
||
} catch (error) {
|
||
if (requestSequence !== voucherPreviewRequest || !voucherPreviewOpen.value) return
|
||
voucherPreviewOpen.value = false
|
||
ElMessage.error(getApiErrorMessage(error, '付款凭证加载失败'))
|
||
} finally { if (requestSequence === voucherPreviewRequest) voucherPreviewLoading.value = false }
|
||
}
|
||
function customerName(customerId: string, snapshot?: string) {
|
||
const masterName = customers.value.find((item) => String(item.id) === String(customerId))?.tenantName?.trim()
|
||
const candidate = String(snapshot || '').trim()
|
||
const placeholder = [String(customerId), `客户 ${customerId}`, `客户${customerId}`].includes(candidate)
|
||
return masterName || (candidate && !placeholder ? candidate : '客户名称暂不可用')
|
||
}
|
||
function ledgerBusinessType(value: unknown) {
|
||
const code = String(value || '').trim().toUpperCase()
|
||
const known: Record<string, string> = { REC: '收款', RECONCILIATION: '对账单', FEE_SOURCE: '费用来源' }
|
||
return known[code] || financeBusinessTypeLabel(code)
|
||
}
|
||
function displayOperator(value: unknown) {
|
||
const operator = String(value || '').trim()
|
||
if (!operator) return '名称数据缺失'
|
||
return /^(?:operator-)?\d+$/i.test(operator) ? '名称数据缺失' : operator
|
||
}
|
||
function formatDateTime(value: unknown) {
|
||
const raw = String(value || '').trim()
|
||
if (!raw) return '时间数据缺失'
|
||
const date = new Date(raw)
|
||
return Number.isNaN(date.getTime()) ? '时间数据缺失' : date.toLocaleString('zh-CN', { hour12: false })
|
||
}
|
||
function formatLastChangedAt(value: unknown) { return String(value || '').trim() ? formatDateTime(value) : '暂无变动' }
|
||
function customerField(value: unknown) { return String(value || '').trim() || '未维护' }
|
||
function displayDocumentNo(value: unknown) { return String(value || '').trim() || '关联单据缺失' }
|
||
function displayLedgerId(value: unknown) { return String(value || '').trim() || '流水号缺失' }
|
||
async function finishExport(task: Awaited<ReturnType<typeof createPlatformExportTask>>) {
|
||
const openDownloadCenter = await showExportTaskCreatedDialog({ taskName: task.taskName, taskNo: task.taskNo, dense: true })
|
||
if (openDownloadCenter) await router.push({ name: 'download-center' })
|
||
}
|
||
async function exportAccounts() {
|
||
const customerId = String(filters.value.customerId || '').trim()
|
||
const status = String(filters.value.status || '').trim()
|
||
accountExportBusy.value = true
|
||
try {
|
||
const task = await createPlatformExportTask({
|
||
exportType: 'FINANCE_CUSTOMER_ACCOUNT_LIST', format: 'XLSX', taskName: '客户账户',
|
||
parameters: {
|
||
...(customerId ? { customerId } : {}),
|
||
...(status ? { status } : {}),
|
||
},
|
||
})
|
||
await finishExport(task)
|
||
} catch (error) { ElMessage.error(getApiErrorMessage(error, '客户账户导出任务创建失败')) }
|
||
finally { accountExportBusy.value = false }
|
||
}
|
||
async function exportLedger() {
|
||
if (!selected.value) return
|
||
const range = Array.isArray(ledgerFilters.value.occurredRange) ? ledgerFilters.value.occurredRange.map(String) : []
|
||
const businessType = String(ledgerFilters.value.businessType || '').trim()
|
||
ledgerExportBusy.value = true
|
||
try {
|
||
const task = await createPlatformExportTask({
|
||
exportType: 'FINANCE_CUSTOMER_ACCOUNT_LEDGER', format: 'XLSX', taskName: '客户账户流水',
|
||
parameters: {
|
||
customerId: selected.value.customerId,
|
||
...(businessType ? { businessType } : {}),
|
||
...(range[0] ? { startDate: range[0] } : {}),
|
||
...(range[1] ? { endDate: range[1] } : {}),
|
||
},
|
||
})
|
||
await finishExport(task)
|
||
} catch (error) { ElMessage.error(getApiErrorMessage(error, '客户账户流水导出任务创建失败')) }
|
||
finally { ledgerExportBusy.value = false }
|
||
}
|
||
async function closeDetail() {
|
||
detailOpen.value = false; selected.value = null; editing.value = false; ledger.value = []
|
||
if (route.name === 'finance-customer-account-detail') await router.push({ name: 'finance-customer-accounts' })
|
||
}
|
||
async function restoreAccountDetail(customerId: string) {
|
||
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, '客户账户详情加载失败,请重试') }
|
||
}
|
||
onMounted(() => {
|
||
const customerId = String(route.params.customerId || '')
|
||
if (route.name === 'finance-customer-account-detail' && customerId) { if (canListCustomers.value) void loadCustomers(); void restoreAccountDetail(customerId) }
|
||
else { if (canListCustomers.value) void loadCustomers(); void load() }
|
||
})
|
||
onBeforeUnmount(clearVoucherPreview)
|
||
</script>
|
||
|
||
<template>
|
||
<AdminPage as="div" class="finance-account-page admin-density admin-workbench" spacing="normal">
|
||
<el-result v-if="!canView" icon="warning" title="无权访问客户账户" sub-title="请联系管理员开通客户账户查看权限" />
|
||
<ListPageLayout v-else-if="!detailOpen" aria-label="客户账户">
|
||
|
||
|
||
<section class="workbench-table is-page-surface"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!stateError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="finance-customer-accounts-table" aria-label="客户账户列表" row-key="customerId" empty-text="暂无客户账户" @update:page="load()" @update:page-size="page = 1; load()">
|
||
<template #toolbar-start><h1>客户账户</h1></template>
|
||
<template #filters><QueryToolbar v-model="filters" ariaLabel="客户账户筛选" :fields="queryFields" preference-key="finance-customer-accounts-query" @search="search" /></template>
|
||
<template #notices><template v-if="stateError"><LoadErrorState title="客户账户加载失败" :message="stateError.message" :loading="loading || customersLoading" @retry="customersError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="stateError" /></template></LoadErrorState></template></template>
|
||
|
||
|
||
<template #toolbar-actions><el-button v-if="canExport" :loading="accountExportBusy" :disabled="loading || accountExportBusy" @click="exportAccounts"><Download :size="16" />导出Excel</el-button></template>
|
||
<template #actions="{ row }"><RowActions text-actions :actions="accountActions(row as FinanceAccountSummary)" :more-label="`更多客户账户操作 · ${(row as FinanceAccountSummary).customerName || '当前客户'}`" @action="handleAccountAction($event, row as FinanceAccountSummary)" /></template>
|
||
<template #customerName="{ row }">{{ customerName(row.customerId, row.customerName) }}</template>
|
||
<template #customerCode="{ row }">{{ customerField(row.customerCode) }}</template><template #contactName="{ row }">{{ customerField(row.contactName) }}</template><template #contactPhone="{ row }">{{ customerField(row.contactPhone) }}</template>
|
||
<template #lastChangedAt="{ row }">{{ formatLastChangedAt(row.lastChangedAt) }}</template>
|
||
<template #actualBalance="{ row }"><strong class="balance-value is-actual" :class="{ 'is-negative': Number(row.actualBalance) < 0 }">{{ money(row.actualBalance) }}</strong></template><template #frozenBalance="{ row }"><span class="balance-value is-frozen">{{ money(row.frozenBalance) }}</span></template><template #availableBalance="{ row }"><strong class="balance-value">{{ money(row.availableBalance) }}</strong></template><template #creditLimit="{ row }">{{ money(row.creditLimit) }}</template><template #creditExpiresOn="{ row }"><span :class="{ 'credit-expired': row.creditExpiresOn && !row.creditActive }">{{ creditExpiryLabel(row as FinanceAccountSummary) }}</span></template><template #creditUsed="{ row }"><span class="balance-value is-frozen">{{ money(row.creditUsed) }}</span></template>
|
||
<template #pendingRechargeCount="{ row }"><el-tag v-if="row.pendingRechargeCount" type="warning">{{ row.pendingRechargeCount }} 笔</el-tag><span v-else>—</span></template>
|
||
<template #status="{ row }"><el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'">{{ row.status === 'ACTIVE' ? '正常' : '停用' }}</el-tag></template>
|
||
<template #pagination-left><span>共 {{ total }} 个客户账户</span></template>
|
||
</DataTable></section>
|
||
</ListPageLayout>
|
||
<AdminPage v-else as="div" class="finance-account-detail admin-density" spacing="normal">
|
||
<section class="surface-panel finance-account-detail__surface workbench-panel is-flush is-page-surface">
|
||
<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>
|
||
<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">
|
||
<LoadErrorState v-if="rechargeError" title="充值申请加载失败" :message="rechargeError.message" :loading="rechargeLoading" @retry="loadRechargeRequests" />
|
||
<div v-else class="detail-table-scroll workbench-table"><DataTable class="recharge-table" :data="sortedRechargeRequests" :columns="rechargeColumns" :loading="rechargeLoading" row-key="requestId" empty-text="暂无充值申请" column-settings-mode="hide">
|
||
<template #actions="{ row }"><div class="density-actions"><el-button data-tone="warning" class="density-action" v-if="canReviewRecharge && row.status === 'PENDING'" size="small" @click="openReview(row as FinanceRechargeRequest)">审核</el-button></div></template>
|
||
<template #createdAt="{ row }">{{ formatDateTime(row.createdAt) }}</template><template #reviewedAt="{ row }">{{ row.reviewedAt ? formatDateTime(row.reviewedAt) : '—' }}</template>
|
||
<template #amount="{ row }"><strong class="balance-value is-actual">{{ money(row.amount) }}</strong></template>
|
||
<template #voucherFileName="{ row }"><el-button v-if="row.voucherFileId" link type="primary" @click="previewVoucher(row as FinanceRechargeRequest)">{{ row.voucherFileName || '预览付款凭证' }}</el-button><span v-else>—</span></template>
|
||
<template #status="{ row }"><el-tag :type="row.status === 'PENDING' ? 'warning' : row.status === 'APPROVED' ? 'success' : 'danger'">{{ row.status === 'PENDING' ? '待审核' : row.status === 'APPROVED' ? '已通过' : '已驳回' }}</el-tag></template>
|
||
</DataTable></div>
|
||
</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>
|
||
</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>
|
||
<template v-if="editing && selected && !ledgerError" #footer><div class="detail-footer"><el-button @click="closeDetail">关闭</el-button><el-button v-if="canUpdateTerms" type="primary" :loading="saving" @click="saveTerms"><Save :size="16" />保存账期</el-button></div></template>
|
||
</DetailPageLayout>
|
||
</section>
|
||
</AdminPage>
|
||
<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="当前账户资料">
|
||
<h3 class="density-dialog-section__title">当前账户(CNY) <InlineHelp v-if="adjustmentForm.type === 'RECHARGE'" title="充值说明">充值会先归还已用授信,剩余金额计入实际余额。</InlineHelp></h3>
|
||
<table class="density-table wallet-summary"><tbody>
|
||
<tr><th scope="row">客户名称</th><td colspan="3">{{ adjustmentAccount ? customerName(adjustmentAccount.customerId, adjustmentAccount.customerName) : '—' }}</td></tr>
|
||
<tr><th scope="row">当前实际余额</th><td :colspan="adjustmentForm.type === 'DEDUCTION' ? 1 : 3">{{ money(adjustmentAccount?.actualBalance) }}</td><template v-if="adjustmentForm.type === 'DEDUCTION'"><th scope="row">当前可用余额</th><td>{{ money(adjustmentAccount?.availableBalance) }}</td></template></tr>
|
||
</tbody></table>
|
||
</section>
|
||
<el-alert v-if="adjustmentForm.type === 'DEDUCTION'" title="扣款先使用未冻结的实际余额,不足部分再使用可用授信。" type="warning" :closable="false" />
|
||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="账户变动登记">
|
||
<h3 class="density-dialog-section__title">{{ adjustmentForm.type === 'RECHARGE' ? '充值登记' : '扣款登记' }}</h3>
|
||
<el-form label-position="left" label-width="100px" class="density-form" :disabled="adjustmentSaving">
|
||
<table class="density-table density-table--editable wallet-adjustment-table"><tbody>
|
||
<tr><td><el-form-item :label="adjustmentForm.type === 'RECHARGE' ? '充值金额' : '扣款金额'" required><el-input v-model="adjustmentForm.amount" inputmode="decimal" placeholder="请输入大于 0 的金额" /></el-form-item></td><td><el-form-item :label="adjustmentForm.type === 'RECHARGE' ? '到账日期' : '扣款日期'" required><el-date-picker v-model="adjustmentForm.occurredDate" type="date" value-format="YYYY-MM-DD" /></el-form-item></td></tr>
|
||
<tr><td><el-form-item :label="adjustmentForm.type === 'RECHARGE' ? '充值方式' : '扣款方式'" required><el-select v-model="adjustmentForm.paymentMethod"><el-option label="银行转账" value="BANK_TRANSFER" /><el-option label="现金" value="CASH" /><el-option label="其他" value="OTHER" /></el-select></el-form-item></td><td><el-form-item label="关联单据"><el-input v-model="adjustmentForm.referenceNo" maxlength="128" placeholder="请输入关联业务单号(选填)" /></el-form-item></td></tr>
|
||
<tr><td colspan="2"><el-form-item :label="adjustmentForm.type === 'RECHARGE' ? '充值备注' : '扣款备注'"><el-input v-model="adjustmentForm.remark" type="textarea" :rows="3" maxlength="1000" 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="creditLimitOpen" :title="`设置授信额度 · ${adjustmentAccount ? customerName(adjustmentAccount.customerId, adjustmentAccount.customerName) : '—'}`" width="min(560px, calc(100vw - 28px))" fullscreenable confirm-text="保存授信额度" :confirm-loading="creditLimitSaving" @confirm="submitCreditLimit">
|
||
<div class="density-dialog-sections">
|
||
<section class="density-dialog-section workbench-evidence" data-tone="summary" aria-label="当前授信资料"><h3 class="density-dialog-section__title">当前授信(CNY)</h3><table class="density-table wallet-summary"><tbody><tr><th scope="row">客户名称</th><td>{{ adjustmentAccount ? customerName(adjustmentAccount.customerId, adjustmentAccount.customerName) : '—' }}</td></tr><tr><th scope="row">当前已用授信</th><td>{{ money(adjustmentAccount?.creditUsed) }}</td></tr></tbody></table></section>
|
||
<el-alert title="授信到期日当天仍有效,次日授信失效;届时负数实际余额将阻止客户提交新订单。" type="warning" :closable="false" />
|
||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="授信额度设置"><h3 class="density-dialog-section__title">授信设置</h3>
|
||
<el-form label-position="left" label-width="100px" class="density-form" :disabled="creditLimitSaving"><table class="density-table density-table--editable"><tbody>
|
||
<tr><td><el-form-item label="授信额度" required><el-input v-model="creditLimitForm.amount" inputmode="decimal" placeholder="请输入大于或等于 0 的金额" /></el-form-item></td></tr>
|
||
<tr><td><el-form-item label="授信到期日" required><el-date-picker v-model="creditLimitForm.expiresOn" type="date" value-format="YYYY-MM-DD" :disabled-date="disableCreditExpiry" placeholder="请选择今天以后的日期" /></el-form-item></td></tr>
|
||
<tr><td><el-form-item label="调整说明"><el-input v-model="creditLimitForm.remark" 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="reviewOpen" :title="`审核充值申请 · ${reviewRequest?.requestNo || '—'}`" width="min(620px, calc(100vw - 28px))" fullscreenable confirm-text="确认审核" :confirm-loading="reviewSaving" @confirm="submitReview">
|
||
<div class="density-dialog-sections">
|
||
<section v-if="reviewRequest" class="density-dialog-section workbench-evidence" data-tone="source" aria-label="充值申请资料"><h3 class="density-dialog-section__title">充值申请</h3><table class="density-table wallet-summary"><tbody><tr><th scope="row">申请单号</th><td>{{ reviewRequest.requestNo || '—' }}</td></tr><tr><th scope="row">充值金额(CNY)</th><td>{{ money(reviewRequest.amount) }}</td></tr><tr><th scope="row">付款凭证</th><td>{{ reviewRequest.voucherFileName || '—' }}</td></tr></tbody></table></section>
|
||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="充值审核登记"><h3 class="density-dialog-section__title">审核登记</h3>
|
||
<el-form label-position="left" label-width="100px" class="density-form" :disabled="reviewSaving"><table class="density-table density-table--editable"><tbody>
|
||
<tr><td><el-form-item label="审核结果" required><el-radio-group v-model="reviewForm.decision"><el-radio value="APPROVE">通过</el-radio><el-radio value="REJECT">驳回</el-radio></el-radio-group></el-form-item></td></tr>
|
||
<tr><td><el-form-item label="审核备注"><el-input v-model="reviewForm.remark" type="textarea" :rows="3" maxlength="1000" 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="voucherPreviewOpen" :title="voucherPreviewName" width="min(920px, calc(100vw - 28px))" fullscreenable :loading="voucherPreviewLoading" :show-footer="false">
|
||
<section class="density-dialog-section" data-tone="source" aria-label="付款凭证"><div class="voucher-preview"><img v-if="voucherPreviewUrl" class="voucher-preview__image" :src="voucherPreviewUrl" :alt="voucherPreviewName" /></div></section>
|
||
</AppDialog>
|
||
</AdminPage>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.finance-account-page {
|
||
min-width: 0;
|
||
}
|
||
|
||
.finance-account-detail {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.finance-account-detail__surface {
|
||
width: min(100%, 1920px);
|
||
margin: 0 auto;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.finance-account-detail__layout {
|
||
width: 100%;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.detail-footer,
|
||
.term-control {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--admin-space-3);
|
||
}
|
||
|
||
.balance-value {
|
||
color: var(--workbench-text);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.balance-value.is-actual {
|
||
color: var(--el-color-success);
|
||
}
|
||
|
||
.balance-value.is-frozen {
|
||
color: var(--el-color-warning);
|
||
}
|
||
|
||
.balance-value.is-negative,
|
||
.credit-expired {
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
.voucher-preview {
|
||
display: grid;
|
||
min-height: 320px;
|
||
place-items: center;
|
||
}
|
||
|
||
.voucher-preview__image {
|
||
display: block;
|
||
max-width: 100%;
|
||
max-height: 70vh;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.ledger-section {
|
||
padding: var(--admin-space-4) 0 0;
|
||
}
|
||
|
||
.recharge-section {
|
||
padding: var(--admin-space-4) 0 0;
|
||
border-bottom: 1px solid var(--workbench-border);
|
||
}
|
||
|
||
.recharge-table {
|
||
min-width: 0;
|
||
}
|
||
|
||
.ledger-query {
|
||
margin-bottom: var(--admin-space-3);
|
||
}
|
||
|
||
:deep(.ledger-query.query-toolbar) {
|
||
padding: 0;
|
||
}
|
||
|
||
.detail-table-scroll {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.ledger-table {
|
||
min-width: 0;
|
||
}
|
||
|
||
.ledger-identifier,
|
||
.ledger-document {
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.term-section {
|
||
width: 100%;
|
||
padding: var(--admin-space-4) 0;
|
||
}
|
||
|
||
.term-editor {
|
||
display: grid;
|
||
grid-template-columns: 120px minmax(0, 220px);
|
||
justify-content: start;
|
||
align-items: center;
|
||
gap: var(--admin-space-5);
|
||
padding: 2px 0;
|
||
}
|
||
|
||
.term-label {
|
||
color: var(--workbench-text);
|
||
font-size: var(--admin-font-section);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.term-control :deep(.el-input-number) {
|
||
width: 220px;
|
||
}
|
||
|
||
.term-unit {
|
||
color: var(--workbench-muted);
|
||
}
|
||
|
||
.wallet-summary { table-layout: fixed; }
|
||
.wallet-summary th { width: var(--admin-label-width); }
|
||
.wallet-adjustment-table { table-layout: fixed; }
|
||
.wallet-adjustment-table td:not([colspan]) { width: 50%; }
|
||
|
||
.detail-footer {
|
||
width: 100%;
|
||
justify-content: flex-end;
|
||
margin-top: 0;
|
||
padding-top: var(--admin-space-3);
|
||
border-top: 0;
|
||
}
|
||
|
||
@media (min-width: 1920px) {
|
||
|
||
.detail-footer {
|
||
padding-top: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1080px) {
|
||
.account-metrics {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.account-metric:nth-child(3),
|
||
.account-metric:nth-child(5) {
|
||
padding-left: 0;
|
||
border-left: 0;
|
||
}
|
||
|
||
.finance-account-detail__surface {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.detail-heading {
|
||
align-items: stretch;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.detail-back {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.detail-footer .el-button {
|
||
min-height: 44px;
|
||
}
|
||
|
||
.term-editor {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.account-metrics {
|
||
grid-template-columns: 1fr;
|
||
gap: 12px;
|
||
}
|
||
|
||
.account-metric,
|
||
.account-metric:first-child,
|
||
.account-metric:nth-child(3),
|
||
.account-metric:nth-child(5) {
|
||
padding-left: 0;
|
||
}
|
||
|
||
.account-metric + .account-metric {
|
||
border-left: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.detail-footer .el-button {
|
||
flex: 1 1 120px;
|
||
}
|
||
}
|
||
</style>
|