| src/assets/element-img/selected.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/service/api/vehicle.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/assetManagement/vehicleManagement/components/VehicleMaterialSelectModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/assetManagement/vehicleManagement/inventoryVehicle/add.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/assetManagement/vehicleManagement/vehicleMaterial/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/assets/element-img/selected.png
src/service/api/vehicle.ts
@@ -134,8 +134,8 @@ /** * 获取车辆素材列表 */ export function getVehicleMaterialList(params: any) { return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/fleetFile/list', params) export function getVehicleMaterialList() { return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/fleetFile/list') } /** src/views/assetManagement/vehicleManagement/components/VehicleMaterialSelectModal.vue
New file @@ -0,0 +1,356 @@ <script setup lang="ts"> import type { ImageInst } from 'naive-ui' import { NButton, NCard, NImage, NModal, NSpin } from 'naive-ui' import { useBoolean } from '@/hooks' import { getVehicleMaterialList } from '@/service/api/vehicle' import { downloadFile } from '@/utils' import iconPreview from '@/assets/element-img/icon-perview.png' import iconDownload from '@/assets/element-img/icon-download.png' import selectedIcon from '@/assets/element-img/selected.png' interface MaterialItem { id: string | number fileId?: string | number fileUrl?: string url?: string fileName?: string name?: string } interface Props { selectedMaterialId?: string | number | null title?: string } const props = withDefaults(defineProps<Props>(), { selectedMaterialId: null, title: '选择车辆素材', }) const emit = defineEmits<{ confirm: [material: MaterialItem] }>() const visible = defineModel<boolean>('visible', { default: false }) const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) const listData = ref<MaterialItem[]>([]) const selectedMaterialId = ref<string | number | null>(null) const imageRefs = ref<ImageInst[]>([]) const count = ref(0) function getMaterialImageUrl(item: MaterialItem) { return item.fileUrl || item.url || '' } function getMaterialName(item: MaterialItem) { return item.fileName || item.name || '车辆素材' } // 兼容列表接口返回数组和分页对象两种结构 async function getList() { startLoading() try { const res = await getVehicleMaterialList() if (res.code === 200 || res.code === 0) { if (Array.isArray(res.data)) { listData.value = res.data count.value = res.data.length } else { listData.value = res.data?.records || [] count.value = res.data?.total || 0 } } else { listData.value = [] count.value = 0 } } catch (error) { console.error('获取车辆素材列表失败', error) listData.value = [] count.value = 0 } finally { endLoading() } } function handleCancel() { visible.value = false } function handleSelect(item: MaterialItem) { selectedMaterialId.value = item.id } function handlePreview(index: number) { if (Array.isArray(imageRefs.value) && imageRefs.value[index]) { imageRefs.value[index].showPreview() } } async function handleDownload(item: MaterialItem) { const fileUrl = getMaterialImageUrl(item) if (!fileUrl) { window.$message?.warning('当前素材无可下载地址') return } const response = await fetch(fileUrl) const blob = await response.blob() const filename = getMaterialName(item) || `vehicle_cover_${Date.now()}.png` downloadFile(blob, filename) } function handleConfirm() { const selectedMaterial = listData.value.find(item => item.id === selectedMaterialId.value) if (!selectedMaterial) { window.$message?.warning('请选择车辆素材') return } emit('confirm', selectedMaterial) visible.value = false } watch(visible, (show) => { if (show) { selectedMaterialId.value = props.selectedMaterialId getList() } }) </script> <template> <NModal v-model:show="visible"> <NCard style=" width: 600px; height: 80vh; max-height: 800px; border-radius: 8px; display: flex; flex-direction: column; " :bordered="false" :segmented="{ content: true }" content-style="padding: 16px 24px; flex: 1; display: flex; flex-direction: column; overflow: hidden;" footer-style="padding: 16px 24px; border-top: 1px solid rgba(46, 61, 77, 0.1); flex-shrink: 0;" > <template #header> <span class="modal-title">{{ title }}</span> </template> <template #header-extra> <div class="close-btn" @click="handleCancel"> <span class="close-icon">×</span> </div> </template> <div class="modal-body"> <NSpin :show="loading" class="body-spin"> <div class="material-grid"> <div v-for="(item, index) in listData" :key="item.id" class="material-item" :class="{ 'is-active': selectedMaterialId === item.id }" @click="handleSelect(item)" > <NImage ref="imageRefs" :src="getMaterialImageUrl(item)" object-fit="cover" class="material-image" fallback-src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" /> <div class="overlay"> <div class="action-btn" title="预览" @click.stop="handlePreview(index)"> <img :src="iconPreview" class="action-icon"> </div> <div class="action-btn" title="下载" @click.stop="handleDownload(item)"> <img :src="iconDownload" class="action-icon"> </div> </div> <!-- <div class="select-mask"> <div class="selected-dot" /> </div> --> <img v-if="selectedMaterialId === item.id" :src="selectedIcon" class="selected-img" > </div> </div> <div v-if="!loading && listData.length === 0" class="empty-wrap"> <img src="/src/assets/element-img/no-data.png" class="empty-img" > </div> </NSpin> </div> <template #footer> <div class="footer-actions"> <NButton class="footer-btn" @click="handleCancel"> 取消 </NButton> <NButton type="primary" class="footer-btn" @click="handleConfirm"> 确定 </NButton> </div> </template> </NCard> </NModal> </template> <style scoped lang="scss"> .modal-title { font-size: 16px; font-weight: 600; color: #171f26; } .close-btn { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; cursor: pointer; } .close-icon { font-size: 18px; color: rgba(23, 31, 38, 0.6); } .modal-body { display: flex; flex: 1; flex-direction: column; min-height: 0; overflow: hidden; } .body-spin { flex: 1; min-height: 0; } .material-grid { display: flex; flex-wrap: wrap; gap: 16px 24px; align-content: flex-start; } .material-item { position: relative; overflow: hidden; border: 1px solid transparent; border-radius: 4px; background: #fff; cursor: pointer; transition: all 0.2s ease; width: 120px; height: 120px; &:hover { border-color: rgba(0, 104, 255, 0.5); } &:hover .overlay { opacity: 1; } &.is-active { border: 1px solid #0068FF; } } .material-image { width: 100%; height: 120px; display: block; } .overlay { position: absolute; top: 0; left: 0; display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; height: 100%; opacity: 0; background: rgba(23, 31, 38, 0.35); transition: opacity 0.2s ease; } .action-btn { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; } .action-icon { width: 32px; height: 32px; } .selected-img { position: absolute; top: 0; right: 0; width: 20px; height: 20px; } .selected-dot { width: 8px; height: 8px; border-radius: 50%; background: #fff; opacity: 0; } .is-active .selected-dot { opacity: 1; } .empty-wrap { display: flex; align-items: center; justify-content: center; height: 100%; } .empty-img { width: 123px; height: 125px; } .footer-actions { display: flex; justify-content: flex-end; gap: 12px; } .footer-btn { min-width: 84px; height: 32px; } </style> src/views/assetManagement/vehicleManagement/inventoryVehicle/add.vue
@@ -9,6 +9,7 @@ import { useTabStore } from '@/store' import { parseFileJson } from '@/utils/common' import { beforeUploadCheck, customUploadRequest } from '@/utils/upload' import VehicleMaterialSelectModal from '../components/VehicleMaterialSelectModal.vue' import excelIcon from '@/assets/element-img/excel.png' import wordIcon from '@/assets/element-img/word.png' import pdfIcon from '@/assets/element-img/pdf.png' @@ -44,6 +45,7 @@ model: null as string | null, // 型号 vehicleColor: '', // 车辆颜色 vehiclePhotos: [] as UploadFileInfo[], // 车辆照片 vehiclePictureId: null as string | number | null, // 车辆图片ID purchaseSource: '', // 采购来源 remark: '', // 备注 operationArea: null as string | null, // 运营区域 @@ -166,6 +168,15 @@ })) } function getVehiclePictureId(value: unknown): string | number | null { const pictureList = parseFileJson(value) if (!pictureList.length) return null const firstPicture = pictureList[0] return firstPicture?.id ?? null } async function loadVehicleDetail() { if (!isEditMode.value || !vehicleId.value) return @@ -184,6 +195,7 @@ model: data.vehicleModel, vehicleColor: data.vehicleColor || '', vehiclePhotos: toUploadFileList(data.vehiclePictures), vehiclePictureId: getVehiclePictureId(data.vehiclePictures), purchaseSource: data.procurementSource || '', remark: data.remarks || data.remark || '', operationArea: data.electronicId === null || data.electronicId === undefined || data.electronicId === '' ? null : String(data.electronicId), @@ -512,6 +524,32 @@ // 保存中状态 const saving = ref(false) const showVehicleMaterialModal = ref(false) function handleOpenVehicleMaterialModal() { showVehicleMaterialModal.value = true } function handleVehicleMaterialConfirm(material: any) { const imageUrl = material?.fileUrl || material?.url || '' if (!imageUrl) { window.$message.warning('所选素材缺少图片地址') return } const pictureId = material?.id || null const pictureName = material?.name || '车辆照片' formModel.value.vehiclePictureId = pictureId formModel.value.vehiclePhotos = pictureId ? [{ id: pictureId, name: pictureName, url: imageUrl, status: 'finished', }] : [] } // 处理保存 async function handleSave() { @@ -560,6 +598,7 @@ insuranceEndTime: formModel.value.insuranceEndTime ? dayjs(formModel.value.insuranceEndTime).format('YYYY-MM-DD HH:mm:ss') : null, qualificationExpireTime: formModel.value.onlineCarLicenseExpireDate ? dayjs(formModel.value.onlineCarLicenseExpireDate).format('YYYY-MM-DD HH:mm:ss') : null, insurancePhone: formModel.value.insurancePhone, vehiclePictureId: formModel.value.vehiclePictureId || null, } // 处理附件 const mapFiles = (files: UploadFileInfo[]) => { @@ -572,14 +611,6 @@ }))) } if (formModel.value.vehiclePhotos.length > 0) { const photo = formModel.value.vehiclePhotos[0] params.vehiclePictures = JSON.stringify({ id: photo.id, name: photo.name, url: photo.url, }) } params.inspectionAttachments = mapFiles(formModel.value.inspectionFiles) params.policyInformation = mapFiles(formModel.value.insuranceFiles) params.operationQualification = mapFiles(formModel.value.onlineCarLicenseFiles) @@ -677,33 +708,24 @@ <span style="color: #ff4d4f; margin-right: 4px">*</span>车辆照片 </div> <div class="photo-upload-area"> <n-upload v-model:file-list="formModel.vehiclePhotos" :max="1" accept=".jpg,.jpeg,.png" :custom-request=" (options) => handleUpload(options, 'vehiclePhotos') " :on-before-upload="beforeImageUpload" list-type="image-card" class="photo-upload" @remove="(data) => handleRemove(data, 'vehiclePhotos')" <img v-if="formModel.vehiclePhotos[0]?.url" :src="formModel.vehiclePhotos[0]?.url" class="vehicle-photo-preview" alt="vehicle-photo" @click="handleOpenVehicleMaterialModal" > <!-- <div class="upload-trigger"> <div class="upload-icon"> + </div> </div> --> <img :src="uploadIcon" style="width: 56px; height: 56px" alt="upload" > </n-upload> <img v-else :src="uploadIcon" style="width: 56px; height: 56px;cursor: pointer;" alt="upload" @click="handleOpenVehicleMaterialModal" > </div> </div> </n-form-item> <div class="flex"> <!-- <div class="flex"> <img :src="noticeIcon" style=" @@ -718,7 +740,7 @@ <span>大小≤5MB,最多</span> <span>上传1张</span> </div> </div> </div> --> </div> <!-- 右侧:表单 --> @@ -1429,6 +1451,13 @@ </n-card> </div> <VehicleMaterialSelectModal v-model:visible="showVehicleMaterialModal" title="车辆图片上传" :selected-material-id="formModel.vehiclePictureId" @confirm="handleVehicleMaterialConfirm" /> <!-- 底部固定按钮 --> <div class="form-actions"> <n-button class="action-btn" @click="handleBack"> @@ -1533,6 +1562,14 @@ flex-direction: column; } .vehicle-photo-preview { width: 56px; height: 56px; object-fit: cover; border-radius: 6px; cursor: pointer; } .photo-tip { display: flex; flex-direction: column; src/views/assetManagement/vehicleManagement/vehicleMaterial/index.vue
@@ -39,7 +39,7 @@ async function getList() { startLoading() try { const res = await getVehicleMaterialList({}) const res = await getVehicleMaterialList() if (res.code === 200 || res.code === 0) { listData.value = Array.isArray(res.data) ? res.data : [] } @@ -102,16 +102,15 @@ } // OSS 上传成功后,再调用业务接口保存车辆素材 const saveRes = await uploadVehicleMaterial({ id: uploadRes.data.id, name: uploadRes.data.name, url: uploadRes.data.url, }) const saveRes = await uploadVehicleMaterial([{ id: '123', name: '321', url: `https://gips0.baidu.com/it/u=1690853528,2506870245&fm=3028&app=3028&f=JPEG&fmt=auto?w=1024&h=1024`, }]) if (saveRes.code === 200 || saveRes.code === 0) { message.success('上传成功') onFinish() getList() return } onError() @@ -122,6 +121,10 @@ } finally { uploadingIds.value.delete(fileId) // 支持多选上传,所有文件处理完成后统一刷新列表 if (uploadingIds.value.size === 0) { getList() } } }