水温接口模拟,视频监控添加

This commit is contained in:
扈兆增 2026-05-18 09:03:20 +08:00
parent b11a2ddea6
commit c960b57e0b
12 changed files with 764 additions and 201 deletions

View File

@ -3,7 +3,7 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV='development'
VITE_APP_TITLE = '水电水利建设项目全过程环 境管理信息平台'
VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api'
# 本地环境
@ -12,14 +12,12 @@ VITE_APP_BASE_API = '/dev-api'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
# 汤伟
VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
# VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
VITE_APP_BASE_URL = 'http://192.168.1.162:8093'
# 测试环境线上
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'
# 线上服务名称
VITE_APP_LYGK_SERVER = '/api/dec-lygk-base-server'
@ -27,3 +25,4 @@ VITE_APP_LYGK_SERVER = '/api/dec-lygk-base-server'
VITE_APP_BASE_API_URL = 'http://10.84.121.21:8093'
## 开发环境预览 图片视频地址
VITE_APP_PREVIEW_URL = 'https://211.99.26.225:12125'
# ?menu=systemManageMenu&page=disposeManage

View File

@ -0,0 +1,4 @@
{
"permissions": {},
"outputStyle": "Vibe"
}

View File

@ -1,19 +0,0 @@
// src/api/config.ts
// 获取环境变量的辅助函数,兼容 Vite 和 Webpack
const getEnvVar = (key: string) => {
// Vite 使用 import.meta.env
if (import.meta.env) {
return import.meta.env[key];
}
};
export const SERVICE_URLS = {
// ABC 服务基础地址
lygk: getEnvVar('VITE_APP_LYGK_SERVER'),
// XYZ 服务基础地址
eng: getEnvVar('VITE_APP_eng_SERVER'),
// 默认服务(如果有)
DEFAULT: ''
};

View File

@ -0,0 +1,8 @@
import request from '@/utils/request';
export function getKendoListCust(data: any) {
return request({
url: '/sw/alongList/qgc/GetKendoListCust',
method: 'post',
data
});
}

View File

@ -1,5 +1,9 @@
<template>
<div class="baselayer-switcher" :style="{ right: drawerOpen ? '480px' : '12px' }">
<div
class="baselayer-switcher"
:style="{ right: drawerOpen ? '480px' : '12px' }"
v-if="uiStore.mapType == '2D'"
>
<div
class="switcher-item"
v-for="item in layers"
@ -11,7 +15,7 @@
<div class="label">{{ item.name }}</div>
</div>
<div class="nineSectionsImg">
<img :src="nineSectionsImg[activeKey]" alt="" />
<img :src="nineSectionsImg[activeKey]" alt="" />
</div>
</div>
</template>
@ -29,7 +33,7 @@ const props = defineProps({
type: Object,
default: () => {},
},
})
});
const uiStore = useUiStore();
const drawerOpen = ref(uiStore.drawerOpen);
@ -45,13 +49,17 @@ const layers = [
{ key: "BASEMAP-white", name: "地形", img: dixingImg },
{ key: "BASEMAP-img", name: "影像", img: yingxiangImg },
];
const nineSectionsImg:any = {'s_province_boundaries':nineSectionsShiliangImg,'BASEMAP-white':nineSectionsDixingImg,'BASEMAP-img':nineSectionsYingxiangImg,}
const nineSectionsImg: any = {
s_province_boundaries: nineSectionsShiliangImg,
"BASEMAP-white": nineSectionsDixingImg,
"BASEMAP-img": nineSectionsYingxiangImg,
};
const activeKey = ref(layers[0].key);
const handleSwitch = (key: string) => {
activeKey.value = key;
props.map.baseLayerSwitcher(key);
}
};
</script>
<style lang="scss" scoped>

View File

@ -12,6 +12,7 @@
mapList="{mapList}"
loading="{loading}"
pointData="{pointData}" -->
<!-- 地图图例 -->
<MapLegend :setLegendDataMap="updateLegendDataMap" />
<!-- 地图筛选器 -->
<MapFilter />

View File

