优化
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user