This commit is contained in:
Mrx
2026-06-04 18:04:21 +08:00
parent c9102f2d51
commit 483fdec6a2
26 changed files with 1282 additions and 193 deletions

View File

@@ -284,6 +284,7 @@ declare global {
const useUserStore: typeof import('./stores/userStore.js')['useUserStore'] const useUserStore: typeof import('./stores/userStore.js')['useUserStore']
const useVModel: typeof import('@vueuse/core')['useVModel'] const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels'] const useVModels: typeof import('@vueuse/core')['useVModels']
const useVehiclePayload: typeof import('./composables/useVehiclePayload.js')['useVehiclePayload']
const useVibrate: typeof import('@vueuse/core')['useVibrate'] const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] const useWakeLock: typeof import('@vueuse/core')['useWakeLock']

View File

@@ -14,6 +14,8 @@ import { useAgentStore } from "@/stores/agentStore";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { showFailToast } from "vant"; import { showFailToast } from "vant";
import { checkFeatureWhitelistStatus, offlineFeature, checkOrderAgent } from "@/api/agent"; import { checkFeatureWhitelistStatus, offlineFeature, checkOrderAgent } from "@/api/agent";
import { vehicleFeatureMap, vehicleFeatureRiskLevels } from "@/config/vehicleFeatureMap";
import { normalizeVehicleReportData } from "@/utils/vehicleReportNormalize";
// 动态导入产品背景图片的函数 // 动态导入产品背景图片的函数
const loadProductBackground = async (productType) => { const loadProductBackground = async (productType) => {
@@ -226,8 +228,10 @@ const processedReportData = computed(() => {
// 拆分CQYGL3F8E数据 // 拆分CQYGL3F8E数据
data = splitCQYGL3F8EForTabs(data); data = splitCQYGL3F8EForTabs(data);
// 车辆报告:解包嵌套 data与微信小程序 normalizeVehicleQueryData 对齐
data = normalizeVehicleReportData(data);
// 过滤掉在featureMap中没有对应的项 // 过滤掉在featureMap中没有对应的项
return data.filter(item => featureMap[item.data.apiID]); return data.filter(item => featureMap[item.data?.apiID]);
}); });
// 获取产品背景图片 // 获取产品背景图片
@@ -553,14 +557,7 @@ const featureMap = {
name: "税务风险", name: "税务风险",
component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/TaxRisk/index.vue")), component: defineAsyncComponent(() => import("@/ui/CQYGL3F8E/components/TaxRisk/index.vue")),
}, },
QCXG7A2B: { ...vehicleFeatureMap,
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG7A2B.vue")),
},
QCXG9P1C: {
name: "名下车辆",
component: defineAsyncComponent(() => import("@/ui/CQCXG9P1C.vue")),
},
BehaviorRiskScan: { BehaviorRiskScan: {
name: "风险行为扫描", name: "风险行为扫描",
component: defineAsyncComponent(() => component: defineAsyncComponent(() =>
@@ -917,7 +914,7 @@ const featureRiskLevels = {
// 🟢 中风险类 - 权重 8-12 // 🟢 中风险类 - 权重 8-12
'QYGL3F8E': 10, // 人企关系加强版 'QYGL3F8E': 10, // 人企关系加强版
'QCXG7A2B': 10, // 名下车辆 ...vehicleFeatureRiskLevels,
'JRZQ09J8': 10, // 收入评估 'JRZQ09J8': 10, // 收入评估
'JRZQ3C9R': 10, // 支付行为指数 'JRZQ3C9R': 10, // 支付行为指数
// 'IVYZ0S0D': 10, // 个人仲裁信息 // 'IVYZ0S0D': 10, // 个人仲裁信息

View File

@@ -203,6 +203,19 @@ const getFeatureIcon = (apiId) => {
JRZQ4AA8: "/inquire_icons/huankuanyali.svg", JRZQ4AA8: "/inquire_icons/huankuanyali.svg",
QCXG7A2B: "/inquire_icons/mingxiacheliang.svg", QCXG7A2B: "/inquire_icons/mingxiacheliang.svg",
QCXG9P1C: "/inquire_icons/mingxiacheliang.svg", QCXG9P1C: "/inquire_icons/mingxiacheliang.svg",
QCXG4D2E: "/inquire_icons/mingxiacheliang.svg",
QCXG5F3A: "/inquire_icons/mingxiacheliang.svg",
QCXG5U0Z: "/inquire_icons/mingxiacheliang.svg",
QCXGGB2Q: "/inquire_icons/mingxiacheliang.svg",
QCXGYTS2: "/inquire_icons/mingxiacheliang.svg",
QCXG1H7Y: "/inquire_icons/mingxiacheliang.svg",
QCXG4I1Z: "/inquire_icons/mingxiacheliang.svg",
QCXG1U4U: "/inquire_icons/mingxiacheliang.svg",
QCXG3Y6B: "/inquire_icons/mingxiacheliang.svg",
QCXG3Z3L: "/inquire_icons/mingxiacheliang.svg",
QCXG6B4E: "/inquire_icons/mingxiacheliang.svg",
QCXGP00W: "/inquire_icons/mingxiacheliang.svg",
QCXGY7F2: "/inquire_icons/mingxiacheliang.svg",
BehaviorRiskScan: "/inquire_icons/fengxianxingwei.svg", BehaviorRiskScan: "/inquire_icons/fengxianxingwei.svg",
IVYZ5733: "/inquire_icons/hunyinzhuangtai.svg", IVYZ5733: "/inquire_icons/hunyinzhuangtai.svg",
IVYZ81NC: "/inquire_icons/hunyinzhuangtai.svg", IVYZ81NC: "/inquire_icons/hunyinzhuangtai.svg",

View File

@@ -0,0 +1,19 @@
import { computed } from 'vue'
import { payloadAsArray, payloadAsObject, unwrapVehiclePayload } from '@/utils/vehiclePayload'
const defaultProps = {
data: null,
params: () => ({}),
apiId: '',
index: 0,
notifyRiskStatus: () => {},
}
export function useVehiclePayload(props = defaultProps) {
const payload = computed(() => unwrapVehiclePayload(props.data))
const obj = computed(() => payloadAsObject(props.data))
const list = computed(() => payloadAsArray(props.data))
const params = computed(() => props.params || {})
return { payload, obj, list, params }
}

View File

@@ -0,0 +1,41 @@
import { defineAsyncComponent } from 'vue'
import { VEHICLE_API_TITLES } from './vehicleReportRegistry'
/** 已单独实现 UI 的 apiID */
const VEHICLE_COMPONENT_LOADERS = {
QCXG7A2B: () => import('@/ui/CQCXG7A2B.vue'),
QCXG9P1C: () => import('@/ui/CQCXG9P1C.vue'),
QCXG4D2E: () => import('@/ui/CQCXG4D2E.vue'),
QCXG5F3A: () => import('@/ui/CQCXG5F3A.vue'),
QCXG5U0Z: () => import('@/ui/CQCXG5U0Z.vue'),
QCXGGB2Q: () => import('@/ui/CQCXGGB2Q.vue'),
QCXGYTS2: () => import('@/ui/CQCXGYTS2.vue'),
QCXG1H7Y: () => import('@/ui/CQCXG1H7Y.vue'),
QCXG4I1Z: () => import('@/ui/CQCXG4I1Z.vue'),
QCXG1U4U: () => import('@/ui/CQCXG1U4U.vue'),
QCXG3Y6B: () => import('@/ui/CQCXG3Y6B.vue'),
QCXG3Z3L: () => import('@/ui/CQCXG3Z3L.vue'),
QCXG6B4E: () => import('@/ui/CQCXG6B4E.vue'),
QCXGP00W: () => import('@/ui/CQCXGP00W.vue'),
QCXGY7F2: () => import('@/ui/CQCXGY7F2.vue'),
}
const fallbackLoader = () => import('@/ui/CQCXGFallback.vue')
export const vehicleFeatureMap = Object.fromEntries(
Object.entries(VEHICLE_API_TITLES).map(([apiId, name]) => {
const loader = VEHICLE_COMPONENT_LOADERS[apiId] || fallbackLoader
return [
apiId,
{
name,
component: defineAsyncComponent(loader),
},
]
}),
)
/** 车辆类模块默认低风险权重 */
export const vehicleFeatureRiskLevels = Object.fromEntries(
Object.keys(VEHICLE_API_TITLES).map(id => [id, 10]),
)

View File

@@ -0,0 +1,45 @@
/**
* 车辆类报告模块apiID产品能力编码→ 展示名称
* 与 qncV4uni-app、后端 ProductFeature / 上游 api 约定对齐
*/
export const VEHICLE_API_TITLES = {
QCXG9F5C: '疑似营运车辆注册平台数',
QCXG3B8Z: '疑似运营车辆查询(月度里程)',
QCXGP1W3: '疑似运营车辆查询(季度里程)',
QCXGM7R9: '疑似运营车辆查询(半年度里程)',
QCXGU2K4: '疑似运营车辆查询(年度里程)',
QCXGY7F2: '二手车 VIN 估值',
QCXG5U0Z: '车辆静态信息查询',
QCXG3M7Z: '人车关系核验ETC',
QCXG1U4U: '车辆里程记录(混合查询)',
QCXG2T6S: '车辆里程记录(品牌查询)',
QCXG3Y6B: '车辆维保简版查询',
QCXG3Z3L: '车辆维保详细版查询',
QCXG1H7Y: '车辆过户简版查询',
QCXG4I1Z: '车辆过户详版查询',
QCXGGB2Q: '车辆二要素核验简版',
QCXGP00W: '车辆出险详版查询',
QCXGYTS2: '人车核验(详版)',
QCXGGJ3A: '车辆 VIN 码查询号牌简版',
QCXGJJ2A: '车辆 VIN 码查车辆信息详版',
QCXG5F3A: '名下车辆车牌查询 B',
QCXG4D2E: '名下车辆数量查询',
QCXG6B4E: '车辆出险记录核验',
QCXG8A3D: '车辆七项信息核验',
QCXG9P1C: '名下车辆车牌查询 A',
QCXG7A2B: '名下车辆',
}
export function getVehicleModuleTitle(apiId, featureName) {
if (!apiId || apiId === '__UNLABELED__') {
if (featureName?.trim())
return featureName.trim()
return '报告模块'
}
const t = VEHICLE_API_TITLES[apiId]
if (t)
return t
if (featureName?.trim())
return featureName.trim()
return apiId
}

76
src/ui/CQCXG1H7Y.vue Normal file
View File

@@ -0,0 +1,76 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const hasData = computed(() => Object.keys(obj.value).length > 0)
const flag = computed(() => obj.value.transferFlag)
const flagText = computed(() => {
if (flag.value === '1' || flag.value === 1) return '已过户'
if (flag.value === '0' || flag.value === 0) return '未过户'
return '未知'
})
const formattedTransferDate = computed(() => {
const raw = obj.value.transferDate
if (!raw) return '-'
if (raw === '近一年内过户') return raw
const s = String(raw)
if (s.length === 6) return `${s.slice(0, 4)}${s.slice(4, 6)}`
return s
})
const transferNum = computed(() => {
const n = obj.value.transferNum
return n === '' || n == null ? '0' : String(n)
})
const bandClass = computed(() => {
if (flag.value === '1' || flag.value === 1) return 'bg-amber-50 border-amber-200'
if (flag.value === '0' || flag.value === 0) return 'bg-green-50 border-green-200'
return 'bg-gray-50 border-gray-200'
})
const riskScore = computed(() => ((flag.value === '1' || flag.value === 1) ? 70 : 100))
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100">
<h3 class="text-lg font-bold text-sky-900">车辆过户简版查询</h3>
<p class="text-sm text-sky-700 mt-1">最近是否发生过户及过户次数</p>
<div class="mt-3 inline-block bg-white rounded-lg px-3 py-2 text-right">
<div class="text-xs text-gray-500">是否过户</div>
<div class="text-xl font-bold">{{ flagText }}</div>
</div>
</div>
<div v-if="hasData" :class="['rounded-lg p-4 border', bandClass]">
<div class="font-semibold">最近过户情况</div>
<p class="text-sm text-gray-600 mt-1">
{{ flag === 1 || flag === '1' ? '该车辆存在过户记录' : flag === 0 || flag === '0' ? '该车辆暂无过户记录' : '未能识别过户状态' }}
</p>
<div class="flex justify-between mt-3 text-sm">
<span class="text-gray-500">最近过户时间</span>
<span class="font-medium">{{ formattedTransferDate }}</span>
</div>
<div class="flex justify-between mt-2 text-sm">
<span class="text-gray-500">累计过户次数</span>
<span class="font-bold">{{ transferNum }} </span>
</div>
</div>
<div v-else class="text-center py-10 text-gray-500">暂无过户信息</div>
</div>
</template>

104
src/ui/CQCXG1U4U.vue Normal file
View File

@@ -0,0 +1,104 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { formatDateZh, formatMileageKm } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const hasData = computed(() => Object.keys(obj.value).length > 0)
const vin = computed(() => obj.value.vehicleInfo?.vin || '')
const mileageList = computed(() => obj.value.mileageInfo?.mileageList || [])
const adjustList = computed(() => obj.value.mileageInfo?.suspectedAdjustMileageList || [])
const suspectedAdjust = computed(() => obj.value.mileageInfo?.suspectedAdjust)
const imageUrl = computed(() => obj.value.imageUrl || '')
const latestRecord = computed(() => {
const list = mileageList.value
return list.length ? list[list.length - 1] : null
})
const suspectedText = computed(() => {
if (suspectedAdjust.value === 'true') return '存在异常里程行为'
if (suspectedAdjust.value === 'false') return '未发现里程异常'
return '未知'
})
function sourceText(source) {
if (source === '0') return '诊断里程'
if (source === '1') return '维保里程'
return '其他'
}
const riskScore = computed(() => (suspectedAdjust.value === 'true' ? 45 : 95))
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100">
<h3 class="text-lg font-bold text-sky-900">车辆里程记录混合查询</h3>
<p class="text-sm text-sky-700 mt-1">里程变化与调表嫌疑综合展示</p>
</div>
<template v-if="hasData">
<div
class="rounded-lg p-4 border"
:class="suspectedAdjust === 'true' ? 'bg-amber-50 border-amber-200' : suspectedAdjust === 'false' ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200'"
>
<div class="flex justify-between gap-4 flex-wrap">
<div>
<div class="text-xs text-gray-500">VIN</div>
<div class="font-mono text-sm">{{ vin || '-' }}</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-500">最新里程</div>
<div class="text-xl font-bold text-blue-600">{{ latestRecord ? formatMileageKm(latestRecord.mileage) : '-' }}</div>
<div class="text-xs text-gray-500">最近{{ latestRecord?.reportTime || '-' }}</div>
</div>
</div>
<p class="text-sm mt-2">里程是否异常{{ suspectedText }}</p>
<img v-if="imageUrl" :src="imageUrl" class="w-full mt-2 rounded-lg" alt="里程凭证" />
</div>
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h4 class="font-semibold mb-3">里程记录时间轴</h4>
<div v-if="mileageList.length" class="space-y-3">
<div v-for="(item, idx) in mileageList" :key="idx" class="flex gap-3">
<div class="w-2 rounded-full shrink-0" :class="item.mileageStatus === '1' ? 'bg-red-500' : 'bg-blue-500'" />
<div class="flex-1 text-sm">
<div class="flex justify-between">
<span>{{ formatDateZh(item.reportTime) }}</span>
<span class="font-semibold text-blue-600">{{ formatMileageKm(item.mileage) }}</span>
</div>
<div class="text-gray-500 mt-1">
来源{{ sourceText(item.source) }}
<span v-if="item.mileageStatus === '1'" class="ml-2 text-red-600 bg-red-50 px-1 rounded text-xs">异常里程</span>
</div>
</div>
</div>
</div>
<div v-else class="text-gray-500 text-sm">暂无里程记录</div>
</div>
<div v-if="adjustList.length" class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<h4 class="font-semibold mb-3">疑似调表记录</h4>
<div v-for="(item, idx) in adjustList" :key="idx" class="py-2 border-b border-gray-200 last:border-0 text-sm">
<div>{{ formatDateZh(item.reportTime) }}</div>
<div class="text-gray-600 mt-1">
调整前 {{ formatMileageKm(item.beforeMileage) }} 调整后 {{ formatMileageKm(item.afterMileage) }}
</div>
</div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无里程数据</div>
</div>
</template>

66
src/ui/CQCXG3Y6B.vue Normal file
View File

@@ -0,0 +1,66 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { formatDateZh, formatMileageKm } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj, params } = useVehiclePayload(props)
const records = computed(() => (Array.isArray(obj.value.record) ? obj.value.record : []))
const hasData = computed(() => Object.keys(obj.value).length > 0)
const vin = computed(() => (records.value[0]?.vin) || params.value.vin_code || '')
const lastRecord = computed(() => (records.value.length ? records.value[records.value.length - 1] : null))
const riskScore = computed(() => 95)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100">
<h3 class="text-lg font-bold">车辆维保简版查询</h3>
<p class="text-sm text-gray-600 mt-1">按时间轴展示维保与材料明细</p>
</div>
<template v-if="hasData">
<div class="bg-gray-50 rounded-lg p-4 border text-sm">
<div class="font-mono">VIN{{ vin || '-' }}</div>
<div class="text-gray-600 mt-1">维保 {{ records.length }}
<span v-if="lastRecord"> · 最近 {{ formatDateZh(lastRecord.lastTime) }} · {{ formatMileageKm(lastRecord.mileage) }}</span>
</div>
</div>
<div v-if="records.length" class="space-y-3">
<div v-for="(item, idx) in records" :key="idx" class="bg-white rounded-lg p-4 border border-gray-200">
<div class="flex justify-between font-medium">
<span>{{ formatDateZh(item.lastTime) }}</span>
<span class="text-blue-600">{{ formatMileageKm(item.mileage) }}</span>
</div>
<p class="text-sm text-gray-500 mt-1">{{ item.repairType || '维保' }} · VIN {{ item.vin || vin || '-' }}</p>
<div v-if="item.details?.length" class="mt-2 text-sm">
<div class="text-xs text-gray-500 mb-1">维修项目</div>
<div v-for="(det, di) in item.details" :key="di" class="py-1">
<span v-if="det.type" class="text-xs bg-blue-50 text-blue-700 px-1 rounded mr-1">{{ det.type }}</span>{{ det.content }}
</div>
</div>
<div v-if="item.materials?.length" class="mt-2 text-sm">
<div class="text-xs text-gray-500 mb-1">使用材料</div>
<div v-for="(m, mi) in item.materials" :key="mi" class="py-1">
<span v-if="m.type" class="text-xs bg-gray-100 px-1 rounded mr-1">{{ m.type }}</span>{{ m.content }}
</div>
</div>
</div>
</div>
<div v-else class="text-center text-gray-500 py-6">暂无维保记录</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无维保数据</div>
</div>
</template>

53
src/ui/CQCXG3Z3L.vue Normal file
View File

@@ -0,0 +1,53 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { formatDateZh, formatMileageKm } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const records = computed(() => (Array.isArray(obj.value.record) ? obj.value.record : []))
const hasData = computed(() => Object.keys(obj.value).length > 0)
const riskScore = computed(() => 95)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100">
<h3 class="text-lg font-bold">车辆维保详细版查询</h3>
<p class="text-sm text-gray-600 mt-1">品牌VIN 及每次维保详细内容</p>
</div>
<template v-if="hasData">
<div class="bg-gray-50 rounded-lg p-4 border text-sm grid grid-cols-2 gap-2">
<div><span class="text-gray-500">品牌</span><div class="font-medium">{{ obj.brandName || '未知' }}</div></div>
<div><span class="text-gray-500">VIN</span><div class="font-mono">{{ obj.vin || '-' }}</div></div>
<div class="col-span-2 text-gray-600">车牌 {{ obj.licensePlate || '未提供' }} · 发动机 {{ obj.engine || '-' }}</div>
</div>
<div v-if="records.length" class="space-y-3">
<div v-for="(item, idx) in records" :key="idx" class="bg-white rounded-lg p-4 border">
<div class="flex justify-between font-medium">
<span>{{ formatDateZh(item.date) }}</span>
<span class="text-blue-600">{{ formatMileageKm(item.mileage) }}</span>
</div>
<p class="text-sm text-gray-500">{{ item.type || '维保' }}</p>
<p v-if="item.content" class="text-sm mt-2">维修内容{{ item.content }}</p>
<p v-if="item.remark" class="text-xs text-gray-500 mt-1">{{ item.remark }}</p>
</div>
</div>
<div v-else class="text-center text-gray-500 py-6">暂无维保记录</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无维保数据</div>
</div>
</template>

15
src/ui/CQCXG4D2E.vue Normal file
View File

@@ -0,0 +1,15 @@
<script setup>
import VehiclePlateList from '@/ui/vehicle/VehiclePlateList.vue'
defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
</script>
<template>
<VehiclePlateList v-bind="$props" title="名下车辆数量查询" />
</template>

84
src/ui/CQCXG4I1Z.vue Normal file
View File

@@ -0,0 +1,84 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { payloadAsArray } from '@/utils/vehiclePayload'
const props = defineProps({
data: { default: null },
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const transfers = computed(() => {
return payloadAsArray(props.data).map((item) => {
const changeMonth = item.changeMonth
let changeMonthFormatted = '-'
if (changeMonth === '近一年内过户') changeMonthFormatted = changeMonth
else if (typeof changeMonth === 'string' && changeMonth.length === 6) {
changeMonthFormatted = `${changeMonth.slice(0, 4)}${changeMonth.slice(4, 6)}`
}
else if (changeMonth) changeMonthFormatted = String(changeMonth)
let intervalText = '-'
if (item.transYear || item.transMonth) {
const years = item.transYear ? `${item.transYear}` : ''
const months = item.transMonth ? `${item.transMonth}个月` : ''
intervalText = `${years}${months}` || '-'
}
return { ...item, changeMonthFormatted, intervalText }
})
})
const totalTimes = computed(() => {
if (!transfers.value.length) return ''
const last = transfers.value[transfers.value.length - 1]
return last.transTimeSum ?? ''
})
const riskScore = computed(() => (transfers.value.length > 2 ? 65 : 90))
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-sky-50 to-blue-50 rounded-lg p-4 border border-sky-100 flex justify-between items-start gap-3">
<div>
<h3 class="text-lg font-bold text-sky-900">车辆过户详版查询</h3>
<p class="text-sm text-sky-700 mt-1">按时间轴展示过户与车牌变更</p>
</div>
<div v-if="totalTimes" class="bg-white rounded-lg px-3 py-2 text-right shrink-0">
<div class="text-xs text-gray-500">总过户次数</div>
<div class="text-lg font-bold">{{ totalTimes }} </div>
</div>
</div>
<div v-if="transfers.length" class="space-y-3">
<div
v-for="(item, index) in transfers"
:key="index"
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
>
<div class="flex justify-between mb-2">
<span class="font-semibold">{{ item.changeMonthFormatted }}</span>
<span class="text-sm text-blue-600"> {{ item.transTimeSum }} 次过户</span>
</div>
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
<div class="text-gray-500 text-xs">过户前车牌</div>
<div class="font-mono font-medium">{{ item.oldCp || '未知' }}</div>
<div v-if="item.cityBefore" class="text-xs text-gray-500 mt-1">城市{{ item.cityBefore }}</div>
</div>
<div>
<div class="text-gray-500 text-xs">过户后车牌</div>
<div class="font-mono font-medium">{{ item.newCp || '未知' }}</div>
<div v-if="item.cityAfter" class="text-xs text-gray-500 mt-1">城市{{ item.cityAfter }}</div>
</div>
</div>
<div v-if="item.intervalText !== '-'" class="text-xs text-gray-500 mt-2">距上次过户{{ item.intervalText }}</div>
</div>
</div>
<div v-else class="text-center py-10 text-gray-500">暂无过户记录</div>
</div>
</template>

15
src/ui/CQCXG5F3A.vue Normal file
View File

@@ -0,0 +1,15 @@
<script setup>
import VehiclePlateList from '@/ui/vehicle/VehiclePlateList.vue'
defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
</script>
<template>
<VehiclePlateList v-bind="$props" title="名下车辆车牌查询 B" />
</template>

52
src/ui/CQCXG5U0Z.vue Normal file
View File

@@ -0,0 +1,52 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
import { payloadAsArray } from '@/utils/vehiclePayload'
const props = defineProps({
data: { default: null },
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const records = computed(() => payloadAsArray(props.data))
const riskScore = computed(() => 100)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-4 border border-blue-100">
<h3 class="text-lg font-bold text-gray-900">车辆静态信息查询</h3>
<p class="text-sm text-gray-600 mt-1">生产排放燃料等静态信息</p>
</div>
<template v-if="records.length">
<div
v-for="(item, idx) in records"
:key="idx"
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
>
<div class="flex flex-wrap items-center gap-2 mb-3">
<span class="text-xs text-blue-700 bg-blue-50 px-2 py-0.5 rounded">车辆 {{ idx + 1 }}</span>
<span class="font-semibold text-gray-900">{{ item.vType || '未知车型' }}</span>
<span class="text-xs text-blue-600 bg-white px-2 py-0.5 rounded-full border">{{ item.vFuelType || '燃料未知' }}</span>
</div>
<div class="grid grid-cols-2 gap-3 text-sm">
<div><span class="text-gray-500 block text-xs">发动机号</span><span class="font-mono">{{ item.engineNO || '-' }}</span></div>
<div><span class="text-gray-500 block text-xs">发动机型号</span><span class="font-mono">{{ item.engineType || '-' }}</span></div>
<div><span class="text-gray-500 block text-xs">生产日期</span>{{ item.vScdate || '-' }}</div>
<div><span class="text-gray-500 block text-xs">排放阶段</span>{{ item.dischargeStage || '-' }}</div>
<div><span class="text-gray-500 block text-xs">车辆分类</span>{{ item.vClassification || '-' }}</div>
<div class="col-span-2"><span class="text-gray-500 block text-xs">生产企业</span>{{ item.vManufacturer || '-' }}</div>
<div class="col-span-2"><span class="text-gray-500 block text-xs">生产厂地址</span>{{ item.vSccdz || '-' }}</div>
</div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无车辆静态信息</div>
</div>
</template>

76
src/ui/CQCXG6B4E.vue Normal file
View File

@@ -0,0 +1,76 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj: data } = useVehiclePayload(props)
const hasData = computed(() => Object.keys(data.value).length > 0)
function yesNoText(val, yesText, noText = '否') {
if (val === '1') return yesText
if (val === '0') return noText
return '-'
}
const riskLevelText = computed(() => {
const d = data.value
if (d.IfHighriskVehicle === '1') return '高风险'
if (d.IsMajorAccidentLevel && d.IsMajorAccidentLevel !== '一般') return '较高风险'
if (d.IsMajorAccidentData && d.IsMajorAccidentData !== '0') return '有事故记录'
return '风险可控'
})
const riskScore = computed(() => {
const t = riskLevelText.value
if (t === '高风险') return 30
if (t === '较高风险' || t === '有事故记录') return 50
return 90
})
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-orange-50 to-red-50 rounded-lg p-4 border border-orange-100">
<h3 class="text-lg font-bold">车辆出险记录核验</h3>
<p class="text-sm text-gray-600 mt-1">综合出险脱保重大事故等信息</p>
</div>
<template v-if="hasData">
<div
class="rounded-lg p-4 text-center border"
:class="riskLevelText === '高风险' ? 'bg-red-50 border-red-200' : riskLevelText.includes('风险') && riskLevelText !== '风险可控' ? 'bg-amber-50 border-amber-200' : 'bg-green-50 border-green-200'"
>
<div class="text-sm text-gray-500">风险等级</div>
<div class="text-2xl font-bold">{{ riskLevelText }}</div>
</div>
<div class="bg-gray-50 rounded-lg p-4 border text-sm space-y-2">
<div v-if="data.LicensePlate" class="text-xl font-bold font-mono">{{ data.LicensePlate }}</div>
<div>{{ data.CarType || '未知车型' }}</div>
<div class="text-gray-600">二手车参考价 {{ data.UsedCarPrice ? `${data.UsedCarPrice}` : '-' }} · 新车 {{ data.PurchasePrice ? `${data.PurchasePrice}` : '-' }}</div>
<div class="text-gray-500 text-xs">车龄 {{ data.CarAge ? `${data.CarAge} 个月` : '-' }} · 初登 {{ data.DebutDate || '-' }}</div>
</div>
<div class="bg-white rounded-lg border divide-y text-sm">
<div class="px-4 py-2 font-semibold bg-gray-50">核心风险指标</div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">高风险车辆</span><span>{{ yesNoText(data.IfHighriskVehicle, '是') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">营运车辆</span><span>{{ yesNoText(data.IsOperation, '是') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">车损险</span><span>{{ yesNoText(data.IfCarDamage, '已投保', '未投保') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">连续投保</span><span>{{ yesNoText(data.IsConInsure, '是') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">历史脱保</span><span>{{ yesNoText(data.IfTuoBao, '有', '无') }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">全损情况</span><span>{{ data.TotalLoss === '1' ? '存在全损' : data.TotalLoss === '0' ? '无全损' : '-' }}</span></div>
<div class="flex justify-between px-4 py-2"><span class="text-gray-500">综合评分</span><span class="font-bold">{{ data.Total || '-' }}</span></div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无出险核验数据</div>
</div>
</template>

View File

@@ -1,65 +1,32 @@
<template>
<div class="card">
<!-- 名下车辆信息展示 -->
<div class="bg-yellow-100 text-yellow-700 p-4 rounded-lg">
<h3 class="text-xl font-semibold">名下车辆</h3>
<p class="text-sm">此人名下拥有车辆{{ data?.carNum }} </p>
</div>
<!-- 校验对象展示 -->
</div>
</template>
<script setup> <script setup>
import { defineProps, watch, computed } from 'vue'; import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'; import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
// 接收父组件传入的 props
const props = defineProps({ const props = defineProps({
data: Object, data: Object,
params: Object, params: Object,
apiId: { apiId: { type: String, default: '' },
type: String, index: { type: Number, default: 0 },
default: '', notifyRiskStatus: { type: Function, default: () => {} },
}, })
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
});
// 脱敏函数:姓名脱敏(保留首位) const { obj } = useVehiclePayload(props)
const maskName = (name) => {
if (!name) return '';
return name.length > 1 ? name[0] + "*".repeat(name.length - 1) : "*";
};
// 脱敏函数身份证号脱敏保留前6位和最后4位 const carNum = computed(() => {
const maskIdCard = (idCard) => { const n = Number(obj.value.carNum)
if (!idCard) return ''; return Number.isFinite(n) ? n : null
return idCard.replace(/^(.{6})(?:\d+)(.{4})$/, "$1****$2"); })
};
// 计算风险评分0-100分分数越高越安全 const riskScore = computed(() => 100)
const riskScore = computed(() => { useRiskNotifier(props, riskScore)
// 名下车辆不算风险始终返回100分最安全
return 100;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
</script> </script>
<style scoped> <template>
/* 自定义样式 */ <div class="card">
</style> <div class="bg-yellow-100 text-yellow-800 p-4 rounded-lg border border-yellow-200">
<h3 class="text-xl font-semibold">名下车辆</h3>
<p class="text-sm mt-2">此人名下拥有车辆{{ carNum != null ? `${carNum}` : '—' }}</p>
</div>
</div>
</template>

View File

@@ -1,130 +1,15 @@
<template>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<!-- 车辆总数统计 -->
<div class="flex justify-between items-center mb-6 pb-4 border-b border-gray-100">
<div class="flex items-center gap-3">
<div>
<div class="text-lg font-semibold text-gray-900">个人名下车辆</div>
</div>
</div>
<div class="bg-blue-50 text-blue-700 px-4 py-2 rounded-full text-sm font-medium">
{{ vehicleCount }}
</div>
</div>
<!-- 车辆列表 -->
<div class="space-y-3" v-if="vehicleList && vehicleList.length > 0">
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:bg-blue-50 hover:border-blue-200 transition-colors duration-200"
v-for="(vehicle, index) in vehicleList" :key="index">
<div class="space-y-3">
<div class="text-xl font-bold text-gray-900 font-mono tracking-wider">
{{ vehicle.plateNum }}
</div>
<div class="flex items-center gap-3">
<div class="inline-flex items-center gap-1 px-3 py-1 rounded text-sm font-medium text-white"
:class="getPlateColorClass(vehicle.plateColor)">
<span>🏷</span>
<span>{{ getPlateColorText(vehicle.plateColor) }}</span>
</div>
<div class="text-sm text-gray-600">
<span class="text-gray-500">车辆类型:</span>
<span class="font-medium text-gray-900 ml-1">{{ getVehicleTypeText(vehicle.vehicleType)
}}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div class="text-center py-12 text-gray-500" v-else>
<div class="text-4xl mb-3">🚫</div>
<div class="text-lg font-medium mb-1">暂无车辆信息</div>
<div class="text-sm">No vehicle records found</div>
</div>
</div>
</template>
<script setup> <script setup>
import { defineProps, computed } from 'vue'; import VehiclePlateList from '@/ui/vehicle/VehiclePlateList.vue'
// 接收父组件传入的 props
const props = defineProps({
data: Object,
params: Object,
});
// 车牌颜色映射
const plateColorMap = {
0: '蓝色 - 普通燃油车',
1: '黄色 - 大型车/货车',
2: '黑色 - 外籍车辆/港澳台车',
3: '白色 - 警车/军车/武警车',
4: '渐变绿色 - 新能源汽车',
5: '黄绿双拼色 - 大型新能源汽车',
6: '蓝白渐变色 - 临时牌照',
7: '临时牌照 - 临时行驶车辆',
11: '绿色 - 新能源汽车',
12: '红色 - 教练车/试验车'
};
// 车辆类型映射
const vehicleTypeMap = {
1: '一型客车',
2: '二型客车',
3: '三型客车',
4: '四型客车',
11: '一型货车',
12: '二型货车',
13: '三型货车',
14: '四型货车',
15: '五型货车',
16: '六型货车',
21: '一型专项作业车',
22: '二型专项作业车',
23: '三型专项作业车',
24: '四型专项作业车',
25: '五型专项作业车',
26: '六型专项作业车'
};
// 计算属性
const vehicleList = computed(() => props.data?.list || []);
const vehicleCount = computed(() => props.data?.vehicleCount || 0);
// 获取车牌颜色文本
const getPlateColorText = (plateColor) => {
return plateColorMap[plateColor] || '未知颜色 - 未知类型';
};
// 获取车牌颜色样式类
const getPlateColorClass = (plateColor) => {
const colorClassMap = {
0: 'bg-blue-500',
1: 'bg-yellow-500',
2: 'bg-gray-800',
3: 'bg-gray-200 text-gray-800',
4: 'bg-green-500',
5: 'bg-gradient-to-r from-yellow-500 to-green-500',
6: 'bg-gradient-to-r from-blue-500 to-white text-blue-800',
7: 'bg-red-500',
11: 'bg-green-500',
12: 'bg-red-500'
};
return colorClassMap[plateColor] || 'bg-gray-500';
};
// 获取车辆类型文本
const getVehicleTypeText = (vehicleType) => {
return vehicleTypeMap[vehicleType] || '未知类型';
};
onMounted(() => {
console.log('车辆数据:', props.data);
});
defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
</script> </script>
<style scoped> <template>
/* 自定义样式 - 仅保留必要的样式 */ <VehiclePlateList v-bind="$props" title="名下车辆车牌查询 A" />
</style> </template>

43
src/ui/CQCXGFallback.vue Normal file
View File

@@ -0,0 +1,43 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: { default: null },
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { payload } = useVehiclePayload(props)
const bodyText = computed(() => {
const p = payload.value
if (p === '000' || p === 0)
return '(本模块暂无示例明细)'
if (p == null || p === '')
return '暂无数据'
if (typeof p === 'string')
return p
try {
return JSON.stringify(p, null, 2)
}
catch {
return String(p)
}
})
const riskScore = computed(() => 100)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card">
<p class="text-xs text-gray-500 mb-2">
以下为结构化数据预览专用版式可联系产品补充独立模块
</p>
<pre class="text-xs text-gray-800 bg-gray-50 rounded-lg p-3 overflow-auto max-h-80 whitespace-pre-wrap break-all">{{ bodyText }}</pre>
</div>
</template>

58
src/ui/CQCXGGB2Q.vue Normal file
View File

@@ -0,0 +1,58 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { maskName } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj, params } = useVehiclePayload(props)
/** verify_code1 一致0 不一致(上游接口约定) */
const isMatch = computed(() => {
const n = Number(obj.value.verify_code)
if (n === 1) return true
if (n === 0) return false
return null
})
const resultText = computed(() => {
if (isMatch.value === true) return '一致'
if (isMatch.value === false) return '不一致'
return '暂无结果'
})
const sectionClass = computed(() => {
if (isMatch.value === true) return 'bg-green-50 border-green-200'
if (isMatch.value === false) return 'bg-red-50 border-red-200'
return 'bg-gray-50 border-gray-200'
})
const riskScore = computed(() => (isMatch.value === false ? 40 : 100))
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-4">
<div class="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-lg p-4">
<h3 class="text-lg font-bold">车辆二要素核验</h3>
<p class="text-sm opacity-90 mt-1">校验人员姓名与车辆号牌是否匹配</p>
</div>
<div :class="['rounded-lg p-6 text-center border', sectionClass]">
<div class="text-4xl font-bold mb-2">{{ isMatch === true ? '✓' : isMatch === false ? '✕' : '?' }}</div>
<div class="text-sm text-gray-500">核验结果</div>
<div class="text-2xl font-bold mt-1">{{ resultText }}</div>
</div>
<div class="divide-y text-sm">
<div class="flex py-2"><span class="w-24 text-gray-500">姓名</span><span>{{ maskName(params.name) }}</span></div>
<div class="flex py-2"><span class="w-24 text-gray-500">车牌号</span><span class="font-mono">{{ params.plate_no || params.car_license || '-' }}</span></div>
<div class="flex py-2"><span class="w-24 text-gray-500">号牌类型</span><span>{{ params.carplate_type || params.car_type || '-' }}</span></div>
</div>
</div>
</template>

99
src/ui/CQCXGP00W.vue Normal file
View File

@@ -0,0 +1,99 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const retdata = computed(() => {
const p = obj.value
if (p.retdata && typeof p.retdata === 'object') return p.retdata
return p
})
const hasData = computed(() => Object.keys(retdata.value).length > 0)
const clxx = computed(() => retdata.value.clxx || {})
const tjxx = computed(() => retdata.value.tjxx || {})
const pzRecords = computed(() => {
const mx = retdata.value.pzlsmx || {}
return Array.isArray(mx.records) ? mx.records : []
})
function formatFen(val) {
if (val !== 0 && !val) return '-'
const n = Number(val)
if (Number.isNaN(n)) return `${val}`
return `${(n / 100).toLocaleString()}`
}
function dangerTypeText(t) {
if (t === '1') return '更换'
if (t === '2') return '维修'
if (t === '3') return '材料'
return '其他'
}
const riskScore = computed(() => {
const count = Number(tjxx.value.claimCount)
if (count > 3) return 40
if (count > 0) return 60
return 90
})
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-red-50 to-orange-50 rounded-lg p-4 border border-red-100">
<h3 class="text-lg font-bold">车辆出险详版查询</h3>
<p class="text-sm text-gray-600 mt-1">碰撞记录与统计精简展示</p>
</div>
<template v-if="hasData">
<div class="bg-gray-50 rounded-lg p-4 border text-sm">
<div class="grid grid-cols-2 gap-2">
<div>
<div class="text-gray-500 text-xs">品牌</div>
<div class="font-medium">{{ clxx.brandName || '未知' }}</div>
<div v-if="clxx.vehicleStyle" class="text-xs text-gray-500">{{ clxx.vehicleStyle }}</div>
</div>
<div>
<div class="text-gray-500 text-xs">VIN</div>
<div class="font-mono text-xs">{{ pzRecords[0]?.vin || clxx.vin || '-' }}</div>
<div v-if="clxx.licensePlate" class="text-xs">车牌 {{ clxx.licensePlate }}</div>
</div>
</div>
<div class="flex flex-wrap gap-3 mt-3 text-xs text-gray-600">
<span>事故 {{ tjxx.claimCount ?? '-' }} </span>
<span>总维修 {{ tjxx.totalAmount || '-' }}</span>
<span>最大单次 {{ tjxx.largestAmount || '-' }}</span>
<span>结案 {{ tjxx.claimCacCount ?? 0 }} / 未结案 {{ tjxx.claimUnCacCount ?? 0 }}</span>
</div>
</div>
<div v-if="pzRecords.length" class="space-y-2">
<h4 class="font-semibold text-sm">碰撞出险记录</h4>
<div v-for="(rec, idx) in pzRecords" :key="idx" class="bg-white rounded-lg p-3 border text-sm">
<div class="flex justify-between">
<span>{{ rec.date || '-' }}</span>
<span class="font-semibold text-red-600">{{ formatFen(rec.serviceMoney) }}</span>
</div>
<p v-if="rec.dangerSingleList?.length" class="text-xs text-gray-500 mt-1">
<span v-for="(d, di) in rec.dangerSingleList" :key="di" class="mr-2">
{{ dangerTypeText(d.dangerSingleType) }}{{ d.dangerSingleName || d.dangerSingleDesc }}
</span>
</p>
</div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无出险详版数据</div>
</div>
</template>

51
src/ui/CQCXGY7F2.vue Normal file
View File

@@ -0,0 +1,51 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj } = useVehiclePayload(props)
const hasData = computed(() => Object.keys(obj.value).length > 0)
const riskScore = computed(() => 100)
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-3">
<div class="bg-gradient-to-br from-violet-50 to-purple-50 rounded-lg p-4 border flex justify-between items-start">
<div>
<h3 class="text-lg font-bold">二手车 VIN 估值</h3>
<p class="text-sm text-gray-600 mt-1">基于车型排量排放等给出参考估值</p>
</div>
<div class="text-right bg-white rounded-lg px-3 py-2 border">
<div class="text-xs text-gray-500">估值</div>
<div class="text-lg font-bold text-violet-700">{{ obj.estimatedValue || '-' }}</div>
</div>
</div>
<template v-if="hasData">
<div class="text-center py-4 bg-violet-50 rounded-lg border border-violet-100">
<div class="text-3xl font-bold text-violet-800">{{ obj.estimatedValue || '-' }}</div>
<p class="text-xs text-gray-500 mt-1">参考估值仅供参考</p>
<p class="text-sm mt-2">{{ obj.seriesName || '未知车系' }} · {{ obj.manufacturerName || '未知厂商' }}</p>
<p v-if="obj.productionDate" class="text-sm text-gray-600">{{ obj.productionDate }} 年出厂</p>
</div>
<div class="divide-y text-sm">
<div class="flex py-2 justify-between"><span class="text-gray-500">厂商</span><span>{{ obj.manufacturerName || '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">车系</span><span>{{ obj.seriesName || '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">车型年款</span><span>{{ obj.modelYear || obj.productionDate || '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">座位数</span><span>{{ obj.seatingCapacity ?? '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">车型名称</span><span>{{ obj.modelName || '-' }}</span></div>
<div class="flex py-2 justify-between"><span class="text-gray-500">指导价</span><span>{{ obj.msrp || '-' }}</span></div>
</div>
</template>
<div v-else class="text-center py-10 text-gray-500">暂无估值结果</div>
</div>
</template>

76
src/ui/CQCXGYTS2.vue Normal file
View File

@@ -0,0 +1,76 @@
<script setup>
import { computed } from 'vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
import { maskName } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
})
const { obj, params } = useVehiclePayload(props)
const status = computed(() => {
const raw = obj.value
const s = Number(raw?.status ?? raw?.result?.status)
if ([0, -1, -2, -4].includes(s)) return s
return null
})
const resultText = computed(() => {
const s = status.value
if (s === 0) return '一致'
if (s === -1) return '不一致'
if (s === -2) return '非法姓名'
if (s === -4) return '无记录'
return '暂无结果'
})
const resultDesc = computed(() => {
const s = status.value
if (s === -2) return '姓名长度或格式不正确,请核对后重试'
if (s === -4) return '未查询到相关核验记录'
return ''
})
const sectionClass = computed(() => {
const s = status.value
if (s === 0) return 'bg-green-50 border-green-200'
if (s === -1) return 'bg-red-50 border-red-200'
if (s === -2) return 'bg-orange-50 border-orange-200'
if (s === -4) return 'bg-gray-100 border-gray-300'
return 'bg-gray-50 border-gray-200'
})
const riskScore = computed(() => {
const s = status.value
if (s === -1) return 35
if (s === -2 || s === -4) return 55
return 100
})
useRiskNotifier(props, riskScore)
</script>
<template>
<div class="card space-y-4">
<div class="bg-gradient-to-br from-indigo-600 to-indigo-800 text-white rounded-lg p-4">
<h3 class="text-lg font-bold">人车核验详版</h3>
<p class="text-sm opacity-90 mt-1">人员与车辆的详细匹配结果</p>
</div>
<div :class="['rounded-lg p-6 text-center border', sectionClass]">
<div class="text-4xl font-bold mb-2">{{ status === 0 ? '✓' : status === -1 ? '✕' : status === -2 ? '!' : '—' }}</div>
<div class="text-sm text-gray-500">认证结果</div>
<div class="text-2xl font-bold mt-1">{{ resultText }}</div>
<p v-if="resultDesc" class="text-sm text-gray-600 mt-2">{{ resultDesc }}</p>
</div>
<div class="divide-y text-sm">
<div class="flex py-2"><span class="w-24 text-gray-500">姓名</span><span>{{ maskName(params.name) }}</span></div>
<div class="flex py-2"><span class="w-24 text-gray-500">车牌号</span><span class="font-mono">{{ params.plate_no || params.car_license || '-' }}</span></div>
<div class="flex py-2"><span class="w-24 text-gray-500">号牌类型</span><span>{{ params.carplate_type || params.car_type || '-' }}</span></div>
</div>
</div>
</template>

View File

@@ -0,0 +1,63 @@
<script setup>
import { computed } from 'vue'
import { plateColorLabel, vehicleTypeLabel } from '@/utils/vehicleReportBlockMaps'
import { useVehiclePayload } from '@/composables/useVehiclePayload'
const props = defineProps({
data: Object,
params: Object,
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => {} },
title: { type: String, default: '个人名下车辆' },
})
const { obj } = useVehiclePayload(props)
const vehicleCount = computed(() => {
const n = obj.value.vehicleCount ?? obj.value.carNum
if (n === '' || n == null)
return null
const num = Number(n)
return Number.isFinite(num) ? num : null
})
const vehicleList = computed(() => {
const list = obj.value.list
return Array.isArray(list) ? list : []
})
</script>
<template>
<div class="card bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex justify-between items-center mb-4 pb-4 border-b border-gray-100">
<div class="text-lg font-semibold text-gray-900">{{ title }}</div>
<div class="bg-blue-50 text-blue-700 px-4 py-2 rounded-full text-sm font-medium">
{{ vehicleCount != null ? vehicleCount : '—' }}
</div>
</div>
<div v-if="vehicleList.length" class="space-y-3">
<div
v-for="(vehicle, index) in vehicleList"
:key="index"
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
>
<div class="text-xl font-bold text-gray-900 font-mono tracking-wider">
{{ vehicle.plateNum ?? '—' }}
</div>
<div class="flex items-center gap-3 mt-2 flex-wrap">
<span class="inline-flex items-center px-3 py-1 rounded text-sm font-medium text-white bg-blue-500">
{{ plateColorLabel(vehicle.plateColor) }}
</span>
<span class="text-sm text-gray-600">
车辆类型<span class="font-medium text-gray-900">{{ vehicleTypeLabel(vehicle.vehicleType) }}</span>
</span>
</div>
</div>
</div>
<div v-else class="text-center py-12 text-gray-500">
<div class="text-lg font-medium mb-1">暂无车辆信息</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,73 @@
/**
* 解包车辆报告业务体:兼容 BaseReport 传入的 data 仍带一层 { apiID, data } 或 JSON 字符串
*/
export function unwrapVehiclePayload(raw, depth = 0) {
if (raw == null || depth > 5)
return raw
if (typeof raw === 'string') {
const s = raw.trim()
if (!s)
return null
try {
return unwrapVehiclePayload(JSON.parse(s), depth + 1)
}
catch {
return raw
}
}
if (Array.isArray(raw))
return raw
if (typeof raw !== 'object')
return raw
const hasApiWrapper = ('apiID' in raw || 'apiId' in raw) && 'data' in raw
if (hasApiWrapper)
return unwrapVehiclePayload(raw.data ?? raw, depth + 1)
if (typeof raw.data === 'string' || (raw.data && typeof raw.data === 'object' && !Array.isArray(raw.data))) {
const inner = raw.data
const onlyDataKey = Object.keys(raw).length <= 2 && 'data' in raw
if (onlyDataKey || (typeof inner === 'object' && inner && !Array.isArray(inner) && Object.keys(inner).length > 0 && !('list' in raw) && !('vehicleCount' in raw) && !('carNum' in raw)))
return unwrapVehiclePayload(inner, depth + 1)
}
return raw
}
/** 解析为数组直连数组、list、record、retdata 等 */
export function payloadAsArray(raw) {
const p = unwrapVehiclePayload(raw)
if (!p)
return []
if (Array.isArray(p))
return p
if (typeof p !== 'object')
return []
if (Array.isArray(p.list))
return p.list
if (Array.isArray(p.record))
return p.record
if (Array.isArray(p.retdata))
return p.retdata
if (typeof p.data === 'string') {
try {
const parsed = JSON.parse(p.data)
return Array.isArray(parsed) ? parsed : []
}
catch {
return []
}
}
return []
}
/** 解析为对象 */
export function payloadAsObject(raw) {
const p = unwrapVehiclePayload(raw)
if (!p || typeof p !== 'object' || Array.isArray(p))
return {}
return p
}

View File

@@ -0,0 +1,73 @@
/** 车辆报告共用车牌颜色、车辆类型、日期里程文案 */
export const PLATE_COLOR_LABELS = {
0: '蓝色',
1: '黄色',
2: '黑色',
3: '白色',
4: '渐变绿',
5: '黄绿双拼',
6: '蓝白渐变',
7: '临牌',
11: '绿色',
12: '红色',
}
export function plateColorLabel(c) {
const n = Number(c)
if (Number.isNaN(n))
return '—'
return PLATE_COLOR_LABELS[n] ?? '其他'
}
const VEHICLE_TYPE_LABELS = {
1: '一型客车',
2: '二型客车',
3: '三型客车',
4: '四型客车',
11: '一型货车',
12: '二型货车',
13: '三型货车',
14: '四型货车',
15: '五型货车',
16: '六型货车',
21: '一型专项作业车',
22: '二型专项作业车',
23: '三型专项作业车',
24: '四型专项作业车',
25: '五型专项作业车',
26: '六型专项作业车',
}
export function vehicleTypeLabel(t) {
if (t == null || t === '')
return '—'
const n = Number(t)
if (!Number.isNaN(n) && VEHICLE_TYPE_LABELS[n])
return VEHICLE_TYPE_LABELS[n]
return String(t)
}
export function formatDateZh(val) {
if (!val)
return '-'
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/)
if (m)
return `${m[1]}${m[2]}${m[3]}`
return String(val)
}
export function formatMileageKm(val) {
if (val !== 0 && !val)
return '-'
const num = Number(val)
if (Number.isNaN(num))
return `${val} km`
return `${num.toLocaleString()} km`
}
export function maskName(name) {
if (!name)
return '-'
return name.length > 1 ? `${name[0]}${'*'.repeat(name.length - 1)}` : '*'
}

View File

@@ -0,0 +1,44 @@
import { unwrapVehiclePayload } from './vehiclePayload'
/**
* 规范 query_data 单项,供 BaseReport 使用:{ data: { apiID, data: 业务体 } }
*/
export function normalizeVehicleReportItem(item) {
if (!item || typeof item !== 'object')
return item
const outer = item.data
if (outer == null)
return item
let apiID = ''
let payload = outer
if (typeof outer === 'object' && !Array.isArray(outer)) {
apiID = String(outer.apiID ?? outer.apiId ?? '')
if ('data' in outer)
payload = outer.data
}
payload = unwrapVehiclePayload(payload)
if (!apiID && payload && typeof payload === 'object' && !Array.isArray(payload)) {
apiID = String(payload.apiID ?? payload.apiId ?? '')
if (apiID && 'data' in payload)
payload = unwrapVehiclePayload(payload.data)
}
return {
...item,
data: {
apiID: apiID || '',
data: payload,
},
}
}
export function normalizeVehicleReportData(items) {
if (!Array.isArray(items))
return []
return items.map(normalizeVehicleReportItem)
}