diff --git a/public/serverConfig.json b/public/serverConfig.json index aec550f..8bb2c29 100644 --- a/public/serverConfig.json +++ b/public/serverConfig.json @@ -19,5 +19,5 @@ "CachingAsyncRoutes": false, "TooltipEffect": "light", "ResponsiveStorageNameSpace": "responsive-", - "AdminHostUrl": "http://192.168.10.81:8848/" + "AdminHostUrl": "http://192.168.10.13:8000" } diff --git a/src/api/utils.ts b/src/api/utils.ts index 2f0d1a1..98ade66 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -1,2 +1,4 @@ const { VITE_APP_BASE_URL } = import.meta.env; -export const baseUrlApi = (url: string) => `${VITE_APP_BASE_URL}/api/${url}`; +import { getConfig } from "@/config"; +export const baseUrlApi = (url: string) => + `${getConfig().AdminHostUrl || VITE_APP_BASE_URL}/api/${url}`; diff --git a/src/api/videoParse.ts b/src/api/videoParse.ts new file mode 100644 index 0000000..07ad0f2 --- /dev/null +++ b/src/api/videoParse.ts @@ -0,0 +1,24 @@ +import { http } from "@/utils/http"; +import { baseUrlApi } from "./utils"; + +type Result = { + msg: any; + success: boolean; + data?: { + person?: Array<any>; + car?: Array<any>; + }; +}; +type Data = { + success: boolean; + msg: any; + count: number; + results?: Array<any>; +}; + +export const getPicList = (params?: object) => { + return http.request<Result>("get", baseUrlApi("tps/pics"), { params }); +}; +export const getTPList = (params?: object) => { + return http.request<Data>("get", baseUrlApi("tps"), { params }); +}; diff --git a/src/router/modules/videoParse.ts b/src/router/modules/videoParse.ts new file mode 100644 index 0000000..c17e2e3 --- /dev/null +++ b/src/router/modules/videoParse.ts @@ -0,0 +1,20 @@ +export default { + path: "/videoParse", + // redirect: "/error/403", + meta: { + icon: "lollipop", + title: "视频分析", + showLink: true, + rank: 10 + }, + children: [ + { + path: "/videoParse/index", + name: "VideoParsePage", + component: () => import("@/views/videoParse/index.vue"), + meta: { + title: "视频分析" + } + } + ] +} as RouteConfigsTable; diff --git a/src/views/videoParse/hook.tsx b/src/views/videoParse/hook.tsx new file mode 100644 index 0000000..e53dddd --- /dev/null +++ b/src/views/videoParse/hook.tsx @@ -0,0 +1,77 @@ +import { addDialog } from "@/components/ReDialog"; +// import { ref, h } from "vue"; +import OurPlayer from "@/components/VideoPlayer/OurPlayer.vue"; +// import { http } from "@/utils/http"; + +export function videoUtil() { + // const currentPlayingIndex = ref(-1); + const columns: TableColumnList = [ + { + label: "视频内时间", + prop: "relative_time", + minWidth: 100, + sortable: true + }, + { + label: "异常类型", + prop: "exception_type", + minWidth: 100 + }, + { + label: "异常ID", + prop: "abnormal_id", + minWidth: 100 + }, + { + label: "异常行为", + prop: "abnormal_behavior", + minWidth: 100, + slot: "violation" + }, + { + label: "异常关键帧", + slot: "image" + }, + { + label: "异常视频定位", + slot: "video" + } + // { + // label: "视频", + // slot: "video" + // }, + ]; + function openDialog(title = "视频", row) { + const highlights = [ + // 进度条时间点高亮 + { + text: row.event_type, + time: row.relative_time + } + ]; + sessionStorage.setItem("video_url", row.abnormal_video); + sessionStorage.setItem("highlights", JSON.stringify(highlights)); + addDialog({ + title: `${title}用户`, + width: "40%", + draggable: true, + fullscreenIcon: true, + closeOnClickModal: false, + contentRenderer: () => <OurPlayer></OurPlayer> + // return h( + // "video", + // { + // controls: true, + // id: "video-" + index, + // onPlay: () => playVideo(row, index) + // }, + // [h("source", { src: row.video_dir, type: "video/mp4" })] + // ); + }); + } + + return { + columns, + openDialog + }; +} diff --git a/src/views/videoParse/index.vue b/src/views/videoParse/index.vue new file mode 100644 index 0000000..ba98cea --- /dev/null +++ b/src/views/videoParse/index.vue @@ -0,0 +1,454 @@ +<script setup lang="ts"> +import { useWindowSize } from "@vueuse/core"; +import { Help } from "@element-plus/icons-vue"; +import { ref, reactive, onMounted } from "vue"; +import { PureTableBar } from "@/components/RePureTableBar"; +import { PureTable } from "@pureadmin/table"; +import { type PaginationProps } from "@pureadmin/table"; +import { getPicList, getTPList } from "@/api/videoParse"; +import { getToken } from "@/utils/auth"; +import { ElMessage, ElMessageBox } from "element-plus"; +import { videoUtil } from "./hook"; + +import type { UploadProps, UploadUserFile } from "element-plus"; + +defineOptions({ + name: "videoParse" +}); +const { columns, openDialog } = videoUtil(); +const fileList = ref<UploadUserFile[]>([ + { + name: "element-plus-logo.svg", + url: "https://element-plus.org/images/element-plus-logo.svg" + } +]); + +const handleRemove: UploadProps["onRemove"] = (file, uploadFiles) => { + console.log(file, uploadFiles); +}; + +const handlePreview: UploadProps["onPreview"] = uploadFile => { + console.log(uploadFile); +}; + +const handleExceed: UploadProps["onExceed"] = (files, uploadFiles) => { + ElMessage.warning( + `The limit is 3, you selected ${files.length} files this time, add up to ${ + files.length + uploadFiles.length + } totally` + ); +}; + +const beforeRemove: UploadProps["beforeRemove"] = uploadFile => { + return ElMessageBox.confirm( + `Cancel the transfer of ${uploadFile.name} ?` + ).then( + () => true, + () => false + ); +}; +const { height } = useWindowSize(); + +const videoData = ref([]); +const loading = ref(true); +const currentPage = ref(1); +const totalNumber = ref(0); +const pageSize = ref(10); +// const isDisplay = true; +const pagination = reactive<PaginationProps>({ + total: totalNumber.value, + pageSize: pageSize.value, + currentPage: currentPage.value, + background: true +}); +const personOddList = ref([]); +const personEvenList = ref([]); +const carOddList = ref([]); +const carEvenList = ref([]); + +function handleUpdate(row) { + openDialog("我的", row); + console.log(row); +} +function handleSizeChange(val) { + pageSize.value = val; + onSearch(); +} + +function handleCurrentChange(val) { + currentPage.value = val; + onSearch(); +} +function onSearch() { + const params = { + video_name: "test1", + page: currentPage.value || undefined, + page_size: pageSize.value || undefined + }; + getTPList(params).then(response => { + if (response.success) { + const dataList = response; + totalNumber.value = dataList.count; + pagination.total = dataList.count; + videoData.value = dataList.results; + setTimeout(() => { + loading.value = false; + }, 500); + } + }); + getPicList(params).then(response => { + if (response.success) { + const dataList = response; + personOddList.value = dataList.data.person.filter( + item => item.id % 2 == 1 + ); + personEvenList.value = dataList.data.person.filter( + item => item.id % 2 == 0 + ); + carOddList.value = dataList.data.car.filter(item => item.id % 2 == 1); + carEvenList.value = dataList.data.car.filter(item => item.id % 2 == 0); + setTimeout(() => { + loading.value = false; + }, 500); + } + }); +} + +function spanStyle(type) { + switch (type) { + case 1: + return "span-first"; + case 2: + return "span-second"; + case 3: + return "span-third"; + default: + ""; + break; + } +} +onMounted(() => { + onSearch(); +}); +</script> + +<template> + <div class="main"> + <el-row :gutter="24"> + <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24"> + <el-card + class="box-card" + :style="{ height: `calc(${height}px - 150px)` }" + > + <template #header> + <div class="card-header"> + <span>视频分析</span> + </div> + </template> + <div class="file-upload flex justify-around"> + <div class="upload-box"> + <el-upload + v-model:file-list="fileList" + class="upload" + :headers="{ token: 'Bearer ' + getToken().accessToken }" + action="http://192.168.10.13:8000/api/tps/upload_video" + multiple + :on-preview="handlePreview" + :on-remove="handleRemove" + :before-remove="beforeRemove" + :limit="1" + :on-exceed="handleExceed" + > + <span>{{ "选择分析文件" }}</span + ><el-button type="primary">选择文件</el-button> + <!-- <template #tip> + <div class="el-upload__tip"> + jpg/png files with a size less than 500KB. + </div> + </template> --> + </el-upload> + </div> + <el-button type="success" size="large"> + 分析 <el-icon class="el-icon--right"><Help /></el-icon + ></el-button> + </div> + <el-divider border-style="dashed" /> + <!-- <el-empty description="暂无数据" v-show="videoData.length == 0" /> --> + <div class="analysis-content flex justify-between"> + <div + class="analysis-box" + :style="{ height: `calc(${height}px - 60vh - 150px)` }" + > + <span class="analysis-box-label">被执法人员推定</span> + <div + class="analysis-table" + :style="{ height: `calc(${height}px - 60vh - 240px)` }" + > + <div class="analysis-table-left"> + <div class="analysis-box-lable w-full flex justify-end"> + <span>目标清晰度</span> + </div> + <el-scrollbar :height="`calc(${height}px - 60vh - 220px)`"> + <div + v-for="(item, index) in personOddList" + :key="index" + class="scrollbar-item" + > + <span :class="spanStyle(item.id)">{{ item.id }}</span> + <el-image + style="height: 100px" + :src="item.pic_path" + :zoom-rate="1.2" + :preview-src-list="[item.pic_path]" + fit="cover" + /> + <span>{{ item.pic_quality }}</span> + </div> + </el-scrollbar> + </div> + <el-divider + direction="vertical" + :style="{ height: `calc(${height}px - 60vh - 220px)` }" + /> + <div class="analysis-table-left"> + <div class="analysis-box-lable w-full flex justify-end"> + <span>目标清晰度</span> + </div> + <el-scrollbar :height="`calc(${height}px - 60vh - 220px)`"> + <div + v-for="(item, index) in personEvenList" + :key="index" + class="scrollbar-item" + > + <span :class="spanStyle(item.id)">{{ item.id }}</span> + <el-image + style="height: 100px" + :src="item.pic_path" + :zoom-rate="1.2" + :preview-src-list="[item.pic_path]" + fit="cover" + /> + <span>{{ item.pic_quality }}</span> + </div> + </el-scrollbar> + </div> + </div> + </div> + <div + class="analysis-box" + :style="{ height: `calc(${height}px - 60vh - 150px)` }" + > + <span class="analysis-box-label">被执法车辆推定</span> + <div + class="analysis-table" + :style="{ height: `calc(${height}px - 60vh - 240px)` }" + > + <div class="analysis-table-left"> + <div class="analysis-box-lable w-full flex justify-end"> + <span>目标清晰度</span> + </div> + <el-scrollbar :height="`calc(${height}px - 60vh - 220px)`"> + <div + v-for="item in carOddList" + :key="item.id" + class="scrollbar-item" + > + <span :class="spanStyle(item.id)">{{ item.id }}</span> + <div class="defect-image"> + <el-image + style="height: 100px" + :src="item.pic_path" + :zoom-rate="1.2" + :preview-src-list="[item.pic_path]" + fit="cover" + /> + <span>{{ item.car_number }}</span> + </div> + <span>{{ item.pic_quality }}</span> + </div> + </el-scrollbar> + </div> + <el-divider + direction="vertical" + :style="{ height: `calc(${height}px - 60vh - 240px)` }" + /> + <div class="analysis-table-left"> + <div class="analysis-box-lable w-full flex justify-end"> + <span>目标清晰度</span> + </div> + <el-scrollbar :height="`calc(${height}px - 60vh - 220px)`"> + <div + v-for="item in carEvenList" + :key="item.id" + class="scrollbar-item" + > + <span :class="spanStyle(item.id)">{{ item.id }}</span> + <div class="defect-image"> + <el-image + style="height: 100px" + :src="item.pic_path" + :zoom-rate="1.2" + :preview-src-list="[item.pic_path]" + fit="cover" + /> + <span>{{ item.car_number }}</span> + </div> + <span>{{ item.pic_quality }}</span> + </div> + </el-scrollbar> + </div> + </div> + </div> + </div> + <PureTableBar + title="视频分析列表" + :columns="columns" + @refresh="onSearch" + > + <template v-slot="{ size, dynamicColumns }"> + <pure-table + border + adaptive + align-whole="center" + table-layout="auto" + :loading="loading" + :size="size" + :data="videoData" + :columns="dynamicColumns" + :pagination="pagination" + :paginationSmall="size === 'small' ? true : false" + :header-cell-style="{ + background: 'var(--el-table-row-hover-bg-color)', + color: 'var(--el-text-color-primary)' + }" + @page-size-change="handleSizeChange" + @page-current-change="handleCurrentChange" + > + <template #violation="{ row }"> + {{ row.is_violation ? "是" : "否" }} + </template> + <template #image="{ row, index }"> + <el-image + preview-teleported + loading="lazy" + :src="row.abnormal_pic" + :preview-src-list="[row.abnormal_pic]" + :initial-index="index" + fit="cover" + class="w-[100px] h-[100px]" + /> + </template> + <template #video="{ row }"> + <el-button + class="reset-margin" + link + type="primary" + :size="size" + @click="handleUpdate(row)" + > + 播放视频 + </el-button> + </template> + </pure-table> + </template> + </PureTableBar> + </el-card> + </el-col> + </el-row> + </div> +</template> +<style scoped lang="scss"> +.card-header { + span { + font-weight: bold; + } +} + +.file-upload { + height: 60px; + + .upload-box { + width: 300px; + + .upload { + span { + margin-right: 10px; + } + } + } +} + +.analysis-content { + .analysis-box { + width: 48%; + padding: 20px; + box-shadow: 5px 5px 5px rgb(0 0 0 / 2.5%), 5px -5px 5px rgb(0 0 0 / 2.5%), + -5px 5px 5px rgb(0 0 0 / 2.5%), -5px -5px 5px rgb(0 0 0 / 2.5%); + // background: skyblue; + // height: 400px; + .analysis-box-label { + font-size: 20px; + font-weight: bold; + } + + .analysis-table { + display: flex; + justify-content: space-between; + width: 100%; + + .analysis-table-left { + width: 45%; + + .analysis-box-lable { + padding-right: 10px; + } + + .scrollbar-item { + display: flex; + align-items: center; + justify-content: space-around; + margin-bottom: 20px; + + .defect-image { + position: relative; + + span { + position: absolute; + bottom: 7px; + left: 0; + width: 100%; + height: 30px; + line-height: 30px; + color: #fff; + text-align: center; + background-color: rgb(0 0 0 / 50%); + } + } + } + } + } + } +} + +:deep(.analysis-box-line) { + height: 320px; +} + +.span-first { + font-size: 28px; + font-weight: bold; + color: #e02828; +} + +.span-second { + font-size: 28px; + font-weight: bold; + color: #e58b03; +} + +.span-third { + font-size: 28px; + font-weight: bold; + color: #516ee0; +} +</style> diff --git a/src/views/welcome/hook.tsx b/src/views/welcome/hook.tsx index df8554c..d01654c 100644 --- a/src/views/welcome/hook.tsx +++ b/src/views/welcome/hook.tsx @@ -1,10 +1,20 @@ import { addDialog } from "@/components/ReDialog"; -import { ref, h } from "vue"; +// import { ref, h } from "vue"; +import OurPlayer from "@/components/VideoPlayer/OurPlayer.vue"; // import { http } from "@/utils/http"; export function welcomeUtil() { - const currentPlayingIndex = ref(-1); - function openDialog(title = "视频", row, index: number) { + // const currentPlayingIndex = ref(-1); + function openDialog(title = "视频", row) { + const highlights = [ + // 进度条时间点高亮 + { + text: row.event_type, + time: row.relative_time + } + ]; + sessionStorage.setItem("video_url", row.video_dir); + sessionStorage.setItem("highlights", JSON.stringify(highlights)); console.log(row.video_dir, "row.video_dir"); addDialog({ title: `${title}用户`, @@ -12,28 +22,27 @@ export function welcomeUtil() { draggable: true, fullscreenIcon: true, closeOnClickModal: false, - contentRenderer: () => { - return h( - "video", - { - controls: true, - id: "video-" + index, - onPlay: () => playVideo(row, index) - }, - [h("source", { src: row.video_dir, type: "video/mp4" })] - ); - } + contentRenderer: () => <OurPlayer></OurPlayer> + // return h( + // "video", + // { + // controls: true, + // id: "video-" + index, + // onPlay: () => playVideo(row, index) + // }, + // [h("source", { src: row.video_dir, type: "video/mp4" })] + // ); }); } - function playVideo(row, index) { - if (currentPlayingIndex.value !== -1) { - const videoElement = document.getElementById( - "video-" + currentPlayingIndex.value - ); - videoElement.pause(); // 暂停当前正在播放的视频 - } - currentPlayingIndex.value = index; - } + // function playVideo(row, index) { + // if (currentPlayingIndex.value !== -1) { + // const videoElement = document.getElementById( + // "video-" + currentPlayingIndex.value + // ); + // videoElement.pause(); // 暂停当前正在播放的视频 + // } + // currentPlayingIndex.value = index; + // } return { openDialog diff --git a/src/views/welcome/index.vue b/src/views/welcome/index.vue index 507bb54..8886506 100644 --- a/src/views/welcome/index.vue +++ b/src/views/welcome/index.vue @@ -44,26 +44,26 @@ const violationMap = ref({ }); // const currentPlayingIndex = ref(-1); // 当前正在播放的视频索引,默认为没有视频正在播放 -// function playVideo(index) { -// // data = JSON.parse(JSON.stringify(data)); -// // // console.log(AdminHostUrl) -// // dialogVisible.value = true; -// // const highlights = [ -// // // 进度条时间点高亮 -// // { -// // text: data.event_type, -// // time: data.relative_time -// // } -// // ]; -// // sessionStorage.setItem("video_url", data.video_dir); -// // sessionStorage.setItem("highlights", JSON.stringify(highlights)); -// if (currentPlayingIndex.value !== -1) { -// const videoElement = document.getElementById( -// "video-" + currentPlayingIndex.value -// ); -// videoElement.pause(); // 暂停当前正在播放的视频 +// function playVideo(data) { +// data = JSON.parse(JSON.stringify(data)); +// // console.log(AdminHostUrl) +// dialogVisible.value = true; +// const highlights = [ +// // 进度条时间点高亮 +// { +// text: data.event_type, +// time: data.relative_time // } -// currentPlayingIndex.value = index; +// ]; +// sessionStorage.setItem("video_url", data.video_dir); +// sessionStorage.setItem("highlights", JSON.stringify(highlights)); +// if (currentPlayingIndex.value !== -1) { +// const videoElement = document.getElementById( +// "video-" + currentPlayingIndex.value +// ); +// videoElement.pause(); // 暂停当前正在播放的视频 +// } +// currentPlayingIndex.value = index; // } function onSearch() { @@ -254,8 +254,8 @@ const pagination = reactive<PaginationProps>({ currentPage: currentPage.value, background: true }); -function handleUpdate(row, index) { - openDialog("我的", row, index); +function handleUpdate(row) { + openDialog("我的", row); console.log(row); } // function handleDelete(row) { @@ -398,13 +398,13 @@ onMounted(() => { <source :src="row.video_dir" type="video/mp4" /> </video> </template> --> - <template #operation="{ row, index }"> + <template #operation="{ row }"> <el-button class="reset-margin" link type="primary" :size="size" - @click="handleUpdate(row, index)" + @click="handleUpdate(row)" > 播放视频 </el-button>