ai-coding
2026-08-20 8ffaad6d2d69a81ace44177580bfca2610be858f
Merge origin/feat-0819-opt into feat/deploy-uat

# Conflicts:
# .kiro/lark-wiki-fetch/specs/design.md
# .kiro/lark-wiki-fetch/specs/requirements.md
# .kiro/lark-wiki-fetch/specs/summary.md
# .kiro/lark-wiki-fetch/specs/tasks.md
# .kiro/lark-wiki-fetch/tests/TC-001-01-no-permission.png
# .kiro/lark-wiki-fetch/tests/TC-002-01-homepage.png
# .kiro/lark-wiki-fetch/tests/TC-002-01-system-config.png
# .kiro/lark-wiki-fetch/tests/TC-002-02-system-config.png
# .kiro/lark-wiki-fetch/tests/TC-003-01-account-management.png
# .kiro/lark-wiki-fetch/tests/TC-005-01-add-vehicle-step1.png
# .kiro/lark-wiki-fetch/tests/TC-005-01-add-vehicle.png
# .kiro/lark-wiki-fetch/tests/TC-006-01-test-plan-list.png
# .kiro/lark-wiki-fetch/tests/TC-007-01-final-state.png
# .kiro/lark-wiki-fetch/tests/TC-007-01-initial-load.png
# .kiro/lark-wiki-fetch/tests/test-cases.md
# .kiro/lark-wiki-fetch/tests/test-report.md
# .kiro/lark-wiki-fetch/tests/test-results.json
# .kiro/lark-wiki-fetch/tests/ui-test.mjs
# src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue

Co-Authored-By: 武世龙 <wusl13@xiaopeng.com>
4个文件已修改
217 ■■■■■ 已修改文件
src/views/assetManagement/vehicleManagement/operatingVehicle/components/DetectionDetailModal.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/components/PreparationDetailModal.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/opsApp.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/components/DetectionDetailModal.vue
@@ -1,5 +1,5 @@
<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'
@@ -29,6 +29,24 @@
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>
@@ -181,6 +199,56 @@
            </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">
src/views/assetManagement/vehicleManagement/operatingVehicle/components/PreparationDetailModal.vue
@@ -30,6 +30,17 @@
  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);
@@ -104,6 +115,30 @@
          </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">
@@ -247,6 +282,32 @@
            </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>
src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue
@@ -582,6 +582,7 @@
  { 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: 'totalCost', align: 'left' as const, width: 140, render: (row: any) => (row.totalCost != null ? `¥${row.totalCost}` : '-') },
  { title: '车损', key: 'vehicleDamage', align: 'left' as const, width: 180, render: (row: any) => renderVehicleDamageCell(row) },
  { title: '操作人', key: 'operationsUserName', align: 'left' as const, width: 140 },
  {
    title: '操作',
@@ -944,6 +945,35 @@
    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}
@@ -1349,6 +1379,11 @@
      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,
@@ -1415,6 +1450,11 @@
      // 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 || '-',
src/views/userContent/contentCenter/opsApp.vue
@@ -5,7 +5,8 @@
  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'
@@ -35,21 +36,14 @@
      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,
@@ -90,20 +84,10 @@
      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,
@@ -153,12 +137,12 @@
          >
            <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