Files
report_viewer/src/ui/CQCXG1U4U.vue
2026-05-13 11:01:42 +08:00

375 lines
10 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>