This commit is contained in:
给我
2026-04-16 14:21:25 +08:00
parent f8e99b7c77
commit c3b2a20ad0
14 changed files with 1026 additions and 572 deletions

58
package-lock.json generated
View File

@ -13,7 +13,9 @@
"axios": "^0.27.2", "axios": "^0.27.2",
"base-64": "^1.0.0", "base-64": "^1.0.0",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"crypto-js": "^4.2.0",
"echarts": "^5.3.3", "echarts": "^5.3.3",
"image-compressor.js": "^1.1.4",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"ol": "^6.15.1", "ol": "^6.15.1",
"vant": "^3.4.3", "vant": "^3.4.3",
@ -5012,6 +5014,12 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true "dev": true
}, },
"node_modules/blueimp-canvas-to-blob": {
"version": "3.29.0",
"resolved": "https://registry.npmmirror.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==",
"license": "MIT"
},
"node_modules/bn.js": { "node_modules/bn.js": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
@ -6370,6 +6378,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/css-color-names": { "node_modules/css-color-names": {
"version": "0.0.4", "version": "0.0.4",
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
@ -10152,6 +10166,17 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/image-compressor.js": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/image-compressor.js/-/image-compressor.js-1.1.4.tgz",
"integrity": "sha512-DF1YFSw+m6FqpXsleD4+q9eu/wFFkm8sHuYhgYy5GWFVencXeuB1/UqC12xz+dCZooPetf5LIb8JOGkgEWmlcg==",
"deprecated": "No longer maintainted, please use `comprossorjs`",
"license": "MIT",
"dependencies": {
"blueimp-canvas-to-blob": "^3.14.0",
"is-blob": "^1.0.0"
}
},
"node_modules/image-size": { "node_modules/image-size": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
@ -10496,6 +10521,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-blob": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/is-blob/-/is-blob-1.0.0.tgz",
"integrity": "sha512-QIZDHQZpRfMEZwSTD7egdNZS7H/awVW9FZ3yJv+gg1z8d8GPXEs76QWL67fZs2BoBqp2dGtamTJpEYFJHmD73g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-boolean-object": { "node_modules/is-boolean-object": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@ -26358,6 +26392,11 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true "dev": true
}, },
"blueimp-canvas-to-blob": {
"version": "3.29.0",
"resolved": "https://registry.npmmirror.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="
},
"bn.js": { "bn.js": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
@ -27455,6 +27494,11 @@
"randomfill": "^1.0.3" "randomfill": "^1.0.3"
} }
}, },
"crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"css-color-names": { "css-color-names": {
"version": "0.0.4", "version": "0.0.4",
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
@ -30390,6 +30434,15 @@
"integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==",
"dev": true "dev": true
}, },
"image-compressor.js": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/image-compressor.js/-/image-compressor.js-1.1.4.tgz",
"integrity": "sha512-DF1YFSw+m6FqpXsleD4+q9eu/wFFkm8sHuYhgYy5GWFVencXeuB1/UqC12xz+dCZooPetf5LIb8JOGkgEWmlcg==",
"requires": {
"blueimp-canvas-to-blob": "^3.14.0",
"is-blob": "^1.0.0"
}
},
"image-size": { "image-size": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
@ -30646,6 +30699,11 @@
"binary-extensions": "^2.0.0" "binary-extensions": "^2.0.0"
} }
}, },
"is-blob": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/is-blob/-/is-blob-1.0.0.tgz",
"integrity": "sha512-QIZDHQZpRfMEZwSTD7egdNZS7H/awVW9FZ3yJv+gg1z8d8GPXEs76QWL67fZs2BoBqp2dGtamTJpEYFJHmD73g=="
},
"is-boolean-object": { "is-boolean-object": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",

View File

@ -13,7 +13,9 @@
"axios": "^0.27.2", "axios": "^0.27.2",
"base-64": "^1.0.0", "base-64": "^1.0.0",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"crypto-js": "^4.2.0",
"echarts": "^5.3.3", "echarts": "^5.3.3",
"image-compressor.js": "^1.1.4",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"ol": "^6.15.1", "ol": "^6.15.1",
"vant": "^3.4.3", "vant": "^3.4.3",

76
src/api/traffic.js Normal file
View File

@ -0,0 +1,76 @@
import { service } from '../utils/request';
/**
* 获取交通预警事件列表
* @param {Object} data - 查询参数
* @param {number} data.eventCategory - 事件分类1-路面事件2-违章事件
* @param {number} data.page - 当前页号
* @param {number} data.pageSize - 每页大小
*/
export function getTrafficEventList(data) {
return service({
url: '/traffic/eventTask/page',
method: 'post',
data
});
}
/**
* 获取交通预警事件详情
* @param {string|number} taskId - 任务ID
*/
export function getTrafficEventDetail(taskId) {
return service({
url: `/traffic/eventTask/detail/${taskId}`,
method: 'get'
});
}
/**
* 定位打卡
* @param {Object} data - 打卡参数
* @param {string|number} data.taskId - 任务ID
* @param {string} data.address - 地址
* @param {string} data.clickLatLong - 经纬度
*/
export function clockIn(data) {
return service({
url: '/traffic/eventTask/click',
method: 'post',
data
});
}
/**
* 任务反馈
* @param {Object} data - 反馈参数
* @param {string|number} data.taskId - 任务ID
* @param {string} data.feedback - 反馈内容
* @param {string} data.images - 图片URL逗号拼接
* @param {string} data.video - 视频URL
* @param {string} data.filesUrl - 附件URL
* @param {string} data.singUrl - 签名URL
*/
export function feedback(data) {
return service({
url: '/traffic/eventTask/feedback',
method: 'post',
data
});
}
/**
* 文件上传(图片/视频)
* @param {File|Blob} file - 文件对象
* @returns {Promise<string>} 上传后的URL
*/
export function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
return service({
url: '/common/upload',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
});
}

View File

