Merge branch 'main' into dev-lilin

This commit is contained in:
lilin 2026-07-06 17:09:42 +08:00
commit e5dcdaf299
20 changed files with 5632 additions and 1817 deletions

View File

@ -7,7 +7,7 @@ VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api'
# 本地环境
# VITE_APP_BASE_URL = 'http://10.84.121.122:8093'
# VITE_APP_BASE_URL = 'http://172.16.54.1:8093'
# 测试环境
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'

View File

@ -1,9 +1,8 @@
import request from '@/utils/request';
// import { SERVICE_URLS } from '../config'; // 引入配置
// 基本情况介绍
export function getBaseWbsb(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/wbsb/GetKendoList',
url: '/eq/wbsb/GetKendoList',
method: 'post',
data: data
});
@ -12,7 +11,16 @@ export function getBaseWbsb(data: any) {
// 水电开发及环境监测 - 获取列表数据
export function getVmsstbprptKendoList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/vmsstbprpt/GetKendoList',
url: '/eq/vmsstbprpt/GetKendoList',
method: 'post',
data: data
});
}
// 水电开发及环境监测 - 获取列表数据(全基地模式)
export function getVmsstbprptKendoListCust(data: any) {
return request({
url: '/eq/vmsstbprpt/GetKendoList',
method: 'post',
data: data
});
@ -21,7 +29,7 @@ export function getVmsstbprptKendoList(data: any) {
// 环保设施建设情况
export function environmentalProtectionConstruction(data: any) {
return request({
url: '/api/wmp-env-server/env/operat/GetKendoListCust',
url: '/eng/base/operat/GetKendoListCust',
method: 'post',
data: data
});
@ -30,7 +38,7 @@ export function environmentalProtectionConstruction(data: any) {
// 环保设施弹框表格
export function envOpdGetKendoListCust(data: any) {
return request({
url: '/api/wmp-env-server/env/oped/GetKendoListCust',
url: '/overview/oped/GetKendoListCust',
method: 'post',
data: data
});
@ -39,7 +47,7 @@ export function envOpdGetKendoListCust(data: any) {
// 获取基地列表
export function getBaseList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/wbsb/GetKendoList',
url: '/eq/wbsb/GetKendoList',
method: 'post',
data: data
});
@ -48,7 +56,7 @@ export function getBaseList(data: any) {
// 获取设施介绍列表
export function getMsstbprptKendoList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/msstbprpt/GetKendoList',
url: '/overview/msstbprpt/GetKendoList',
method: 'post',
data: data
});
@ -57,7 +65,7 @@ export function getMsstbprptKendoList(data: any) {
// 环保自动监测工作开展情况
export function baseEvnmAutoMonitorGetKendoListCust(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/evnmAutoMonitor/GetKendoListCust',
url: '/wte/base/evnmAutoMonitor/GetKendoListCust',
method: 'post',
data: data
});
@ -66,7 +74,7 @@ export function baseEvnmAutoMonitorGetKendoListCust(data: any) {
// 环保自动监测弹窗列表
export function vmsstbprptGetKendoList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/vmsstbprpt/GetKendoList',
url: '/eq/vmsstbprpt/GetKendoList',
method: 'post',
data: data
});
@ -75,7 +83,7 @@ export function vmsstbprptGetKendoList(data: any) {
// 获取基本信息
export function baseMsstbprptGetKendoList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/msstbprpt/GetKendoList',
url: '/overview/msstbprpt/GetKendoList',
method: 'post',
data: data
});

View File

@ -28,7 +28,7 @@ export function postIdUrl(data: any) {
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
});
}

View File