@ -40,7 +40,6 @@ watch(
);
const isFullScreen = ref(false);
const mapType = ref("2D");
//
const controllers: any = computed(() => [
@ -78,7 +77,7 @@ const controllers: any = computed(() => [
{
name: "3D",
key: "dim",
icon: mapType.value === "2D" ? "a-3D" : "a-2D",
icon: uiStore.mapType === "2D" ? "a-3D" : "a-2D",
},
],
},
@ -91,7 +90,7 @@ const controllers: any = computed(() => [
},
],
},
mapType.value === "2D"
uiStore.mapType === "2D"
? {
children: [
{
@ -102,7 +101,7 @@ const controllers: any = computed(() => [
],
}
: {},
mapType.value === "2D"
uiStore.mapType === "2D"
? {
children: [
{
@ -113,7 +112,7 @@ const controllers: any = computed(() => [
],
}
: {},
mapType.value === "3D"
uiStore.mapType === "3D"
? {
children: [
{
@ -124,7 +123,7 @@ const controllers: any = computed(() => [
],
}
: {},
mapType.value === "3D"
uiStore.mapType === "3D"
? {
children: [
{
@ -175,13 +174,13 @@ const handleControllerClick = (item: any) => {
map.zoomToggle("out");
break;
case "dim":
mapType.value = mapType.value === "2D" ? "3D" : "2D";
if (mapType.value === "2D") {
uiStore.mapType = uiStore.mapType === "2D" ? "3D" : "2D";
if (uiStore.mapType === "2D") {
uiStore.drawerOpen = true;
} else {
uiStore.drawerOpen = false;
}
props.onClick("dim", mapType.value);
props.onClick("dim", uiStore.mapType);
break;
case "rightDrawer":
// 使

View File

@ -12,7 +12,6 @@ import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; // Ant Design 全局样式重置
import dayjs from 'dayjs'; // ant 中文语言
import 'dayjs/locale/zh-cn';
// @ts-ignore
import 'virtual:svg-icons-register';
// 3d地图

View File

@ -1,106 +1,138 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="水温监测工作开展情况">
<div class="facility-grid" >
<div v-for="facility in facilities" :key="facility.name" class="facility-card">
<div style="width: 60px;height: 62px;display: flex;align-items: center;justify-content: center;">
<div class="facility-icon">
<i style="color: #fff;" :class="facility.icon" type="icon-shengtailiuliang2"></i>
</div>
</div>
<SidePanelItem title="水温监测工作开展情况">
<div class="facility-grid">
<div v-for="facility in facilities" :key="facility.name" class="facility-card">
<div
style="
width: 60px;
height: 62px;
display: flex;
align-items: center;
justify-content: center;
"
>
<div class="facility-icon">
<i
style="color: #fff"
:class="facility.icon"
type="icon-shengtailiuliang2"
></i>
</div>
</div>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div style="font-size: 16px;"> <span class="facility-count">{{ facility.count }}</span><span></span></div>
</div>
</div>
</div>
</SidePanelItem>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div style="font-size: 16px">
<span class="facility-count">{{ facility.count }}</span
><span></span>
</div>
</div>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { ref, onMounted } from "vue";
import SidePanelItem from "@/components/SidePanelItem/index.vue";
import { getKendoListCust } from "@/api/sw";
// 便
defineOptions({
name: 'shuiwenjiancegongzuokaizhangqingkuang'
name: "shuiwenjiancegongzuokaizhangqingkuang",
});
//
const facilities = ref([
{
name: '表层水温',
count: 145,
icon: 'icon iconfont icon-shuizhijiancezhan'
},
{
name: '垂向水温',
count: 24,
icon: 'icon iconfont icon-diwenshuijianhuan'
},
{
name: "表层水温",
count: 145,
icon: "icon iconfont icon-shuizhijiancezhan",
},
{
name: "垂向水温",
count: 24,
icon: "icon iconfont icon-diwenshuijianhuan",
},
]);
const init = async () => {
const params = {
filter: {
logic: "and",
filters: [
{ field: "rvcd", operator: "eq", dataType: "string", value: "SJLY1U" },
{ field: "tm", operator: "gte", dataType: "date", value: "2026-04-17 00:00:00" },
{ field: "tm", operator: "lte", dataType: "date", value: "2026-05-17 23:00:00" },
],
},
sort: [{ field: "sort", dir: "asc" }],
};
await getKendoListCust(params);
};
//
onMounted(() => {
// init();
});
</script>
<style lang="scss" scoped>
.facility-grid {
width: 406px;
flex-flow: wrap;
display: flex;
justify-content: space-between;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
width: 406px;
flex-flow: wrap;
display: flex;
justify-content: space-between;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,
Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,
Noto Color Emoji;
}
.facility-card {
width: 200px;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
transition: all 0.3s;
cursor: pointer;
box-sizing: border-box;
width: 200px;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
transition: all 0.3s;
cursor: pointer;
box-sizing: border-box;
}
.facility-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
// margin-right: 8px;
background: #2f6b98;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
// margin-right: 8px;
background: #2f6b98;
border-radius: 50%;
.anticon {
font-size: 24px;
color: #fff;
}
.anticon {
font-size: 24px;
color: #fff;
}
}
.facility-info {
flex: 1;
flex: 1;
}
.facility-name {
font-size: 16px;
color: #333;
// margin-bottom: 4px;
// font-weight: 500;
font-size: 16px;
color: #333;
// margin-bottom: 4px;
// font-weight: 500;
}
.facility-count {
font-size: 18px;
color: #2f6b98;
// font-weight: 600;
font-size: 18px;
color: #2f6b98;
// font-weight: 600;
}
</style>

View File

@ -1,69 +1,59 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="沿程水温变化" :prompt="prompts" :select="select" :datetimePicker="datetimePicker">
<SidePanelItem
title="沿程水温变化"
:prompt="prompts"
:select="select"
:datetimePicker="datetimePicker"
>
<div ref="chartRef" class="chart-container"></div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts';
import type { ECharts } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { ref, onMounted, onBeforeUnmount } from "vue";
import * as echarts from "echarts";
import type { ECharts } from "echarts";
import SidePanelItem from "@/components/SidePanelItem/index.vue";
import { getKendoListCust } from "@/api/sw";
// 便
defineOptions({
name: 'yanchengshuiwenChangeMod'
name: "yanchengshuiwenChangeMod",
});
//
const select = ref({
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined,
});
//
const datetimePicker = ref({
show: true,
value: '2026-05-15 15',
format: 'YYYY-MM-DD HH',
picker: 'date' as const,
options: []
show: true,
value: "2026-05-15 15",
format: "YYYY-MM-DD HH",
picker: "date" as const,
options: [],
});
const prompts = ref({
show: true,
value: '注:最新数据时间为2026-04-08 23',
value: "注:最新数据时间为2026-04-08 23",
});
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: ECharts | null = null;
// -
const stationNames = ref([
'班多',
'龙羊峡',
'拉西瓦',
'李家峡',
'公伯峡',
'苏只',
'积石峡'
]);
const stationNames = ref([]);
// - (°C) null
const waterTemperatures = ref([
null,
5.7,
5.7,
6.5,
7.2,
7.8,
8.4
]);
const waterTemperatures = ref([]);
//
const currentTime = '2026-04-08 23';
const currentTime = "2026-04-08 23";
//
const initChart = () => {
@ -73,17 +63,17 @@ const initChart = () => {
const option = {
title: {
text: '水温(°C)',
text: "水温(°C)",
left: 5,
top: 0,
textStyle: {
color: '#000000',
color: "#000000",
fontSize: 12,
fontWeight: 'normal',
}
fontWeight: "normal",
},
},
tooltip: {
trigger: 'axis',
trigger: "axis",
formatter: (params: any) => {
if (params && params.length > 0) {
const data = params[0];
@ -92,31 +82,31 @@ const initChart = () => {
return `${currentTime}<br/>${data.name}${data.value}°C`;
}
}
return '';
}
return "";
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true
left: "3%",
right: "4%",
bottom: "3%",
top: "15%",
containLabel: true,
},
xAxis: {
type: 'category',
type: "category",
data: stationNames.value,
boundaryGap: true,
axisLine: {
show: true,
lineStyle: {
color: '#8f8f8f'
}
color: "#8f8f8f",
},
},
axisTick: {
show: false
show: false,
},
axisLabel: {
color: '#333',
color: "#333",
fontSize: 12,
margin: 8,
interval: 0,
@ -125,79 +115,79 @@ const initChart = () => {
// 1, 3, 5
//
if (index % 2 === 0) {
return `${value}\n `; // + +
return `${value}\n `; // + +
} else {
return ` \n${value}`; // + +
return ` \n${value}`; // + +
}
}
},
},
splitLine: {
show: true,
lineStyle: {
color: '#e0e0e0',
type: 'solid'
}
}
color: "#e0e0e0",
type: "solid",
},
},
},
yAxis: {
type: 'value',
type: "value",
min: 0,
max: 10,
// max: 10,
interval: 2,
axisLine: {
show: true,
lineStyle: {
color: '#8f8f8f'
}
color: "#8f8f8f",
},
},
axisTick: {
show: true,
length: 3,
lineStyle: {
color: '#8f8f8f'
}
color: "#8f8f8f",
},
},
axisLabel: {
color: '#333',
fontSize: 12
color: "#333",
fontSize: 12,
},
splitLine: {
show: true,
lineStyle: {
color: '#e0e0e0',
type: 'solid'
}
}
color: "#e0e0e0",
type: "solid",
},
},
},
series: [
{
name: '水温',
type: 'line',
name: "水温",
type: "line",
smooth: true,
symbol: 'circle',
symbol: "circle",
symbolSize: 6,
lineStyle: {
color: '#6ca4f7',
width: 2
color: "#6ca4f7",
width: 2,
},
itemStyle: {
color: '#6ca4f7'
color: "#6ca4f7",
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(108, 164, 247, 0.3)'
color: "rgba(108, 164, 247, 0.3)",
},
{
offset: 1,
color: 'rgba(108, 164, 247, 0.05)'
}
])
color: "rgba(108, 164, 247, 0.05)",
},
]),
},
data: waterTemperatures.value
}
]
data: waterTemperatures.value,
},
],
};
chartInstance.setOption(option);
@ -208,23 +198,65 @@ const handleResize = () => {
chartInstance?.resize();
};
//
onMounted(() => {
// DOM
const init = async () => {
const params = {
filter: {
logic: "and",
filters: [
{ field: "rvcd", operator: "eq", dataType: "string", value: "SJLY1U" },
{ field: "tm", operator: "gte", dataType: "date", value: "2026-04-17 00:00:00" },
{ field: "tm", operator: "lte", dataType: "date", value: "2026-05-17 23:00:00" },
],
},
sort: [{ field: "sort", dir: "asc" }],
};
let res = await getKendoListCust(params);
stationNames.value = res.data.data
.filter((item: any) => item.sttp == "1")
.map((item: any) => item.stnm);
console.log(stationNames.value);
waterTemperatures.value = res.data.data
.filter((item: any) => item.sttp == "2")
.map((item: any) => item.temperature);
// waterTemperatures.value = res.data.data.map((item: any) => item.temperature);
console.log(waterTemperatures.value);
//
setTimeout(() => {
initChart();
if (!chartInstance) {
initChart();
} else {
//
chartInstance.setOption({
xAxis: {
data: stationNames.value,
},
series: [
{
data: waterTemperatures.value,
},
],
});
}
//
setTimeout(() => {
chartInstance?.resize();
}, 100);
}, 50);
};
window.addEventListener('resize', handleResize);
//
onMounted(() => {
// DOM
init();
window.addEventListener("resize", handleResize);
});
//
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
window.removeEventListener("resize", handleResize);
chartInstance?.dispose();
});
</script>

