优化
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/measurement/InboundMeasurementDialog.vue'), 'utf8')
|
||||
const panelSource = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/measurement/InboundMeasurementPanel.vue'), 'utf8')
|
||||
const taskListSource = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
const orderListSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/pages/UnifiedOrdersView.vue'), 'utf8')
|
||||
|
||||
describe('InboundMeasurementDialog', () => {
|
||||
it('owns the complete measurement workspace behind one centered dialog', () => {
|
||||
expect(source).toContain('class="inbound-measurement-workspace-dialog"')
|
||||
expect(source).toContain('width="min(1240px, calc(100vw - 32px))"')
|
||||
expect(source).toContain('height: 92vh')
|
||||
expect(source).toContain('<WeightOcrPanel')
|
||||
expect(source).toContain('<InboundMeasurementPanel')
|
||||
expect(source).toContain('@summary-changed="handleWeightSummaryChanged"')
|
||||
expect(source).toContain('measurementPanelRef.value?.updateActualGrossWeight(actualGrossWeightKg)')
|
||||
})
|
||||
|
||||
it('is the only measurement dialog opened by both formal list entries', () => {
|
||||
for (const entrySource of [taskListSource, orderListSource]) {
|
||||
expect(entrySource).toContain('<InboundMeasurementDialog')
|
||||
expect(entrySource).not.toContain('<InboundMeasurementPanel')
|
||||
expect(entrySource).not.toContain('<WeightOcrPanel')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps draft saves open, closes only after submit, and refreshes parent entries consistently', () => {
|
||||
expect(source).toContain('@saved="handleUpdated"')
|
||||
expect(source).toContain('@updated="handleUpdated"')
|
||||
expect(source).toContain('@submitted="handleSubmitted"')
|
||||
expect(source).toContain("emit('updated')")
|
||||
expect(source).toContain("emit('update:modelValue', false)")
|
||||
expect(source).toContain("emit('submitted')")
|
||||
expect(panelSource).toContain("emit('saved')")
|
||||
expect(panelSource).toContain("emit('submitted')")
|
||||
})
|
||||
|
||||
it('uses the existing permission-aware panels and protects unsaved work on close', () => {
|
||||
expect(source).toContain('measurementPanelRef.value?.isDirty')
|
||||
expect(source).toContain('当前测量数据尚未保存')
|
||||
expect(source).toContain(':close-on-click-modal="false"')
|
||||
expect(source).toContain(':before-close="beforeClose"')
|
||||
expect(source).toContain('measurementPanelRef?.canEdit')
|
||||
expect(source).toContain('measurementPanelRef?.canSubmit')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import InboundMeasurementPanel from './InboundMeasurementPanel.vue'
|
||||
import WeightOcrPanel from '../weight/components/WeightOcrPanel.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
customerId: string
|
||||
inboundId: string
|
||||
inboundCode?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
updated: []
|
||||
submitted: []
|
||||
closed: []
|
||||
}>()
|
||||
|
||||
const measurementPanelRef = ref<InstanceType<typeof InboundMeasurementPanel> | null>(null)
|
||||
const title = computed(() => `测量 · ${props.inboundCode?.trim() || props.inboundId}`)
|
||||
|
||||
async function mayClose() {
|
||||
if (!measurementPanelRef.value?.isDirty) return true
|
||||
try {
|
||||
await ElMessageBox.confirm('当前测量数据尚未保存,关闭后将丢失修改。确定关闭吗?', '关闭测量', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '放弃修改并关闭',
|
||||
cancelButtonText: '继续编辑',
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function requestClose() {
|
||||
if (await mayClose()) emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
async function beforeClose(done: () => void) {
|
||||
if (await mayClose()) done()
|
||||
}
|
||||
|
||||
function handleWeightSummaryChanged(actualGrossWeightKg: number | null) {
|
||||
measurementPanelRef.value?.updateActualGrossWeight(actualGrossWeightKg)
|
||||
}
|
||||
|
||||
function handleUpdated() {
|
||||
emit('updated')
|
||||
}
|
||||
|
||||
function handleSubmitted() {
|
||||
emit('update:modelValue', false)
|
||||
emit('submitted')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
class="inbound-measurement-workspace-dialog"
|
||||
:title="title"
|
||||
width="min(1240px, calc(100vw - 32px))"
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:before-close="beforeClose"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<div class="measurement-dialog-workspace">
|
||||
<section class="surface-panel measurement-dialog-weight" aria-label="称重图片、OCR 与托盘核对">
|
||||
<WeightOcrPanel
|
||||
:customer-id="customerId"
|
||||
:inbound-id="inboundId"
|
||||
@changed="handleUpdated"
|
||||
@summary-changed="handleWeightSummaryChanged"
|
||||
/>
|
||||
</section>
|
||||
<InboundMeasurementPanel
|
||||
ref="measurementPanelRef"
|
||||
:customer-id="customerId"
|
||||
:inbound-id="inboundId"
|
||||
@updated="handleUpdated"
|
||||
@saved="handleUpdated"
|
||||
@submitted="handleSubmitted"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="measurement-dialog-footer">
|
||||
<el-button :disabled="Boolean(measurementPanelRef?.saving || measurementPanelRef?.submitting)" @click="requestClose">取消</el-button>
|
||||
<el-button
|
||||
v-if="measurementPanelRef?.canEdit"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="measurementPanelRef?.saving"
|
||||
:disabled="!measurementPanelRef?.mayMaintain"
|
||||
@click="measurementPanelRef?.saveDraft()"
|
||||
>保存测量数据</el-button>
|
||||
<el-button
|
||||
v-if="measurementPanelRef?.canSubmit"
|
||||
type="primary"
|
||||
:loading="measurementPanelRef?.submitting"
|
||||
:disabled="!measurementPanelRef?.maySubmit"
|
||||
@click="measurementPanelRef?.submitForReview()"
|
||||
>提交审核</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.measurement-dialog-workspace { display: grid; min-width: 0; gap: 20px; }
|
||||
.measurement-dialog-weight { min-width: 0; padding: 20px; }
|
||||
.measurement-dialog-footer { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
|
||||
.measurement-dialog-footer :deep(.el-button) { margin-left: 0; }
|
||||
@media (max-width: 720px) {
|
||||
.measurement-dialog-weight { padding: 14px; }
|
||||
.measurement-dialog-footer { display: grid; grid-template-columns: 1fr; }
|
||||
.measurement-dialog-footer :deep(.el-button) { width: 100%; min-height: 44px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.inbound-measurement-workspace-dialog.el-dialog { display: flex; max-width: calc(100vw - 32px); height: 92vh; max-height: 92vh; margin-bottom: 0; flex-direction: column; overflow: hidden; }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__header { flex: 0 0 auto; margin: 0; padding: 18px 24px; border-bottom: 1px solid var(--cc-border); }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__body { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 20px 24px; }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__footer { flex: 0 0 auto; padding: 14px 24px; border-top: 1px solid var(--cc-border); }
|
||||
.inbound-measurement-workspace-dialog .measurement-panel { border: 0; border-radius: 0; box-shadow: none; }
|
||||
.inbound-measurement-workspace-dialog .measurement-panel__heading .measurement-panel__actions,
|
||||
.inbound-measurement-workspace-dialog .measurement-panel__footer { display: none; }
|
||||
@media (max-width: 720px) {
|
||||
.inbound-measurement-workspace-dialog.el-dialog { width: calc(100vw - 20px) !important; max-width: calc(100vw - 20px); }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__header { padding: 16px; }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__body { padding: 14px; }
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.inbound-measurement-workspace-dialog.el-dialog { width: calc(100vw - 12px) !important; max-width: calc(100vw - 12px); }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__header { padding: 14px 12px; }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__body { padding: 10px; }
|
||||
}
|
||||
</style>
|
||||
@@ -24,6 +24,8 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
changed: []
|
||||
updated: []
|
||||
saved: []
|
||||
submitted: []
|
||||
}>()
|
||||
|
||||
interface EditableMeasurementItem extends SaveInboundMeasurementItemRequest {
|
||||
@@ -360,6 +362,7 @@ async function saveDraft() {
|
||||
form.version = version
|
||||
ElMessage.success('确认箱数已保存,计算结果已由服务端更新')
|
||||
await loadMeasurement()
|
||||
emit('saved')
|
||||
emit('changed')
|
||||
} catch (error) {
|
||||
const details = setOperationError(error, '测量草稿保存失败,请检查后重试')
|
||||
@@ -394,6 +397,7 @@ async function submitForReview() {
|
||||
})
|
||||
ElMessage.success('测量数据已提交审核')
|
||||
await loadMeasurement()
|
||||
emit('submitted')
|
||||
emit('changed')
|
||||
} catch (error) {
|
||||
const details = setOperationError(error, '测量数据提交失败,请检查图片和称重结果后重试')
|
||||
@@ -459,7 +463,7 @@ watch(
|
||||
|
||||
onBeforeUnmount(releaseImagePreview)
|
||||
|
||||
defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit })
|
||||
defineExpose({ saveDraft, submitForReview, updateActualGrossWeight, mayMaintain, maySubmit, saving, submitting, canEdit, canSubmit, isDirty })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { ImageOff } from '@lucide/vue'
|
||||
import { downloadWeightThumbnail } from '../../weight/api/weightOcr'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
customerId: string
|
||||
inboundId: string
|
||||
fileId?: string | null
|
||||
alt: string
|
||||
emptyText?: string
|
||||
}>(), {
|
||||
fileId: null,
|
||||
emptyText: '未绑定测量图片',
|
||||
})
|
||||
|
||||
defineEmits<{ open: [] }>()
|
||||
|
||||
const imageUrl = ref('')
|
||||
const loading = ref(false)
|
||||
const failed = ref(false)
|
||||
let requestId = 0
|
||||
|
||||
function releaseImage() {
|
||||
if (imageUrl.value) URL.revokeObjectURL(imageUrl.value)
|
||||
imageUrl.value = ''
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const currentRequest = ++requestId
|
||||
releaseImage()
|
||||
failed.value = false
|
||||
if (!props.fileId) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const blob = await downloadWeightThumbnail(props.customerId, props.inboundId, props.fileId)
|
||||
if (currentRequest !== requestId) return
|
||||
imageUrl.value = URL.createObjectURL(blob)
|
||||
} catch {
|
||||
if (currentRequest === requestId) failed.value = true
|
||||
} finally {
|
||||
if (currentRequest === requestId) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => [props.customerId, props.inboundId, props.fileId], load, { immediate: true })
|
||||
onBeforeUnmount(() => {
|
||||
requestId += 1
|
||||
releaseImage()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="measurement-evidence-thumb"
|
||||
type="button"
|
||||
:disabled="!fileId"
|
||||
:aria-label="fileId ? `查看${alt}` : emptyText"
|
||||
@click="$emit('open')"
|
||||
>
|
||||
<img v-if="imageUrl" :src="imageUrl" :alt="alt" loading="lazy" />
|
||||
<span v-else class="measurement-evidence-thumb__state" :class="{ 'is-loading': loading }">
|
||||
<ImageOff v-if="failed || !fileId" :size="22" aria-hidden="true" />
|
||||
<span>{{ !fileId ? emptyText : failed ? '图片加载失败,点击重试' : '图片加载中' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.measurement-evidence-thumb { display: block; width: 100%; min-width: 96px; aspect-ratio: 4 / 3; overflow: hidden; padding: 0; border: 1px solid var(--cc-border); border-radius: 6px; background: #f2f4f7; cursor: zoom-in; }
|
||||
.measurement-evidence-thumb:disabled { cursor: default; }
|
||||
.measurement-evidence-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
.measurement-evidence-thumb__state { display: grid; width: 100%; height: 100%; place-items: center; align-content: center; gap: 7px; padding: 8px; color: #667085; font-size: 12px; }
|
||||
.measurement-evidence-thumb__state.is-loading { color: #98a2b3; }
|
||||
.measurement-evidence-thumb:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 2px; }
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as InboundMeasurementPanel } from './InboundMeasurementPanel.vue'
|
||||
export { default as InboundMeasurementDialog } from './InboundMeasurementDialog.vue'
|
||||
export { default as MeasurementEvidenceThumbnail } from './components/MeasurementEvidenceThumbnail.vue'
|
||||
export * from './api'
|
||||
export type * from './types'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user