@ -3,16 +3,16 @@ import request from '@/utils/request';
// AI识别大坝环境-获取统计数据Tab数量
export function getAisbdbyxList(data: any) {
return request({
url: '/api/wmp-env-server/env/ai/com/qgcDmRun/GetKendoListCust',
url: '/warn/ai/com/qgcDmRun/GetKendoListCust',
method: 'post',
data
});
}
// AI识别大坝环境-获取饼图+表格数据
// AI识别大坝环境-获取饼图+表格数据 /api/wmp-env-server/env/ai/com/GetKendoListCust
export function getAiChartTable(data: any) {
return request({
url: '/api/wmp-env-server/env/ai/com/GetKendoListCust',
url: '/warn/ai/com/GetKendoListCust',
method: 'post',
data
});
@ -21,7 +21,7 @@ export function getAiChartTable(data: any) {
// AI识别大坝环境-获取电站列表
export function getAiRstcd(data: any) {
return request({
url: '/api/wmp-env-server/env/ai/com/qgc/aiRstcd/GetKendoListCust',
url: '/warn/ai/com/qgc/aiRstcd/GetKendoListCust',
method: 'post',
data
});
@ -30,7 +30,7 @@ export function getAiRstcd(data: any) {
// AI识别过鱼设施运行-获取统计数据
export function getAiGyssList(data: any) {
return request({
url: '/api/wmp-env-server/env/ai/com/qgcProtect/GetKendoListCust',
url: '/warn/ai/com/qgcProtect/GetKendoListCust',
method: 'post',
data
});
@ -39,7 +39,7 @@ export function getAiGyssList(data: any) {
// 生态流量低于限值告警-获取统计数据
export function getStllGaojingList(data: any) {
return request({
url: '/api/wmp-env-server/env/warn/stcd/eng/GetKendoListCust',
url: '/warn/stcd/eng/GetKendoListCust',
method: 'post',
data
});
@ -48,7 +48,7 @@ export function getStllGaojingList(data: any) {
// 生态流量低于限值告警-获取表格详情数据
export function getStllGaojingDetail(data: any) {
return request({
url: '/api/wmp-env-server/env/warn/stcd/GetKendoListCust',
url: '/warn/stcd/GetKendoListCust',
method: 'post',
data
});
@ -57,7 +57,7 @@ export function getStllGaojingDetail(data: any) {
// 水质监测数据告警-获取统计数据
export function getShuizhiGaojingList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/msstbprpt/GetKendoList',
url: '/wq/msstbprpt/GetKendoList',
method: 'post',
data
});
@ -66,7 +66,7 @@ export function getShuizhiGaojingList(data: any) {
// AI识别大坝环境-获取漂浮物/清漂日历数据
export function getAiPfwRecord(data: any) {
return request({
url: '/api/wmp-env-server/env/ai/com/qgc/aiPfwRecord/GetKendoListCust',
url: '/warn/ai/com/qgc/aiPfwRecord/GetKendoListCust',
method: 'post',
data
});
@ -75,16 +75,16 @@ export function getAiPfwRecord(data: any) {
// AI识别大坝环境-获取叠梁门日历数据
export function getAiRecord(data: any) {
return request({
url: '/api/wmp-env-server/env/ai/com/qgc/aiRecord/GetKendoListCust',
url: '/warn/ai/com/qgc/aiRecord/GetKendoListCust',
method: 'post',
data
});
}
// AI识别大坝环境-获取日历详情视频数据
// AI识别大坝环境-获取日历详情视频数据 /warn/ai/com/qgc/aiFile/GetKendoListCust
export function getAiFile(data: any) {
return request({
url: '/api/wmp-env-server/env/ai/com/qgc/aiFile/GetKendoListCust',
url: '/warn/ai/com/qgc/aiFile/GetKendoListCust',
method: 'post',
data
});

View File

@ -2,6 +2,23 @@
<template>
<div class="basic_body">
<div ref="chartContainer" class="chart-container"></div>
<!-- 水电开发情况弹窗 -->
<a-modal
v-model:open="modalVisible"
title="水电开发情况"
width="1536px"
:footer="null"
:destroy-on-close="true"
>
<ShuiDianKaiFQKTwoLayer
v-if="modalVisible"
:defaultJidiInfo="modalProps.defaultJidiInfo"
:seriesName="modalProps.seriesName"
:stateName="modalProps.stateName"
:defaultTab="modalProps.defaultTab"
/>
</a-modal>
</div>
</template>
@ -10,21 +27,31 @@ import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { getVmsstbprptKendoList } from '@/api/home';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import ShuiDianKaiFQKTwoLayer from '@/modules/shuidianhuangjingjieruMod/TwoLayer/ShuiDianKaiFQKTwoLayer.vue';
defineOptions({
name: 'developStatusChart'
});
const emit = defineEmits<{
(e: 'chart-click', params: any): void;
}>();
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
const chartContainer = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const dataLoading = ref(false);
const modalVisible = ref(false);
const modalProps = ref<any>({});
function openModal(props: any) {
modalProps.value = props;
modalVisible.value = true;
}
defineExpose({
openModal,
dataLoading
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
@ -75,7 +102,6 @@ const getData = async () => {
const res1Data = res1?.data?.data || [];
const res2Data = res2?.data?.data || [];
// debugger
if (res1Data.length > 0 || res2Data.length > 0) {
const ttpwrlist = res1Data.map((item: any) => ({
@ -234,12 +260,16 @@ const initChart = () => {
chartInstance.setOption(option);
//
chartInstance.on('click', (params: any) => {
emit('chart-click', {
const currentJidiInfo = JidiSelectEventStore.selectedItem;
openModal({
defaultJidiInfo: {
baseid: currentJidiInfo?.wbsCode || 'all',
basename: currentJidiInfo?.wbsName || '全部基地'
},
seriesName: params.seriesName,
name: params.name,
dataIndex: params.dataIndex
stateName: params.name,
defaultTab: params.name
});
});

View File

@ -38,6 +38,20 @@
</div>
</div>
</div>
<a-modal
v-model:open="modalVisible"
title="环境监测数据接入情况"
width="1536px"
:footer="null"
:destroy-on-close="true"
>
<HuanJingSJJRQK
v-if="modalVisible"
:dataDimensionVal="modalProps.dataDimensionVal"
:name="modalProps.name"
/>
</a-modal>
</div>
</template>
@ -45,20 +59,30 @@
import { ref, watch } from 'vue';
import { getVmsstbprptKendoList } from '@/api/home';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import HuanJingSJJRQK from '@/modules/shuidianhuangjingjieruMod/TwoLayer/HuanJingSJJRQK.vue';
defineOptions({
name: 'EngEnvironmentData'
});
const emit = defineEmits<{
(e: 'item-click', params: any): void;
}>();
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
const dataLoading = ref(false);
const data = ref<Record<string, number>>({ one: 0, two: 0, three: 0 });
const modalVisible = ref(false);
const modalProps = ref<any>({});
function openModal(props: any) {
modalProps.value = props;
modalVisible.value = true;
}
defineExpose({
openModal,
dataLoading
});
const getData = async () => {
if (!baseid.value) return;
@ -77,7 +101,8 @@ const getData = async () => {
...baseFilters,
{ field: 'prsc', operator: 'in', dataType: 'string', value: ['1', '2', '3'] }
]
}
},
group: [{ dir: 'desc', field: 'sttpCode' }]
};
const params2 = {
@ -88,7 +113,8 @@ const getData = async () => {
{ field: 'runState', operator: 'eq', dataType: 'number', value: 4 },
{ field: 'dtin', operator: 'eq', dataType: 'number', value: 1 }
]
}
},
group: [{ dir: 'desc', field: 'sttpCode' }]
};
const params3 = {
@ -99,7 +125,8 @@ const getData = async () => {
{ field: 'prsc', operator: 'in', dataType: 'string', value: ['1', '2', '3'] },
{ field: 'dtinEnv', operator: 'in', dataType: 'string', value: ['2'] }
]
}
},
group: [{ dir: 'desc', field: 'dtinEnv' }]
};
try {
@ -113,21 +140,22 @@ const getData = async () => {
const record = { one: 0, two: 0, three: 0 };
const responses = [res1, res2, res3];
responses.forEach((res, index) => {
const total = res?.data?.total ?? 0;
switch (index) {
case 0:
record.one = total;
break;
case 1:
record.two = total;
break;
case 2:
record.three = total;
break;
if (res?.data?.data && Array.isArray(res.data.data) && res.data.data.length > 0) {
const count = res.data.data[0].count ?? 0;
switch (index) {
case 0:
record.one = count;
break;
case 1:
record.two = count;
break;
case 2:
record.three = count;
break;
}
}
});
data.value = record;
// debugger
} catch (error) {
console.error('EngEnvironmentData getData error:', error);
} finally {
@ -136,7 +164,11 @@ const getData = async () => {
};
const handleItemClick = (name: string) => {
emit('item-click', { name });
const currentJidiInfo = JidiSelectEventStore.selectedItem;
openModal({
dataDimensionVal: currentJidiInfo?.wbsCode || 'all',
name
});
};
watch(

View File

@ -21,6 +21,14 @@
:options="jiDiList"
@change="handleBaseChange"
/>
<a-select
v-model:value="searchData.stcd"
placeholder=" "
style="width: 200px"
show-search
:filter-option="filterOption"
:options="rstcdOptions"
/>
<a-input
v-if="tabIndex !== '7'"
v-model:value="searchData.stnm"
@ -44,7 +52,6 @@
:search-params="searchParams"
:transform-data="customTransform"
>
<template #coenvwState="{ column, record }">
<a-tag :color="record.coenvwState > 0 ? 'green' : '#999'">
{{ record.coenvwState > 0 ? '在线' : '离线' }}
@ -85,13 +92,15 @@ const props = defineProps<{
const tabIndex = ref(String(props.activeKey ?? ''));
const searchData = ref<any>({
baseId: props.baseId || 'all',
stnm: null
stnm: null,
stcd: null
});
const modelStore = useModelStore();
const tableRef = ref();
// ==================== ====================
const jiDiList: any = ref([]);
const rstcdOptions: any = ref([]);
// ==================== ====================
const columns = computed(() => {
@ -103,15 +112,13 @@ const columns = computed(() => {
title: '所属基地',
dataIndex: 'baseName',
width: '150px',
merge: true
merge: true
},
{
key: 'stnm',
title: '电站名称',
dataIndex: 'stnm',
width: '176px',
width: '176px'
},
{
key: 'dvtp',
@ -167,8 +174,7 @@ const columns = computed(() => {
key: 'stnm',
title: '测站名称',
dataIndex: 'stnm',
width: '176px',
width: '176px'
},
{
key: 'ennm',
@ -189,8 +195,7 @@ const columns = computed(() => {
key: 'stindx',
title: '监测指标',
dataIndex: 'stindx',
width: '150px',
width: '150px'
}
];
@ -260,15 +265,66 @@ const searchParams = computed(() => {
// ==================== ====================
const handleTabChange = (key: string) => {
tabIndex.value = key;
handleReset();
handleSearch();
};
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const getRstcdData = async () => {
const parmas = {
filter: {
logic: 'and',
filters: [
searchData.value.baseId != 'all'
? {
field: 'baseId',
operator: 'contains',
dataType: 'string',
value: '02'
}
: null,
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'ENG'
}
].filter(Boolean)
},
sort: [
{
field: 'baseId',
dir: 'asc'
},
{
field: 'rvcdStepSort',
dir: 'asc'
},
{
field: 'rstcdStepSort',
dir: 'asc'
},
{
field: 'siteStepSort',
dir: 'asc'
}
],
select: ['stcd', 'stnm']
};
const res = await vmsstbprptGetKendoList(parmas);
rstcdOptions.value = res.data.data.map((item) => {
return {
value: item.stcd,
label: item.stnm
};
});
};
const handleBaseChange = () => {
handleSearch();
searchData.value.stcd = null
getRstcdData();
// handleSearch();
};
const dvtpText = (dvtp: number) => {
@ -287,7 +343,13 @@ const handleSearch = () => {
value: searchData.value.baseId
});
}
if(searchData.value.stcd){
filters.push({
field: 'stcd',
operator: 'eq',
value: searchData.value.stcd
});
}
if (searchData.value.stnm) {
filters.push({
field: 'stnm',
@ -441,8 +503,10 @@ const handleSearch = () => {
const handleReset = () => {
searchData.value = {
baseId: 'all',
stnm: null
stnm: null,
stcd: null
};
getRstcdData()
handleSearch();
};
@ -542,7 +606,7 @@ const handleViewDetail = (record: any, type: string) => {
sttpCode === 'WTVT' || sttpCode === 'WTVT,' ? '1' : '0';
} else {
modelStore.modalVisible = true;
modelStore.params.sttp = sttpCode;
modelStore.params.sttp = sttpCode;
modelStore.params.stcd = record.stcd;
modelStore.title = record.stnm;
}
@ -557,6 +621,7 @@ onMounted(() => {
value: element.wbsCode
});
});
getRstcdData()
handleSearch();
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,317 @@
<template>
<div class="video-details">
<a-tabs v-model:activeKey="activeKey" @change="handleTabChange">
<a-tab-pane key="2" tab="运行">
<a-spin :spinning="loading">
<div class="video-container">
<VideoPlayer
:list="videoList"
:activeFid="activeFid"
:activeMedia="activeMedia"
:loading="loading"
:page="page"
:pageSize="pageSize"
:pageSizeOptions="pageSizeOptions"
:total="total"
@select="handleSelectMedia"
@pageChange="handlePageChange"
/>
</div>
</a-spin>
</a-tab-pane>
<a-tab-pane key="1" tab="未运行">
<a-spin :spinning="loading">
<div class="video-container">
<VideoPlayer
:list="videoList"
:activeFid="activeFid"
:activeMedia="activeMedia"
:loading="loading"
:page="page"
:pageSize="pageSize"
:pageSizeOptions="pageSizeOptions"
:total="total"
@select="handleSelectMedia"
@pageChange="handlePageChange"
/>
</div>
</a-spin>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import VideoPlayer from '@/modules/yunXingGaoJIng/Aisbdbyx/components/VideoPlayer.vue';
import { getAiFile } from '@/api/zngj';
import { postIdUrl } from '@/api/mapModal';
import dayjs from 'dayjs';
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
// ==================== Props ====================
const props = defineProps<{
baseid?: string;
month?: any;
type?: string;
vdDate?: string;
activeKey?: string;
stnm?: string;
rstcd?: string;
pieCode?: string[];
}>();
// ==================== ====================
const activeKey = ref(props.activeKey || '2');
const loading = ref(false);
const videoList = ref<any[]>([]);
const activeFid = ref('');
const activeMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
const page = ref(1);
const pageSize = ref(20);
const pageSizeOptions = ['20', '50', '100'];
const total = ref(0);
// ==================== ====================
const fetchVideoList = async () => {
loading.value = true;
try {
const filters: any[] = [
// baseId
props.pieCode && props.pieCode.length > 0 && props.baseid === 'all'
? {
field: 'baseId',
operator: 'in',
dataType: 'string',
value: props.pieCode
}
: props.baseid && props.baseid !== 'all'
? { field: 'baseId', operator: 'eq', value: props.baseid }
: null,
// rstcd
props.pieCode && props.pieCode.length > 0 && props.baseid !== 'all'
? {
field: 'rstcd',
operator: 'in',
dataType: 'string',
value: props.pieCode
}
: null,
// type
{ field: 'type', operator: 'eq', value: props.type },
// dataType/
{ field: 'dataType', operator: 'eq', value: activeKey.value },
//
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dayjs(props.vdDate)
.startOf('day')
.format('YYYY-MM-DD 00:00:00')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dayjs(props.vdDate)
.endOf('day')
.format('YYYY-MM-DD 23:59:59')
},
// stnm
props.stnm
? { field: 'stnm', operator: 'contains', value: props.stnm }
: null,
// rstcd
props.rstcd
? {
field: 'rstcd',
operator: 'in',
dataType: 'string',
value: [props.rstcd]
}
: null
].filter(Boolean);
const params = {
filter: { logic: 'and', filters },
sort: [{ field: 'tm', dir: 'desc' }],
skip: (page.value - 1) * pageSize.value,
take: pageSize.value
};
const res: any = await getAiFile(params);
const rData = res?.data ?? {};
const rawData = rData?.data || [];
total.value = rData?.total || rawData.length;
// fid
const expandedData = rawData
.map((item: any) => {
const fids = item.fid ? item.fid.split(',') : [];
return fids.map((fid: string) => ({ ...item, fid: fid.trim() }));
})
.flat();
// fid
const allFids = expandedData
.filter((item: any) => item.fid)
.map((item: any) => item.fid)
.join(',');
// postIdUrl
let mediaMap: Record<string, any> = {};
if (allFids) {
const formData = new FormData();
formData.append('id', allFids);
const mediaRes: any = await postIdUrl(formData);
mediaMap = mediaRes?.data || {};
}
//
const list: any[] = [];
expandedData.forEach((vd: any) => {
const fileInfo = mediaMap[vd.fid];
const hasValidExtension =
fileInfo && ['.jpg', '.png'].includes(fileInfo.ext);
let flpth = '';
let imgPath = '';
let type = '';
if (hasValidExtension !== undefined && !hasValidExtension) {
flpth = fileInfo.fullpath;
type = 'vdsp';
} else if (hasValidExtension !== undefined) {
imgPath = fileInfo.fullpath;
type = 'img';
}
const tm = vd.tm ? dayjs(vd.tm).format('YYYY-MM-DD HH:mm:ss') : '-';
list.push({
...vd,
flpth,
imgPath,
flnm: `${vd.ennm || ''}${tm}`,
type,
tm
});
});
//
const listMap = list.filter(
item => item.flpth !== '' || item.imgPath !== ''
);
videoList.value = listMap;
//
if (listMap.length > 0) {
const first = listMap[0];
activeFid.value = first.fid;
if (first.type === 'img') {
activeMedia.value = {
type: 'image',
src: baseUrl + '?' + first.fid + '&view=jpg'
};
} else if (first.type === 'vdsp') {
activeMedia.value = {
type: 'video',
src: baseUrl + '?' + first.fid + '&view=jpg'
};
} else {
activeMedia.value = { type: '', src: '' };
}
} else {
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
}
} catch (error) {
console.error('获取视频详情列表失败:', error);
videoList.value = [];
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
} finally {
loading.value = false;
}
};
// ==================== ====================
const handleTabChange = () => {
page.value = 1;
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
fetchVideoList();
};
const handleSelectMedia = (item: any) => {
activeFid.value = item.fid;
if (item.type === 'img') {
activeMedia.value = {
type: 'image',
src: baseUrl + '?' + item.fid + '&view=jpg'
};
} else if (item.type === 'vdsp') {
activeMedia.value = {
type: 'video',
src: baseUrl + '?' + item.fid + '&view=jpg'
};
} else {
activeMedia.value = { type: '', src: '' };
}
};
const handlePageChange = (newPage: number, newPageSize: number) => {
page.value = newPage;
pageSize.value = newPageSize;
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
fetchVideoList();
};
// ==================== ====================
watch(
() => [props.vdDate, props.type, activeKey.value],
() => {
if (props.vdDate) {
page.value = 1;
fetchVideoList();
}
}
);
// ==================== ====================
onMounted(() => {
if (props.vdDate) {
fetchVideoList();
}
});
</script>
<style lang="scss" scoped>
.video-details {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
:deep(.ant-tabs) {
flex: 1;
display: flex;
flex-direction: column;
}
:deep(.ant-tabs-content) {
flex: 1;
}
:deep(.ant-tabs-tabpane) {
height: 100%;
}
.video-container {
height: 100%;
}
}
</style>

View File

@ -3,7 +3,12 @@
<SidePanelItem title="AI识别过鱼设施运行" :datetimePicker="datetimePicker" @update-values="handlePanelChange">
<a-spin :spinning="dataLoading" tip="加载中...">
<div class="facility-grid">
<div v-for="facility in facilities" :key="facility.key" class="facility-card">
<div
v-for="facility in facilities"
:key="facility.key"
class="facility-card"
@click="handleCardClick(facility.action)"
>
<div style="width: 70px;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>
@ -20,12 +25,30 @@
</div>
</a-spin>
</SidePanelItem>
<!-- AI识别过鱼设施运行详情弹框 -->
<a-modal
v-model:open="modalVisible"
title="AI识别过鱼设施运行"
width="1536px"
:footer="null"
@cancel="handleModalClose"
>
<AISPTK
v-if="modalVisible"
:month="datetimePicker.value"
:action="currentAction"
:baseid="baseid"
:ly-name="lyName"
/>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import AISPTK from './AISPTK.vue';
import { getAiGyssList } from '@/api/zngj';
import dayjs from 'dayjs';
@ -39,6 +62,10 @@ const baseid = ref('');
const dataLoading = ref(false);
const panelDetail = ref<any>({});
// ==================== ====================
const modalVisible = ref(false);
const currentAction = ref('AI_5006');
const lyName = computed(() => baseid.value === '02' ? '升鱼斗运行' : '升鱼机运行');
// ==================== ====================
@ -56,37 +83,43 @@ const facilities = computed(() => [
key: 'gys',
name: '赶鱼栅运行',
count: panelDetail.value?.fpGysCnt ?? 0,
icon: 'icon iconfont icon-ganyushan'
icon: 'icon iconfont icon-ganyushan',
action: 'AI_5006'
},
{
key: 'syj',
name: lyName.value,
count: panelDetail.value?.fpSyjCnt ?? 0,
icon: 'icon iconfont icon-liuliangjiancezhan1'
icon: 'icon iconfont icon-liuliangjiancezhan1',
action: 'AI_5007'
},
{
key: 'agv',
name: 'AGV小车',
count: panelDetail.value?.fpAgvCnt ?? 0,
icon: 'icon iconfont icon-agvxiaoche'
icon: 'icon iconfont icon-agvxiaoche',
action: 'AI_5008'
},
{
key: 'yych',
name: '运鱼车运行',
count: panelDetail.value?.fpYychCnt ?? 0,
icon: 'icon iconfont icon-yunyuche'
icon: 'icon iconfont icon-yunyuche',
action: 'AI_5002'
},
{
key: 'yyc',
name: '运鱼船出港',
count: panelDetail.value?.fpYycCnt ?? 0,
icon: 'icon iconfont icon-yunyuchuan'
icon: 'icon iconfont icon-yunyuchuan',
action: 'AI_5003'
},
{
key: 'ydh',
name: '鱼道放流',
count: panelDetail.value?.fpYdhCnt ?? 0,
icon: 'icon iconfont icon-yudaofangliu'
icon: 'icon iconfont icon-yudaofangliu',
action: 'AI_5012'
}
]);
@ -99,6 +132,15 @@ const handlePanelChange = (data: any) => {
getCardData();
};
const handleCardClick = (action: string) => {
currentAction.value = action;
modalVisible.value = true;
};
const handleModalClose = () => {
modalVisible.value = false;
};
// ==================== ====================
const getCardData = async () => {
if (!datetimePicker.value.value || !baseid.value) return;

View File

@ -4,6 +4,21 @@
<a-table :columns="columns" :data-source="tableData" :pagination="false" size="middle" :customRow="customRow"
:loading="dataLoading" bordered class="custom-table">
</a-table>
<a-modal
v-model:open="modalVisible"
title="水电开发情况"
width="1536px"
:footer="null"
:destroy-on-close="true"
>
<ShuiDianKaiFQKTwoLayer
v-if="modalVisible"
:defaultJidiInfo="modalProps.defaultJidiInfo"
:seriesName="modalProps.seriesName"
:stateName="modalProps.stateName"
:defaultTab="modalProps.defaultTab"
/>
</a-modal>
</div>
</template>
@ -11,6 +26,7 @@
import { ref, watch } from 'vue';
import { getVmsstbprptKendoList } from '@/api/home';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import ShuiDianKaiFQKTwoLayer from './TwoLayer/ShuiDianKaiFQKTwoLayer.vue';
defineOptions({
name: 'DataTable'
@ -25,6 +41,26 @@ const baseid = ref('');
const dataLoading = ref(false);
const tableData = ref<any[]>([]);
const modalVisible = ref(false);
const modalProps = ref<any>({});
const openDialog = (record: any) => {
const currentJidiInfo = JidiSelectEventStore.selectedItem;
const isAll = currentJidiInfo?.wbsCode === 'all';
const stateName = record.bldsttCcode === '0' ? '未建' : record.bldsttCcode === '1' ? '在建' : '已建';
modalProps.value = {
defaultJidiInfo: {
baseid: isAll ? record.key : currentJidiInfo?.wbsCode || 'all',
basename: isAll ? record.name : currentJidiInfo?.wbsName || '全部基地'
},
seriesName: '装机容量(万kW)',
stateName,
defaultTab: stateName
};
modalVisible.value = true;
};
const columns = ref([
{
title: '基地名称',
@ -183,6 +219,7 @@ const customRow = (record: any, index: number) => {
},
onClick: () => {
if (record.name !== '总计') {
openDialog(record);
emit('row-click', record);
}
}

View File

@ -1,58 +1,24 @@
<!-- SidePanelItem.vue -->
<template>
<div class="basic_body">
<SidePanelItem title="大中型水电开发及环境监测数据接入情况" :prompt="prompt">
<SidePanelItem title="大中型水电开发及环境监测数据接入情况" >
<div>
<div class="body_top">
<!-- {/* 水电开发情况图表 */} -->
<div style="flex: 1;">
<div>水电开发情况</div>
<div style="color: #757575;font-size: 12px;line-height: 16px;">图释数量/装机容量</div>
<div>
<DevelopStatusChart @chart-click="handleChartClick" />
</div>
<a-spin :spinning="developStatusChartRef?.dataLoading">
<DevelopStatusChart ref="developStatusChartRef" />
</a-spin>
</div>
<!-- style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center" }} -->
<div style="flex: 1;display: flex; flex-direction: column; align-items: center;">
<EngEnvironmentData @item-click="handleEnvItemClick" />
<a-spin :spinning="engEnvironmentDataRef?.dataLoading">
<EngEnvironmentData ref="engEnvironmentDataRef" />
</a-spin>
</div>
</div>
<DataTable @row-click="handleRowClick" />
<DataTable />
</div>
<!-- <div v-else class="zanwushujv"> <a-empty /></div> -->
</SidePanelItem>
<!-- 水电开发情况弹窗 -->
<a-modal
v-model:open="shuidianModalVisible"
:title="shuidianModalTitle"
width="1536px"
:footer="null"
:destroy-on-close="true"
>
<ShuiDianKaiFQKTwoLayer
v-if="shuidianModalVisible"
:defaultJidiInfo="shuidianModalProps.defaultJidiInfo"
:seriesName="shuidianModalProps.seriesName"
:stateName="shuidianModalProps.stateName"
:defaultTab="shuidianModalProps.defaultTab"
/>
</a-modal>
<!-- 环境监测数据接入情况弹窗 -->
<a-modal
v-model:open="huanjingModalVisible"
:title="huanjingModalTitle"
width="1536px"
:footer="null"
:destroy-on-close="true"
>
<HuanJingSJJRQK
v-if="huanjingModalVisible"
:dataDimensionVal="huanjingModalProps.dataDimensionVal"
:name="huanjingModalProps.name"
/>
</a-modal>
</div>
</template>
@ -62,36 +28,21 @@ import SidePanelItem from '@/components/SidePanelItem/index.vue';
import DevelopStatusChart from "@/components/developStatusChart/index.vue"
import EngEnvironmentData from "@/components/engEnvironmentData/index.vue"
import DataTable from "./DataTable.vue"
import ShuiDianKaiFQKTwoLayer from "./TwoLayer/ShuiDianKaiFQKTwoLayer.vue"
import HuanJingSJJRQK from "./TwoLayer/HuanJingSJJRQK.vue"
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { Spin } from 'ant-design-vue';
// 便
defineOptions({
name: 'shuidianhuangjingjieruMod'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const prompt = ref({
show: true,
value: '统计大中型已建,在建水电站',
})
const developStatusChartRef = ref<InstanceType<typeof DevelopStatusChart> | null>(null);
const engEnvironmentDataRef = ref<InstanceType<typeof EngEnvironmentData> | null>(null);
//
const shuidianModalVisible = ref(false);
const shuidianModalTitle = ref('水电开发情况');
const shuidianModalProps = ref<any>({});
//
const huanjingModalVisible = ref(false);
const huanjingModalTitle = ref('环境监测数据接入情况');
const huanjingModalProps = ref<any>({});
// DataTable
const handleRowClick = (record: any) => {
const currentJidiInfo = JidiSelectEventStore.selectedItem;
shuidianModalProps.value = {
developStatusChartRef.value?.openModal({
defaultJidiInfo: {
baseid: currentJidiInfo?.wbsCode || 'all',
basename: currentJidiInfo?.wbsName || '全部基地'
@ -99,41 +50,10 @@ const handleRowClick = (record: any) => {
seriesName: currentJidiInfo?.wbsCode === 'other' ? '数量(座)' : '装机容量(万kW)',
stateName: record.bldsttCcode === '0' ? '未建' : record.bldsttCcode === '1' ? '在建' : '已建',
defaultTab: record.bldsttCcode === '0' ? '未建' : record.bldsttCcode === '1' ? '在建' : '已建'
};
shuidianModalTitle.value = '水电开发情况';
shuidianModalVisible.value = true;
});
};
// DevelopStatusChart
const handleChartClick = (params: any) => {
const currentJidiInfo = JidiSelectEventStore.selectedItem;
shuidianModalProps.value = {
defaultJidiInfo: {
baseid: currentJidiInfo?.wbsCode || 'all',
basename: currentJidiInfo?.wbsName || '全部基地'
},
seriesName: params.seriesName,
stateName: params.name,
defaultTab: params.name
};
shuidianModalTitle.value = '水电开发情况';
shuidianModalVisible.value = true;
};
// EngEnvironmentData
const handleEnvItemClick = (params: any) => {
const currentJidiInfo = JidiSelectEventStore.selectedItem;
huanjingModalProps.value = {
dataDimensionVal: currentJidiInfo?.wbsCode || 'all',
name: params.name
};
huanjingModalTitle.value = '环境监测数据接入情况';
huanjingModalVisible.value = true;
};
//
onMounted(() => {
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,218 @@
<template>
<div class="video-section">
<div class="video-mode">
<div class="video-main" v-loading="loading">
<div v-if="activeMedia.type === 'image' && activeMedia.src" class="media-display">
<img :src="activeMedia.src" style="width: 100%; height: 100%; object-fit: contain" />
</div>
<div v-else-if="activeMedia.type === 'video' && activeMedia.src" class="media-display">
<LiveVideoBox :videoData="{ url: activeMedia.src }" height="100%" :showTitle="false" />
</div>
<div v-else class="media-empty">
<a-empty :description="emptyText" />
</div>
</div>
<div class="video-sidebar" v-loading="loading">
<div class="sidebar-list" v-if="list.length > 0">
<div
v-for="item in list"
:key="item.fid"
class="sidebar-item"
:class="{ active: activeFid === item.fid }"
@click="handleSelect(item)"
>
<div class="item-thumb">
<img v-if="item.type === 'img'" :src="item.imgPath" />
<div v-else-if="item.type === 'vdsp'" class="thumb-placeholder">
<img :src="videoCover" />
</div>
<span v-else class="no-media">无媒体</span>
</div>
<div class="item-time">{{ item.stName || item.ennm }}{{ item.tm }}</div>
</div>
</div>
<a-empty v-else description="暂无数据" class="sidebar-empty" />
</div>
</div>
<div class="sidebar-pagination" style="margin-top: 10px; display: flex; justify-content: end">
<a-pagination
v-model:current="currentPage"
v-model:pageSize="currentPageSize"
:page-size-options="pageSizeOptions"
:total="total"
show-size-changer
size="small"
@change="handlePageChange"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import LiveVideoBox from '@/views/shiPinJianKong/components/LiveVideoBox.vue';
import videoCover from '@/assets/images/videFm.jpg';
interface Props {
list: any[];
activeFid: string;
activeMedia: { type: string; src: string };
loading: boolean;
page: number;
pageSize: number;
pageSizeOptions: string[];
total: number;
emptyText?: string;
}
const props = withDefaults(defineProps<Props>(), {
emptyText: '请选择右侧数据'
});
const emit = defineEmits<{
(e: 'select', item: any): void;
(e: 'pageChange', page: number, pageSize: number): void;
}>();
const currentPage = ref(props.page);
const currentPageSize = ref(props.pageSize);
watch(
() => props.page,
val => {
currentPage.value = val;
}
);
watch(
() => props.pageSize,
val => {
currentPageSize.value = val;
}
);
const handleSelect = (item: any) => {
emit('select', item);
};
const handlePageChange = (page: number, pageSize: number) => {
currentPage.value = page;
currentPageSize.value = pageSize;
emit('pageChange', page, pageSize);
};
</script>
<style scoped>
.video-mode {
display: flex;
gap: 12px;
width: 100%;
height: 520px;
.video-main {
flex: 0 0 80%;
background: #000;
border-radius: 4px;
overflow: hidden;
.media-display {
width: 100%;
height: 100%;
}
.media-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
}
.video-sidebar {
flex: 0 0 20%;
display: flex;
flex-direction: column;
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
.sidebar-list {
flex: 1;
overflow-y: auto;
.sidebar-item {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f7ff;
}
.item-thumb {
width: 100%;
height: 110px;
background: #1a1a1a;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumb-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
.no-media {
font-size: 12px;
color: #999;
}
}
}
.item-time {
margin-top: 6px;
font-size: 12px;
color: #666;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.sidebar-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.sidebar-pagination {
padding: 8px;
text-align: center;
border-top: 1px solid #e8e8e8;
}
}
}
/* 样式继承自父组件 */
</style>

View File

@ -0,0 +1,119 @@
import * as echarts from 'echarts';
import type { ECharts, EChartsOption } from 'echarts';
export interface ChartDataItem {
name: string;
value: number;
key: string;
}
export function usePieChart(
chartRefs: Map<string, HTMLElement>,
chartData: { value: ChartDataItem[] },
tabIndex: { value: string },
typeMap: Record<string, string>,
legendCallback?: (e: any) => void
) {
let chartInstance: ECharts | null = null;
const initChart = (retryCount = 0) => {
const dom = chartRefs.get(tabIndex.value);
if (!dom) {
if (retryCount < 10) {
setTimeout(() => initChart(retryCount + 1), 100);
}
return;
}
const rect = dom.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
if (retryCount < 10) {
setTimeout(() => initChart(retryCount + 1), 100);
}
return;
}
if (!chartInstance) {
chartInstance = echarts.init(dom);
}
const option: EChartsOption = {
tooltip: {
trigger: 'item',
formatter: (params: any) => `${params.marker} ${params.name}: ${params.value}(次)`
},
legend: {
orient: 'vertical',
right: 10,
top: 80,
textStyle: { fontSize: 12 }
},
series: []
};
if (!chartData.value || chartData.value.length === 0) {
chartInstance.setOption(option, true);
return;
}
const total = chartData.value.reduce((acc: number, item: ChartDataItem) => acc + (item.value || 0), 0);
const titleText = typeMap[tabIndex.value] || '识别(次)';
option.series = [
{
type: 'pie',
radius: ['26%', '35%'],
center: ['35%', '50%'],
avoidLabelOverlap: false,
label: {
show: true,
formatter: '{c}'
},
labelLine: {
show: true,
length: 12,
length2: 12
},
data: chartData.value.map((item: ChartDataItem) => ({
name: item.name,
value: item.value
}))
}
];
option.title = {
text: String(total),
left: '34%',
top: '44%',
textAlign: 'center',
textStyle: {
fontSize: 24,
fontWeight: 'bold',
color: '#333'
},
subtext: titleText,
subtextStyle: {
fontSize: 12,
color: '#333'
}
};
chartInstance.setOption(option, true);
if (legendCallback) {
chartInstance.off('legendselectchanged');
chartInstance.on('legendselectchanged', legendCallback);
}
};
const dispose = () => {
chartInstance?.dispose();
chartInstance = null;
};
const resize = () => {
chartInstance?.resize();
};
return { initChart, dispose, resize };
}

View File

@ -0,0 +1,131 @@
import { ref } from 'vue';
import { postIdUrl } from '@/api/mapModal';
import dayjs from 'dayjs';
export interface VideoListItem {
fid: string;
media: any;
thumbUrl: string;
isVideo: boolean;
isImage: boolean;
tm: string;
ennm?: string;
[key: string]: any;
}
export function useVideoList(
fetchFn: (params: any) => Promise<any>,
baseUrl: string
) {
const page = ref(1);
const take = ref(20);
const total = ref(0);
const loading = ref(false);
const list = ref<VideoListItem[]>([]);
const activeFid = ref('');
const activeMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
const fetchList = async (filter: any, options: any = {}) => {
loading.value = true;
try {
const skip = (page.value - 1) * take.value;
const params = { filter, skip, take: take.value, ...options };
const res = await fetchFn(params);
const dataObj = res?.data?.data ?? {};
const rawData = dataObj?.data || [];
total.value = dataObj?.total || 0;
const allFids: string[] = [];
rawData.forEach((item: any) => {
if (item.fid) {
item.fid.split(',').forEach((f: string) => {
const trimmed = f.trim();
if (trimmed) allFids.push(trimmed);
});
}
});
let mediaMap: Record<string, any> = {};
if (allFids.length > 0) {
const formData = new FormData();
formData.append('id', allFids.join(','));
const mediaRes: any = await postIdUrl(formData);
mediaMap = mediaRes?.data || {};
}
const flatList: VideoListItem[] = [];
rawData.forEach((item: any) => {
const fids = item.fid
? item.fid.split(',').map((f: string) => f.trim()).filter(Boolean)
: [];
const tm = item.tm ? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss') : '-';
fids.forEach((fid: string) => {
const m = mediaMap[fid];
const ext = m?.ext || '';
const isVideo = ext === '.mp4';
const isImage = ext === '.jpg' || ext === '.png';
flatList.push({
...item,
fid,
media: m || null,
thumbUrl: isImage ? baseUrl + '?' + fid + '&view=jpg' : '',
isVideo,
isImage,
tm
});
});
});
list.value = flatList;
if (list.value.length > 0 && !activeFid.value) {
selectMedia(list.value[0]);
}
} catch (error) {
console.error('获取视频列表失败:', error);
list.value = [];
} finally {
loading.value = false;
}
};
const selectMedia = (item: VideoListItem) => {
activeFid.value = item.fid;
const { isVideo, isImage } = item;
if (isImage) {
activeMedia.value = { type: 'image', src: baseUrl + '?' + item.fid + '&view=jpg' };
} else if (isVideo) {
activeMedia.value = { type: 'video', src: baseUrl + '?' + item.fid + '&view=jpg' };
} else {
activeMedia.value = { type: '', src: '' };
}
};
const handlePageChange = () => {
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
};
const reset = () => {
page.value = 1;
activeFid.value = '';
activeMedia.value = { type: '', src: '' };
};
return {
page,
take,
total,
loading,
list,
activeFid,
activeMedia,
fetchList,
selectMedia,
handlePageChange,
reset
};
}

View File

@ -0,0 +1,60 @@
export enum TabType {
PFW = '1',
DLM = '2',
QP = '3'
}
export enum AiType {
PFW = 'AI_5001',
DLM = 'AI_5005',
QP = 'AI_5011'
}
export interface ChartDataItem {
name: string;
value: number;
key: string;
}
export interface VideoListItem {
fid: string;
media: any;
thumbUrl: string;
isVideo: boolean;
isImage: boolean;
tm: string;
ennm?: string;
[key: string]: any;
}
export interface CalendarDayData {
dt: string;
status?: string;
qpStatus?: string;
fid?: string;
}
export interface CalendarStatusInfo {
name: string;
className: string;
type: string;
dataType: string;
}
export interface TabItem {
key: number;
value: string;
unit: string;
count: number;
}
export interface SearchData {
baseid: string;
stnm: string;
month: any;
}
export interface DetailTabItem {
key: string;
label: string;
}

View File

@ -32,7 +32,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import { ref, onUnmounted, watch, computed, nextTick } from 'vue';
import { CloseOutlined } from '@ant-design/icons-vue';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
@ -279,27 +279,30 @@ const handleClose = () => {
emit('close', props.videoData?.key);
};
// URL
//
watch(
videoUrl,
(newUrl, oldUrl) => {
() => props.videoData,
(newData, oldData) => {
const newUrl = newData?.url;
const oldUrl = oldData?.url;
if (newUrl && newUrl !== oldUrl) {
// URL DOM
setTimeout(() => initPlayer(), 200);
// URL
nextTick(() => {
if (player) {
player.dispose();
player = null;
}
initPlayer();
});
} else if (newUrl && !oldUrl) {
//
setTimeout(() => initPlayer(), 100);
nextTick(() => initPlayer());
}
},
{ immediate: true }
{ immediate: true, deep: true }
);
onMounted(() => {
if (videoUrl.value) {
initPlayer();
}
});
onUnmounted(() => {
//
if (player) {