2026-08-11 18:06:35 +08:00
< script setup lang = "ts" >
2026-09-15 09:58:15 +08:00
import '@/styles/adminDensity.css'
import '../financeDensity.css'
2026-09-05 09:14:06 +08:00
import { computed , onBeforeUnmount , onMounted , reactive , ref } from 'vue'
2026-08-13 11:57:44 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-09-18 16:48:40 +08:00
import { ArrowLeft , CreditCard , Download , MinusCircle , PlusCircle , Save } from '@lucide/vue'
2026-08-11 18:06:35 +08:00
import { ElMessage } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-17 17:49:10 +08:00
import { createPlatformExportTask } from '@/modules/downloads'
2026-08-14 12:50:42 +08:00
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
2026-08-17 17:49:10 +08:00
import { getApiErrorDetails , getApiErrorMessage , type ApiErrorDetails } from '@/shared/api/http'
2026-08-11 18:06:35 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-09-05 08:34:40 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-11 18:06:35 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-17 12:31:27 +08:00
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
2026-08-11 18:06:35 +08:00
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'
2026-09-18 16:48:40 +08:00
import RowActions , { type RowActionItem } from '@/shared/components/RowActions.vue'
2026-08-17 17:49:10 +08:00
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
2026-09-05 11:54:55 +08:00
import { adjustFinanceAccount , downloadFinanceRechargeVoucher , getFinanceCustomerTerms , listFinanceAccountLedger , listFinanceAccounts , listFinanceRechargeRequests , reviewFinanceRechargeRequest , updateFinanceCreditLimit , updateFinanceCustomerTerms } from '../api/finance'
2026-08-13 11:57:44 +08:00
import { financeBusinessTypeLabel } from '../status'
2026-09-05 08:34:40 +08:00
import type { FinanceAccountSummary , FinanceCustomerTerms , FinanceLedgerEntry , FinanceRechargeRequest } from '../types'
2026-08-11 18:06:35 +08:00
const access = useAccessPreviewStore ()
2026-08-13 11:57:44 +08:00
const route = useRoute ()
const router = useRouter ()
2026-08-14 12:50:42 +08:00
const filters = ref < QueryModel >({ customerId : '' , status : '' })
const customers = ref < PlatformTenantResource [] >([])
const customersLoading = ref ( false )
const customersError = ref < ApiErrorDetails | null >( null )
2026-08-11 18:43:56 +08:00
const rows = ref < FinanceAccountSummary [] >([])
2026-08-11 18:06:35 +08:00
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
2026-08-11 18:43:56 +08:00
const page = ref ( 1 )
const pageSize = ref ( 20 )
const total = ref ( 0 )
2026-08-13 11:57:44 +08:00
const detailOpen = ref ( false )
2026-08-11 18:06:35 +08:00
const editing = ref ( false )
const saving = ref ( false )
2026-08-11 18:43:56 +08:00
const selected = ref < FinanceAccountSummary | null >( null )
const ledger = ref < FinanceLedgerEntry [] >([])
const ledgerLoading = ref ( false )
const ledgerError = ref < ApiErrorDetails | null >( null )
2026-08-11 18:06:35 +08:00
const selectedTerms = ref < FinanceCustomerTerms | null >( null )
const creditDays = ref ( 30 )
2026-08-13 11:57:44 +08:00
const ledgerFilters = ref < QueryModel >({ businessType : '' , occurredRange : [] })
const ledgerPage = ref ( 1 )
const ledgerPageSize = ref ( 20 )
const ledgerTotal = ref ( 0 )
2026-09-05 08:34:40 +08:00
const rechargeRequests = ref < FinanceRechargeRequest [] >([])
const rechargeLoading = ref ( false )
const rechargeError = ref < ApiErrorDetails | null >( null )
2026-08-17 17:49:10 +08:00
const accountExportBusy = ref ( false )
const ledgerExportBusy = ref ( false )
2026-09-05 08:34:40 +08:00
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 : '' })
2026-09-05 11:54:55 +08:00
const creditLimitOpen = ref ( false )
const creditLimitSaving = ref ( false )
2026-09-10 22:43:10 +08:00
const creditLimitForm = reactive ({ amount : '' , expiresOn : '' , remark : '' })
2026-09-05 08:34:40 +08:00
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 ( '付款凭证' )
2026-08-11 18:06:35 +08:00
2026-08-13 14:37:59 +08:00
const canView = computed (() => access . can ( 'platform:finance:customer-account:list' ))
2026-08-14 12:50:42 +08:00
const canListCustomers = computed (() => canView . value || access . can ( 'platform:customer:list' ))
2026-08-13 12:37:49 +08:00
const canViewTerms = computed (() => access . can ( 'platform:finance:customer-terms:view' ))
2026-08-11 18:43:56 +08:00
const canUpdateTerms = computed (() => canViewTerms . value && access . can ( 'platform:finance:customer-terms:update' ))
2026-08-17 17:49:10 +08:00
const canExport = computed (() => access . can ( 'platform:finance:customer-account:export' ) && access . can ( 'platform:download:list' ))
2026-09-05 08:34:40 +08:00
const canAdjust = computed (() => access . can ( 'platform:finance:customer-account:adjust' ))
const canReviewRecharge = computed (() => access . can ( 'platform:finance:recharge:review' ))
2026-08-14 12:50:42 +08:00
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 ) })) },
2026-08-11 18:43:56 +08:00
{ key : 'status' , type : 'select' , label : '状态' , width : 150 , placeholder : '全部状态' , options : [{ label : '正常' , value : 'ACTIVE' }, { label : '停用' , value : 'DISABLED' }] },
2026-08-14 12:50:42 +08:00
])
2026-08-11 18:06:35 +08:00
const columns : DataTableColumn [] = [
2026-09-07 10:11:23 +08:00
{ key : 'actions' , label : '操作' , width : 290 , fixed : 'left' , slotName : 'actions' },
2026-08-14 14:27:03 +08:00
{ key : 'customerName' , label : '客户' , minWidth : 180 , slotName : 'customerName' },
2026-08-17 19:52:04 +08:00
{ key : 'customerCode' , label : '客户编号' , width : 150 , slotName : 'customerCode' },
{ key : 'contactName' , label : '联系人' , width : 130 , slotName : 'contactName' },
{ key : 'contactPhone' , label : '联系电话' , width : 150 , slotName : 'contactPhone' },
2026-09-05 08:34:40 +08:00
{ key : 'actualBalance' , label : '实际余额(CNY)' , width : 155 , slotName : 'actualBalance' },
{ key : 'frozenBalance' , label : '冻结金额(CNY)' , width : 150 , slotName : 'frozenBalance' },
2026-09-05 11:54:55 +08:00
{ key : 'availableBalance' , label : '总可用余额(CNY)' , width : 155 , slotName : 'availableBalance' },
{ key : 'creditLimit' , label : '授信额度(CNY)' , width : 155 , slotName : 'creditLimit' },
2026-09-10 22:43:10 +08:00
{ key : 'creditExpiresOn' , label : '授信有效期' , width : 140 , slotName : 'creditExpiresOn' },
2026-09-05 11:54:55 +08:00
{ key : 'creditUsed' , label : '已用授信(CNY)' , width : 150 , slotName : 'creditUsed' },
2026-09-05 08:34:40 +08:00
{ key : 'pendingRechargeCount' , label : '待审核充值' , width : 120 , slotName : 'pendingRechargeCount' },
2026-08-11 18:06:35 +08:00
{ key : 'status' , label : '客户状态' , width : 110 , slotName : 'status' },
2026-08-15 10:03:03 +08:00
{ key : 'lastChangedAt' , label : '最近变动' , width : 170 , slotName : 'lastChangedAt' },
2026-08-11 18:06:35 +08:00
]
2026-08-13 11:57:44 +08:00
const ledgerQueryFields : QueryField [] = [
{ key : 'businessType' , type : 'select' , label : '流水类型' , width : 180 , placeholder : '全部类型' , options : [
2026-08-17 16:45:46 +08:00
{ label : '生成对账单' , value : 'RECONCILIATION_CREATED' }, { label : '结算单生效' , value : 'SETTLEMENT_ISSUED' },
2026-09-01 10:14:38 +08:00
{ label : '收款' , value : 'RECEIPT' },
{ label : '供应商线下直付' , value : 'SUPPLIER_DIRECT_PAID' },
{ label : '供应商账期付款' , value : 'SUPPLIER_ON_ACCOUNT_PAYMENT' },
2026-08-17 16:45:46 +08:00
{ label : '账务调整' , value : 'ADJUSTMENT' }, { label : '作废' , value : 'VOID' },
2026-08-13 11:57:44 +08:00
] },
{ key : 'occurredRange' , type : 'daterange' , label : '发生日期' , width : 280 },
]
const ledgerColumns : DataTableColumn [] = [
2026-08-17 16:45:46 +08:00
{ key : 'ledgerId' , label : '流水号' , width : 180 , slotName : 'ledgerId' }, { key : 'occurredAt' , label : '发生时间' , width : 170 , slotName : 'occurredAt' }, { key : 'businessType' , label : '业务类型' , width : 140 , slotName : 'businessType' },
2026-09-05 08:34:40 +08:00
{ 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' },
2026-09-05 11:54:55 +08:00
{ key : 'creditUsedAfter' , label : '变动后已用授信' , width : 160 , slotName : 'creditUsedAfter' },
2026-09-05 08:34:40 +08:00
{ key : 'operatorName' , label : '操作人' , width : 140 , slotName : 'operatorName' }, { key : 'remark' , label : '备注' , minWidth : 180 },
2026-08-13 11:57:44 +08:00
]
2026-09-05 08:34:40 +08:00
const rechargeColumns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 150 , fixed : 'left' , slotName : 'actions' },
{ key : 'requestNo' , label : '申请单号' , minWidth : 190 }, { key : 'createdAt' , label : '提交时间' , width : 170 , slotName : 'createdAt' },
2026-09-19 17:02:17 +08:00
{ key : 'amount' , label : '充值金额(CNY)' , width : 150 , slotName : 'amount' },
2026-09-05 08:34:40 +08:00
{ 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' )))
2026-08-11 18:06:35 +08:00
2026-08-11 19:59:15 +08:00
async function load ( model : QueryModel = filters . value ) {
Object . assign ( filters . value , model )
2026-08-11 18:43:56 +08:00
if ( ! canView . value ) return
2026-08-11 18:06:35 +08:00
loading . value = true
2026-08-11 18:43:56 +08:00
loadError . value = null
2026-08-11 18:06:35 +08:00
try {
2026-08-14 12:50:42 +08:00
const result = await listFinanceAccounts ({ page : page . value , size : pageSize . value , customerId : String ( filters . value . customerId || '' ). trim () || undefined , status : String ( filters . value . status || '' ) })
2026-08-11 18:43:56 +08:00
rows . value = result . records
total . value = result . total
2026-08-11 18:06:35 +08:00
} catch ( error ) { loadError . value = getApiErrorDetails ( error , '客户账户加载失败,请重试' ) }
finally { loading . value = false }
}
2026-08-14 12:50:42 +08:00
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 }
}
2026-08-11 18:43:56 +08:00
function search ( model : QueryModel ) { page . value = 1 ; void load ( model ) }
2026-08-11 18:06:35 +08:00
async function openAccount ( row : Record < string , unknown >, edit : boolean ) {
2026-08-11 18:43:56 +08:00
selected . value = row as unknown as FinanceAccountSummary
editing . value = edit
2026-08-13 11:57:44 +08:00
if ( ! edit ) {
await router . push ({ name : 'finance-customer-account-detail' , params : { customerId : selected . value . customerId } })
return
}
detailOpen . value = true
2026-08-11 18:43:56 +08:00
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
}
2026-08-13 11:57:44 +08:00
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 ) : []
2026-08-11 18:43:56 +08:00
try {
2026-08-13 11:57:44 +08:00
const result = await listFinanceAccountLedger ( selected . value . customerId , { page : ledgerPage . value , size : ledgerPageSize . value , businessType : String ( ledgerFilters . value . businessType || '' ), startDate : range [ 0 ], endDate : range [ 1 ] })
2026-08-11 18:43:56 +08:00
ledger . value = result . records
2026-08-13 11:57:44 +08:00
ledgerTotal . value = result . total
2026-08-11 18:43:56 +08:00
} catch ( error ) { ledgerError . value = getApiErrorDetails ( error , '客户账户流水加载失败,请重试' ) }
finally { ledgerLoading . value = false }
2026-08-11 18:06:35 +08:00
}
2026-09-05 08:34:40 +08:00
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 ()])
}
2026-08-11 18:43:56 +08:00
async function retryDrawer () { if ( selected . value ) await openAccount ( selected . value as unknown as Record < string , unknown >, editing . value ) }
2026-08-11 18:06:35 +08:00
async function saveTerms () {
if ( ! selected . value || ! Number . isInteger ( creditDays . value ) || creditDays . value < 0 || creditDays . value > 3650 ) { ElMessage . warning ( '账期必须是 0 到 3650 的整数天' ); return }
saving . value = true
2026-08-11 18:43:56 +08:00
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 ) }
2026-08-11 18:06:35 +08:00
finally { saving . value = false }
}
2026-09-05 08:34:40 +08:00
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 } `
}
2026-09-10 22:43:10 +08:00
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 } 已到期`
}
2026-09-05 08:34:40 +08:00
function paymentMethodLabel ( value : unknown ) {
return ({ BANK_TRANSFER : '银行转账' , WECHAT : '微信' , ALIPAY : '支付宝' , CASH : '现金' , OTHER : '其他' } as Record < string , string >)[ String ( value )] || '其他'
}
function walletBusinessType ( value : unknown ) {
2026-09-05 11:54:55 +08:00
return ({ RECHARGE : '充值' , DEDUCTION : '扣款' , FREEZE : '冻结' , UNFREEZE : '解冻' , CONSUMPTION : '消费结算' , CREDIT_LIMIT : '授信调整' } as Record < string , string >)[ String ( value )] || ledgerBusinessType ( value )
2026-09-05 08:34:40 +08:00
}
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 ( ! 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 }
}
2026-09-05 11:54:55 +08:00
function openCreditLimit ( account : FinanceAccountSummary ) {
adjustmentAccount . value = account
2026-09-10 22:43:10 +08:00
Object . assign ( creditLimitForm , {
amount : Number ( account . creditLimit || 0 ). toFixed ( 2 ),
expiresOn : account . creditActive ? account . creditExpiresOn || '' : '' ,
remark : '' ,
})
2026-09-05 11:54:55 +08:00
creditLimitOpen . value = true
}
2026-09-07 10:11:23 +08:00
function handleAccountMore ( command : string , account : FinanceAccountSummary ) {
if ( command === 'credit' ) openCreditLimit ( account )
if ( command === 'terms' ) void openAccount ( account as unknown as Record < string , unknown >, true )
}
2026-09-18 16:48:40 +08:00
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 )
}
2026-09-05 11:54:55 +08:00
async function submitCreditLimit () {
if ( ! 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
}
2026-09-10 22:43:10 +08:00
if ( ! creditLimitForm . expiresOn || creditLimitForm . expiresOn <= today ()) {
ElMessage . warning ( '授信到期日必须晚于今天' )
return
}
2026-09-05 11:54:55 +08:00
creditLimitSaving . value = true
try {
await updateFinanceCreditLimit ( adjustmentAccount . value . customerId , {
2026-09-10 22:43:10 +08:00
creditLimit : amount . toFixed ( 2 ), creditExpiresOn : creditLimitForm . expiresOn ,
remark : creditLimitForm . remark . trim () || undefined ,
2026-09-05 11:54:55 +08:00
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 }
}
2026-09-05 08:34:40 +08:00
function openReview ( request : FinanceRechargeRequest ) {
reviewRequest . value = request
Object . assign ( reviewForm , { decision : 'APPROVE' , remark : '' })
reviewOpen . value = true
}
async function submitReview () {
if ( ! 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 () {
if ( voucherPreviewUrl . value ) URL . revokeObjectURL ( voucherPreviewUrl . value )
voucherPreviewUrl . value = ''
}
async function previewVoucher ( request : FinanceRechargeRequest ) {
clearVoucherPreview ()
voucherPreviewName . value = request . voucherFileName || '付款凭证'
voucherPreviewOpen . value = true
voucherPreviewLoading . value = true
try {
const blob = await downloadFinanceRechargeVoucher ( request . requestId )
voucherPreviewUrl . value = URL . createObjectURL ( blob )
} catch ( error ) {
voucherPreviewOpen . value = false
ElMessage . error ( getApiErrorMessage ( error , '付款凭证加载失败' ))
} finally { voucherPreviewLoading . value = false }
}
2026-08-14 14:27:03 +08:00
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 ()
2026-08-17 16:45:46 +08:00
const known : Record < string , string > = { REC : '收款' , RECONCILIATION : '对账单' , FEE_SOURCE : '费用来源' }
2026-08-14 14:27:03 +08:00
return known [ code ] || financeBusinessTypeLabel ( code )
}
function displayOperator ( value : unknown ) {
const operator = String ( value || '' ). trim ()
2026-08-17 16:45:46 +08:00
if ( ! operator ) return '名称数据缺失'
return /^(?:operator-)?\d+$/i . test ( operator ) ? '名称数据缺失' : operator
2026-08-14 14:27:03 +08:00
}
function formatDateTime ( value : unknown ) {
const raw = String ( value || '' ). trim ()
2026-08-17 16:45:46 +08:00
if ( ! raw ) return '时间数据缺失'
2026-08-14 14:27:03 +08:00
const date = new Date ( raw )
2026-08-17 16:45:46 +08:00
return Number . isNaN ( date . getTime ()) ? '时间数据缺失' : date . toLocaleString ( 'zh-CN' , { hour12 : false })
2026-08-14 14:27:03 +08:00
}
2026-08-17 16:45:46 +08:00
function formatLastChangedAt ( value : unknown ) { return String ( value || '' ). trim () ? formatDateTime ( value ) : '暂无变动' }
2026-08-17 19:52:04 +08:00
function customerField ( value : unknown ) { return String ( value || '' ). trim () || '未维护' }
2026-08-17 16:45:46 +08:00
function displayDocumentNo ( value : unknown ) { return String ( value || '' ). trim () || '关联单据缺失' }
function displayLedgerId ( value : unknown ) { return String ( value || '' ). trim () || '流水号缺失' }
2026-08-17 17:49:10 +08:00
async function finishExport ( task : Awaited < ReturnType < typeof createPlatformExportTask > > ) {
2026-09-15 09:58:15 +08:00
const openDownloadCenter = await showExportTaskCreatedDialog ({ taskName : task . taskName , taskNo : task . taskNo , dense : true })
2026-08-17 17:49:10 +08:00
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 }
2026-08-13 11:57:44 +08:00
}
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 {
2026-09-05 08:34:40 +08:00
selected . value = ( await listFinanceAccounts ({ page : 1 , size : 1 , customerId })). records . find ( row => row . customerId === customerId ) || null
2026-08-13 11:57:44 +08:00
if ( ! selected . value ) { ledgerError . value = { message : '未找到该客户账户,可能已停用或无权访问' }; return }
2026-09-05 08:34:40 +08:00
await Promise . all ([ loadLedger (), loadRechargeRequests ()])
2026-08-13 11:57:44 +08:00
} catch ( error ) { ledgerError . value = getApiErrorDetails ( error , '客户账户详情加载失败,请重试' ) }
}
onMounted (() => {
const customerId = String ( route . params . customerId || '' )
2026-08-14 14:27:03 +08:00
if ( route . name === 'finance-customer-account-detail' && customerId ) { if ( canListCustomers . value ) void loadCustomers (); void restoreAccountDetail ( customerId ) }
2026-08-14 12:50:42 +08:00
else { if ( canListCustomers . value ) void loadCustomers (); void load () }
2026-08-13 11:57:44 +08:00
})
2026-09-05 08:34:40 +08:00
onBeforeUnmount ( clearVoucherPreview )
2026-08-11 18:06:35 +08:00
</ script >
< template >
2026-09-15 09:58:15 +08:00
< AdminPage as = "div" class = "finance-account-page admin-density" spacing = "normal" >
2026-08-11 18:43:56 +08:00
< el-result v-if = "!canView" icon="warning" title="无权访问客户账户" sub-title="请联系管理员开通客户账户查看权限" />
2026-08-13 11:57:44 +08:00
< ListPageLayout v-else-if = "!detailOpen" aria-label="客户账户" >
2026-09-30 14:01:58 +08:00
< section >< 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 >
2026-08-17 17:49:10 +08:00
< template # toolbar -actions >< el-button v-if = "canExport" :loading="accountExportBusy" :disabled="loading || accountExportBusy" @click="exportAccounts"><Download :size="16" /> 导出Excel </ el-button ></ template >
2026-09-30 14:01:58 +08:00
< template # actions = "{ row }" >< RowActions text -actions : actions = "accountActions(row as FinanceAccountSummary)" : more -label ="` 更多客户账户操作 · $ { ( row as FinanceAccountSummary ) .customerName || ' 当前客户 ' } `" @action ="handleAccountAction($event, row as FinanceAccountSummary)" /></ template >
2026-08-14 14:27:03 +08:00
< template # customerName = "{ row }" >{{ customerName ( row . customerId , row . customerName ) }}</ template >
2026-08-17 19:52:04 +08:00
< template # customerCode = "{ row }" >{{ customerField ( row . customerCode ) }}</ template >< template # contactName = "{ row }" >{{ customerField ( row . contactName ) }}</ template >< template # contactPhone = "{ row }" >{{ customerField ( row . contactPhone ) }}</ template >
2026-08-17 16:45:46 +08:00
< template # lastChangedAt = "{ row }" >{{ formatLastChangedAt ( row . lastChangedAt ) }}</ template >
2026-09-10 22:43:10 +08:00
< 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 >
2026-09-05 08:34:40 +08:00
< template # pendingRechargeCount = "{ row }" >< el-tag v-if = "row.pendingRechargeCount" type="warning" >{{ row.pendingRechargeCount }} 笔 </ el -tag >< span v-else > — </ span ></ template >
2026-08-11 18:06:35 +08:00
< template # status = "{ row }" >< el-tag : type = "row.status === 'ACTIVE' ? 'success' : 'info'" >{{ row . status === 'ACTIVE' ? '正常' : '停用' }}</ el-tag ></ template >
2026-09-05 08:34:40 +08:00
< template # pagination -left >< span > 共 {{ total }} 个客户账户 </ span ></ template >
2026-09-30 14:01:58 +08:00
</ DataTable ></ section >
2026-08-11 18:06:35 +08:00
</ ListPageLayout >
2026-09-15 09:58:15 +08:00
< AdminPage v-else as = "div" class = "finance-account-detail admin-density" spacing = "normal" >
2026-08-17 12:31:27 +08:00
< section class = "surface-panel finance-account-detail__surface" >
2026-09-15 09:58:15 +08:00
< 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 >
2026-08-17 12:31:27 +08:00
< 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" >
2026-09-15 09:58:15 +08:00
< 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" >
2026-09-05 08:34:40 +08:00
< LoadErrorState v-if = "rechargeError" title="充值申请加载失败" :message="rechargeError.message" :loading="rechargeLoading" @retry="loadRechargeRequests" />
< div v-else class = "detail-table-scroll" >< DataTable class = "recharge-table" :data = "sortedRechargeRequests" :columns = "rechargeColumns" :loading = "rechargeLoading" row -key =" requestId " empty -text =" 暂无充值申请 " column -settings-mode =" hide " >
2026-09-15 09:58:15 +08:00
< 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 >
2026-09-05 08:34:40 +08:00
< template # createdAt = "{ row }" >{{ formatDateTime ( row . createdAt ) }}</ template >< template # reviewedAt = "{ row }" >{{ row . reviewedAt ? formatDateTime ( row . reviewedAt ) : '—' }}</ template >
2026-09-19 17:02:17 +08:00
< template # amount = "{ row }" >< strong class = "balance-value is-actual" >{{ money ( row . amount ) }}</ strong ></ template >
2026-09-05 08:34:40 +08:00
< 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 >
2026-09-15 09:58:15 +08:00
< FormSection v-if = "!editing" class="ledger-section density-form-section" title="账户流水" :bordered="false" >
2026-08-26 18:13:12 +08:00
< QueryToolbar v-model = "ledgerFilters" class="ledger-query" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; void loadLedger($event)" />
2026-09-05 11:54:55 +08:00
< div class = "detail-table-scroll" >< 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 >
2026-08-17 12:31:27 +08:00
</ FormSection >
2026-09-15 09:58:15 +08:00
< 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 >
2026-08-17 12:31:27 +08:00
</ 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 >
2026-08-13 11:57:44 +08:00
</ AdminPage >
2026-09-15 09:58:15 +08:00
< AppDialog class = "admin-density-dialog" v-model = "adjustmentOpen" :title="adjustmentForm.type === 'RECHARGE' ? '手工充值' : '手工扣款'" width="min(720px, calc(100vw - 28px))" :confirm-text="adjustmentForm.type === 'RECHARGE' ? '确认充值' : '确认扣款'" :confirm-type="adjustmentForm.type === 'RECHARGE' ? 'primary' : 'danger'" :confirm-loading="adjustmentSaving" @confirm="submitAdjustment" >
2026-09-05 11:54:55 +08:00
< el-alert : title = "adjustmentForm.type === 'RECHARGE' ? '充值会先归还已用授信,剩余金额计入实际余额。' : '扣款先使用未冻结的实际余额,不足部分再使用可用授信。'" : type = "adjustmentForm.type === 'RECHARGE' ? 'info' : 'warning'" :closable = "false" />
2026-09-15 09:58:15 +08:00
< el-form label -position =" top " class = "wallet-form density-form" >< div class = "wallet-form__grid" >< el-form-item label = "客户名称" >< el-input : model -value =" adjustmentAccount ? customerName ( adjustmentAccount.customerId , adjustmentAccount.customerName ) : ''" readonly /></ el-form-item >< el-form-item label = "当前实际余额" >< el-input :model-value = "money(adjustmentAccount?.actualBalance)" readonly /></ el-form-item >< el-form-item v-if = "adjustmentForm.type === 'DEDUCTION'" label="当前可用余额"><el-input :model-value="money(adjustmentAccount?.availableBalance)" readonly /></el-form-item><el-form-item :label="adjustmentForm.type === 'RECHARGE' ? '充值金额' : '扣款金额'" required><el-input v-model="adjustmentForm.amount" inputmode="decimal" placeholder="请输入大于 0 的金额" /></el-form-item><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><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><el-form-item label="关联单据"><el-input v-model="adjustmentForm.referenceNo" maxlength="128" placeholder="请输入关联业务单号(选填)" /></el-form-item></div><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 ></ el-form >
2026-09-05 08:34:40 +08:00
</ AppDialog >
2026-09-15 09:58:15 +08:00
< AppDialog class = "admin-density-dialog" v-model = "creditLimitOpen" title="设置授信额度" width="min(560px, calc(100vw - 28px))" confirm-text="保存授信额度" :confirm-loading="creditLimitSaving" @confirm="submitCreditLimit" >
2026-09-10 22:43:10 +08:00
< el-alert title = "授信到期日当天仍有效,次日授信失效;届时负数实际余额将阻止客户提交新订单。" type = "warning" :closable = "false" />
2026-09-15 09:58:15 +08:00
< el-form label -position =" top " class = "wallet-form density-form" >< el-form-item label = "客户名称" >< el-input : model -value =" adjustmentAccount ? customerName ( adjustmentAccount.customerId , adjustmentAccount.customerName ) : ''" readonly /></ el-form-item >< el-form-item label = "当前已用授信" >< el-input :model-value = "money(adjustmentAccount?.creditUsed)" readonly /></ el-form-item >< el-form-item label = "授信额度" required >< el-input v-model = "creditLimitForm.amount" inputmode="decimal" placeholder="请输入大于或等于 0 的金额" /></el-form-item><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><el-form-item label="调整说明"><el-input v-model="creditLimitForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入授信调整说明(选填)" /></ el-form-item ></ el-form >
2026-09-05 11:54:55 +08:00
</ AppDialog >
2026-09-15 09:58:15 +08:00
< AppDialog class = "admin-density-dialog" v-model = "reviewOpen" title="审核充值申请" width="min(620px, calc(100vw - 28px))" confirm-text="确认审核" :confirm-loading="reviewSaving" @confirm="submitReview" >
2026-09-19 17:02:17 +08:00
< el-descriptions class = "density-descriptions" v-if = "reviewRequest" :column="2" border><el-descriptions-item label="申请单号">{{ reviewRequest.requestNo }}</el-descriptions-item><el-descriptions-item label="充值金额">{{ money(reviewRequest.amount) }}</el-descriptions-item><el-descriptions-item label="付款凭证" >{{ reviewRequest.voucherFileName || '—' }} </ el -descriptions-item ></ el-descriptions >
2026-09-15 09:58:15 +08:00
< el-form label -position =" top " class = "review-form density-form" >< 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><el-form-item label="审核备注"><el-input v-model="reviewForm.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请输入审核备注(选填)" /></ el-form-item ></ el-form >
2026-09-05 08:34:40 +08:00
</ AppDialog >
2026-09-15 09:58:15 +08:00
< AppDialog class = "admin-density-dialog" v-model = "voucherPreviewOpen" :title="voucherPreviewName" width="min(920px, calc(100vw - 28px))" :show-footer="false" @closed="clearVoucherPreview" >
2026-09-05 08:34:40 +08:00
< div v-loading = "voucherPreviewLoading" class="voucher-preview"><img v-if="voucherPreviewUrl" class="voucher-preview__image" :src="voucherPreviewUrl" :alt="voucherPreviewName" /></ div >
</ AppDialog >
2026-08-11 18:06:35 +08:00
</ AdminPage >
</ template >
< style scoped >
2026-08-17 16:45:46 +08:00
. finance - account - page {
min - width : 0 ;
}
. finance - account - detail {
width : 100 % ;
min - width : 0 ;
}
. finance - account - detail__surface {
width : min ( 100 % , 1920 px );
margin : 0 auto ;
overflow : hidden ;
}
. finance - account - detail__layout {
width : 100 % ;
margin : 0 auto ;
}
. detail - footer ,
. term - control {
display : flex ;
align - items : center ;
2026-09-15 09:58:15 +08:00
gap : var ( -- admin - space - 3 );
2026-09-05 08:34:40 +08:00
}
. balance - value {
color : var ( -- cc - text );
font - variant - numeric : tabular - nums ;
}
. balance - value . is - actual {
color : var ( -- el - color - success );
}
. balance - value . is - frozen {
color : var ( -- el - color - warning );
}
2026-09-10 22:43:10 +08:00
. balance - value . is - negative ,
. credit - expired {
color : var ( -- el - color - danger );
}
2026-09-05 08:34:40 +08:00
. voucher - preview {
display : grid ;
min - height : 320 px ;
place - items : center ;
}
. voucher - preview__image {
display : block ;
max - width : 100 % ;
max - height : 70 vh ;
object - fit : contain ;
}
2026-08-17 16:45:46 +08:00
. ledger - section {
2026-09-15 09:58:15 +08:00
padding : var ( -- admin - space - 4 ) 0 0 ;
2026-08-17 16:45:46 +08:00
}
2026-09-05 08:34:40 +08:00
. recharge - section {
2026-09-15 09:58:15 +08:00
padding : var ( -- admin - space - 4 ) 0 0 ;
2026-09-05 08:34:40 +08:00
border - bottom : 1 px solid var ( -- cc - border , # e3e8ef );
}
. recharge - table {
min - width : 0 ;
}
2026-08-17 16:45:46 +08:00
. ledger - query {
2026-09-15 09:58:15 +08:00
margin - bottom : var ( -- admin - space - 3 );
2026-08-17 16:45:46 +08:00
}
: deep (. ledger - query . query - toolbar ) {
padding : 0 ;
border : 0 ;
background : transparent ;
}
. 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 % ;
2026-09-15 09:58:15 +08:00
padding : var ( -- admin - space - 4 ) 0 ;
2026-08-17 16:45:46 +08:00
}
. term - editor {
display : grid ;
grid - template - columns : 120 px minmax ( 0 , 220 px );
justify - content : start ;
align - items : center ;
2026-09-15 09:58:15 +08:00
gap : var ( -- admin - space - 5 );
2026-08-17 16:45:46 +08:00
padding : 2 px 0 ;
}
. term - label {
color : var ( -- cc - text );
2026-09-15 09:58:15 +08:00
font - size : var ( -- admin - font - section );
2026-08-17 16:45:46 +08:00
font - weight : 600 ;
}
. term - control : deep (. el - input - number ) {
width : 220 px ;
}
. term - unit {
color : var ( -- cc - muted );
}
2026-09-05 08:34:40 +08:00
. wallet - form ,
. review - form {
2026-09-15 09:58:15 +08:00
margin - top : var ( -- admin - space - 5 );
2026-09-05 08:34:40 +08:00
}
. wallet - form__grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ));
2026-09-15 09:58:15 +08:00
gap : 0 var ( -- admin - space - 5 );
2026-09-05 08:34:40 +08:00
}
. wallet - form : deep (. el - date - editor ),
. wallet - form : deep (. el - select ) {
width : 100 % ;
}
2026-08-17 16:45:46 +08:00
. detail - footer {
width : 100 % ;
justify - content : flex - end ;
margin - top : 0 ;
2026-09-15 09:58:15 +08:00
padding - top : var ( -- admin - space - 3 );
2026-08-17 16:45:46 +08:00
border - top : 0 ;
}
2026-08-17 16:58:35 +08:00
@ media ( min - width : 1920 px ) {
. detail - footer {
padding - top : 0 ;
}
}
2026-08-17 16:45:46 +08:00
@ media ( max - width : 1080 px ) {
. account - metrics {
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ));
}
2026-08-20 19:10:19 +08:00
. account - metric : nth - child ( 3 ),
. account - metric : nth - child ( 5 ) {
2026-08-17 16:45:46 +08:00
padding - left : 0 ;
border - left : 0 ;
}
. finance - account - detail__surface {
width : 100 % ;
}
}
2026-09-05 08:34:40 +08:00
@ media ( max - width : 720 px ) {
. wallet - form__grid {
grid - template - columns : 1 fr ;
}
}
2026-08-17 16:45:46 +08:00
@ media ( max - width : 720 px ) {
. detail - heading {
align - items : stretch ;
flex - direction : column ;
}
. detail - back {
margin - left : 0 ;
}
. detail - footer . el - button {
min - height : 44 px ;
}
. term - editor {
grid - template - columns : 1 fr ;
}
. account - metrics {
grid - template - columns : 1 fr ;
gap : 12 px ;
}
. account - metric ,
. account - metric : first - child ,
2026-08-20 19:10:19 +08:00
. account - metric : nth - child ( 3 ),
. account - metric : nth - child ( 5 ) {
2026-08-17 16:45:46 +08:00
padding - left : 0 ;
}
. account - metric + . account - metric {
border - left : 0 ;
}
}
@ media ( max - width : 480 px ) {
. detail - footer . el - button {
flex : 1 1 120 px ;
}
}
2026-08-11 18:06:35 +08:00
</ style >