375 lines
10 KiB
Vue
375 lines
10 KiB
Vue
<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>
|