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:
zlwuandCursor committed 2026-08-02 10:34:41 +08:00
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>