This commit is contained in:
2026-08-08 11:05:45 +08:00
parent 6f709267b7
commit 4835904507
5 changed files with 133 additions and 13 deletions
+14 -3
View File
@@ -76,6 +76,7 @@ const decisionOpen = ref(false)
const reviewSubmitting = ref(false)
const approvalResultOpen = ref(false)
const approvalResult = ref<EnterpriseApprovalResponse | null>(null)
const credentialSource = ref<'creation' | 'approval'>('approval')
const decision = reactive({ type: 'approve' as 'approve' | 'reject', comment: '' })
const decisionFormRef = ref<FormInstance>()
const passwordDialogOpen = ref(false)
@@ -558,8 +559,17 @@ async function saveCustomer() {
saved = await updatePlatformTenant(editingId.value, payload)
} else {
const { unifiedSocialCreditCode: _qualificationCreditCode, ...payload } = form.value
saved = await createPlatformTenant(payload)
const created = await createPlatformTenant(payload)
saved = created.tenant
editingId.value = String(saved.id)
credentialSource.value = 'creation'
approvalResult.value = {
tenantId: saved.id,
administratorCreated: true,
administratorUsername: created.administratorUsername,
temporaryPassword: created.temporaryPassword,
}
approvalResultOpen.value = true
}
editorStatus.value = saved.status
savedFormSnapshot.value = JSON.stringify(form.value)
@@ -780,6 +790,7 @@ async function submitDecision() {
: '企业资料已审核通过,注册账号继续作为企业管理员'
ElMessage.success(successMessage)
if (approved?.administratorCreated) {
credentialSource.value = 'approval'
approvalResult.value = approved
approvalResultOpen.value = true
}
@@ -930,9 +941,9 @@ onMounted(() => { void loadCustomers() })
<template #footer><el-button @click="qualificationPreviewOpen = false">关闭预览</el-button></template>
</el-dialog>
<el-dialog v-model="approvalResultOpen" title="审核通过,管理员账号已创建" width="min(560px, calc(100vw - 28px))" class="approval-result-dialog" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
<el-dialog v-model="approvalResultOpen" :title="credentialSource === 'creation' ? '客户已创建,管理员账号已生成' : '审核通过,管理员账号已创建'" width="min(560px, calc(100vw - 28px))" class="approval-result-dialog" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
<div v-if="approvalResult" class="approval-result-content">
<div class="approval-result-warning" role="alert"><ShieldCheck :size="20" /><div><strong>请立即保存以下一次性凭证</strong><span>临时密码只在本次审核响应中返回,关闭后无法再次查看。</span></div></div>
<div class="approval-result-warning" role="alert"><ShieldCheck :size="20" /><div><strong>请立即保存以下一次性凭证</strong><span>临时密码只在本次{{ credentialSource === 'creation' ? '创建' : '审核' }}响应中返回,关闭后无法再次查看。</span></div></div>
<dl class="approval-credentials">
<div><dt>登录账号</dt><dd><code>{{ approvalResult.administratorUsername || '未返回' }}</code><CopyButton v-if="approvalResult.administratorUsername" :value="approvalResult.administratorUsername" label="登录账号" size="small" /></dd></div>
<div><dt>临时密码</dt><dd><code>{{ approvalResult.temporaryPassword || '未返回' }}</code><CopyButton v-if="approvalResult.temporaryPassword" :value="approvalResult.temporaryPassword" label="临时密码" size="small" /></dd></div>
+3 -2
View File
@@ -281,10 +281,11 @@ describe('platform IAM roles', () => {
establishedDate: '2020-01-01', registeredCapital: 1000, registeredCapitalCurrency: 'CNY',
registeredAddress: '深圳市宝安区', internalRemark: '重点客户',
}
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: tenant })
const creation = { tenant, administratorUsername: '13800138000', temporaryPassword: 'Password1234' }
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: creation })
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: tenant })
await expect(createPlatformTenant(create)).resolves.toEqual(tenant)
await expect(createPlatformTenant(create)).resolves.toEqual(creation)
await expect(updatePlatformTenant('8', create)).resolves.toEqual(tenant)
await expect(changePlatformTenantStatus('8', 'DISABLED')).resolves.toEqual(tenant)
+7 -1
View File
@@ -141,6 +141,12 @@ export interface EnterpriseApprovalResponse {
temporaryPassword: string | null
}
export interface PlatformTenantCreationResponse {
tenant: PlatformTenantResource
administratorUsername: string
temporaryPassword: string
}
export interface ResetPlatformTenantOwnerPasswordRequest {
newPassword: string
forceChangePassword: boolean
@@ -350,7 +356,7 @@ export async function checkPlatformTenantContactPhone(contactPhone: string, excl
}
export function createPlatformTenant(payload: CreatePlatformTenantRequest) {
return postData<PlatformTenantResource>('/platform/v1/iam/tenants', payload)
return postData<PlatformTenantCreationResponse>('/platform/v1/iam/tenants', payload)
}
export function updatePlatformTenant(tenantId: number | string, payload: UpdatePlatformTenantRequest) {
@@ -40,7 +40,7 @@ const pendingWeight: WeightResultResource = {
ocrRawText: '285.5',
ocrWeightValue: 285.5,
ocrWeightUnit: 'KG',
confidence: 0.50972,
confidence: 0.98,
adjustedWeightValue: null,
adjustedWeightUnit: null,
finalWeightKg: null,
@@ -83,9 +83,11 @@ function mountPanel(permissions = allWeightPermissions) {
})
}
async function openWeightDialog(wrapper: VueWrapper) {
async function openWeightDialog(wrapper: VueWrapper, reviewEvidence = true) {
await wrapper.get('button[aria-label="查看称重图片 1"]').trigger('click')
await flushPromises()
const evidence = document.body.querySelector<HTMLInputElement>('.ocr-review-check input[type="checkbox"]')
if (reviewEvidence && evidence) await new DOMWrapper(evidence).setValue(true)
}
function dialogWeightInput() {
@@ -106,6 +108,12 @@ function dialogConfirmButton() {
return new DOMWrapper(button)
}
function dialogEvidenceCheckbox() {
const input = document.body.querySelector<HTMLInputElement>('.ocr-review-check input[type="checkbox"]')
if (!input) throw new Error('OCR evidence checkbox is missing')
return new DOMWrapper(input)
}
describe('WeightOcrPanel confirmation behavior', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -191,12 +199,17 @@ describe('WeightOcrPanel confirmation behavior', () => {
expect(deleteWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', 0)
})
it('confirms an unchanged OCR value with one request', async () => {
it('requires image review before confirming even a high-confidence OCR value', async () => {
const wrapper = mountPanel()
await flushPromises()
await openWeightDialog(wrapper)
await openWeightDialog(wrapper, false)
expect(document.body.textContent).not.toContain('识别重量 285.500 KG')
expect(document.body.textContent).toContain('OCR 原文')
expect(document.body.textContent).toContain('285.5')
expect(document.body.textContent).toContain('98%')
expect(document.body.textContent).toContain('OCR 可能误读数字或小数点')
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
await dialogEvidenceCheckbox().setValue(true)
await dialogConfirmButton().trigger('click')
await flushPromises()
@@ -205,6 +218,28 @@ describe('WeightOcrPanel confirmation behavior', () => {
expect(correctWeightResult).not.toHaveBeenCalled()
})
it('requires explicit image review before confirming a low-confidence OCR value', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue({
...workspace,
weightResults: [{ ...pendingWeight, ocrRawText: '5360', ocrWeightValue: 5360, confidence: 0.51863 }],
})
const wrapper = mountPanel()
await flushPromises()
await openWeightDialog(wrapper, false)
expect(document.body.textContent).toContain('OCR 可能误读数字或小数点')
expect(document.body.textContent).toContain('OCR 原文')
expect(document.body.textContent).toContain('5360')
expect(document.body.textContent).toContain('52%')
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
await dialogEvidenceCheckbox().setValue(true)
await dialogConfirmButton().trigger('click')
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 0 })
})
it('sends the adjusted value and a specific audit reason in the same confirmation request', async () => {
const wrapper = mountPanel()
await flushPromises()
@@ -379,6 +414,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
expect(dialogConfirmButton().attributes('disabled')).toBeDefined()
await dialogWeightInput().setValue('1.500')
await dialogEvidenceCheckbox().setValue(true)
await dialogConfirmButton().trigger('click')
await flushPromises()
@@ -41,6 +41,7 @@ const previewUrl = ref('')
const previewShowsRecognition = ref(false)
const previewResultId = ref('')
const previewSnapshot = ref<WeightResultResource | null>(null)
const previewEvidenceReviewed = ref(false)
const weightDrafts = ref<Record<string, { value: number | null; unit: 'KG' | 'LB' }>>({})
let loadSequence = 0
@@ -55,11 +56,34 @@ const editable = computed(() => Boolean(workspace.value && editableStatuses.has(
const rows = computed(() => workspace.value?.weightResults ?? [])
const previewResult = computed(() => rows.value.find((row) => row.id === previewResultId.value)
|| previewSnapshot.value)
const previewConfirmDisabled = computed(() => {
const row = previewResult.value
return !row
|| !isWeightDraftValid(row)
|| Boolean(activeAction.value)
|| (requiresEvidenceReview(row) && !previewEvidenceReviewed.value)
})
function formatWeight(value: number | null, unit = 'KG') {
return value == null ? '—' : `${Number(value).toFixed(3)} ${unit}`
}
function formatConfidence(value: number | null) {
return value == null ? '未返回' : `${Math.round(value * 100)}%`
}
function recognitionLabel(status: string) {
return ({
PENDING_CONFIRM: '待确认',
NEEDS_MANUAL: '需人工录入',
CONFIRMED: '已确认',
} as Record<string, string>)[status] || status
}
function requiresEvidenceReview(row: WeightResultResource) {
return row.recognitionStatus !== 'CONFIRMED' && row.adjustedWeightValue == null
}
function actionKey(action: string, id: string) {
return `${action}:${id}`
}
@@ -201,6 +225,10 @@ async function confirmResult(row: WeightResultResource, closeAfterSuccess = fals
ElMessage.warning('请输入大于 0 的有效重量')
return
}
if (requiresEvidenceReview(row) && !previewEvidenceReviewed.value) {
ElMessage.warning('请先对照图片核对小数点和重量单位')
return
}
const key = actionKey('confirm', row.id)
activeAction.value = key
actionError.value = null
@@ -238,6 +266,7 @@ async function openPreview(row: WeightResultResource, localUrl = '') {
releasePreview()
previewResultId.value = row.id
previewSnapshot.value = row
previewEvidenceReviewed.value = false
previewShowsRecognition.value = Boolean(localUrl)
previewError.value = null
previewOpen.value = true
@@ -300,7 +329,12 @@ watch(
{ immediate: true },
)
watch(previewOpen, (open) => { if (!open) releasePreview() })
watch(previewOpen, (open) => {
if (!open) {
previewEvidenceReviewed.value = false
releasePreview()
}
})
onBeforeUnmount(releasePreview)
defineExpose({ reload: loadWorkspace })
@@ -409,7 +443,7 @@ defineExpose({ reload: loadWorkspace })
title="确认称重图片"
:loading="previewLoading"
:confirm-loading="Boolean(previewResult && isActionLoading('confirm', previewResult.id))"
:confirm-disabled="!previewResult || !isWeightDraftValid(previewResult) || Boolean(activeAction)"
:confirm-disabled="previewConfirmDisabled"
:show-confirm="Boolean(previewResult && previewResult.recognitionStatus !== 'CONFIRMED' && canConfirm && editable)"
confirm-text="确认重量"
confirm-type="success"
@@ -427,6 +461,22 @@ defineExpose({ reload: loadWorkspace })
</LoadErrorState>
<template v-else-if="previewResult">
<img v-if="previewUrl" class="weight-preview" :src="previewUrl" alt="当前入库单的称重图片" />
<dl class="ocr-evidence" aria-label="OCR 识别证据">
<div><dt>识别状态</dt><dd>{{ recognitionLabel(previewResult.recognitionStatus) }}</dd></div>
<div><dt>置信度</dt><dd>{{ formatConfidence(previewResult.confidence) }}</dd></div>
<div class="ocr-evidence__raw"><dt>OCR 原文</dt><dd>{{ previewResult.ocrRawText || '未识别到有效重量' }}</dd></div>
<div v-if="previewResult.adjustmentReason" class="ocr-evidence__raw">
<dt>人工修正</dt><dd>{{ previewResult.adjustmentReason }}</dd>
</div>
</dl>
<el-alert
v-if="previewResult.recognitionStatus !== 'CONFIRMED' && requiresEvidenceReview(previewResult)"
class="ocr-review-alert"
type="warning"
:closable="false"
show-icon
title="OCR 可能误读数字或小数点,确认前请对照原图核对。"
/>
<div v-if="previewResult.recognitionStatus === 'CONFIRMED'" class="preview-weight is-confirmed">
<span>确认重量</span><strong>{{ formatWeight(previewResult.finalWeightKg) }}</strong>
</div>
@@ -452,6 +502,12 @@ defineExpose({ reload: loadWorkspace })
:options="['KG', 'LB']"
:disabled="Boolean(activeAction) || !editable || !canEdit || !canConfirm"
/>
<el-checkbox
v-if="requiresEvidenceReview(previewResult)"
v-model="previewEvidenceReviewed"
class="ocr-review-check"
:disabled="Boolean(activeAction)"
>已对照图片核对小数点和重量单位</el-checkbox>
</div>
</template>
</AppDialog>
@@ -479,10 +535,18 @@ defineExpose({ reload: loadWorkspace })
.weight-card__value strong { color: #101828; font-size: 17px; }
.weight-card__value:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 4px; }
.weight-preview { display: block; width: 100%; max-height: min(58vh, 560px); object-fit: contain; border-radius: 6px; background: #f2f4f7; }
.ocr-evidence { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 18px; margin: 14px 0 0; padding: 12px 0; border-block: 1px solid var(--cc-border); }
.ocr-evidence div { min-width: 0; }
.ocr-evidence dt { color: var(--cc-muted); font-size: 12px; }
.ocr-evidence dd { margin: 4px 0 0; color: #344054; font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; white-space: pre-wrap; }
.ocr-evidence__raw { grid-column: 1 / -1; }
.ocr-review-alert { margin-top: 12px; }
.preview-weight { display: grid; grid-template-columns: minmax(0, 1fr) 120px; align-items: end; gap: 12px; margin-top: 14px; }
.preview-weight__recognized { grid-column: 1 / -1; color: var(--cc-muted); font-size: 12px; }
.preview-weight label { display: grid; gap: 6px; color: #475467; font-size: 12px; }
.preview-weight :deep(.el-input-number), .preview-weight :deep(.el-segmented) { width: 100%; }
.ocr-review-check { grid-column: 1 / -1; white-space: normal; }
.ocr-review-check :deep(.el-checkbox__label) { line-height: 1.5; white-space: normal; }
.preview-weight.is-confirmed { display: flex; align-items: baseline; justify-content: space-between; padding: 12px 0 0; }
.preview-weight.is-confirmed span { color: var(--cc-muted); font-size: 12px; }
.preview-weight.is-confirmed strong { color: #067647; font-size: 20px; }
@@ -497,5 +561,7 @@ defineExpose({ reload: loadWorkspace })
.weight-grid { grid-template-columns: 1fr; }
.preview-weight { grid-template-columns: 1fr; }
.preview-weight__recognized { grid-column: 1; }
.ocr-evidence { grid-template-columns: 1fr; }
.ocr-evidence__raw { grid-column: 1; }
}
</style>