Files
report_viewer/src/ui/CQCXG1U4U.vue

375 lines
10 KiB
Vue
Raw Normal View History

2026-05-13 11:01:42 +08:00
<template>
<div class="card">
<div class="header-box">
<div class="header-left">
<h3 class="header-title">车辆里程记录混合查询</h3>
<p class="header-desc">综合诊断与维保记录展示车辆里程变化与是否存在调表嫌疑</p>
</div>
</div>
<template v-if="hasData">
<!-- 概览里程是否异常 + 最新里程 -->
<div class="summary-card" :class="suspectClass">
<div class="summary-main">
<div class="summary-left">
<div class="vin-label">VIN</div>
<div class="vin-value font-mono">{{ vin || '-' }}</div>
</div>
<div class="summary-right">
<div class="summary-label">最新里程</div>
<div class="summary-mileage">{{ latestMileageText }}</div>
<div class="summary-sub">最近记录日期{{ latestReportTime || '-' }}</div>
</div>
</div>
<div class="summary-meta">
<div class="meta-line">
<span>里程是否异常</span>
<span class="strong" :class="suspectClass">{{ suspectedText }}</span>
</div>
<div class="meta-line" v-if="imageUrl">
<span>行驶证图片</span>
</div>
<div v-if="imageUrl" class="image-wrap">
<img :src="imageUrl" alt="行驶证图片" class="licence-img" />
</div>
</div>
</div>
<!-- 里程时间轴 -->
<div class="detail-card">
<h4 class="section-title">里程记录时间轴</h4>
<div v-if="mileageList && mileageList.length" class="timeline">
<div v-for="(item, idx) in mileageList" :key="idx" class="timeline-item">
<div class="timeline-left">
<div class="dot-wrap">
<div class="dot" :class="item.mileageStatus === '1' ? 'dot-abnormal' : 'dot-normal'">
</div>
<div v-if="idx !== mileageList.length - 1" class="line"></div>
</div>
</div>
<div class="timeline-content">
<div class="row-main">
<div class="date">{{ formatDate(item.reportTime) }}</div>
<div class="km">{{ formatMileage(item.mileage) }}</div>
</div>
<div class="row-sub">
<span>来源{{ sourceText(item.source) }}</span>
<span v-if="item.mileageStatus === '1'" class="badge-abnormal">异常里程</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-small">
暂无里程记录
</div>
</div>
<!-- 异常里程列表 -->
<div class="detail-card" v-if="adjustList && adjustList.length">
<h4 class="section-title">疑似调表记录</h4>
<div class="adjust-list">
<div v-for="(item, idx) in adjustList" :key="idx" class="adjust-item">
<div class="adjust-time">{{ formatDate(item.reportTime) }}</div>
<div class="adjust-body">
<div>
<div class="adjust-label">调整前</div>
<div class="adjust-value">{{ formatMileage(item.beforeMileage) }}</div>
</div>
<div class="adjust-arrow"></div>
<div>
<div class="adjust-label">调整后</div>
<div class="adjust-value">{{ formatMileage(item.afterMileage) }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<div v-else class="empty">
<div class="icon"></div>
<div class="title">暂无里程数据</div>
<div class="sub">未查询到车辆里程记录或返回数据为空</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const hasData = computed(() => !!props.data && Object.keys(props.data).length > 0);
const vin = computed(() => props.data?.vehicleInfo?.vin || '');
const mileageList = computed(() => props.data?.mileageInfo?.mileageList || []);
const adjustList = computed(() => props.data?.mileageInfo?.suspectedAdjustMileageList || []);
const suspectedAdjust = computed(() => props.data?.mileageInfo?.suspectedAdjust);
const imageUrl = computed(() => props.data?.imageUrl || '');
const latestRecord = computed(() => {
const list = mileageList.value;
if (!list || !list.length) return null;
// 默认接口数据已按时间升序,直接取最后一条
return list[list.length - 1];
});
const latestMileageText = computed(() => {
if (!latestRecord.value) return '-';
return formatMileage(latestRecord.value.mileage);
});
const latestReportTime = computed(() => latestRecord.value?.reportTime || '');
const suspectedText = computed(() => {
if (suspectedAdjust.value === 'true') return '存在异常里程行为';
if (suspectedAdjust.value === 'false') return '未发现里程异常';
return '未知';
});
const suspectClass = computed(() => {
if (suspectedAdjust.value === 'true') return 'suspect-yes';
if (suspectedAdjust.value === 'false') return 'suspect-no';
return 'suspect-unknown';
});
const formatMileage = (val) => {
if (!val && val !== 0) return '-';
const num = Number(val);
if (Number.isNaN(num)) return `${val} km`;
// 默认 km按 km 展示
return `${num.toLocaleString()} km`;
};
const sourceText = (source) => {
if (source === '0') return '诊断里程';
if (source === '1') return '维保里程';
return '其他';
};
const formatDate = (val) => {
if (!val) return '-';
// 期望格式 yyyy-MM-dd
const m = String(val).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) {
return `${m[1]}${m[2]}${m[3]}`;
}
return val;
};
const riskScore = computed(() => 100);
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
</script>
<style scoped>
.card {
@apply bg-white rounded-2xl p-6 shadow-sm border border-gray-100;
}
.header-box {
@apply flex items-center mb-4 px-5 py-4 rounded-2xl bg-gradient-to-r from-sky-50 via-blue-50 to-indigo-50;
}
.header-left {
@apply flex flex-col;
}
.header-title {
@apply text-2xl font-semibold m-0 text-sky-900;
}
.header-desc {
@apply text-base mt-3 m-0 text-sky-800 opacity-90;
}
.summary-card {
@apply rounded-2xl border px-5 py-4 mb-4;
}
.summary-card.suspect-yes {
@apply bg-amber-50 border-amber-100;
}
.summary-card.suspect-no {
@apply bg-emerald-50 border-emerald-100;
}
.summary-card.suspect-unknown {
@apply bg-gray-50 border-gray-200;
}
.summary-main {
@apply flex flex-col mb-3 gap-3;
}
.summary-left {
@apply flex flex-col gap-1;
}
.vin-label {
@apply text-sm text-gray-500;
}
.vin-value {
@apply text-base text-gray-900;
}
.summary-right {
@apply text-left;
}
.summary-label {
@apply text-sm text-gray-500;
}
.summary-mileage {
@apply text-2xl font-bold text-sky-800 mt-1;
}
.summary-sub {
@apply text-sm text-sky-700 opacity-90;
}
.summary-meta {
@apply space-y-1 text-base text-gray-800;
}
.meta-line {
@apply flex flex-wrap items-center gap-2;
}
.meta-line .strong {
@apply font-semibold;
}
.image-wrap {
@apply mt-2 flex justify-center;
}
.licence-img {
@apply rounded-xl border border-gray-200 max-w-full;
max-height: 220px;
object-fit: contain;
}
.detail-card {
@apply rounded-2xl border border-gray-100 bg-gray-50/60 px-5 py-4 mb-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.timeline {
@apply mt-2;
}
.timeline-item {
@apply flex mb-3;
}
.timeline-left {
@apply mr-3 flex flex-col items-center;
}
.dot-wrap {
@apply flex flex-col items-stretch;
}
.dot {
@apply w-3 h-3 rounded-full bg-gray-400 self-center;
}
.dot-normal {
@apply bg-emerald-500;
}
.dot-abnormal {
@apply bg-red-500;
}
.line {
@apply flex-1 w-px bg-gray-300 mx-auto;
}
.timeline-content {
@apply flex-1 rounded-2xl border border-gray-100 bg-white px-4 py-3;
}
.row-main {
@apply flex items-baseline justify-between mb-1;
}
.row-main .date {
@apply text-base font-medium text-gray-900;
}
.row-main .km {
@apply text-lg font-semibold text-gray-900;
}
.row-sub {
@apply flex items-center justify-between text-sm text-gray-600 mt-1;
}
.badge-abnormal {
@apply inline-flex items-center px-2 py-0.5 rounded-full bg-red-50 text-red-700 text-xs font-medium;
}
.adjust-list {
@apply space-y-3;
}
.adjust-item {
@apply rounded-2xl border border-amber-100 bg-amber-50/70 px-4 py-3;
}
.adjust-time {
@apply text-sm text-gray-700 mb-2;
}
.adjust-body {
@apply flex items-center justify-between gap-4;
}
.adjust-label {
@apply text-xs text-gray-500 mb-1;
}
.adjust-value {
@apply text-base font-semibold text-gray-900;
}
.adjust-arrow {
@apply text-2xl text-gray-400;
}
.empty {
@apply text-center py-10 text-gray-500;
}
.empty-small {
@apply text-center py-6 text-sm text-gray-500;
}
.empty .icon {
@apply text-3xl mb-2;
}
.empty .title {
@apply text-lg font-medium mb-1;
}
.empty .sub {
@apply text-sm;
}
</style>