<script setup lang="tsx">
|
import { onMounted, ref, watch } from 'vue'
|
import type { FormInst, FormRules, UploadFileInfo } from 'naive-ui'
|
import { useRoute, useRouter } from 'vue-router'
|
import dayjs from 'dayjs'
|
import { addVehicle, editVehicle, getVehicleById } from '@/service/api/vehicle'
|
import { queryVehicleModelNoPageList } from '@/service/api/vehicleConvoy'
|
import { fetchElectronicFenceAreaList } from '@/service/api/point'
|
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'
|
import uploadIcon from '@/assets/element-img/upload.png'
|
import noticeIcon from '@/assets/element-img/notice.png'
|
import noDataIcon from '@/assets/element-img/no-data.png'
|
|
const router = useRouter()
|
const route = useRoute()
|
const tabStore = useTabStore()
|
|
const vehicleFormRef = ref<FormInst | null>(null)
|
const supplementFormRef = ref<FormInst | null>(null)
|
const completeInfoFormRef = ref<FormInst | null>(null)
|
const otherInfoFormRef = ref<FormInst | null>(null)
|
|
type UploadField = 'vehiclePhotos' | 'inspectionFiles' | 'insuranceFiles' | 'onlineCarLicenseFiles'
|
|
const fileIconMap: Record<string, string> = {
|
xls: excelIcon,
|
xlsx: excelIcon,
|
doc: wordIcon,
|
docx: wordIcon,
|
pdf: pdfIcon,
|
}
|
|
function createInitialFormModel() {
|
return {
|
// 车辆信息
|
vehicleCode: '', // 车辆编码
|
vin: '', // 车辆VIN码
|
plateNumber: '', // 车牌号
|
model: null as string | null, // 型号
|
vehicleColor: '', // 车辆颜色
|
vehiclePhotos: [] as UploadFileInfo[], // 车辆照片
|
vehiclePictureId: null as string | number | null, // 车辆图片ID
|
purchaseSource: '', // 采购来源
|
remark: '', // 备注
|
operationArea: null as string | null, // 运营区域
|
vehicleAttribute: null as number | null, // 车辆属性
|
vehicleModelType: null as number | null, // 车辆类型
|
ownerType: '1', // 所有人类型 1企业 2个人
|
ownerName: '', // 车辆所有人
|
driveMotorNo: '', // 驱动电机编号
|
registerDate: null as number | null, // 车辆注册日期
|
driveMotorPeakPower: '', // 驱动电机峰值功率
|
wheelBase: '', // 车辆轴距
|
invoiceDeviceNo: '', // 网约车发票打印设备序列号
|
gpsBrand: '', // 卫星定位装置品牌
|
gpsModel: '', // 卫星定位装置型号
|
gpsImei: '', // IMEI号
|
gpsInstallDate: null as number | null, // 安装日期
|
|
// 完善信息
|
inspectionExpireDate: null as number | null, // 年检到期时间
|
insuranceCompany: '', // 保险公司
|
insuranceNo: '', // 保单号
|
insuranceType: null as number | null, // 保险类型
|
insuranceAmount: '', // 保险金额
|
insuranceEffectiveTime: null as number | null, // 保险生效日期
|
insuranceEndTime: null as number | null, // 保险到期时间
|
onlineCarLicenseExpireDate: null as number | null, // 网约车运营资质到期时间
|
insurancePhone: '', // 保险电话
|
inspectionFiles: [] as UploadFileInfo[], // 年检资料附件
|
insuranceFiles: [] as UploadFileInfo[], // 保险资料
|
onlineCarLicenseFiles: [] as UploadFileInfo[], // 网约车运营资质
|
}
|
}
|
|
// 表单数据
|
const formModel = ref(createInitialFormModel())
|
|
// 运营区域选项
|
const operationAreaOptions = ref<{ label: string, value: string | number }[]>([])
|
const isOperationAreaSelectOpen = ref(false)
|
|
// 车辆型号选项
|
const modelOptions = ref<{ label: string, value: string | number }[]>([])
|
const isModelSelectOpen = ref(false)
|
|
// 车辆属性选项
|
const vehicleAttributeOptions = [
|
{ label: '测试', value: 1 },
|
{ label: '运营', value: 2 },
|
]
|
const isVehicleAttributeSelectOpen = ref(false)
|
const isVehicleModelTypeSelectOpen = ref(false)
|
const isInsuranceTypeSelectOpen = ref(false)
|
|
const vehicleModelTypeOptions = [
|
{ label: '小型纯电动客车', value: 1 },
|
{ label: '小型插电式混合动力客车', value: 2 },
|
{ label: '轻型纯电动厢式货车', value: 3 },
|
{ label: '重型纯电动仓栅式货车', value: 4 },
|
]
|
|
const ownerTypeOptions = [
|
{ label: '企业', value: '1' },
|
{ label: '个人', value: '2' },
|
]
|
|
const insuranceTypeOptions = [
|
{ label: '交强险', value: 1 },
|
{ label: '商业险', value: 2 },
|
{ label: '新能源附加险', value: 3 },
|
]
|
|
const vehicleId = ref<string | null>(typeof route.query.id === 'string' ? route.query.id : null)
|
const isEditMode = ref(route.query.mode === 'edit')
|
const pageTitle = ref(isEditMode.value ? '编辑车辆' : '新增车辆')
|
|
function syncPageTitle() {
|
pageTitle.value = isEditMode.value ? '编辑车辆' : '新增车辆'
|
route.meta.title = pageTitle.value
|
document.title = pageTitle.value
|
const currentMatchedRoute = route.matched[route.matched.length - 1]
|
if (currentMatchedRoute) {
|
currentMatchedRoute.meta.title = pageTitle.value
|
}
|
|
const targetTabPaths = [route.fullPath, tabStore.currentTabPath].filter(Boolean)
|
targetTabPaths.forEach((fullPath) => {
|
if (tabStore.getTabIndex(fullPath) === -1)
|
return
|
tabStore.modifyTab(fullPath, (tab) => {
|
if (!tab)
|
return
|
tab.meta.title = pageTitle.value
|
// 标签页文案优先走 route.name 对应的国际化 key,这里切换成一个不存在的 name,
|
// 让 TabBar 回退显示 meta.title,从而正确展示“编辑车辆”
|
tab.name = (isEditMode.value ? 'editInventoryVehicle' : 'addInventoryVehicle') as never
|
})
|
})
|
}
|
|
function toNumberOrNull(value: unknown) {
|
if (value === null || value === undefined || value === '')
|
return null
|
const numberValue = Number(value)
|
return Number.isNaN(numberValue) ? null : numberValue
|
}
|
|
function toTimestampOrNull(value: unknown) {
|
if (!value)
|
return null
|
const timestamp = dayjs(value as string | number | Date).valueOf()
|
return Number.isNaN(timestamp) ? null : timestamp
|
}
|
|
function toUploadFileList(value: unknown): UploadFileInfo[] {
|
return parseFileJson(value).map((item: any, index: number) => ({
|
id: item?.id || `${index}`,
|
name: item?.name || `附件${index + 1}`,
|
url: item?.url || '',
|
status: 'finished',
|
}))
|
}
|
|
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
|
try {
|
const res = await getVehicleById(vehicleId.value)
|
if (res.code !== 200 || !res.data) {
|
window.$message.error(res.msg || '获取车辆详情失败')
|
return
|
}
|
const data = res.data
|
formModel.value = {
|
...createInitialFormModel(),
|
vehicleCode: data.vehicleCode || '',
|
vin: data.vehicleVin || '',
|
plateNumber: data.plateNumber || '',
|
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),
|
vehicleAttribute: toNumberOrNull(data.vehicleAttribute),
|
vehicleModelType: toNumberOrNull(data.vehicleModelType),
|
ownerType: String(data.ownerType ?? '1'),
|
ownerName: data.ownerName || data.vehicleOwner || '',
|
driveMotorNo: data.driveMotorNo || data.driveMotorNumber || '',
|
registerDate: toTimestampOrNull(data.registerDate || data.registrationDate),
|
driveMotorPeakPower: data.driveMotorPeakPower || '',
|
wheelBase: data.wheelBase || data.vehicleWheelbase || '',
|
invoiceDeviceNo: data.invoiceDeviceNo || data.invoicePrinterSerialNumber || '',
|
gpsBrand: data.gpsBrand || data.satelliteBrand || '',
|
gpsModel: data.gpsModel || data.satelliteModel || '',
|
gpsImei: data.gpsImei || data.satelliteImei || '',
|
gpsInstallDate: toTimestampOrNull(data.gpsInstallDate || data.satelliteInstallDate),
|
inspectionExpireDate: toTimestampOrNull(data.annualInspectionDate || data.inspectionExpireDate),
|
insuranceCompany: data.insuranceCompany || '',
|
insuranceNo: data.insuranceNo || '',
|
insuranceType: toNumberOrNull(data.insuranceType),
|
insuranceAmount: data.insuranceAmount || '',
|
insuranceEffectiveTime: toTimestampOrNull(data.insuranceStartTime || data.insuranceEffectiveTime),
|
insuranceEndTime: toTimestampOrNull(data.insuranceEndTime),
|
onlineCarLicenseExpireDate: toTimestampOrNull(data.qualificationExpireTime || data.onlineCarLicenseExpireDate),
|
insurancePhone: data.insurancePhone || '',
|
inspectionFiles: toUploadFileList(data.inspectionAttachments),
|
insuranceFiles: toUploadFileList(data.policyInformation),
|
onlineCarLicenseFiles: toUploadFileList(data.operationQualification),
|
}
|
}
|
catch (error) {
|
console.error('获取车辆详情失败:', error)
|
window.$message.error('获取车辆详情失败')
|
}
|
}
|
|
// 获取车辆型号列表(预留接口)
|
async function getVehicleModelList() {
|
try {
|
const { data } = await queryVehicleModelNoPageList() // TODO: 对接获取车辆型号接口
|
modelOptions.value = data.map((item: any) => ({
|
label: item.name,
|
value: item.id,
|
}))
|
}
|
catch (error) {
|
console.error('获取型号列表失败:', error)
|
}
|
}
|
|
// 获取运营区域列表
|
async function getOperationAreaList() {
|
try {
|
const { data } = await fetchElectronicFenceAreaList()
|
if (data) {
|
// 过滤出 areaType 为 1 的运营区域
|
operationAreaOptions.value = data
|
.filter((item: any) => item.areaType === 1)
|
.map((item: any) => ({
|
label: item.areaName,
|
value: item.electronicId,
|
}))
|
}
|
}
|
catch (error) {
|
console.error('获取运营区域失败:', error)
|
}
|
}
|
|
onMounted(() => {
|
syncPageTitle()
|
getOperationAreaList()
|
getVehicleModelList()
|
loadVehicleDetail()
|
})
|
|
const onlyAllowNumberAndDash = (value: string) => !value || /^[\d-]+$/.test(value)
|
|
// 表单验证规则
|
const formRules: FormRules = {
|
vehicleCode: [
|
{ required: true, message: '请输入车辆编码', trigger: ['blur', 'input'] },
|
{ len: 5, message: '车辆编码长度为5位', trigger: ['blur', 'input'] },
|
{ pattern: /^\w+$/, message: '车辆编码仅支持字母、数字、下划线', trigger: ['blur', 'input'] },
|
],
|
vin: [
|
{ required: true, message: '请输入车辆VIN码', trigger: ['blur', 'input'] },
|
{ pattern: /^[A-Z0-9]{17}$/, message: 'VIN码为17位字符,仅支持大写字母与数字', trigger: ['blur', 'input'] },
|
],
|
plateNumber: [
|
{ required: true, message: '请输入车牌号', trigger: ['blur', 'input'] },
|
{
|
pattern: /^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z]((\d{5}[DF])|([DF]([A-HJ-NP-Z0-9])\d{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]))$|^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][0-9A-Z]{6}$/, // 新增8位新能源车牌校验
|
message: '车牌号格式不正确',
|
trigger: ['blur', 'input'],
|
},
|
],
|
vehicleColor: [
|
{ required: true, message: '请输入车辆颜色', trigger: ['blur', 'input'] },
|
{ min: 2, max: 3, message: '车辆颜色长度为2-3位', trigger: ['blur', 'input'] },
|
{ pattern: /^[\u4E00-\u9FA5a-z]+$/i, message: '车辆颜色仅支持中文和英文', trigger: ['blur', 'input'] },
|
],
|
vehiclePhotos: [
|
{ required: true, type: 'array', message: '请上传车辆照片', trigger: ['change'] },
|
],
|
model: [
|
{ required: true, message: '请选择型号', trigger: ['blur', 'change'] },
|
],
|
purchaseSource: [
|
{ required: true, message: '请输入采购来源', trigger: ['blur', 'input'] },
|
{ min: 1, max: 50, message: '采购来源长度为1-50位', trigger: ['blur', 'input'] },
|
],
|
ownerName: [
|
{ max: 50, message: '车辆所有人长度不能超过50位', trigger: ['blur', 'input'] },
|
],
|
driveMotorNo: [
|
{ max: 50, message: '驱动电机编号长度不能超过50位', trigger: ['blur', 'input'] },
|
],
|
driveMotorPeakPower: [
|
{ max: 20, message: '驱动电机峰值功率长度不能超过20位', trigger: ['blur', 'input'] },
|
],
|
wheelBase: [
|
{ max: 20, message: '车辆轴距长度不能超过20位', trigger: ['blur', 'input'] },
|
],
|
invoiceDeviceNo: [
|
{ max: 50, message: '网约车发票打印设备序列号长度不能超过50位', trigger: ['blur', 'input'] },
|
],
|
gpsBrand: [
|
{ max: 50, message: '品牌长度不能超过50位', trigger: ['blur', 'input'] },
|
],
|
gpsModel: [
|
{ max: 50, message: '型号长度不能超过50位', trigger: ['blur', 'input'] },
|
],
|
gpsImei: [
|
{ max: 50, message: 'IMEI号长度不能超过50位', trigger: ['blur', 'input'] },
|
],
|
remark: [
|
{ max: 200, message: '备注长度不能超过200位', trigger: ['blur', 'input'] },
|
],
|
operationArea: [
|
{ required: true, type: 'number', message: '请选择运营区域', trigger: ['blur', 'change'] },
|
],
|
vehicleAttribute: [
|
{ required: true, type: 'number', message: '请选择车辆属性', trigger: ['blur', 'change'] },
|
],
|
// 完善信息验证
|
inspectionExpireDate: [
|
{
|
validator: (rule, value) => {
|
if (value && value <= Date.now()) {
|
return new Error('年检到期时间必须大于当前时间')
|
}
|
return true
|
},
|
trigger: ['blur', 'change'],
|
},
|
],
|
insuranceCompany: [
|
{ min: 1, max: 100, message: '保险公司长度为1-100位', trigger: ['blur', 'input'] },
|
{ pattern: /^[\u4E00-\u9FA5a-z0-9]+$/i, message: '保险公司仅支持中文、英文、数字', trigger: ['blur', 'input'] },
|
],
|
insuranceNo: [
|
{ max: 50, message: '保单号长度不能超过50位', trigger: ['blur', 'input'] },
|
],
|
insuranceAmount: [
|
{ max: 20, message: '保险金额长度不能超过20位', trigger: ['blur', 'input'] },
|
],
|
insuranceEndTime: [
|
{
|
validator: (rule, value) => {
|
if (value && value <= Date.now()) {
|
return new Error('保险到期时间必须大于当前时间')
|
}
|
return true
|
},
|
trigger: ['blur', 'change'],
|
},
|
],
|
onlineCarLicenseExpireDate: [
|
{
|
validator: (rule, value) => {
|
if (value && value <= Date.now()) {
|
return new Error('网约车运营资质到期时间必须大于当前时间')
|
}
|
return true
|
},
|
trigger: ['blur', 'change'],
|
},
|
],
|
insuranceFiles: [
|
{
|
validator: (rule, value: UploadFileInfo[]) => {
|
const hasInsuranceInfo = Boolean(
|
formModel.value.insuranceCompany
|
|| formModel.value.insurancePhone
|
|| formModel.value.insuranceEndTime,
|
)
|
if (hasInsuranceInfo && (!value || value.length === 0)) {
|
return new Error('填写了保险信息后,必须上传保险资料')
|
}
|
return true
|
},
|
trigger: ['blur', 'change'],
|
},
|
],
|
onlineCarLicenseFiles: [
|
{
|
validator: (rule, value: UploadFileInfo[]) => {
|
if (formModel.value.onlineCarLicenseExpireDate && (!value || value.length === 0)) {
|
return new Error('设置了网约车运营到期时间后,必须上传网约车运营资质')
|
}
|
return true
|
},
|
trigger: ['blur', 'change'],
|
},
|
],
|
insurancePhone: [
|
{
|
validator: (rule, value) => {
|
if (!value)
|
return true
|
const phone = String(value)
|
const patterns = [
|
/^1[3-9]\d{9}$/,
|
/^\d{3,5}$/,
|
/^0\d{2,3}-?\d{7,8}$/,
|
/^0\d{9,11}$/,
|
/^(400|800)\d{7}$/,
|
/^\d{7,8}$/,
|
]
|
const isValid = patterns.some(pattern => pattern.test(phone))
|
if (!isValid) {
|
return new Error('请输入正确的电话号码(手机号/固话/400-800/特殊服务号)')
|
}
|
return true
|
},
|
trigger: ['blur', 'input'],
|
},
|
],
|
inspectionFiles: [
|
{
|
validator: (rule, value: UploadFileInfo[]) => {
|
// 如果设置了年检到期时间,必须上传年检资料
|
if (formModel.value.inspectionExpireDate && (!value || value.length === 0)) {
|
return new Error('设置了年检到期时间后,必须上传年检资料附件')
|
}
|
return true
|
},
|
trigger: ['blur', 'change'],
|
},
|
],
|
}
|
|
// 仅允许上传 PDF 的校验
|
function beforePdfUpload(data: { file: UploadFileInfo, fileList: UploadFileInfo[] }) {
|
const file = data.file.file
|
if (!file)
|
return false
|
const ext = file.name.split('.').pop()?.toLowerCase()
|
if (file.type !== 'application/pdf' && ext !== 'pdf') {
|
window.$message.error('仅支持上传 PDF 文件')
|
return false
|
}
|
return beforeUploadCheck(data)
|
}
|
|
// 图片上传前的校验
|
function beforeImageUpload(data: { file: UploadFileInfo, fileList: UploadFileInfo[] }) {
|
const file = data.file.file
|
if (!file)
|
return false
|
|
const ext = file.name.split('.').pop()?.toLowerCase()
|
const allowedExts = ['jpg', 'jpeg', 'png']
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg']
|
if (!ext || (!allowedExts.includes(ext) && !allowedTypes.includes(file.type))) {
|
window.$message.error('只能上传 jpg/jpeg/png 格式的图片')
|
return false
|
}
|
return beforeUploadCheck(data)
|
}
|
// 自定义上传请求
|
async function handleUpload(options: { file: UploadFileInfo, onFinish: () => void, onError: () => void, onProgress: (e: { percent: number }) => void }, field: UploadField) {
|
// 在异步请求前,缓存原始文件ID,因为 customUploadRequest 可能会改变 options.file.id
|
const originalFileId = options.file.id
|
let res: any
|
try {
|
res = await customUploadRequest(options)
|
}
|
catch {
|
res = undefined
|
}
|
if (res?.id) {
|
const fileList = formModel.value[field] as UploadFileInfo[]
|
// 使用原始ID来查找,确保能定位到正确的文件对象
|
const fileItem = fileList.find(f => f.id === originalFileId)
|
if (fileItem) {
|
// 用服务器返回的真实ID和URL更新文件对象
|
fileItem.id = res.id
|
fileItem.url = res.url
|
}
|
}
|
else {
|
const fileList = formModel.value[field] as UploadFileInfo[]
|
formModel.value[field] = fileList.filter(f => f.id !== originalFileId && f.id !== options.file.id && f !== options.file)
|
}
|
}
|
|
// 删除文件
|
function handleRemove(data: { file: UploadFileInfo }, field: UploadField) {
|
const list = formModel.value[field]
|
formModel.value[field] = list.filter(item => item.id !== data.file.id)
|
}
|
|
// 删除指定文件
|
function removeFile(index: number, field: UploadField) {
|
const list = formModel.value[field]
|
list.splice(index, 1)
|
}
|
|
// 获取文件图标
|
function getFileIcon(fileName: string) {
|
const ext = fileName.split('.').pop()?.toLowerCase()
|
return (ext && fileIconMap[ext]) || pdfIcon
|
}
|
|
// 保存中状态
|
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() {
|
try {
|
await vehicleFormRef.value?.validate()
|
await supplementFormRef.value?.validate()
|
await completeInfoFormRef.value?.validate()
|
await otherInfoFormRef.value?.validate()
|
}
|
catch {
|
window.$message.error('请完善必填信息')
|
return
|
}
|
|
try {
|
saving.value = true
|
|
const params: any = {
|
id: isEditMode.value ? vehicleId.value : undefined,
|
vehicleCode: formModel.value.vehicleCode,
|
vehicleVin: formModel.value.vin,
|
plateNumber: formModel.value.plateNumber,
|
vehicleColor: formModel.value.vehicleColor,
|
vehicleModel: formModel.value.model,
|
procurementSource: formModel.value.purchaseSource,
|
remarks: formModel.value.remark,
|
electronicId: formModel.value.operationArea,
|
vehicleModelType: formModel.value.vehicleModelType || '',
|
ownerType: formModel.value.ownerType,
|
ownerName: formModel.value.ownerName,
|
driveMotorNo: formModel.value.driveMotorNo,
|
registerDate: formModel.value.registerDate ? dayjs(formModel.value.registerDate).format('YYYY-MM-DD HH:mm:ss') : null,
|
driveMotorPeakPower: formModel.value.driveMotorPeakPower,
|
wheelBase: formModel.value.wheelBase,
|
invoiceDeviceNo: formModel.value.invoiceDeviceNo,
|
gpsBrand: formModel.value.gpsBrand,
|
gpsModel: formModel.value.gpsModel,
|
gpsImei: formModel.value.gpsImei,
|
gpsInstallDate: formModel.value.gpsInstallDate ? dayjs(formModel.value.gpsInstallDate).format('YYYY-MM-DD HH:mm:ss') : null,
|
annualInspectionDate: formModel.value.inspectionExpireDate ? dayjs(formModel.value.inspectionExpireDate).format('YYYY-MM-DD HH:mm:ss') : null,
|
insuranceCompany: formModel.value.insuranceCompany,
|
insuranceNo: formModel.value.insuranceNo,
|
insuranceType: formModel.value.insuranceType,
|
insuranceAmount: Number(formModel.value.insuranceAmount),
|
insuranceStartTime: formModel.value.insuranceEffectiveTime ? dayjs(formModel.value.insuranceEffectiveTime).format('YYYY-MM-DD HH:mm:ss') : null,
|
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[]) => {
|
if (!files || files.length === 0)
|
return null
|
return JSON.stringify(files.map(f => ({
|
id: f.id,
|
name: f.name,
|
url: f.url,
|
})))
|
}
|
|
params.inspectionAttachments = mapFiles(formModel.value.inspectionFiles)
|
params.policyInformation = mapFiles(formModel.value.insuranceFiles)
|
params.operationQualification = mapFiles(formModel.value.onlineCarLicenseFiles)
|
|
const { code } = isEditMode.value ? await editVehicle(params) : await addVehicle(params)
|
if (code === 200) {
|
window.$message.success(isEditMode.value ? '编辑车辆成功' : '新增车辆成功')
|
window.sessionStorage.setItem('inventoryVehicleNeedRefresh', '1')
|
if (route.query.source === 'operatingVehicle') {
|
window.sessionStorage.setItem('operatingVehicleNeedRefresh', '1')
|
}
|
// 返回列表页
|
router.go(-1)
|
}
|
}
|
catch {
|
// 错误已经在 request 拦截器中处理
|
}
|
finally {
|
saving.value = false
|
}
|
}
|
|
// 处理返回
|
function handleBack() {
|
window.$dialog.warning({
|
title: '提示',
|
content: '是否放弃当前修改?',
|
positiveText: '确定',
|
negativeText: '取消',
|
onPositiveClick: () => {
|
router.back()
|
},
|
})
|
}
|
|
// 监听年检/保险到期时间变化,触发附件验证
|
watch([
|
() => formModel.value.inspectionExpireDate,
|
() => formModel.value.insuranceCompany,
|
() => formModel.value.insuranceEndTime,
|
() => formModel.value.insurancePhone,
|
() => formModel.value.onlineCarLicenseExpireDate,
|
], () => {
|
supplementFormRef.value?.validate(
|
() => {},
|
(rule) => {
|
return ['inspectionFiles'].includes(String(rule?.key))
|
},
|
)
|
completeInfoFormRef.value?.validate(
|
() => {},
|
(rule) => {
|
return ['insuranceFiles'].includes(String(rule?.key))
|
},
|
)
|
otherInfoFormRef.value?.validate(
|
() => {},
|
(rule) => {
|
return ['onlineCarLicenseFiles'].includes(String(rule?.key))
|
},
|
)
|
})
|
|
watch(
|
() => [route.query.mode, route.query.id],
|
([mode, id]) => {
|
isEditMode.value = mode === 'edit'
|
vehicleId.value = typeof id === 'string' ? id : null
|
syncPageTitle()
|
},
|
)
|
</script>
|
|
<template>
|
<div class="page-container">
|
<div class="content-wrapper">
|
<!-- 车辆信息 -->
|
<n-card :bordered="false" class="section-card">
|
<div class="section-header">
|
<div class="section-title">
|
车辆信息
|
</div>
|
</div>
|
<n-form
|
ref="vehicleFormRef"
|
:model="formModel"
|
:rules="formRules"
|
label-placement="left"
|
in-line
|
require-mark-placement="left"
|
>
|
<div class="vehicle-info-layout">
|
<!-- 左侧:车辆照片 -->
|
<div>
|
<n-form-item path="vehiclePhotos" :show-label="false">
|
<div class="photo-section">
|
<div class="photo-label">
|
<span style="color: #ff4d4f; margin-right: 4px">*</span>车辆照片
|
</div>
|
<div class="photo-upload-area">
|
<img
|
v-if="formModel.vehiclePhotos[0]?.url"
|
:src="formModel.vehiclePhotos[0]?.url"
|
class="vehicle-photo-preview"
|
alt="vehicle-photo"
|
@click="handleOpenVehicleMaterialModal"
|
>
|
<img
|
v-else
|
:src="uploadIcon"
|
style="width: 56px; height: 56px;cursor: pointer;"
|
alt="upload"
|
@click="handleOpenVehicleMaterialModal"
|
>
|
</div>
|
</div>
|
</n-form-item>
|
<!-- <div class="flex">
|
<img
|
:src="noticeIcon"
|
style="
|
width: 12px;
|
height: 12px;
|
margin-right: 6px;
|
margin-top: 3px;
|
"
|
>
|
<div class="photo-tip">
|
<span>支持jpg/png,</span>
|
<span>大小≤5MB,最多</span>
|
<span>上传1张</span>
|
</div>
|
</div> -->
|
</div>
|
|
<!-- 右侧:表单 -->
|
<div class="form-section">
|
<n-grid :cols="3" :x-gap="24" :y-gap="8">
|
<!-- 车辆VIN码 -->
|
<n-gi>
|
<n-form-item label="车辆VIN码" path="vin">
|
<n-input
|
v-model:value="formModel.vin"
|
placeholder="请输入"
|
clearable
|
maxlength="17"
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<!-- 车牌编码 -->
|
<n-gi>
|
<n-form-item label="车牌编码" path="vehicleCode">
|
<n-input
|
v-model:value="formModel.vehicleCode"
|
placeholder="请输入"
|
clearable
|
maxlength="5"
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<!-- 车牌号 -->
|
<n-gi>
|
<n-form-item label="车牌号" path="plateNumber">
|
<n-input
|
v-model:value="formModel.plateNumber"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<!-- 型号 -->
|
<n-gi>
|
<n-form-item label="型号" path="model">
|
<n-select
|
v-model:value="formModel.model"
|
:options="modelOptions"
|
placeholder="请选择"
|
clearable
|
@update:show="isModelSelectOpen = $event"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class="arrow-icon"
|
:class="{
|
'arrow-icon--open': isModelSelectOpen,
|
}"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataIcon"
|
style="width: 50%"
|
alt="no-data"
|
>
|
</div>
|
</template>
|
</n-select>
|
</n-form-item>
|
</n-gi>
|
|
<!-- 车辆颜色 -->
|
<n-gi>
|
<n-form-item label="车辆颜色" path="vehicleColor">
|
<n-input
|
v-model:value="formModel.vehicleColor"
|
placeholder="请输入"
|
clearable
|
maxlength="3"
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<!-- 采购来源 -->
|
<n-gi>
|
<n-form-item label="采购来源" path="purchaseSource">
|
<n-input
|
v-model:value="formModel.purchaseSource"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<!-- 备注 -->
|
<n-gi>
|
<n-form-item label="备注" path="remark">
|
<n-input
|
v-model:value="formModel.remark"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<!-- 运营区域 -->
|
<n-gi>
|
<n-form-item label="运营区域" path="operationArea">
|
<n-select
|
v-model:value="formModel.operationArea"
|
:options="operationAreaOptions"
|
placeholder="请选择"
|
filterable
|
clearable
|
@update:show="isOperationAreaSelectOpen = $event"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class="arrow-icon"
|
:class="{
|
'arrow-icon--open': isOperationAreaSelectOpen,
|
}"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataIcon"
|
style="width: 50%"
|
alt="no-data"
|
>
|
</div>
|
</template>
|
</n-select>
|
</n-form-item>
|
</n-gi>
|
|
<!-- 车辆属性 -->
|
<n-gi>
|
<n-form-item label="车辆属性" path="vehicleAttribute">
|
<n-select
|
v-model:value="formModel.vehicleAttribute"
|
:options="vehicleAttributeOptions"
|
placeholder="请选择"
|
filterable
|
clearable
|
@update:show="isVehicleAttributeSelectOpen = $event"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class="arrow-icon"
|
:class="{
|
'arrow-icon--open': isVehicleAttributeSelectOpen,
|
}"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataIcon"
|
style="width: 50%"
|
alt="no-data"
|
>
|
</div>
|
</template>
|
</n-select>
|
</n-form-item>
|
</n-gi>
|
</n-grid>
|
</div>
|
</div>
|
</n-form>
|
</n-card>
|
|
<n-card :bordered="false" class="section-card">
|
<!-- <div class="section-header">
|
<div class="section-title">
|
补充信息
|
</div>
|
</div> -->
|
<n-form
|
ref="supplementFormRef"
|
:model="formModel"
|
:rules="formRules"
|
label-placement="left"
|
in-line
|
require-mark-placement="left"
|
>
|
<n-grid :cols="3" :x-gap="24" :y-gap="8">
|
<n-gi>
|
<n-form-item label="车辆类型" path="vehicleModelType">
|
<n-select
|
v-model:value="formModel.vehicleModelType"
|
:options="vehicleModelTypeOptions"
|
placeholder="请选择"
|
clearable
|
@update:show="isVehicleModelTypeSelectOpen = $event"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class="arrow-icon"
|
:class="{
|
'arrow-icon--open': isVehicleModelTypeSelectOpen,
|
}"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataIcon"
|
style="width: 50%"
|
alt="no-data"
|
>
|
</div>
|
</template>
|
</n-select>
|
</n-form-item>
|
</n-gi>
|
|
<n-gi>
|
<n-form-item label="所有人类型" path="ownerType">
|
<n-radio-group v-model:value="formModel.ownerType">
|
<n-space>
|
<n-radio
|
v-for="item in ownerTypeOptions"
|
:key="item.value"
|
:value="item.value"
|
>
|
{{ item.label }}
|
</n-radio>
|
</n-space>
|
</n-radio-group>
|
</n-form-item>
|
</n-gi>
|
|
<n-gi>
|
<n-form-item label="车辆所有人" path="ownerName">
|
<n-input
|
v-model:value="formModel.ownerName"
|
placeholder="请输入企业/车主名称"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<n-gi>
|
<n-form-item label="驱动电机编号" path="driveMotorNo">
|
<n-input
|
v-model:value="formModel.driveMotorNo"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<n-gi>
|
<n-form-item label="车辆注册日期" path="registerDate">
|
<n-date-picker
|
v-model:value="formModel.registerDate"
|
type="date"
|
placeholder="请选择"
|
clearable
|
format="yyyy/MM/dd"
|
:actions="[]"
|
style="width: 100%"
|
/>
|
</n-form-item>
|
</n-gi>
|
|
<n-gi>
|
<n-form-item label="驱动电机峰值功率" path="driveMotorPeakPower">
|
<n-input
|
v-model:value="formModel.driveMotorPeakPower"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
</n-grid>
|
|
<n-grid :cols="3" :x-gap="24" :y-gap="8" class="mt-8px">
|
<n-gi>
|
<n-form-item label="年检到期日期" path="inspectionExpireDate">
|
<n-date-picker
|
v-model:value="formModel.inspectionExpireDate"
|
type="date"
|
placeholder="请选择"
|
clearable
|
format="yyyy/MM/dd"
|
:actions="[]"
|
style="width: 100%"
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="车辆轴距" path="wheelBase">
|
<n-input
|
v-model:value="formModel.wheelBase"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="网约车发票打印设备序列号" path="invoiceDeviceNo">
|
<n-input
|
v-model:value="formModel.invoiceDeviceNo"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
</n-grid>
|
|
<n-form-item
|
label="年检资料附件"
|
path="inspectionFiles"
|
class="upload-block mt-8px"
|
>
|
<div class="file-upload-wrapper">
|
<div class="file-upload-container">
|
<n-upload
|
v-model:file-list="formModel.inspectionFiles"
|
:max="5"
|
accept=".pdf"
|
:custom-request="
|
(options) => handleUpload(options, 'inspectionFiles')
|
"
|
:on-before-upload="beforePdfUpload"
|
:show-file-list="false"
|
>
|
<div style="display: flex; align-items: center">
|
<n-button size="small" class="file-upload-btn">
|
<template #icon>
|
<svg
|
xmlns="http://www.w3.org/2000/svg"
|
width="14"
|
height="14"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2"
|
stroke-linecap="round"
|
stroke-linejoin="round"
|
>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
<polyline points="17 8 12 3 7 8" />
|
<line x1="12" y1="3" x2="12" y2="15" />
|
</svg>
|
</template>
|
<span style="font-size: 13px;color: #171F26;">点击上传</span>
|
</n-button>
|
<span class="file-upload-hint">仅支持PDF,大小≤5MB,最多上传5个</span>
|
</div>
|
</n-upload>
|
</div>
|
<div
|
v-if="formModel.inspectionFiles.length > 0"
|
class="custom-file-list"
|
>
|
<div
|
v-for="(file, index) in formModel.inspectionFiles"
|
:key="file.id || index"
|
class="custom-file-item"
|
>
|
<img
|
class="file-icon"
|
:src="getFileIcon(file.name)"
|
alt="file-icon"
|
style="width: 24px; height: 24px"
|
>
|
<span class="file-name">{{ file.name }}</span>
|
<span class="file-close" @click="removeFile(index, 'inspectionFiles')">×</span>
|
</div>
|
</div>
|
</div>
|
</n-form-item>
|
|
<div class="sub-section-title">
|
卫星定位装置信息
|
</div>
|
|
<n-grid :cols="4" :x-gap="24" :y-gap="8">
|
<n-gi>
|
<n-form-item label="品牌" path="gpsBrand">
|
<n-input
|
v-model:value="formModel.gpsBrand"
|
placeholder="默认品牌名称"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="型号" path="gpsModel">
|
<n-input
|
v-model:value="formModel.gpsModel"
|
placeholder="默认型号名称"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="IMEI号" path="gpsImei">
|
<n-input
|
v-model:value="formModel.gpsImei"
|
placeholder="默认IMEI号"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="安装日期" path="gpsInstallDate">
|
<n-date-picker
|
v-model:value="formModel.gpsInstallDate"
|
type="date"
|
placeholder="请选择"
|
clearable
|
format="yyyy/MM/dd"
|
:actions="[]"
|
style="width: 100%"
|
/>
|
</n-form-item>
|
</n-gi>
|
</n-grid>
|
</n-form>
|
</n-card>
|
|
<n-card :bordered="false" class="section-card">
|
<div class="section-header">
|
<div class="section-title">
|
保险信息
|
</div>
|
</div>
|
<n-form
|
ref="completeInfoFormRef"
|
:model="formModel"
|
:rules="formRules"
|
label-placement="left"
|
in-line
|
require-mark-placement="left"
|
>
|
<n-grid :cols="3" :x-gap="24" :y-gap="8">
|
<n-gi>
|
<n-form-item label="保险公司名称" path="insuranceCompany">
|
<n-input
|
v-model:value="formModel.insuranceCompany"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="保险号" path="insuranceNo">
|
<n-input
|
v-model:value="formModel.insuranceNo"
|
placeholder="请输入"
|
clearable
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="保险类型" path="insuranceType">
|
<n-select
|
v-model:value="formModel.insuranceType"
|
:options="insuranceTypeOptions"
|
placeholder="请选择"
|
clearable
|
@update:show="isInsuranceTypeSelectOpen = $event"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
class="arrow-icon"
|
:class="{
|
'arrow-icon--open': isInsuranceTypeSelectOpen,
|
}"
|
style="width: 16px; height: 16px"
|
alt="arrow"
|
>
|
</template>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataIcon"
|
style="width: 50%"
|
alt="no-data"
|
>
|
</div>
|
</template>
|
</n-select>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="保险金额" path="insuranceAmount">
|
<n-input
|
v-model:value="formModel.insuranceAmount"
|
placeholder="请输入"
|
clearable
|
:allow-input="onlyAllowNumberAndDash"
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="保险生效日期" path="insuranceEffectiveTime">
|
<n-date-picker
|
v-model:value="formModel.insuranceEffectiveTime"
|
type="date"
|
placeholder="请输入"
|
clearable
|
format="yyyy/MM/dd"
|
:actions="[]"
|
style="width: 100%"
|
/>
|
</n-form-item>
|
</n-gi>
|
<n-gi>
|
<n-form-item label="保险到期日期" path="insuranceEndTime">
|
<n-date-picker
|
v-model:value="formModel.insuranceEndTime"
|
type="date"
|
placeholder="请输入"
|
clearable
|
format="yyyy/MM/dd"
|
:actions="[]"
|
style="width: 100%"
|
/>
|
</n-form-item>
|
</n-gi>
|
</n-grid>
|
|
<!-- 保险资料 - 单独一行 -->
|
<n-form-item
|
label="保险资料"
|
path="insuranceFiles"
|
class="upload-block mt-8px"
|
>
|
<div class="file-upload-wrapper">
|
<div class="file-upload-container">
|
<n-upload
|
v-model:file-list="formModel.insuranceFiles"
|
:max="5"
|
accept=".pdf"
|
:custom-request="
|
(options) => handleUpload(options, 'insuranceFiles')
|
"
|
:on-before-upload="beforePdfUpload"
|
:show-file-list="false"
|
>
|
<div style="display: flex; align-items: center">
|
<n-button size="small" class="file-upload-btn">
|
<template #icon>
|
<svg
|
xmlns="http://www.w3.org/2000/svg"
|
width="14"
|
height="14"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2"
|
stroke-linecap="round"
|
stroke-linejoin="round"
|
>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
<polyline points="17 8 12 3 7 8" />
|
<line x1="12" y1="3" x2="12" y2="15" />
|
</svg>
|
</template>
|
<span style="font-size: 13px;color: #171F26;">点击上传</span>
|
</n-button>
|
<span class="file-upload-hint">仅支持PDF,大小≤5MB,最多上传5个</span>
|
</div>
|
</n-upload>
|
</div>
|
<!-- 自定义文件列表 -->
|
<div
|
v-if="formModel.insuranceFiles.length > 0"
|
class="custom-file-list"
|
>
|
<div
|
v-for="(file, index) in formModel.insuranceFiles"
|
:key="file.id || index"
|
class="custom-file-item"
|
>
|
<img
|
class="file-icon"
|
:src="getFileIcon(file.name)"
|
alt="file-icon"
|
style="width: 24px; height: 24px"
|
>
|
<span class="file-name">{{ file.name }}</span>
|
<span class="file-close" @click="removeFile(index, 'insuranceFiles')">×</span>
|
</div>
|
</div>
|
</div>
|
</n-form-item>
|
</n-form>
|
</n-card>
|
|
<n-card :bordered="false" class="section-card">
|
<div class="section-header">
|
<div class="section-title">
|
其他信息
|
</div>
|
</div>
|
<n-form
|
ref="otherInfoFormRef"
|
:model="formModel"
|
:rules="formRules"
|
label-placement="left"
|
in-line
|
require-mark-placement="left"
|
>
|
<n-grid :cols="3" :x-gap="24" :y-gap="8">
|
<n-gi>
|
<n-form-item
|
label="网约车运营到期时间"
|
path="onlineCarLicenseExpireDate"
|
>
|
<n-date-picker
|
v-model:value="formModel.onlineCarLicenseExpireDate"
|
type="date"
|
placeholder="请输入"
|
clearable
|
format="yyyy/MM/dd"
|
:actions="[]"
|
style="width: 100%"
|
/>
|
</n-form-item>
|
</n-gi>
|
</n-grid>
|
|
<n-form-item
|
label="网约车运营资质"
|
path="onlineCarLicenseFiles"
|
class="upload-block"
|
>
|
<div class="file-upload-wrapper">
|
<div class="file-upload-container">
|
<n-upload
|
v-model:file-list="formModel.onlineCarLicenseFiles"
|
:max="5"
|
accept=".pdf"
|
:custom-request="
|
(options) => handleUpload(options, 'onlineCarLicenseFiles')
|
"
|
:on-before-upload="beforePdfUpload"
|
:show-file-list="false"
|
>
|
<div style="display: flex; align-items: center">
|
<n-button size="small" class="file-upload-btn">
|
<template #icon>
|
<svg
|
xmlns="http://www.w3.org/2000/svg"
|
width="14"
|
height="14"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2"
|
stroke-linecap="round"
|
stroke-linejoin="round"
|
>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
<polyline points="17 8 12 3 7 8" />
|
<line x1="12" y1="3" x2="12" y2="15" />
|
</svg>
|
</template>
|
<span style="font-size: 13px;color: #171F26;">点击上传</span>
|
</n-button>
|
<span class="file-upload-hint">仅支持PDF,大小≤5MB,最多上传5个</span>
|
</div>
|
</n-upload>
|
</div>
|
<div
|
v-if="formModel.onlineCarLicenseFiles.length > 0"
|
class="custom-file-list"
|
>
|
<div
|
v-for="(file, index) in formModel.onlineCarLicenseFiles"
|
:key="file.id || index"
|
class="custom-file-item"
|
>
|
<img
|
class="file-icon"
|
:src="getFileIcon(file.name)"
|
alt="file-icon"
|
style="width: 24px; height: 24px"
|
>
|
<span class="file-name">{{ file.name }}</span>
|
<span class="file-close" @click="removeFile(index, 'onlineCarLicenseFiles')">×</span>
|
</div>
|
</div>
|
</div>
|
</n-form-item>
|
</n-form>
|
</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">
|
取消
|
</n-button>
|
<n-button
|
type="primary"
|
class="action-btn"
|
:loading="saving"
|
@click="handleSave"
|
>
|
保存
|
</n-button>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.page-container {
|
display: flex;
|
flex-direction: column;
|
background: #f5f5f5;
|
}
|
|
.content-wrapper {
|
flex: 1;
|
overflow-y: auto;
|
padding-bottom: 80px;
|
}
|
|
.section-card {
|
background: #fff;
|
border-radius: 6px;
|
margin-bottom: 12px;
|
}
|
|
.section-card:last-child {
|
margin-bottom: 0;
|
}
|
|
:deep(.n-card__content) {
|
padding: 16px 24px;
|
}
|
|
.section-header {
|
margin-bottom: 16px;
|
}
|
|
.section-title {
|
position: relative;
|
padding-left: 12px;
|
font-size: 16px;
|
font-weight: 500;
|
color: #000;
|
line-height: 22px;
|
}
|
|
.section-title::before {
|
content: "";
|
position: absolute;
|
left: 0;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 4px;
|
height: 16px;
|
/* background: #0068FF; */
|
background-color: #0068FF;
|
border-radius: 2px;
|
}
|
|
.sub-section-title {
|
margin: 16px 0 12px;
|
font-size: 14px;
|
font-weight: 500;
|
color: #171f26;
|
}
|
|
/* 车辆信息布局 */
|
.vehicle-info-layout {
|
display: flex;
|
gap: 16px;
|
}
|
|
.photo-section {
|
flex-shrink: 0;
|
width: 140px;
|
display: flex;
|
}
|
|
.photo-label {
|
font-size: 13px;
|
color: #000;
|
margin-right: 8px;
|
line-height: 22px;
|
}
|
:deep(.n-base-selection-input){
|
font-size: 13px !important;
|
}
|
|
.photo-upload-area {
|
display: flex;
|
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;
|
justify-content: flex-start;
|
font-size: 12px;
|
color: #999;
|
line-height: 18px;
|
}
|
|
.form-section {
|
flex: 1;
|
min-width: 0;
|
}
|
|
:deep(.n-form-item) {
|
margin-bottom: 0;
|
}
|
|
:deep(.n-form-item-label) {
|
padding-right: 12px;
|
}
|
|
:deep(.n-form-item-label__asterisk) {
|
color: #ff4d4f;
|
}
|
|
/* 完善信息表单布局 */
|
.form-row {
|
margin-bottom: 8px;
|
}
|
|
.form-row-group {
|
display: flex;
|
justify-content: flex-start;
|
gap: 24px;
|
margin-bottom: 8px;
|
}
|
|
.form-item-inline {
|
flex: 0 0 auto;
|
margin-bottom: 0;
|
}
|
|
.form-item-inline :deep(.n-form-item-label) {
|
/* width: 100px; */
|
}
|
|
.upload-row {
|
margin-bottom: 8px;
|
}
|
|
:deep(.n-input),
|
:deep(.n-select),
|
:deep(.n-date-picker) {
|
font-size: 13px;
|
}
|
|
/* 上传组件样式 */
|
.upload-item {
|
align-items: flex-start;
|
}
|
|
.upload-wrapper {
|
width: 100%;
|
}
|
|
.upload-tip {
|
font-size: 12px;
|
color: #999;
|
margin-bottom: 8px;
|
line-height: 20px;
|
}
|
|
.photo-upload :deep(.n-upload-trigger) {
|
width: 56px;
|
height: 56px;
|
}
|
|
.photo-upload :deep(.n-upload-file-list) {
|
display: flex;
|
gap: 8px;
|
}
|
|
.photo-upload :deep(.n-upload-file) {
|
width: 56px;
|
height: 56px;
|
}
|
|
.photo-upload :deep(.n-upload) {
|
width: 56px;
|
}
|
.photo-upload :deep(.n-upload-dragger) {
|
border: none;
|
}
|
|
/* 文件上传样式 */
|
.file-upload-wrapper {
|
width: 100%;
|
}
|
|
.file-upload-container {
|
display: flex;
|
align-items: center;
|
justify-content: flex-start;
|
gap: 8px;
|
width: 100%;
|
}
|
|
:deep(.n-upload __upload-17kgb7r) {
|
width: 56px;
|
}
|
|
.file-upload-btn {
|
flex-shrink: 0;
|
}
|
|
.file-upload-hint {
|
font-size: 12px;
|
color: #999;
|
line-height: 20px;
|
margin-left: 8px;
|
}
|
|
/* 自定义文件列表 */
|
.custom-file-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
margin-top: 12px;
|
}
|
|
.custom-file-item {
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
padding: 6px 12px;
|
/* border: 1px solid #d9d9d9; */
|
border-radius: 4px;
|
font-size: 13px;
|
color: #333;
|
max-width: 280px;
|
transition: all 0.3s;
|
background-color: #f4f6f8;
|
padding: 8px 12px;
|
}
|
|
.custom-file-item:hover {
|
border-color: #1890ff;
|
background: #fff;
|
}
|
|
.file-icon {
|
font-size: 18px;
|
line-height: 1;
|
flex-shrink: 0;
|
}
|
|
.file-name {
|
flex: 1;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
max-width: 200px;
|
}
|
|
.file-close {
|
font-size: 18px;
|
color: #999;
|
cursor: pointer;
|
line-height: 1;
|
flex-shrink: 0;
|
padding: 0 2px;
|
transition: color 0.3s;
|
}
|
|
.file-close:hover {
|
color: #ff4d4f;
|
}
|
|
.upload-btn {
|
color: #1890ff;
|
font-size: 13px;
|
padding: 0;
|
}
|
|
.upload-btn:hover {
|
color: #40a9ff;
|
}
|
|
.upload-icon-text {
|
font-size: 16px;
|
margin-right: 4px;
|
}
|
|
.upload-hint {
|
font-size: 12px;
|
color: #999;
|
margin-top: 4px;
|
line-height: 20px;
|
}
|
|
:deep(.n-upload-file-list .n-upload-file) {
|
padding: 8px 12px;
|
background: #fafafa;
|
border-radius: 2px;
|
margin-top: 8px;
|
}
|
|
:deep(.n-upload-file-list .n-upload-file:first-child) {
|
margin-top: 0;
|
}
|
|
/* 底部固定按钮 */
|
.form-actions {
|
position: absolute;
|
bottom: 0;
|
left: 0;
|
right: 0;
|
height: 64px;
|
background: #fff;
|
border-top: 1px solid #e8e8e8;
|
display: flex;
|
align-items: center;
|
justify-content: flex-end;
|
padding: 0 24px;
|
gap: 12px;
|
z-index: 999;
|
}
|
|
.action-btn {
|
min-width: 88px;
|
height: 32px;
|
font-size: 13px;
|
}
|
</style>
|