@ -20,7 +20,7 @@ export function login(data) {
*/ */
export function idCardlogin(data) { export function idCardlogin(data) {
return service({ return service({
url:'/mosty-base/idCardNoLogin', url:'/auth/loginByIdCardNo',
method: 'post', method: 'post',
data data
}); });
@ -52,7 +52,7 @@ export function getUserInfo(data) {
// 新增的用户信息用于民辅警一起 // 新增的用户信息用于民辅警一起
export function getUserOrFjInfo(params) { export function getUserOrFjInfo(params) {
return service({ return service({
url: `/mosty-base/sysUser/getUserOrFjInfo`, url: `/admin/users/getInfo`,
method: "get", method: "get",
params params
}); });

View File

@ -28,7 +28,7 @@
<div class="preview-label">已拍照片</div> <div class="preview-label">已拍照片</div>
<div class="photo-grid"> <div class="photo-grid">
<div v-for="(photo, index) in photos" :key="index" class="photo-item"> <div v-for="(photo, index) in photos" :key="index" class="photo-item">
<van-image :src="photo" fit="cover" class="photo-img"> <van-image :src="photo.base64" fit="cover" class="photo-img">
<template v-slot:loading> <template v-slot:loading>
<van-loading type="spinner" size="20" /> <van-loading type="spinner" size="20" />
</template> </template>
@ -68,7 +68,7 @@
<!-- 底部保存按钮 --> <!-- 底部保存按钮 -->
<div class="submit-bar"> <div class="submit-bar">
<van-button block round type="primary" class="submit-btn" @click="handleSave"> <van-button block round type="primary" class="submit-btn" :loading="isSubmitting" @click="handleSave">
保存 保存
</van-button> </van-button>
</div> </div>
@ -76,9 +76,11 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, computed, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { Toast } from "vant"; import { Toast } from "vant";
import { feedback, uploadFile } from "@/api/traffic";
import { hintToast } from "@/utils/tools";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -94,8 +96,10 @@ const pageTitle = computed(() => {
// 表单数据 // 表单数据
const feedbackText = ref(""); const feedbackText = ref("");
const photos = ref([]); // photos 存储 { base64, fileId } 对象base64 用于预览fileId 用于提交
const photos = ref(['http://220.166.58.28:172/profile/upload/2026/04/15/bb_total_active_20260415150856A001.png']);
const videos = ref([]); const videos = ref([]);
const isSubmitting = ref(false);
// 返回 // 返回
function goBack() { function goBack() {
@ -108,19 +112,17 @@ function handleTakePhoto() {
if (window.bridge && window.bridge.pZ) { if (window.bridge && window.bridge.pZ) {
window.bridge.pZ("photo"); window.bridge.pZ("photo");
} else { } else {
// H5 环境模拟 hintToast("请使用APP进行拍照");
const mockPhoto = "https://picsum.photos/400/300?random=" + Date.now();
photos.value.push(mockPhoto);
} }
} catch (err) { } catch (err) {
console.error("拍照失败:", err); console.error("拍照失败:", err);
Toast("请使用APP进行拍照"); hintToast("请使用APP进行拍照");
} }
} }
// 录视频 // 录视频
function handleTakeVideo() { function handleTakeVideo() {
Toast("视频录制功能开发中..."); hintToast("视频录制功能开发中...");
} }
// 删除照片 // 删除照片
@ -133,38 +135,81 @@ function deleteVideo(index) {
videos.value.splice(index, 1); videos.value.splice(index, 1);
} }
// 保存反馈 // 将 base64 转为 Blob
async function handleSave() { function base64ToBlob(base64) {
if (photos.value.length === 0 && videos.value.length === 0 && !feedbackText.value) { const parts = base64.split(',');
Toast("请至少上传照片或填写反馈内容"); const mime = parts[0].match(/:(.*?);/)[1];
return; const byteString = atob(parts[1]);
} const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
try { for (let i = 0; i < byteString.length; i++) {
Toast.loading({ uint8Array[i] = byteString.charCodeAt(i);
message: "提交中...",
forbidClick: true,
duration: 0,
});
// 模拟提交
await new Promise((resolve) => setTimeout(resolve, 1500));
Toast.clear();
Toast.success("保存成功!");
setTimeout(() => {
router.back();
}, 1000);
} catch (error) {
Toast.clear();
Toast.fail("保存失败,请重试");
} }
return new Blob([arrayBuffer], { type: mime });
} }
// 接收原生APP返回的图片 // 接收原生APP返回的图片
function setimagebase64(base64) { function setimagebase64(base64) {
photos.value.push(`data:image/jpeg;base64,${base64}`); const preview = `data:image/jpeg;base64,${base64}`;
const tempId = `uploading_${Date.now()}`;
photos.value.push({ id: tempId, base64: preview, fileId: "" });
const blob = base64ToBlob(base64);
const fileName = `photo_${Date.now()}.jpg`;
const file = new File([blob], fileName, { type: blob.type });
uploadFile(file)
.then((res) => {
const idx = photos.value.findIndex((p) => p.id === tempId);
if (idx !== -1) {
photos.value[idx].fileId = res;
}
})
.catch((e) => {
console.error("图片上传失败:", e);
const idx = photos.value.findIndex((p) => p.id === tempId);
if (idx !== -1) {
photos.value.splice(idx, 1);
}
});
} }
// 保存反馈
function handleSave() {
if (!feedbackText.value.trim()) {
hintToast("请输入反馈内容");
return;
}
if (isSubmitting.value) return;
isSubmitting.value = true;
Toast.loading({ message: "提交中...", forbidClick: true, duration: 0 });
const images = photos.value.map((p) => p.fileId).filter(Boolean).join(",");
feedback({
taskId: alertId,
feedback: feedbackText.value,
images,
video: "",
filesUrl: "",
singUrl: ""
})
.then(() => {
Toast.clear();
hintToast("保存成功");
setTimeout(() => {
router.back();
}, 1000);
})
.catch((e) => {
Toast.clear();
hintToast(e.message || "保存失败,请重试");
})
.finally(() => {
isSubmitting.value = false;
});
}
onMounted(() => {
window.setimagebase64 = setimagebase64;
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -48,19 +48,19 @@
<div class="tips-title">打卡说明</div> <div class="tips-title">打卡说明</div>
<ul class="tips-list"> <ul class="tips-list">
<li>请确保在指定地点范围内打卡</li> <li>请确保在指定地点范围内打卡</li>
<li>请拍摄现场实景照片作为打卡凭证</li>
<li>打卡成功后任务状态将更新为"执行中"</li> <li>打卡成功后任务状态将更新为"执行中"</li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- :disabled="!isLocationVerified || isSubmitting" -->
<!-- 底部提交按钮 --> <!-- 底部提交按钮 -->
<div class="submit-bar"> <div class="submit-bar">
<van-button <van-button
block block
round round
type="primary" type="primary"
:disabled="!photo || !isLocationVerified"
:loading="isSubmitting"
class="submit-btn" class="submit-btn"
@click="handleSubmit" @click="handleSubmit"
> >
@ -83,7 +83,8 @@
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { Toast } from "vant"; import { clockIn } from "@/api/traffic";
import { hintToast } from "@/utils/tools";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -91,8 +92,10 @@ const route = useRoute();
// 打卡数据 // 打卡数据
const photo = ref(""); const photo = ref("");
const location = ref("正在获取位置..."); const location = ref("正在获取位置...");
const locationCoords = ref("");
const isLocationVerified = ref(false); const isLocationVerified = ref(false);
const fileInputRef = ref(null); const fileInputRef = ref(null);
const isSubmitting = ref(false);
// 获取URL参数 // 获取URL参数
const taskId = route.query.id; const taskId = route.query.id;
@ -102,12 +105,18 @@ function goBack() {
router.back(); router.back();
} }
// 模拟获取位置 // 获取实际位置
function initLocation() { function initLocation() {
setTimeout(() => { try {
location.value = "中山路与发展大道交叉口"; const loc = JSON.parse(bridge.getLocation());
isLocationVerified.value = true; const addr = loc.address || loc.addr || "";
}, 1500); location.value = addr || "未知位置";
locationCoords.value = `${loc.app_x},${loc.app_y}`;
isLocationVerified.value = !!(loc.app_x && loc.app_y);
} catch (e) {
location.value = "获取位置失败";
isLocationVerified.value = false;
}
} }
// 点击拍照 // 点击拍照
@ -125,7 +134,6 @@ function handleFileChange(event) {
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
// 重置 input 以允许重复选择同一文件
event.target.value = ""; event.target.value = "";
} }
@ -136,18 +144,28 @@ function deletePhoto() {
// 提交打卡 // 提交打卡
function handleSubmit() { function handleSubmit() {
if (!photo.value) { // if (!isLocationVerified.value) {
Toast("请先拍照"); // hintToast("正在获取位置信息,请稍候");
return; // return;
} // }
if (!isLocationVerified.value) { // if (isSubmitting.value) return;
Toast("正在获取位置信息,请稍候"); // isSubmitting.value = true;
return; clockIn({
} taskId,
Toast.success("打卡成功!"); address: '测试',
setTimeout(() => { // address: location.value,
router.back(); clickLatLong: "1,1",
}, 1000); // clickLatLong: locationCoords.value,
}).then(() => {
hintToast("打卡成功");
setTimeout(() => {
router.back();
}, 1000);
}).catch((e) => {
hintToast(e.message || "打卡失败");
}).finally(() => {
isSubmitting.value = false;
});
} }
onMounted(() => { onMounted(() => {

View File

@ -61,6 +61,7 @@
import "vant/es/toast/style"; import "vant/es/toast/style";
import Base64 from "base-64"; import Base64 from "base-64";
import { login, idCardlogin, getUserOrFjInfo } from "../../api/user.js"; import { login, idCardlogin, getUserOrFjInfo } from "../../api/user.js";
import { encryptAES } from "../../utils/crypto.js";
import { computed, ref, reactive, onMounted } from "vue"; import { computed, ref, reactive, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useStore } from "vuex"; import { useStore } from "vuex";
@ -80,13 +81,17 @@ const form = reactive({
}); });
const countdown = ref(0); const countdown = ref(0);
const isLoading = ref(false); const isLoading = ref(false);
const idCardForm = reactive({
idCard: "510922199202281888"
});
onMounted(() => { onMounted(() => {
// _idCardlogin(); // 已屏蔽自动登录 _idCardlogin(); // 已屏蔽自动登录
}); });
//身份证登录 //身份证登录
function _idCardlogin() { function _idCardlogin() {
try { try {
let userinfo = JSON.parse(bridge.getLocation()); // let userinfo = JSON.parse(bridge.getLocation());
let sfzh='510922199202281888'
Toast.loading({ Toast.loading({
message: "登录中...", message: "登录中...",
forbidClick: true, forbidClick: true,
@ -95,11 +100,12 @@ function _idCardlogin() {
duration: 0, duration: 0,
}); });
isLoading.value = true; isLoading.value = true;
form.userName = userinfo.app_sfzh; // form.userName = userinfo.app_sfzh;
idCardlogin({ idCardNo: userinfo.app_sfzh }) const encryptedIdCard = encryptAES(sfzh);
idCardlogin({ username: encryptedIdCard })
.then((res) => { .then((res) => {
const token = res.jwtToken; const token = res.accessToken;
_getUserInfo(res.userId, res.idEntityCard, res.deptList[0].deptName, res.deptList); _getUserInfo();
store.commit("setToken", token); store.commit("setToken", token);
}) })
.catch((err) => { .catch((err) => {
@ -118,6 +124,34 @@ const sendCode = async () => {
} }
}, 1000); }, 1000);
}; };
// 身份证号格式校验
function validateIdCard(val) {
return /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(val);
}
// 身份证登录提交
function onIdCardSubmit() {
Toast.loading({
message: "登录中...",
forbidClick: true,
loadingType: "spinner",
overlay: true,
duration: 0,
});
isLoading.value = true;
const encryptedIdCard = encryptAES(idCardForm.idCard);
idCardlogin({ username: encryptedIdCard })
.then((res) => {
const token = res.jwtToken;
_getUserInfo(res.userId, idCardForm.idCard, res.deptList[0].deptName, res.deptList);
store.commit("setToken", token);
})
.catch((err) => {
Toast.clear();
isLoading.value = false;
});
}
//手动登录 //手动登录
const onSubmit = (e) => { const onSubmit = (e) => {
if (TabActiveName.value === 'a') { if (TabActiveName.value === 'a') {
@ -145,21 +179,10 @@ const onSubmit = (e) => {
} }
}; };
//获取用户信息 //获取用户信息
function _getUserInfo(id, sfzh, deptName, deptList) { function _getUserInfo() {
getUserOrFjInfo({ id, sfzh }).then((res) => { getUserOrFjInfo({}).then((res) => {
let { userName, id, idEntityCard, mobile, inDustRialId, sex } = res; console.log(res,'+++++++');
let userinfo = { store.commit("userStatus", res);
deptName,
userName,
deptList,
id,
idEntityCard,
mobile,
inDustRialId,
sex,
};
store.commit("userStatus", userinfo);
let path = getStorage("homeUrl");
setTimeout(() => { setTimeout(() => {
isLoading.value = false; isLoading.value = false;
router.replace("/Home"); router.replace("/Home");

View File

@ -4,18 +4,18 @@
<div class="header-bg"> <div class="header-bg">
<div class="user-section"> <div class="user-section">
<div class="avatar"> <div class="avatar">
{{ userNameChar }} {{ userInfo.nickName?userInfo.nickName?.charAt(0):"" }}
</div> </div>
<div class="user-info"> <div class="user-info">
<div class="user-name">{{ userInfo.userName || '张警官' }}</div> <div class="user-name">{{ userInfo.nickName }}</div>
<div class="dept-name">{{ userInfo.deptName || '交警支队一大队' }}</div> <div class="dept-name">{{ userInfo.departName }}</div>
</div> </div>
</div> </div>
<!-- 当前位置 --> <!-- 当前位置 -->
<div class="location"> <div class="location" v-if="userInfo.workAddress">
<van-icon name="location" class="location-icon" /> <van-icon name="location" class="location-icon" />
<span>{{ currentLocation || '武汉市江汉区中山大道与建设大道交叉口' }}</span> <span>{{ userInfo.workAddress }}</span>
</div> </div>
</div> </div>
@ -113,7 +113,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { List } from "vant"; import { List } from "vant";
import BottomTabs from "@/components/bottomTabs.vue"; import BottomTabs from "@/components/bottomTabs.vue";
@ -308,7 +308,9 @@ const currentPage = ref(0);
const filteredAlerts = computed(() => { const filteredAlerts = computed(() => {
return allAlerts.value.filter(alert => alert.status === "未执行"); return allAlerts.value.filter(alert => alert.status === "未执行");
}); });
onMounted(()=>{
userInfo.value=JSON.parse(localStorage.getItem('userInfo'))
})
// 加载更多数据 // 加载更多数据
function onLoad() { function onLoad() {
// 模拟异步加载 // 模拟异步加载

View File

@ -7,170 +7,173 @@
<div class="nav-placeholder"></div> <div class="nav-placeholder"></div>
</div> </div>
<!-- 预警信息卡片 --> <!-- 加载状态 -->
<div class="detail-card"> <van-loading v-if="loading" class="loading-state" color="#2563eb">加载中...</van-loading>
<!-- 头部信息 -->
<div class="card-header"> <template v-else>
<div class="header-left"> <!-- 预警信息卡片 -->
<span class="level-tag" :class="alertDetail.levelClass"> <div class="detail-card">
{{ alertDetail.level }} <!-- 头部信息 -->
<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> </span>
<span class="alert-title">{{ alertDetail.title }}</span>
</div> </div>
<span class="status-tag" :class="alertDetail.statusClass">
{{ alertDetail.status }} <!-- 图片 -->
</span> <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>
<!-- --> <!-- 执行记录卡 -->
<div class="card-image" v-if="alertDetail.image"> <div class="record-card">
<van-image <div class="card-title">执行记录</div>
:src="alertDetail.image"
fit="cover"
class="alert-img"
/>
</div>
<!-- 详细信息 --> <!-- 蓝色边框卡片 -->
<div class="card-details"> <div class="execution-box">
<div class="detail-item"> <!-- 绿色边框内层卡片 -->
<van-icon name="clock" class="detail-icon" /> <div class="info-box" v-if="taskDetail">
<span class="label">检测时间</span> <div class="info-row">
<span class="value">{{ alertDetail.time }}</span> <span class="info-label">任务地点</span>
</div> <span class="info-value">{{ taskDetail.clickAddress }}</span>
<div class="detail-item"> </div>
<van-icon name="location" class="detail-icon" /> <div class="info-row" v-if="taskDetail.deptName">
<span class="label">检测地点</span> <span class="info-text">{{ taskDetail.deptName }}</span>
<span class="value">{{ alertDetail.location }}</span> <span class="info-divider" v-if="taskDetail.phoneNumber">|</span>
</div> <span class="info-text" v-if="taskDetail.phoneNumber">联系电话{{ taskDetail.phoneNumber }}</span>
</div>
</div>
<!-- 视频展示按钮 --> <!-- 任务描述 -->
<div class="video-btn-wrapper"> <div class="task-desc" v-if="taskDetail">
<van-button block round class="video-btn"> {{ taskDetail.taskContent }}
<van-icon name="video-o" class="video-icon" /> </div>
视频展示
</van-button> <!-- 打卡情况 -->
<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>
</div> </div>
</div> </template>
<!-- 执行记录卡片 -->
<div class="record-card">
<div class="card-title">执行记录</div>
<!-- 蓝色边框卡片 -->
<div class="execution-box">
<!-- 绿色边框内层卡片 -->
<div class="info-box">
<div class="info-row">
<span class="info-label">执行时间</span>
<span class="info-value">03/29 09:30</span>
</div>
<div class="info-row">
<span class="info-text">交通指挥中心</span>
<span class="info-divider">|</span>
<span class="info-text">联系电话027-8888-8888</span>
</div>
<div class="info-row">
<span class="info-label">任务地点</span>
<span class="info-value">{{ alertDetail.location }}</span>
</div>
</div>
<!-- 任务描述 -->
<div class="task-desc">
协助疏导交通确保道路畅通维持现场秩序
</div>
<!-- 打卡情况 - 执行中和已完成状态显示 -->
<div v-if="status === '执行中' || status === '已完成'" class="checkin-box">
<div class="checkin-row">
<span class="checkin-date">11-16</span>
<span class="checkin-time">16:20:15</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>四川省德阳市旌阳区嘉明街道光兴街2号</span>
</div>
</div>
<!-- 按钮区域 -->
<!-- 已完成状态 - 显示三次反馈按钮 -->
<div v-if="status === '已完成'" class="feedback-buttons">
<van-button block round class="feedback-btn" @click="showFeedback(1)">
第一次反馈结果
</van-button>
<van-button block round class="feedback-btn" @click="showFeedback(2)">
第二次反馈结果
</van-button>
<van-button block round class="feedback-btn" @click="showFeedback(3)">
第三次反馈结果
</van-button>
</div>
<!-- 未执行状态 - 显示定位打卡按钮 -->
<div v-else-if="status === '未执行'" class="action-buttons">
<van-button block round type="primary" class="action-btn" @click="handleCheckIn">
定位打卡
</van-button>
</div>
<!-- 执行中状态 - 显示反馈按钮 -->
<div v-else-if="status === '执行中'" class="feedback-buttons">
<van-button block round class="feedback-btn success" @click="showFeedback(1)">
第一次反馈结果
</van-button>
<van-button block round class="feedback-btn success" @click="showFeedback(2)">
第二次反馈结果
</van-button>
<van-button block round type="primary" class="action-btn" @click="handleFeedback">
立即反馈
</van-button>
</div>
</div>
</div>
<!-- 反馈结果弹窗 --> <!-- 反馈结果弹窗 -->
<van-popup v-model:show="showResultDialog" round position="center" class="feedback-popup"> <van-popup v-model:show="showResultDialog" round position="center" class="feedback-popup">
<div class="popup-content"> <div class="popup-content" v-if="currentFeedback.title">
<h2 class="popup-title">{{ currentFeedback.title }}</h2> <h2 class="popup-title">{{ currentFeedback.title }}</h2>
<div class="popup-info"> <div class="popup-info">
<p>反馈时间{{ currentFeedback.time }}</p> <p>反馈时间{{ currentFeedback.feedbackTime }}</p>
</div> </div>
<!-- 现场照片 --> <!-- 现场照片 -->
<div class="popup-section"> <div class="popup-section" v-if="currentFeedback.images && currentFeedback.images.length > 0">
<p class="section-label">现场照片</p> <p class="section-label">现场照片</p>
<div class="image-grid"> <div class="image-grid">
<van-image <van-image
v-for="(img, idx) in currentFeedback.images" v-for="(img, idx) in currentFeedback.images"
:key="idx" :key="idx"
@click.stop="ImagePreview([img])"
:src="img" :src="img"
fit="cover" fit="cover"
class="feedback-img" class="feedback-img"
/> />
</div> </div>
</div> </div>
<!-- 现场照片 -->
<!-- 场地视频 --> <div class="popup-section" v-if="currentFeedback.video && currentFeedback.video.length > 0">
<div class="popup-section">
<p class="section-label">场地视频</p> <p class="section-label">场地视频</p>
<div class="video-item"> <div class="image-grid">
<van-icon name="video-o" class="video-icon" /> <span v-for="(img, idx) in currentFeedback.video"
<span>视频 1</span> :key="idx">视频{{ idx+1 }}</span>
</div> </div>
</div> </div>
<!-- 反馈内容 --> <!-- 反馈内容 -->
<div class="popup-section"> <div class="popup-section" v-if="currentFeedback.feedback">
<h3 class="content-title">反馈内容</h3> <h3 class="content-title">反馈内容</h3>
<p class="content-text">{{ currentFeedback.content }}</p> <p class="content-text">{{ currentFeedback.feedback }}</p>
</div> </div>
<!-- 关闭按钮 --> <!-- 关闭按钮 -->
@ -179,75 +182,141 @@
</van-button> </van-button>
</div> </div>
</van-popup> </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> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, computed, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { getTrafficEventDetail } from "@/api/traffic";
import { ImagePreview } from 'vant';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
// 获取URL参数 // 获取URL参数
const alertId = route.query.id || "1"; const taskId = route.query.id || "";
const status = route.query.status || "未执行"; 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 showResultDialog = ref(false);
const selectedFeedbackIndex = ref(1); const currentFeedback = ref({});
// 预警详情数据 // 视频弹窗状态
const alertDetail = ref({ const showVideoDialog = ref(false);
id: alertId, const currentVideoUrl = ref("");
level: "一级",
levelClass: "level-red",
title: "交通事故",
status: status,
statusClass: computed(() => {
if (status === "执行中") return "status-blue";
if (status === "已完成") return "status-green";
return "status-orange";
}).value,
image: "https://images.unsplash.com/photo-1772962622823-77778ff7b192?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmFmZmljJTIwYWNjaWRlbnQlMjBlbWVyZ2VuY3l8ZW58MXx8fHwxNzc0NjA1NzE3fDA&ixlib=rb-4.1.0&q=80&w=1080",
location: "解放大道光谷广场",
time: "03/29 08:00",
issuer: "110指挥中心",
phone: "027-110"
});
// 反馈数据 // 状态文本映射
const feedbackData = { const statusMap = {
1: { 0: "待执行",
title: "第一次反馈结果", 1: "执行中",
time: "03/29 10:15", 2: "已完成",
content: "已到达现场,正在疏导交通。现场车流量较大,已设置临时路障引导车辆分流。",
images: [
"https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=800&q=80",
"https://images.unsplash.com/photo-1486006920555-c77dcf18193c?w=800&q=80"
]
},
2: {
title: "第二次反馈结果",
time: "03/29 10:45",
content: "交通疏导进行中拥堵情况有所缓解。已联系拖车处理事故车辆预计15分钟后完全恢复通行。",
images: [
"https://images.unsplash.com/photo-1485833077593-4278bba3f11f?w=800&q=80",
"https://images.unsplash.com/photo-1502444330042-d1a1ddf9bb5b?w=800&q=80"
]
},
3: {
title: "第三次反馈结果",
time: "03/29 11:10",
content: "任务已完成,交通已恢复正常。事故车辆已清离现场,临时路障已撤除,道路完全畅通。",
images: [
"https://images.unsplash.com/photo-1464037866556-6812c9d1c72e?w=800&q=80",
"https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800&q=80"
]
}
}; };
const currentFeedback = computed(() => feedbackData[selectedFeedbackIndex.value] || feedbackData[1]); // 等级映射
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() { function goBack() {
@ -258,7 +327,7 @@ function goBack() {
function handleCheckIn() { function handleCheckIn() {
router.push({ router.push({
path: "/checkInPage", path: "/checkInPage",
query: { id: alertId } query: { id: taskId }
}); });
} }
@ -266,20 +335,51 @@ function handleCheckIn() {
function handleFeedback() { function handleFeedback() {
router.push({ router.push({
path: "/alert-handle", path: "/alert-handle",
query: { id: alertId, status: status } query: { id: taskId, status: alertDetail.value.status }
}); });
} }
// 显示反馈弹窗 // 显示反馈弹窗
function showFeedback(index) { function showFeedback(index) {
selectedFeedbackIndex.value = index; if (allDetail.value.feebacks[index - 1]) {
showResultDialog.value = true; 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() { function closePopup() {
showResultDialog.value = false; 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -372,6 +472,11 @@ function closePopup() {
padding: 4px 12px; padding: 4px 12px;
border-radius: 20px; border-radius: 20px;
font-size: 12px; font-size: 12px;
&.status-gray {
background: #f1f5f9;
color: #64748b;
border: 1px solid #e2e8f0;
}
&.status-orange { &.status-orange {
background: #fff7ed; background: #fff7ed;
@ -597,6 +702,7 @@ function closePopup() {
.popup-content { .popup-content {
padding: 24px; padding: 24px;
width: 70vw;
} }
.popup-title { .popup-title {
@ -680,4 +786,35 @@ function closePopup() {
background: #d1d5db; 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> </style>

View File

@ -10,7 +10,7 @@
<!-- 标签栏 --> <!-- 标签栏 -->
<div class="tabs-bar"> <div class="tabs-bar">
<button <button
v-for="tab in displayTabs" v-for="tab in tabs"
:key="tab.id" :key="tab.id"
class="tab-item" class="tab-item"
:class="{ active: activeTab === tab.id }" :class="{ active: activeTab === tab.id }"
@ -21,97 +21,95 @@
</div> </div>
<!-- 预警列表 --> <!-- 预警列表 -->
<div class="alerts-list"> <div class="alerts-list" ref="listRef">
<div <!-- 加载状态 -->
v-for="alert in filteredAlerts" <van-loading v-if="initLoading" class="loading-state" color="#2563eb">加载中...</van-loading>
:key="alert.id"
class="alert-card" <!-- 列表内容 -->
<van-list
v-else
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoadMore"
> >
<!-- 等级标题和状态标签 --> <div
<div class="card-header"> v-for="alert in filteredAlerts"
<div class="card-title-row"> :key="alert.id"
class="alert-card"
>
<!-- 等级标题和状态标签 -->
<div class="card-header">
<div class="card-title-row">
<span
class="level-tag"
:class="getLevelClass(alert.eventLevel)"
>
{{ levelMap[alert.eventLevel] }}
</span>
<span class="alert-title">{{ alert.eventType }}</span>
</div>
<span <span
class="level-tag" class="status-tag"
:class="alert.levelClass" :class="getStatusClass(alert.taskStatus)"
> >
{{ alert.level }} {{ statusMap[alert.taskStatus] }}
</span> </span>
<span class="alert-title">{{ alert.title }}</span>
</div> </div>
<span
class="status-tag"
:class="alert.statusTagClass"
>
{{ alert.status }}
</span>
</div>
<!-- 图片 --> <!-- 图片 -->
<div class="card-image" v-if="alert.image"> <div class="card-image" v-if="alert.image">
<van-image <van-image
:src="alert.image" :src="alert.imgUrl"
fit="cover" fit="cover"
class="alert-img" 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">{{ alert.location }}</span>
</div> </div>
<div class="detail-item">
<van-icon name="clock" class="detail-icon" /> <!-- 详细信息 -->
<span class="label">检测时间</span> <div class="card-details">
<span class="value">{{ alert.time }}</span> <div class="detail-item">
<van-icon name="location" class="detail-icon" />
<span class="label">检测点位</span>
<span class="value">{{ alert.siteName }}</span>
</div>
<div class="detail-item">
<van-icon name="clock" class="detail-icon" />
<span class="label">检测时间</span>
<span class="value">{{ alert.eventTime }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="card-actions">
<!-- 待执行/执行中/已完成 - 显示查看详情 -->
<van-button
block
round
type="primary"
class="action-btn"
@click="handleDetail(alert)"
>
查看详情
</van-button>
<!-- 待派发 - 显示定位打卡 -->
<!-- <van-button
block
round
type="primary"
class="action-btn"
@click="handleCheckIn(alert)"
>
定位打卡
</van-button> -->
</div> </div>
</div> </div>
</van-list>
<!-- 操作按钮 -->
<div class="card-actions">
<!-- 未执行/已完成 - 显示查看详情 -->
<van-button
v-if="alert.status === '未执行' || alert.status === '已完成'"
block
round
type="primary"
class="action-btn"
@click="handleDetail(alert)"
>
查看详情
</van-button>
<!-- 待处理 - 显示定位打卡 -->
<van-button
v-else-if="alert.status === '待处理'"
block
round
type="primary"
class="action-btn"
@click="handleCheckIn(alert)"
>
定位打卡
</van-button>
<!-- 执行中 - 显示查看详情 -->
<van-button
v-else-if="alert.status === '执行中'"
block
round
type="primary"
class="action-btn"
@click="handleDetail(alert)"
>
查看详情
</van-button>
</div>
</div>
<!-- 空状态 --> <!-- 空状态 -->
<van-empty <van-empty
v-if="filteredAlerts.length === 0" v-if="filteredAlerts.length === 0 && !initLoading"
description="暂无数据" description="暂无数据"
class="empty-state" class="empty-state"
/> />
@ -120,97 +118,142 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, computed, onMounted, watch } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getTrafficEventList } from "@/api/traffic";
const router = useRouter(); const router = useRouter();
const filteredAlerts = ref([]);
const userInfo=ref(JSON.parse(localStorage.getItem('userInfo')))
// 状态 // 状态
const activeTab = ref("all"); const activeTab = ref("all");
const loading = ref(false);
const initLoading = ref(false);
const finished = ref(false);
// 标签栏 // 标签栏
const tabs = [ const tabs = ref([
{ id: "all", label: "全部" }, { id: "all", label: "全部" },
{ id: "not-executed", label: "未执行" }, { id: "not-executed", label: "未执行" },
{ id: "pending", label: "待处理" },
{ id: "processing", label: "执行中" }, { id: "processing", label: "执行中" },
{ id: "completed", label: "已完成" }, { id: "completed", label: "已完成" },
]; ]);
// 显示的标签(过滤掉"待处理" const listQuery = ref({
const displayTabs = computed(() => { eventCategory: '1',
return tabs.filter(tab => tab.id !== "pending"); page: 1,
pageSize: 10,
taskStatus:"",
userId:"4"
// userId:userInfo.value.id
}); });
// 预警数据(与 Greeting message 一致) // 分类映射tabId -> status
const alerts = [ const categoryMap = {
{ "all": null,
id: 1, "not-executed": 0, // 待派发
level: "一级", "processing": 1, // 执行中
levelClass: "level-red", "completed": 2 // 已完成
title: "道路拥堵", };
status: "未执行",
statusTagClass: "status-gray", // 状态文本映射
image: "https://images.unsplash.com/photo-1771433053081-458d46481c09?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmFmZmljJTIwY29uZ2VzdGlvbiUyMHJvYWR8ZW58MXx8fHwxNzc0NjA1NTc2fDA&ixlib=rb-4.1.0&q=80&w=1080", const statusMap = {
location: "中山路与发展大道交叉口", 0: "待执行",
time: "03/29 08:30", 1: "执行中",
deadline: "03/29 12:00", 2: "已完成",
issuer: "交通指挥中心", };
category: "all"
}, // 等级映射
{ const levelMap = {
id: 2, 0: "一级",
level: "级", 1: "级",
levelClass: "level-blue", 2: "三级",
title: "道路施工", 3: "四级"
status: "未执行", };
statusTagClass: "status-gray",
image: "https://images.unsplash.com/photo-1460533078824-f51edbff2726?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyb2FkJTIwY29uc3RydWN0aW9uJTIwd29ya3xlbnwxfHx8fDE3NzQ2MDI2MTJ8MA&ixlib=rb-4.1.0&q=80&w=1080", // 重置列表
location: "上新河桥", function resetList() {
time: "03/29 10:15", filteredAlerts.value = [];
deadline: "03/29 15:00", listQuery.value.page = 1;
issuer: "二大队李队长", finished.value = false;
category: "all" }
},
{ // 获取列表数据
id: 3, const fetchAlerts = async (isLoadMore = false) => {
level: "二级", if (isLoadMore) {
levelClass: "level-orange", loading.value = true;
title: "交通事故", } else {
status: "执行中", initLoading.value = true;
statusTagClass: "status-blue",
image: "https://images.unsplash.com/photo-1772962622823-77778ff7b192?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmFmZmljJTIwYWNjaWRlbnQlMjBlbWVyZ2VuY3l8ZW58MXx8fHwxNzc0NjA1NzE3fDA&ixlib=rb-4.1.0&q=80&w=1080",
location: "解放大道光谷广场",
time: "03/29 07:20",
deadline: "03/29 10:30",
issuer: "110指挥中心",
category: "processing"
},
{
id: 4,
level: "三级",
levelClass: "level-blue",
title: "违章停车",
status: "已完成",
statusTagClass: "status-green",
image: "https://images.unsplash.com/photo-1685582664602-24ea8d0a4ca7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmFmZmljJTIwcG9saWNlJTIwcGF0cm9sfGVufDF8fHx8MTc3NDYwNTgzNHww&ixlib=rb-4.1.0&q=80&w=1080",
location: "武汉大道CBD商业区",
time: "03/28 15:50",
deadline: "03/28 17:00",
issuer: "三大队王队长",
category: "completed"
} }
];
// 过滤后的预警列表 try {
const filteredAlerts = computed(() => { // 根据标签添加状态筛选
if (activeTab.value === "all") return alerts; listQuery.value.taskStatus = categoryMap[activeTab.value];
if (activeTab.value === "not-executed") return alerts.filter(a => a.status === "未执行"); const res = await getTrafficEventList(listQuery.value);
if (activeTab.value === "pending") return alerts.filter(a => a.status === "待处理"); console.log(res, 'res');
if (activeTab.value === "processing") return alerts.filter(a => a.status === "执行中"); if (res && res.content) {
if (activeTab.value === "completed") return alerts.filter(a => a.status === "已完成"); const records = res.content || [];
return alerts; if (isLoadMore) {
}); filteredAlerts.value.push(...records);
} else {
filteredAlerts.value = records;
}
// 判断是否加载完成
const totalPages = res.content.totalPages || res.content.pages || 1;
const currentPage = res.content.pageNum || res.content.current || listQuery.value.page;
if (currentPage >= totalPages) {
finished.value = true;
}
} else if (res && Array.isArray(res)) {
if (isLoadMore) {
filteredAlerts.value.push(...res);
} else {
filteredAlerts.value = res;
}
finished.value = true;
} else {
filteredAlerts.value = [];
finished.value = true;
}
} catch (error) {
console.error("获取交通预警列表失败:", error);
if (!isLoadMore) {
filteredAlerts.value = [];
}
finished.value = true;
} finally {
loading.value = false;
initLoading.value = false;
}
};
// 加载更多
function onLoadMore() {
listQuery.value.page++;
fetchAlerts(true);
}
// 等级样式
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 goBack() { function goBack() {
@ -232,6 +275,17 @@ function handleCheckIn(alert) {
query: { id: alert.id } query: { id: alert.id }
}); });
} }
// 切换标签时重新加载
watch(activeTab, () => {
resetList();
fetchAlerts();
});
// 页面初始化
onMounted(() => {
fetchAlerts();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -286,6 +340,7 @@ function handleCheckIn(alert) {
border-radius: 20px; border-radius: 20px;
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
border: none;
&.active { &.active {
background: #2563eb; background: #2563eb;

View File

@ -10,11 +10,11 @@
<!-- 标签栏 --> <!-- 标签栏 -->
<div class="tabs-bar"> <div class="tabs-bar">
<button <button
v-for="tab in displayTabs" v-for="tab in tabs"
:key="tab.id" :key="tab.id"
class="tab-item" class="tab-item"
:class="{ active: activeTab === tab.id }" :class="{ active: activeTab === tab.id }"
@click="handleTabChange(tab.id)" @click="activeTab = tab.id"
> >
{{ tab.label }} {{ tab.label }}
</button> </button>
@ -31,20 +31,20 @@
<div class="card-header"> <div class="card-header">
<div class="vehicle-info"> <div class="vehicle-info">
<van-icon name="cart" class="vehicle-icon" /> <van-icon name="cart" class="vehicle-icon" />
<span class="plate-number">{{ alert.plateNumber }}</span> <span class="plate-number">{{ alert.plateNo }}</span>
<span class="plate-color">{{ alert.plateColor }}</span> <span class="plate-color">{{ alert.plateColor }}</span>
<span class="separator">|</span> <span class="separator">|</span>
<span class="vehicle-type">{{ alert.vehicleType }}</span> <span class="vehicle-type">{{ alert.vehicleType }}</span>
</div> </div>
<span class="status-tag" :class="alert.statusClass"> <span class="status-tag" :class="getStatusClass(alert.taskStatus)">
{{ alert.status }} {{ statusMap[alert.taskStatus] }}
</span> </span>
</div> </div>
<!-- 图片 --> <!-- 图片 -->
<div class="card-image" v-if="alert.image"> <div class="card-image" v-if="alert.image">
<van-image <van-image
:src="alert.image" :src="alert.imgUrl"
fit="cover" fit="cover"
class="alert-img" class="alert-img"
/> />
@ -52,10 +52,10 @@
<!-- 等级和标题 --> <!-- 等级和标题 -->
<div class="card-title-row"> <div class="card-title-row">
<span class="level-tag" :class="alert.levelClass"> <span class="level-tag" :class="getLevelClass(alert.eventLevel)">
{{ alert.level }} {{ levelMap[alert.eventLevel] }}
</span> </span>
<span class="alert-title">{{ alert.title }}</span> <span class="alert-title">{{ alert.eventContent }}</span>
</div> </div>
<!-- 详细信息 --> <!-- 详细信息 -->
@ -63,12 +63,12 @@
<div class="detail-item"> <div class="detail-item">
<van-icon name="location" class="detail-icon" /> <van-icon name="location" class="detail-icon" />
<span class="label">检测点位</span> <span class="label">检测点位</span>
<span class="value">{{ alert.location }}</span> <span class="value">{{ alert.siteName }}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<van-icon name="clock" class="detail-icon" /> <van-icon name="clock" class="detail-icon" />
<span class="label">检测时间</span> <span class="label">检测时间</span>
<span class="value">{{ alert.time }}</span> <span class="value">{{ alert.eventTime }}</span>
</div> </div>
</div> </div>
@ -115,149 +115,148 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, onMounted, watch } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter } from "vue-router";
import { getTrafficEventList } from "@/api/traffic";
const router = useRouter(); const router = useRouter();
const route = useRoute();
// 状态 // 状态
const activeTab = ref("all"); const activeTab = ref("all");
const loading = ref(false);
const initLoading = ref(false);
const finished = ref(false);
const showMatchDialog = ref(false); const showMatchDialog = ref(false);
const selectedViolationId = ref(null); const selectedViolationId = ref(null);
// 标签栏 // 标签栏
const tabs = [ const tabs = [
{ id: "all", label: "全部" }, { id: "all", label: "全部" },
{ id: "unexecuted", label: "未执行" }, { id: "not-executed", label: "未执行" },
{ id: "processing", label: "执行中" }, { id: "processing", label: "执行中" },
{ id: "completed", label: "已完成" }, { id: "completed", label: "已完成" },
]; ];
// 显示的标签(违章预警不显示待处理) // 查询参数
const displayTabs = computed(() => { const listQuery = ref({
return tabs; eventCategory: 2,
page: 1,
pageSize: 10,
taskStatus: "",
userId: "4"
}); });
// 违章数据(与 Greeting message 一致) // 分类映射
const violations = [ const categoryMap = {
{ "all": null,
id: 1, "not-executed": 0,
plateNumber: "鄂A12345", "processing": 1,
plateColor: "蓝色", "completed": 2
vehicleType: "小型汽车", };
level: "四级",
levelClass: "level-blue", // 列表数据
title: "违规停车", const filteredAlerts = ref([]);
status: "执行中",
statusClass: "status-blue", // 状态映射
image: "https://images.unsplash.com/photo-1710939968666-a7447d3c584e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjYXIlMjBzaWxob3VldHRlJTIwc3Vuc2V0fGVufDF8fHx8MTc3NDYwODA4Mnww&ixlib=rb-4.1.0&q=80&w=1080", const statusMap = {
location: "江汉路步行街入口", 0: "待执行",
time: "03/27 10:00", 1: "执行中",
warning: "来车预警", 2: "已完成",
category: "all" };
},
{ // 等级映射
id: 3, const levelMap = {
plateNumber: "鄂A99999", 0: "一级",
plateColor: "蓝色", 1: "二级",
vehicleType: "小型汽车", 2: "三级",
level: "级", 3: "级"
levelClass: "level-yellow", };
title: "闯红灯",
status: "执行中", // 重置列表
statusClass: "status-blue", function resetList() {
image: "https://images.unsplash.com/photo-1449965408869-eaa3f722e40d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmFmZmljJTIwbGlnaHQlMjByZWR8ZW58MXx8fHwxNzc0NjA4MDg0fDA&ixlib=rb-4.1.0&q=80&w=1080", filteredAlerts.value = [];
location: "解放大道循礼门路口", listQuery.value.page = 1;
time: "03/27 09:15", finished.value = false;
warning: "来车预警", }
category: "all"
}, // 获取列表数据
{ const fetchAlerts = async (isLoadMore = false) => {
id: 4, if (isLoadMore) {
plateNumber: "鄂A88888", loading.value = true;
plateColor: "蓝色", } else {
vehicleType: "小型汽车", initLoading.value = true;
level: "二级",
levelClass: "level-orange",
title: "违规变道",
status: "已完成",
statusClass: "status-green",
image: "https://images.unsplash.com/photo-1756467988694-9953cd7ade0a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmFmZmljJTIwcG9saWNlJTIwZW5mb3JjZW1lbnR8ZW58MXx8fHwxNzc0NjA4MDgyfDA&ixlib=rb-4.1.0&q=80&w=1080",
location: "中山大道民生路口",
time: "03/26 15:20",
warning: "来车预警",
category: "completed"
},
{
id: 6,
plateNumber: "鄂B33333",
plateColor: "蓝色",
vehicleType: "小型汽车",
level: "一级",
levelClass: "level-red",
title: "违规掉头",
status: "已完成",
statusClass: "status-green",
image: "https://images.unsplash.com/photo-1533473359331-0135ef1b58bf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjYXIlMjB0dXJuaW5nJTIwc3RyZWV0fGVufDF8fHx8MTc3NDYwODA4NXww&ixlib=rb-4.1.0&q=80&w=1080",
location: "建设大道花桥街口",
time: "03/26 11:50",
warning: "来车预警",
category: "completed"
},
{
id: 7,
plateNumber: "鄂A77777",
plateColor: "蓝色",
vehicleType: "小型汽车",
level: "二级",
levelClass: "level-orange",
title: "违规停车",
status: "未执行",
statusClass: "status-gray",
image: "https://images.unsplash.com/photo-1710939968666-a7447d3c584e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjYXIlMjBzaWxob3VldHRlJTIwc3Vuc2V0fGVufDF8fHx8MTc3NDYwODA4Mnww&ixlib=rb-4.1.0&q=80&w=1080",
location: "武昌区中南路",
time: "04/09 08:30",
warning: "来车预警",
category: "all"
},
{
id: 8,
plateNumber: "鄂A66666",
plateColor: "蓝色",
vehicleType: "小型汽车",
level: "三级",
levelClass: "level-yellow",
title: "违规掉头",
status: "未执行",
statusClass: "status-gray",
image: "https://images.unsplash.com/photo-1533473359331-0135ef1b58bf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjYXIlMjB0dXJuaW5nJTIwc3RyZWV0fGVufDF8fHx8MTc3NDYwODA4NXww&ixlib=rb-4.1.0&q=80&w=1080",
location: "江汉区解放大道",
time: "04/09 09:15",
warning: "来车预警",
category: "all"
} }
];
// 过滤后的列表 try {
const filteredAlerts = computed(() => { listQuery.value.taskStatus = categoryMap[activeTab.value];
if (activeTab.value === "all") return violations; const res = await getTrafficEventList(listQuery.value);
if (activeTab.value === "unexecuted") return violations.filter(v => v.status === "未执行"); if (res && res.content) {
if (activeTab.value === "processing") return violations.filter(v => v.status === "执行中"); const records = res.content.records || res.content.list || res.content;
if (activeTab.value === "completed") return violations.filter(v => v.status === "已完成"); if (isLoadMore) {
return violations; filteredAlerts.value.push(...records);
}); } else {
filteredAlerts.value = records;
}
const totalPages = res.content.totalPages || res.content.pages || 1;
const currentPage = res.content.pageNum || res.content.current || listQuery.value.page;
if (currentPage >= totalPages) {
finished.value = true;
}
} else if (res && Array.isArray(res)) {
if (isLoadMore) {
filteredAlerts.value.push(...res);
} else {
filteredAlerts.value = res;
}
finished.value = true;
} else {
filteredAlerts.value = [];
finished.value = true;
}
} catch (error) {
console.error("获取违章预警列表失败:", error);
if (!isLoadMore) {
filteredAlerts.value = [];
}
finished.value = true;
} finally {
loading.value = false;
initLoading.value = false;
}
};
// 加载更多
function onLoadMore() {
listQuery.value.page++;
fetchAlerts(true);
}
// 等级样式
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 goBack() { function goBack() {
router.back(); router.back();
} }
// 切换标签
function handleTabChange(tabId) {
activeTab.value = tabId;
}
// 查看详情 // 查看详情
function handleDetail(alert) { function handleDetail(alert) {
router.push({ router.push({
@ -282,6 +281,17 @@ function handleMatch(matched) {
}); });
} }
} }
// 切换标签时重新加载
watch(activeTab, () => {
resetList();
fetchAlerts();
});
// 页面初始化
onMounted(() => {
fetchAlerts();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

32
src/utils/crypto.js Normal file
View File

@ -0,0 +1,32 @@
import CryptoJS from 'crypto-js';
const KEY = 'GZFkEC$u*9xWdQsy';
const IV = 'UKUEWnKv4l2KkZCX';
/**
* AES CBC模式 PKCS5Padding加密
*/
export function encryptAES(plaintext) {
const key = CryptoJS.enc.Utf8.parse(KEY);
const iv = CryptoJS.enc.Utf8.parse(IV);
const encrypted = CryptoJS.AES.encrypt(plaintext, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
/**
* AES CBC模式 PKCS5Padding解密
*/
export function decryptAES(ciphertext) {
const key = CryptoJS.enc.Utf8.parse(KEY);
const iv = CryptoJS.enc.Utf8.parse(IV);
const decrypted = CryptoJS.AES.decrypt(ciphertext, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}

View File

@ -3,21 +3,18 @@ import axios from 'axios';
import { hintToast } from "./tools"; import { hintToast } from "./tools";
let baseUrl2 = ''; //二类区地址 let baseUrl2 = ''; //二类区地址
let baseUrlZddwUrl = ''; //重点单位地址 let baseUrlZddwUrl = ''; //重点单位地址
let zyURL = '/mosty-api'; let gjURL = '/api';
try { try {
let fwzxToken = bridge.getToken(); let fwzxToken = bridge.getToken();
baseUrl2 = `http://118.122.165.45:35623`; baseUrl2 = `http://220.166.58.28:172`;
baseUrlZddwUrl = `http://118.122.165.45:35623`; baseUrlZddwUrl = `http://220.166.58.28:172`;
zyURL = `http://118.122.165.45:35623/mosty-api`; gjURL = `http://220.166.58.28:172/api`;
// baseUrl2 = `http://172.20.19.221:8006`;
// baseUrlZddwUrl = `http://172.20.19.221:8006`;
// zyURL = `http://172.20.19.221:8006/mosty-api`;
} catch (error) { } catch (error) {
} }
const service = axios.create({ const service = axios.create({
baseURL: zyURL, baseURL: gjURL,
timeout: 60000 timeout: 60000
}); });
@ -28,7 +25,7 @@ service.interceptors.request.use(
//1.统一注入token //1.统一注入token
let token = window.localStorage.getItem('token') let token = window.localStorage.getItem('token')
if (token) { if (token) {
config.headers.Authorization = token; config.headers.Authorization = `Bearer ${token}`;
} }
//2.设置headers icode //2.设置headers icode
// config.headers.code = ''; // config.headers.code = '';

View File

@ -11,9 +11,8 @@ module.exports = {
devServer: { devServer: {
port: 9528, port: 9528,
proxy: { proxy: {
'/mosty-api': { '/api': {
target: "http://118.122.165.45:35623", target: "http://220.166.58.28:172",
// target: "http://192.168.31.91:8006",
changeOrigin: true, changeOrigin: true,
}, },
} }