优化
This commit is contained in:
@@ -23,6 +23,6 @@ export async function bindLoadingPhoto(no: string, slot: LoadingPhotoSlot, body:
|
||||
}
|
||||
export async function readLoadingPhoto(no: string, photo: LoadingPhoto) {
|
||||
const blob = await http.get<Blob, Blob>(`${path(no)}/${photo.slot}/files/${encodeURIComponent(photo.fileId)}/content`, { responseType: 'blob' })
|
||||
if (!(blob instanceof Blob) || !['image/jpeg', 'image/png'].includes(blob.type) || !blob.size) throw new Error('图片读取失败,请重新核对')
|
||||
if (!(blob instanceof Blob) || !(['image/jpeg', 'image/png'].includes(blob.type) || photo.slot === 'LOADING_SCENE' && blob.type === 'application/pdf') || blob.type !== photo.contentType || !blob.size) throw new Error('资料读取失败,请重新核对')
|
||||
return blob
|
||||
}
|
||||
|
||||
@@ -24,6 +24,26 @@ beforeEach(() => {
|
||||
vi.mocked(api.bindLoadingPhoto).mockResolvedValue(data({ recordVersion: '1', photos: [photo] }))
|
||||
vi.mocked(api.readLoadingPhoto).mockRejectedValue(new Error('preview unavailable'))
|
||||
})
|
||||
it('refreshes photos added or replaced by PDA without writing and notifies the parent to reload the stage', async () => {
|
||||
can.mockReturnValue(false)
|
||||
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ photos: [photo] }))
|
||||
const wrapper = setup(); await flushPromises()
|
||||
const photos = api.loadingPhotoSlots.flatMap((slot, category) => Array.from({ length: 5 }, (_, index) => ({
|
||||
...photo, slot, fileId: `${category + 2}${index}`, fileName: `pda-${category}-${index}.png`,
|
||||
})))
|
||||
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ recordVersion: '26', stage: 'DELIVERING', complete: true, photos }))
|
||||
await wrapper.get('[aria-label="刷新装车资料"]').trigger('click'); await flushPromises()
|
||||
for (const item of photos) expect(wrapper.text()).toContain(item.fileName)
|
||||
expect(wrapper.text()).not.toContain(photo.fileName)
|
||||
expect(wrapper.findAll('tbody tr').map(row => row.findAll('td')[1]!.text())).toEqual(Object.values(api.loadingPhotoLabels).map(label => `${label}5/5 张`))
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
expect(api.uploadLoadingPhoto).not.toHaveBeenCalled(); expect(api.bindLoadingPhoto).not.toHaveBeenCalled()
|
||||
vi.mocked(api.getLoadingDocuments).mockRejectedValueOnce(new Error('刷新失败'))
|
||||
await wrapper.get('[aria-label="刷新装车资料"]').trigger('click'); await flushPromises()
|
||||
expect(wrapper.text()).toContain(photos[0]!.fileName)
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('offers five whole-dispatch slots and only reports saved after binding succeeds', async () => {
|
||||
const wrapper = setup(); await flushPromises()
|
||||
expect(wrapper.findAll('input[type="file"]')).toHaveLength(5)
|
||||
@@ -38,6 +58,25 @@ it('offers five whole-dispatch slots and only reports saved after binding succee
|
||||
expect(table.get('tbody tr').text()).toContain('loading.png')
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('shows an imported PDF license as an authenticated document and accepts an exact PDF replacement', async () => {
|
||||
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:license')
|
||||
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})
|
||||
const license = { ...photo, fileName: 'license.pdf', contentType: 'application/pdf' }
|
||||
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ photos: [license] }))
|
||||
vi.mocked(api.readLoadingPhoto).mockResolvedValue(new Blob(['%PDF-1.4'], { type: 'application/pdf' }))
|
||||
const wrapper = setup()
|
||||
try {
|
||||
await flushPromises()
|
||||
expect(wrapper.get('a').attributes('href')).toBe('blob:license')
|
||||
expect(wrapper.findComponent({ name: 'ElImage' }).exists()).toBe(false)
|
||||
const input = wrapper.get('input[aria-label="更换license.pdf"]')
|
||||
expect(input.attributes('accept')).toContain('application/pdf')
|
||||
expect(wrapper.get('input[aria-label="上传车牌照片"]').attributes('accept')).not.toContain('application/pdf')
|
||||
Object.defineProperty(input.element, 'files', { configurable: true, value: [new File(['%PDF-1.4'], 'new.pdf', { type: 'application/pdf' })] })
|
||||
await input.trigger('change'); await flushPromises()
|
||||
expect(api.bindLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ replaceFileId: '101' }))
|
||||
} finally { wrapper.unmount(); vi.restoreAllMocks() }
|
||||
})
|
||||
it('shows every photo in a category, stops at five and replaces the selected image only', async () => {
|
||||
const photos = Array.from({ length: 5 }, (_, index) => ({ ...photo, fileId: String(101 + index), fileName: `photo-${index}.png` }))
|
||||
vi.mocked(api.getLoadingDocuments).mockResolvedValue(data({ recordVersion: '5', photos }))
|
||||
|
||||
@@ -35,17 +35,18 @@ async function previews() {
|
||||
async function load() {
|
||||
if (busy.value) return
|
||||
busy.value = true; error.value = null
|
||||
try { accept(await getLoadingDocuments(number)); await previews() }
|
||||
try { accept(await getLoadingDocuments(number)); await previews(); return true }
|
||||
catch (cause) { if (active) { needsRefresh.value = true; error.value = getApiErrorDetails(cause, '装车资料读取失败') } }
|
||||
finally { if (active) busy.value = false }
|
||||
}
|
||||
async function refresh() { if (await load()) emit('saved') }
|
||||
function photosFor(slot: LoadingPhotoSlot) { return snapshot.value?.photos.filter(photo => photo.slot === slot) || [] }
|
||||
async function upload(slot: LoadingPhotoSlot, event: Event, replaceFileId?: string) {
|
||||
const input = event.target as HTMLInputElement, files = Array.from(input.files || []); input.value = ''
|
||||
if (!files.length || busy.value || !editable.value) return
|
||||
const capacity = replaceFileId ? 1 : 5 - photosFor(slot).length
|
||||
if (files.length > capacity) { ElMessage.warning(`本次最多可选择${capacity}张图片`); return }
|
||||
if (files.some(file => !['image/jpeg','image/png'].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024)) { ElMessage.warning('请选择不超过10MiB的JPG或PNG图片'); return }
|
||||
if (files.some(file => !(['image/jpeg','image/png'].includes(file.type) || slot === 'LOADING_SCENE' && file.type === 'application/pdf') || !file.size || file.size > 10 * 1024 * 1024)) { ElMessage.warning('请选择不超过10MiB的JPG或PNG,司机驾驶证也支持PDF'); return }
|
||||
busy.value = true; error.value = null
|
||||
let saved = 0, reconciled = false, bindingFailed = false
|
||||
try {
|
||||
@@ -86,17 +87,20 @@ defineExpose({ busy })
|
||||
<template>
|
||||
<div v-loading="busy" class="density-dialog-sections">
|
||||
<p>可边装车边上传五类装车资料,每类至少 1 张、最多 5 张;装车确认完成且五类齐全后自动进入派送中。</p>
|
||||
<p>司机驾驶证、车牌照片在理货审核通过后从本趟资料自动带入,可补充或更换;驾驶证支持 PDF。</p>
|
||||
<div><el-button :loading="busy" :disabled="busy" aria-label="刷新装车资料" @click="refresh">刷新</el-button></div>
|
||||
<p v-if="needsRefresh">保存状态尚未核实,请先重新读取资料后再上传。</p>
|
||||
<LoadErrorState v-if="error" compact title="装车资料操作失败" :message="error.message" :show-retry="true" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<table class="density-table loading-documents-table" aria-label="装车照片"><thead><tr><th class="photo-actions">操作</th><th class="photo-category">照片类别</th><th>文件名称</th><th class="photo-preview">图片预览</th><th class="photo-time">保存时间</th></tr></thead><tbody>
|
||||
<tr v-for="slot in loadingPhotoSlots" :key="slot">
|
||||
<td><label v-if="editable" class="photo-upload" :class="{ 'is-disabled': busy || photosFor(slot).length >= 5 }">{{ photosFor(slot).length >= 5 ? '已满 5 张' : '上传图片' }}<input type="file" multiple accept="image/jpeg,image/png" :aria-label="`上传${loadingPhotoLabels[slot]}`" :disabled="busy || photosFor(slot).length >= 5" @change="upload(slot, $event)" /></label><span v-else>—</span></td>
|
||||
<td><label v-if="editable" class="photo-upload" :class="{ 'is-disabled': busy || photosFor(slot).length >= 5 }">{{ photosFor(slot).length >= 5 ? '已满 5 张' : '上传资料' }}<input type="file" multiple :accept="slot === 'LOADING_SCENE' ? 'image/jpeg,image/png,application/pdf' : 'image/jpeg,image/png'" :aria-label="`上传${loadingPhotoLabels[slot]}`" :disabled="busy || photosFor(slot).length >= 5" @change="upload(slot, $event)" /></label><span v-else>—</span></td>
|
||||
<td>{{ loadingPhotoLabels[slot] }}<div>{{ photosFor(slot).length }}/5 张</div></td>
|
||||
<td><div v-for="photo in snapshot?.photos.filter(item => item.slot === slot) || []" :key="photo.fileId">{{ photo.fileName }}</div><span v-if="!snapshot?.photos.some(item => item.slot === slot)">未上传</span></td>
|
||||
<td><template v-for="photo in snapshot?.photos.filter(item => item.slot === slot) || []" :key="photo.fileId">
|
||||
<el-image v-if="images[photo.fileId]" :src="images[photo.fileId]" :preview-src-list="[images[photo.fileId]!]" fit="contain" class="loading-photo-image" preview-teleported />
|
||||
<a v-if="images[photo.fileId] && photo.contentType === 'application/pdf'" :href="images[photo.fileId]" target="_blank" rel="noopener noreferrer">查看驾驶证 PDF</a>
|
||||
<el-image v-else-if="images[photo.fileId]" :src="images[photo.fileId]" :preview-src-list="[images[photo.fileId]!]" fit="contain" class="loading-photo-image" preview-teleported />
|
||||
<el-button v-else link :disabled="busy" @click="load">图片读取失败,点击重试</el-button>
|
||||
<label v-if="editable" class="photo-upload">更换<input type="file" accept="image/jpeg,image/png" :aria-label="`更换${photo.fileName}`" :disabled="busy" @change="upload(slot, $event, photo.fileId)" /></label>
|
||||
<label v-if="editable" class="photo-upload">更换<input type="file" :accept="slot === 'LOADING_SCENE' ? 'image/jpeg,image/png,application/pdf' : 'image/jpeg,image/png'" :aria-label="`更换${photo.fileName}`" :disabled="busy" @change="upload(slot, $event, photo.fileId)" /></label>
|
||||
</template><span v-if="!snapshot?.photos.some(item => item.slot === slot)">—</span></td>
|
||||
<td><div v-for="photo in snapshot?.photos.filter(item => item.slot === slot) || []" :key="photo.fileId">{{ photo.savedAt?.replace('T', ' ') || '—' }}</div><span v-if="!snapshot?.photos.some(item => item.slot === slot)">—</span></td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user