优化
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { createTruckAppointment, deleteTruckAppointmentAttachment, getTruckAppointment, listTruckAppointmentAttachments, listTruckAppointmentHistory, listTruckAppointments, performTruckAppointmentAction, referenceTruckAppointmentAttachment } from './platformTrucking'
|
||||
import { createTruckAppointment, deleteTruckAppointmentAttachment, getTruckAppointment, listTruckAppointmentAttachments, listTruckAppointmentHistory, listTruckAppointments, performTruckAppointmentAction, referenceTruckAppointmentAttachment, uploadTruckAppointmentAttachment } from './platformTrucking'
|
||||
|
||||
vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn() }, unwrapApiResponse: (response: { data: unknown }) => response.data }))
|
||||
|
||||
@@ -40,4 +40,14 @@ describe('truck appointment API', () => {
|
||||
expect(http.post).toHaveBeenCalledWith('/platform/v1/truck-appointments/77/attachments', { fileId: '9007199254740993', fileName: '派车单.pdf' })
|
||||
expect(http.delete).toHaveBeenCalledWith('/platform/v1/truck-appointments/77/attachments/1')
|
||||
})
|
||||
|
||||
it('uploads an attachment through the frozen multipart endpoint', async () => {
|
||||
vi.mocked(http.post).mockResolvedValue({ data: { id: '1', fileId: '2', fileName: '派车单.pdf' } })
|
||||
const file = new File(['dispatch'], '派车单.pdf', { type: 'application/pdf' })
|
||||
await uploadTruckAppointmentAttachment('77', file, 'DISPATCH_ORDER')
|
||||
expect(http.post).toHaveBeenCalledWith('/platform/v1/truck-appointments/77/attachments/upload', expect.any(FormData), { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
const form = vi.mocked(http.post).mock.calls[0]?.[1] as FormData
|
||||
expect(form.get('file')).toBe(file)
|
||||
expect(form.get('attachmentType')).toBe('DISPATCH_ORDER')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,6 +71,14 @@ export async function referenceTruckAppointmentAttachment(id: string | number, p
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function uploadTruckAppointmentAttachment(id: string | number, file: File, attachmentType?: string | null) {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
if (attachmentType) form.append('attachmentType', attachmentType)
|
||||
const response = await http.post<ApiEnvelope<TruckAttachmentReference>, ApiEnvelope<TruckAttachmentReference>>(`${basePath}/${id}/attachments/upload`, form, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function deleteTruckAppointmentAttachment(id: string | number, attachmentId: string | number) {
|
||||
await http.delete<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`${basePath}/${id}/attachments/${attachmentId}`)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('TruckAppointmentForm interaction contract', () => {
|
||||
expect(source).not.toContain('label="承运商 ID"')
|
||||
expect(source).not.toContain('label="客户关联 ID"')
|
||||
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
|
||||
expect(source).toContain('customer.legalName?.trim() || customer.tenantName')
|
||||
expect(source).toContain('function customerLabel(customer: PlatformTenantResource) { return customer.tenantName }')
|
||||
expect(source).not.toContain('customer.tenantCode')
|
||||
})
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const selectableWarehouses = computed(() => warehouses.value.filter((item) => (
|
||||
const selectableCarriers = computed(() => carriers.value.filter((item) => (
|
||||
item.status === 'ACTIVE' || String(item.id) === String(draft.carrierId)
|
||||
)))
|
||||
function customerLabel(customer: PlatformTenantResource) { return customer.legalName?.trim() || customer.tenantName }
|
||||
function customerLabel(customer: PlatformTenantResource) { return customer.tenantName }
|
||||
function selectCustomer(customerId: string) { const customer = customers.value.find((item) => String(item.id) === customerId); draft.customerId = customerId || null; draft.customerName = customer ? customerLabel(customer) : null }
|
||||
function selectCarrier(carrierId: string) { draft.carrierId = carrierId || null; draft.carrierName = carriers.value.find((item) => String(item.id) === carrierId)?.partnerName || null }
|
||||
function selectContainer(containerId: string) { const item = containers.value.find((entry) => entry.id === containerId); draft.containerBookingId = containerId || null; if (item) { draft.loadingPlanId = item.planId; draft.warehouseId = item.warehouseId } }
|
||||
|
||||
@@ -12,10 +12,13 @@ describe('TruckAttachmentDialog interaction governance', () => {
|
||||
expect(source).not.toContain('item.objectKey')
|
||||
})
|
||||
|
||||
it('hides the unavailable manual reference flow but keeps deletion', () => {
|
||||
it('provides scoped upload and deletion without exposing manual file ids', () => {
|
||||
expect(source).not.toContain('referenceTruckAppointmentAttachment')
|
||||
expect(source).not.toContain('输入文件服务 ID')
|
||||
expect(source).toContain('uploadTruckAppointmentAttachment')
|
||||
expect(source).toContain('上传并关联')
|
||||
expect(source).toContain('deleteTruckAppointmentAttachment')
|
||||
expect(source).toContain("confirmButtonText: '确认', cancelButtonText: '取消'")
|
||||
expect(source).toContain('<Trash2 :size="15" />删除')
|
||||
expect(source).toContain('size="small" type="danger" plain')
|
||||
expect(source).not.toContain(' text type="danger"')
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Trash2 } from '@lucide/vue'
|
||||
import { deleteTruckAppointmentAttachment } from '../api/platformTrucking'
|
||||
import { FileUp, Trash2 } from '@lucide/vue'
|
||||
import { deleteTruckAppointmentAttachment, uploadTruckAppointmentAttachment } from '../api/platformTrucking'
|
||||
import type { TruckAttachmentReference } from '../types'
|
||||
const props = defineProps<{ modelValue: boolean; appointmentId?: string; attachments: TruckAttachmentReference[]; canManage?: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [boolean]; changed: [] }>()
|
||||
const busy = ref(false)
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const selectedType = ref('OTHER')
|
||||
const attachmentTypeLabels: Record<string, string> = { DISPATCH_ORDER: '派车单', DRIVER_DOCUMENT: '司机证件', VEHICLE_DOCUMENT: '车辆证件', OTHER: '其他附件' }
|
||||
function attachmentTypeLabel(value: string | null | undefined) { return value ? attachmentTypeLabels[value] || '业务附件' : '业务附件' }
|
||||
async function remove(item: TruckAttachmentReference) { if (!props.appointmentId || !item.id) return; await ElMessageBox.confirm('确认删除该附件引用?', '删除确认', { type: 'warning' }); busy.value = true; try { await deleteTruckAppointmentAttachment(props.appointmentId, item.id); emit('changed'); ElMessage.success('附件引用已删除') } catch { ElMessage.error('删除失败') } finally { busy.value = false } }
|
||||
function selectFile(upload: { raw?: File }) { selectedFile.value = upload.raw || null }
|
||||
async function upload() { if (!props.appointmentId || !selectedFile.value) return; busy.value = true; try { await uploadTruckAppointmentAttachment(props.appointmentId, selectedFile.value, selectedType.value); selectedFile.value = null; emit('changed'); ElMessage.success('附件上传成功') } catch { ElMessage.error('上传失败') } finally { busy.value = false } }
|
||||
async function remove(item: TruckAttachmentReference) { if (!props.appointmentId || !item.id) return; await ElMessageBox.confirm('确认删除该附件引用?', '删除确认', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }); busy.value = true; try { await deleteTruckAppointmentAttachment(props.appointmentId, item.id); emit('changed'); ElMessage.success('附件引用已删除') } catch { ElMessage.error('删除失败') } finally { busy.value = false } }
|
||||
</script>
|
||||
<template>
|
||||
<el-dialog :model-value="props.modelValue" title="预约附件" width="min(640px, 94vw)" @update:model-value="emit('update:modelValue', $event)">
|
||||
<div v-if="props.canManage" class="upload-panel"><el-select v-model="selectedType" aria-label="附件类型" style="width:160px"><el-option v-for="(label, type) in attachmentTypeLabels" :key="type" :label="label" :value="type" /></el-select><el-upload :auto-upload="false" :limit="1" :show-file-list="true" :on-change="selectFile" :on-remove="() => { selectedFile = null }"><el-button><FileUp :size="15" />选择附件</el-button></el-upload><el-button type="primary" :loading="busy" :disabled="!selectedFile" @click="upload">上传并关联</el-button></div>
|
||||
<el-empty v-if="attachments.length === 0" description="暂无预约附件" :image-size="72" />
|
||||
<div v-for="item in attachments" :key="item.fileId" class="attachment-row">
|
||||
<div><strong>{{ item.fileName || '未命名文件' }}</strong><small>{{ attachmentTypeLabel(item.attachmentType) }}{{ item.uploadedAt ? ` · ${item.uploadedAt}` : '' }}</small></div>
|
||||
@@ -22,6 +27,7 @@ async function remove(item: TruckAttachmentReference) { if (!props.appointmentId
|
||||
</el-dialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
.upload-panel { display:flex; align-items:center; flex-wrap:wrap; gap:8px; margin-bottom:16px; }
|
||||
.attachment-row { display:flex; justify-content:space-between; gap:12px; margin:14px 0; padding:12px; border:1px solid var(--el-border-color-lighter); border-radius:6px; overflow-wrap:anywhere; }.attachment-row small { display:block; margin-top:4px; color:var(--el-text-color-secondary); }
|
||||
@media (max-width: 560px) { .attachment-row { flex-direction:column; } }
|
||||
</style>
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('TruckAppointmentsView', () => {
|
||||
it('filters with an active name-only customer selector and stable customerId', () => {
|
||||
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
|
||||
expect(source).toContain("customer.status === 'ACTIVE'")
|
||||
expect(source).toContain("customer.legalName?.trim() || customer.tenantName")
|
||||
expect(source).toContain("label: customer.tenantName")
|
||||
expect(source).toContain("customerId: String(model.customerId || '')")
|
||||
expect(source).not.toContain('customer.tenantCode')
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ const access = useAccessPreviewStore()
|
||||
const customers = ref<PlatformTenantResource[]>([]); const customersLoading = ref(false)
|
||||
const filters = ref({ customerId: '', keyword: '', status: '' as TruckAppointmentStatus | '', containerBookingId: '', containerType: '' }); const applied = ref({ ...filters.value })
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.legalName?.trim() || customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'keyword', type: 'input', label: '关键词', placeholder: '派车单号、车牌号、运输公司' },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: TRUCK_APPOINTMENT_STATUSES.map((value) => ({ value, label: truckAppointmentStatusLabels[value] })) },
|
||||
{ key: 'containerType', type: 'select', label: '柜型', placeholder: '全部柜型', options: [
|
||||
|
||||
Reference in New Issue
Block a user