Files
jg_app/src/pages/violationAlerts/detail.vue
2026-04-27 19:05:11 +08:00

744 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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="violation-detail-page">
<TopNav title="违规任务详情"></TopNav>
<div class="constBox">
<!-- 违章信息卡片 -->
<div class="detail-card">
<!-- 车牌号和状态 -->
<div class="card-header">
<div class="vehicle-info">
<van-icon name="cart" class="vehicle-icon" />
<span class="plate-number">{{ violationDetail.plateNo }}</span>
<span class="plate-color">{{ violationDetail.plateColor }}</span>
<span class="separator">|</span>
<span class="vehicle-type">{{ violationDetail.vehicleType }}</span>
</div>
<span class="status-tag" :class="getStatusClass(allDetail.taskStatus)"> {{ statusMap[allDetail.taskStatus] }} </span>
</div>
<!-- 等级和标题 -->
<div class="title-row">
<span class="level-tag" :class="getLevelClass(violationDetail.eventLevel)">
{{ levelMap[violationDetail.eventLevel] }}
</span>
<span class="alert-title">{{ violationDetail.eventContent }}</span>
</div>
<!-- 图片 -->
<div class="card-image">
<van-image :src="violationDetail.imgUrls" fit="cover" class="alert-img" />
</div>
<!-- 检测信息 -->
<div class="card-details">
<div class="detail-item">
<van-icon name="location" class="detail-icon" />
<span class="label">检测点位</span>
<span class="value">{{ violationDetail.siteName }}</span>
</div>
<div class="detail-item">
<van-icon name="clock" class="detail-icon" />
<span class="label">检测时间</span>
<span class="value">{{ violationDetail.eventTime }}</span>
</div>
</div>
</div>
<!-- 卡口预警折叠面板 -->
<van-collapse v-model="activeNames" :border="false" class="alert-collapse">
<van-collapse-item name="1" :border="false">
<template #title><div class="collapse-title">卡口预警</div></template>
<van-list :finished="true" finished-text="没有更多了" >
<div class="alert-list">
<div v-for="(item, index) in checkinAlerts" :key="item.id" class="alert-item">
<!-- 顶部序号和基本信息 -->
<div class="item-header">
<div class="item-index">{{ index + 1 }}</div>
<div class="item-info">
<div class="info-row">
<van-icon name="location-o" class="info-icon" />
<span class="label">检测点位</span>
<span class="value">{{ item.siteName }}</span>
</div>
<div class="info-row">
<van-icon name="clock-o" class="info-icon" />
<span class="label">检测时间</span>
<span class="value">{{ item.passTime }}</span>
</div>
</div>
</div>
<!-- 抓拍图片区域 -->
<div class="capture-section">
<div class="section-title">抓拍图片</div>
<van-image :src="item.imgUrl" fit="cover" class="capture-img" radius="12" />
</div>
<!-- 详细网格信息 -->
<div class="detail-grid">
<div class="grid-item">
<span class="label">速度</span>
<span class="value">{{ item.speed }}</span>
</div>
<div class="grid-item">
<span class="label">车道号</span>
<span class="value">{{ item.laneNo }}</span>
</div>
<div class="grid-item">
<span class="label">方向</span>
<span class="value">{{ item.direction }}</span>
</div>
<div class="grid-item">
<van-icon name="user-o" class="grid-icon" />
<span class="label">执法人员</span>
<span class="value">{{ item.userName }}</span>
</div>
<div class="grid-item">
<span class="label">工号</span>
<span class="value">{{ item.workNo }}</span>
</div>
</div>
<!-- 分割线 -->
<div v-if="index < checkinAlerts.length - 1" class="item-divider"></div>
</div>
</div>
</van-list>
</van-collapse-item>
</van-collapse>
<!-- 打卡情况 - 执行中和已完成状态显示 -->
<div v-if="allDetail.taskStatus == '1' || allDetail.taskStatus == '2'" class="checkin-card">
<div class="checkin-time-row">
<span class="checkin-date">{{allDetail.clickTime}}</span>
<span class="checkin-type">定位打卡</span>
</div>
<div class="checkin-row">
<span class="checkin-label">打卡账号</span>
<span class="checkin-value">21515800</span>
</div>
<div class="checkin-row checkin-location">
<van-icon name="location" class="location-icon" />
<span>{{allDetail.clickAddress}}</span>
</div>
<!-- 未拦截成功标识 -->
<div v-if="violationId !== '1' && violationId !== '3' && allDetail.taskStatus == '1'" class="intercept-status">
<span v-if="violationId === '6'" class="result-btn" @click="goToResult">处罚结果</span>
<span v-else class="fail-tag">未拦截成功</span>
</div>
</div>
</div>
<!-- 底部按钮 - 未执行状态 -->
<div v-if="allDetail.taskStatus == '0'" class="action-bar">
<van-button block round type="primary" class="action-btn" @click="handleCheckIn">定位打卡</van-button>
</div>
<!-- 底部按钮 - 执行中状态 -->
<div v-if="allDetail.taskStatus == '1'" class="action-bar">
<van-button block round type="primary" class="action-btn" @click="showResultDialog">执行结果</van-button>
</div>
<!-- 执行结果弹框 -->
<van-popup v-model:show="showMatchDialog" round class="result-popup" :style="{ width: '80%' }">
<div class="popup-content">
<h2 class="popup-title">执行结果</h2>
<div class="popup-buttons">
<van-button block round color="#00bc4a" class="popup-btn success" @click="handlePlateMatch(true)">
<van-icon name="checked" class="btn-icon" />
处罚上报
</van-button>
<van-button block round class="popup-btn error" @click="handlePlateMatch(false)">
<van-icon name="cross" class="btn-icon" />
未拦截成功
</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import TopNav from "@/components/topNav.vue";
import { ref, computed, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { getTrafficEventDetail, interceptNotSuccess } from "@/api/traffic";
const router = useRouter();
const route = useRoute();
// 获取URL参数
const violationId = route.query.id || "1";
const status = route.query.status || "未执行";
// 加载状态
const loading = ref(false);
// 执行结果弹框状态
const showMatchDialog = ref(false);
// 状态映射
const statusMap = {
0: "待执行",
1: "执行中",
2: "已完成",
};
// 等级映射
const levelMap = {
0: "一级",
1: "二级",
2: "三级",
3: "四级"
};
// 根据ID获取详情
const violationDetail = ref({})
const allDetail = ref({})
const taskDetail = ref({})
// 卡口预警模拟数据
const checkinAlerts = ref([])
const activeNames = ref(['1']) // 默认展开第一个面板
// 获取详情数据
const fetchDetail = async () => {
if (!violationId) return;
loading.value = true;
try {
const res = await getTrafficEventDetail(violationId);
if (res) {
const data = res.data || res;
// 处理详情数据
violationDetail.value = data.eventDetailVO;
taskDetail.value = data.eventDetailVO.tasksVo;
allDetail.value = data
checkinAlerts.value = data.interceptWarns || [];
}
} catch (error) {
console.error("获取详情失败:", error);
} finally {
loading.value = false;
}
};
// 状态标签样式
const statusTagClass = computed(() => {
if (status === "执行中") return "status-blue";
if (status === "已完成") return "status-green";
return "status-gray";
});
// 等级样式
function getLevelClass(level) {
const map = {
1: "level-red",
2: "level-orange",
3: "level-yellow",
4: "level-blue",
};
return map[level] || "level-blue";
}
// 状态样式
function getStatusClass(status) {
const map = {
0: "status-gray",
1: "status-blue",
2: "status-green",
};
return map[status] || "status-gray";
}
// 定位打卡
function handleCheckIn() {
router.push({
path: "/checkInPage",
query: { id: violationId }
});
}
// 显示执行结果弹框
function showResultDialog() {
showMatchDialog.value = true;
}
// 前往处罚结果
function goToResult() {
router.push({
path: "/dataReport",
query: { id: violationId, status: status }
});
}
// 处理车牌匹配结果
function handlePlateMatch(matched) {
showMatchDialog.value = false;
if (matched) {
router.push({
path: "/dataReport",
query: { id: violationId, status: status }
});
} else {
// 未拦截成功
interceptNotSuccess(violationId).then(() => {
router.back();
});
}
}
// 页面初始化
onMounted(() => {
fetchDetail();
});
</script>
<style lang="scss" scoped>
.violation-detail-page {
height: 100vh;
background: #f1f5f9;
.constBox{
height: calc(100% - 13vw - 74px - env(safe-area-inset-bottom));
overflow: hidden;
overflow-y: auto;
}
}
.detail-card {
margin: 16px;
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 16px 12px;
}
.vehicle-info {
display: flex;
align-items: center;
gap: 6px;
.vehicle-icon {
font-size: 18px;
color: #2563eb;
}
.plate-number {
font-size: 15px;
font-weight: 600;
color: #333;
}
.plate-color,
.vehicle-type {
font-size: 14px;
color: #64748b;
}
.separator {
color: #d1d5db;
margin: 0 2px;
}
}
.status-tag {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
&.status-gray {
background: #f1f5f9;
color: #64748b;
border: 1px solid #e2e8f0;
}
&.status-blue {
background: #eff6ff;
color: #2563eb;
border: 1px solid #bfdbfe;
}
&.status-green {
background: #f0fdf4;
color: #16a34a;
border: 1px solid #bbf7d0;
}
}
.title-row {
display: flex;
align-items: center;
gap: 8px;
padding: 0 16px 12px;
.level-tag {
padding: 4px 8px;
border-radius: 8px;
font-size: 12px;
&.level-red {
background: #fee2e2;
color: #dc2626;
}
&.level-orange {
background: #ffedd5;
color: #ea580c;
}
&.level-yellow {
background: #fef9c3;
color: #ca8a04;
}
&.level-blue {
background: #dbeafe;
color: #2563eb;
}
}
.alert-title {
font-size: 15px;
font-weight: 500;
color: #333;
}
}
.card-image {
padding: 0 16px 12px;
.alert-img {
width: 100%;
height: 180px;
border-radius: 12px;
}
}
.card-details {
padding: 0 16px 16px;
.detail-item {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
.detail-icon {
font-size: 16px;
color: #2563eb;
margin-right: 8px;
}
.label {
color: #94a3b8;
}
.value {
color: #475569;
}
}
}
.checkin-card {
margin: 0 16px 16px;
background: white;
border-radius: 20px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.checkin-time-row {
margin-bottom: 12px;
font-size: 14px;
.checkin-date {
color: #64748b;
}
.checkin-type {
color: #2563eb;
margin-left: 8px;
}
}
.checkin-row {
margin-bottom: 12px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
.checkin-label {
color: #64748b;
}
.checkin-value {
color: #333;
}
&.checkin-location {
color: #475569;
display: flex;
align-items: flex-start;
gap: 6px;
.location-icon {
font-size: 16px;
color: #94a3b8;
margin-top: 2px;
}
}
}
.intercept-status {
margin-top: 12px;
.result-btn {
display: block;
width: 100%;
height: 44px;
background: #2563eb;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 500;
color: white;
text-align: center;
line-height: 44px;
}
.fail-tag {
color: #ef4444;
font-size: 14px;
}
}
.result-btn {
height: 44px;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 500;
color: white;
margin-top: 16px;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid #e5e5e5;
padding: 12px 16px;
padding-bottom: calc(12px + env(safe-area-inset-bottom));
.action-btn {
height: 48px;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border: none;
border-radius: 16px;
font-size: 16px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
}
.result-popup {
width: 90%;
max-width: 360px;
}
.popup-content {
padding: 24px;
}
.popup-title {
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 20px;
}
.popup-buttons {
display: flex;
flex-direction: column;
gap: 12px;
.popup-btn {
height: 48px;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
&.success {
background: #16a34a;
border: none;
color: white;
}
&.error {
background: white;
border: 1px solid #ef4444;
color: #ef4444;
}
.btn-icon {
font-size: 20px;
}
}
}
.alert-collapse {
margin: 0 16px 16px;
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.van-collapse-item__title) {
padding: 16px;
align-items: center;
background: transparent;
&::after {
display: none;
}
}
:deep(.van-collapse-item__content) {
padding: 0 16px 16px;
color: #333;
}
.collapse-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.alert-list {
.alert-item {
padding-top: 12px;
.item-header {
display: flex;
gap: 12px;
margin-bottom: 16px;
.item-index {
width: 20px;
height: 20px;
background: #3b82f6;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
flex-shrink: 0;
margin-top: 2px;
}
.item-info {
flex: 1;
.info-row {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
.info-icon {
font-size: 16px;
color: #94a3b8;
margin-right: 8px;
}
.label {
color: #64748b;
min-width: 70px;
}
.value {
color: #333;
font-weight: 500;
}
}
}
}
.capture-section {
margin-bottom: 16px;
.section-title {
font-size: 14px;
color: #64748b;
margin-bottom: 8px;
}
.capture-img {
width: 120px;
height: 80px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 16px;
font-size: 14px;
.grid-item {
display: flex;
align-items: center;
.grid-icon {
font-size: 16px;
color: #94a3b8;
margin-right: 4px;
}
.label {
color: #64748b;
}
.value {
color: #333;
font-weight: 500;
}
}
}
.item-divider {
height: 1px;
background: #f1f5f9;
margin: 20px 0;
}
}
}
</style>