Files
jg_app/src/pages/trafficAlerts/detail.vue

820 lines
18 KiB
Vue
Raw Normal View History

2026-04-10 17:10:36 +08:00
<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>
2026-04-16 14:21:25 +08:00
<!-- 加载状态 -->
<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] }}
2026-04-10 17:10:36 +08:00
</span>
</div>
2026-04-16 14:21:25 +08:00
<!-- 图片 -->
<div class="card-image" v-if="alertDetail.image">
<van-image
:src="alertDetail.imgUrl"
fit="cover"
class="alert-img"
/>
2026-04-10 17:10:36 +08:00
</div>
2026-04-16 14:21:25 +08:00
<!-- 详细信息 -->
<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>
2026-04-10 17:10:36 +08:00
</div>
2026-04-16 14:21:25 +08:00
<div class="detail-item">
<van-icon name="location" class="detail-icon" />
<span class="label">检测地点</span>
<span class="value">{{ alertDetail.siteName }}</span>
2026-04-10 17:10:36 +08:00
</div>
2026-04-16 14:21:25 +08:00
<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>
2026-04-10 17:10:36 +08:00
</div>
2026-04-16 14:21:25 +08:00
<!-- 视频展示按钮 -->
<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>
2026-04-10 17:10:36 +08:00
</div>
2026-04-16 14:21:25 +08:00
</div>
2026-04-10 17:10:36 +08:00
2026-04-16 14:21:25 +08:00
<!-- 执行记录卡片 -->
<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>
2026-04-10 17:10:36 +08:00
</div>
2026-04-16 14:21:25 +08:00
<!-- 任务描述 -->
<div class="task-desc" v-if="taskDetail">
{{ taskDetail.taskContent }}
2026-04-10 17:10:36 +08:00
</div>
2026-04-16 14:21:25 +08:00
<!-- 打卡情况 -->
<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>
2026-04-10 17:10:36 +08:00
2026-04-16 14:21:25 +08:00
<!-- 待派发状态 - 显示定位打卡按钮 -->
<div v-if="showCheckInBtn" class="action-buttons">
<van-button block round type="primary" class="action-btn" @click="handleCheckIn">
定位打卡
</van-button>
</div>
2026-04-10 17:10:36 +08:00
2026-04-16 14:21:25 +08:00
<!-- 执行中/已完成状态 - 显示反馈按钮 -->
<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>
2026-04-10 17:10:36 +08:00
</div>
</div>
2026-04-16 14:21:25 +08:00
</template>
2026-04-10 17:10:36 +08:00
<!-- 反馈结果弹窗 -->
<van-popup v-model:show="showResultDialog" round position="center" class="feedback-popup">
2026-04-16 14:21:25 +08:00
<div class="popup-content" v-if="currentFeedback.title">
2026-04-10 17:10:36 +08:00
<h2 class="popup-title">{{ currentFeedback.title }}</h2>
<div class="popup-info">
2026-04-16 14:21:25 +08:00
<p>反馈时间{{ currentFeedback.feedbackTime }}</p>
2026-04-10 17:10:36 +08:00
</div>
<!-- 现场照片 -->
2026-04-16 14:21:25 +08:00
<div class="popup-section" v-if="currentFeedback.images && currentFeedback.images.length > 0">
2026-04-10 17:10:36 +08:00
<p class="section-label">现场照片</p>
<div class="image-grid">
<van-image
v-for="(img, idx) in currentFeedback.images"
:key="idx"
2026-04-16 14:21:25 +08:00
@click.stop="ImagePreview([img])"
2026-04-10 17:10:36 +08:00
:src="img"
fit="cover"
class="feedback-img"
/>
</div>
</div>
2026-04-16 14:21:25 +08:00
<!-- 现场照片 -->
<div class="popup-section" v-if="currentFeedback.video && currentFeedback.video.length > 0">
2026-04-10 17:10:36 +08:00
<p class="section-label">场地视频</p>
2026-04-16 14:21:25 +08:00
<div class="image-grid">
<span v-for="(img, idx) in currentFeedback.video"
:key="idx">视频{{ idx+1 }}</span>
2026-04-10 17:10:36 +08:00
</div>
</div>
<!-- 反馈内容 -->
2026-04-16 14:21:25 +08:00
<div class="popup-section" v-if="currentFeedback.feedback">
2026-04-10 17:10:36 +08:00
<h3 class="content-title">反馈内容</h3>
2026-04-16 14:21:25 +08:00
<p class="content-text">{{ currentFeedback.feedback }}</p>
2026-04-10 17:10:36 +08:00
</div>
<!-- 关闭按钮 -->
<van-button block round class="close-btn" @click="closePopup">
关闭
</van-button>
</div>
</van-popup>
2026-04-16 14:21:25 +08:00
<!-- 视频播放弹窗 -->
<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>
2026-04-10 17:10:36 +08:00
</div>
</template>
<script setup>
2026-04-16 14:21:25 +08:00
import { ref, computed, onMounted } from "vue";
2026-04-10 17:10:36 +08:00
import { useRouter, useRoute } from "vue-router";
2026-04-16 14:21:25 +08:00
import { getTrafficEventDetail } from "@/api/traffic";
import { ImagePreview } from 'vant';
2026-04-10 17:10:36 +08:00
const router = useRouter();
const route = useRoute();
// 获取URL参数
2026-04-16 14:21:25 +08:00
const taskId = route.query.id || "";
const statusText = route.query.status || "";
2026-04-10 17:10:36 +08:00
2026-04-16 14:21:25 +08:00
// 加载状态
const loading = ref(false);
2026-04-10 17:10:36 +08:00
// 预警详情数据
2026-04-16 14:21:25 +08:00
const alertDetail = ref({});
const allDetail=ref({})
const taskDetail=ref({})
// 打卡记录
const checkinRecords = ref([]);
2026-04-10 17:10:36 +08:00
// 反馈数据
2026-04-16 14:21:25 +08:00
const feedbackList = ref([]);
// 反馈弹窗状态
const showResultDialog = ref(false);
const currentFeedback = ref({});
// 视频弹窗状态
const showVideoDialog = ref(false);
const currentVideoUrl = ref("");
// 状态文本映射
const statusMap = {
0: "待执行",
1: "执行中",
2: "已完成",
2026-04-10 17:10:36 +08:00
};
2026-04-16 14:21:25 +08:00
// 等级映射
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;
}
};
2026-04-10 17:10:36 +08:00
// 返回
function goBack() {
router.back();
}
// 定位打卡
function handleCheckIn() {
router.push({
path: "/checkInPage",
2026-04-16 14:21:25 +08:00
query: { id: taskId }
2026-04-10 17:10:36 +08:00
});
}
// 立即反馈
function handleFeedback() {
router.push({
path: "/alert-handle",
2026-04-16 14:21:25 +08:00
query: { id: taskId, status: alertDetail.value.status }
2026-04-10 17:10:36 +08:00
});
}
// 显示反馈弹窗
function showFeedback(index) {
2026-04-16 14:21:25 +08:00
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;
2026-04-10 17:10:36 +08:00
}
// 关闭弹窗
function closePopup() {
showResultDialog.value = false;
}
2026-04-16 14:21:25 +08:00
// 判断按钮显示
const showCheckInBtn = computed(() => {
return allDetail.value.taskStatus == "0";
});
const showFeedbackBtns = computed(() => {
return allDetail.value.taskStatus == "1" || allDetail.value.taskStatus == "2";
});
// 页面初始化
onMounted(() => {
fetchDetail();
});
2026-04-10 17:10:36 +08:00
</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;
2026-04-16 14:21:25 +08:00
&.status-gray {
background: #f1f5f9;
color: #64748b;
border: 1px solid #e2e8f0;
}
2026-04-10 17:10:36 +08:00
&.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;
2026-04-16 14:21:25 +08:00
width: 70vw;
2026-04-10 17:10:36 +08:00
}
.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;
}
}
2026-04-16 14:21:25 +08:00
.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;
}
}
2026-04-10 17:10:36 +08:00
</style>