This commit is contained in:
2026-08-14 14:27:03 +08:00
parent 724848feb6
commit a81af4a664
64 changed files with 961 additions and 176 deletions
@@ -1,6 +1,19 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import type { PlatformTenantResource } from '@/modules/iam'
import { loadAllActiveCustomers } from '../task-list'
function activeCustomer(id: string, tenantName: string, legalName: string | null, tenantCode: string): PlatformTenantResource {
return {
id, tenantName, legalName, tenantCode, status: 'ACTIVE', legalRepresentative: null,
unifiedSocialCreditCode: null, contactName: null, contactPhone: null, contactEmail: null,
servicePackage: null, contactWechat: null, salesOwnerName: null, paymentTermDays: null,
enterpriseType: null, establishedDate: null, registeredCapital: null,
registeredCapitalCurrency: null, registeredAddress: null, internalRemark: null,
createdAt: null, approvedAt: null,
}
}
describe('InboundTaskList measurement scope', () => {
it('uses the inbound tenant isolation key for measurement and review requests', () => {
@@ -10,6 +23,42 @@ describe('InboundTaskList measurement scope', () => {
expect(source).not.toContain('selectedInbound.customerId || selectedInbound.tenantId')
})
it('loads only active customers and exposes a searchable name-only select', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
const queryFieldsSource = source.slice(source.indexOf('const queryFields'), source.indexOf('const columns'))
expect(source).toContain('loadAllActiveCustomers')
expect(queryFieldsSource).toContain("type: 'select'")
expect(queryFieldsSource).toContain('filterable: true')
expect(queryFieldsSource).toContain('label: customer.legalName?.trim() || customer.tenantName')
expect(queryFieldsSource).not.toContain('tenantCode')
expect(queryFieldsSource).not.toContain('客户编码')
})
it('requests every active-customer page and merges name-only options', async () => {
const calls: Array<{ page: number; size: number; status: 'ACTIVE' }> = []
const records = await loadAllActiveCustomers(async (query) => {
calls.push(query)
if (query.page === 1) {
return {
pages: 2,
records: [activeCustomer('10001', '客户一', '客户一有限公司', 'CUS-001')],
}
}
return {
pages: 2,
records: [activeCustomer('10002', '客户二', null, 'CUS-002')],
}
})
expect(calls).toEqual([
{ page: 1, size: 200, status: 'ACTIVE' },
{ page: 2, size: 200, status: 'ACTIVE' },
])
expect(records.map((customer) => customer.legalName?.trim() || customer.tenantName)).toEqual(['客户一有限公司', '客户二'])
expect(records.map((customer) => customer.legalName?.trim() || customer.tenantName).join(' ')).not.toContain('CUS-')
})
it('opens the shared unified order detail from the view action', () => {
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
@@ -17,7 +17,7 @@ import type { InboundOrderSummary } from '../core/types'
import InboundMeasurementPanel from '../measurement/InboundMeasurementPanel.vue'
import MeasurementReviewPanel from '../review/MeasurementReviewPanel.vue'
import { inboundProcessStatusLabels, inboundReviewStatusLabels, inboundStatusLabel } from '../status'
import { buildInboundTaskQuery, type InboundTaskMode } from '../task-list'
import { buildInboundTaskQuery, loadAllActiveCustomers, type InboundTaskMode } from '../task-list'
type FilterModel = QueryModel & {
customerId: string
@@ -43,6 +43,7 @@ const warehouseOptions = ref<Array<{ label: string; value: string }>>([])
const selectedInbound = ref<InboundOrderSummary | null>(null)
const taskDialogOpen = ref(false)
let requestSerial = 0
let optionsRequestSerial = 0
let disposed = false
let activatedOnce = false
@@ -109,14 +110,15 @@ function dateParam(value: string | undefined, end = false) {
}
async function loadOptions() {
const serial = ++optionsRequestSerial
customerLoadError.value = null
try {
const [customerPage, warehousePage] = await Promise.all([
listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }),
const [customerRecords, warehousePage] = await Promise.all([
loadAllActiveCustomers((query) => listPlatformTenants(query)),
listPlatformWarehouses(1, 200),
])
if (disposed) return
customers.value = customerPage.records
if (disposed || serial !== optionsRequestSerial) return
customers.value = customerRecords
warehouseOptions.value = warehousePage.records.map((warehouse) => ({
label: `${warehouse.warehouseName} · ${warehouse.warehouseCode}`,
value: String(warehouse.id),
@@ -190,7 +192,7 @@ onActivated(() => {
}
void loadTasks()
})
onBeforeUnmount(() => { disposed = true; requestSerial += 1 })
onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSerial += 1 })
</script>
<template>
@@ -241,7 +241,7 @@ onBeforeUnmount(() => {
<div class="inbound-operation-dialog__footer">
<el-button :disabled="Boolean(reviewPanelRef?.actionLoading)" @click="reviewDialogOpen = false">取消</el-button>
<el-button v-if="reviewPanelRef?.isPending && reviewPanelRef?.canReview" type="danger" plain :loading="reviewPanelRef?.actionLoading === 'reject'" @click="reviewPanelRef?.openReasonDialog('reject')">驳回</el-button>
<el-button v-if="reviewPanelRef?.isPending && reviewPanelRef?.canReview" type="primary" :loading="reviewPanelRef?.actionLoading === 'approve'" @click="reviewPanelRef?.confirmApprove()">通过并完成入库</el-button>
<el-button v-if="reviewPanelRef?.isPending && reviewPanelRef?.canReview" type="primary" :loading="reviewPanelRef?.actionLoading === 'approve'" @click="reviewPanelRef?.confirmApprove()">通过并进入待分柜</el-button>
<el-button v-if="reviewPanelRef?.isRejected && reviewPanelRef?.canRelease" type="warning" plain :loading="reviewPanelRef?.actionLoading === 'release'" @click="reviewPanelRef?.openReasonDialog('release')">异常放行</el-button>
</div>
</template>
@@ -177,6 +177,14 @@ describe('InboundFeePanel', () => {
expect(document.querySelector('[data-testid="adjust-fee-button"]')).not.toBeNull()
})
it('allows adjustment for a calculated fee waiting for split', async () => {
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
const wrapper = mountPanel()
await flushPromises()
expect(wrapper.find('[data-testid="adjust-fee-button"]').exists()).toBe(true)
})
it('submits a reasoned adjustment and emits the refreshed snapshot', async () => {
const wrapper = mountPanel()
await flushPromises()
@@ -184,12 +192,15 @@ describe('InboundFeePanel', () => {
await flushPromises()
const numbers = [...document.querySelectorAll<HTMLInputElement>('.adjustment-form input')]
numbers[0]!.value = '1200'
numbers[0]!.value = '140'
numbers[0]!.dispatchEvent(new Event('input', { bubbles: true }))
numbers[0]!.dispatchEvent(new Event('change', { bubbles: true }))
numbers[1]!.value = '80'
numbers[1]!.value = '8.2'
numbers[1]!.dispatchEvent(new Event('input', { bubbles: true }))
numbers[1]!.dispatchEvent(new Event('change', { bubbles: true }))
numbers[2]!.value = '80'
numbers[2]!.dispatchEvent(new Event('input', { bubbles: true }))
numbers[2]!.dispatchEvent(new Event('change', { bubbles: true }))
const reason = document.querySelector<HTMLTextAreaElement>('.adjustment-form textarea')!
reason.value = '补充已确认的附加费用'
reason.dispatchEvent(new Event('input', { bubbles: true }))
@@ -203,7 +214,7 @@ describe('InboundFeePanel', () => {
expect(adjustPublishedInboundFee).toHaveBeenCalledWith(
'10001',
'88',
{ subtotalAmount: 1200, additionalAmount: 80, reason: '补充已确认的附加费用' },
{ expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费用' },
expect.stringMatching(/^fee-adjust-/),
)
expect(wrapper.emitted('changed')?.[0]?.[0]).toMatchObject({ feeVersion: 3 })
@@ -219,10 +230,12 @@ describe('InboundFeePanel', () => {
await flushPromises()
const numbers = [...document.querySelectorAll<HTMLInputElement>('.adjustment-form input')]
numbers[0]!.value = '1200'
numbers[0]!.value = '140'
numbers[0]!.dispatchEvent(new Event('input', { bubbles: true }))
numbers[1]!.value = '80'
numbers[1]!.value = '8.2'
numbers[1]!.dispatchEvent(new Event('input', { bubbles: true }))
numbers[2]!.value = '80'
numbers[2]!.dispatchEvent(new Event('input', { bubbles: true }))
const reason = document.querySelector<HTMLTextAreaElement>('.adjustment-form textarea')!
reason.value = '并发场景价格修正'
reason.dispatchEvent(new Event('input', { bubbles: true }))
+24 -13
View File
@@ -43,13 +43,16 @@ let adjustmentFingerprint = ''
let adjustmentIdempotencyKey = ''
const adjustmentForm = reactive({
subtotalAmount: 0,
expectedFeeVersion: 0,
chargeableQuantity: 0,
unitPrice: 0,
additionalAmount: 0,
reason: '',
})
const adjustmentRules: FormRules = {
subtotalAmount: [{ required: true, message: '请输入调整后的基础运费', trigger: 'change' }],
chargeableQuantity: [{ required: true, message: '请输入调整后的计费量', trigger: 'change' }],
unitPrice: [{ required: true, message: '请输入调整后的单价', trigger: 'change' }],
additionalAmount: [{ required: true, message: '请输入调整后的附加费', trigger: 'change' }],
reason: [
{ required: true, whitespace: true, message: '请填写费用调整原因', trigger: 'blur' },
@@ -61,8 +64,8 @@ const canView = computed(() => access.can('firstleg:fba-pricing:view'))
const canAdjust = computed(() => access.can('firstleg:fba-pricing:adjust'))
const customerContextId = computed(() => props.customerId || props.tenantId || '')
const adjustmentEligible = computed(() => (
snapshot.value?.feeStatus === 'SENT'
&& snapshot.value.nextProcessStatus === 'WAIT_SPLIT'
['CALCULATED', 'CONFIRMED', 'SENT'].includes(snapshot.value?.feeStatus || '')
&& snapshot.value?.nextProcessStatus === 'WAIT_SPLIT'
))
const chargeModeLabel = computed(() => snapshot.value?.chargeMode === 'KG' ? '按重量计费' : '按体积计费')
const chargeUnitLabel = computed(() => snapshot.value?.chargeUnit === 'KG' ? 'KG' : 'CBM')
@@ -142,7 +145,9 @@ async function loadFee(options: { preserveAdjustment?: boolean } = {}) {
}
function fillAdjustmentForm(value: InboundFeeSnapshot) {
adjustmentForm.subtotalAmount = Number(value.subtotalAmount)
adjustmentForm.expectedFeeVersion = Number(value.feeVersion)
adjustmentForm.chargeableQuantity = Number(value.chargeableQuantity)
adjustmentForm.unitPrice = Number(value.unitPrice)
adjustmentForm.additionalAmount = Number(value.additionalAmount)
adjustmentForm.reason = ''
adjustmentError.value = null
@@ -159,7 +164,9 @@ function openAdjustment() {
function adjustmentPayload(): AdjustPublishedInboundFeeRequest {
return {
subtotalAmount: Number(adjustmentForm.subtotalAmount),
expectedFeeVersion: adjustmentForm.expectedFeeVersion,
chargeableQuantity: Number(adjustmentForm.chargeableQuantity),
unitPrice: Number(adjustmentForm.unitPrice),
additionalAmount: Number(adjustmentForm.additionalAmount),
reason: adjustmentForm.reason.trim(),
}
@@ -244,7 +251,7 @@ defineExpose({ reload: loadFee })
data-testid="adjust-fee-button"
type="primary"
@click="openAdjustment"
><PencilLine :size="16" />调整已发布费用</el-button>
><PencilLine :size="16" />调整初算费用</el-button>
</div>
</header>
@@ -343,14 +350,14 @@ defineExpose({ reload: loadFee })
<AppDialog
v-model="adjustOpen"
title="调整已发布费用"
title="调整入库费用"
width="min(560px, calc(100vw - 28px))"
confirm-text="确认调整并生成新版本"
confirm-type="warning"
:confirm-loading="adjusting"
@confirm="submitAdjustment"
>
<el-alert type="warning" :closable="false" show-icon title="调整会生成新的已发布版本,不会回退实物入库状态。" />
<el-alert type="warning" :closable="false" show-icon title="调整会生成新的费用版本;小计和总额由服务端按计费量、单价和附加费重算。" />
<el-alert v-if="conflictRefreshed" class="adjustment-alert" type="warning" :closable="false" show-icon>
<template #title>费用已被其他操作员更新,系统已刷新最新快照</template>
<p>当前输入仍保留,请先核对最新版本后再提交。</p>
@@ -362,16 +369,20 @@ defineExpose({ reload: loadFee })
</el-alert>
<el-form ref="adjustFormRef" :model="adjustmentForm" :rules="adjustmentRules" label-position="top" class="adjustment-form" :disabled="adjusting">
<div class="adjustment-amounts">
<el-form-item label="调整后基础运费" prop="subtotalAmount" required>
<el-input-number v-model="adjustmentForm.subtotalAmount" aria-label="调整后基础运费" :min="0" :max="999999999999.99" :precision="2" :step="100" controls-position="right" />
<span>{{ snapshot?.currency }}</span>
<el-form-item label="调整后计费量" prop="chargeableQuantity" required>
<el-input-number v-model="adjustmentForm.chargeableQuantity" aria-label="调整后计费量" :min="0.0001" :max="999999999999.9999" :precision="4" :step="1" controls-position="right" />
<span>{{ chargeUnitLabel }}</span>
</el-form-item>
<el-form-item label="调整后单价" prop="unitPrice" required>
<el-input-number v-model="adjustmentForm.unitPrice" aria-label="调整后单价" :min="0" :max="999999999999.9999" :precision="4" :step="0.1" controls-position="right" />
<span>{{ snapshot?.currency }} / {{ chargeUnitLabel }}</span>
</el-form-item>
<el-form-item label="调整后附加费" prop="additionalAmount" required>
<el-input-number v-model="adjustmentForm.additionalAmount" aria-label="调整后附加费" :min="0" :max="999999999999.99" :precision="2" :step="10" controls-position="right" />
<span>{{ snapshot?.currency }}</span>
</el-form-item>
</div>
<div class="adjustment-total"><span>调整后总额</span><strong>{{ formatMoney(adjustmentForm.subtotalAmount + adjustmentForm.additionalAmount, snapshot?.currency) }}</strong></div>
<div class="adjustment-total"><span>当前服务端计算金额</span><strong>{{ formatMoney(snapshot?.totalAmount, snapshot?.currency) }}</strong><small>提交后以服务端返回的新版本金额为准</small></div>
<el-form-item label="调整原因" prop="reason" required>
<el-input v-model="adjustmentForm.reason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="说明价格变更、补充费用或修正原因,记录将进入审计历史" />
</el-form-item>
+2 -2
View File
@@ -34,7 +34,7 @@ describe('inbound fee API', () => {
it('adjusts a published snapshot with the required idempotency key', async () => {
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: { ...snapshot, feeVersion: 3 } })
const payload = { subtotalAmount: 1200, additionalAmount: 80, reason: '补充已确认的附加费' }
const payload = { expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费' }
await expect(adjustPublishedInboundFee('10001', '88', payload, 'fee-adjust-1'))
.resolves.toMatchObject({ feeVersion: 3 })
@@ -54,7 +54,7 @@ describe('inbound fee API', () => {
const error = await adjustPublishedInboundFee(
'10001',
'88',
{ subtotalAmount: 1200, additionalAmount: 80, reason: '价格修正' },
{ expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '价格修正' },
'fee-adjust-2',
).catch((reason: unknown) => reason)
+3 -1
View File
@@ -47,7 +47,9 @@ export interface InboundFeeSnapshot {
}
export interface AdjustPublishedInboundFeeRequest {
subtotalAmount: number
expectedFeeVersion: number
chargeableQuantity: number
unitPrice: number
additionalAmount: number
reason: string
}
@@ -110,6 +110,9 @@ describe('InboundOrdersView', () => {
expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeCustomerChange + 1)
expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ page: 1, size: 20 }))
expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' })
const customerQueryField = (toolbar.props('fields') as Array<{ key: string; type?: string; props?: Record<string, unknown> }>).find((field) => field.key === 'customerId')
expect(customerQueryField?.type).toBe('select')
expect(customerQueryField?.props?.filterable).toBe(true)
expect(document.body.textContent).toContain('RK-001')
expect(document.body.textContent).toContain('查看')
expect(document.body.textContent).toContain('示例企业有限公司')
@@ -117,6 +120,18 @@ describe('InboundOrdersView', () => {
expect(wrapper.find('.inbound-tabs .is-active').text()).toContain('全部')
})
it('keeps the customer filter name-only and does not add a customer-code input', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/pages/InboundOrdersView.vue'), 'utf8')
const queryFieldsSource = source.slice(source.indexOf('const allQueryFields'), source.indexOf('const columns'))
expect(queryFieldsSource).toContain("key: 'customerId'")
expect(queryFieldsSource).toContain("type: 'select'")
expect(queryFieldsSource).toContain('filterable: true')
expect(queryFieldsSource).toContain('legalName?.trim() || customer.tenantName')
expect(queryFieldsSource).not.toContain('tenantCode')
expect(queryFieldsSource).not.toContain('客户编码')
})
it('uses backend-supported warehouse IDs from tenant-scoped options without coercing Long IDs', async () => {
const wrapper = mountPage()
const toolbar = await selectCustomer(wrapper)
@@ -65,7 +65,7 @@ const tabs: Array<{ value: InboundListTab | ''; label: string; description: stri
{ value: 'WAIT_MEASURE', label: '待测量', description: '等待测量或补正' },
{ value: 'WAIT_REVIEW', label: '待审核', description: '等待测量审核' },
{ value: 'WAIT_FEE_SEND', label: '待费用/入库', description: '等待费用编排完成' },
{ value: 'WAIT_SPLIT', label: '待分柜', description: '费用已发布并完成入库,等待分柜' },
{ value: 'WAIT_SPLIT', label: '待分柜', description: '测量审核完成,等待分柜;费用可继续维护' },
]
const advancedFilterKeys = new Set(['orderStatus', 'feeReviewStatus', 'customerName', 'destinationCode', 'holdFlag'])
@@ -55,7 +55,7 @@ describe('MeasurementReviewPanel', () => {
expect(wrapper.text()).toContain('调整原因:去除托盘重量后采用净值')
expect(wrapper.findComponent(DataTable).exists()).toBe(true)
expect(wrapper.text()).not.toContain('租户')
expect(wrapper.text()).toContain('通过并完成入库')
expect(wrapper.text()).toContain('通过并进入待分柜')
expect(wrapper.text()).not.toContain('确认费用')
expect(getMeasurementReviewDetail).toHaveBeenCalledWith('10001', '90001')
const source = await import('./MeasurementReviewPanel.vue?raw').then((module) => module.default)
@@ -77,25 +77,37 @@ describe('MeasurementReviewPanel', () => {
})
it('emits changed after a successful review write', async () => {
vi.mocked(approveMeasurement).mockResolvedValue({ feeId: '1', feeNo: 'FEE-1', feeVersion: 1, feeStatus: 'SENT', inboundId: '90001', totalAmount: 100, currency: 'CNY', nextProcessStatus: 'WAIT_SPLIT' })
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
const buttons = wrapper.findAll('button')
const approve = buttons.find((button) => button.text().includes('通过并完成入库'))
const approve = buttons.find((button) => button.text().includes('通过并进入待分柜'))
expect(approve).toBeDefined()
await approve?.trigger('click')
await flushPromises()
expect(ElMessageBox.confirm).toHaveBeenCalledWith(
expect.stringContaining('自动试算并发布费用'),
expect.stringContaining('初算失败不影响审核通过'),
'确认测量审核通过',
expect.objectContaining({ confirmButtonText: '通过并完成入库' }),
expect.objectContaining({ confirmButtonText: '通过并进入待分柜' }),
)
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 7 })
expect(wrapper.emitted('changed')).toHaveLength(1)
})
it('keeps approval successful when initial pricing fails', async () => {
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'UNPRICED', initialPricingSucceeded: false, initialPricingMessage: '缺少有效价格', initialFee: null })
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))?.trigger('click')
await flushPromises()
expect(wrapper.emitted('changed')).toHaveLength(1)
expect(wrapper.text()).toContain('初算失败不阻断审核')
})
it('rejects with the inbound optimistic-lock version instead of the measurement version', async () => {
vi.mocked(rejectMeasurement).mockResolvedValue(undefined)
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
@@ -145,20 +145,24 @@ async function confirmApprove() {
if (!detail.value || !isPending.value || !canReview.value) return
try {
await ElMessageBox.confirm(
'审核通过后,系统会自动试算并发布费用,同时完成托盘库位占用和入库。客户无需再次确认费用。是否继续?',
'审核通过后,入库单将进入待分柜。系统会尝试生成初算费用,但初算失败不影响审核通过,后续可由平台人员补充调整。是否继续?',
'确认测量审核通过',
{ confirmButtonText: '通过并完成入库', cancelButtonText: '返回', type: 'warning' },
{ confirmButtonText: '通过并进入待分柜', cancelButtonText: '返回', type: 'warning' },
)
} catch {
return
}
actionLoading.value = 'approve'
try {
await approveMeasurement(customerId.value, props.inboundId, {
const result = await approveMeasurement(customerId.value, props.inboundId, {
measurementId: detail.value.measurementId,
expectedVersion: detail.value.inboundVersion,
})
ElMessage.success('审核已通过,费用发布和入库正在完成')
if (result.initialPricingSucceeded) {
ElMessage.success('审核已通过,入库单已进入待分柜,费用已完成初算')
} else {
ElMessage.warning(`审核已通过,入库单已进入待分柜;费用初算未完成${result.initialPricingMessage ? `:${result.initialPricingMessage}` : ''}`)
}
await reload()
emit('changed')
} catch (error) {
@@ -295,10 +299,10 @@ defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canRevie
</div>
<div class="review-actions" :class="{ 'review-actions--disabled': !isPending && !isRejected }">
<div><strong>审核结论</strong><span v-if="isPending">通过后将自动完成计费、费用发布和入库。</span><span v-else-if="isRejected">当前测量已驳回,可修正后重新提交,或按权限申请异常放行。</span><span v-else>当前记录不在可操作的测量审核状态。</span></div>
<div><strong>审核结论</strong><span v-if="isPending">通过后进入待分柜;费用仅做初算,初算失败不阻断审核。</span><span v-else-if="isRejected">当前测量已驳回,可修正后重新提交,或按权限申请异常放行。</span><span v-else>当前记录不在可操作的测量审核状态。</span></div>
<div class="action-buttons">
<el-button v-if="isPending && canReview" type="danger" plain :loading="actionLoading === 'reject'" @click="openReasonDialog('reject')"><X :size="16" />驳回</el-button>
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve'" @click="confirmApprove"><Check :size="16" />通过并完成入库</el-button>
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve'" @click="confirmApprove"><Check :size="16" />通过并进入待分柜</el-button>
<el-button v-if="isRejected && canRelease" type="warning" plain :loading="actionLoading === 'release'" @click="openReasonDialog('release')"><ShieldAlert :size="16" />异常放行</el-button>
</div>
</div>
+2 -2
View File
@@ -1,7 +1,7 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type {
ExceptionReleaseRequest,
FeeSnapshotAfterApprove,
MeasurementApprovalResponse,
MeasurementReviewDetail,
MeasurementReviewHistoryPage,
MeasurementReviewHistoryPayload,
@@ -63,7 +63,7 @@ export function approveMeasurement(
request: MeasurementReviewRequest,
key?: string,
) {
return postReview<FeeSnapshotAfterApprove>(
return postReview<MeasurementApprovalResponse>(
`/platform/v1/inbounds/${inboundId}/measurements/approve`,
tenantId,
request,
+10
View File
@@ -116,6 +116,16 @@ export interface ExceptionReleaseRequest {
reason: string
}
export interface MeasurementApprovalResponse {
inboundId: string | null
measurementId: string | null
processStatus: 'WAIT_SPLIT' | string
feeStatus: 'CALCULATED' | 'UNPRICED' | string
initialPricingSucceeded: boolean
initialPricingMessage: string | null
initialFee: FeeSnapshotAfterApprove | null
}
export interface FeeSnapshotAfterApprove {
feeId: string | null
feeNo: string | null
+27
View File
@@ -1,7 +1,34 @@
import type { PlatformTenantResource } from '@/modules/iam'
import type { InboundListQuery } from './core/types'
export type InboundTaskMode = 'measurement' | 'review'
export interface ActiveCustomerPage {
records: PlatformTenantResource[]
pages: number
}
export interface ActiveCustomerQuery {
page: number
size: number
status: 'ACTIVE'
}
export async function loadAllActiveCustomers(
fetchPage: (query: ActiveCustomerQuery) => Promise<ActiveCustomerPage>,
) {
const records: PlatformTenantResource[] = []
let page = 1
let pages = 1
do {
const response = await fetchPage({ page, size: 200, status: 'ACTIVE' })
records.push(...response.records)
pages = Math.max(1, response.pages)
page += 1
} while (page <= pages)
return records
}
export function buildInboundTaskQuery(
mode: InboundTaskMode,
query: Omit<InboundListQuery, 'tab' | 'processStatus' | 'measureReviewStatus'>,