View File

@ -4,6 +4,7 @@ import { ref } from 'vue';
export const useUiStore = defineStore('ui', () => {
// 右侧抽屉状态
const drawerOpen = ref(true);
const mapType = ref('2D');
// 切换抽屉状态
const toggleDrawer = () => {
@ -19,5 +20,6 @@ export const useUiStore = defineStore('ui', () => {
drawerOpen,
toggleDrawer,
setDrawerOpen,
mapType
};
});

View File

@ -1,5 +1,503 @@
<template>
<div>
<h2>视频监控</h2>
<div class="shiPinJianKongZhuanTi-page">
<a-tabs
v-model:activeKey="activeTab"
type="card"
:tabBarGutter="8"
class="custom-tabs"
>
<template #tabBarExtraContent>
<a-button type="primary" size="small" @click="handleAction">
<template #icon><PlusOutlined /></template>
添加监控点
</a-button>
</template>
<a-tab-pane v-for="tab in tabs" :key="tab.key" :tab="tab.title" />
</a-tabs>
<div class="tabs-content">
<!-- 顶部标题和图标选择区域 -->
<div class="content-header">
<div class="header-left">
<span class="title">监控列表</span>
</div>
<div class="header-right">
<a-radio-group v-model:value="viewLayout" button-style="solid">
<a-radio-button value="1">
<AppstoreOutlined />
</a-radio-button>
<a-radio-button value="4">
<LayoutOutlined />
</a-radio-button>
<a-radio-button value="6">
<BorderOutlined />
</a-radio-button>
<a-radio-button value="9">
<GridOutlined />
</a-radio-button>
</a-radio-group>
</div>
</div>
<!-- 主体内容区域 -->
<div class="content-body">
<!-- 左侧监控列表 -->
<div class="left-panel">
<!-- Tab切换实时视频/录像 -->
<a-tabs v-model:activeKey="monitorType" size="small" class="monitor-tabs">
<a-tab-pane key="live" tab="实时视频" />
<a-tab-pane key="record" tab="录像" />
</a-tabs>
<!-- 查询和状态筛选 -->
<div class="search-bar">
<a-input-search
v-model:value="searchText"
placeholder="搜索监控点"
size="small"
style="flex: 1"
@search="handleSearch"
/>
<a-button-group size="small" class="status-filter">
<a-button
:type="statusFilter === 'online' ? 'primary' : 'default'"
@click="statusFilter = 'online'"
>
在线
</a-button>
<a-button
:type="statusFilter === 'offline' ? 'primary' : 'default'"
@click="statusFilter = 'offline'"
>
离线
</a-button>
</a-button-group>
</div>
<!-- 树形结构 -->
<div class="tree-container">
<a-tree
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
:tree-data="treeData"
:field-names="{ title: 'title', key: 'key', children: 'children' }"
block-node
show-icon
>
<template #switcherIcon="{ switcherCls }">
<DownOutlined :class="switcherCls" />
</template>
<template #title="{ title }">
<span>{{ title }}</span>
</template>
</a-tree>
</div>
</div>
<!-- 中间视频内容区域 -->
<div class="center-panel">
<div class="video-grid" :class="`layout-${viewLayout}`">
<div v-for="index in parseInt(viewLayout)" :key="index" class="video-item">
<div class="video-placeholder">
<VideoCameraOutlined class="video-icon" />
<span class="video-label">摄像头 {{ index }}</span>
</div>
</div>
</div>
</div>
<!-- 右侧回放面板仅录像模式显示 -->
<div v-if="monitorType === 'record'" class="right-panel">
<div class="replay-header">
<span class="replay-title">回放</span>
</div>
<!-- 时间选择 -->
<div class="date-picker">
<a-range-picker
v-model:value="dateRange"
format="YYYY-MM-DD"
style="width: 100%"
size="small"
/>
</div>
<!-- 录像列表 -->
<div class="record-list">
<a-list :data-source="recordList" :loading="recordLoading" size="small">
<template #renderItem="{ item }">
<a-list-item class="record-item">
<div class="record-info">
<div class="record-name">{{ item.name }}</div>
<div class="record-time">{{ item.time }}</div>
</div>
<a-button size="small" type="link">
<PlayCircleOutlined />
</a-button>
</a-list-item>
</template>
</a-list>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
// import {
// PlusOutlined,
// AppstoreOutlined,
// LayoutOutlined,
// BorderOutlined,
// GridOutlined,
// DownOutlined,
// VideoCameraOutlined,
// PlayCircleOutlined
// } from '@ant-design/icons-vue'
import type { Dayjs } from "dayjs";
import dayjs from "dayjs";
// Tab
const tabs = ref([
{ key: "1", title: "实时监控" },
{ key: "2", title: "录像回放" },
{ key: "3", title: "告警记录" },
]);
const activeTab = ref("1");
// 1/4/6/9
const viewLayout = ref("1");
// /
const monitorType = ref("live");
//
const searchText = ref("");
// 线/线
const statusFilter = ref<"online" | "offline">("online");
//
const expandedKeys = ref<string[]>(["0-0", "0-1"]);
const selectedKeys = ref<string[]>([]);
const treeData = [
{
title: "区域一",
key: "0-0",
children: [
{ title: "摄像头01", key: "0-0-0", isLeaf: true },
{ title: "摄像头02", key: "0-0-1", isLeaf: true },
{ title: "摄像头03", key: "0-0-2", isLeaf: true },
],
},
{
title: "区域二",
key: "0-1",
children: [
{ title: "摄像头04", key: "0-1-0", isLeaf: true },
{ title: "摄像头05", key: "0-1-1", isLeaf: true },
],
},
{
title: "区域三",
key: "0-2",
children: [
{ title: "摄像头06", key: "0-2-0", isLeaf: true },
{ title: "摄像头07", key: "0-2-1", isLeaf: true },
{ title: "摄像头08", key: "0-2-2", isLeaf: true },
{ title: "摄像头09", key: "0-2-3", isLeaf: true },
],
},
];
// 7
const dateRange = ref<[Dayjs, Dayjs]>([dayjs().subtract(7, "day"), dayjs()]);
//
const recordLoading = ref(false);
const recordList = ref([
{ name: "录像文件01", time: "2026-05-17 10:00:00 - 10:30:00" },
{ name: "录像文件02", time: "2026-05-17 09:00:00 - 09:30:00" },
{ name: "录像文件03", time: "2026-05-16 14:00:00 - 14:30:00" },
{ name: "录像文件04", time: "2026-05-16 10:00:00 - 10:30:00" },
{ name: "录像文件05", time: "2026-05-15 16:00:00 - 16:30:00" },
{ name: "录像文件06", time: "2026-05-15 10:00:00 - 10:30:00" },
{ name: "录像文件07", time: "2026-05-14 11:00:00 - 11:30:00" },
{ name: "录像文件08", time: "2026-05-13 15:00:00 - 15:30:00" },
{ name: "录像文件09", time: "2026-05-12 09:00:00 - 09:30:00" },
{ name: "录像文件10", time: "2026-05-11 14:00:00 - 14:30:00" },
]);
//
const handleAction = () => {
console.log("添加监控点");
};
//
const handleSearch = (value: string) => {
console.log("搜索:", value);
};
</script>
<style scoped lang="scss">
.shiPinJianKongZhuanTi-page {
position: relative;
z-index: 900;
pointer-events: all;
width: 100%;
height: 100%;
background-color: #ffffff;
padding: 20px;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
}
.custom-tabs {
margin-bottom: 16px;
:deep(.ant-tabs-nav) {
margin-bottom: 0;
&::before {
border-bottom: none;
}
}
:deep(.ant-tabs-tab) {
border-radius: 4px 4px 0 0;
transition: all 0.2s ease;
&:hover {
color: #1890ff;
}
}
:deep(.ant-tabs-tab-active) {
background-color: #fff;
border-color: #d9d9d9;
border-bottom-color: #fff;
}
}
.tabs-content {
flex: 1;
background-color: #fafafa;
border-radius: 4px;
border: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
overflow: hidden;
}
//
.content-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #e8e8e8;
.header-left {
.title {
font-size: 16px;
font-weight: 500;
color: #262626;
}
}
.header-right {
:deep(.ant-radio-group) {
.ant-radio-button-wrapper {
padding: 4px 12px;
.anticon {
font-size: 16px;
}
}
}
}
}
//
.content-body {
flex: 1;
display: flex;
overflow: hidden;
}
//
.left-panel {
width: 280px;
background-color: #fff;
border-right: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
flex-shrink: 0;
.monitor-tabs {
padding: 8px 8px 0;
:deep(.ant-tabs-nav) {
margin-bottom: 0;
}
}
.search-bar {
display: flex;
gap: 8px;
padding: 12px;
border-bottom: 1px solid #f0f0f0;
.status-filter {
flex-shrink: 0;
}
}
.tree-container {
flex: 1;
overflow-y: auto;
padding: 8px;
:deep(.ant-tree) {
.ant-tree-node-content-wrapper {
&:hover {
background-color: #f5f5f5;
}
}
.ant-tree-node-selected {
.ant-tree-node-content-wrapper {
background-color: #e6f7ff;
}
}
}
}
}
//
.center-panel {
flex: 1;
padding: 16px;
overflow: auto;
background-color: #f5f5f5;
.video-grid {
display: grid;
gap: 12px;
height: 100%;
&.layout-1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
&.layout-4 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
&.layout-6 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
}
&.layout-9 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
}
}
.video-item {
background-color: #000;
border-radius: 4px;
overflow: hidden;
min-height: 150px;
.video-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
.video-icon {
font-size: 48px;
margin-bottom: 8px;
opacity: 0.5;
}
.video-label {
font-size: 14px;
color: #999;
}
}
}
}
//
.right-panel {
width: 280px;
background-color: #fff;
border-left: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
flex-shrink: 0;
.replay-header {
padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
.replay-title {
font-size: 14px;
font-weight: 500;
color: #262626;
}
}
.date-picker {
padding: 12px;
border-bottom: 1px solid #f0f0f0;
}
.record-list {
flex: 1;
overflow-y: auto;
padding: 8px;
.record-item {
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
.record-info {
flex: 1;
min-width: 0;
.record-name {
font-size: 13px;
color: #262626;
margin-bottom: 4px;
}
.record-time {
font-size: 11px;
color: #8c8c8c;
}
}
}
}
}
</style>