fix(driver): 限制驾驶证上传为 JPG 和 PNG 图片

This commit is contained in:
2026-11-06 21:02:20 +08:00
parent f69ea1476d
commit a9373e468a
4 changed files with 39 additions and 7 deletions
@@ -41,7 +41,7 @@ describe('driver APIs', () => {
it('uploads a real multipart file with the version and leaves multipart boundary to the browser', async () => {
vi.mocked(http.post).mockResolvedValue(ok(record))
const file = new File(['%PDF-1.7'], 'license.pdf', { type: 'application/pdf' })
const file = new File(['image'], 'license.png', { type: 'image/png' })
await uploadDriverLicense(id, 2, file)
const [url, body] = vi.mocked(http.post).mock.calls[0]!
expect(url).toBe(`${base}/${id}/license`)
@@ -50,6 +50,18 @@ describe('driver APIs', () => {
expect((body as FormData).get('file')).toBe(file)
})
it.each(['application/pdf', 'image/gif', 'text/plain'])('rejects %s driver license uploads before sending a request', async type => {
await expect(uploadDriverLicense(id, 2, new File(['content'], 'license', { type }))).rejects.toThrow('JPG 或 PNG')
expect(http.post).not.toHaveBeenCalled()
})
it('rejects empty and oversized driver license images before sending a request', async () => {
const empty = new File([], 'empty.png', { type: 'image/png' })
const oversized = new File([new Uint8Array(10 * 1024 * 1024 + 1)], 'large.jpg', { type: 'image/jpeg' })
for (const file of [empty, oversized]) await expect(uploadDriverLicense(id, 2, file)).rejects.toThrow('最大 10MiB')
expect(http.post).not.toHaveBeenCalled()
})
it('takes task qualification from server and keeps history snapshots distinct', async () => {
vi.mocked(http.get).mockResolvedValueOnce(ok(record.qualification)).mockResolvedValueOnce(ok({ page: '1', size: '20', total: '1', pages: '1', records: [{ recordVersion: '1', action: 'UPLOAD_LICENSE', snapshot: { ...record, recordVersion: '1', reviewStatus: 'PENDING' } }] }))
const expectedEnd = '2026-10-20T18:00:00Z'
@@ -67,6 +67,9 @@ export async function submitDriver(id: string, recordVersion: number): Promise<D
return driver(unwrapApiResponse(await http.post<ApiEnvelope<DriverWire>, ApiEnvelope<DriverWire>>(`${path(id)}/submit`, { recordVersion })))
}
export async function uploadDriverLicense(id: string, recordVersion: number, file: File): Promise<DriverDetail> {
if (!['image/jpeg', 'image/png'].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024) {
throw new Error('请选择非空 JPG 或 PNG,最大 10MiB')
}
const form = new FormData()
form.append('recordVersion', String(recordVersion))
form.append('file', file)
@@ -43,8 +43,8 @@ watch([open, () => props.driver?.id], ([value]) => {
function selectFile(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0] || null
if (file && (!['application/pdf', 'image/jpeg', 'image/png'].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024)) {
ElMessage.warning('请选择非空 PDF、JPG 或 PNG,最大 10MiB'); input.value = ''; selectedFile.value = null; return
if (file && (!['image/jpeg', 'image/png'].includes(file.type) || !file.size || file.size > 10 * 1024 * 1024)) {
ElMessage.warning('请选择非空 JPG 或 PNG,最大 10MiB'); input.value = ''; selectedFile.value = null; return
}
selectedFile.value = file
}
@@ -107,7 +107,7 @@ onBeforeUnmount(() => { warehouseRequest++; editSession++ })
<el-form-item label="保险到期日"><el-date-picker v-model="profile.insuranceExpiresOn" type="date" value-format="YYYY-MM-DD" /></el-form-item>
<h3 class="full-width driver-section-heading">证照附件<el-tag v-if="current?.licenseFileId" size="small" type="info">已有附件</el-tag></h3>
<div class="full-width">
<label v-if="access.can(permission.driverUpload)">驾驶证<input ref="fileInput" type="file" accept="application/pdf,image/jpeg,image/png" :disabled="saving" @change="selectFile" /></label>
<label v-if="access.can(permission.driverUpload)">驾驶证(JPG / PNG)<input ref="fileInput" type="file" accept="image/jpeg,image/png" :disabled="saving" @change="selectFile" /></label>
</div>
<el-form-item label="备注" class="full-width"><el-input v-model="form.remark" type="textarea" :rows="2" maxlength="1000" /></el-form-item>
</el-form>
@@ -307,6 +307,23 @@ describe('driver page workflows', () => {
expect(api.getDriverQualification).toHaveBeenCalledTimes(1)
})
it('accepts image selection and clears it when a PDF is selected instead', async () => {
vi.mocked(api.updateDriver).mockResolvedValue(structuredClone(driver))
const wrapper = mount(DriverEditorDialog, { modelValue: false, driver })
await wrapper.setProps({ modelValue: true }); await flushPromises()
const picker = wrapper.get('input[type="file"]')
expect(picker.attributes('accept')).toBe('image/jpeg,image/png')
Object.defineProperty(picker.element, 'files', { value: [new File(['image'], 'license.png', { type: 'image/png' })], configurable: true })
await picker.trigger('change')
expect(ElMessage.warning).not.toHaveBeenCalled()
Object.defineProperty(picker.element, 'files', { value: [new File(['%PDF'], 'license.pdf', { type: 'application/pdf' })], configurable: true })
await picker.trigger('change')
expect(ElMessage.warning).toHaveBeenCalledWith('请选择非空 JPG 或 PNG,最大 10MiB')
wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
expect(api.updateDriver).toHaveBeenCalledTimes(1)
expect(api.uploadDriverLicense).not.toHaveBeenCalled()
})
it('does not create a second draft when attachment upload fails after saving', async () => {
vi.mocked(api.createDriver).mockResolvedValue({ ...structuredClone(driver), recordVersion: 0 })
vi.mocked(api.uploadDriverLicense).mockRejectedValueOnce(new Error('文件服务不可用')).mockResolvedValueOnce({ ...structuredClone(driver), recordVersion: 1 })
@@ -317,7 +334,7 @@ describe('driver page workflows', () => {
inputs[1]!.vm.$emit('update:modelValue', '123')
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
selects.at(-1)!.vm.$emit('update:modelValue', 'UTC')
const file = new File(['%PDF-test'], 'license.pdf', { type: 'application/pdf' })
const file = new File(['image'], 'license.png', { type: 'image/png' })
const picker = wrapper.get('input[type="file"]')
Object.defineProperty(picker.element, 'files', { value: [file] })
await picker.trigger('change')
@@ -342,7 +359,7 @@ describe('driver page workflows', () => {
expect(api.updateDriver).toHaveBeenCalledWith(driver.id, 2, expect.anything())
await wrapper.setProps({ modelValue: false }); await flushPromises()
await wrapper.setProps({ driver: other, modelValue: true }); await flushPromises()
const file = new File(['%PDF-other'], 'other-driver.pdf', { type: 'application/pdf' })
const file = new File(['image'], 'other-driver.jpg', { type: 'image/jpeg' })
const picker = wrapper.get('input[type="file"]')
Object.defineProperty(picker.element, 'files', { value: [file], configurable: true })
await picker.trigger('change')
@@ -365,7 +382,7 @@ describe('driver page workflows', () => {
const wrapper = mount(DriverEditorDialog, { modelValue: false, driver })
await wrapper.setProps({ modelValue: true }); await flushPromises()
const picker = wrapper.get('input[type="file"]')
Object.defineProperty(picker.element, 'files', { value: [new File(['%PDF'], 'driver.pdf', { type: 'application/pdf' })] })
Object.defineProperty(picker.element, 'files', { value: [new File(['image'], 'driver.jpg', { type: 'image/jpeg' })] })
await picker.trigger('change')
wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
wrapper.unmount()