feat(admin): synchronize fleet photos and multi-image loading documents

This commit is contained in:
2026-10-26 17:12:38 +08:00
parent e399f3f76f
commit acb90007de
29 changed files with 556 additions and 95 deletions
@@ -12,9 +12,9 @@ vi.mock('../api/dispatchLoadingDocuments', async importOriginal => ({ ...await i
const photo: api.LoadingPhoto = { slot: 'LOADING_SCENE', fileId: '101', fileName: 'loading.png', contentType: 'image/png', savedAt: null }
const data = (extra: Partial<api.LoadingDocuments> = {}): api.LoadingDocuments => ({ dispatchNo: 'PC/1', recordVersion: '0', dispatchRecordVersion: '3', stage: 'AFTER_LOADING', editable: true, complete: false, photos: [], ...extra })
function setup() { return mount(DispatchLoadingDocuments, { props: { dispatchNo: 'PC/1' }, global: { plugins: [ElementPlus] } }) }
async function upload(wrapper: ReturnType<typeof setup>) {
const input = wrapper.get('input[aria-label="装车照片 1"]')
Object.defineProperty(input.element, 'files', { configurable: true, value: [new File(['image'], 'loading.png', { type: 'image/png' })] })
async function upload(wrapper: ReturnType<typeof setup>, files = [new File(['image'], 'loading.png', { type: 'image/png' })]) {
const input = wrapper.get('input[aria-label="上传司机驾驶证"]')
Object.defineProperty(input.element, 'files', { configurable: true, value: files })
await input.trigger('change'); await flushPromises()
}
beforeEach(() => {
@@ -29,7 +29,7 @@ it('offers five whole-dispatch slots and only reports saved after binding succee
expect(wrapper.findAll('input[type="file"]')).toHaveLength(5)
const table = wrapper.get('table[aria-label="装车照片"]')
expect(table.findAll('thead th').map(cell => cell.text())).toEqual(['操作', '照片类别', '文件名称', '图片预览', '保存时间'])
expect(table.findAll('tbody tr').map(row => row.findAll('td')[1]!.text())).toEqual(['装车照片 1', '装车照片 2', '装车照片 3', '装车照片 4', '装车照片 5'])
expect(table.findAll('tbody tr').map(row => row.findAll('td')[1]!.text())).toEqual(Object.values(api.loadingPhotoLabels).map(label => `${label}0/5 张`))
await upload(wrapper)
expect(api.uploadLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.any(File), expect.any(String))
expect(api.bindLoadingPhoto).toHaveBeenCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ recordVersion: '0', fileId: '101' }))
@@ -38,6 +38,64 @@ 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 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 }))
const replacement = { ...photo, fileId: '999' }
vi.mocked(api.uploadLoadingPhoto).mockResolvedValue(replacement)
vi.mocked(api.bindLoadingPhoto).mockResolvedValue(data({ recordVersion: '6', photos: photos.map(item => item.fileId === '103' ? replacement : item) }))
const wrapper = setup(); await flushPromises()
expect(wrapper.get('input[aria-label="上传司机驾驶证"]').attributes('disabled')).toBeDefined()
for (const item of photos) expect(wrapper.text()).toContain(item.fileName)
const input = wrapper.get('input[aria-label="更换photo-2.png"]')
Object.defineProperty(input.element, 'files', { configurable: true, value: [new File(['new'], 'new.png', { type: 'image/png' })] })
await input.trigger('change'); await flushPromises()
expect(api.bindLoadingPhoto).toHaveBeenLastCalledWith('PC/1', 'LOADING_SCENE', expect.objectContaining({ recordVersion: '5', fileId: '999', replaceFileId: '103' }))
expect(wrapper.text()).toContain('photo-0.png'); expect(wrapper.text()).toContain('photo-4.png')
wrapper.unmount()
})
it('saves a multiple selection with fresh versions and notifies the parent once', async () => {
let latest = data(), id = 200
vi.mocked(api.getLoadingDocuments).mockImplementation(async () => latest)
vi.mocked(api.uploadLoadingPhoto).mockImplementation(async () => ({ ...photo, fileId: String(++id) }))
vi.mocked(api.bindLoadingPhoto).mockImplementation(async (_no, _slot, body) => {
latest = data({ recordVersion: String(Number(body.recordVersion) + 1), photos: [...latest.photos, { ...photo, fileId: body.fileId }] }); return latest
})
const wrapper = setup(); await flushPromises()
expect(wrapper.get('input[aria-label="上传司机驾驶证"]').attributes('multiple')).toBeDefined()
await upload(wrapper, Array.from({ length: 3 }, () => new File(['image'], 'photo.png', { type: 'image/png' })))
expect(api.bindLoadingPhoto).toHaveBeenCalledTimes(3)
expect(vi.mocked(api.bindLoadingPhoto).mock.calls.map(call => call[2].recordVersion)).toEqual(['0', '1', '2'])
expect(wrapper.emitted('saved')).toHaveLength(1); expect(wrapper.text()).toContain('3/5 张')
await upload(wrapper, Array.from({ length: 3 }, () => new File(['image'], 'photo.png', { type: 'image/png' })))
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(3)
wrapper.unmount()
})
it('stops a batch after an unknown binding result and displays the server snapshot', async () => {
let latest = data(), id = 200, binds = 0
vi.mocked(api.getLoadingDocuments).mockImplementation(async () => latest)
vi.mocked(api.uploadLoadingPhoto).mockImplementation(async () => ({ ...photo, fileId: String(++id) }))
vi.mocked(api.bindLoadingPhoto).mockImplementation(async (_no, _slot, body) => {
latest = data({ recordVersion: String(++binds), photos: [...latest.photos, { ...photo, fileId: body.fileId }] })
if (binds === 2) throw new Error('保存响应超时')
return latest
})
const wrapper = setup(); await flushPromises()
await upload(wrapper, Array.from({ length: 3 }, () => new File(['image'], 'photo.png', { type: 'image/png' })))
expect(api.uploadLoadingPhoto).toHaveBeenCalledTimes(2); expect(api.bindLoadingPhoto).toHaveBeenCalledTimes(2)
expect(wrapper.text()).toContain('2/5 张'); expect(wrapper.text()).toContain('已读取最新资料')
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
})
it('requires a successful refresh after both binding and reconciliation fail', async () => {
vi.mocked(api.getLoadingDocuments).mockResolvedValueOnce(data()).mockResolvedValueOnce(data()).mockRejectedValueOnce(new Error('读取失败')).mockResolvedValue(data())
vi.mocked(api.bindLoadingPhoto).mockRejectedValueOnce(new Error('保存超时'))
const wrapper = setup(); await flushPromises(); await upload(wrapper)
expect(wrapper.text()).toContain('保存状态尚未核实')
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
wrapper.findComponent({ name: 'LoadErrorState' }).vm.$emit('retry'); await flushPromises()
expect(wrapper.find('input[aria-label="上传司机驾驶证"]').exists()).toBe(true)
expect(api.bindLoadingPhoto).toHaveBeenCalledTimes(1); wrapper.unmount()
})
it('unlocks after binding failure and accepts a new upload with a fresh key', async () => {
vi.mocked(api.bindLoadingPhoto).mockRejectedValueOnce(new Error('network unknown'))
const wrapper = setup(); await flushPromises(); await upload(wrapper)