优化
This commit is contained in:
@@ -321,7 +321,7 @@ describe('fee source and statement native dialogs', () => {
|
||||
const prompt = vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue(Object.assign('confirm', { value: '测试原因', action: 'confirm' as const }))
|
||||
const { wrapper } = await mountPage('statements')
|
||||
await rowClick(wrapper, label)
|
||||
expect(prompt).toHaveBeenCalledWith('请输入操作原因', expect.stringContaining(statement.billNo), expect.objectContaining({ customClass: 'admin-density-confirm', inputPattern: /\S+/ }))
|
||||
expect(prompt).toHaveBeenCalledWith('请输入操作原因', expect.stringContaining(statement.billNo), expect.objectContaining({ customClass: 'admin-density-confirm admin-workbench', inputPattern: /\S+/ }))
|
||||
expect(api.actionReconciliationStatement).toHaveBeenCalledWith(statement.id, { action, reason: '测试原因', expectedVersion: 3 })
|
||||
})
|
||||
it('does not change statement state when its reason prompt is cancelled', async () => {
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
expect(page).toContain('<DetailPageLayout')
|
||||
expect(page).toContain('class="density-detail-overview account-metrics"')
|
||||
for (const metric of ['实际余额', '冻结金额', '总可用余额', '授信额度', '已用授信', '可用授信']) expect(page).toContain(metric)
|
||||
expect(page).toContain('class="ledger-query"')
|
||||
expect(page).toContain('class="ledger-query workbench-query"')
|
||||
expect(page).toContain('@search="ledgerPage = 1; void loadLedger($event)"')
|
||||
expect(page).toContain('class="ledger-table"')
|
||||
expect(page).toContain('<template #toolbar-actions>')
|
||||
@@ -87,10 +87,10 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
for (const column of ['实际余额(CNY)', '冻结金额(CNY)', '总可用余额(CNY)', '授信额度(CNY)', '已用授信(CNY)', '待审核充值']) expect(page).toContain(column)
|
||||
expect(page).toContain('grid-template-columns: 120px minmax(0, 220px)')
|
||||
expect(page).toContain('justify-content: start')
|
||||
expect(page).toContain('class="surface-panel finance-account-detail__surface"')
|
||||
expect(page).toContain('class="surface-panel finance-account-detail__surface workbench-panel is-flush is-page-surface"')
|
||||
expect(page).toContain('class="density-detail-header"')
|
||||
expect(page).toContain('padding: var(--admin-space-4) 0')
|
||||
expect(page).toContain('class="detail-table-scroll"')
|
||||
expect(page).toContain('class="detail-table-scroll workbench-table"')
|
||||
expect(page).toContain('class="ledger-identifier"')
|
||||
expect(page).toContain('class="ledger-document"')
|
||||
expect(page).toContain('text-overflow: ellipsis')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import '@/styles/adminDensity.css'
|
||||
import '../financeDensity.css'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
@@ -440,12 +441,12 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="finance-account-page admin-density" spacing="normal">
|
||||
<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><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()">
|
||||
<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>
|
||||
@@ -463,7 +464,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<AdminPage v-else as="div" class="finance-account-detail admin-density" spacing="normal">
|
||||
<section class="surface-panel finance-account-detail__surface">
|
||||
<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>
|
||||
@@ -472,7 +473,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
<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"><DataTable class="recharge-table" :data="sortedRechargeRequests" :columns="rechargeColumns" :loading="rechargeLoading" row-key="requestId" empty-text="暂无充值申请" column-settings-mode="hide">
|
||||
<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>
|
||||
@@ -481,8 +482,8 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</DataTable></div>
|
||||
</FormSection>
|
||||
<FormSection v-if="!editing" class="ledger-section density-form-section" title="账户流水" :bordered="false">
|
||||
<QueryToolbar v-model="ledgerFilters" class="ledger-query" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; void loadLedger($event)" />
|
||||
<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>
|
||||
<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>
|
||||
@@ -490,9 +491,9 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</DetailPageLayout>
|
||||
</section>
|
||||
</AdminPage>
|
||||
<AppDialog class="admin-density-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">
|
||||
<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" data-tone="summary" aria-label="当前账户资料">
|
||||
<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>
|
||||
@@ -500,7 +501,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</tbody></table>
|
||||
</section>
|
||||
<el-alert v-if="adjustmentForm.type === 'DEDUCTION'" title="扣款先使用未冻结的实际余额,不足部分再使用可用授信。" type="warning" :closable="false" />
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="账户变动登记">
|
||||
<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>
|
||||
@@ -512,11 +513,11 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</section>
|
||||
</div>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-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">
|
||||
<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" 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>
|
||||
<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" data-tone="work" aria-label="授信额度设置"><h3 class="density-dialog-section__title">授信设置</h3>
|
||||
<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>
|
||||
@@ -525,10 +526,10 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</section>
|
||||
</div>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-dialog" v-model="reviewOpen" :title="`审核充值申请 · ${reviewRequest?.requestNo || '—'}`" width="min(620px, calc(100vw - 28px))" fullscreenable confirm-text="确认审核" :confirm-loading="reviewSaving" @confirm="submitReview">
|
||||
<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" 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" data-tone="work" aria-label="充值审核登记"><h3 class="density-dialog-section__title">审核登记</h3>
|
||||
<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>
|
||||
@@ -536,7 +537,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</section>
|
||||
</div>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-dialog" v-model="voucherPreviewOpen" :title="voucherPreviewName" width="min(920px, calc(100vw - 28px))" fullscreenable :loading="voucherPreviewLoading" :show-footer="false">
|
||||
<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>
|
||||
@@ -571,7 +572,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
color: var(--cc-text);
|
||||
color: var(--workbench-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -607,7 +608,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
|
||||
.recharge-section {
|
||||
padding: var(--admin-space-4) 0 0;
|
||||
border-bottom: 1px solid var(--cc-border, #e3e8ef);
|
||||
border-bottom: 1px solid var(--workbench-border);
|
||||
}
|
||||
|
||||
.recharge-table {
|
||||
@@ -620,8 +621,6 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
|
||||
:deep(.ledger-query.query-toolbar) {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.detail-table-scroll {
|
||||
@@ -656,7 +655,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
}
|
||||
|
||||
.term-label {
|
||||
color: var(--cc-text);
|
||||
color: var(--workbench-text);
|
||||
font-size: var(--admin-font-section);
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -666,7 +665,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
}
|
||||
|
||||
.term-unit {
|
||||
color: var(--cc-muted);
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
.wallet-summary { table-layout: fixed; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import '@/styles/adminDensity.css'
|
||||
import '../financeDensity.css'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
@@ -396,13 +397,13 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="finance-arap-page admin-density" spacing="normal">
|
||||
<AdminPage as="div" class="finance-arap-page admin-density admin-workbench" spacing="normal">
|
||||
<OperationErrorToast :details="reviewError || mutationError" />
|
||||
<el-result v-if="!canView" icon="warning" title="无权访问客户应收应付" sub-title="请联系管理员开通账单查看权限" />
|
||||
<ListPageLayout v-else-if="!detailOpen" aria-label="客户应收应付">
|
||||
|
||||
|
||||
<section><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!customersError && !loadError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" max-height="calc(100vh - 300px)" preference-key="finance-customer-arap-table" aria-label="客户应收应付列表" row-key="id" empty-text="暂无客户应收应付数据" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<section class="workbench-table is-page-surface"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!customersError && !loadError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" max-height="calc(100vh - 300px)" preference-key="finance-customer-arap-table" aria-label="客户应收应付列表" row-key="id" empty-text="暂无客户应收应付数据" @selection-change="onSelectionChange" @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-arap-query" @search="search" /></template>
|
||||
<template #notices><template v-if="stateError"><LoadErrorState title="客户应收应付加载失败" :message="stateError.message" :loading="customersLoading || loading" @retry="customersError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="stateError" /></template></LoadErrorState></template></template>
|
||||
@@ -418,18 +419,18 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<AdminPage v-else as="div" class="finance-arap-detail admin-density" spacing="normal">
|
||||
<section class="surface-panel finance-arap-detail__surface">
|
||||
<section class="surface-panel finance-arap-detail__surface workbench-panel is-flush is-page-surface">
|
||||
<DetailPageLayout class="finance-arap-detail__layout finance-density-detail" aria-label="费用来源详情">
|
||||
<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?.documentNo || '费用来源详情' }}</h1><span class="finance-detail-context">{{ selected ? customerName(selected.customerId, selected.customerName) : '—' }}</span><StatusBadge v-if="detail" :status="statusLabel(detail.reviewStatus)" /></div><div class="finance-detail-actions"><el-button v-if="canEditSource && detail && selected && !isUpstreamSource(detail) && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected, 'EDIT')"><Pencil :size="16" />编辑</el-button><el-button v-if="canEditSource && detail && selected && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected, 'SUBMIT')"><Send :size="16" />提交审核</el-button><el-button v-if="canReviewSource && detail?.reviewStatus === 'PENDING' && selected" @click="openReview(selected)"><FileCheck2 :size="16" />审核</el-button></div></div></template>
|
||||
<template v-if="detail" #summary><dl class="density-detail-overview arap-metrics" aria-label="费用来源概览"><div class="arap-metric is-amount"><dt>结算金额</dt><dd>{{ money(detail.amount) }} <small>CNY</small></dd></div><div class="arap-metric"><dt>收付方向</dt><dd>{{ financeDirectionLabel(detail.direction) }}</dd></div><div class="arap-metric"><dt>来源状态</dt><dd>{{ sourceStatusDisplay(detail.sourceStatus) }}</dd></div><div class="arap-metric"><dt>审核状态</dt><dd>{{ statusLabel(detail.reviewStatus) }}</dd></div></dl></template>
|
||||
<LoadErrorState v-if="detailError" title="费用来源详情加载失败" :message="detailError.message" :loading="detailLoading" @retry="selected && loadDetail(selected)"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<template v-else-if="detail"><div class="arap-detail-grid"><FormSection class="arap-information density-form-section" title="单据信息" :bordered="false"><el-descriptions border :column="2" class="arap-detail density-descriptions"><el-descriptions-item label="客户">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ displayFeeName(detail.feeItemName) || '—' }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ sourceTypeDisplay(detail.sourceType) }}</el-descriptions-item><el-descriptions-item label="关联业务">{{ detail.inboundCode || detail.containerNo || detail.sourceBusinessId || '业务关联待补充' }}</el-descriptions-item><el-descriptions-item v-if="detail.direction === 'PAYABLE'" label="供应商">{{ detail.supplierName || '供应商待补充' }}</el-descriptions-item><el-descriptions-item label="业务日期">{{ formatBusinessDate(detail.businessDate) }}</el-descriptions-item><el-descriptions-item label="原币金额">{{ detail.originalCurrency }} {{ money(detail.originalAmount) }}</el-descriptions-item><el-descriptions-item label="折算汇率">{{ detail.exchangeRate }}<template v-if="detail.exchangeRateMonth">({{ detail.exchangeRateMonth }})</template></el-descriptions-item><el-descriptions-item v-if="detail.quantity != null" label="计费依据">{{ detail.quantity }} {{ detail.quantityUnit || '' }}<template v-if="detail.unitPrice != null"> × {{ detail.originalCurrency }} {{ detail.unitPrice }}</template></el-descriptions-item><el-descriptions-item v-if="detail.periodStart" label="计费期间">{{ detail.periodStart }} 至 {{ detail.periodEnd }}</el-descriptions-item></el-descriptions></FormSection><FormSection class="arap-settlement density-form-section" title="审核与结算" :bordered="false"><el-descriptions border :column="1" class="arap-detail density-descriptions"><el-descriptions-item label="费用状态">{{ sourceStatusDisplay(detail.sourceStatus) }}</el-descriptions-item><el-descriptions-item label="已生成对账单">{{ detail.statementGenerated ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="审核原因">{{ detail.reviewReason || '未填写' }}</el-descriptions-item></el-descriptions></FormSection></div><FormSection class="operation-log density-form-section" title="操作记录" :bordered="false"><DataTable :data="operationLogRows" :columns="operationLogColumns" row-key="rowKey" column-settings-mode="hide" empty-text="暂无操作记录"><template #occurredAt="{ row }">{{ formatDateTime(row.occurredAt) }}</template><template #action="{ row }">{{ financeOperationActionLabel(row.action) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template><template #result="{ row }">{{ row.remark || financeOperationResultLabel(row.result) }}</template></DataTable></FormSection></template>
|
||||
<template v-else-if="detail"><div class="arap-detail-grid"><FormSection class="arap-information density-form-section" title="单据信息" :bordered="false"><el-descriptions border :column="2" class="arap-detail density-descriptions"><el-descriptions-item label="客户">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ displayFeeName(detail.feeItemName) || '—' }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ sourceTypeDisplay(detail.sourceType) }}</el-descriptions-item><el-descriptions-item label="关联业务">{{ detail.inboundCode || detail.containerNo || detail.sourceBusinessId || '业务关联待补充' }}</el-descriptions-item><el-descriptions-item v-if="detail.direction === 'PAYABLE'" label="供应商">{{ detail.supplierName || '供应商待补充' }}</el-descriptions-item><el-descriptions-item label="业务日期">{{ formatBusinessDate(detail.businessDate) }}</el-descriptions-item><el-descriptions-item label="原币金额">{{ detail.originalCurrency }} {{ money(detail.originalAmount) }}</el-descriptions-item><el-descriptions-item label="折算汇率">{{ detail.exchangeRate }}<template v-if="detail.exchangeRateMonth">({{ detail.exchangeRateMonth }})</template></el-descriptions-item><el-descriptions-item v-if="detail.quantity != null" label="计费依据">{{ detail.quantity }} {{ detail.quantityUnit || '' }}<template v-if="detail.unitPrice != null"> × {{ detail.originalCurrency }} {{ detail.unitPrice }}</template></el-descriptions-item><el-descriptions-item v-if="detail.periodStart" label="计费期间">{{ detail.periodStart }} 至 {{ detail.periodEnd }}</el-descriptions-item></el-descriptions></FormSection><FormSection class="arap-settlement density-form-section" title="审核与结算" :bordered="false"><el-descriptions border :column="1" class="arap-detail density-descriptions"><el-descriptions-item label="费用状态">{{ sourceStatusDisplay(detail.sourceStatus) }}</el-descriptions-item><el-descriptions-item label="已生成对账单">{{ detail.statementGenerated ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="审核原因">{{ detail.reviewReason || '未填写' }}</el-descriptions-item></el-descriptions></FormSection></div><FormSection class="operation-log density-form-section" title="操作记录" :bordered="false"><div class="workbench-table"><DataTable :data="operationLogRows" :columns="operationLogColumns" row-key="rowKey" column-settings-mode="hide" empty-text="暂无操作记录"><template #occurredAt="{ row }">{{ formatDateTime(row.occurredAt) }}</template><template #action="{ row }">{{ financeOperationActionLabel(row.action) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template><template #result="{ row }">{{ row.remark || financeOperationResultLabel(row.result) }}</template></DataTable></div></FormSection></template>
|
||||
</DetailPageLayout>
|
||||
</section>
|
||||
</AdminPage>
|
||||
<AppDialog class="admin-density-dialog" v-model="reviewOpen" :title="`费用来源审核 · ${selected?.documentNo || '费用来源'}`" fullscreenable confirm-text="提交审核" :confirm-loading="reviewSaving" @confirm="submitReview">
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="reviewOpen" :title="`费用来源审核 · ${selected?.documentNo || '费用来源'}`" fullscreenable confirm-text="提交审核" :confirm-loading="reviewSaving" @confirm="submitReview">
|
||||
<div class="density-dialog-sections">
|
||||
<section v-if="detail" class="density-dialog-section" data-tone="source" aria-label="费用来源资料">
|
||||
<section v-if="detail" class="density-dialog-section workbench-evidence" data-tone="source" aria-label="费用来源资料">
|
||||
<h3 class="density-dialog-section__title">费用来源</h3>
|
||||
<table class="density-table fee-dialog-facts"><tbody>
|
||||
<tr><th scope="row">客户</th><td colspan="3">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</td></tr>
|
||||
@@ -438,7 +439,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
<tr><th scope="row">收付方向</th><td>{{ financeDirectionLabel(detail.direction) }}</td><th scope="row">审核状态</th><td>{{ statusLabel(detail.reviewStatus) }}</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="费用审核登记"><h3 class="density-dialog-section__title">审核登记</h3>
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="费用审核登记"><h3 class="density-dialog-section__title">审核登记</h3>
|
||||
<el-form class="density-form" label-position="left" label-width="110px" :disabled="reviewSaving"><table class="density-table density-table--editable"><tbody>
|
||||
<tr><td><el-form-item label="审核结论"><el-segmented v-model="reviewForm.action" :options="[{ label: '通过', value: 'APPROVE' }, { label: '驳回', value: 'REJECT' }]" /></el-form-item></td></tr>
|
||||
<tr><td><el-form-item :label="reviewForm.action === 'REJECT' ? '驳回原因' : '审核说明(可选)'" :required="reviewForm.action === 'REJECT'"><el-input v-model="reviewForm.reason" type="textarea" :rows="3" maxlength="500" :placeholder="reviewForm.action === 'REJECT' ? '请填写驳回原因' : '请输入审核说明(可选)'" show-word-limit /></el-form-item></td></tr>
|
||||
@@ -446,9 +447,9 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
</section>
|
||||
</div>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-dialog" v-model="mutationOpen" :title="`${mutationTitle} · ${selected?.documentNo || '费用来源'}`" fullscreenable :confirm-type="mutationMode === 'VOID' ? 'danger' : 'primary'" :confirm-text="mutationMode === 'VOID' ? '确认作废' : '确认'" :confirm-loading="mutationSaving" @confirm="submitMutation">
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="mutationOpen" :title="`${mutationTitle} · ${selected?.documentNo || '费用来源'}`" fullscreenable :confirm-type="mutationMode === 'VOID' ? 'danger' : 'primary'" :confirm-text="mutationMode === 'VOID' ? '确认作废' : '确认'" :confirm-loading="mutationSaving" @confirm="submitMutation">
|
||||
<div class="density-dialog-sections">
|
||||
<section v-if="detail" class="density-dialog-section" data-tone="source" aria-label="费用来源资料">
|
||||
<section v-if="detail" class="density-dialog-section workbench-evidence" data-tone="source" aria-label="费用来源资料">
|
||||
<h3 class="density-dialog-section__title">费用来源</h3>
|
||||
<table class="density-table fee-dialog-facts"><tbody>
|
||||
<tr><th scope="row">客户</th><td colspan="3">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</td></tr>
|
||||
@@ -457,7 +458,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
<tr><th scope="row">收付方向</th><td>{{ financeDirectionLabel(detail.direction) }}</td><th scope="row">审核状态</th><td>{{ statusLabel(detail.reviewStatus) }}</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="费用操作登记"><h3 class="density-dialog-section__title">{{ mutationTitle }}</h3>
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="费用操作登记"><h3 class="density-dialog-section__title">{{ mutationTitle }}</h3>
|
||||
<el-form class="density-form" label-position="left" label-width="110px" :disabled="mutationSaving"><table class="density-table density-table--editable"><tbody>
|
||||
<tr v-if="mutationMode === 'EDIT' || mutationMode === 'ADJUST'"><td><el-form-item label="金额(CNY)" required><el-input-number v-model="mutationForm.amount" :min="0.01" :precision="2" :controls="false" placeholder="请输入调整金额" /></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label="操作原因" required><el-input v-model="mutationForm.reason" type="textarea" :rows="3" maxlength="500" placeholder="请填写操作原因" show-word-limit /></el-form-item></td></tr>
|
||||
@@ -518,14 +519,14 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
}
|
||||
|
||||
.muted-action {
|
||||
color: var(--cc-muted);
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
.arap-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr);
|
||||
gap: var(--admin-space-6);
|
||||
border-bottom: 1px solid var(--cc-border, #e3e8ef);
|
||||
border-bottom: 1px solid var(--workbench-border);
|
||||
}
|
||||
|
||||
.arap-information,
|
||||
@@ -536,7 +537,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
|
||||
.arap-settlement {
|
||||
padding-left: var(--admin-space-6);
|
||||
border-left: 1px solid var(--cc-border, #e3e8ef);
|
||||
border-left: 1px solid var(--workbench-border);
|
||||
}
|
||||
|
||||
|
||||
@@ -548,7 +549,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
|
||||
:deep(.arap-detail .el-descriptions__label) {
|
||||
width: 112px;
|
||||
color: var(--cc-muted);
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
.operation-log :deep(.el-empty) {
|
||||
@@ -562,7 +563,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
.operation-log small {
|
||||
display: block;
|
||||
margin-top: var(--admin-space-1);
|
||||
color: var(--cc-muted);
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
@@ -598,7 +599,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
.arap-settlement {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
border-top: 1px solid var(--cc-border, #e3e8ef);
|
||||
border-top: 1px solid var(--workbench-border);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import '@/styles/adminDensity.css'
|
||||
import '../financeDensity.css'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
@@ -514,7 +515,7 @@ async function createStatement() {
|
||||
async function performAction(row: FinanceStatementReadModel, actionName: ReconciliationActionPayload['action']) {
|
||||
const title = ({ REJECT: '驳回对账单', VOID: '作废对账单', REOPEN: '重开对账单' } as Partial<Record<ReconciliationActionPayload['action'], string>>)[actionName] || '对账单操作'
|
||||
const reason = ['REJECT', 'VOID', 'REOPEN'].includes(actionName)
|
||||
? await ElMessageBox.prompt('请输入操作原因', `${title} · ${row.billNo || '—'}`, { customClass: 'admin-density-confirm', inputType: 'textarea', inputPattern: /\S+/, inputErrorMessage: '操作原因不能为空', confirmButtonText: '确认', cancelButtonText: '取消' }).then((result) => result.value).catch(() => null)
|
||||
? await ElMessageBox.prompt('请输入操作原因', `${title} · ${row.billNo || '—'}`, { customClass: 'admin-density-confirm admin-workbench', inputType: 'textarea', inputPattern: /\S+/, inputErrorMessage: '操作原因不能为空', confirmButtonText: '确认', cancelButtonText: '取消' }).then((result) => result.value).catch(() => null)
|
||||
: undefined
|
||||
if (reason === null) return
|
||||
actionBusy.value = true
|
||||
@@ -606,9 +607,9 @@ watch(detailRouteId, (statementId) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="finance-statements-page admin-density" spacing="normal">
|
||||
<AdminPage as="div" class="finance-statements-page admin-density admin-workbench" spacing="normal">
|
||||
<el-result v-if="!canView" icon="warning" title="无权访问客户对账单" sub-title="请联系管理员开通财务查看权限" />
|
||||
<DetailPageLayout v-else-if="isDetailRoute" class="surface-panel statement-detail-page finance-density-detail" aria-label="客户对账单详情">
|
||||
<DetailPageLayout v-else-if="isDetailRoute" class="surface-panel statement-detail-page finance-density-detail workbench-panel is-flush is-page-surface" aria-label="客户对账单详情">
|
||||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="returnToList"><ArrowLeft :size="16" />返回客户对账单</el-button><h1 class="density-detail-title">{{ detail?.billNo || '—' }}</h1><StatusBadge v-if="detail" :status="financeStatusLabel(detail.status)" /></div></div></template>
|
||||
<template v-if="detail" #summary>
|
||||
<dl class="density-detail-overview"><div><dt>客户</dt><dd>{{ customerDisplayName(detail.customerId, detail.customerName) }}</dd></div><div><dt>期间</dt><dd>{{ detail.periodStart || '—' }} 至 {{ detail.periodEnd || '—' }}</dd></div></dl>
|
||||
@@ -642,7 +643,7 @@ watch(detailRouteId, (statementId) => {
|
||||
<LoadErrorState v-if="detailError" title="明细加载失败" :message="detailError.message" :loading="detailLoading" @retry="retrySelectedDetail"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<el-skeleton v-else-if="detailLoading" :rows="4" animated />
|
||||
<FormSection class="density-form-section" v-else-if="detail" title="账单明细" :bordered="false">
|
||||
<div class="detail-table-scroll">
|
||||
<div class="detail-table-scroll workbench-table">
|
||||
<DataTable class="statement-detail-table statement-line-table" :data="detail.lines" size="small" border stripe :columns="detailLineColumns" column-settings-mode="hide" row-key="statementLineId" empty-text="该对账单暂无账单明细" aria-label="客户对账单账单明细">
|
||||
<template #direction="{ row }">{{ row.direction === 'PAYABLE' ? '应付' : '应收' }}</template>
|
||||
<template #sourceType="{ row }">{{ financeSourceTypeLabel(row.sourceType) }}</template>
|
||||
@@ -657,12 +658,12 @@ watch(detailRouteId, (statementId) => {
|
||||
<template #periodEnd>{{ detail.periodEnd || '—' }}</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
<el-collapse v-if="detail.revisions.length" class="revision-collapse"><el-collapse-item title="调整记录" name="revisions"><el-table :data="detail.revisions" size="small" border stripe><el-table-column prop="revisionNo" label="修订" width="72" /><el-table-column label="动作" width="130"><template #default="{ row }">{{ revisionActionLabel(row.action) }}</template></el-table-column><el-table-column prop="beforeAmount" label="调整前" /><el-table-column prop="afterAmount" label="调整后" /><el-table-column prop="reason" label="原因" /><el-table-column label="操作人" min-width="140"><template #default="{ row }">{{ operatorDisplayName(row.operatorName) }}</template></el-table-column><el-table-column label="发生时间" min-width="170"><template #default="{ row }">{{ formatDateTime(row.occurredAt) }}</template></el-table-column></el-table></el-collapse-item></el-collapse>
|
||||
<el-collapse v-if="detail.revisions.length" class="revision-collapse"><el-collapse-item title="调整记录" name="revisions"><div class="workbench-table"><el-table :data="detail.revisions" size="small" border stripe><el-table-column prop="revisionNo" label="修订" width="72" /><el-table-column label="动作" width="130"><template #default="{ row }">{{ revisionActionLabel(row.action) }}</template></el-table-column><el-table-column prop="beforeAmount" label="调整前" /><el-table-column prop="afterAmount" label="调整后" /><el-table-column prop="reason" label="原因" /><el-table-column label="操作人" min-width="140"><template #default="{ row }">{{ operatorDisplayName(row.operatorName) }}</template></el-table-column><el-table-column label="发生时间" min-width="170"><template #default="{ row }">{{ formatDateTime(row.occurredAt) }}</template></el-table-column></el-table></div></el-collapse-item></el-collapse>
|
||||
</FormSection>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="payments"><template #label><span class="density-tab-icon" aria-hidden="true">💰</span><span>账单支付历史表</span></template>
|
||||
<FormSection class="density-form-section" v-if="detail" title="支付历史" :bordered="false">
|
||||
<div class="detail-table-scroll">
|
||||
<div class="detail-table-scroll workbench-table">
|
||||
<DataTable class="statement-detail-table statement-payment-table" :data="detail.paymentHistory" size="small" border stripe :columns="paymentColumns" column-settings-mode="hide" row-key="transactionId" empty-text="该对账单暂无有效收款记录" aria-label="客户对账单支付历史">
|
||||
<template #customerName>{{ customerDisplayName(detail.customerId, detail.customerName) }}</template>
|
||||
<template #transactionDate="{ row }">{{ formatDateTime(row.transactionDate).slice(0, 10) }}</template>
|
||||
@@ -679,7 +680,7 @@ watch(detailRouteId, (statementId) => {
|
||||
<ListPageLayout v-else aria-label="客户对账单">
|
||||
|
||||
|
||||
<section><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!customerError && !error" v-model:page="page" v-model:page-size="pageSize" :data="paginatedRows" :columns="columns" :loading="loading" :total="filteredRows.length" aria-label="客户对账单列表" row-key="id" empty-text="暂无客户对账单" preference-key="finance-customer-statements" @row-click="openDetail">
|
||||
<section class="workbench-table is-page-surface"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!customerError && !error" v-model:page="page" v-model:page-size="pageSize" :data="paginatedRows" :columns="columns" :loading="loading" :total="filteredRows.length" aria-label="客户对账单列表" row-key="id" empty-text="暂无客户对账单" preference-key="finance-customer-statements" @row-click="openDetail">
|
||||
<template #toolbar-start><h1>客户对账单</h1></template>
|
||||
<template #filters><QueryToolbar v-model="filters" ariaLabel="客户对账单筛选" :fields="fields" :loading="loading" preference-key="finance-statements-query" :search-on-clear="false" @search="load" /></template>
|
||||
<template #notices><template v-if="customerError || error"><LoadErrorState title="客户对账单加载失败" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState></template></template>
|
||||
@@ -708,9 +709,9 @@ watch(detailRouteId, (statementId) => {
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
|
||||
<AppDialog class="admin-density-dialog" v-model="createOpen" title="生成客户对账单" width="min(680px, 94vw)" fullscreenable :confirm-loading="createBusy" @confirm="createStatement">
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="createOpen" title="生成客户对账单" width="min(680px, 94vw)" fullscreenable :confirm-loading="createBusy" @confirm="createStatement">
|
||||
<el-alert v-if="createError" type="warning" :closable="true" show-icon title="暂时无法生成对账单" @close="createError = null"><p>{{ createError.message }}</p></el-alert>
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="对账单生成设置">
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="对账单生成设置">
|
||||
<h3 class="density-dialog-section__title">生成设置 <InlineHelp title="对账单生成说明">按账期自动汇总,或指定审核通过、已发布且未出账的应收费用;生成币种为CNY。</InlineHelp></h3>
|
||||
<el-form class="density-form" label-position="left" label-width="90px" :disabled="createBusy"><table class="density-table density-table--editable statement-create-table"><tbody>
|
||||
<tr><td colspan="2"><el-form-item label="生成方式"><el-radio-group v-model="createForm.generationMode"><el-radio-button value="BY_CUTOFF">按账期自动汇总</el-radio-button><el-radio-button value="SELECTED" :disabled="!canSelectSources">指定费用生成</el-radio-button></el-radio-group></el-form-item></td></tr>
|
||||
@@ -726,13 +727,13 @@ watch(detailRouteId, (statementId) => {
|
||||
</section>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog class="admin-density-dialog" v-model="adjustOpen" :title="`调整客户对账单 · ${adjustStatement?.billNo || '—'}`" width="min(900px, 96vw)" fullscreenable :confirm-loading="adjustBusy" @confirm="submitAdjustment">
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="adjustOpen" :title="`调整客户对账单 · ${adjustStatement?.billNo || '—'}`" width="min(900px, 96vw)" fullscreenable :confirm-loading="adjustBusy" @confirm="submitAdjustment">
|
||||
<OperationErrorToast :details="adjustError" />
|
||||
<el-form class="density-form density-dialog-sections" label-position="left" label-width="110px" :disabled="adjustBusy">
|
||||
<section v-if="adjustStatement" class="density-dialog-section" data-tone="summary" aria-label="当前对账单"><h3 class="density-dialog-section__title">当前对账单</h3><table class="density-table statement-dialog-facts"><tbody><tr><th scope="row">对账单号</th><td>{{ adjustStatement.billNo || '—' }}</td><th scope="row">客户</th><td>{{ customerDisplayName(adjustStatement.customerId, adjustStatement.customerName) }}</td></tr></tbody></table></section>
|
||||
<section v-if="adjustStatement" class="density-dialog-section workbench-evidence" data-tone="summary" aria-label="当前对账单"><h3 class="density-dialog-section__title">当前对账单</h3><table class="density-table statement-dialog-facts"><tbody><tr><th scope="row">对账单号</th><td>{{ adjustStatement.billNo || '—' }}</td><th scope="row">客户</th><td>{{ customerDisplayName(adjustStatement.customerId, adjustStatement.customerName) }}</td></tr></tbody></table></section>
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="对账单调整登记"><h3 class="density-dialog-section__title">调整登记</h3><div class="statement-adjust-editor">
|
||||
<table class="density-table density-table--editable"><tbody><tr><td><el-form-item label="整体调整原因"><el-input v-model="adjustReason" type="textarea" maxlength="500" show-word-limit placeholder="请输入整体调整原因" /></el-form-item></td></tr></tbody></table>
|
||||
<el-table :data="adjustLines" size="small" border :stripe="false" row-key="statementLineId"><el-table-column label="选择" width="64"><template #default="{ row }"><el-checkbox v-model="row.selected" :aria-label="`选择${row.feeItemName || '费用明细'}`" /></template></el-table-column><el-table-column prop="feeItemName" label="费用项目" min-width="150" show-overflow-tooltip /><el-table-column prop="originalAmount" label="原金额(CNY)" width="130" /><el-table-column label="新金额(CNY)" width="180"><template #default="{ row }"><el-input-number v-model="row.adjustedAmount" :min="0.01" :precision="2" placeholder="请输入新金额" /></template></el-table-column><el-table-column label="明细说明" min-width="220"><template #default="{ row }"><el-input v-model="row.reason" maxlength="500" placeholder="请输入明细说明" /></template></el-table-column></el-table>
|
||||
<div class="workbench-evidence"><table class="density-table density-table--editable"><tbody><tr><td><el-form-item label="整体调整原因"><el-input v-model="adjustReason" type="textarea" maxlength="500" show-word-limit placeholder="请输入整体调整原因" /></el-form-item></td></tr></tbody></table></div>
|
||||
<div class="workbench-table"><el-table :data="adjustLines" size="small" border :stripe="false" row-key="statementLineId"><el-table-column label="选择" width="64"><template #default="{ row }"><el-checkbox v-model="row.selected" :aria-label="`选择${row.feeItemName || '费用明细'}`" /></template></el-table-column><el-table-column prop="feeItemName" label="费用项目" min-width="150" show-overflow-tooltip /><el-table-column prop="originalAmount" label="原金额(CNY)" width="130" /><el-table-column label="新金额(CNY)" width="180"><template #default="{ row }"><el-input-number v-model="row.adjustedAmount" :min="0.01" :precision="2" placeholder="请输入新金额" /></template></el-table-column><el-table-column label="明细说明" min-width="220"><template #default="{ row }"><el-input v-model="row.reason" maxlength="500" placeholder="请输入明细说明" /></template></el-table-column></el-table></div>
|
||||
</div></section>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
@@ -748,7 +749,7 @@ watch(detailRouteId, (statementId) => {
|
||||
.statement-dialog-facts th { width: var(--admin-label-width); }
|
||||
.statement-adjust-editor { display: grid; gap: var(--admin-space-5); }
|
||||
.statement-detail-page { min-width: 0; width: min(100%, 1920px); margin: 0 auto; overflow: hidden; }
|
||||
.statement-summary-section { padding: var(--admin-space-4) var(--admin-space-6); border-bottom: 1px solid var(--cc-border-subtle); }
|
||||
.statement-summary-section { padding: var(--admin-space-4) var(--admin-space-6); border-bottom: 1px solid var(--workbench-border); }
|
||||
.statement-outstanding { color: var(--el-color-warning-dark-2); }
|
||||
.statement-detail-tabs { min-width: 0; }
|
||||
.statement-detail-table { min-width: 0; }
|
||||
|
||||
@@ -42,9 +42,9 @@ describe('Finance formal settlement desktop presentation', () => {
|
||||
})
|
||||
|
||||
it('uses a continuous detail layout with locally scrollable wide tables', () => {
|
||||
expect(source).toContain('<DetailPageLayout v-else-if="detailMode" class="settlement-detail-page surface-panel finance-density-detail"')
|
||||
expect(source).toContain('<DetailPageLayout v-else-if="detailMode" class="settlement-detail-page surface-panel finance-density-detail workbench-panel is-flush is-page-surface"')
|
||||
expect(source).toContain('class="settlement-detail-content"')
|
||||
expect(source).toContain('class="detail-table-scroll"')
|
||||
expect(source).toContain('class="detail-table-scroll workbench-table"')
|
||||
expect(source).toContain(':deep(.detail-table-scroll .el-table) { min-width: 920px; }')
|
||||
expect(source).not.toContain('class="settlement-detail-card"')
|
||||
})
|
||||
@@ -64,7 +64,7 @@ describe('Finance formal settlement desktop presentation', () => {
|
||||
|
||||
it('keeps receipt and payment dialogs readable at desktop width', () => {
|
||||
expect(source.match(/width="min\(760px, 94vw\)"/g)).toHaveLength(2)
|
||||
expect(source.match(/class="density-dialog-section partial-summary"/g)).toHaveLength(2)
|
||||
expect(source.match(/class="density-dialog-section partial-summary workbench-evidence"/g)).toHaveLength(2)
|
||||
expect(source).toContain('density-table density-table--editable settlement-operation-table')
|
||||
expect(source).toContain('.settlement-operation-table td:not([colspan]) { width: 50%; }')
|
||||
expect(source).toContain('<OperationErrorToast :message="receiptValidationError" />')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import '@/styles/adminDensity.css'
|
||||
import '../financeDensity.css'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
@@ -453,7 +454,7 @@ async function submitPayment() {
|
||||
|
||||
async function performAction(row: SettlementRow, action: 'VOID' | 'REOPEN') {
|
||||
const reason = await ElMessageBox.prompt('请输入操作原因', `${action === 'VOID' ? '作废' : '重开'}${title.value} · ${row.settlementNo || '—'}`, {
|
||||
customClass: 'admin-density-confirm',
|
||||
customClass: 'admin-density-confirm admin-workbench',
|
||||
inputType: 'textarea', inputPattern: /\S+/, inputErrorMessage: '操作原因不能为空', confirmButtonText: '确认', cancelButtonText: '取消',
|
||||
}).then((result) => result.value.trim()).catch(() => null)
|
||||
if (reason === null) return
|
||||
@@ -519,12 +520,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="finance-settlement-page admin-density" spacing="normal">
|
||||
<AdminPage as="div" class="finance-settlement-page admin-density admin-workbench" spacing="normal">
|
||||
<OperationErrorToast v-if="createOpen || paymentOpen" :details="error" />
|
||||
<OperationErrorToast :message="receiptValidationError" />
|
||||
<OperationErrorToast :details="receiptError" />
|
||||
<el-result v-if="!canView" icon="warning" :title="`无权访问${title}`" sub-title="请联系管理员开通结算单查看权限" />
|
||||
<DetailPageLayout v-else-if="detailMode" class="settlement-detail-page surface-panel finance-density-detail" :aria-label="`${title}详情`">
|
||||
<DetailPageLayout v-else-if="detailMode" class="settlement-detail-page surface-panel finance-density-detail workbench-panel is-flush is-page-surface" :aria-label="`${title}详情`">
|
||||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" aria-label="返回列表" @click="backToList"><ArrowLeft :size="16" />返回列表</el-button><h1 class="density-detail-title">{{ detail?.settlementNo || '—' }}</h1><StatusBadge v-if="detail" :status="financeStatusLabel(detail.status)" /></div><div class="finance-detail-actions">
|
||||
<el-button v-if="detail && canExport" :loading="exportSubmitting" :disabled="exportSubmitting" @click="exportDetail"><Download :size="15" />导出</el-button>
|
||||
<el-button v-if="detail && canReceive && ['ISSUED','PARTIALLY_PAID'].includes(detail.status)" type="primary" :loading="receiptBusy" @click="openReceiptFromDetail"><HandCoins :size="15" />部分收款</el-button>
|
||||
@@ -562,7 +563,7 @@ onMounted(() => {
|
||||
</el-descriptions>
|
||||
</FormSection>
|
||||
<FormSection class="density-form-section" title="费用明细">
|
||||
<div class="detail-table-scroll">
|
||||
<div class="detail-table-scroll workbench-table">
|
||||
<el-table :data="detail.lines" size="small" border stripe empty-text="暂无费用明细">
|
||||
<el-table-column prop="feeItemName" label="费用类型" min-width="150" />
|
||||
<el-table-column prop="sourceType" label="来源类型" min-width="140"><template #default="scope">{{ financeSourceTypeLabel(scope.row.sourceType) }}</template></el-table-column>
|
||||
@@ -575,7 +576,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection class="density-form-section" :title="kind === 'receivable' ? '收款记录' : '付款记录'">
|
||||
<div class="detail-table-scroll">
|
||||
<div class="detail-table-scroll workbench-table">
|
||||
<el-table :data="detail.transactions" size="small" border stripe :empty-text="kind === 'receivable' ? '暂无收款记录' : '暂无付款记录'">
|
||||
<el-table-column prop="transactionDate" :label="kind === 'receivable' ? '收款日期' : '付款日期'" width="130" />
|
||||
<el-table-column prop="amount" label="金额(CNY)" width="130" align="right"><template #default="scope">{{ money(scope.row.amount) }}</template></el-table-column>
|
||||
@@ -589,7 +590,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection class="density-form-section" title="修订记录" :bordered="false">
|
||||
<div class="detail-table-scroll">
|
||||
<div class="detail-table-scroll workbench-table">
|
||||
<el-table :data="detail.revisions" size="small" border stripe empty-text="暂无调整记录">
|
||||
<el-table-column prop="action" label="动作" min-width="120"><template #default="scope">{{ settlementRevisionActionLabel(scope.row.action) || financeOperationActionLabel(scope.row.action) }}</template></el-table-column>
|
||||
<el-table-column prop="beforeAmount" label="调整前(CNY)" width="140" align="right"><template #default="scope">{{ money(scope.row.beforeAmount) }}</template></el-table-column>
|
||||
@@ -605,7 +606,7 @@ onMounted(() => {
|
||||
<ListPageLayout v-else :aria-label="title">
|
||||
|
||||
|
||||
<section><DataTable :preference-key="`finance-settlement-${kind}-table`" appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!customerError && !error" v-model:page="page" v-model:page-size="pageSize" :data="paginatedRows" :columns="columns" :loading="loading" :total="filteredRows.length" :aria-label="`${title}列表`" row-key="id" empty-text="暂无符合条件的结算单">
|
||||
<section class="workbench-table is-page-surface"><DataTable :preference-key="`finance-settlement-${kind}-table`" appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!customerError && !error" v-model:page="page" v-model:page-size="pageSize" :data="paginatedRows" :columns="columns" :loading="loading" :total="filteredRows.length" :aria-label="`${title}列表`" row-key="id" empty-text="暂无符合条件的结算单">
|
||||
<template #toolbar-start><h1>{{ title }}</h1></template>
|
||||
<template #filters>
|
||||
<QueryToolbar v-model="filters" :ariaLabel="`${title}筛选`" :fields="fields" :preference-key="`finance-settlement-${kind}-query`" :search-on-clear="false" @search="load" />
|
||||
@@ -654,8 +655,8 @@ onMounted(() => {
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
|
||||
<AppDialog class="admin-density-dialog" v-model="createOpen" :title="`生成${title}`" width="min(600px, 94vw)" fullscreenable :confirm-loading="createBusy" @confirm="submitCreate">
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="结算单生成设置"><h3 class="density-dialog-section__title">生成设置</h3>
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="createOpen" :title="`生成${title}`" width="min(600px, 94vw)" fullscreenable :confirm-loading="createBusy" @confirm="submitCreate">
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="结算单生成设置"><h3 class="density-dialog-section__title">生成设置</h3>
|
||||
<el-form class="density-form" label-position="left" label-width="120px" :disabled="createBusy"><table class="density-table density-table--editable"><tbody>
|
||||
<tr><td><el-form-item label="客户"><el-select v-model="createForm.customerId" filterable placeholder="请选择客户"><el-option v-for="item in activeCustomers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label="币种"><span>CNY</span></el-form-item></td></tr>
|
||||
@@ -669,16 +670,16 @@ onMounted(() => {
|
||||
</section>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog class="admin-density-dialog" v-model="paymentOpen" :title="`登记付款 · ${detail?.settlementNo || '—'}`" width="min(760px, 94vw)" fullscreenable :confirm-loading="paymentBusy" @confirm="submitPayment">
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="paymentOpen" :title="`登记付款 · ${detail?.settlementNo || '—'}`" width="min(760px, 94vw)" fullscreenable :confirm-loading="paymentBusy" @confirm="submitPayment">
|
||||
<div class="density-dialog-sections">
|
||||
<section v-if="detail" class="density-dialog-section partial-summary" data-tone="summary" aria-label="当前结算资料"><h3 class="density-dialog-section__title">当前结算(CNY)</h3>
|
||||
<section v-if="detail" class="density-dialog-section partial-summary workbench-evidence" data-tone="summary" aria-label="当前结算资料"><h3 class="density-dialog-section__title">当前结算(CNY)</h3>
|
||||
<table class="density-table settlement-dialog-facts"><tbody>
|
||||
<tr><th scope="row">结算单号</th><td>{{ detail.settlementNo || '—' }}</td><th scope="row">供应商</th><td>{{ detailPartyName() }}</td></tr>
|
||||
<tr><th scope="row">应付金额</th><td>{{ money(detail.amount) }}</td><th scope="row">已付金额</th><td class="is-success">{{ money(detail.paidAmount) }}</td></tr>
|
||||
<tr><th scope="row">当前余额</th><td colspan="3" class="is-danger">{{ money(detail.outstandingAmount) }}</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="付款登记"><h3 class="density-dialog-section__title">付款登记</h3>
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="付款登记"><h3 class="density-dialog-section__title">付款登记</h3>
|
||||
<el-form class="settlement-operation-form density-form" label-position="left" label-width="140px" :disabled="paymentBusy"><table class="density-table density-table--editable settlement-operation-table"><tbody>
|
||||
<tr><td><el-form-item label="付款日期"><el-date-picker v-model="paymentForm.transactionDate" type="date" value-format="YYYY-MM-DD" /></el-form-item></td><td><el-form-item label="本次付款金额(CNY)"><el-input-number v-model="paymentForm.amount" :min="0.01" :max="detail?.outstandingAmount || 0.01" :precision="2" placeholder="请输入付款金额" /></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label="付款方式"><el-select v-model="paymentForm.method"><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="paymentForm.referenceNo" placeholder="请输入付款流水号" /></el-form-item></td></tr>
|
||||
@@ -690,16 +691,16 @@ onMounted(() => {
|
||||
</div>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog class="admin-density-dialog" v-model="receiptOpen" :title="`登记收款 · ${detail?.settlementNo || '—'}`" width="min(760px, 94vw)" fullscreenable :confirm-loading="receiptBusy" @confirm="submitReceipt">
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="receiptOpen" :title="`登记收款 · ${detail?.settlementNo || '—'}`" width="min(760px, 94vw)" fullscreenable :confirm-loading="receiptBusy" @confirm="submitReceipt">
|
||||
<div class="density-dialog-sections">
|
||||
<section v-if="detail" class="density-dialog-section partial-summary" data-tone="summary" aria-label="当前结算资料"><h3 class="density-dialog-section__title">当前结算(CNY)</h3>
|
||||
<section v-if="detail" class="density-dialog-section partial-summary workbench-evidence" data-tone="summary" aria-label="当前结算资料"><h3 class="density-dialog-section__title">当前结算(CNY)</h3>
|
||||
<table class="density-table settlement-dialog-facts"><tbody>
|
||||
<tr><th scope="row">结算单号</th><td>{{ detail.settlementNo || '—' }}</td><th scope="row">客户公司</th><td>{{ detailPartyName() }}</td></tr>
|
||||
<tr><th scope="row">应收金额</th><td>{{ money(detail.amount) }}</td><th scope="row">已收金额</th><td class="is-success">{{ money(detail.paidAmount) }}</td></tr>
|
||||
<tr><th scope="row">当前余额</th><td colspan="3" class="is-danger">{{ money(detail.outstandingAmount) }}</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="收款登记"><h3 class="density-dialog-section__title">收款登记</h3>
|
||||
<section class="density-dialog-section workbench-evidence" data-tone="work" aria-label="收款登记"><h3 class="density-dialog-section__title">收款登记</h3>
|
||||
<el-form class="settlement-operation-form density-form" label-position="left" label-width="140px" :disabled="receiptBusy"><table class="density-table density-table--editable settlement-operation-table"><tbody>
|
||||
<tr><td><el-form-item label="收款日期"><el-date-picker v-model="receiptForm.receivedDate" type="date" value-format="YYYY-MM-DD" /></el-form-item></td><td><el-form-item label="本次收款金额(CNY)"><el-input-number v-model="receiptForm.amount" :min="0.01" :max="detail?.outstandingAmount || 0.01" :precision="2" placeholder="请输入收款金额" /></el-form-item></td></tr>
|
||||
<tr><td colspan="2"><el-form-item label="收款方式"><el-select v-model="receiptForm.paymentMethod"><el-option label="银行转账" value="BANK_TRANSFER" /><el-option label="现金" value="CASH" /><el-option label="其他" value="OTHER" /></el-select></el-form-item></td></tr>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
const settlement = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceFormalSettlementPage.vue'), 'utf8')
|
||||
const density = readFileSync(resolve(process.cwd(), 'src/styles/adminDensity.css'), 'utf8')
|
||||
const workbench = readFileSync(resolve(process.cwd(), 'src/styles/adminWorkbench.css'), 'utf8')
|
||||
const financeDensity = readFileSync(resolve(process.cwd(), 'src/modules/finance/financeDensity.css'), 'utf8')
|
||||
const financeRoutes = readFileSync(resolve(process.cwd(), 'src/modules/finance/routes.ts'), 'utf8')
|
||||
const accounts = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerAccountsView.vue'), 'utf8')
|
||||
@@ -24,7 +25,7 @@ describe('finance presentation governance', () => {
|
||||
expect(financeRoutes).toContain("finance/receivable-statistics")
|
||||
expect(financeRoutes).toContain("platform:finance:receivable-statistics:list")
|
||||
expect(financeRoutes).toContain('showPageHeading: false')
|
||||
expect(settlement).toContain('class="settlement-detail-page surface-panel finance-density-detail"')
|
||||
expect(settlement).toContain('class="settlement-detail-page surface-panel finance-density-detail workbench-panel is-flush is-page-surface"')
|
||||
expect(settlement).toContain('返回列表')
|
||||
expect(settlement).not.toContain('AppDrawer')
|
||||
expect(settlement).toContain('登记后剩余余额')
|
||||
@@ -33,12 +34,12 @@ describe('finance presentation governance', () => {
|
||||
})
|
||||
|
||||
it('uses one continuous desktop detail surface with shared summaries and scroll-safe tables', () => {
|
||||
expect(settlement).toContain('class="settlement-detail-page surface-panel finance-density-detail"')
|
||||
expect(settlement).toContain('class="settlement-detail-page surface-panel finance-density-detail workbench-panel is-flush is-page-surface"')
|
||||
expect(settlement).toContain('class="settlement-detail-content"')
|
||||
expect(settlement).toContain('class="settlement-detail-info density-descriptions"')
|
||||
for (const title of ['基本信息', '费用明细', '修订记录']) expect(settlement).toContain('title="' + title + '"')
|
||||
expect(settlement).toContain(':title="kind === \'receivable\' ? \'收款记录\' : \'付款记录\'"')
|
||||
expect(settlement.match(/class="detail-table-scroll"/g)).toHaveLength(3)
|
||||
expect(settlement.match(/class="detail-table-scroll workbench-table"/g)).toHaveLength(3)
|
||||
expect(settlement).toContain(':deep(.detail-table-scroll .el-table) { min-width: 920px; }')
|
||||
expect(settlement).not.toContain('class="detail-section surface-panel"')
|
||||
expect(settlement.match(/width="min\(760px, 94vw\)"/g)).toHaveLength(2)
|
||||
@@ -136,7 +137,7 @@ describe('finance presentation governance', () => {
|
||||
})
|
||||
|
||||
it('opens statement details on a compact dedicated route instead of below the list', () => {
|
||||
expect(statements).toContain('v-else-if="isDetailRoute" class="surface-panel statement-detail-page finance-density-detail"')
|
||||
expect(statements).toContain('v-else-if="isDetailRoute" class="surface-panel statement-detail-page finance-density-detail workbench-panel is-flush is-page-surface"')
|
||||
expect(statements).toContain('@row-click="openDetail"')
|
||||
expect(statements).toContain("name: 'finance-customer-statement-detail'")
|
||||
expect(financeDensity).toContain('.detail-page-layout.finance-density-detail { gap: 0; }')
|
||||
@@ -153,8 +154,13 @@ describe('finance presentation governance', () => {
|
||||
expect(density).toContain('.density-detail-overview')
|
||||
expect(accounts).toContain(':deep(.ledger-query.query-toolbar)')
|
||||
expect(accounts).toContain('padding: 0;')
|
||||
expect(accounts).toContain('border: 0;')
|
||||
expect(accounts).toContain('background: transparent;')
|
||||
const summaryStyles = density.match(/\.density-detail-overview \{([^}]*)\}/)?.[1]
|
||||
expect(summaryStyles).toContain('display: flex;')
|
||||
expect(summaryStyles).toContain('flex-wrap: wrap;')
|
||||
const queryStyles = workbench.match(/\.query-toolbar\.workbench-query \{([^}]*)\}/)?.[1]
|
||||
expect(accounts).toContain('class="ledger-query workbench-query"')
|
||||
expect(queryStyles).toContain('border-color: var(--workbench-border);')
|
||||
expect(queryStyles).toContain('background: var(--workbench-surface);')
|
||||
expect(accounts).not.toContain('展示应收、应付及每次变动后的未收金额。')
|
||||
expect(accounts).not.toContain('到期日按账单日期加客户账期计算;每个客户独立设置,默认 30 天。')
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import '@/styles/adminDensity.css'
|
||||
import '../financeDensity.css'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
@@ -89,10 +90,10 @@ onMounted(() => { void loadCustomers(); void load() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage class="receivable-statistics-page admin-density" spacing="normal">
|
||||
<AdminPage class="receivable-statistics-page admin-density admin-workbench" spacing="normal">
|
||||
<el-result v-if="!canView" icon="warning" title="无权访问应收款统计表" sub-title="请联系管理员开通应收款统计查看权限" />
|
||||
<template v-else>
|
||||
<DataTable class="statistics-table" appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!error" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="result?.totalCustomers ?? 0" :row-key="statisticsRowKey" preference-key="finance-receivable-statistics-table" aria-label="应收款统计列表" empty-text="当前月份暂无应收数据" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<div class="workbench-table is-page-surface"><DataTable class="statistics-table" appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!error" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="result?.totalCustomers ?? 0" :row-key="statisticsRowKey" preference-key="finance-receivable-statistics-table" aria-label="应收款统计列表" empty-text="当前月份暂无应收数据" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-start><h1>应收款统计表</h1></template>
|
||||
<template #filters><section class="query-toolbar statistics-query" aria-label="应收款统计筛选">
|
||||
<div class="query-field"><span>统计月份</span><el-date-picker v-model="month" type="month" value-format="YYYY-MM" format="YYYY年MM月" :clearable="false" aria-label="统计月份" /></div>
|
||||
@@ -109,26 +110,27 @@ onMounted(() => { void loadCustomers(); void load() })
|
||||
<template #toolbar-actions><div class="report-actions"><el-button :disabled="loading || !result" @click="printCurrent"><Printer :size="16" />打印</el-button><el-button v-if="canExport" type="primary" :loading="exporting" :disabled="loading || exporting" @click="exportCurrent"><Download :size="16" />导出Excel</el-button></div></template>
|
||||
<template #customerName="{ row }">{{ displayCustomerName(row) }}</template><template #totalReceivable="{ row }">{{ money(row.totalReceivable) }}</template><template #totalReceived="{ row }">{{ money(row.totalReceived) }}</template><template #totalOutstanding="{ row }"><span class="outstanding-cell">{{ money(row.totalOutstanding) }}</span></template>
|
||||
<template #pagination-left>统计月份:{{ result?.statisticsMonth }} · 币种:CNY</template>
|
||||
</DataTable>
|
||||
</DataTable></div>
|
||||
</template>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.statistics-query { display: flex; align-items: end; gap: var(--admin-space-5); padding: var(--admin-space-6); }
|
||||
.query-field { display: grid; min-width: 220px; gap: var(--admin-space-2); color: var(--cc-muted); font-size: var(--admin-font-body); }
|
||||
.query-field { display: grid; min-width: 220px; gap: var(--admin-space-2); color: var(--workbench-muted); font-size: var(--admin-font-body); }
|
||||
.query-field :deep(.el-select), .query-field :deep(.el-date-editor) { width: 100%; }
|
||||
.query-actions, .report-actions { display: flex; gap: var(--admin-space-3); }
|
||||
.report-actions { margin-left: auto; }
|
||||
.query-actions :deep(.el-button), .report-actions :deep(.el-button) { margin-left: 0; }
|
||||
.outstanding-cell { color: var(--el-color-danger); font-variant-numeric: tabular-nums; }
|
||||
.statistics-table { overflow: hidden; }
|
||||
.statistics-report-heading { display: flex; align-items: center; justify-content: space-between; padding: var(--admin-space-5) var(--admin-space-6); border-bottom: 1px solid var(--cc-border, #e3e8ef); }
|
||||
.statistics-report-heading { display: flex; align-items: center; justify-content: space-between; padding: var(--admin-space-5) var(--admin-space-6); border-bottom: 1px solid var(--workbench-border); }
|
||||
.statistics-table h2 { margin: 0; font-size: var(--admin-font-section); }
|
||||
.statistics-report-heading span { color: var(--cc-muted); font-size: var(--admin-font-body); }
|
||||
.statistics-report-heading span { color: var(--workbench-muted); font-size: var(--admin-font-body); }
|
||||
@media print {
|
||||
.statistics-query, :deep(.data-table__toolbar), :deep(.data-table__pagination) { display: none !important; }
|
||||
.receivable-statistics-page { gap: 10px; color: #000; }
|
||||
.receivable-statistics-page { gap: 10px; padding: 0; color: #000; }
|
||||
.workbench-table > :deep(.data-table) { border-radius: var(--admin-radius); box-shadow: none; }
|
||||
.statistics-summary > div, .statistics-table { border-color: #999; box-shadow: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -277,7 +277,7 @@ describe('formal settlement native dialogs', () => {
|
||||
const wrapper = await mountPage(kind)
|
||||
await click(wrapper, action === 'VOID' ? '作废' : '重开', '.el-table__body-wrapper button')
|
||||
const current = kind === 'receivable' ? ar : ap
|
||||
expect(prompt).toHaveBeenCalledWith('请输入操作原因', expect.stringContaining(current.settlementNo), expect.objectContaining({ customClass: 'admin-density-confirm', inputPattern: /\S+/ }))
|
||||
expect(prompt).toHaveBeenCalledWith('请输入操作原因', expect.stringContaining(current.settlementNo), expect.objectContaining({ customClass: 'admin-density-confirm admin-workbench', inputPattern: /\S+/ }))
|
||||
expect(api.actionFinanceSettlement).toHaveBeenCalledWith(current.id, { action, reason: '测试原因', expectedVersion: current.version })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user