优化
This commit is contained in:
@@ -121,6 +121,14 @@ test('keeps actions first and exposes list, balance, detail, confirmation, recei
|
|||||||
assert.match(viewSource, new RegExp(expected))
|
assert.match(viewSource, new RegExp(expected))
|
||||||
}
|
}
|
||||||
assert.match(viewSource, /auth\.hasPermission\('finance:bill:confirm'\)/)
|
assert.match(viewSource, /auth\.hasPermission\('finance:bill:confirm'\)/)
|
||||||
|
assert.match(viewSource, /row\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
|
||||||
|
assert.match(viewSource, /size="small" type="success" plain/)
|
||||||
|
assert.match(viewSource, /confirmStatement\(row as FinanceBillSummary\)/)
|
||||||
|
assert.match(viewSource, /detail\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
|
||||||
|
assert.match(viewSource, /confirmStatement\(detail\)/)
|
||||||
|
assert.match(viewSource, /confirmMerchantFinanceStatement\(statementId, statement\.version, key\)/)
|
||||||
|
assert.match(viewSource, /ElMessage\.success\('对账单已确认'\)/)
|
||||||
|
assert.match(viewSource, /ElMessage\.error\(error instanceof ApiError \? error\.message : '对账单确认失败,请重试'\)/)
|
||||||
for (const forbidden of ['发起争议', '新增收款', '核销', '修改金额']) {
|
for (const forbidden of ['发起争议', '新增收款', '核销', '修改金额']) {
|
||||||
assert.doesNotMatch(viewSource, new RegExp(forbidden))
|
assert.doesNotMatch(viewSource, new RegExp(forbidden))
|
||||||
}
|
}
|
||||||
@@ -134,3 +142,10 @@ test('keeps the existing customer finance view permission mapping', () => {
|
|||||||
assert.match(permissionSource, /code: 'finance:bill:confirm'/)
|
assert.match(permissionSource, /code: 'finance:bill:confirm'/)
|
||||||
assert.match(permissionSource, /'finance:bill:confirm': \['merchant:finance:statement:confirm'\]/)
|
assert.match(permissionSource, /'finance:bill:confirm': \['merchant:finance:statement:confirm'\]/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('grants the customer confirmation action only from its independent backend authority', () => {
|
||||||
|
const allowed = (codes) => permissionContracts.hasMappedMerchantPermission('finance:bill:confirm', new Set(codes))
|
||||||
|
assert.equal(allowed([]), false)
|
||||||
|
assert.equal(allowed(['merchant:finance:statement:list', 'merchant:finance:statement:view']), false)
|
||||||
|
assert.equal(allowed(['merchant:finance:statement:confirm']), true)
|
||||||
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const bills = ref<FinanceBillSummary[]>([])
|
|||||||
const detail = ref<FinanceBillDetail | null>(null)
|
const detail = ref<FinanceBillDetail | null>(null)
|
||||||
const selectedBillId = ref('')
|
const selectedBillId = ref('')
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
const confirming = ref(false)
|
const confirmingBillId = ref('')
|
||||||
const confirmationKeys = new Map<string, string>()
|
const confirmationKeys = new Map<string, string>()
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
@@ -112,19 +112,19 @@ async function loadDetail(billId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmStatement() {
|
async function confirmStatement(statement: FinanceBillSummary) {
|
||||||
if (!detail.value || detail.value.customerConfirmationStatus !== 'PENDING_CONFIRMATION') return
|
if (!canConfirm.value || statement.customerConfirmationStatus !== 'PENDING_CONFIRMATION' || confirmingBillId.value) return
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`确认对账单 ${detail.value.billNo} 的金额与费用明细无误?确认后平台将记录当前登录成员和确认时间。`,
|
`确认对账单 ${statement.billNo}(¥ ${formatAmount(statement.totalAmount)})的金额与费用明细无误?确认后平台将记录当前登录成员和确认时间。`,
|
||||||
'确认客户对账单',
|
'确认客户对账单',
|
||||||
{ type: 'warning', confirmButtonText: '确认无误', cancelButtonText: '取消' },
|
{ type: 'warning', confirmButtonText: '确认无误', cancelButtonText: '取消' },
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
confirming.value = true
|
confirmingBillId.value = statement.id
|
||||||
const statementId = detail.value.id
|
const statementId = statement.id
|
||||||
let key = confirmationKeys.get(statementId)
|
let key = confirmationKeys.get(statementId)
|
||||||
if (!key) {
|
if (!key) {
|
||||||
const randomPart = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
const randomPart = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||||
@@ -132,14 +132,16 @@ async function confirmStatement() {
|
|||||||
confirmationKeys.set(statementId, key)
|
confirmationKeys.set(statementId, key)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await confirmMerchantFinanceStatement(statementId, detail.value.version, key)
|
await confirmMerchantFinanceStatement(statementId, statement.version, key)
|
||||||
confirmationKeys.delete(statementId)
|
confirmationKeys.delete(statementId)
|
||||||
ElMessage.success('对账单已确认')
|
ElMessage.success('对账单已确认')
|
||||||
await Promise.all([loadDetail(statementId), loadBills(page.value)])
|
const refreshes: Array<Promise<void>> = [loadBills(page.value)]
|
||||||
|
if (detailVisible.value && selectedBillId.value === statementId) refreshes.push(loadDetail(statementId))
|
||||||
|
await Promise.all(refreshes)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof ApiError ? error.message : '对账单确认失败,请重试')
|
ElMessage.error(error instanceof ApiError ? error.message : '对账单确认失败,请重试')
|
||||||
} finally {
|
} finally {
|
||||||
confirming.value = false
|
confirmingBillId.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +183,8 @@ onMounted(() => { void loadBills(1) })
|
|||||||
<el-button @click="loadBills(1)"><RefreshCcw :size="15" />重试</el-button>
|
<el-button @click="loadBills(1)"><RefreshCcw :size="15" />重试</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-else v-loading="loading" :data="bills" row-key="billNo" class="finance-table" aria-label="客户账单列表">
|
<el-table v-else v-loading="loading" :data="bills" row-key="billNo" class="finance-table" aria-label="客户账单列表">
|
||||||
<el-table-column label="操作" width="90" fixed="left">
|
<el-table-column label="操作" width="190" fixed="left">
|
||||||
<template #default="{ row }"><el-button link type="primary" @click="openDetail(row as FinanceBillSummary)"><Eye :size="15" />查看</el-button></template>
|
<template #default="{ row }"><div class="table-actions"><el-button link type="primary" @click="openDetail(row as FinanceBillSummary)"><Eye :size="15" />查看</el-button><el-button v-if="row.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm" size="small" type="success" plain :loading="confirmingBillId === row.id" :disabled="Boolean(confirmingBillId)" @click="confirmStatement(row as FinanceBillSummary)">确认账单</el-button></div></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="billNo" label="账单编号" min-width="160" />
|
<el-table-column prop="billNo" label="账单编号" min-width="160" />
|
||||||
<el-table-column prop="issueDate" label="出账日期" width="120"><template #default="{ row }">{{ formatDate(row.issueDate) }}</template></el-table-column>
|
<el-table-column prop="issueDate" label="出账日期" width="120"><template #default="{ row }">{{ formatDate(row.issueDate) }}</template></el-table-column>
|
||||||
@@ -203,7 +205,7 @@ onMounted(() => { void loadBills(1) })
|
|||||||
<div v-if="detailError" class="state-panel state-panel--error" role="alert"><p>{{ detailError }}</p><el-button @click="loadDetail(selectedBillId)">重试</el-button></div>
|
<div v-if="detailError" class="state-panel state-panel--error" role="alert"><p>{{ detailError }}</p><el-button @click="loadDetail(selectedBillId)">重试</el-button></div>
|
||||||
<template v-else-if="detail">
|
<template v-else-if="detail">
|
||||||
<div class="detail-heading"><div><h2>{{ detail.billNo }}</h2><p>出账 {{ formatDate(detail.issueDate) }} · 到期 {{ formatDate(detail.dueDate) }} · 未收 <strong class="amount-warning">¥ {{ formatAmount(detail.outstandingAmount) }}</strong></p></div><div class="detail-statuses"><el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag><el-tag :type="confirmationType(detail.customerConfirmationStatus)">{{ confirmationLabel(detail.customerConfirmationStatus) }}</el-tag></div></div>
|
<div class="detail-heading"><div><h2>{{ detail.billNo }}</h2><p>出账 {{ formatDate(detail.issueDate) }} · 到期 {{ formatDate(detail.dueDate) }} · 未收 <strong class="amount-warning">¥ {{ formatAmount(detail.outstandingAmount) }}</strong></p></div><div class="detail-statuses"><el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag><el-tag :type="confirmationType(detail.customerConfirmationStatus)">{{ confirmationLabel(detail.customerConfirmationStatus) }}</el-tag></div></div>
|
||||||
<section class="confirmation-panel" aria-label="客户确认信息"><div><strong>{{ confirmationLabel(detail.customerConfirmationStatus) }}</strong><p v-if="detail.customerConfirmationStatus === 'CONFIRMED'">{{ detail.customerConfirmedByName || '当前企业成员' }} · {{ formatDateTime(detail.customerConfirmedAt) }}</p><p v-else>请核对本期金额和费用明细,确认后将记录当前登录成员与确认时间。</p></div><el-button v-if="detail.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm" type="primary" :loading="confirming" @click="confirmStatement">确认账单</el-button></section>
|
<section class="confirmation-panel" aria-label="客户确认信息"><div><strong>{{ confirmationLabel(detail.customerConfirmationStatus) }}</strong><p v-if="detail.customerConfirmationStatus === 'CONFIRMED'">{{ detail.customerConfirmedByName || '当前企业成员' }} · {{ formatDateTime(detail.customerConfirmedAt) }}</p><p v-else>请核对本期金额和费用明细,确认后将记录当前登录成员与确认时间。</p></div><el-button v-if="detail.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm" type="primary" :loading="confirmingBillId === detail.id" :disabled="Boolean(confirmingBillId)" @click="confirmStatement(detail)">确认账单</el-button></section>
|
||||||
<section class="detail-section"><h3>费用明细</h3><el-table :data="detail.lines" size="small"><el-table-column prop="feeName" label="费用项目" min-width="180" /><el-table-column label="金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.amount) }}</template></el-table-column></el-table></section>
|
<section class="detail-section"><h3>费用明细</h3><el-table :data="detail.lines" size="small"><el-table-column prop="feeName" label="费用项目" min-width="180" /><el-table-column label="金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.amount) }}</template></el-table-column></el-table></section>
|
||||||
<section class="detail-section"><h3>收款记录</h3><el-table v-if="detail.receipts.length" :data="detail.receipts" size="small"><el-table-column prop="receivedAt" label="收款日期" width="110"><template #default="{ row }">{{ formatDate(row.receivedAt) }}</template></el-table-column><el-table-column prop="receiptNo" label="收款编号" min-width="130" /><el-table-column label="本次金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.allocatedAmount) }}</template></el-table-column><el-table-column prop="method" label="方式" min-width="100" /></el-table><div v-else class="sub-empty">暂无收款记录</div></section>
|
<section class="detail-section"><h3>收款记录</h3><el-table v-if="detail.receipts.length" :data="detail.receipts" size="small"><el-table-column prop="receivedAt" label="收款日期" width="110"><template #default="{ row }">{{ formatDate(row.receivedAt) }}</template></el-table-column><el-table-column prop="receiptNo" label="收款编号" min-width="130" /><el-table-column label="本次金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.allocatedAmount) }}</template></el-table-column><el-table-column prop="method" label="方式" min-width="100" /></el-table><div v-else class="sub-empty">暂无收款记录</div></section>
|
||||||
<p class="readonly-notice">如对账单内容有疑问,请先线下联系平台财务,核对无误后再确认。</p>
|
<p class="readonly-notice">如对账单内容有疑问,请先线下联系平台财务,核对无误后再确认。</p>
|
||||||
@@ -218,7 +220,7 @@ onMounted(() => { void loadBills(1) })
|
|||||||
.finance-summary { display: flex; flex-wrap: wrap; gap: 8px 26px; color: var(--muted); font-size: 13px; }.finance-summary strong { margin-left: 6px; color: var(--navy); font-size: 18px; }.amount-warning, .amount-warning strong { color: #b45309; }
|
.finance-summary { display: flex; flex-wrap: wrap; gap: 8px 26px; color: var(--muted); font-size: 13px; }.finance-summary strong { margin-left: 6px; color: var(--navy); font-size: 18px; }.amount-warning, .amount-warning strong { color: #b45309; }
|
||||||
.content-card { min-width: 0; padding: 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface, #fff); }
|
.content-card { min-width: 0; padding: 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface, #fff); }
|
||||||
.filter-bar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }.keyword-input { width: 260px; }.status-select { width: 140px; }
|
.filter-bar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }.keyword-input { width: 260px; }.status-select { width: 140px; }
|
||||||
.finance-table { width: 100%; }.finance-table :deep(.el-button) { display: inline-flex; align-items: center; gap: 4px; }
|
.finance-table { width: 100%; }.finance-table :deep(.el-button) { display: inline-flex; align-items: center; gap: 4px; }.table-actions { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
||||||
.state-panel { display: flex; min-height: 180px; align-items: center; justify-content: center; gap: 12px; flex-direction: column; color: var(--muted); text-align: center; }.state-panel p { margin: 0; }.state-panel--error { color: #b42318; background: #fff8f7; border: 1px solid #f5c2bd; border-radius: 6px; }
|
.state-panel { display: flex; min-height: 180px; align-items: center; justify-content: center; gap: 12px; flex-direction: column; color: var(--muted); text-align: center; }.state-panel p { margin: 0; }.state-panel--error { color: #b42318; background: #fff8f7; border: 1px solid #f5c2bd; border-radius: 6px; }
|
||||||
.table-empty { display: flex; align-items: center; flex-direction: column; gap: 8px; padding: 42px 12px; color: var(--muted); }.table-empty strong { color: var(--navy); }.table-empty span { font-size: 12px; }
|
.table-empty { display: flex; align-items: center; flex-direction: column; gap: 8px; padding: 42px 12px; color: var(--muted); }.table-empty strong { color: var(--navy); }.table-empty span { font-size: 12px; }
|
||||||
.pagination-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 14px; color: var(--muted); font-size: 12px; }
|
.pagination-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 14px; color: var(--muted); font-size: 12px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user