feat(web-admin): add vehicle photo on tripartite vehicle page
Also include cert attachment list cell already wired in this page WIP. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e2f087e6b9
commit
a552049de4
2 files changed
+278
-5
No files matched your search
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div v-if="items.length" class="pl-cert-list-attachment-cell">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
class="pl-cert-list-attachment-cell__item"
|
||||
:title="item.fullLabel"
|
||||
>
|
||||
<AttachmentUploadField
|
||||
v-if="item.url"
|
||||
:model-value="item.url"
|
||||
disabled
|
||||
:category-labels="item.categoryLabels"
|
||||
/>
|
||||
<div v-else class="pl-cert-list-attachment-cell__empty">
|
||||
<span class="pl-cert-list-attachment-cell__empty-text">未上传</span>
|
||||
<span class="pl-cert-list-attachment-cell__caption">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="pl-cert-list-attachment-cell__none">—</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { CertRead } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { normalizeAttachmentUrls } from '@/utils/download-attachment'
|
||||
import {
|
||||
PL_VEHICLE_CERT_PART_TRAILER,
|
||||
normalizeVehicleCertPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
certList?: CertRead[] | null
|
||||
/** 车辆证书:按主车/挂车加前缀 */
|
||||
showVehiclePart?: boolean
|
||||
}>(),
|
||||
{
|
||||
certList: () => [],
|
||||
showVehiclePart: false
|
||||
}
|
||||
)
|
||||
|
||||
type ItemDef = {
|
||||
key: string
|
||||
label: string
|
||||
fullLabel: string
|
||||
url: string
|
||||
categoryLabels: Record<string, string>
|
||||
}
|
||||
|
||||
function parseUrls(raw?: string | null): string[] {
|
||||
const normalized = normalizeAttachmentUrls(raw)
|
||||
if (!normalized) return []
|
||||
return normalized
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function partPrefix(cert: CertRead, enablePart: boolean): string {
|
||||
if (!enablePart) return ''
|
||||
return normalizeVehicleCertPart(cert.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER ? '挂·' : '主·'
|
||||
}
|
||||
|
||||
const items = computed<ItemDef[]>(() => {
|
||||
const certs = props.certList || []
|
||||
if (!certs.length) return []
|
||||
|
||||
const enablePart =
|
||||
props.showVehiclePart &&
|
||||
certs.some((cert) => normalizeVehicleCertPart(cert.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER)
|
||||
|
||||
const result: ItemDef[] = []
|
||||
certs.forEach((cert, certIndex) => {
|
||||
const certName = String(cert.certName || '').trim() || '证书'
|
||||
const prefix = partPrefix(cert, enablePart)
|
||||
const fullLabel = `${prefix}${certName}`
|
||||
const label = fullLabel.length > 6 ? `${fullLabel.slice(0, 5)}…` : fullLabel
|
||||
const urls = parseUrls(cert.attachmentUrls)
|
||||
|
||||
if (!urls.length) {
|
||||
result.push({
|
||||
key: `${cert.id || cert.certId || certIndex}-empty`,
|
||||
label,
|
||||
fullLabel,
|
||||
url: '',
|
||||
categoryLabels: {}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
urls.forEach((url, urlIndex) => {
|
||||
result.push({
|
||||
key: `${cert.id || cert.certId || certIndex}-${urlIndex}`,
|
||||
label,
|
||||
fullLabel,
|
||||
url,
|
||||
categoryLabels: { [url]: label }
|
||||
})
|
||||
})
|
||||
})
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-cert-list-attachment-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pl-cert-list-attachment-cell__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
.pl-cert-list-attachment-cell__empty {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96px;
|
||||
height: 56px;
|
||||
overflow: hidden;
|
||||
border: 1px dashed var(--border, #d9e8e2);
|
||||
border-radius: 8px;
|
||||
background: var(--table-head, #f8fafc);
|
||||
color: var(--mint-text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pl-cert-list-attachment-cell__empty-text {
|
||||
margin-bottom: 14px;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.pl-cert-list-attachment-cell__caption {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 2px 4px;
|
||||
background: rgba(15, 23, 42, 0.62);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.pl-cert-list-attachment-cell__none {
|
||||
color: var(--mint-text);
|
||||
}
|
||||
|
||||
:deep(.attachment-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.attachment-list) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
:deep(.attachment-item) {
|
||||
width: 96px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.attachment-preview) {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
:deep(.attachment-footer) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -112,6 +112,23 @@
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('photoUrls')"
|
||||
label="照片"
|
||||
min-width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<img
|
||||
v-if="firstPhotoUrl(row.photoUrls)"
|
||||
class="pl-photo-thumb"
|
||||
:src="resolvePhotoDisplayUrl(firstPhotoUrl(row.photoUrls))"
|
||||
alt="车辆照片"
|
||||
/>
|
||||
<span v-else class="pl-photo-thumb pl-photo-thumb--empty">未上传</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="licensePlate"
|
||||
label="车牌号"
|
||||
@@ -132,6 +149,17 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('certAttachments')"
|
||||
label="证书附件"
|
||||
min-width="340"
|
||||
align="left"
|
||||
header-align="left"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlCertListAttachmentCell :cert-list="row.certList" show-vehicle-part />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车辆类型" min-width="100" align="center" header-align="center" v-if="isColumnVisible('vehicleType')">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getVehicleTypeTagType(row.vehicleType)" size="small">
|
||||
@@ -257,6 +285,15 @@
|
||||
maxlength="512"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="车辆照片" style="grid-column: 1 / -1">
|
||||
<AttachmentUploadField
|
||||
ref="photoUploadRef"
|
||||
v-model="formData.photoUrls"
|
||||
:limit="1"
|
||||
accept="image/*"
|
||||
upload-folder="pl/vehicle-photo"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 关联人员子表 -->
|
||||
@@ -375,6 +412,16 @@
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
<template #photo>
|
||||
<AttachmentUploadField
|
||||
v-if="viewDetail?.photoUrls"
|
||||
:model-value="viewDetail.photoUrls"
|
||||
disabled
|
||||
:limit="1"
|
||||
accept="image/*"
|
||||
/>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
<template #vehicleDetailTabs>
|
||||
<PlVehicleCertPersonnelTabs
|
||||
:cert-list="viewDetail?.certList || []"
|
||||
@@ -401,6 +448,7 @@
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { Check, Close, Delete, DocumentCopy, Edit, Plus, RefreshLeft, Search, Van } from '@element-plus/icons-vue'
|
||||
import {
|
||||
@@ -417,6 +465,7 @@ import {
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import PlVehicleCertBadges from '@/components/business/pl-vehicle-cert-badges/index.vue'
|
||||
import PlCertListAttachmentCell from '@/components/business/pl-cert-list-attachment-cell/index.vue'
|
||||
import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import SystemViewDialog, {
|
||||
@@ -427,6 +476,7 @@ import { pickBxList, pickBxPageResult } from '@/utils/http/bx'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { formatPlVehiclePlateDisplay, normalizePlPersonnelSummaryDisplay } from '@/utils/pl-display'
|
||||
import { firstPhotoUrl, resolvePhotoDisplayUrl } from '@/utils/pl-photo'
|
||||
import { ensureVehicleCertSlots } from '@/utils/pl-cert-slots'
|
||||
import {
|
||||
PL_VEHICLE_CERT_NAME_OPTIONS,
|
||||
@@ -452,6 +502,8 @@ const vehicleList = ref<Vehicle[]>([])
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'photoUrls', label: '照片' },
|
||||
{ prop: 'certAttachments', label: '证书附件' },
|
||||
{ prop: 'vehicleType', label: '车辆类型' },
|
||||
{ prop: 'vehicleStatus', label: '车辆状态' },
|
||||
{ prop: 'loadCapacity', label: '载重(吨)' },
|
||||
@@ -466,7 +518,7 @@ const {
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-vehicle-list-columns', listColumnDefs)
|
||||
} = usePersistedListColumns('pl-vehicle-list-columns-v3', listColumnDefs)
|
||||
|
||||
const personnelOptions = ref<Personnel[]>([])
|
||||
const selectedRows = ref<Vehicle[]>([])
|
||||
@@ -474,6 +526,7 @@ const dialogVisible = ref(false)
|
||||
const pasteVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref()
|
||||
const photoUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const isEdit = ref(false)
|
||||
|
||||
const personnelList = ref<VehiclePersonnelRow[]>([])
|
||||
@@ -508,7 +561,8 @@ const formData = reactive({
|
||||
carrierName: '',
|
||||
loadCapacity: null as number | null,
|
||||
vehicleStatus: 'available',
|
||||
remark: ''
|
||||
remark: '',
|
||||
photoUrls: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
@@ -586,7 +640,16 @@ const viewSections = computed<ViewSection[]>(() => {
|
||||
},
|
||||
{ label: '备注', value: d.remark || '—', span: true },
|
||||
{ label: '创建时间', value: formatDateTime(d.createTime) }
|
||||
],
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '车辆照片',
|
||||
fields: [],
|
||||
slot: 'photo'
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
fields: [],
|
||||
slot: 'vehicleDetailTabs'
|
||||
}
|
||||
]
|
||||
@@ -884,7 +947,8 @@ function handleAdd() {
|
||||
carrierName: '',
|
||||
loadCapacity: null,
|
||||
vehicleStatus: 'available',
|
||||
remark: ''
|
||||
remark: '',
|
||||
photoUrls: ''
|
||||
})
|
||||
resetSubTables()
|
||||
certList.value = ensureVehicleCertSlots([], formData.trailerNo, VEHICLE_TYPE_LOCK)
|
||||
@@ -928,7 +992,8 @@ async function handleEdit(row: Vehicle) {
|
||||
carrierName: detail.carrierName,
|
||||
loadCapacity: detail.loadCapacity ?? null,
|
||||
vehicleStatus: detail.vehicleStatus,
|
||||
remark: detail.remark
|
||||
remark: detail.remark,
|
||||
photoUrls: detail.photoUrls || ''
|
||||
})
|
||||
personnelList.value = resolvePersonnelRows(
|
||||
(detail.personnelList || []).map(p => ({
|
||||
@@ -960,6 +1025,10 @@ async function handleSubmit() {
|
||||
ElMessage.warning('证书附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
if (photoUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.carrierId) {
|
||||
const carrier = carrierList.value.find(c => c.id === formData.carrierId)
|
||||
@@ -979,6 +1048,7 @@ async function handleSubmit() {
|
||||
loadCapacity: formData.loadCapacity,
|
||||
vehicleStatus: formData.vehicleStatus,
|
||||
remark: formData.remark,
|
||||
photoUrls: formData.photoUrls || undefined,
|
||||
personnelList: personnelList.value.map(p => ({
|
||||
personnelId: p.personnelId,
|
||||
personnelName: p.personnelName.trim(),
|
||||
@@ -1095,4 +1165,24 @@ onMounted(() => {
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pl-photo-thumb {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--el-border-color, #d9e8e2);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pl-photo-thumb--empty {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user