Files
jg_app/src/pages/trafficAlerts/detail.vue
2026-04-16 14:21:25 +08:00

820 lines
18 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="alert-detail-page">
<!-- 顶部导航栏 -->
<div class="nav-bar">
<van-icon name="arrow-left" class="nav-back" @click="goBack" />
<h1 class="nav-title">路况预警详情</h1>
<div class="nav-placeholder"></div>
</div>
<!-- 加载状态 -->
<van-loading v-if="loading" class="loading-state" color="#2563eb">加载中...</van-loading>
<template v-else>
<!-- 预警信息卡片 -->
<div class="detail-card">
<!-- 头部信息 -->
<div class="card-header">
<div class="header-left">
<span class="level-tag" :class="getLevelClass(alertDetail.eventLevel)">
{{ levelMap[alertDetail.eventLevel] }}
</span>
<span class="alert-title">{{ alertDetail.eventType }}</span>
</div>
<span class="status-tag" :class="getStatusClass(allDetail.taskStatus)">
{{ statusMap[allDetail.taskStatus] }}
</span>
</div>
<!-- 图片 -->
<div class="card-image" v-if="alertDetail.image">
<van-image
:src="alertDetail.imgUrl"
fit="cover"
class="alert-img"
/>
</div>
<!-- 详细信息 -->
<div class="card-details">
<div class="detail-item">
<van-icon name="clock" class="detail-icon" />
<span class="label">检测时间</span>
<span class="value">{{ alertDetail.eventTime }}</span>
</div>
<div class="detail-item">
<van-icon name="location" class="detail-icon" />
<span class="label">检测地点</span>
<span class="value">{{ alertDetail.siteName }}</span>
</div>
<div class="detail-item" v-if="alertDetail.tasksVo">
<van-icon name="phone-o" class="detail-icon" />
<span class="label">联系电话</span>
<span class="value">{{ alertDetail.phoneNumber }}</span>
</div>
<!-- 视频展示按钮 -->
<div class="video-btn-wrapper" v-if="alertDetail.videoUrls && alertDetail.videoUrls.length > 0">
<van-button block round class="video-btn" @click="openVideo(alertDetail.videoUrls[0])">
<van-icon name="video-o" class="video-icon" />
视频展示
</van-button>
</div>
</div>
</div>
<!-- 执行记录卡片 -->
<div class="record-card">
<div class="card-title">执行记录</div>
<!-- 蓝色边框卡片 -->
<div class="execution-box">
<!-- 绿色边框内层卡片 -->
<div class="info-box" v-if="taskDetail">
<div class="info-row">
<span class="info-label">任务地点</span>
<span class="info-value">{{ taskDetail.clickAddress }}</span>
</div>
<div class="info-row" v-if="taskDetail.deptName">
<span class="info-text">{{ taskDetail.deptName }}</span>
<span class="info-divider" v-if="taskDetail.phoneNumber">|</span>
<span class="info-text" v-if="taskDetail.phoneNumber">联系电话{{ taskDetail.phoneNumber }}</span>
</div>
</div>
<!-- 任务描述 -->
<div class="task-desc" v-if="taskDetail">
{{ taskDetail.taskContent }}
</div>
<!-- 打卡情况 -->
<div v-if="checkinRecords.length > 0" class="checkin-box">
<div v-for="(record, index) in checkinRecords" :key="index" class="checkin-row-wrapper">
<div class="checkin-row">
<span class="checkin-time">{{ record.date }}</span>
<span class="checkin-type">{{ record.type }}</span>
</div>
<div class="checkin-row">
<span class="checkin-label">打卡账号</span>
<span class="checkin-value">{{ record.account }}</span>
</div>
<div class="checkin-row checkin-location" v-if="record.location">
<van-icon name="location" class="location-icon" />
<span>{{ record.location }}</span>
</div>
</div>
</div>
<!-- 待派发状态 - 显示定位打卡按钮 -->
<div v-if="showCheckInBtn" class="action-buttons">
<van-button block round type="primary" class="action-btn" @click="handleCheckIn">
定位打卡
</van-button>
</div>
<!-- 执行中/已完成状态 - 显示反馈按钮 -->
<div v-if="showFeedbackBtns" class="feedback-buttons">
<van-button
v-for="(feedback, index) in allDetail.feebacks"
:key="index"
block
round
class="feedback-btn success"
@click="showFeedback(index + 1)"
>
{{ index+1 }}次反馈结果
</van-button>
<van-button
v-if="allDetail.taskStatus == '1'"
block
round
type="primary"
class="action-btn"
@click="handleFeedback"
>
立即反馈
</van-button>
</div>
</div>
</div>
</template>
<!-- 反馈结果弹窗 -->
<van-popup v-model:show="showResultDialog" round position="center" class="feedback-popup">
<div class="popup-content" v-if="currentFeedback.title">
<h2 class="popup-title">{{ currentFeedback.title }}</h2>
<div class="popup-info">
<p>反馈时间{{ currentFeedback.feedbackTime }}</p>
</div>
<!-- 现场照片 -->
<div class="popup-section" v-if="currentFeedback.images && currentFeedback.images.length > 0">
<p class="section-label">现场照片</p>
<div class="image-grid">
<van-image
v-for="(img, idx) in currentFeedback.images"
:key="idx"
@click.stop="ImagePreview([img])"
:src="img"
fit="cover"
class="feedback-img"
/>
</div>
</div>
<!-- 现场照片 -->
<div class="popup-section" v-if="currentFeedback.video && currentFeedback.video.length > 0">
<p class="section-label">场地视频</p>
<div class="image-grid">
<span v-for="(img, idx) in currentFeedback.video"
:key="idx">视频{{ idx+1 }}</span>
</div>
</div>
<!-- 反馈内容 -->
<div class="popup-section" v-if="currentFeedback.feedback">
<h3 class="content-title">反馈内容</h3>
<p class="content-text">{{ currentFeedback.feedback }}</p>
</div>
<!-- 关闭按钮 -->
<van-button block round class="close-btn" @click="closePopup">
关闭
</van-button>
</div>
</van-popup>
<!-- 视频播放弹窗 -->
<van-popup v-model:show="showVideoDialog" round position="center" class="video-popup" :close-on-click-overlay="true">
<div class="video-content">
<video
v-if="currentVideoUrl"
:src="currentVideoUrl"
controls
autoplay
class="video-player"
/>
<p v-if="!currentVideoUrl" class="video-tip">暂无视频</p>
<van-icon name="cross" class="video-close" @click="showVideoDialog = false" />
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { getTrafficEventDetail } from "@/api/traffic";
import { ImagePreview } from 'vant';
const router = useRouter();
const route = useRoute();
// 获取URL参数
const taskId = route.query.id || "";
const statusText = route.query.status || "";
// 加载状态
const loading = ref(false);
// 预警详情数据
const alertDetail = ref({});
const allDetail=ref({})
const taskDetail=ref({})
// 打卡记录
const checkinRecords = ref([]);
// 反馈数据
const feedbackList = ref([]);
// 反馈弹窗状态
const showResultDialog = ref(false);
const currentFeedback = ref({});
// 视频弹窗状态
const showVideoDialog = ref(false);
const currentVideoUrl = ref("");
// 状态文本映射
const statusMap = {
0: "待执行",
1: "执行中",
2: "已完成",
};
// 等级映射
const levelMap = {
0: "一级",
1: "二级",
2: "三级",
3: "四级"
};
// 等级样式
function getLevelClass(level) {
const map = {
1: "level-red",
2: "level-orange",
3: "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 formatTime(timestamp) {
if (!timestamp) return "";
const date = new Date(timestamp);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${month}/${day} ${hours}:${minutes}`;
}
// 获取详情数据
const fetchDetail = async () => {
if (!taskId) return;
loading.value = true;
try {
const res = await getTrafficEventDetail(taskId);
console.log(res, '详情res');
if (res) {
const data = res.data || res;
// 处理详情数据
alertDetail.value = data.eventDetailVO;
taskDetail.value = data.tasksVo;
allDetail.value = data
// 处理打卡记录
checkinRecords.value = (data.checkinRecords || data.signList || []).map(item => ({
date: formatTime(item.signTime || item.createTime),
type: item.type === 1 ? "【签到打卡】" : "【签退打卡】",
account: item.userName || item.account || "",
location: item.address || item.location || ""
}));
// 处理反馈列表
feedbackList.value = (data.feedbackList || data.reportList || []).map((item, index) => ({
title: `${index + 1}反馈结果`,
time: formatTime(item.reportTime || item.createTime),
content: item.reportContent || item.content || "",
images: item.imageUrls || item.images || []
}));
}
} catch (error) {
console.error("获取详情失败:", error);
} finally {
loading.value = false;
}
};
// 返回
function goBack() {
router.back();
}
// 定位打卡
function handleCheckIn() {
router.push({
path: "/checkInPage",
query: { id: taskId }
});
}
// 立即反馈
function handleFeedback() {
router.push({
path: "/alert-handle",
query: { id: taskId, status: alertDetail.value.status }
});
}
// 显示反馈弹窗
function showFeedback(index) {
if (allDetail.value.feebacks[index - 1]) {
currentFeedback.value = JSON.parse(JSON.stringify(allDetail.value.feebacks[index - 1]));
if(typeof currentFeedback.value.images=='string' && currentFeedback.value.images){
currentFeedback.value.images = currentFeedback.value.images.split(',')
}
if(typeof currentFeedback.value.video=='string' && currentFeedback.value.video){
currentFeedback.value.video = currentFeedback.value.video.split(',')
}
currentFeedback.value.title=`${index}次反馈结果`
showResultDialog.value = true;
}
}
//预览图片
function onClickImg(url) {
ImagePreview([url]);
}
// 打开视频弹窗
function openVideo(url) {
currentVideoUrl.value = url;
showVideoDialog.value = true;
}
// 关闭弹窗
function closePopup() {
showResultDialog.value = false;
}
// 判断按钮显示
const showCheckInBtn = computed(() => {
return allDetail.value.taskStatus == "0";
});
const showFeedbackBtns = computed(() => {
return allDetail.value.taskStatus == "1" || allDetail.value.taskStatus == "2";
});
// 页面初始化
onMounted(() => {
fetchDetail();
});
</script>
<style lang="scss" scoped>
.alert-detail-page {
min-height: 100vh;
background: #f1f5f9;
padding-bottom: 100px;
}
.nav-bar {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
background: white;
border-bottom: 1px solid #e5e5e5;
padding: 12px 16px;
.nav-back {
font-size: 24px;
color: #333;
}
.nav-title {
font-size: 17px;
font-weight: 600;
color: #333;
}
.nav-placeholder {
width: 24px;
}
}
.detail-card,
.record-card {
margin: 16px;
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.detail-card {
padding: 16px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.level-tag {
padding: 4px 8px;
border-radius: 8px;
font-size: 12px;
&.level-red {
background: #fee2e2;
color: #dc2626;
}
&.level-orange {
background: #ffedd5;
color: #ea580c;
}
&.level-blue {
background: #dbeafe;
color: #2563eb;
}
}
.alert-title {
font-size: 15px;
font-weight: 600;
color: #333;
}
.status-tag {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
&.status-gray {
background: #f1f5f9;
color: #64748b;
border: 1px solid #e2e8f0;
}
&.status-orange {
background: #fff7ed;
color: #ea580c;
border: 1px solid #fed7aa;
}
&.status-blue {
background: #eff6ff;
color: #2563eb;
border: 1px solid #bfdbfe;
}
&.status-green {
background: #f0fdf4;
color: #16a34a;
border: 1px solid #bbf7d0;
}
}
}
.card-image {
margin-bottom: 12px;
.alert-img {
width: 100%;
height: 180px;
border-radius: 12px;
}
}
.card-details {
.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;
}
}
.video-btn-wrapper {
margin-top: 12px;
.video-btn {
height: 40px;
background: #2563eb;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
color: white;
.video-icon {
margin-right: 6px;
}
}
}
}
.record-card {
padding: 16px;
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
}
.execution-box {
background: #eff6ff;
border: 2px solid #2563eb;
border-radius: 16px;
padding: 16px;
}
.info-box {
background: #f0fdf4;
border: 2px solid #16a34a;
border-radius: 12px;
padding: 10px;
.info-row {
display: flex;
flex-wrap: wrap;
margin-bottom: 6px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
.info-label {
color: #64748b;
}
.info-value {
color: #333;
font-weight: 500;
}
.info-text {
color: #64748b;
}
.info-divider {
color: #d1d5db;
margin: 0 4px;
}
}
}
.task-desc {
margin-top: 12px;
font-size: 15px;
color: #333;
font-weight: 600;
line-height: 1.5;
padding: 0 4px;
}
.checkin-box {
margin-top: 12px;
background: #f9fafb;
border-radius: 12px;
padding: 12px;
.checkin-row {
display: flex;
flex-wrap: wrap;
margin-bottom: 8px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
.checkin-date,
.checkin-time {
color: #64748b;
}
.checkin-type {
color: #475569;
margin-left: 8px;
}
.checkin-label {
color: #64748b;
}
.checkin-value {
color: #333;
}
&.checkin-location {
color: #475569;
.location-icon {
font-size: 14px;
margin-right: 4px;
margin-top: 2px;
}
}
}
}
.feedback-buttons,
.action-buttons {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 8px;
.feedback-btn {
height: 40px;
background: #2563eb;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
color: white;
&.success {
background: #16a34a;
}
}
.action-btn {
height: 40px;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
}
}
.feedback-popup {
width: 90%;
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
}
.popup-content {
padding: 24px;
width: 70vw;
}
.popup-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.popup-info {
margin-bottom: 16px;
font-size: 14px;
color: #64748b;
}
.popup-section {
margin-bottom: 16px;
.section-label {
font-size: 14px;
color: #64748b;
margin-bottom: 8px;
}
.content-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.content-text {
font-size: 14px;
color: #64748b;
line-height: 1.6;
}
}
.image-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
.feedback-img {
width: 100%;
height: 120px;
border-radius: 8px;
}
}
.video-item {
display: flex;
align-items: center;
gap: 8px;
background: #f9fafb;
padding: 12px;
border-radius: 8px;
.video-icon {
font-size: 20px;
color: #2563eb;
}
span {
font-size: 14px;
color: #64748b;
}
}
.close-btn {
height: 40px;
background: #e5e7eb;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
color: #333;
margin-top: 16px;
&:active {
background: #d1d5db;
}
}
.video-content {
position: relative;
width: 90vw;
.video-player {
width: 100%;
display: block;
max-height: 70vh;
}
.video-tip {
text-align: center;
color: #999;
padding: 40px 0;
margin: 0;
}
.video-close {
position: absolute;
top: 8px;
right: 8px;
font-size: 20px;
color: #fff;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
padding: 4px;
z-index: 1;
cursor: pointer;
}
}
</style>