This commit is contained in:
2026-10-26 18:52:21 +08:00
parent 22242701e4
commit a9dc19a86b
12 changed files with 88 additions and 25 deletions
@@ -12,6 +12,15 @@ beforeEach(() => vi.clearAllMocks())
const base = '/platform/v1/last-leg/fleet/outsourced-business'
const photo: OutsourcedBusinessPhoto = { slot: 'LOADING_SCENE', category: 'LOADING', fileId: '9007199254740993', fileName: 'loading.png', contentType: 'image/png', sizeBytes: '10', uploadedAt: '2026-10-20T10:00:00Z', savedAt: null }
describe('outsourced business HTTP contract', () => {
it('reads an imported PDF only for the driver license category and rejects a mismatched response', async () => {
const license: OutsourcedBusinessPhoto = { ...photo, slot: 'LOADING_SCENE', category: 'LOADING', contentType: 'application/pdf' }
const blob = new Blob(['%PDF-1.4'], { type: 'application/pdf' })
vi.mocked(http.get).mockResolvedValue(blob)
await expect(getOutsourcedBusinessPhotoContent('DS/1', license)).resolves.toBe(blob)
await expect(getOutsourcedBusinessPhotoContent('DS/1', { ...license, slot: 'LOADING_LIST' })).rejects.toThrow('格式无效')
vi.mocked(http.get).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
await expect(getOutsourcedBusinessPhotoContent('DS/1', license)).rejects.toThrow('格式无效')
})
it.each(['OWNED', 'DISPATCHED'] as const)('accepts server-classified pooled %s vehicles in lists, details and write responses', async ownership => {
const detail = outsourcedBusinessFixture(); detail.row.vehicle.ownership = ownership
vi.mocked(http.get).mockResolvedValueOnce({ data: { records: [detail.row], page: 1, size: 20, total: 1, summary: { dispatchCount: 1, afterLoadingCount: 1, deliveringCount: 0, deliveredCount: 0 } } }).mockResolvedValue({ data: detail })
@@ -44,7 +44,7 @@ export async function saveOutsourcedBusinessOperations(no: string, body: Outsour
}
export async function uploadOutsourcedBusinessPhoto(no: string, slot: OutsourcedBusinessPhotoSlot, file: File, key: string) {
requireSlot(slot)
if (!['image/jpeg','image/png'].includes(file.type) || file.size <= 0) throw new Error('请选择有效的JPG或PNG图片')
if (!(['image/jpeg','image/png'].includes(file.type) || slot === 'LOADING_SCENE' && file.type === 'application/pdf') || file.size <= 0) throw new Error('请选择JPG/PNG,司机驾驶证也支持PDF')
const form = new FormData(); form.append('file', file); form.append('idempotencyKey', key)
const value = unwrapApiResponse(await http.post<ApiEnvelope<OutsourcedBusinessPhoto>, ApiEnvelope<OutsourcedBusinessPhoto>>(`${path(no)}/photos/${slot}/uploads`, form))
if (value.slot !== slot || !value.fileId) throw new Error('上传文件与原照片分类不一致')
@@ -68,7 +68,7 @@ export async function getOutsourcedBusinessPhotoContent(no: string, photo: Outso
try {
const blob = await http.get<Blob, Blob>(`${path(no)}/photos/${photo.slot}/files/${encodeURIComponent(photo.fileId)}/content`, { responseType: 'blob', params: revisionVersion == null ? undefined : { revisionVersion: outsourcedBusinessVersion(revisionVersion) } })
await downloadError(blob, 200)
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
} catch (cause) { if (axios.isAxiosError(cause)) await downloadError(cause.response?.data, cause.response?.status || 0); throw cause }
}
@@ -12,6 +12,15 @@ beforeEach(() => vi.clearAllMocks())
const base = '/platform/v1/last-leg/fleet/owned-business'
const photo: OwnedBusinessPhoto = { slot: 'FUEL_START', category: 'FUEL', fileId: '9007199254740993', fileName: 'fuel.png', contentType: 'image/png', sizeBytes: '10', uploadedAt: '2026-10-20T10:00:00Z', savedAt: null }
describe('owned business HTTP contract', () => {
it('reads an imported PDF only for the driver license category and rejects a mismatched response', async () => {
const license: OwnedBusinessPhoto = { ...photo, slot: 'LOADING_SCENE', category: 'LOADING', contentType: 'application/pdf' }
const blob = new Blob(['%PDF-1.4'], { type: 'application/pdf' })
vi.mocked(http.get).mockResolvedValue(blob)
await expect(getOwnedBusinessPhotoContent('DS/1', license)).resolves.toBe(blob)
await expect(getOwnedBusinessPhotoContent('DS/1', { ...license, slot: 'LOADING_LIST' })).rejects.toThrow('格式无效')
vi.mocked(http.get).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
await expect(getOwnedBusinessPhotoContent('DS/1', license)).rejects.toThrow('格式无效')
})
it.each(['OWNED', 'DISPATCHED'] as const)('accepts server-classified non-pooled %s vehicles in lists, details and write responses', async ownership => {
const detail = ownedBusinessFixture(); detail.row.vehicle.ownership = ownership; detail.row.vehicle.vehicleId = null
vi.mocked(http.get).mockResolvedValueOnce({ data: { records: [detail.row], page: 1, size: 20, total: 1, summary: { dispatchCount: 1, afterLoadingCount: 1, deliveringCount: 0, deliveredCount: 0 } } }).mockResolvedValue({ data: detail })
@@ -38,7 +38,7 @@ export async function saveOwnedBusinessOperations(no: string, body: OwnedBusines
return value
}
export async function uploadOwnedBusinessPhoto(no: string, slot: OwnedBusinessPhotoSlot, file: File, key: string) {
if (!['image/jpeg','image/png'].includes(file.type) || file.size <= 0) throw new Error('请选择有效的JPG或PNG图片')
if (!(['image/jpeg','image/png'].includes(file.type) || slot === 'LOADING_SCENE' && file.type === 'application/pdf') || file.size <= 0) throw new Error('请选择JPG/PNG,司机驾驶证也支持PDF')
const form = new FormData(); form.append('file', file); form.append('idempotencyKey', key)
const value = unwrapApiResponse(await http.post<ApiEnvelope<OwnedBusinessPhoto>, ApiEnvelope<OwnedBusinessPhoto>>(`${path(no)}/photos/${slot}/uploads`, form))
if (value.slot !== slot || !value.fileId) throw new Error('上传文件与原照片分类不一致')
@@ -60,7 +60,7 @@ export async function getOwnedBusinessPhotoContent(no: string, photo: OwnedBusin
try {
const blob = await http.get<Blob, Blob>(`${path(no)}/photos/${photo.slot}/files/${encodeURIComponent(photo.fileId)}/content`, { responseType: 'blob', params: revisionVersion == null ? undefined : { revisionVersion: ownedBusinessVersion(revisionVersion) } })
await downloadError(blob, 200)
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
} catch (cause) { if (axios.isAxiosError(cause)) await downloadError(cause.response?.data, cause.response?.status || 0); throw cause }
}
@@ -57,7 +57,7 @@ describe('outsourced business per-slot photo save',()=>{
const detail=outsourcedBusinessFixture();detail.photos=[{...photo,savedAt:'2026-10-20T10:01:00Z'}]
const wrapper=mount(OutsourcedBusinessPhotosEditor,{global,props:{modelValue:true,detail}});await flushPromises()
expect(calls.content).not.toHaveBeenCalled()
await wrapper.findAll('button').find(button=>button.text()==='查看图片')!.trigger('click');await flushPromises()
await wrapper.findAll('button').find(button=>button.text()==='查看资料')!.trigger('click');await flushPromises()
expect(calls.content).toHaveBeenCalledExactlyOnceWith('DS-OUT-1',detail.photos[0]);expect(wrapper.find('img').attributes('src')).toBe('blob:outsourced-loading')
await wrapper.setProps({modelValue:false});expect(revoke).toHaveBeenCalledWith('blob:outsourced-loading');wrapper.unmount()
})
@@ -42,7 +42,7 @@ async function upload(slot: OutsourcedBusinessPhotoSlot, event: Event, replaceFi
const input = event.target as HTMLInputElement, file = input.files?.[0]; input.value = ''
if (!file || busy.value || !uploadable.value) return
if (!replaceFileId && savedPhotos(slot).length >= (slot.startsWith('FUEL_') ? 1 : 5)) return
if (!['image/jpeg','image/png'].includes(file.type) || !file.size) { error.value = { message: '请选择有效的JPG或PNG图片' }; return }
if (!(['image/jpeg','image/png'].includes(file.type) || slot === 'LOADING_SCENE' && file.type === 'application/pdf') || !file.size) { error.value = { message: '请选择JPG/PNG,司机驾驶证也支持PDF' }; return }
busy.value = true; error.value = null
try {
ensureScope()
@@ -62,12 +62,13 @@ function beforeClose(done: () => void) { if (!busy.value) done() }
<template #state><LoadErrorState v-if="error" title="照片作业未完成" :message="error.message" :show-retry="false"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState><p v-if="!uploadable">{{ detail?.row.actions.upload.blockReason || '当前仅可查看照片' }}</p></template>
<template v-for="role in outsourcedBusinessPhotoRoles" :key="role.slot" #[role.viewSlot]>
<div v-for="photo in savedPhotos(role.slot)" :key="photo.fileId">
<img v-if="urls[photo.fileId]" :src="urls[photo.fileId]" :alt="role.label" class="outsourced-business-photo" />
<p>{{ photo.fileName }} · 已保存</p><el-button :loading="reading === photo.fileId" :disabled="Boolean(reading)" @click="preview(role.slot, photo.fileId)">查看图片</el-button>
<label v-if="uploadable" class="outsourced-business-upload">替换图片<input type="file" accept="image/jpeg,image/png" :disabled="busy" @change="upload(role.slot, $event, photo.fileId)" /></label>
<a v-if="urls[photo.fileId] && photo.contentType === 'application/pdf'" :href="urls[photo.fileId]" target="_blank" rel="noopener noreferrer">查看驾驶证 PDF</a>
<img v-else-if="urls[photo.fileId]" :src="urls[photo.fileId]" :alt="role.label" class="outsourced-business-photo" />
<p>{{ photo.fileName }} · 已保存</p><el-button :loading="reading === photo.fileId" :disabled="Boolean(reading)" @click="preview(role.slot, photo.fileId)">查看资料</el-button>
<label v-if="uploadable" class="outsourced-business-upload">替换资料<input type="file" :accept="role.slot === 'LOADING_SCENE' ? 'image/jpeg,image/png,application/pdf' : 'image/jpeg,image/png'" :disabled="busy" @change="upload(role.slot, $event, photo.fileId)" /></label>
</div>
<p v-if="!savedPhotos(role.slot).length">暂无已保存照片</p>
<label v-if="uploadable && savedPhotos(role.slot).length < (role.slot.startsWith('FUEL_') ? 1 : 5)" class="outsourced-business-upload">上传图片<input type="file" accept="image/jpeg,image/png" :disabled="busy" @change="upload(role.slot, $event)" /></label>
<label v-if="uploadable && savedPhotos(role.slot).length < (role.slot.startsWith('FUEL_') ? 1 : 5)" class="outsourced-business-upload">上传资料<input type="file" :accept="role.slot === 'LOADING_SCENE' ? 'image/jpeg,image/png,application/pdf' : 'image/jpeg,image/png'" :disabled="busy" @change="upload(role.slot, $event)" /></label>
</template>
</OutsourcedBusinessPhotosDialog>
</template>
@@ -42,7 +42,7 @@ async function upload(slot: OwnedBusinessPhotoSlot, event: Event, replaceFileId?
const input = event.target as HTMLInputElement, file = input.files?.[0]; input.value = ''
if (!file || busy.value || !uploadable.value) return
if (!replaceFileId && savedPhotos(slot).length >= (slot.startsWith('FUEL_') ? 1 : 5)) return
if (!['image/jpeg','image/png'].includes(file.type) || !file.size) { error.value = { message: '请选择有效的JPG或PNG图片' }; return }
if (!(['image/jpeg','image/png'].includes(file.type) || slot === 'LOADING_SCENE' && file.type === 'application/pdf') || !file.size) { error.value = { message: '请选择JPG/PNG,司机驾驶证也支持PDF' }; return }
busy.value = true; error.value = null
try {
ensureScope()
@@ -62,12 +62,13 @@ function beforeClose(done: () => void) { if (!busy.value) done() }
<template #state><LoadErrorState v-if="error" title="照片作业未完成" :message="error.message" :show-retry="false"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState><p v-if="!uploadable">{{ detail?.row.actions.upload.blockReason || '当前仅可查看照片' }}</p></template>
<template v-for="role in ownedBusinessPhotoRoles" :key="role.slot" #[role.viewSlot]>
<div v-for="photo in savedPhotos(role.slot)" :key="photo.fileId">
<img v-if="urls[photo.fileId]" :src="urls[photo.fileId]" :alt="role.label" class="owned-business-photo" />
<p>{{ photo.fileName }} · 已保存</p><el-button :loading="reading === photo.fileId" :disabled="Boolean(reading)" @click="preview(role.slot, photo.fileId)">查看图片</el-button>
<label v-if="uploadable" class="owned-business-upload">替换图片<input type="file" accept="image/jpeg,image/png" :disabled="busy" @change="upload(role.slot, $event, photo.fileId)" /></label>
<a v-if="urls[photo.fileId] && photo.contentType === 'application/pdf'" :href="urls[photo.fileId]" target="_blank" rel="noopener noreferrer">查看驾驶证 PDF</a>
<img v-else-if="urls[photo.fileId]" :src="urls[photo.fileId]" :alt="role.label" class="owned-business-photo" />
<p>{{ photo.fileName }} · 已保存</p><el-button :loading="reading === photo.fileId" :disabled="Boolean(reading)" @click="preview(role.slot, photo.fileId)">查看资料</el-button>
<label v-if="uploadable" class="owned-business-upload">替换资料<input type="file" :accept="role.slot === 'LOADING_SCENE' ? 'image/jpeg,image/png,application/pdf' : 'image/jpeg,image/png'" :disabled="busy" @change="upload(role.slot, $event, photo.fileId)" /></label>
</div>
<p v-if="!savedPhotos(role.slot).length">暂无已保存照片</p>
<label v-if="uploadable && savedPhotos(role.slot).length < (role.slot.startsWith('FUEL_') ? 1 : 5)" class="owned-business-upload">上传图片<input type="file" accept="image/jpeg,image/png" :disabled="busy" @change="upload(role.slot, $event)" /></label>
<label v-if="uploadable && savedPhotos(role.slot).length < (role.slot.startsWith('FUEL_') ? 1 : 5)" class="owned-business-upload">上传资料<input type="file" :accept="role.slot === 'LOADING_SCENE' ? 'image/jpeg,image/png,application/pdf' : 'image/jpeg,image/png'" :disabled="busy" @change="upload(role.slot, $event)" /></label>
</template>
</OwnedBusinessPhotosDialog>
</template>
@@ -5,7 +5,7 @@ export type OutsourcedBusinessPhotoSlot = 'LOADING_SCENE' | 'LOADING_LIST' | 'PR
export interface OutsourcedBusinessMoney { currency: 'CNY' | 'USD'; amount: string }
export interface OutsourcedBusinessArrival { destinationKey: string; actualArrivedAt: string | null }
export interface OutsourcedBusinessOperations { money: OutsourcedBusinessMoney | null; arrivals: OutsourcedBusinessArrival[] }
export interface OutsourcedBusinessPhoto { slot: OutsourcedBusinessPhotoSlot; category: 'LOADING'; fileId: string; fileName: string; contentType: 'image/jpeg' | 'image/png'; sizeBytes: string; uploadedAt: string; savedAt: string | null }
export interface OutsourcedBusinessPhoto { slot: OutsourcedBusinessPhotoSlot; category: 'LOADING'; fileId: string; fileName: string; contentType: 'image/jpeg' | 'image/png' | 'application/pdf'; sizeBytes: string; uploadedAt: string; savedAt: string | null }
export interface OutsourcedBusinessDestination { destinationKey: string; warehouseCode: string | null; warehouseName: string | null; address: string | null; appointments: { appointmentNo: string; confirmationNo: string | null; scheduledAt: string | null; allocatedPackages: string }[]; actualArrivedAt: string | null }
export interface OutsourcedBusinessRow {
dispatchNo: string; stage: OutsourcedBusinessStage; plannedDepartureAt: string | null; loadingCompletedAt: string; departedAt: string | null; deliveryCompletedAt: string | null
@@ -26,7 +26,7 @@ export type OutsourcedBusinessAction = 'SAVE_OPERATIONS' | 'UPLOAD_PHOTO' | 'BIN
export interface OutsourcedBusinessCommand { dispatchNo: string; action: OutsourcedBusinessAction; idempotencyKey: string; state: 'SUCCEEDED' | 'PENDING' | 'UNKNOWN_RESULT' | 'REJECTED'; recordVersion: string | null; replayed: boolean; result: OutsourcedBusinessDetail | OutsourcedBusinessPhoto | null; errorCode: string | null; retryAllowed: boolean }
export interface OutsourcedBusinessHistory { eventSource: 'DISPATCH' | 'OPERATIONS'; recordVersion: string | null; dispatchRecordVersion: string | null; action: string; operatorName: string | null; occurredAt: string; reason: string | null; operations: OutsourcedBusinessOperations | null; photos: OutsourcedBusinessPhoto[] }
export const outsourcedBusinessStages: Record<OutsourcedBusinessStage, string> = { AFTER_LOADING: '装车完成', DELIVERING: '派送中', DELIVERED: '派送完成' }
export const outsourcedBusinessActionLabels: Record<string,string> = { CREATE: '新建排车', EDIT: '维护车辆信息', ADD_CARGO: '添加货物', REMOVE_CARGO: '移除货物', SUBMIT_REVIEW: '提交调度审核', DISPATCH_REVIEW: '调度审核', LOADING_REVIEW: '理货员审核', RETURN_TO_REVIEW: '退回重审', PICK: '真实拣货', LOADING_COMPLETE: '确认装车完成', ARRANGE_DELIVERY: '安排派送', DEPART: '实际发车', CANCEL: '取消排车', SAVE_OPERATIONS: '登记运营信息', BIND_PHOTO: '保存分类照片' }
export const outsourcedBusinessActionLabels: Record<string,string> = { CREATE: '新建排车', EDIT: '维护车辆信息', ADD_CARGO: '添加货物', REMOVE_CARGO: '移除货物', SUBMIT_REVIEW: '提交调度审核', DISPATCH_REVIEW: '调度审核', LOADING_REVIEW: '理货员审核', RETURN_TO_REVIEW: '退回重审', PICK: '真实拣货', LOADING_COMPLETE: '确认装车完成', ARRANGE_DELIVERY: '安排派送', DEPART: '实际发车', CANCEL: '取消排车', SAVE_OPERATIONS: '登记运营信息', BIND_PHOTO: '保存分类照片', INVALIDATE_RESOURCE_PHOTOS: '车辆或司机资料变更,原凭证失效' }
export const outsourcedBusinessPhotoRoles: { slot: OutsourcedBusinessPhotoSlot; viewSlot: string; label: string; category: 'LOADING' }[] = [
{ slot: 'LOADING_SCENE', viewSlot: 'loading-scene', label: '司机驾驶证', category: 'LOADING' }, { slot: 'LOADING_LIST', viewSlot: 'loading-list', label: '车牌照片', category: 'LOADING' },
{ slot: 'PRE_DEPARTURE_CHECK', viewSlot: 'pre-departure-check', label: '装车完成', category: 'LOADING' }, { slot: 'DRIVER_LICENSE', viewSlot: 'driver-license', label: '封车照片', category: 'LOADING' },
@@ -5,7 +5,7 @@ export type OwnedBusinessPhotoSlot = 'LOADING_SCENE' | 'LOADING_LIST' | 'PRE_DEP
export interface OwnedBusinessMoney { currency: 'CNY' | 'USD'; amount: string }
export interface OwnedBusinessArrival { destinationKey: string; actualArrivedAt: string | null }
export interface OwnedBusinessOperations { money: OwnedBusinessMoney | null; actualDistanceMiles: string | null; fuelConsumptionLitresPer100Km: string | null; arrivals: OwnedBusinessArrival[] }
export interface OwnedBusinessPhoto { slot: OwnedBusinessPhotoSlot; category: 'LOADING' | 'FUEL'; fileId: string; fileName: string; contentType: 'image/jpeg' | 'image/png'; sizeBytes: string; uploadedAt: string; savedAt: string | null }
export interface OwnedBusinessPhoto { slot: OwnedBusinessPhotoSlot; category: 'LOADING' | 'FUEL'; fileId: string; fileName: string; contentType: 'image/jpeg' | 'image/png' | 'application/pdf'; sizeBytes: string; uploadedAt: string; savedAt: string | null }
export interface OwnedBusinessDestination { destinationKey: string; warehouseCode: string | null; warehouseName: string | null; address: string | null; appointments: { appointmentNo: string; confirmationNo: string | null; scheduledAt: string | null; allocatedPackages: string }[]; actualArrivedAt: string | null }
export interface OwnedBusinessRow {
dispatchNo: string; stage: OwnedBusinessStage; plannedDepartureAt: string; loadingCompletedAt: string; departedAt: string | null; deliveryCompletedAt: string | null
@@ -26,7 +26,7 @@ export type OwnedBusinessAction = 'SAVE_OPERATIONS' | 'UPLOAD_PHOTO' | 'BIND_PHO
export interface OwnedBusinessCommand { dispatchNo: string; action: OwnedBusinessAction; idempotencyKey: string; state: 'SUCCEEDED' | 'PENDING' | 'UNKNOWN_RESULT' | 'REJECTED'; recordVersion: string | null; replayed: boolean; result: OwnedBusinessDetail | OwnedBusinessPhoto | null; errorCode: string | null; retryAllowed: boolean }
export interface OwnedBusinessHistory { eventSource: 'DISPATCH' | 'OPERATIONS'; recordVersion: string | null; dispatchRecordVersion: string | null; action: string; operatorName: string | null; occurredAt: string; reason: string | null; operations: OwnedBusinessOperations | null; photos: OwnedBusinessPhoto[] }
export const ownedBusinessStages: Record<OwnedBusinessStage, string> = { AFTER_LOADING: '装车完成', DELIVERING: '派送中', DELIVERED: '派送完成' }
export const ownedBusinessActionLabels: Record<string,string> = { CREATE: '新建排车', EDIT: '维护车辆信息', ADD_CARGO: '添加货物', REMOVE_CARGO: '移除货物', SUBMIT_REVIEW: '提交调度审核', DISPATCH_REVIEW: '调度审核', LOADING_REVIEW: '理货员审核', RETURN_TO_REVIEW: '退回重审', PICK: '真实拣货', LOADING_COMPLETE: '确认装车完成', ARRANGE_DELIVERY: '安排派送', DEPART: '实际发车', CANCEL: '取消排车', SAVE_OPERATIONS: '登记运营信息', BIND_PHOTO: '保存分类照片' }
export const ownedBusinessActionLabels: Record<string,string> = { CREATE: '新建排车', EDIT: '维护车辆信息', ADD_CARGO: '添加货物', REMOVE_CARGO: '移除货物', SUBMIT_REVIEW: '提交调度审核', DISPATCH_REVIEW: '调度审核', LOADING_REVIEW: '理货员审核', RETURN_TO_REVIEW: '退回重审', PICK: '真实拣货', LOADING_COMPLETE: '确认装车完成', ARRANGE_DELIVERY: '安排派送', DEPART: '实际发车', CANCEL: '取消排车', SAVE_OPERATIONS: '登记运营信息', BIND_PHOTO: '保存分类照片', INVALIDATE_RESOURCE_PHOTOS: '车辆或司机资料变更,原凭证失效' }
export const ownedBusinessPhotoRoles: { slot: OwnedBusinessPhotoSlot; viewSlot: string; label: string; category: 'LOADING' | 'FUEL' }[] = [
{ slot: 'LOADING_SCENE', viewSlot: 'loading-scene', label: '司机驾驶证', category: 'LOADING' }, { slot: 'LOADING_LIST', viewSlot: 'loading-list', label: '车牌照片', category: 'LOADING' },
{ slot: 'PRE_DEPARTURE_CHECK', viewSlot: 'pre-departure-check', label: '装车完成', category: 'LOADING' }, { slot: 'DRIVER_LICENSE', viewSlot: 'driver-license', label: '封车照片', category: 'LOADING' },