数字督察一体化平台-前端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

916 lines
28 KiB

<template>
<el-scrollbar height="100vh">
<div class="wrapper">
<datav-header />
<main>
<el-row :gutter="16">
<el-col :span="6">
<datav-card
title="内部视频"
sub-title="登录客户端"
:titleClick="titleClickFun"
>
<div class="row mt-4 mb-20">
<div class="col col-8">
<label>视频总数</label>
<span>{{ videoStatus.all }}</span>
</div>
<div class="col col-8">
<label>在线数</label>
<span>{{ videoStatus.on }}</span>
</div>
<div class="col col-8">
<label>在线率</label>
<span>{{ videoStatus.percentage }}</span>
</div>
</div>
<div class="mb-12">
<VideoPlay
:url="activeUrl"
:showOperateBtns="false"
/>
</div>
<el-scrollbar max-width="100%">
<div class="flex gap" style="width: 840px">
<div
v-for="(item, index) in videos"
:key="item"
@click="handlePlay(item, index)"
style="width: 160px"
>
<img
:src="item.imgUrl"
:active="activeVideoIndex === index"
/>
</div>
</div>
</el-scrollbar>
</datav-card>
<datav-card
title="问题数排名"
sub-title="查实问题数"
style="height: 470px"
>
<datav-tabs
type="bottom-button"
v-model="proRankTab"
ref="videoProRank"
>
<datav-tab-item label="分县市局" name="1">
<el-scrollbar height="400px">
<datav-chart-bar
:data="fxsjRankList"
size="large"
:color="colors"
/>
</el-scrollbar>
</datav-tab-item>
<datav-tab-item label="局属单位" name="2">
<el-scrollbar height="280px">
<datav-chart-bar
:data="jsdwRankList"
size="large"
:color="colors"
/>
</el-scrollbar>
</datav-tab-item>
</datav-tabs>
</datav-card>
</el-col>
<el-col :span="12">
<datav-date-picker v-model="time" />
<div class="flex gap-42">
<datav-statistic
:value="overview.total"
title="问题数"
style="width: 20%"
/>
<datav-statistic
:value="overview.completionProblem"
title="办结问题数"
style="width: 20%"
/>
<datav-statistic
:value="overview.discoverProblem"
title="查实问题数"
style="width: 20%"
/>
<datav-statistic
:value="overview.relativeOrg"
title="问责单位数"
style="width: 20%"
/>
<datav-statistic
:value="overview.relativePer"
title="问责人次"
style="width: 20%"
/>
</div>
<v-charts
style="height: 420px"
:option="option"
autoresize
@click="handleClick"
ref="videoMap"
/>
<datav-card title="问题趋势">
<v-charts
style="height: 250px"
:option="proTrend"
autoresize
ref="videoProTrend"
/>
<div class="gobal-dropdown-container">
<el-dropdown
class="test"
@command="handleCommand"
>
<span
class="el-dropdown-link my-gobal-yearselect"
>
{{ selectedYear + " 年" }}
</span>
<template #dropdown>
<el-dropdown-menu style="width: 90px">
<el-dropdown-item
v-for="year in years"
:key="year"
:command="year"
>{{ year + " 年" }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</datav-card>
</el-col>
<el-col :span="6">
<datav-card title="实时预警">
<el-scrollbar height="450px">
<div
class="message message-info pointer"
v-for="item in videoInspections"
:key="item"
:active="item.distributionState === '1'"
@click="handleOpenData"
>
<div class="message-title">
{{
item.distributionState === "1"
? "已处理"
: "待处理"
}}
</div>
<div>{{ item.title }}</div>
<div>{{ item.content }}</div>
<div class="message-footer flex between">
<span>{{ item.fsdwGajgmc }}</span>
<span>{{ item.rqsj }}</span>
</div>
</div>
<el-empty description="暂无数据" v-if="videoInspections.length === 0" />
</el-scrollbar>
</datav-card>
<datav-card title="问题涉及方面分布">
<v-charts
style="height: 300px"
:option="ProblemTypeRateChart"
autoresize
ref="videoProblemTypeRate"
/>
</datav-card>
</el-col>
</el-row>
</main>
</div>
</el-scrollbar>
</template>
<script setup>
import LivePlayer from "@liveqing/liveplayer-v3";
import changshaMap from "@/assets/data/changsha.json";
import vCharts from "vue-echarts";
import * as echarts from "echarts/core";
import {
getAllVideoSuperviseCount,
getVideoSuperviseProblemTypeRate,
getVideoSuperviseProblemRank,
getVideoSuperviseMap,
getVideoSuperviseTrend,
getVideoStatus,
} from "@/api/screen/videoSupervise.ts";
import { listVideoInspection } from "@/api/data/videoInspection";
import moment from "moment/moment.js";
import { mapOrgNameMapping } from "@/enums/orgMapping.js";
import { onMounted } from "vue";
// region 所有变量
const router = useRouter();
const time = ref([
moment().startOf("year").format("YYYY-MM-DD"),
moment().format("YYYY-MM-DD"),
]);
const proRankTab = ref("1");
const fxsjRankList = ref([]);
const jsdwRankList = ref([]);
const overview = ref({
total: 0,
discoverProblem: 0,
completionProblem: 0,
relativeOrg: 0,
relativePer: 0,
completionRate: 0,
});
const mapIconList = ref([
{
originalName: "浏阳市局",
name: "浏阳市",
discoverProblem: 135,
completionProblem: 135,
relativeOrg: 89,
relativePer: 152,
completionRate: 100,
},
]);
const currentYear = new Date().getFullYear();
const years = ref([
currentYear.toString(),
(currentYear - 1).toString(),
(currentYear - 2).toString(),
]); // 年份列表
const selectedYear = ref(currentYear); // 当前选中的年份
const videoProRank = ref(null);
const videoMap = ref(null);
const videoProTrend = ref(null);
const videoProblemTypeRate = ref(null);
let videoProRankIntervalId;
let videoMapIntervalId;
let videoTrendIntervalId;
let videoProblemTypeRateIntervalId;
const colors = [
{
color: "linear-gradient( 270deg, #FB002D 0%, #822232 100%)",
percentage: 80,
},
{
color: "linear-gradient( 270deg, #FFB90E 0%, #71501D 100%)",
percentage: 60,
},
];
// endregion
// region 所有图表
const proTrend = ref({
xAxis: {
type: "category",
boundaryGap: true,
data: [
"1月",
"2月",
"3月",
"4月",
"5月",
"6月",
"7月",
"8月",
"9月",
"10月",
"11月",
],
axisTick: {
// 去掉X轴刻度线
show: false,
},
},
yAxis: {
type: "value",
splitLine: {
show: true,
lineStyle: {
color: "#193775",
},
},
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "line",
label: {
backgroundColor: "#6a7985",
},
},
},
series: [
{
type: "line",
smooth: true,
lineStyle: {
color: "#28E6FF",
width: 4,
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(40,230,255,0.47)", // 渐变起始颜色
},
{
offset: 1,
color: "rgba(40,230,255,0)", // 渐变结束颜色
},
]),
},
data: [],
},
],
grid: {
left: "5%", // 图表距离容器左边的距离
right: "2%", // 图表距离容器右边的距离
top: "10%", // 图表距离容器上边的距离
bottom: "20%", // 图表距离容器下边的距离
containLabel: false, // 是否包含坐标轴的标签
},
});
echarts.registerMap("changsha", changshaMap);
const option = ref({
geo: {
map: "changsha",
itemStyle: {
normal: {
opacity: 0, // 设置透明度
},
},
},
tooltip: {
trigger: "item",
position: "bottom",
formatter: function (params) {
const dataItem = mapIconList.value.find((item) =>
item.name.includes(params.name.substring(0, 2))
);
// <li>办结率 <span>${dataItem.completionRate}%</span></li>
if (dataItem) {
return `
<div class="tooltip">
<div class="tooltip-title">${dataItem.originalName}</div>
<div class="tooltip-content">
<ul class="tooltip-ul" >
<li>查实问题数 <span>${dataItem.discoverProblem}</span></li>
<li>办结问题数 <span>${dataItem.completionProblem}</span></li>
<li>涉及单位数 <span>${dataItem.relativeOrg}</span></li>
<li>涉及人数 <span>${dataItem.relativePer}</span></li>
</ul>
</div>
</div>`;
} else {
return `<div class="tooltip">
<div class="tooltip-title">${params.name}</div>
<div class="tooltip-content">
<ul class="tooltip-ul"">
<li>查实问题数 <span>-</span></li>
<li>办结问题数 <span>-</span></li>
<li>涉及单位数 <span>-</span></li>
<li>涉及人数 <span>-</span></li>
</ul>
</div>
</div>`;
}
},
},
visualMap: {
type: "piecewise",
bottom: 10,
pieces: [
{ gte: 0, lte: 200, label: "低于平均问题30%", color: "#4987F6" },
{
gte: 200,
lte: 400,
label: "平均问题上下浮动30%内",
color: "#F6A149",
},
{ gte: 400, label: "高于平均问题30%", color: "#D34343" },
],
right: 10,
realtime: false,
orient: "horizontal",
textStyle: {
color: "#fff",
},
calculable: true,
inRange: {
color: ["#4987F6", "#F6A149", "#D34343"],
},
},
series: [
{
name: "长沙",
type: "map",
map: "changsha",
hoverAnimation: true,
roam: true,
label: {
show: true,
color: "white",
},
itemStyle: {
normal: {
areaColor: "#02215E",
borderColor: "#24D2EE",
borderWidth: 1, // 高亮时边框宽度
},
},
emphasis: {
areaColor: "#FFD700", // 高亮时区域颜色
borderColor: "#FF0000", // 高亮时边框颜色
borderWidth: 4, // 高亮时边框宽度
},
data: [],
},
],
});
const ProblemTypeRateChart = ref({
series: [
{
type: "pie",
radius: ["40%", "70%"],
label: {
color: "#fff",
},
data: [],
},
],
tooltip: {
trigger: "item",
},
}); // 问题涉及方面分布
// endregion
const titleClickFun =()=>{
window.open("http://65.47.26.34")
}
// region 所有函数
const getVideoSuperviseProblemRankData = async (timeValue) => {
const res = await getVideoSuperviseProblemRank(timeValue);
fxsjRankList.value = res.fxsjVideoSuperviseProblemRankList;
jsdwRankList.value = res.jsdwVideoSuperviseProblemRankList;
};
const getAllVideoSuperviseCountData = async (timeValue) => {
const res = await getAllVideoSuperviseCount(timeValue);
overview.value = res.overview;
};
const getVideoSuperviseMapData = async (timeValue) => {
const res = await getVideoSuperviseMap(timeValue);
const mappedData = mapOrgNameMapping(
res.videoSuperviseMapIconVoList,
"discoverProblem"
);
// 总数
const maxItem = mappedData.reduce(
(max, item) => (Number(item.value) > Number(max.value) ? item : max),
mappedData[0]
);
const range60Percent = maxItem.value * 0.6; // 不用取整,小数也可以
const range80Percent = maxItem.value * 0.8; // 不用取整,小数也可以
mapIconList.value = mappedData;
option.value.series[0].data = mappedData;
option.value.visualMap.pieces = [
{
gte: 0,
lte: range60Percent,
label: "低于最大问题数的60%",
color: "#4987F6",
},
{
gte: range60Percent,
lte: range80Percent,
label: "介于最大问题数的60%~80%",
color: "#F6A149",
},
{ gte: range80Percent, label: "高于最大问题数的80%", color: "#D34343" },
];
};
const getVideoSuperviseTrendData = async (year) => {
const res = await getVideoSuperviseTrend(year);
const temp = res.videoSuperviseTrendList;
const categories = temp.map((item) => item.name);
const values = temp.map((item) => item.value);
proTrend.value.xAxis.data = categories;
proTrend.value.series[0].data = values;
};
const getVideoSuperviseProblemTypeRateData = async (timeValue) => {
const res = await getVideoSuperviseProblemTypeRate(timeValue);
ProblemTypeRateChart.value.series[0].data =
res.videoSuperviseProblemTypeRate;
};
const getData = () => {
getVideoSuperviseProblemRankData(time.value);
getAllVideoSuperviseCountData(time.value);
getVideoSuperviseMapData(time.value);
getVideoSuperviseTrendData(selectedYear.value);
getVideoSuperviseProblemTypeRateData(time.value);
};
// endregion
// region 动画
// 问题数排名动画
const videoProRankAnimation = () => {
proRankTab.value = ((parseInt(proRankTab.value) % 2) + 1).toString();
};
videoProRankIntervalId = setInterval(videoProRankAnimation, 3000);
// 地图动画
const videoMapAnimation = () => {
const myVideoMap = videoMap?.value?.chart;
if (!myVideoMap) return;
// 来个随机数,9个图 0~8的整数就ok
const randomNum = Math.floor(Math.random() * 9);
myVideoMap.dispatchAction({
type: "downplay", //downplay 取消高亮
seriesIndex: 0,
});
myVideoMap.dispatchAction({
type: "highlight", //highLight 高亮指定的数据图形
seriesIndex: 0, //系列index
dataIndex: randomNum, //数据index
});
myVideoMap.dispatchAction({
type: "showTip", //showTip 显示提示框
seriesIndex: 0,
dataIndex: randomNum,
});
};
// 启动定时器,每隔 2 秒执行一次
// videoMapIntervalId = setInterval(videoMapAnimation, 2000);
// 问题趋势
const videoTrendAnimation = () => {
const trendTemp = videoProTrend?.value?.chart;
if (!trendTemp) return;
const randomNum = Math.floor(Math.random() * 12);
trendTemp.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: randomNum,
});
};
videoTrendIntervalId = setInterval(videoTrendAnimation, 2000);
// 问题涉及方面分布动画
const videoProblemTypeRateAnimation = () => {
const temp = videoProblemTypeRate?.value?.chart;
if (!temp) return;
const length = ProblemTypeRateChart.value.series[0].data.length;
const randomNum = Math.floor(Math.random() * length);
temp?.dispatchAction({ type: "downplay", seriesIndex: 0 });
temp?.dispatchAction({
type: "highlight",
seriesIndex: 0,
dataIndex: randomNum,
}); // 显示
temp?.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: randomNum,
});
};
videoProblemTypeRateIntervalId = setInterval(
videoProblemTypeRateAnimation,
2000
);
// endregion
// region 事件||监听
onMounted(() => {
getData();
setupEventListeners();
});
watch(time, () => {
getData();
});
const handleCommand = (year) => {
selectedYear.value = year; // 更新当前选中的年份
getVideoSuperviseTrendData(selectedYear.value);
};
const handleClick = (params) => {
const departId = params.data.departId;
const url = router.resolve({
path: "/datav/subOneVideoInsp",
query: { departId: departId },
}).href;
window.open(url, "_blank");
};
// 鼠标经过地图事件
const setupEventListeners = () => {
videoProRankAnimationStop();
videoMapAnimationStop();
videoTrendAnimationStop();
videoProblemTypeRateAnimationStop();
};
const videoProRankAnimationStop = () => {
const temp = videoProRank.value?.$el;
temp.addEventListener("mouseenter", () => {
clearInterval(videoProRankIntervalId);
});
temp.addEventListener("mouseleave", () => {
clearInterval(videoProRankIntervalId); // 确保没有多个定时器同时运行
videoProRankIntervalId = setInterval(videoProRankAnimation, 3000);
});
};
const videoMapAnimationStop = () => {
const temp = videoMap?.value?.chart;
// 鼠标移入
temp.on("mousemove", (e) => {
clearInterval(videoMapIntervalId);
temp.dispatchAction({ type: "downplay", seriesIndex: 0 });
temp.dispatchAction({
type: "highlight",
seriesIndex: 0,
dataIndex: e.dataIndex,
});
temp.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: e.dataIndex,
});
});
// 鼠标移出
temp.on("mouseout", () => {
clearInterval(videoMapIntervalId); // 确保没有多个定时器同时运行
videoMapIntervalId = setInterval(videoMapAnimation, 2000);
});
};
// videoProTrend
const videoTrendAnimationStop = () => {
const temp = videoProTrend?.value.getDom();
temp.addEventListener("mouseenter", () => {
clearInterval(videoTrendIntervalId);
});
temp.addEventListener("mouseleave", () => {
clearInterval(videoTrendIntervalId);
videoTrendIntervalId = setInterval(videoTrendAnimation, 2000);
});
};
const videoProblemTypeRateAnimationStop = () => {
const temp = videoProblemTypeRate?.value?.chart;
temp.on("mousemove", (e) => {
clearInterval(videoProblemTypeRateIntervalId);
temp.dispatchAction({ type: "downplay", seriesIndex: 0 });
temp.dispatchAction({
type: "highlight",
seriesIndex: 0,
dataIndex: e.dataIndex,
});
temp.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: e.dataIndex,
});
});
temp.on("mouseout", () => {
clearInterval(videoProblemTypeRateIntervalId);
videoProblemTypeRateIntervalId = setInterval(
videoProblemTypeRateAnimation,
2000
);
});
};
import { listVideoConfigByDepartId } from "@/api/system/videoConfig";
const videos = ref([]);
const activeUrl = ref("");
const videoStatus = ref({
all: 0,
percentage: "0%",
on: 0,
});
onMounted(() => {
getVideoStatus().then((data) => {
videoStatus.value = data;
});
listVideoConfigByDepartId("12630").then((data) => {
videos.value = data;
if (data.length > 0) {
activeUrl.value = data[0].videoUrl;
}
});
});
//视频循环
setInterval(()=>{
activeVideoIndex.value = (parseInt(activeVideoIndex.value) % videos.value.length + 1) || 0
activeUrl.value=videos?.value[parseInt(activeVideoIndex.value) % videos.value.length - 1].videoUrl;
},3000)
const activeVideoIndex = ref(0);
function handlePlay(item, index) {
activeVideoIndex.value = index;
activeUrl.value = item.videoUrl;
}
const videoInspections = ref([]);
async function getVideoInspection() {
const data = await listVideoInspection({
current: 1,
size: 5,
});
videoInspections.value = data.records;
}
onMounted(() => {
getVideoInspection();
});
function handleOpenData() {
window.open(router.resolve('/data/VideoInspection').href)
}
</script>
<style lang="scss" scoped>
@import "@/style/datav.scss";
iframe {
border: none;
width: 100%;
min-height: 90px;
}
img {
width: 100%;
display: block;
border: 1px solid transparent;
&[active="true"] {
border-color: #1cd9ff;
}
}
.message {
padding: 10px;
margin-bottom: 10px;
background: linear-gradient(
180deg,
rgba(208, 18, 18, 0) 0%,
rgba(255, 0, 0, 0.26) 100%
);
border: 1px solid;
border-image: linear-gradient(
180deg,
rgba(255, 71, 71, 0.59),
rgba(251, 95, 95, 1)
)
1 1;
.message-title {
color: #ff0017;
}
&[active="true"] {
background: linear-gradient(
180deg,
rgba(18, 104, 208, 0) 0%,
rgba(0, 166, 255, 0.26) 100%
);
border: 1px solid;
border-image: linear-gradient(
180deg,
rgba(71, 209, 255, 0.28),
rgba(8, 177, 255, 1)
)
1 1;
.message-title {
color: #1cd9ff;
}
}
.message-title {
font-size: 23px;
font-weight: 700;
margin-bottom: 10px;
}
.message-footer {
font-size: 14px;
color: #597ae9;
margin-top: 10px;
}
}
:deep() {
// 弹框整体
.tooltip {
position: relative;
width: 169.88px;
height: 178px;
background: linear-gradient(
180deg,
rgba(1, 4, 87, 0.8) 0%,
rgba(3, 21, 119, 0.8) 100%
);
border: 1px solid #4e8fff;
margin: -10px -10px -10px -10px;
}
//浏阳市局
.tooltip-title {
width: 169.88px;
height: 43px;
background: linear-gradient(
180deg,
rgba(1, 4, 87, 0.8) 0%,
rgba(3, 21, 119, 0.8) 100%
);
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
text-align: center; /* 水平居中 */
font-weight: 400;
font-size: 16px;
color: #ffffff;
line-height: 22px;
border-bottom: 1px solid #253755; /* 设置下边框 */
}
.tooltip-content {
width: 170px;
height: 132px;
//font-size: 11px;
margin-top: -12px;
background: linear-gradient(180deg, #010457 0%, #031577 100%);
}
.tooltip-content ul {
list-style-type: none; /* 移除默认的小圆点 */
padding: 0;
}
.tooltip-content ul li {
display: flex;
justify-content: space-around;
height: 26px;
color: #597AE9;
font-weight: 400;
font-size: 14px;
}
.tooltip-ul span {
display: inline-block;
float: right;
color: #fff;
font-size: 14px;
//text-align: center; /* 水平居中 */
}
/* 小尖角 */
.tooltip::before {
content: "";
position: absolute;
top: 90px; /* 调整尖角的垂直位置 */
left: -10px; /* 调整尖角的水平位置 */
width: 0;
height: 0;
//border-top: 10px solid red; /* 顶边颜色 */
//border-bottom: 10px solid green; /* 底边颜色 */
//border-right: 10px solid blue; /* 右边颜色,与背景颜色相同 */
}
}
.gobal-dropdown-container {
position: absolute;
right: 20px;
top: 15px;
}
.test {
width: 95px;
height: 25px;
background: #1c3472;
}
.gobal-dropdown-container {
position: absolute;
right: 20px;
top: 15px;
}
.my-gobal-yearselect {
font-size: 14px;
padding-top: 6px;
padding-left: 20px;
width: 90px;
color: #fff;
}
</style>