feat: 按业务要求微调模块

master
donghao 5 days ago
parent c740711751
commit 9d98404e71

@ -1,8 +1,16 @@
/*
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-06-10 17:36:23
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-06-16 11:29:01
* @FilePath: \Web-Traffic-Police\src\config\index.ts
* @Description: ,`customMade`, koroFileHeader : https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
const env = import.meta.env
export const config = {
apiHost: 'http://192.168.10.103:8888', // 接口地址
apiMapTimeout: 600_000, // 接口轮询时间
apiMapTimeout: 60_000, // 接口轮询时间
env: env.VITE_APP_ENV,
baseURL: env.VITE_APP_BASE_API,
timeout: 10000,

@ -0,0 +1,13 @@
/*
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-06-16 15:41:45
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-06-16 15:43:58
* @FilePath: \Web-Traffic-Police\src\utils\forFiles.ts
* @Description:
*/
export function fetchFileNameByPath(path) {
const lastSlash = path.lastIndexOf("/");
if (lastSlash === -1) return path; // 如果没有斜杠,返回整个路径
return path.substring(lastSlash + 1);
}

@ -1,23 +1,24 @@
<!-- src/components/PollingComponent.vue -->
<script setup lang="ts">
import { config } from '@/config';
import { config } from "@/config";
import Type1ObjectDetect from "./components/Type1ObjectDetect.vue";
import Type2LicensePlateRecog from "./components/Type2LicensePlateRecog.vue";
import Type3TargetRecog from "./components/Type3TargetRecog.vue";
import Type4AudioDetection from "./components/Type4AudioDetection.vue";
import { useAllData } from './hooks/useAllData';
import { useAllData } from "./hooks/useAllData";
//
interface ListItem {
titles: string[];
video_url: string;
}
const { filterMenuByData, filterDetailsByData, fetchTypeByTitle } = useAllData()
const { filterMenuByData, filterDetailsByData, fetchTypeByTitle } =
useAllData();
//
const dataList = ref<ListItem[]>([]);
const menuList = ref<string[]>([])
const menuList = ref<string[]>([]);
const detailInfo = ref<ListItem | null>(); //
const isLoading = ref<boolean>(false);
const isHomePage = ref<boolean>(true)
const isHomePage = ref<boolean>(true);
const currentTilte = ref<string>("");
const currentType = ref<string>("1");
let pollingTimer: number | null = null;
@ -30,11 +31,11 @@ const fetchList = async (): Promise<ListItem[]> => {
isLoading.value = true;
// API
const response = await fetch(config.apiHost);
if (!response.ok) throw new Error('请求失败');
if (!response.ok) throw new Error("请求失败");
const result = await response.json();
// TypeScript
if (!Array.isArray(result)) {
throw new Error('返回数据格式错误');
throw new Error("返回数据格式错误");
}
return result as ListItem[];
} catch (err) {
@ -44,11 +45,31 @@ const fetchList = async (): Promise<ListItem[]> => {
}
};
//
const fetchMenuByData = async () => {
try {
isLoading.value = true;
// API
const response = await fetch(config.apiHost + "/titles");
if (!response.ok) throw new Error("请求失败");
const result = await response.json();
// TypeScript
if (!Array.isArray(result)) {
throw new Error("返回数据格式错误");
}
menuList.value = result;
} catch (err) {
// return []; //
} finally {
isLoading.value = false;
}
};
//
const fetchInfoList = (result: ListItem[]) => {
dataList.value = result;
menuList.value = filterMenuByData(result);
}
// menuList.value = filterMenuByData(result);
};
//
const startPolling = () => {
@ -69,7 +90,6 @@ const stopPolling = () => {
abortController?.abort();
};
/**跳转页面 */
const tabDetails = (title: string) => {
currentTilte.value = title;
@ -77,17 +97,18 @@ const tabDetails = (title: string) => {
detailInfo.value = filterDetailsByData(toRaw(dataList.value), title);
currentType.value = fetchTypeByTitle(title);
isHomePage.value = false;
console.log(currentType.value, detailInfo.value, 'tabDetails');
}
console.log(currentType.value, detailInfo.value, "tabDetails");
};
//
onMounted(() => {
startPolling()
fetchMenuByData();
startPolling();
});
//
onUnmounted(() => {
stopPolling()
stopPolling();
});
</script>
@ -95,28 +116,55 @@ onUnmounted(() => {
<div class="flex items-center justify-center data-overview-wrap">
<div class="menu-wrap" v-if="isHomePage">
<!-- 递钱 递烟 -->
<div class="flex items-center justify-center menu-item" v-for="title in menuList" :key="title"
@click="tabDetails(title)">
<div
class="flex items-center justify-center menu-item"
v-for="title in menuList"
:key="title"
@click="tabDetails(title)"
>
<span>{{ title }}</span>
</div>
</div>
<div class="w-full content-box" v-else>
<!-- 标题栏 -->
<div class="tilte-bar type-first-top" @click="() => isHomePage = true">
<div class="tilte-bar type-first-top" @click="() => (isHomePage = true)">
<span></span> <span>{{ currentTilte }}</span>
</div>
<ul class="content-list-box">
<ul class="content-list-box" v-if="detailInfo?.length">
<li class="content-detail-box">
<Type2LicensePlateRecog typeKey="2" :info="detailInfo" :title="currentTilte" v-if="currentType === '2'" />
<Type3TargetRecog typeKey="3" :info="detailInfo" :title="currentTilte" v-else-if="currentType === '3'" />
<Type4AudioDetection typeKey="4" :info="detailInfo" :title="currentTilte" v-else-if="currentType === '4'" />
<Type1ObjectDetect typeKey="1" :info="detailInfo" :title="currentTilte" v-else />
<Type2LicensePlateRecog
typeKey="2"
:info="detailInfo"
:title="currentTilte"
v-if="currentType === '2'"
/>
<Type3TargetRecog
typeKey="3"
:info="detailInfo"
:title="currentTilte"
v-else-if="currentType === '3'"
/>
<Type4AudioDetection
typeKey="4"
:info="detailInfo"
:title="currentTilte"
v-else-if="currentType === '4'"
/>
<Type1ObjectDetect
typeKey="1"
:info="detailInfo"
:title="currentTilte"
v-else
/>
</li>
</ul>
<el-empty v-else>
<el-button type="primary" @click="() => (isHomePage = true)">返回</el-button>
</el-empty>
</div>
</div>
</template>
<style lang="scss">
@import url('./DataOverview.scss');
@import url("./DataOverview.scss");
</style>

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-06-12 10:26:59
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-06-13 14:17:19
* @LastEditTime: 2025-06-18 11:01:59
* @FilePath: \Web-Traffic-Police\src\views\dataView\components\Type1ObjectDetect.vue
* @Description: 目标检测
-->
@ -13,7 +13,7 @@
>
<div
class="type-first-mid-box"
v-for="(item, index) in info.slice(
v-for="(item, index) in currentInfo.slice(
(pagination.page - 1) * pagination.pageSize,
pagination.page * pagination.pageSize
)"
@ -26,41 +26,55 @@
controls
></video>
</div>
<div
class="type-first-mid-box-text max-h-[140px] overflow-auto"
v-if="title === '酒精度数'"
>
<span>酒精度数</span>
<span style="font-weight: normal"
>{{ item["酒精度数"] }}mg/100ml</span
>
</div>
<div class="type-first-mid-box-text" v-else-if="title === ''">
<span>语音角色</span>
<div class="max-h-[120px] overflow-auto">
<p v-for="(v, k) in item['语音角色']" :key="k">
<span>角色{{ Number(k) + 1 }}</span>
<span style="font-weight: normal">{{
transformKeyframes(v)
}}</span>
</p>
<div class="type-first-mid-box-text max-h-[140px] overflow-auto">
<div>
<span>视频名称</span>
<span style="font-weight: normal">{{
fetchFileNameByPath(item?.video_url)
}}</span>
</div>
<template v-if="title === '酒精度数'">
<div>
<span>酒精度数</span>
<span style="font-weight: normal"
>{{ item["酒精度数"] }}mg/100ml</span
>
</div>
<div>
<span>酒精检测时间</span>
<span style="font-weight: normal"
>{{ item["酒精检测时间"] }}</span
>
</div>
</template>
<div v-else-if="title === ''">
<span>语音角色</span>
<div class="max-h-[75px] overflow-auto">
<div v-for="(v, k) in item['语音角色']" :key="k">
<span>角色{{ Number(k) + 1 }}</span>
<span style="font-weight: normal">{{
transformKeyframes(v)
}}</span>
</div>
</div>
</div>
<div v-else>
<span>关键帧</span>
<span style="font-weight: normal">{{
transformKeyframes(item[`${title}时间`])
}}</span>
</div>
</div>
<div class="type-first-mid-box-text max-h-[140px] overflow-auto" v-else>
<span>关键帧</span>
<span style="font-weight: normal">{{
transformKeyframes(item[`${title}时间`])
}}</span>
</div>
</div>
</div>
<div class="type-first-bottom mt-[87px]" v-if="info?.length">
<div class="type-first-bottom mt-[87px]" v-if="currentInfo?.length">
<BasePagination
class="bg-transparent"
:showTable="false"
:total="info?.length"
:total="currentInfo?.length"
:pageSize="pagination.pageSize"
:dataSource="info"
:dataSource="currentInfo"
:isFixedPagination="true"
:page="pagination.page"
@change="changePage"
@ -72,6 +86,7 @@
<script lang="ts" setup>
import { BasePagination } from "@/components/CustomTable";
import { fetchFileNameByPath } from "@/utils/forFiles";
defineOptions({
name: "Type1ObjectDetectWarp",
@ -87,11 +102,16 @@ const props = withDefaults(defineProps<Props>(), {
title: String,
});
const pagination = ref({ page: 1, pageSize: 8 });
const currentInfo = ref<Record<string, any> | null>(null);
//
function changePage({ page, pageSize }) {
pagination.value = { page, pageSize };
}
function transformKeyframes(data) {
if (!Array.isArray(data)) {
return "";
}
//
const result = data.map((item) => {
// item [, ]
@ -104,6 +124,22 @@ function transformKeyframes(data) {
//
return `["${result.filter(Boolean).join('", "')}"]`;
}
watch(
props.info,
() => {
if (["遮挡", "人机分离"].includes(props.title)) {
currentInfo.value = props.info.filter((item) => item?.[props.title]);
} else {
currentInfo.value = props.info;
}
},
{
immediate: true,
deep: true,
}
);
</script>
<style lang="scss" scoped>
.type-first {
@ -137,7 +173,7 @@ function transformKeyframes(data) {
box-sizing: border-box;
width: 100%;
height: 162px;
padding: 16px;
padding: 16px 16px 0;
font-family: PingFang SC, PingFang SC;
font-weight: bold;
font-size: 14px;

@ -2,21 +2,31 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-06-12 10:27:06
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-06-13 14:07:24
* @LastEditTime: 2025-06-18 10:54:50
* @FilePath: \Web-Traffic-Police\src\views\dataView\components\Type2LicensePlateRecog.vue
* @Description: 车牌识别 人脸检测
-->
<template>
<div class="type-second">
<div class="type-second-mid">
<div class="type-second-mid-video">
<div class="arrow-icon left-arrow" @click="toPrev()" v-if="info?.length > 1"></div>
<video
class="w-[100%] h-[100%]"
:src="currentInfo?.video_url"
controls
></video>
<div class="arrow-icon right-arrow" @click="toNext()" v-if="info?.length > 1"></div>
<div class="type-second-mid-video" v-if="info?.length > 0">
<div
class="arrow-icon left-arrow"
@click="toPrev()"
v-if="info?.length > 1"
></div>
<div class=" h-[100%] flex flex-col justify-center items-center">
<video
:src="currentInfo?.video_url"
controls
></video>
<p>{{ fetchFileNameByPath(currentInfo?.video_url) }}</p>
</div>
<div
class="arrow-icon right-arrow"
@click="toNext()"
v-if="info?.length > 1"
></div>
</div>
<ul
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 type-second-mid-box"
@ -30,8 +40,10 @@
)"
:key="index"
>
<div class="type-second-mid-crad-image max-w-[100%] overflow-hidden">
<img :src="item['车牌图片']" class="h-[100%]" />
<div
class="flex items-center justify-center overflow-hidden bg-black type-second-mid-crad-image"
>
<img :src="item['车牌图片']" class="max-h-[100%] max-w-[100%] scale-[1.5]" />
</div>
<div class="type-second-mid-crad-text text_color_6">
<p>
@ -41,7 +53,8 @@
}}</span>
</p>
<p>
<span>牌照号码</span><span class="text_des_1">{{ item["车牌号"] }}</span>
<span>牌照号码</span
><span class="text_des_1">{{ item["车牌号"] }}</span>
</p>
</div>
</li>
@ -55,8 +68,10 @@
)"
:key="index"
>
<div class="flex items-center justify-center type-second-mid-crad-image max-w-[100%] overflow-hidden">
<img :src="item['人脸图片']" class="h-[100%]" />
<div
class="flex items-center justify-center type-second-mid-crad-image max-w-[100%] overflow-hidden"
>
<img :src="item['人脸图片']" class="h-[100%]" />
</div>
<div class="type-second-mid-crad-text text_color_6">
<p>
@ -84,7 +99,7 @@
</template>
<script lang="ts" setup>
import { BasePagination } from "@/components/CustomTable";
import { fetchFileNameByPath } from "@/utils/forFiles";
defineOptions({
name: "Type2LicensePlateRecogWarp",
});
@ -100,11 +115,11 @@ const props = withDefaults(defineProps<Props>(), {
});
//
const objectRegTypeMap = {
'黄': "#E3A108",
"蓝": "#3B64AC",
"绿": "#2BC284",
"白": "#1d2129",
}
["黄"]: "#E3A108",
["蓝"]: "#3B64AC",
["绿"]: "#2BC284",
["白"]: "#1d2129",
};
const pagination = ref({ page: 1, pageSize: 8 });
const currentIndex = ref<number>(0); //
const currentInfo = ref<Record<string, any> | null>(null);
@ -127,7 +142,7 @@ function convertFRArrToObjects() {
for (let i = 0; i < currentInfo.value?.["人脸图片"].length; i++) {
result.push({
["人脸图片"]: currentInfo.value?.["人脸图片"][i],
faceNO: (Number(i) + 1 < 10 ? "0" : "") + (Number(i) + 1),
faceNO: (Number(i) + 1 < 10 ? "0" : "") + (Number(i) + 1),
});
}
return result;
@ -140,7 +155,7 @@ function changePage({ page, pageSize }) {
//
function changeCurrentVideo(index: number) {
pagination.value = {...toRaw(pagination.value), page: 1 };
pagination.value = { ...toRaw(pagination.value), page: 1 };
currentIndex.value = index;
currentInfo.value = props.info[index];
}
@ -172,7 +187,11 @@ watch(
watch(
() => [currentIndex.value],
() => {
console.log(currentInfo.value, props.title, "watch-车牌识别-currentInfo.value");
console.log(
currentInfo.value,
props.title,
"watch-车牌识别-currentInfo.value"
);
switch (props.title) {
case "车牌识别":
currentInfoList.value = convertLPRArrToObjects();
@ -219,9 +238,8 @@ watch(
background-position: center;
background-repeat: no-repeat;
}
.right-arrow{
.right-arrow {
background-image: url("@/assets/images/right_arrow.png");
}
video {
width: 720px;

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-06-12 10:37:10
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-06-13 14:13:16
* @LastEditTime: 2025-06-16 16:16:04
* @FilePath: \Web-Traffic-Police\src\views\dataView\components\Type4AudioDetection.vue
* @Description: 递烟
-->
@ -18,11 +18,10 @@
@click="toPrev()"
v-if="info?.length > 1"
></div>
<video
class="w-[720px] h-[100%]"
:src="currentInfo?.video_url"
controls
></video>
<div class="h-[100%] flex flex-col justify-center items-center">
<video :src="currentInfo?.video_url" controls></video>
<p>{{ fetchFileNameByPath(currentInfo?.video_url) }}</p>
</div>
<div
class="arrow-icon right-arrow"
@click="toNext()"
@ -32,11 +31,11 @@
<div class="type-fourth-mid-tab">
<el-button
v-for="v in regTypeMap"
:type="currentRegType === v.key ? 'primary' : ''"
:type="currentRegType === v ? 'primary' : ''"
class="w-[76px] h-[32px!important]"
:key="v.key"
:key="v"
@click="changeRegType(v)"
>{{ v.title }}</el-button
>{{ v }}</el-button
>
</div>
<div
@ -50,7 +49,9 @@
)"
:key="index"
>
<div class="flex items-center justify-center type-fourth-mid-crad-image max-w-[100%] overflow-hidden">
<div
class="flex items-center justify-center type-fourth-mid-crad-image max-w-[100%] overflow-hidden"
>
<img :src="item['图片']" class="h-[100%]" />
</div>
<div class="type-fourth-mid-crad-text">
@ -76,6 +77,8 @@
</template>
<script lang="ts" setup>
import { BasePagination } from "@/components/CustomTable";
import { fetchFileNameByPath } from "@/utils/forFiles";
defineOptions({
name: "Type3TargetRecog",
});
@ -91,25 +94,8 @@ const props = withDefaults(defineProps<Props>(), {
title: String,
});
//
const regTypeMap = [
{
key: "0",
title: "全部",
},
{
key: "人",
title: "人员",
},
{
key: "车",
title: "车辆",
},
{
key: "信号灯",
title: "路灯",
},
];
const currentRegType = ref<string>("0");
const regTypeMap = ref<string[]>([]);
const currentRegType = ref<string>("全部");
const pagination = ref({ page: 1, pageSize: 8 });
const currentIndex = ref<number>(0); //
const currentInfo = ref<Record<string, any> | null>(null);
@ -154,7 +140,7 @@ function toNext() {
//
function changeRegType(record) {
currentRegType.value = record.key;
currentRegType.value = record;
console.log(record, "changeRegType");
}
@ -162,6 +148,7 @@ watch(
props.info,
() => {
currentInfo.value = props.info[0];
regTypeMap.value = ['全部', ...Object.keys(currentInfo.value?.["目标检测"])];
},
{
immediate: true,
@ -179,12 +166,12 @@ watch(
);
pagination.value = { ...toRaw(pagination.value), page: 1 };
const currRegTypeKey = toRaw(currentRegType.value);
if (currRegTypeKey === "0") {
currentInfoList.value = [
...convertObjects("人"),
...convertObjects("车"),
...convertObjects("信号灯"),
];
if (currRegTypeKey === "全部") {
let targerArr = []
Object.keys(currentInfo.value?.["目标检测"]).forEach((key) => {
targerArr = targerArr.concat([...convertObjects(key)])
})
currentInfoList.value = targerArr
} else {
currentInfoList.value = convertObjects(currRegTypeKey);
}

@ -2,7 +2,7 @@
* @Author: donghao donghao@supervision.ltd
* @Date: 2025-06-12 10:37:10
* @LastEditors: donghao donghao@supervision.ltd
* @LastEditTime: 2025-06-13 09:22:29
* @LastEditTime: 2025-06-18 10:50:56
* @FilePath: \Web-Traffic-Police\src\views\dataView\components\Type4AudioDetection.vue
* @Description: 音频检测
-->
@ -43,6 +43,7 @@
<script lang="ts" setup>
import { BaseTable, BasePagination } from "@/components/CustomTable";
import onLineIcon from "@/assets/common/online_icon.png";
import { fetchFileNameByPath } from "@/utils/forFiles";
defineOptions({
name: "Type4AudioDetectionWarp",
@ -66,18 +67,28 @@ const columns = [
{
label: "视频",
property: "video_url",
width: 240,
width: 200,
formatter: (row) => {
return h("video", {
src: row.video_url,
controls: "controls",
style: {
width: "180px",
width: "160px",
height: "100px",
},
});
},
},
{
label: "文件名称",
property: "video_url",
formatter: (row, column, value) => {
if (value) {
return fetchFileNameByPath(value);
}
return "-";
},
},
{
label: "告知执法依据",
property: "告知执法依据",
@ -110,6 +121,23 @@ const columns = [
}
},
},
//
{
label: "告知处罚内容",
property: "告知处罚内容",
formatter: (row, column, value) => {
if (value) {
return h("img", {
src: onLineIcon,
style: {
width: "20px",
height: "20px",
marginRight: "10px",
},
});
}
},
},
{
label: "告知申辩权利",
property: "告知申辩权利",
@ -129,7 +157,7 @@ const columns = [
{
label: "告知复议诉讼等救济途径",
property: "告知复议诉讼等救济途径",
width: 190,
width: 120,
formatter: (row, column, value) => {
if (value) {
return h("img", {
@ -146,7 +174,7 @@ const columns = [
{
label: "告知执法全程被记录",
property: "告知执法全程被记录",
width: 160,
width: 120,
formatter: (row, column, value) => {
if (value) {
return h("img", {
@ -194,7 +222,7 @@ const columns = [
},
},
{
label: "语速(字/分钟)",
label: "语速",
property: "语速",
},
];

@ -29,6 +29,7 @@ export const useAllData = () => {
"过激行为",
"语音角色",
"多次吹气",
"视频质量检测",
],
"3": ["目标检测"],
"2": ["车牌识别", "人脸检测"],

Loading…
Cancel
Save