feat: 车辆运维字段优化
Co-Authored-By: 武世龙 <wusl13@xiaopeng.com>
| | |
| | | [ |
| | | ".kiro/lark-doc-implement", |
| | | ".kiro/lark-wiki-dev", |
| | | ".kiro/lark-wiki-dev_084245" |
| | | ".kiro/lark-wiki-dev_084245", |
| | | ".kiro/lark-wiki-fetch" |
| | | ] |
| | |
| | | <script setup lang="ts"> |
| | | import { NImage, NModal } from 'naive-ui' |
| | | import { NImage, NModal, NTag } from 'naive-ui' |
| | | import { computed } from 'vue' |
| | | import dayjs from 'dayjs' |
| | | |
| | |
| | | function handleClose() { |
| | | emit('update:show', false) |
| | | } |
| | | |
| | | /** |
| | | * 车辆运维字段优化:AEB调至"中" 胶囊状态(只读回显,不可编辑) |
| | | * |
| | | * 状态枚举:0=历史无数据(存量/老版本未上报,DB DEFAULT)、1=检测完成、2=待检测。 |
| | | * 判定规则:检测完成 = (aebMiddle === 1) || (存在照片) |
| | | * |
| | | * 为什么要「有照片」兜底:公司 DDL 门禁要求新增 int 列显式 NOT NULL, |
| | | * 故 `aeb_middle` 为 NOT NULL DEFAULT 0——"老版本 APP 只传照片、不传状态"的记录会落为 0, |
| | | * 此时以有照片判定为检测完成,与 APP 端"识别到照片上传自动流转"的语义保持一致。 |
| | | * 注:枚举里 1 固定表示"检测完成"(与同表既有检测项 1=pass 对齐),故此处判 === 1。 |
| | | */ |
| | | const aebMiddleDone = computed(() => { |
| | | const status = props.detailData?.aebMiddleStatus |
| | | if (status !== null && status !== undefined && Number(status) === 1) |
| | | return true |
| | | return Boolean(props.detailData?.aebMiddleImages?.length) |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- |
| | | 车辆运维字段优化:AEB调至"中" |
| | | 回显胶囊状态(不可编辑)+ 照片原图;状态优先取后端 aebMiddle,为空(历史数据)时按有无照片派生 |
| | | --> |
| | | <div class="manual-item"> |
| | | <div class="check-label"> |
| | | AEB调至"中": |
| | | </div> |
| | | <div class="image-list"> |
| | | <NTag :bordered="false" :type="aebMiddleDone ? 'success' : 'default'" size="small"> |
| | | {{ aebMiddleDone ? '检测完成' : '待检测' }} |
| | | </NTag> |
| | | <template v-if="detailData.aebMiddleImages && detailData.aebMiddleImages.length"> |
| | | <NImage |
| | | v-for="(img, index) in detailData.aebMiddleImages" |
| | | :key="index" |
| | | :src="img || 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png'" |
| | | :width="56" |
| | | :height="56" |
| | | object-fit="cover" |
| | | fallback-src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" |
| | | class="check-image" |
| | | /> |
| | | </template> |
| | | <span v-else class="no-data">-</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 车辆运维字段优化:二排座椅调节(按需求仅回显照片) --> |
| | | <div class="manual-item"> |
| | | <div class="check-label"> |
| | | 二排座椅调节: |
| | | </div> |
| | | <div class="image-list"> |
| | | <template v-if="detailData.secondRowSeatImages && detailData.secondRowSeatImages.length"> |
| | | <NImage |
| | | v-for="(img, index) in detailData.secondRowSeatImages" |
| | | :key="index" |
| | | :src="img || 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png'" |
| | | :width="56" |
| | | :height="56" |
| | | object-fit="cover" |
| | | fallback-src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" |
| | | class="check-image" |
| | | /> |
| | | </template> |
| | | <span v-else class="no-data">-</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 支付功能 --> |
| | | <!-- <div class="manual-item"> |
| | | <div class="check-label"> |
| | |
| | | emit("update:show", false); |
| | | } |
| | | |
| | | /** |
| | | * 车辆运维字段优化:车内摄像头分组 |
| | | * 「车内摄像头」标签下分摄像头1/2/3 三行展示,逐个可追溯; |
| | | * 数据由 detail.vue 用 collectImageUrls 解析后传入,历史数据为空时模板显示 "-" |
| | | */ |
| | | const cameraGroups = computed(() => [ |
| | | { label: '摄像头1', images: props.detailData?.camera1Images ?? [] }, |
| | | { label: '摄像头2', images: props.detailData?.camera2Images ?? [] }, |
| | | { label: '摄像头3', images: props.detailData?.camera3Images ?? [] }, |
| | | ]) |
| | | |
| | | function formatPressure(val: any) { |
| | | if (val === null || val === undefined || val === "") return "-"; |
| | | const num = Number(val); |
| | |
| | | </div> |
| | | <div class="value" style="min-width: 200px"> |
| | | 右后轮:{{ formatPressure(detailData.tirePressures[3].value) }} |
| | | </div> |
| | | </div> |
| | | <!-- 车辆运维字段优化:胎压照片(最多2张,无数据显示 "-") --> |
| | | <div class="flex mt-12px"> |
| | | <div style="flex: 1"> |
| | | <div class="label">胎压照片</div> |
| | | <div |
| | | class="flex gap-16px" |
| | | v-if="detailData.tirePressureImages && detailData.tirePressureImages.length" |
| | | > |
| | | <div |
| | | class="pic" |
| | | v-for="(img, idx) in detailData.tirePressureImages" |
| | | :key="idx" |
| | | > |
| | | <NImage |
| | | :src="img" |
| | | width="56" |
| | | height="56" |
| | | fallback-src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <div v-else class="value">-</div> |
| | | </div> |
| | | </div> |
| | | <div class="flex mt-12px"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- |
| | | 车辆运维字段优化:车内摄像头 |
| | | 「车内摄像头」一个标签下分 摄像头1/2/3 三行展示,便于追溯具体摄像头; |
| | | 每个摄像头最多 2 张,历史数据为空时显示 "-" |
| | | --> |
| | | <div class="mt-12px"> |
| | | <div class="label">车内摄像头</div> |
| | | <div |
| | | class="mt-8px" |
| | | v-for="cam in cameraGroups" |
| | | :key="cam.label" |
| | | > |
| | | <div class="label">{{ cam.label }}</div> |
| | | <div class="flex gap-16px" v-if="cam.images && cam.images.length"> |
| | | <div class="pic" v-for="(img, idx) in cam.images" :key="idx"> |
| | | <NImage |
| | | :src="img" |
| | | width="56" |
| | | height="56" |
| | | fallback-src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <div v-else class="value">-</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </NModal> |
| | |
| | | { title: '序号', key: 'index', align: 'left' as const, width: 80, render: (row: any, index: number) => (cleanPage.value.pageNum - 1) * cleanPage.value.pageSize + index + 1 }, |
| | | { title: '清洁时间', key: 'operationTime', align: 'left' as const, width: 180, render: (row: any) => row.operationTime ? dayjs(row.operationTime).format('YYYY/MM/DD HH:mm:ss') : '-' }, |
| | | { title: '车身标识及清洁', key: 'bodyEmblem', align: 'left' as const, width: 160, render: (row: any) => renderImageCell(row.bodyEmblem) }, |
| | | { title: '车损', key: 'vehicleDamage', align: 'left' as const, width: 180, render: (row: any) => renderVehicleDamageCell(row) }, |
| | | { title: '操作人', key: 'operationsUserName', align: 'left' as const, width: 140 }, |
| | | { |
| | | title: '操作', |
| | |
| | | return <span>-</span> |
| | | return ( |
| | | <NSpace size="small" justify="flex-start"> |
| | | {urls.map((u: string, idx: number) => ( |
| | | <NImage |
| | | key={idx} |
| | | src={u || 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png'} |
| | | width={40} |
| | | height={40} |
| | | object-fit="cover" |
| | | fallback-src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png" |
| | | style={{ borderRadius: '4px', cursor: 'pointer' }} |
| | | /> |
| | | ))} |
| | | </NSpace> |
| | | ) |
| | | } |
| | | |
| | | /** |
| | | * 车辆运维字段优化:车身清洁列表「车损」列 |
| | | * 需求:回显车损照片(原图);若 APP 选"否"未上传照片,后台回显 "-"。 |
| | | * vehicleDamage: 0=否 1=是(历史数据为 null,按"否"处理) |
| | | */ |
| | | function renderVehicleDamageCell(row: any) { |
| | | const isDamaged = Number(row?.vehicleDamage) === 1 |
| | | const urls = collectImageUrls(row?.vehicleDamagePicture) |
| | | // 选"否"或无照片:按需求统一显示 "-" |
| | | if (!isDamaged && !urls.length) |
| | | return <span>-</span> |
| | | return ( |
| | | <NSpace size="small" justify="start" align="center"> |
| | | <span>{isDamaged ? '是' : '否'}</span> |
| | | {urls.map((u: string, idx: number) => ( |
| | | <NImage |
| | | key={idx} |
| | |
| | | orderSystemCheckImages: collectImageUrls(row.systemCheckPicture), |
| | | interactiveScreenImages: collectImageUrls(row.interactiveScreenPicture), |
| | | paymentCheckImages: collectImageUrls(row.paymentFunctionPicture), |
| | | // 车辆运维字段优化:AEB调至"中"(状态+照片)、二排座椅调节(照片) |
| | | // 胶囊状态优先取后端 aebMiddle(0待检测/1检测完成),为空(历史数据)时按有无照片派生 |
| | | aebMiddleImages: collectImageUrls(row.aebMiddlePicture), |
| | | aebMiddleStatus: row.aebMiddle ?? null, |
| | | secondRowSeatImages: collectImageUrls(row.secondRowSeatPicture), |
| | | vehicleLocation: row.vehicleLocation || '-', |
| | | operator: row.operatorStartName || '-', |
| | | detectionTime: row.operationTime, |
| | |
| | | // bodyCleanImages: collectImageUrls(row.bodyEmblem), |
| | | interiorCleanImages: collectImageUrls(row.cabinCleaning), |
| | | lostItemImages: collectImageUrls(row.lostItems), |
| | | // 车辆运维字段优化:胎压照片 + 车内摄像头1/2/3(历史数据为空时弹窗内显示 "-") |
| | | tirePressureImages: collectImageUrls(row.tirePressurePicture), |
| | | camera1Images: collectImageUrls(row.camera1Picture), |
| | | camera2Images: collectImageUrls(row.camera2Picture), |
| | | camera3Images: collectImageUrls(row.camera3Picture), |
| | | vehicleLocation: row.vehicleLocation || '-', |
| | | lostItemRemark: row.lostItemsRemark || row.lostRemark || row.lostDescribe || '', |
| | | operator: row.operationsUserName || '-', |
| | |
| | | getVehicleDescription, |
| | | saveVehicleDescription, |
| | | } from '@/service/api/contentCenter' |
| | | import VehiclePrepareForm from './components/VehiclePrepareForm.vue' |
| | | // 车辆运维字段优化:整备标准说明收敛为单一富文本后,不再使用 VehiclePrepareForm(子分项表单) |
| | | // 该组件文件暂予保留,如后续确认无回滚需求可一并删除 |
| | | import SimpleRichTextForm from './components/SimpleRichTextForm.vue' |
| | | import AgreementListForm from './components/AgreementListForm.vue' |
| | | |
| | |
| | | const data = currentForm.getData() |
| | | let saveRes: any = null |
| | | |
| | | if (activeTab.value === 'prepare') { |
| | | const map: Record<string, number> = { |
| | | tirePressure: 1, |
| | | lighting: 2, |
| | | cabinClean: 4, |
| | | } |
| | | const activeKey = typeof currentForm.getActiveKey === 'function' ? currentForm.getActiveKey() : 'tirePressure' |
| | | const t = map[activeKey] ?? 1 |
| | | const content = data[activeKey] || '' |
| | | saveRes = await saveVehicleDescription({ descriptionType: t, descriptionContent: content }) |
| | | } |
| | | else if (['charge', 'inspect', 'bodyClean', 'insurance', 'annual', 'maintenance', 'qualification'].includes(activeTab.value)) { |
| | | if (['prepare', 'charge', 'inspect', 'bodyClean', 'insurance', 'annual', 'maintenance', 'qualification'].includes(activeTab.value)) { |
| | | const typeMap: Record<string, number> = { |
| | | // 车辆运维字段优化:车辆整备标准说明收敛为单一富文本,写入新增的 descriptionType=12 |
| | | // (原按胎压1/灯光2/舱内清洁4 三个子分项配置,收敛后不再区分模块) |
| | | prepare: 12, |
| | | charge: 6, |
| | | inspect: 7, |
| | | // 车辆清洁标准说明复用既有 type 3(车身标识及清洁),不新增类型,避免与存量文案数据分裂 |
| | | bodyClean: 3, |
| | | insurance: 8, |
| | | annual: 9, |
| | |
| | | return |
| | | } |
| | | |
| | | if (tabKey === 'prepare') { |
| | | const types = [1, 2, 4] |
| | | const res = await Promise.all(types.map(t => getVehicleDescription(t))) |
| | | if (res.every(r => r && r.code === 200)) { |
| | | const data: Record<string, string> = { |
| | | tirePressure: res[0].data?.descriptionContent || '', |
| | | lighting: res[1].data?.descriptionContent || '', |
| | | cabinClean: res[2].data?.descriptionContent || '', |
| | | } |
| | | currentForm.setData(data) |
| | | } |
| | | } |
| | | else if (['charge', 'inspect', 'bodyClean', 'insurance', 'annual', 'maintenance', 'qualification'].includes(tabKey)) { |
| | | if (['prepare', 'charge', 'inspect', 'bodyClean', 'insurance', 'annual', 'maintenance', 'qualification'].includes(tabKey)) { |
| | | const typeMap: Record<string, number> = { |
| | | // 车辆运维字段优化:整备读取收敛后的单一富文本 type=12 |
| | | prepare: 12, |
| | | charge: 6, |
| | | inspect: 7, |
| | | bodyClean: 3, |
| | |
| | | > |
| | | <n-tab-pane v-for="tab in tabs" :key="tab.key" :name="tab.key" :tab="tab.label"> |
| | | <div class="tab-scroll-area"> |
| | | <VehiclePrepareForm |
| | | v-if="tab.key === 'prepare'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <!-- |
| | | 车辆运维字段优化:车辆整备标准说明已收敛为单一富文本(descriptionType=12), |
| | | 与车辆清洁(3)/车辆检测(7)等一致复用 SimpleRichTextForm,不再按子分项配置 |
| | | --> |
| | | <SimpleRichTextForm |
| | | v-else-if="['charge', 'inspect', 'bodyClean', 'insurance', 'annual', 'maintenance', 'qualification'].includes(tab.key)" |
| | | v-if="['prepare', 'charge', 'inspect', 'bodyClean', 'insurance', 'annual', 'maintenance', 'qualification'].includes(tab.key)" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <AgreementListForm |