电站专题-初版

This commit is contained in:
王兴凯 2026-07-31 11:13:49 +08:00
parent 22c00106c1
commit d6ef2e3495
57 changed files with 5673 additions and 1041 deletions

View File

@ -11,9 +11,9 @@ VITE_APP_BASE_URL = 'http://localhost:8093'
# 测试环境 # 测试环境
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093' # VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
# 汤伟 # 汤伟
VITE_APP_BASE_URL = 'http://10.84.111.235:8093' # VITE_APP_BASE_URL = 'http://10.84.111.235:8093'
# 李林 # 李林
# VITE_APP_BASE_URL = 'http://10.84.111.25:8093' VITE_APP_BASE_URL = 'http://10.84.111.25:8093'
## 开发环境 附件服务地址 ## 开发环境 附件服务地址
VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125' VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125'

View File

@ -109,7 +109,7 @@ export class MapCesium implements MapInterface {
this.containerId = container.id; this.containerId = container.id;
this.containerElement = container; this.containerElement = container;
this.showLoadingOverlay(container); this.showLoadingOverlay(container);
const token = 'bearer fa8aa37c-1e52-4631-a699-625b4147ace8'; const token = 'bearer b734a443-2c8f-4f4a-8698-44828cc5f709';
this.viewer = new Cesium.Viewer(container, { this.viewer = new Cesium.Viewer(container, {
animation: false, animation: false,

View File

@ -0,0 +1,115 @@
<template>
<!-- 第一步输入修改依据 -->
<a-modal
v-model:open="dataSourceVisible"
:title="title"
ok-text="确定"
cancel-text="取消"
@ok="handleDataSourceConfirm"
>
<div>
<p style="color: red; margin-bottom: 8px">请输入修改依据以继续删除操作</p>
<a-textarea
v-model:value="dataSource"
placeholder="请输入修改依据"
:rows="4"
/>
</div>
</a-modal>
<!-- 第二步确认删除 -->
<a-modal
v-model:open="confirmVisible"
title="确认删除"
ok-text="确认删除"
cancel-text="取消"
:ok-button-props="{ danger: true }"
:confirm-loading="confirmLoading"
@ok="handleFinalDelete"
>
<div>
<p style="color: red; font-weight: bold">请慎重操作</p>
<p>{{ label }}名称{{ deleteRecord?.stnm }}</p>
<p>修改依据{{ dataSource || '无' }}</p>
<p>确定要删除该{{ label }}</p>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { deletePowerInfo } from '@/api/DataQueryMenuModule';
import { useDraggable } from '@/utils/drag';
const props = withDefaults(
defineProps<{
/** 自定义删除函数,接收 (record, reason) 参数 */
deleteFn?: (record: any, reason: string) => Promise<any>;
/** 弹窗标题 */
title?: string;
/** 显示标签(如"电站"、"数据" */
label?: string;
}>(),
{
title: '删除电站',
label: '电站'
}
);
const emit = defineEmits(['success']);
const dataSourceVisible = ref(false);
const confirmVisible = ref(false);
const confirmLoading = ref(false);
const dataSource = ref('');
const deleteRecord = ref<any>(null);
const onSuccess = ref<Function>(() => {});
//
const open = (record: any, callback: Function) => {
deleteRecord.value = record;
dataSource.value = '';
dataSourceVisible.value = true;
onSuccess.value = callback;
};
useDraggable(dataSourceVisible, { boundary: true, resetOnOpen: true });
useDraggable(confirmVisible, { boundary: true, resetOnOpen: true });
//
const handleDataSourceConfirm = () => {
dataSourceVisible.value = false;
confirmVisible.value = true;
};
//
const handleFinalDelete = async () => {
confirmLoading.value = true; //
try {
let res: any;
if (props.deleteFn) {
// 使
res = await props.deleteFn(deleteRecord.value, dataSource.value);
} else {
// 使
const params = {
ids: [deleteRecord.value.stcd],
source: dataSource.value
};
res = await deletePowerInfo(params);
}
if (res?.code == 0 || res?.success) {
message.success('删除成功');
confirmVisible.value = false;
onSuccess.value();
} else {
message.error(res?.msg || '删除失败');
}
} catch (error) {
message.error('删除失败,请重试');
} finally {
confirmLoading.value = false; //
}
};
defineExpose({ open });
</script>

View File

@ -103,7 +103,7 @@ const buildGroupedColumns = () => {
const groupMap = new Map<string, ColumnItem[]>(); const groupMap = new Map<string, ColumnItem[]>();
// defaultConfig: true // defaultConfig: true
const filteredList = props.userColumnList.filter(item => item.defaultConfig !== true); const filteredList = props.userColumnList.filter(item => item.enable == 1);
// groupType // groupType
for (const item of filteredList) { for (const item of filteredList) {

View File

@ -19,6 +19,16 @@
<!-- ===== 概述 ===== --> <!-- ===== 概述 ===== -->
<a-col :span="24"><div class="form-group-title">概述</div></a-col> <a-col :span="24"><div class="form-group-title">概述</div></a-col>
<a-col :span="8">
<a-form-item label="电站编码" name="stcd">
<a-input
v-model:value="formData.stcd"
placeholder="请输入电站编码"
:disabled="!isAdd"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8"> <a-col :span="8">
<a-form-item label="电站名称" name="ennm"> <a-form-item label="电站名称" name="ennm">
<a-input <a-input
@ -1121,11 +1131,14 @@ const formRef = ref();
const confirmLoading = ref(false); const confirmLoading = ref(false);
const formData = ref<any>({}); const formData = ref<any>({});
const originalRecord = ref<any>({}); const originalRecord = ref<any>({});
const formRules = ref<any>({ const formRules = computed(() => ({
stcd: props.isAdd
? [{ required: true, message: '请输入电站编码', trigger: 'blur' }]
: [],
ennm: [{ required: true, message: '请输入电站名称', trigger: 'blur' }], ennm: [{ required: true, message: '请输入电站名称', trigger: 'blur' }],
baseId: [{ required: true, message: '请选择基地', trigger: 'change' }], baseId: [{ required: true, message: '请选择基地', trigger: 'change' }],
reachcd: [{ required: true, message: '请选择所在河段', trigger: 'change' }] reachcd: [{ required: true, message: '请选择所在河段', trigger: 'change' }]
}); }));
// //
const confirmModalVisible = ref(false); const confirmModalVisible = ref(false);

View File

@ -60,6 +60,7 @@
<!-- 编辑电站 Modal --> <!-- 编辑电站 Modal -->
<EditPowerModal <EditPowerModal
v-model:open="editVisible" v-model:open="editVisible"
:is-add="isAdd"
:dvtp-list="dvtpList" :dvtp-list="dvtpList"
:record="editRecord" :record="editRecord"
:top-hynm-list="topHynmList" :top-hynm-list="topHynmList"
@ -491,7 +492,7 @@ const fetchColumnConfig = () => {
// API orderIndex checked 1 // API orderIndex checked 1
const sorted = [...list] const sorted = [...list]
.filter((item: any) => item.checked === 1 && item.defaultConfig !== true) .filter((item: any) => item.checked === 1 && item.enable == 1)
.sort((a: any, b: any) => a.orderIndex - b.orderIndex); .sort((a: any, b: any) => a.orderIndex - b.orderIndex);
const cols = sorted.map((item: any) => { const cols = sorted.map((item: any) => {

View File

@ -42,7 +42,7 @@ const columns = ref<any[]>([
{ key: 'baseName', title: '水电基地', dataIndex: 'baseName', visible: true, width: 120, ellipsis: true }, { key: 'baseName', title: '水电基地', dataIndex: 'baseName', visible: true, width: 120, ellipsis: true },
{ key: 'ennm', title: '所属电站', dataIndex: 'ennm', visible: true, width: 100, ellipsis: true }, { key: 'ennm', title: '所属电站', dataIndex: 'ennm', visible: true, width: 100, ellipsis: true },
{ key: 'zzfldxName', title: '放流对象', dataIndex: 'zzfldxName', visible: true, width: 120, ellipsis: true }, { key: 'zzfldx', title: '放流对象', dataIndex: 'zzfldx', visible: true, width: 120, ellipsis: true },
{ key: 'zzflcnt', title: '放流规模(尾)', dataIndex: 'zzflcnt', visible: true, width: 120 }, { key: 'zzflcnt', title: '放流规模(尾)', dataIndex: 'zzflcnt', visible: true, width: 120 },
{ key: 'zzflbjfs', title: '标记方式', dataIndex: 'zzflbjfs', visible: true, width: 100, ellipsis: true }, { key: 'zzflbjfs', title: '标记方式', dataIndex: 'zzflbjfs', visible: true, width: 100, ellipsis: true },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="export-container"> <div class="export-container body_one">
<a-form <a-form
ref="formRef" ref="formRef"
:model="formData" :model="formData"
@ -321,4 +321,9 @@ onMounted(() => {
} }
} }
} }
.body_one {
position: relative;
z-index: 900;
pointer-events: all;
}
</style> </style>

View File

@ -548,4 +548,9 @@ onMounted(() => {
}); });
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss">
.body_one {
position: relative;
z-index: 900;
pointer-events: all;
}</style>

View File

@ -205,4 +205,9 @@ onMounted(() => {
}); });
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss">
.body_one {
position: relative;
z-index: 900;
pointer-events: all;
}</style>

View File

@ -14,9 +14,13 @@ VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_URL = 'http://10.84.111.235:8093' VITE_APP_BASE_URL = 'http://10.84.111.235:8093'
# 李林 # 李林
# VITE_APP_BASE_URL = 'http://10.84.111.25:8093' # VITE_APP_BASE_URL = 'http://10.84.111.25:8093'
# 小扈
# VITE_APP_BASE_URL = 'http://10.84.111.182:8093'
## 开发环境 附件服务地址 ## 开发环境 附件服务地址
VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125' VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125'
## 生产环境 附件服务地址
VITE_APP_ATTACHMENT_URL1 = 'http://211.99.26.225:12127'
# 地图服务地址 # 地图服务地址
VITE_APP_MAP_URL = 'https://211.99.26.225:18085' VITE_APP_MAP_URL = 'https://211.99.26.225:18085'
# ?menu=systemManageMenu&page=disposeManage # ?menu=systemManageMenu&page=disposeManage

View File

@ -10,5 +10,7 @@ VITE_APP_BASE_URL = 'http://localhost:8093'
VITE_APP_BASE_API_URL = 'https://211.99.26.225:12130/prod-api' VITE_APP_BASE_API_URL = 'https://211.99.26.225:12130/prod-api'
## 生产环境 附件服务地址 ## 生产环境 附件服务地址
VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125' VITE_APP_ATTACHMENT_URL = 'https://211.99.26.225:12125'
## 生产环境 附件服务地址
VITE_APP_ATTACHMENT_URL1 = 'http://211.99.26.225:12127'
# 地图服务地址 # 地图服务地址
VITE_APP_MAP_URL = 'https://211.99.26.225:18085' VITE_APP_MAP_URL = 'https://211.99.26.225:18085'

View File

@ -0,0 +1,47 @@
import request from '@/utils/request';
/**
* +
* POST /wmp-env-server/env/vp/basin/GetKendoListCust
* @param params -- { filter: { rstcd }, group, groupResultFlat }
*/
export function getBotanicalGardenList(data: any) {
return request({
url: '/wmp-env-server/env/vp/basin/GetKendoListCust',
method: 'post',
data: data,
});
}
/**
*
* POST /wmp-env-server/env/vp/bcount/GetKendoListCust
* @param params -- { filter: { year, stcd } }
*/
export function getBotanicalGardenStatistic(params: any) {
const { filter, group, groupResultFlat } = params;
return request({
url: '/wmp-env-server/env/vp/bcount/GetKendoListCust',
method: 'post',
data: {
data: {
filter,
group,
groupResultFlat
}
}
});
}
/**
* BasicTable listUrl
* POST /wmp-env-server/env/vpr/basinVpIntDetail
* @param params -- BasicTable { filter, skip, take, sort, ... }
*/
export function getBotanicalGardenDetailData(params: any) {
return request({
url: '/vap/vpr/basinVpIntDetail',
method: 'post',
data: params
});
}

View File

@ -0,0 +1,154 @@
import request from '@/utils/request';
/**
* TreeSelect
* POST /sys/psbmodulelbb/getTreeConfiguredps
* getStationList
*/
export function getTreeConfiguredps() {
return request({
url: '/sys/psbmodulelbb/getTreeConfiguredps',
method: 'post',
data: {
filter: {
logic: 'and',
filters: [
{
field: 'wbsType',
operator: 'eq',
value: 'PSB_RVCD'
}
],
sort: [
{
field: 'baseId',
dir: 'asc'
}
]
}
}
});
}
/**
* psbmodulelbb
* POST /wmp-sys-server/sys/psbmodulelbb/GetKendoListCust
*/
export function getPowerSubjectLayout(
stcd: string,
templateId?: string,
ext = 'true'
) {
const filters = {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
},
{
field: 'templateId',
operator: 'eq',
dataType: 'string',
value: templateId || null
},
{
field: 'ext',
operator: 'eq',
dataType: 'string',
value: ext
}
]
};
return request({
url: '/sys/psbmodulelbb/GetKendoListCust',
method: 'post',
data: { filter: filters }
});
}
/**
* -
* POST /wmp-env-server/fb/station/eng/GetAggregateData
*/
export function getPowerStationReleaseYear(data) {
return request({
url: '/fb/station/eng/GetAggregateData',
method: 'post',
data: data
});
}
/**
* -
* POST /wmp-env-server/fb/station/eng/GetKendoListCust
*/
export function getPowerStationReleaseData(params: any) {
return request({
url: '/fb/station/eng/GetKendoListCust',
method: 'post',
data:params
});
}
/**
*
* POST /wmp-eng-server/common/societyeffb/GetKendoListCust
*/
export function getSocialInvestmentData(data: any) {
return request({
url: '/wmp-eng-server/common/societyeffb/GetKendoListCust',
method: 'post',
data: data
});
}
/**
*
* POST /dec-lygk-base-server/base/msstbprpt/GetKendoList
*/
export function getZenZhiZhanList(data: any) {
return request({
url: '/fb/msstbprpt/GetKendoList',
method: 'post',
data: data
});
}
/**
*
* POST /wmp-env-server/fb/breedStage/GetKendoListCust
*/
export function getHatcheryOperationData(data:any) {
return request({
url: '/fb/breedStage/GetKendoListCust',
method: 'post',
data: data
});
}
/**
*
* POST /wmp-env-server/env/fishDic/GetKendoList
*/
export function getFishDic(data:any) {
return request({
url: '/fpr/fishDic/GetKendoList',
method: 'post',
data: data
});
}
/**
* -
* POST /wmp-env-server/fb/msfbrd/qgc/GetKendoListCust
*/
export function getPowerStationReleaseDetail(data: any) {
return request({
url: '/fb/msfbrdm/qgc/GetKendoListCust',
method: 'post',
data: data
});
}

View File

@ -1,26 +1,98 @@
import request from '@/utils/request'; import request from '@/utils/request';
// 获取所有倾斜摄影 //
export function warnruleGetKendoList(data: any) { export function warnruleGetKendoList(data: any) {
return request({ return request({
url: '/api/wmp-sys-server/sys/warn/rule/GetKendoList', url: '/sys/warnRule/GetKendoListCust',
method: 'post', method: 'post',
data: data data: data
}); });
} }
//预警类型下拉框 //水质等级下拉框 export function bindGetKendoList(data: any) {
export function dictGetRemoteDictValue(data: any) {
return request({ return request({
url: '/api/dec-modules-usm-springcloud-starter/usm/v1/dict/getRemoteDictValue', url: '/sys/warnRule/bind/GetKendoList',
method: 'get', method: 'post',
params: data data: data
}); });
} }
//新增修改获取相关输入数据 //新增修改获取相关输入数据
export function ruleysList(data: any) { export function ruleysList(data: any) {
return request({ return request({
url: '/api/wmp-sys-server/sys/warn/rule/ysList', url: '/sys/warnRule/ysList',
method: 'post', method: 'post',
data: data data: data
}); });
} }
// 新增/修改预警规则
export function warnruleAddOrUpdate(data: any) {
return request({
url: '/sys/warnRule/addOrUpdate',
method: 'post',
data: data
});
}
// 删除预警规则
export function warnruleDelete(params: { id: string }) {
return request({
url: '/sys/warnRule/delete',
method: 'get',
params: params
});
}
// 获取所属电站列表
export function getStationList() {
return request({
url: '/sys/psbmodulelbb/getTreeConfiguredps',
method: 'post',
data: {
filter: {
logic: 'and',
filters: [
{
field: 'wbsType',
operator: 'eq',
value: 'PSB_RVCD'
}
]
}
}
});
}
// 删除绑定预警规则
export function warnruleBindDelete(params: { id: string }) {
return request({
url: '/sys/warnRule/bind/delete',
method: 'get',
params: params
});
}
// 根据电站获取预警规则列表
export function getRuleListByStcd(params: { stcd: string; ruleType: string }) {
return request({
url: '/sys/warnRule/getRuleListByStcd',
method: 'get',
params
});
}
// 根据预警规则ID获取详情含 detail 数据)
export function getDetailById(params: { ruleId: string }) {
return request({
url: '/sys/warnRule/getDetailById',
method: 'get',
params
});
}
// 修改规则是否展示
export function updateShow(data:any) {
return request({
url: '/sys/warnRule/updateShow',
method: 'post',
params:data
});
}

View File

@ -0,0 +1,25 @@
import request from '@/utils/request';
/**
*
* rstcd sttpCode
*/
export function videoSurveillance(data: any) {
return request({
url: '/vd/msstbprpt/GetKendoList',
method: 'post',
data
});
}
/**
*
* stcd
*/
export function realTimeVideo(data:any) {
return request({
url: '/vd/runData/GetKendoListCust',
method: 'post',
data
});
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="8px" viewBox="0 0 20 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>arrowDownLine2</title>
<defs>
<filter id="filter-1">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 0.184314 0 0 0 0 0.419608 0 0 0 0 0.596078 0 0 0 1.000000 0"></feColorMatrix>
</filter>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
<stop stop-color="#00F4FE" stop-opacity="0" offset="0%"></stop>
<stop stop-color="#00EDFD" offset="50.3632549%"></stop>
<stop stop-color="#00E6FC" stop-opacity="0" offset="100%"></stop>
</linearGradient>
</defs>
<g id="1-首页" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="1.1.1-电站专题" transform="translate(-1544.000000, -1239.000000)">
<g id="编组备份-4" transform="translate(1476.000000, 70.000000)">
<g id="编组-3" transform="translate(16.000000, 1072.000000)">
<g id="arrowDownLine2" transform="translate(51.571254, 96.742752)" filter="url(#filter-1)">
<g>
<polygon id="ArrowDownLine" fill="url(#linearGradient-2)" fill-rule="nonzero" transform="translate(10.505921, 4.257248) scale(-1, 1) rotate(-270.000000) translate(-10.505921, -4.257248) " points="7 -5.48550424 7.85749293 -6 14.0118417 4.25724788 7.85749293 14.5144958 7 14 12.8457465 4.25724788"></polygon>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="0 0 56 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>FishStation</title>
<defs>
<path d="M9.0836405,48.6439317 C20.4848773,59.0912405 38.196623,58.3179289 48.6439317,46.9166921 C59.0912405,35.5154553 58.3179289,17.8037096 46.9166921,7.35640088 C35.5154553,-3.09090785 17.8037096,-2.31759633 7.35640088,9.0836405 C-3.09090785,20.4848773 -2.31759633,38.196623 9.0836405,48.6439317 Z" id="path-1"></path>
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="56.0003326" height="56.0003326" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<circle id="path-3" cx="28.0001663" cy="28.0001663" r="21"></circle>
<mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="42" height="42" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-5">
<stop stop-color="#05D9FF" offset="0%"></stop>
<stop stop-color="#02B2FF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="08_增殖放流" transform="translate(-1512.000000, -537.000000)">
<g id="BGSM备份-4" transform="translate(1464.000000, 436.000000)">
<g id="编组-3" transform="translate(16.000000, 61.000000)">
<g id="FishStation" transform="translate(32.000000, 40.000000)">
<use id="椭圆形" stroke-opacity="0.45" stroke="#007FCC" mask="url(#mask-2)" stroke-width="8.75" stroke-dasharray="42,1.75" xlink:href="#path-1"></use>
<circle id="椭圆形" stroke-opacity="0.65" stroke="#02B2FF" stroke-width="0.875" fill="#014380" cx="28.0001663" cy="28.0001663" r="24.0625"></circle>
<use id="椭圆形备份-3" stroke="#02B2FF" mask="url(#mask-4)" stroke-width="3.5" stroke-dasharray="3.5,0.875" xlink:href="#path-3"></use>
<path d="M23.6237633,32.0387229 L23.6237633,35.0001891 L26.7401593,33.2949884 C36.8668397,35.0001891 42.0001663,28.7188456 42.0001663,28.7188456 C39.2962975,25.3989847 32.606162,24.0521677 32.606162,24.0521677 L26.464835,21.0001663 L26.4191017,24.2761678 C23.5323113,24.634568 19.2697742,27.7313803 19.2697742,27.7313803 C18.6295063,27.1023125 14.0001663,24.0960544 14.0001663,24.0960544 C16.2000349,25.7116552 16.2000349,31.7689877 14.0001663,33.3845939 L19.2697742,29.7949868 C19.8204292,30.3335222 23.6237633,32.0387229 23.6237633,32.0387229 Z M36.6838963,26.9231206 C37.5089642,26.9231206 38.151097,27.5960533 38.151097,28.4043213 C38.151097,29.166854 37.5089642,29.8397895 36.6838963,29.8397895 C35.8597635,29.8397895 35.2185622,29.1668568 35.2185622,28.4043213 C35.217628,27.5960533 35.8597608,26.9231206 36.6838963,26.9231206 Z" id="形状" fill="url(#linearGradient-5)" fill-rule="nonzero"></path>
<circle id="椭圆形" fill="#FDDD60" cx="21" cy="22.75" r="1"></circle>
<circle id="椭圆形备份-5" fill="#FDDD60" cx="40.2501663" cy="21.8751663" r="1.75"></circle>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,5 +1,6 @@
<template> <template>
<div <div
id="map-baselayer"
class="baselayer-switcher" class="baselayer-switcher"
:style="{ right: drawerOpen ? '480px' : '12px' }" :style="{ right: drawerOpen ? '480px' : '12px' }"
v-if="uiStore.mapType == '2D'" v-if="uiStore.mapType == '2D'"
@ -62,6 +63,16 @@ const nineSectionsImg: any = {
const activeKey = ref(layers[0].key); const activeKey = ref(layers[0].key);
// store
watch(
() => mapViewStore.activeBaseLayerKey,
(newKey) => {
if (newKey && layers.some(l => l.key === newKey)) {
activeKey.value = newKey;
}
}
);
/** /**
* 判断图层管理中的基础底图customBaseLayer是否被选中 * 判断图层管理中的基础底图customBaseLayer是否被选中
*/ */

View File

@ -974,7 +974,8 @@ onUnmounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-direction: column; flex-direction: column;
background-color: #fff;
// box-shadow: 0 1px 2px #00000026;
.qgc_title { .qgc_title {
width: 100%; width: 100%;
background-color: #e5edf3; background-color: #e5edf3;

View File

@ -10,6 +10,7 @@ import {
drawDotImg5, drawDotImg5,
offset5 offset5
} from '@/utils/GisUrlList'; } from '@/utils/GisUrlList';
import { LAYOUT_GRID_SKELETONS } from '@/views/dianZhanZhuanTi/layoutGridSkeletons';
declare global { declare global {
interface Window { interface Window {
__lyConfigs?: { __lyConfigs?: {
@ -321,162 +322,82 @@ const getListByPosition = (data: any, position: string) =>
/** /**
* *
* @param layoutType - * @param layoutType -
* @param data - * @param data - { type, data: BclDataItem[] }
* @param offset - * @param offset - + padding + gap 460440px + 10px + 10px
* @param bottomRowHeight - '200px'
*/ */
export const setMapLegendPos = ( export const setMapLegendPos = (
layoutType: string, layoutType: string,
data: any, data: any,
offset = 456 offset = 460,
bottomRowHeight?: string
) => { ) => {
const menuStateString = localStorage.getItem('menuState'); //处理澜沧江左侧菜单状态 const menuStateString = localStorage.getItem('menuState');
const menuState = const menuState =
menuStateString !== null ? JSON.parse(menuStateString) : true; menuStateString !== null ? JSON.parse(menuStateString) : true;
const _theme = localStorage.getItem('ly-theme') || window.__lyConfigs?.theme; const _theme = localStorage.getItem('ly-theme') || window.__lyConfigs?.theme;
const leftEle = document.querySelector('#page-layout-left') as HTMLElement;
const rightEle = document.querySelector('#page-layout-right') as HTMLElement; const legend = document.querySelector('#qgc-legendtl') as HTMLElement;
const bottomEle = document.querySelector( const filter = document.querySelector('#map-filter-container') as HTMLElement;
'#page-layout-bottom' const compassControl = document.querySelector('#map-compassControl') as HTMLElement;
) as HTMLElement; const controller = document.querySelector('#map-controller') as HTMLElement;
const legend = document.querySelector('#qgc-legendtl') as HTMLElement; // 图例 const monitor = document.querySelector('#map-monitor') as HTMLElement;
const filter = document.querySelector('#map-filter-container') as HTMLElement; // 全局表单 const baselayer = document.querySelector('#map-baselayer') as HTMLElement;
const compassControl = document.querySelector(
'#map-compassControl'
) as HTMLElement; // 全局表单
const controller = document.querySelector('#map-controller') as HTMLElement; // 地图工具栏
const monitor = document.querySelector('#map-monitor') as HTMLElement; // 地图工具栏
const baselayer = document.querySelector('#map-baselayer') as HTMLElement; // 底图模式切换
// const vd = document.querySelector('#vd_operate') as HTMLElement // 底部视频
const left = [
'layout1',
'layout2',
'layout3',
'layout4',
'layout6',
'layout7',
'layout8',
'layout9',
'layout10',
'layout11',
'layout14',
'layout15',
'layout16',
'layout17'
]; // 左侧布局
const right = [
'layout1',
'layout2',
'layout3',
'layout4',
'layout5',
'layout6',
'layout8',
'layout10',
'layout11',
'layout15',
'layout16',
'layout17'
]; // 右侧布局
const bottom1 = [
'layout1',
'layout6',
'layout8',
'layout9',
'layout10',
'layout16'
]; // 三行底部布局
const bottom2 = ['layout2', 'layout15']; // 四行底部布局
const w = `${offset}px`;
const l = `${_theme === 'ly-8' ? (menuState ? 643 : 510) : offset}px`;
let b = `0px`;
const le = ['layout17', 'layout10'].includes(layoutType) ? 0 : 1;
const leftList = getListByPosition(data, 'left'); const leftList = getListByPosition(data, 'left');
const rightList = getListByPosition(data, 'right'); const rightList = getListByPosition(data, 'right');
const bottomList = getListByPosition(data, 'bottom'); const bottomList = getListByPosition(data, 'bottom');
let bottom = '0';
const hasLeft = leftList?.length > 0;
const hasRight = rightList?.length > 0;
const hasBottom = bottomList?.length > 0;
console.log('[图例] hasLeft:', hasLeft, 'hasBottom:', hasBottom, 'bottomRowHeight:', bottomRowHeight);
const w = `${offset}px`;
const l = `${_theme === 'ly-8' ? (menuState ? 643 : 510) : offset}px`;
let b = `0px`;
if (_theme === 'ly-8') { if (_theme === 'ly-8') {
if (window.__mapMode === '3D') { b = window.__mapMode === '3D' ? '200px' : `${menuState ? 200 : 50}px`;
b = `200px`; }
} else {
b = `${menuState ? 200 : 50}px`; // 底部偏移
// px 值:直接 + padding(10px)
// fr 值:按占比算,如 3 行各 1fr = calc((100% - 40px) / 3 + 10px)
let bottomOffset = '12px';
if (hasBottom && bottomRowHeight) {
const pxMatch = bottomRowHeight.match(/^(\d+)px$/);
if (pxMatch) {
bottomOffset = `${parseInt(pxMatch[1]) + 10}px`;
} else if (bottomRowHeight.endsWith('fr')) {
const rows = (LAYOUT_GRID_SKELETONS[layoutType]?.gridTemplateRows || '').split(' ').filter(Boolean);
const n = rows.length; // 总行数
const gaps = (n - 1) * 10; // 行间距
bottomOffset = `calc((100% - 20px - ${gaps}px) / ${n} + 10px)`;
} }
} }
if (bottomList?.length > 0) {
if (bottom1.includes(layoutType)) {
bottom = 'calc((100% - 16px) / 3 + 8px)';
}
if (bottom2.includes(layoutType)) {
bottom = 'calc((100% - 24px) / 4 + 8px)';
}
} else {
// 没有底部布局时,底部高度为
bottom = '28px';
}
const rle = ['layout6'].includes(layoutType) || bottom != '28px' ? 0 : 1;
const leftHide = leftEle?.classList?.contains('hide');
const rightHide = rightEle?.classList?.contains('hide');
const bottomHide = bottomEle?.classList?.contains('hide');
if (legend) { if (legend) {
legend.style.left = legend.style.left = hasLeft ? `calc(${l} - 10px)` : b;
!leftHide && left.includes(layoutType) && leftList?.length > le ? l : b; legend.style.bottom = bottomOffset;
legend.style.bottom = bottomHide
? '0'
: bottomList?.length > 0
? bottom
: '12px';
} }
if (filter) { if (filter) {
if (layoutType === 'layout10') { filter.style.left = hasLeft ? l : b;
filter.style.left =
!leftHide && left.includes(layoutType) && leftList?.length > 1 ? l : b;
} else {
filter.style.left =
!leftHide &&
left.includes(layoutType) &&
leftList?.length > 0 &&
layoutType !== 'layout17'
? l
: b;
}
} }
if (compassControl) { if (compassControl) {
if (layoutType === 'layout10') { compassControl.style.left = hasLeft ? l : b;
compassControl.style.left =
!leftHide && left.includes(layoutType) && leftList?.length > 1 ? l : b;
} else {
compassControl.style.left =
!leftHide &&
left.includes(layoutType) &&
leftList?.length > 0 &&
layoutType !== 'layout17'
? l
: b;
}
} }
if (controller) { if (controller) {
controller.style.right = controller.style.right = hasRight ? w : '0';
!rightHide && right.includes(layoutType) && rightList?.length > rle controller.style.bottom = hasBottom ? `calc(${bottomOffset} + 10px)` : '0';
? w
: '0';
controller.style.bottom = bottomHide ? '0' : bottom;
} }
if (monitor) { if (monitor) {
monitor.style.right = monitor.style.right = hasRight ? w : '0';
!rightHide && right.includes(layoutType) && rightList?.length > rle
? w
: '0';
// monitor.style.bottom = bottomHide ? '0' : bottom
} }
if (baselayer) { if (baselayer) {
baselayer.style.right = baselayer.style.right = hasRight ? `calc(${w} + 60px)` : '60px';
!rightHide && right.includes(layoutType) && rightList?.length > rle baselayer.style.bottom = hasBottom ? bottomOffset : '0';
? `calc(${w} + 60px)`
: '60px';
baselayer.style.bottom = bottomHide ? '0' : bottom;
} }
}; };

View File

@ -142,7 +142,7 @@ export class MapCesium implements MapInterface {
try { try {
this.containerId = container.id; this.containerId = container.id;
this.containerElement = container; this.containerElement = container;
const token = 'bearer fa8aa37c-1e52-4631-a699-625b4147ace8'; const token = 'bearer b734a443-2c8f-4f4a-8698-44828cc5f709';
this.viewer = new Cesium.Viewer(container, { this.viewer = new Cesium.Viewer(container, {
animation: false, animation: false,

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="map-controller" :style="{ right: drawerOpen ? '480px' : '12px' }"> <div id="map-controller" class="map-controller" :style="{ right: drawerOpen ? '480px' : '12px' }">
<div <div
class="map-controller-group" class="map-controller-group"
v-for="(item, index) in controllers" v-for="(item, index) in controllers"

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mapLegendView" v-show="!uiStore.isRoaming"> <div id="qgc-legendtl" class="mapLegendView" v-show="!uiStore.isRoaming">
<div class="legendTitle"> <div class="legendTitle">
图例 图例
<span class="legendBtn" @click="isOpen = !isOpen"> <span class="legendBtn" @click="isOpen = !isOpen">

View File

@ -30,7 +30,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'; import { ref, onMounted, onUnmounted, watch, nextTick, computed, inject } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { wqGetKendoListCust } from '@/api/sz' import { wqGetKendoListCust } from '@/api/sz'
@ -247,8 +247,12 @@ watch(tabs, (newVal) => {
}); });
} }
}); });
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
(newVal) => { (newVal) => {
if (newVal && newVal.wbsCode) { if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode; baseid.value = newVal.wbsCode;
@ -751,8 +755,9 @@ onMounted(() => {
setTimeout(() => { setTimeout(() => {
initChart(); initChart();
// selectedItem // selectedItem
if (JidiSelectEventStore.selectedItem?.wbsCode) { const currentStation = injectedStation?.value || JidiSelectEventStore.selectedItem;
baseid.value = JidiSelectEventStore.selectedItem.wbsCode; if (currentStation?.wbsCode) {
baseid.value = currentStation.wbsCode;
getEchartsData(); getEchartsData();
} }
}, 100); }, 100);

View File

@ -29,7 +29,8 @@ import {
onMounted, onMounted,
onBeforeUnmount, onBeforeUnmount,
watch, watch,
nextTick nextTick,
inject
} from 'vue'; } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import type { ECharts } from 'echarts'; import type { ECharts } from 'echarts';
@ -727,8 +728,12 @@ onBeforeUnmount(() => {
}); });
}); });
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
async newVal => { async newVal => {
if (!newVal || !newVal.wbsCode) { if (!newVal || !newVal.wbsCode) {
return; return;

View File

@ -44,7 +44,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch, inject, computed } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { environmentalProtectionConstruction } from '@/api/home'; import { environmentalProtectionConstruction } from '@/api/home';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent'; import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
@ -142,9 +142,12 @@ const handleCardClick = (facility: any) => {
modalVisible.value = true; modalVisible.value = true;
}; };
// // 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
(newVal) => { (newVal) => {
if (newVal && newVal.wbsCode) { if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode; baseid.value = newVal.wbsCode;

View File

@ -62,8 +62,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick,watch } from 'vue'; import { ref, onMounted, onUnmounted, nextTick, watch, inject, computed } from 'vue';
// import { ref, watch } from 'vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent'; import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
import LsstjkTk from './LsstjkTk.vue'; import LsstjkTk from './LsstjkTk.vue';
@ -288,9 +287,13 @@ const getSelectData = async () => {
}); });
}; };
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
// ==================== ==================== // ==================== ====================
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
async newVal => { async newVal => {
if (newVal && newVal.wbsCode) { if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode; baseid.value = newVal.wbsCode;

View File

@ -8,6 +8,7 @@ export const useMapViewStore = defineStore('map-view', () => {
const searchTimeRange = ref<[any, any]>([dayjs().subtract(1, 'M'), dayjs()]); const searchTimeRange = ref<[any, any]>([dayjs().subtract(1, 'M'), dayjs()]);
const selectedBaseId = ref(''); const selectedBaseId = ref('');
const currentZoomLevel = ref(4.5); const currentZoomLevel = ref(4.5);
const activeBaseLayerKey = ref('s_province_boundaries');
// 备注:统一写入当前选中的图层 key始终保持去重后的结果。 // 备注:统一写入当前选中的图层 key始终保持去重后的结果。
const setCheckedLayerKeys = (keys: string[] = []) => { const setCheckedLayerKeys = (keys: string[] = []) => {
@ -72,12 +73,18 @@ export const useMapViewStore = defineStore('map-view', () => {
currentZoomLevel.value = 4.5; currentZoomLevel.value = 4.5;
}; };
const setActiveBaseLayerKey = (key: string) => {
activeBaseLayerKey.value = key;
};
return { return {
checkedLayerKeys, checkedLayerKeys,
legendCheckedState, legendCheckedState,
searchTimeRange, searchTimeRange,
selectedBaseId, selectedBaseId,
currentZoomLevel, currentZoomLevel,
activeBaseLayerKey,
setActiveBaseLayerKey,
setCheckedLayerKeys, setCheckedLayerKeys,
getCheckedLayerKeys, getCheckedLayerKeys,
setLegendCheckedState, setLegendCheckedState,

View File

@ -20,7 +20,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'; import { ref, onMounted, onUnmounted, nextTick, watch, inject, computed } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts'; import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
@ -637,8 +637,12 @@ const getselsectData = async () => {
// //
}; };
//// ////
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
newVal => { newVal => {
baseid.value = newVal.wbsCode; baseid.value = newVal.wbsCode;
getselsectData(); getselsectData();

View File

@ -98,7 +98,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, watch } from 'vue'; import { ref, onMounted, watch, inject, computed } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { import {
qgcetQgcStaticData, qgcetQgcStaticData,
@ -301,8 +301,12 @@ const getData = async () => {
spinning.value = false; spinning.value = false;
} }
}; };
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
newVal => { newVal => {
if (newVal && newVal.wbsCode) { if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode; baseid.value = newVal.wbsCode;

View File

@ -61,7 +61,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch, inject, computed } from 'vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent'; import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
import SsstdcgkTk from './SsstdcgkTk.vue'; import SsstdcgkTk from './SsstdcgkTk.vue';
@ -322,9 +322,13 @@ const getSelectData = async () => {
}); });
}; };
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
// ==================== ==================== // ==================== ====================
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
async newVal => { async newVal => {
if (newVal && newVal.wbsCode) { if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode; baseid.value = newVal.wbsCode;

View File

@ -22,7 +22,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { ref, onMounted, onBeforeUnmount, nextTick, watch, inject, computed } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts'; import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
@ -1170,9 +1170,13 @@ watch(
{ immediate: true } { immediate: true }
); );
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
const wbsCode = ref(''); const wbsCode = ref('');
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
newVal => { newVal => {
console.log(newVal); console.log(newVal);
wbsCode.value = newVal.wbsCode; wbsCode.value = newVal.wbsCode;

View File

@ -17,7 +17,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'; import { ref, onMounted, onUnmounted, nextTick, watch, inject, computed } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import type { ECharts } from 'echarts'; import type { ECharts } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
@ -282,8 +282,12 @@ const handlePanelChange1 = async data => {
} }
}; };
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
watch( watch(
() => JidiSelectEventStore.selectedItem, () => stationSource.value,
newVal => { newVal => {
if (newVal && newVal.wbsCode) { if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode; baseid.value = newVal.wbsCode;

View File

@ -103,7 +103,7 @@ const buildGroupedColumns = () => {
const groupMap = new Map<string, ColumnItem[]>(); const groupMap = new Map<string, ColumnItem[]>();
// defaultConfig: true // defaultConfig: true
const filteredList = props.userColumnList.filter(item => item.defaultConfig !== true); const filteredList = props.userColumnList.filter(item => item.enable == 1);
// groupType // groupType
for (const item of filteredList) { for (const item of filteredList) {

View File

@ -481,7 +481,7 @@ const fetchColumnConfig = () => {
// API orderIndex checked 1 // API orderIndex checked 1
const sorted = [...list] const sorted = [...list]
.filter((item: any) => item.checked === 1 && item.defaultConfig !== true) .filter((item: any) => item.checked === 1 && item.enable == 1)
.sort((a: any, b: any) => a.orderIndex - b.orderIndex); .sort((a: any, b: any) => a.orderIndex - b.orderIndex);
const cols = sorted.map((item: any) => { const cols = sorted.map((item: any) => {

View File

@ -97,12 +97,12 @@ const previewVideoUrl = ref('');
useDraggable(imagePreviewVisible, { boundary: true, resetOnOpen: true }); useDraggable(imagePreviewVisible, { boundary: true, resetOnOpen: true });
useDraggable(videoPreviewVisible, { boundary: true, resetOnOpen: true }); useDraggable(videoPreviewVisible, { boundary: true, resetOnOpen: true });
const openImagePreview = (url: string) => { const openImagePreview = (url: string) => {
previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url; previewImageUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL1 + '/?' + url;
imagePreviewVisible.value = true; imagePreviewVisible.value = true;
}; };
const openVideoPreview = (url: string) => { const openVideoPreview = (url: string) => {
previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL + '/?' + url; previewVideoUrl.value = import.meta.env.VITE_APP_ATTACHMENT_URL1 + '/?' + url;
videoPreviewVisible.value = true; videoPreviewVisible.value = true;
}; };

View File

@ -0,0 +1,161 @@
<template>
<SidePanelItem title="社会投资扶贫就业">
<a-spin v-if="loading" class="spin-center" />
<a-empty v-else-if="!hasData" description="暂无数据" class="empty-center" />
<div v-else class="card-list">
<a-card
v-for="item in cardItems"
:key="item.title"
class="stat-card"
hoverable
size="small"
>
<div class="card-content">
<i class="icon iconfont" :class="item.icon" />
<span class="card-title">{{ item.title }}</span>
</div>
<span
class="card-value"
:style="item.color ? { color: item.color } : {}"
>
{{ item.totalNum ?? '-' }}
</span>
</a-card>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, computed, watch, inject } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { getSocialInvestmentData } from '@/api/dianZhanZhuanTi';
defineOptions({ name: 'DZSheHuiTouZiFP' });
// ==================== ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
const cardItems = ref<
Array<{ icon: string; title: string; totalNum: number | string | null; color?: string }>
>([]);
// ==================== API ====================
const fetchData = async () => {
if (!stcd.value) return;
loading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{ field: 'stcd', operator: 'eq', dataType: 'string', value: stcd.value }
]
},
group: [{ dir: 'des', field: 'stcd' }],
aggregate: [
{ aggregate: 'sum', field: 'stjtz' },
{ aggregate: 'sum', field: 'dirjob' },
{ aggregate: 'sum', field: 'indjob' },
{ aggregate: 'sum', field: 'poorjobeff' },
{ aggregate: 'sum', field: 'ldtz' }
]
};
const res = await getSocialInvestmentData(params);
const data = res?.data?.data?.[0];
if (data) {
hasData.value = true;
const { dirjob, indjob, poorjobeff, ldtz, stjtz } = data;
cardItems.value = [
{ icon: 'icon-sum', title: '扶贫就业收益(万元)', totalNum: poorjobeff, color: '#FCA900' },
{ icon: 'icon-people', title: '直接就业人口(人)', totalNum: dirjob },
{ icon: 'icon-people', title: '间接就业人口(人)', totalNum: indjob },
{ icon: 'icon-sum', title: '总投资(万元)', totalNum: stjtz, color: '#FCA900' },
{ icon: 'icon-sum', title: '总拉动投资(万元)', totalNum: ldtz, color: '#FCA900' }
];
} else {
hasData.value = false;
}
} catch {
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
watch(stcd, (newStcd) => {
if (newStcd) {
fetchData();
}
});
</script>
<style scoped>
.spin-center,
.empty-center {
/* display: flex;
align-items: center;
justify-content: center; */
min-height: 197px;
}
.card-list {
display: flex;
flex-direction: column;
gap: 4px;
overflow: auto;
height: 197px;
}
.stat-card {
cursor: default;
width: 100%;
}
.stat-card :deep(.ant-card-body) {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 1px 10px;
}
.card-content {
display: flex;
align-items: center;
width: 50%;
overflow: hidden;
}
.card-content .icon {
font-size: 18px;
flex-shrink: 0;
margin-right: 17px;
color: #2f6b98;
}
.card-title {
font-size: 14px;
color: #000000d9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-value {
width: 50%;
font-size: 24px;
/* font-weight: 600; */
flex-shrink: 0;
margin-left: 8px;
text-align: right;
}
</style>

View File

@ -0,0 +1,498 @@
<template>
<div class="dz-video-monitor">
<!-- 标题栏 -->
<div class="video-header">
<span class="video-title">水电站视频监控</span>
<span class="more-btn" @click="openMoreModal">
<i class="iconfont icon-moreVideo" />
</span>
</div>
<!-- 视频缩略图跑马灯 -->
<div class="video-carousel-wrapper" ref="carouselRef">
<div v-if="videoList.length === 0" class="video-empty">
<a-empty description="暂无视频数据" />
</div>
<template v-else>
<!-- 左箭头 -->
<div
v-if="videoList.length > visibleCount"
class="carousel-arrow arrow-left"
@click="prevSlide"
>
<LeftOutlined />
</div>
<!-- 视频轨道 -->
<div
class="carousel-track"
:style="{ transform: `translateX(-${scrollOffset}px)` }"
>
<div
v-for="item in videoList"
:key="item.vdid"
class="video-item"
:style="{ width: itemWidth + 'px' }"
>
<div class="video-thumb">
<img :src="item.imgPath" :alt="item.vdnm || item.flnm" />
<div class="play-overlay" @click="playVideo(item)">
<i class="iconfont icon-play" />
</div>
</div>
<a-tooltip :title="item.vdnm || item.flnm">
<div class="video-name">{{ item.vdnm || item.flnm }}</div>
</a-tooltip>
</div>
</div>
<!-- 右箭头 -->
<div
v-if="videoList.length > visibleCount"
class="carousel-arrow arrow-right"
@click="nextSlide"
>
<RightOutlined />
</div>
</template>
</div>
<!-- 单个视频播放弹窗 -->
<a-modal
v-model:open="playModalVisible"
title="视频播放"
width="900px"
:footer="null"
destroyOnClose
>
<VideoPlayer
:list="videoList"
:activeFid="activeFid"
:activeMedia="activeMedia"
:loading="false"
:page="1"
:pageSize="20"
:pageSizeOptions="['20', '50', '100']"
:total="videoList.length"
@select="handleSelectMedia"
/>
</a-modal>
<!-- 更多视频弹窗 -->
<a-modal
v-model:open="moreModalVisible"
title="视频监控"
width="90%"
:footer="null"
destroyOnClose
>
<VideoInfo :is-active="moreModalVisible" />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import {
ref,
inject,
watch,
onMounted,
onUnmounted,
type Ref
} from 'vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import VideoPlayer from '@/modules/yunXingGaoJIng/Aisbdbyx/components/VideoPlayer.vue';
import VideoInfo from '@/components/MapModal/components/videoInfo.vue';
import { videoSurveillance, realTimeVideo } from '@/api/video';
import { useModelStore } from '@/store/modules/model';
import dayjs from 'dayjs';
const modelStore = useModelStore();
defineOptions({
name: 'DZShuiDianZhanSPJK'
});
interface VideoItem {
vdid: string;
vdnm?: string;
flnm?: string;
imgPath: string;
url?: string;
/** VideoPlayer 兼容字段 */
fid: string;
type: string;
flpth: string;
tm: string;
ennm: string;
}
interface DianZhanStation {
wbsCode: string;
stcd: string;
ennm: string;
lgtd: number;
lttd: number;
}
const dianZhanStation = inject<Ref<DianZhanStation | null>>('dianZhanStation');
//
const videoList = ref<VideoItem[]>([]);
//
const carouselRef = ref<HTMLElement | null>(null);
const visibleCount = ref(4);
const itemWidth = ref(160);
const scrollOffset = ref(0);
const currentSlide = ref(0);
//
const playModalVisible = ref(false);
const activeFid = ref('');
const activeMedia = ref<{ type: string; src: string }>({ type: '', src: '' });
//
const moreModalVisible = ref(false);
//
const updateVisibleCount = () => {
if (carouselRef.value) {
const width = carouselRef.value.offsetWidth;
visibleCount.value = Math.max(
1,
Math.floor((width - 60) / (itemWidth.value + 8))
);
}
};
//
const nextSlide = () => {
const maxSlide = Math.max(0, videoList.value.length - visibleCount.value);
if (currentSlide.value < maxSlide) {
currentSlide.value++;
scrollOffset.value = currentSlide.value * (itemWidth.value + 8);
}
};
const prevSlide = () => {
if (currentSlide.value > 0) {
currentSlide.value--;
scrollOffset.value = currentSlide.value * (itemWidth.value + 8);
}
};
//
const playVideo = (item: VideoItem) => {
setActiveVideo(item);
playModalVisible.value = true;
};
//
const setActiveVideo = (item: VideoItem) => {
activeFid.value = item.fid;
activeMedia.value = {
type: 'video',
src: item.flpth || item.url || ''
};
};
// VideoPlayer
const handleSelectMedia = (item: any) => {
activeFid.value = item.fid;
activeMedia.value = {
type: 'video',
src: item.flpth || item.src || ''
};
// debugger
};
//
const openMoreModal = () => {
moreModalVisible.value = true;
// stcd modelStoreVideoInfo modelStore.params.stcd
if (dianZhanStation?.value?.stcd) {
modelStore.params.stcd = dianZhanStation.value.stcd;
}
};
//
const fetchVideoData = async (stcd: string) => {
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: stcd
},
{
field: 'sttpCode',
operator: 'in',
dataType: 'string',
value: ['VD_FBFM', 'VD_EQS']
}
]
}
};
//
const surveillanceRes = await videoSurveillance(params);
const stcdList = surveillanceRes?.data?.data || [];
if (!stcdList.length) {
videoList.value = [];
return;
}
// stcd
const stcds = stcdList.map((item: any) => item.stcd).filter(Boolean);
if (!stcds.length) {
videoList.value = [];
return;
}
//
const timeRange = [
dayjs().subtract(7, 'day').format('YYYY-MM-DD 00:00:00'),
dayjs().format('YYYY-MM-DD 23:59:59')
];
const params1 = {
take: 100,
skip: 0,
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: stcds
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: timeRange[0]
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: timeRange[1]
}
// {
// field: 'tm',
// operator: 'gte',
// dataType: 'date',
// value: '2024-05-01 00:00:00'
// },
// {
// field: 'tm',
// operator: 'lte',
// dataType: 'date',
// value: '2026-05-31 23:59:59'
// }
]
},
sort: [
{
field: 'tm',
dir: 'desc'
}
]
};
const videoRes = await realTimeVideo(params1);
const videoData = videoRes?.data?.data || videoRes?.data || [];
// debugger
const videoItems: VideoItem[] = (
Array.isArray(videoData) ? videoData : []
).map((item: any) => ({
vdid: item.vdid,
vdnm: item.vdnm,
flnm: item.flnm,
imgPath: item.imgPath || '',
url: item.url || item.flv || item.hls,
// VideoPlayer
fid: item.vdid,
type: 'vdsp',
flpth: item.url || item.flv || item.hls || '',
tm: item.tm ? dayjs(item.tm).format('YYYY-MM-DD HH:mm:ss') : '-',
ennm: item.ennm || ''
}));
videoList.value = videoItems;
// debugger
currentSlide.value = 0;
scrollOffset.value = 0;
} catch (error) {
console.error('获取视频数据失败:', error);
videoList.value = [];
}
};
//
watch(
() => dianZhanStation?.value,
newVal => {
if (newVal?.stcd) {
fetchVideoData(newVal.stcd);
}
},
{ deep: true, immediate: true }
);
onMounted(() => {
updateVisibleCount();
window.addEventListener('resize', updateVisibleCount);
});
onUnmounted(() => {
window.removeEventListener('resize', updateVisibleCount);
});
</script>
<style lang="scss" scoped>
.dz-video-monitor {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: rgba(255, 255, 255);
.video-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px 12px 3px 0px;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
background: #e5edf3;
.video-title {
font-size: 16px;
color: #2f6b98;
font-weight: 500;
display: inline-block;
border-left:2px solid #2f6b98;
padding-left: 16px;
}
.more-btn {
cursor: pointer;
font-size: 18px;
color: #666;
transition: color 0.2s;
&:hover {
color: #005293;
}
}
}
.video-carousel-wrapper {
flex: 1;
display: flex;
align-items: center;
position: relative;
overflow: hidden;
padding: 0 28px;
.video-empty {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.carousel-track {
display: flex;
gap: 8px;
transition: transform 0.3s ease;
align-items: center;
height: 100%;
.video-item {
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
.video-thumb {
position: relative;
width: 100%;
height: 110px;
background: #1a1a1a;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.play-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
cursor: pointer;
i {
font-size: 16px;
color: #fff;
}
}
}
.video-name {
padding: 4px 6px;
font-size: 12px;
color: #333;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.carousel-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 5;
width: 24px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
color: #fff;
cursor: pointer;
border-radius: 2px;
transition: background 0.2s;
&:hover {
background: rgba(0, 0, 0, 0.5);
}
&.arrow-left {
left: 2px;
}
&.arrow-right {
right: 2px;
}
}
}
}
</style>

View File

@ -0,0 +1,299 @@
<template>
<SidePanelItem title="生态流量数据分析">
<div class="ecological-flow">
<!-- 底部图表区 -->
<div class="bottom-section">
<a-spin v-show="hasData" :spinning="isChartLoading" tip="加载中...">
<div class="chart-wrapper" ref="chartRef"></div>
</a-spin>
<a-empty
v-show="!hasData && !isChartLoading"
description="暂无数据"
class="empty-wrapper h-full flex items-center justify-center"
style="flex-direction: column"
/>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick, inject } from 'vue';
import * as echarts from 'echarts';
import { getMonitorDataWaterTempVerticalDayList } from '@/api/mapModal';
import dayjs from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
defineOptions({
name: 'EcologicalFlow'
});
//
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
const hasLoaded = ref<boolean>(false);
const isChartLoading = ref<boolean>(false);
const hasData = ref<boolean>(true);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// 7
const getDateRange = (): { startTime: string; endTime: string } => {
const endDate = dayjs();
const startDate = dayjs().subtract(7, 'day');
return {
startTime: startDate.format('YYYY-MM-DD HH:mm:ss'),
endTime: endDate.format('YYYY-MM-DD HH:mm:ss')
};
};
//
const updateChart = (data: any[]) => {
if (!chartInstance) return;
if (!data || data.length === 0) {
chartInstance.clear();
return;
}
const sorted = [...data].sort(
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
);
const xAxisData = sorted.map(item => dayjs(item.dt).format('YYYY-MM-DD'));
const qiData = sorted.map(item => item.qi);
const qecData = sorted.map(item => item.qec);
const qecLimitData = sorted.map(item => item.qecLimit);
const beforeQecData = sorted.map(item => item.beforeQec);
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(50, 50, 50, 0.9)',
textStyle: { color: '#fff', fontSize: 12 },
axisPointer: { type: 'cross' },
formatter: (params: any) => {
if (!params || params.length === 0) return '';
const dataIndex = params[0].dataIndex;
const fullTime = sorted[dataIndex]?.dt
? dayjs(sorted[dataIndex].dt).format('YYYY-MM-DD')
: '';
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
const formatRules: Record<
string,
{ format: (v: number) => string; unit: string }
> = {
入库流量: { format: v => String(Math.round(v)), unit: '(m³/s)' },
生态流量: { format: v => v.toFixed(1), unit: '(m³/s)' },
生态流量限值: { format: v => v.toFixed(1), unit: '(m³/s)' },
};
params.forEach((param: any) => {
if (param.value == null) return;
const rule = Object.entries(formatRules).find(([key]) =>
param.seriesName.includes(key)
);
const displayValue = rule
? rule[1].format(Number(param.value))
: param.value;
const unit = rule ? rule[1].unit : '';
html += `
<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
<span style="flex:1;font-size:14px;text-align:left;margin-right:6px;">${param.seriesName}: </span>
<span style="font-size:14px;min-width:60px;text-align:right;"><strong>${displayValue}</strong> ${unit}</span>
</div>
`;
});
return html;
}
},
legend: {
type: 'scroll',
top: -5,
data: ['入库流量', '生态流量', '生态流量限值'],
textStyle: { fontSize: 12, color: '#000' }
},
grid: {
left: 40,
right: 20,
top: 50,
bottom: 30
},
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#000000' } },
axisTick: { show: false },
axisLabel: { fontSize: 12 },
splitLine: {
show: true,
lineStyle: { color: '#bfbfbf', type: 'solid' }
}
},
yAxis: {
name: '流量(m³/s)',
type: 'value',
position: 'left',
axisLine: { lineStyle: { color: '#000000' } },
scale: true,
splitNumber: 9,
boundaryGap: ['10%', 0]
},
series: [
{
name: '入库流量',
type: 'line',
data: qiData,
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: { color: '#4B79AB', width: 2 },
itemStyle: { color: '#4B79AB' }
},
{
name: '生态流量',
type: 'line',
data: qecData,
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: { color: '#00A050', width: 2 },
itemStyle: { color: '#00A050' }
},
{
name: '生态流量限值',
type: 'line',
data: qecLimitData,
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: { color: '#F7A737', width: 2 },
itemStyle: { color: '#F7A737' }
},
],
dataZoom: [
{
type: 'inside',
xAxisIndex: [0],
throttle: 50,
start: 0,
end: 100
}
],
};
chartInstance.setOption(option, true);
};
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) {
chartInstance.dispose();
}
nextTick(() => {
chartInstance = echarts.init(chartRef.value);
});
};
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
};
// 7
const fetchChartData = async () => {
if (!stcd.value) return;
isChartLoading.value = true;
try {
const { startTime, endTime } = getDateRange();
const res = await getMonitorDataWaterTempVerticalDayList({
filter: {
logic: 'and',
filters: [
{ field: 'stcd', operator: 'eq', dataType: 'string', value: stcd.value },
// { field: 'tm', operator: 'gte', dataType: 'date', value: '2021-01-01' },
// { field: 'tm', operator: 'lte', dataType: 'date', value: '2026-07-01' }
{ field: 'tm', operator: 'gte', dataType: 'date', value: startTime },
{ field: 'tm', operator: 'lte', dataType: 'date', value: endTime }
]
},
sort: [{ field: 'tm', dir: 'asc' }]
});
const rawData = res?.data?.data || [];
hasData.value = rawData.length > 0;
if (hasData.value) {
updateChart(rawData);
}
hasLoaded.value = true;
} catch (error) {
console.error('获取图表数据失败:', error);
} finally {
isChartLoading.value = false;
}
};
//
watch(
() => dianZhanStation.value?.stcd,
(newStcd, oldStcd) => {
if (newStcd && newStcd !== oldStcd) {
hasLoaded.value = false;
destroyChart();
nextTick(() => {
initChart();
fetchChartData();
});
window.addEventListener('resize', handleResize);
}
},
{ immediate: true }
);
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
destroyChart();
window.removeEventListener('resize', handleResize);
});
</script>
<style lang="scss" scoped>
.ecological-flow {
.bottom-section {
height: 213px;
background: #fff;
position: relative;
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
.chart-wrapper {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,381 @@
<template>
<div class="detail-container">
<BasicSearch
ref="formRef"
:searchList="searchList"
:initialValues="initialValues"
@finish="handleSearch"
@reset="handleReset"
>
<template #actions>
<a-button @click="handleExport" :loading="exportLoading">
<template #icon><DownloadOutlined /></template>
导出
</a-button>
</template>
</BasicSearch>
<BasicTable
ref="tableRef"
:columns="columns"
:listUrl="getPowerStationReleaseDetail"
:searchParams="tableSearchParams"
:transformData="customTransform"
:defaultPageSize="20"
:scrollY="400"
>
<template #ennm="{ record }">
<a @click="handleStationClick(record)">{{ record.ennm }}</a>
</template>
<template #fid="{ record }">
<a
v-if="record.fid && record.fid.length > 1"
@click="handlePreview(record.fid)"
>查看</a
>
<span v-else class="no-data-link">查看</span>
</template>
</BasicTable>
<!-- 视频预览弹窗 -->
<a-modal
v-model:open="previewVisible"
title="视频资料"
:footer="null"
width="800px"
>
<template v-if="previewData && previewData.length">
<a-empty v-if="!previewVisible" description="暂无视频数据" />
</template>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, reactive, onMounted, nextTick } from 'vue';
import { DownloadOutlined } from '@ant-design/icons-vue';
import dayjs from 'dayjs';
import BasicSearch from '@/components/BasicSearch/index.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getPowerStationReleaseDetail } from '@/api/dianZhanZhuanTi';
import { useModelStore } from '@/store/modules/model';
import { message } from 'ant-design-vue';
import { getDictItemsByCode } from '@/api/dict';
const props = defineProps<{
year: string;
stcdSelectOptions: { label: string; value: string }[];
stcd: string;
}>();
const currentBaseId = (window as any).__lyConfigs?.baseId ?? '02';
// ==================== ====================
const runStateList: any = ref([]);
const getdict = async () => {
const res = await getDictItemsByCode({ dictCode: 'EngFl' });
runStateList.value = (res.data || []).map((item: any) => ({
label: item.dictName,
value: item.itemCode
}));
runStateList.value.unshift({
label: '全部',
value: 'all'
});
};
onMounted(() => {
getdict();
nextTick(() => {
tableRef.value?.getList(buildFilter.value);
});
});
// ==================== ====================
const formRef = ref();
const tableRef = ref();
const exportLoading = ref(false);
const state = reactive({
years: props.year,
stcd: props.stcd,
runState: 'all',
search: ''
});
// ==================== ====================
const previewVisible = ref(false);
const previewData = ref<string[]>([]);
function handlePreview(fid: string) {
if (fid && fid.length > 1) {
previewData.value = [fid];
previewVisible.value = true;
}
}
// ==================== ====================
const searchList = computed(() => [
{
type: 'DataPicker',
name: 'years',
label: '年份',
picker: 'year',
fieldProps: {
allowClear: false,
format: 'YYYY',
valueFormat: 'YYYY'
}
},
{
type: 'Select',
name: 'stcd',
label: '所属电站',
options: props.stcdSelectOptions
},
{
type: 'Select',
name: 'runState',
label: '工作开展情况',
options: runStateList.value
},
{
type: 'Input',
name: 'search',
label: '',
placeholder: '请输入电站名称,实际放鱼种类'
}
]);
const initialValues = computed(() => ({
years: props.year,
stcd: props.stcd,
runState: 'all',
search: ''
}));
// ==================== / ====================
function handleSearch(values: any) {
state.years = values.years ? dayjs(values.years).format('YYYY') : '';
state.stcd = values.stcd ?? '';
state.runState = values.runState ?? 'all';
state.search = values.search ?? '';
tableRef.value?.getList(buildFilter.value);
}
function handleReset() {
state.years = props.year;
state.stcd = props.stcd;
state.runState = 'all';
state.search = '';
}
// ==================== ====================
const buildFilter = computed(() => {
const { search, runState, years, stcd } = state;
return {
logic: 'and',
filters: [
currentBaseId && currentBaseId !== 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: currentBaseId
}
: null,
runState != 'all'
? {
field: 'runState',
operator: 'eq',
dataType: 'string',
value: runState
}
: null,
years
? {
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: dayjs(years).startOf('year').format('YYYY-MM-DD HH:mm:ss')
}
: null,
years
? {
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: dayjs(years).endOf('year').format('YYYY-MM-DD HH:mm:ss')
}
: null,
stcd
? { field: 'stcd', operator: 'eq', dataType: 'string', value: stcd }
: null,
search
? {
field: 'ennm',
operator: 'contains',
dataType: 'string',
value: search
}
: null,
{
field: 'bldsttCcode',
operator: 'in',
dataType: 'string',
value: [1, 2]
}
].filter(Boolean)
};
});
const tableSearchParams = computed(() => ({
filter: buildFilter.value
}));
const customTransform = (res: any) => ({
records: res?.data?.data || res?.data?.items || [],
total: res?.data?.total || 0
});
// ==================== ====================
const columns = [
{
title: '电站名称',
dataIndex: 'ennm',
key: 'ennm',
width: 150,
ellipsis: true,
merge: true,
slots: { customRender: 'ennm' }
},
{
title: '实际放鱼时间',
dataIndex: 'tm',
key: 'tm',
width: 160,
ellipsis: true
},
{
title: '实际放鱼种类',
dataIndex: 'ftpName',
key: 'ftpName',
width: 160,
ellipsis: true
},
{
title: '实际放鱼数量(万尾)',
dataIndex: 'fcntjc',
key: 'fcntjc',
width: 160,
ellipsis: true
},
{
title: '放鱼平均规格(cm)',
dataIndex: 'fsz',
key: 'fsz',
width: 160,
ellipsis: true
},
{
title: '计划放鱼时段',
dataIndex: 'timeSlot',
key: 'timeSlot',
width: 160,
ellipsis: true,
merge: true
},
{
title: '计划放鱼种类',
dataIndex: 'planFtpName',
key: 'planFtpName',
width: 160,
ellipsis: true,
merge: true
},
{
title: '计划放鱼数量(万尾)',
dataIndex: 'fcntjh',
key: 'fcntjh',
width: 160,
ellipsis: true,
merge: true
},
{
title: '放流地点',
dataIndex: 'rplace',
key: 'rplace',
width: 160,
ellipsis: true
},
{
title: '所在河流',
dataIndex: 'river',
key: 'river',
width: 160,
ellipsis: true
},
{
title: '工作开展情况',
dataIndex: 'runStateName',
key: 'runStateName',
width: 120,
ellipsis: true,
merge: true
},
{
title: '视频资料',
dataIndex: 'fid',
key: 'fid',
width: 100,
ellipsis: true,
fixed: 'right',
slots: { customRender: 'fid' }
}
];
// ==================== ====================
const modelStore = useModelStore();
function handleStationClick(record: any) {
modelStore.params = {
...modelStore.params,
sttp: 'ENG',
stcd: record.stcd,
sttpCode: record.sttpCode || 'ENG'
};
modelStore.title = record.ennm ? `${record.ennm} ` : '';
modelStore.modalVisible = true;
}
// ==================== ====================
function handleExport() {
if (exportLoading.value) return;
exportLoading.value = true;
if (tableRef.value && typeof tableRef.value.exportTable === 'function') {
tableRef.value
.exportTable({
fileName: `电站放流详情_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
});
} else {
message.info('暂无导出数据');
exportLoading.value = false;
}
}
</script>
<style scoped lang="scss">
.detail-container {
display: flex;
flex-direction: column;
height: 100%;
}
.no-data-link {
color: #d9d9d9;
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,280 @@
<template>
<SidePanelItem title="水电站介绍">
<div class="station-intro">
<a-empty v-if="mediaData.length === 0" description="暂无介绍信息" />
<template v-else>
<div
class="container"
@mouseenter="isHovering = true"
@mouseleave="isHovering = false"
>
<div
class="carousel-track"
:class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
>
<div
v-for="(item, index) in renderMediaData"
:key="index"
class="carousel-item"
@click="openDetail"
>
<img :src="item.image" :alt="item.title" />
</div>
</div>
<div class="pagination-dots-fixed">
<span
v-for="(dot, index) in mediaData"
:key="index"
class="dot"
:class="{ active: getCurrentRealIndex() === index }"
@click="goToSlide(index)"
></span>
</div>
</div>
<div class="description-text">
{{ currentDescription }}
</div>
</template>
</div>
<!-- 详情弹框 -->
<a-modal v-model:open="modalVisible" title="设施详情" width="80%" :footer="null">
<div v-if="detailDataSource.length" class="detail-container">
<ArtsDetail :dataSource="detailDataSource" :index="0" />
</div>
</a-modal>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { inject, watch, type Ref, ref, computed, onMounted, onUnmounted } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
import { overviewvmsstbprptGetKendoList } from '@/api/home';
defineOptions({
name: 'ShuiDianZhanJieShao'
});
interface MediaItem {
image: string;
title: string;
description: string;
}
interface DianZhanStation {
wbsCode: string;
stcd: string;
ennm: string;
lgtd: number;
lttd: number;
}
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
const dianZhanStation = inject<Ref<DianZhanStation | null>>('dianZhanStation');
//
const modalVisible = ref(false);
//
const mediaData = ref<MediaItem[]>([]);
const renderMediaData = ref<MediaItem[]>([]);
const currentIndex = ref(1);
const isHovering = ref(false);
const isTransitioning = ref(false);
let timer: any = null;
const detailDataSource = computed(() => {
return mediaData.value.map(item => ({
url: item.image || '',
description: item.description || '',
title: item.title || ''
}));
});
const currentDescription = computed(() => {
const realIndex = getCurrentRealIndex();
return mediaData.value[realIndex]?.description || '';
});
const getCurrentRealIndex = () => {
const len = mediaData.value.length;
if (len === 0) return 0;
let realIndex = currentIndex.value - 1;
if (realIndex < 0) realIndex = len - 1;
if (realIndex >= len) realIndex = 0;
return realIndex;
};
const initRenderData = () => {
const len = mediaData.value.length;
if (len === 0) {
renderMediaData.value = [];
return;
}
if (len === 1) {
renderMediaData.value = [...mediaData.value];
currentIndex.value = 0;
return;
}
renderMediaData.value = [
mediaData.value[len - 1],
...mediaData.value,
mediaData.value[0]
];
currentIndex.value = 1;
};
const goToSlide = (targetIndex: number) => {
if (isTransitioning.value || mediaData.value.length <= 1) return;
currentIndex.value = targetIndex + 1;
};
const startAutoPlay = () => {
if (timer) clearInterval(timer);
if (mediaData.value.length <= 1) return;
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value) {
currentIndex.value++;
}
}, 4000);
};
const openDetail = () => {
if (mediaData.value.length) {
modalVisible.value = true;
}
};
const fetchStationInfo = async (stcd: string) => {
try {
const params = {
filter: {
logic: 'and',
filters: [
{ field: 'stcd', operator: 'eq', dataType: 'string', value: stcd },
{ field: 'sttpCode', operator: 'eq', dataType: 'string', value: 'ENG' }
]
},
sort: [{ field: 'siteStepSort', dir: 'asc' }],
select: ['inffile', 'introduce', 'ennm', 'logo', 'precis']
};
const res = await overviewvmsstbprptGetKendoList(params);
const dataList = res?.data?.data;
if (dataList && Array.isArray(dataList)) {
mediaData.value = dataList
.filter((item: any) => item.inffile || item.introduce)
.map((item: any) => ({
image: item.inffile ? `${baseUrl}?${item.inffile}&view=jpg` : '',
title: item.ennm || '',
description: item.introduce || ''
}));
} else {
mediaData.value = [];
}
initRenderData();
startAutoPlay();
} catch {
mediaData.value = [];
initRenderData();
}
};
watch(
() => dianZhanStation?.value,
(newVal) => {
if (newVal?.stcd) {
fetchStationInfo(newVal.stcd);
}
},
{ deep: true, immediate: true }
);
onUnmounted(() => {
if (timer) clearInterval(timer);
});
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 228px;
position: relative;
overflow: hidden;
.carousel-track {
display: flex;
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out;
&.no-transition {
transition: none;
}
.carousel-item {
min-width: 100%;
height: 100%;
position: relative;
flex-shrink: 0;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.pagination-dots-fixed {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
gap: 6px;
z-index: 10;
.dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #d8d8d8;
cursor: pointer;
transition: background-color 0.3s ease;
&.active {
background-color: #005293;
}
&:hover {
opacity: 0.8;
}
}
}
}
.description-text {
font-size: 14px;
line-height: 1.5;
transition: all 0.3s ease;
margin-top: 12px;
min-height: 44px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
.detail-container {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,464 @@
<template>
<SidePanelItem
title="珍稀植物园运行数据"
:moreSelect="moreSelect"
:select="select"
@update-values="handlePanelChange"
>
<!-- 加载状态 -->
<a-spin v-if="cardLoading" class="spin-center" />
<!-- 空数据 -->
<a-empty
v-else-if="!hasCardData"
description="暂无数据"
class="empty-center"
/>
<!-- 左右卡片 -->
<div v-else class="card-wrapper" @click="openModal">
<div class="card-item card-left">
<div class="card-icon">
<i class="icon iconfont icon-chengliu"></i>
</div>
<span class="card-name"
>{{ showData.botanyName || ''
}}{{ showData.unitName ? `(${showData.unitName})` : '' }}</span
>
<h5 class="card-count">{{ showData.tamarixChinensis ?? '-' }}</h5>
<img :src="arrowUpSvg" alt="" class="arrow-icon" />
<div class="card-rate">{{ showData.tamarixChinensisLive ?? '-' }}</div>
<p class="card-label">存活率</p>
</div>
<div class="card-item card-right">
<div class="card-icon"> <i class="icon iconfont icon-zhenxizhiwuyuan"></i></div>
<span class="card-name">其他</span>
<h5 class="card-count">{{ showData.other || 0 }}</h5>
<img :src="arrowUpSvg" alt="" class="arrow-icon" />
<div class="card-rate">{{ showData.otherLive ?? '-' }}</div>
<p class="card-label">存活率</p>
</div>
</div>
<!-- 详情弹窗 -->
<a-modal
v-model:open="modalVisible"
title="珍稀植物园运行数据列表"
:footer="null"
width="80%"
:destroyOnClose="true"
:centered="true"
class="detail-modal"
>
<div class="search-form">
<a-space size="middle">
<div>年份:</div>
<a-select
v-model:value="modalYear"
style="width: 100px"
:options="select.options"
@change="onModalSearch"
/>
<a-button type="primary" @click="onModalSearch">查询</a-button>
</a-space>
</div>
<BasicTable
ref="tableRef"
:list-url="getBotanicalGardenDetailData"
:columns="detailColumns"
:scrollY="400"
:search-params="modalSearchParams"
:transform-data="detailTransform"
/>
</a-modal>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, computed, watch, inject, nextTick } from 'vue';
import { message } from 'ant-design-vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
getBotanicalGardenList,
getBotanicalGardenStatistic,
getBotanicalGardenDetailData
} from '@/api/ZXZWYYunXingShuJu';
import arrowUpSvg from './arrowUpLg.svg';
defineOptions({ name: 'ZXZWYYunXingShuJu' });
// ==================== ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const apiId = computed(() => dianZhanStation.value?.stcd || '');
// ==================== SidePanelItem ====================
const isInitializing = ref(false);
/** moreSelect: 植物园(标题栏树形选择器) */
const moreSelect = ref({
show: true,
value: undefined as string | undefined,
options: [] as any[]
});
/** select: 年份(标题栏普通选择器) */
const select = ref({
show: true,
value: undefined as string | undefined,
options: [] as any[],
width: '120px',
minWidth: '120px'
});
/** 各地植物园的年份列表:{ [stcd]: year[] } */
const yearMap = ref<Record<string, string[]>>({});
// ==================== ====================
const cardLoading = ref(false);
const hasCardData = ref(false);
const showData = ref({
botanyName: '',
unitName: '',
tamarixChinensis: '-' as string | number,
tamarixChinensisLive: '-' as string | number,
other: 0 as string | number,
otherLive: '-' as string | number
});
// ==================== Modal ====================
const modalVisible = ref(false);
const modalYear = ref<string>();
const tableRef = ref<any>(null);
/** 当前选中的植物园 stcd从 moreSelect.options 中找) */
const currentGardenStcd = computed(() => {
const val = moreSelect.value.value;
const options = moreSelect.value.options || [];
const found = (options as any[]).find((o: any) => o.value === val);
return found?.stcd || '';
});
const modalSearchParams = computed(() => ({
group: [],
groupResultFlat: false
}));
// ==================== ====================
const detailColumns = [
{
key: 'stnm',
title: '植物园名称',
dataIndex: 'stnm',
width: '150px',
ellipsis: true
},
{ key: 'tm', title: '年份', dataIndex: 'tm', width: '80px' },
{ key: 'tetp', title: '保护种类', dataIndex: 'tetp', width: '120px' },
{ key: 'unitName', title: '单位', dataIndex: 'unitName', width: '80px' },
{
key: 'tecntDisplay',
title: '实际种植/种植要求',
dataIndex: 'tecntDisplay',
width: '140px'
},
{
key: 'surnumDisplay',
title: '存活数量/移栽规模',
dataIndex: 'surnumDisplay',
width: '140px'
},
{ key: 'rate', title: '存活率(%)', dataIndex: 'rate', width: '100px' }
];
/** 预处理详情数据:拼接种植/存活字段 */
const detailTransform = (res: any) => {
const records = (res?.data?.data || []).map((item: any) => ({
...item,
tecntDisplay: `${item.tecnt ?? '-'}/${item.plantask ?? '-'}`,
surnumDisplay: `${item.surnum ?? '-'}/${item.transplant ?? '-'}`
}));
return { records, total: res?.data?.total || 0 };
};
// ==================== API ====================
/** 获取植物园下拉列表 */
const fetchGardenList = async () => {
if (!apiId.value) return;
isInitializing.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: apiId.value
}
]
},
group: [
{
dir: 'des',
field: 'year'
},
{
dir: 'des',
field: 'stcd'
},
{
dir: 'des',
field: 'name'
}
],
groupResultFlat: 'true'
};
const res = await getBotanicalGardenList(params);
const list = res?.data?.data?.[0]?.data || [];
if (list.length === 0) return;
const gardenMap = new Map<string, { stcd: string; stnm: string }>();
const yearMapTemp: Record<string, string[]> = {};
list.forEach((item: any) => {
if (!gardenMap.has(item.stcd)) {
gardenMap.set(item.stcd, { stcd: item.stcd, stnm: item.name });
}
if (!yearMapTemp[item.stcd]) {
yearMapTemp[item.stcd] = [];
}
yearMapTemp[item.stcd].push(item.year);
});
yearMap.value = yearMapTemp;
// moreSelect 使field-names label 'title'
const gardens = Array.from(gardenMap.values());
moreSelect.value.options = gardens.map(g => ({
title: g.stnm,
value: g.stnm,
stcd: g.stcd
}));
//
const firstGarden = gardens[0];
moreSelect.value.value = firstGarden?.stnm;
updateYearOptions(firstGarden?.stcd);
} catch {
message.error('获取植物园列表失败');
} finally {
isInitializing.value = false;
}
};
/** 根据植物园 stcd 更新年份选项 */
const updateYearOptions = (stcd: string) => {
const years = yearMap.value[stcd] || [];
select.value.options = years.map(y => ({ label: y, value: y }));
select.value.value = years[0];
};
/** 获取卡片统计数据 */
const fetchStatistic = async () => {
const year = select.value.value;
const stcd = currentGardenStcd.value;
if (!year || !stcd) return;
cardLoading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'tm',
operator: 'eq',
dataType: 'string',
value: year
},
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
}
]
}
};
const res = await getBotanicalGardenStatistic(params);
const list = res?.data?.data?.[0]?.data || [];
hasCardData.value = list.length > 0;
if (list.length > 0) {
const result = list.reduce((acc: any, cur: any) => {
if (cur.name === '其他') {
acc.other = cur.tecnt;
acc.otherLive = cur.rate;
} else {
acc.botanyName = cur.name;
acc.tamarixChinensis = cur.tecnt;
acc.tamarixChinensisLive = cur.rate;
acc.unitName = cur.unitName;
}
return acc;
}, {});
showData.value = {
botanyName: result.botanyName || '',
unitName: result.unitName || '',
tamarixChinensis: result.tamarixChinensis ?? '-',
tamarixChinensisLive: result.tamarixChinensisLive ?? '-',
other: result.other ?? 0,
otherLive: result.otherLive ?? '-'
};
}
} catch {
hasCardData.value = false;
} finally {
cardLoading.value = false;
}
};
// ==================== ====================
/** SidePanelItem 控件变化回调 */
const handlePanelChange = async (data: any) => {
if (isInitializing.value) return;
//
if (
data.moreSelect !== undefined &&
data.moreSelect !== moreSelect.value.value
) {
moreSelect.value.value = data.moreSelect;
const stcd = (moreSelect.value.options as any[]).find(
(o: any) => o.value === data.moreSelect
)?.stcd;
updateYearOptions(stcd);
return; // select.value.value updateYearOptions handlePanelChange
}
//
if (data.select !== undefined && data.select !== select.value.value) {
select.value.value = data.select;
fetchStatistic();
}
};
/** 打开详情弹窗 */
const openModal = () => {
modalYear.value = select.value.value;
modalVisible.value = true;
nextTick(() => {
onModalSearch();
});
};
/** 弹窗内搜索 */
const onModalSearch = () => {
if (!modalYear.value || !currentGardenStcd.value) return;
tableRef.value?.getList({
endTime: [{ field: 'tm', operator: 'eq', value: modalYear.value }],
stcd: currentGardenStcd.value
});
};
// ==================== ====================
watch(apiId, newId => {
if (newId) {
fetchGardenList();
}
}, { immediate: true });
</script>
<style scoped>
.spin-center,
.empty-center {
min-height: 197px;
}
.card-wrapper {
display: flex;
gap: 8px;
cursor: pointer;
min-height: 197px;
padding: 0px 5px;
}
.card-icon{
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #2f6b98;
display: flex;
align-items: center;
justify-content: center;
}
.card-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
border: 1px solid #e8e8e8;
border-radius: 4px;
transition: box-shadow 0.2s;
position: relative;
}
.card-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-left .icon {
font-size: 20px;
color: #fff;
}
.card-right .icon {
font-size: 20px;
color: #fff;
}
.card-name {
font-size: 14px;
color: #3E3E3E;
margin-top: 2px;
}
.card-count {
font-size: 24px;
line-height: 32px;
color: #2f6b98;
margin-bottom: 12px;
}
.arrow-icon {
width: 61px;
height: 64px;
margin-top: 2px;
}
.card-rate {
font-size: 14px;
color: #2f6b98;
position: absolute;
left: 50%;
top: 62%;
transform: translate(-50%, 0%);
}
.card-label {
font-size: 14px;
color: #3E3E3E;
text-indent:0 !important;
/* margin: 0; */
}
.detail-modal .search-form {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,292 @@
<template>
<!-- 详情弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="mFilter?.ctitle || '详情'"
width="80%"
:footer="null"
:bodyStyle="{ height: '700px' }"
centered
destroyOnClose
@cancel="handleModalCancel"
>
<!-- Tab 切换 -->
<div class="modal-tabs" v-if="mFilter?.proTable?.length">
<div
v-for="item in mFilter.proTable"
:key="item.key"
class="modal-tabs__item"
:class="{ 'modal-tabs__item--active': activeTab?.key === item.key }"
@click="switchTab(item)"
>
{{ item.title }}
</div>
</div>
<!-- 筛选表单 -->
<BasicSearch
v-if="activeTab && searchList.length"
:key="activeTab.key"
:searchList="searchList"
@finish="handleTableSearch"
@reset="handleTableReset"
/>
<!-- 数据表格 -->
<BasicTable
v-if="activeTab"
:key="activeTab.key"
ref="tableRef"
:columns="tableColumns"
:listUrl="tableListUrl"
:searchParams="tableSearchParams"
:transformData="customTransform"
:defaultPageSize="20"
:scrollY="400"
>
<template #fid="{ record }">
<a v-if="record.fid" @click="handleAttachmentPreview(record.fid)">查看</a>
<span v-else class="no-data-link">查看</span>
</template>
</BasicTable>
</a-modal>
<!-- 附件预览弹窗 -->
<a-modal
v-model:open="attachmentModalVisible"
title="附件预览"
width="80vw"
:footer="null"
destroyOnClose
:bodyStyle="{ height: '70vh' }"
>
<AttachmentPreview :fid="currentAttachmentFid" />
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, nextTick } from 'vue';
import request from '@/utils/request';
import BasicSearch from '@/components/BasicSearch/index.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import AttachmentPreview from '@/components/MapModal/components/AIYXSB/AttachmentPreview.vue';
import { getFishDic } from '@/api/dianZhanZhuanTi';
defineOptions({ name: 'ZenZhiZhanYunXingDetailModal' });
const props = defineProps<{
selectedStcd: string;
}>();
// tab key
const igKeys = ['1-2-1'];
// ==================== ====================
const modalVisible = ref(false);
const activeTab = ref<any>(null);
const mFilter = ref<any>(null);
const searchForms = ref<any>({});
const tableRef = ref();
const fishDic = ref<Array<{ value: string; key: string }>>([]);
// ==================== ====================
const attachmentModalVisible = ref(false);
const currentAttachmentFid = ref('');
// ==================== activeTab.filter ====================
const searchList = computed(() => {
if (!activeTab.value?.filter) return [];
return activeTab.value.filter.map((f: any) => {
const item: any = {
type: f.type === 'select' ? 'Select' : 'Input',
name: f.name,
label: f.label,
placeholder: f.placeholder || `请输入${f.label}`
};
if (f.type === 'select' && f.name === 'ftp') {
item.options = fishDic.value.map((d: any) => ({
label: d.key,
value: d.value
}));
}
return item;
});
});
// ==================== proliferationColumns ====================
const buildColumns = (cols: any[]): any[] => {
return cols
.filter((col: any) => col.visible !== false)
.map((col: any) => {
const base: any = {
title: col.title,
dataIndex: col.dataIndex,
key: col.key || col.dataIndex,
width: col.width || 150,
ellipsis: col.ellipsis ?? true
};
if (col.dataIndex === 'fid' || col.key === 'fid') {
base.slots = { customRender: 'fid' };
}
if (col.children?.length) {
base.children = buildColumns(col.children);
}
return base;
});
};
const tableColumns = computed(() => {
if (!activeTab.value?.columns) return [];
return buildColumns(activeTab.value.columns);
});
// ==================== ====================
const tableSearchParams = computed(() => {
const filters: any[] = [];
if (props.selectedStcd) {
filters.push({
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: props.selectedStcd
});
}
if (searchForms.value) {
Object.keys(searchForms.value).forEach(key => {
if (searchForms.value[key] !== undefined && searchForms.value[key] !== '') {
filters.push({
field: key,
operator: 'contains',
dataType: 'string',
value: searchForms.value[key]
});
}
});
}
const result: any = {
filter: { logic: 'and', filters }
};
if (!igKeys.includes(activeTab.value?.key)) {
result.sort = [{ field: 'tm', dir: 'desc' }];
}
return result;
});
// ==================== listUrl ====================
const tableListUrl = computed(() => {
return (params: any) =>
request({
url: activeTab.value?.url || '',
method: 'post',
data: params
});
});
const customTransform = (res: any) => ({
records: res?.data?.data || res?.data?.items || [],
total: res?.data?.total || 0
});
// ==================== ====================
const switchTab = (item: any) => {
searchForms.value = {};
activeTab.value = item;
nextTick(() => {
tableRef.value?.getList(tableSearchParams.value.filter);
});
};
const handleTableSearch = (values: any) => {
searchForms.value = { ...values };
tableRef.value?.getList(tableSearchParams.value.filter);
};
const handleTableReset = () => {
searchForms.value = {};
tableRef.value?.getList(tableSearchParams.value.filter);
};
const handleModalCancel = () => {
modalVisible.value = false;
activeTab.value = null;
mFilter.value = null;
searchForms.value = {};
};
const handleAttachmentPreview = (fid: string) => {
if (fid) {
currentAttachmentFid.value = fid;
attachmentModalVisible.value = true;
}
};
// ==================== API ====================
const fetchFishDic = async () => {
if (fishDic.value.length > 0) return; //
try {
const res = await getFishDic({
filter: { logic: 'and', filters: [] },
select: ['id', 'name']
});
// debugger
const data = res?.data?.data || [];
fishDic.value = data.map((item: any) => ({
value: item.id,
key: item.name
}));
} catch (e) {
console.error('获取鱼类字典失败', e);
}
};
// ==================== ====================
const open = (val: any) => {
searchForms.value = {};
mFilter.value = val;
activeTab.value = val.proTable?.[0] || null;
modalVisible.value = true;
fetchFishDic();
nextTick(() => {
tableRef.value?.getList(tableSearchParams.value.filter);
});
};
defineExpose({ open });
</script>
<style scoped>
.modal-tabs {
display: flex;
gap: 0;
margin-bottom: 16px;
border-bottom: 1px solid #e8e8e8;
}
.modal-tabs__item {
padding: 8px 20px;
cursor: pointer;
color: #666;
font-size: 14px;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.modal-tabs__item:hover {
color: #2f6b98;
}
.modal-tabs__item--active {
color: #2f6b98;
border-bottom-color: #2f6b98;
font-weight: 500;
}
.no-data-link {
color: #d9d9d9;
cursor: not-allowed;
}
</style>

View File

@ -0,0 +1,600 @@
<template>
<SidePanelItem
title="增殖站运行及评估"
:select="stationSelect"
@update-values="onPanelChange"
>
<a-spin v-if="loading" class="spin-center" />
<a-empty v-else-if="!hasData" description="暂无数据" class="empty-center" />
<div v-else class="step-card-wrapper">
<div
v-for="(item, index) in stepList"
:key="item.key"
class="step-card"
:class="{
'step-card--active': activeIndex === index,
'step-card--last': index === 4
}"
:style="{ zIndex: activeIndex === index ? 999 : 989 - index * 10 }"
@click="index === 4 ? undefined : setActiveIndex(index)"
>
<!-- 激活状态展开子步骤 -->
<template v-if="activeIndex === index">
<div class="step-card__header">{{ item.title }}</div>
<div class="step-card__icon">
<img :src="fishStation2Svg" alt="" />
</div>
<div class="step-card__arrow">
<img :src="arrowDownLine2Svg" alt="" />
</div>
<div class="step-card__list">
<div
v-for="val in item.textList"
:key="val.key"
class="step-card__list-item"
:class="{
'is-disabled': val.disable,
'is-active': val.curNewSurFace
}"
:style="val.disable ? {} : { cursor: 'pointer' }"
@click="!val.disable && val.proTable?.length && detailModalRef?.open(val)"
>
{{ val.ctitle }}
</div>
</div>
<p class="step-card__step">STEP{{ index + 1 }}</p>
</template>
<!-- 折叠状态 -->
<template v-else>
<div class="step-card__fold-label">{{ item.title }}</div>
<p class="step-card__step">STEP{{ index + 1 }}</p>
</template>
</div>
</div>
<!-- 详情弹窗子组件 -->
<ZenZhiZhanYunXingDetailModal
ref="detailModalRef"
:selected-stcd="selectedStcd"
/>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, computed, watch, inject } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import ZenZhiZhanYunXingDetailModal from './ZenZhiZhanYunXingDetailModal.vue';
import fishStation2Svg from '@/assets/icons/fishStation2.svg';
import arrowDownLine2Svg from '@/assets/icons/arrowDownLine2.svg';
import {
getHatcheryOperationData
} from '@/api/dianZhanZhuanTi';
import { msstbprptGetKendoList } from '@/api/zzfl';
import {
qinYuXinXi,
qinYuSiWanJiLu,
qinYuPeiYuFangShi,
qinYuPeiYuShuiWen,
qinYuPeiYuShuiZi,
qinYuPeiYuTouWei,
taoTaiQinYu,
yuLeiRenGo,
yuLeiShouJin,
yuleifuhua,
miaoZhongPeiYu,
miaoZhongJiLu,
yuBingFangZhi
} from '@/components/MapModal/components/NormalOperationData/proliferationColumns';
defineOptions({ name: 'ZenZhiZhanYunXingPG' });
// ==================== ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const apiId = computed(() => dianZhanStation.value?.stcd || '');
// ==================== 5 proTable ====================
const INIT_STEP_LIST = [
{
title: '亲鱼选配与培育',
key: '0',
textList: [
{
ctitle: '亲鱼选配',
key: '1-1',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '亲鱼信息',
key: '1-1-1',
url: '/fb/bsmfr/GetKendoListCust',
columns: qinYuXinXi,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'signnum', label: '标记编号' }
]
},
{
title: '亲鱼死亡记录',
key: '1-1-2',
url: '/fb/bsdeathr/GetKendoListCust',
columns: qinYuSiWanJiLu,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'fpnum', label: '鱼池编号' },
{ type: 'input', name: 'signnum', label: '标记编号' }
]
}
]
},
{
ctitle: '亲鱼培育',
key: '1-2',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '亲鱼培育方式',
key: '1-2-1',
url: '/fb/bsctmob/GetKendoListCust',
columns: qinYuPeiYuFangShi,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
},
{
title: '亲鱼培育水温观测记录',
key: '1-2-2',
url: '/fb/fbbwtr/GetKendoListCust',
columns: qinYuPeiYuShuiWen,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
},
{
title: '亲鱼培育水质监测记录',
key: '1-2-3',
url: '/fb/fbcwqr/GetKendoListCust',
columns: qinYuPeiYuShuiZi,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
},
{
title: '亲鱼培育投喂记录',
key: '1-2-4',
url: '/fb/bsrfir/GetKendoListCust',
columns: qinYuPeiYuTouWei,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
}
]
},
{
ctitle: '亲鱼补充与淘汰',
key: '1-3',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '淘汰亲鱼信息',
key: '1-3-1',
url: '/fb/outfishr/GetKendoListCust',
columns: taoTaiQinYu,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'signno', label: '标记编号' }
]
}
]
}
]
},
{
title: '人工繁殖',
key: '1',
textList: [
{
ctitle: '亲本选择',
key: '2-1',
disable: true,
curNewSurFace: '',
proTable: []
},
{
ctitle: '人工催产',
key: '2-2',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '鱼类人工催产记录',
key: '2-2-1',
url: '/fb/fishartinlr/GetKendoListCust',
columns: yuLeiRenGo,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] }
]
}
]
},
{
ctitle: '人工授精',
key: '2-3',
disable: true,
curNewSurFace: '',
proTable: []
},
{
ctitle: '孵化',
key: '2-4',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '鱼类受精卵孵化过程记录',
key: '2-4-1',
url: '/fb/fishhatchrecr/GetKendoListCust',
columns: yuLeiShouJin,
filter: [
{ type: 'select', name: 'ftp', label: '鱼类', options: [] },
{ type: 'input', name: 'fishsrc', label: '亲鱼来源' }
]
},
{
title: '鱼类孵化过程巡查记录',
key: '2-4-2',
url: '/fb/fishhatchpror/GetKendoListCust',
columns: yuleifuhua,
filter: [
{ type: 'input', name: 'devno', label: '孵化设施编号' }
]
}
]
}
]
},
{
title: '鱼苗和鱼种的培育',
key: '2',
textList: [
{
ctitle: '鱼苗和鱼种的培育',
key: '3-1',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '苗种培育水环境监测记录',
key: '3-1-1',
url: '/fb/scwemr/GetKendoListCust',
columns: miaoZhongPeiYu,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
},
{
title: '苗种培育记录',
key: '3-1-2',
url: '/fb/fishbreedr/GetKendoListCust',
columns: miaoZhongJiLu,
filter: [
{ type: 'input', name: 'farmno', label: '鱼池编号' }
]
}
]
}
]
},
{
title: '鱼病防治',
key: '3',
textList: [
{
ctitle: '鱼病防治',
key: '4-1',
disable: false,
curNewSurFace: '',
proTable: [
{
title: '鱼病防治记录',
key: '4-1-1',
url: '/fb/fishdpacr/GetKendoListCust',
columns: yuBingFangZhi,
filter: [
{ type: 'input', name: 'fpnum', label: '鱼池编号' }
]
}
]
}
]
},
{
title: '放流',
key: '4',
textList: []
}
];
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
const stepList = ref<any[]>([]);
const activeIndex = ref(0);
const stationOptions = ref<
Array<{ value: string; label: string; stcd: string }>
>([]);
const selectedStation = ref<string>('');
const selectedStcd = ref<string>('');
// ==================== ref ====================
const detailModalRef = ref();
// ==================== SidePanelItem ====================
const stationSelect = computed(() => ({
show: true,
value: selectedStation.value,
options: stationOptions.value,
width: '160px'
}));
// ==================== API ====================
/** 拉取增殖站下拉列表 */
const fetchStationList = async (stcd: string) => {
if (!stcd) return;
try {
const params: any = {
filter: {
logic: 'and',
filters: [
{
field: 'rstcd',
operator: 'contains',
dataType: 'string',
value: stcd
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'FB'
}
]
},
select: [
'id',
'stcd',
'rstcd',
'ennm',
'stnm',
'sttpCode',
'lgtd',
'lttd',
'orderIndex'
]
};
const res = await msstbprptGetKendoList(params);
const data = res?.data?.data || [];
if (data.length > 0) {
const list = data.map((item: any) => ({
label: item.stnm,
value: item.stcd,
stcd: item.stcd
}));
stationOptions.value = list.map((item: any) => ({
value: item.value,
label: item.label,
stcd: item.stcd
}));
selectedStation.value = list[0].value;
selectedStcd.value = list[0].stcd;
} else {
stationOptions.value = [];
selectedStation.value = '';
selectedStcd.value = '';
}
} catch (e) {
console.error('获取增殖站列表失败', e);
}
};
/** 拉取各步骤运行状态 */
const fetchStepStatus = async (stcd: string) => {
if (!stcd) return;
loading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
}
]
}
};
const stepRes = await getHatcheryOperationData(params);
const stepData = stepRes?.data?.data || [];
if (stepData.length > 0) {
hasData.value = true;
const newStepList = JSON.parse(JSON.stringify(INIT_STEP_LIST));
//
stepData.forEach((item: any) => {
const { firstGrade, newest } = item;
if (newest) {
const idx = newStepList.findIndex((s: any) => s.title === firstGrade);
if (idx !== -1) activeIndex.value = idx;
}
});
//
newStepList.forEach((step: any) => {
step.textList.forEach((val: any) => {
let curNewSurFace = '';
stepData.forEach((dItem: any) => {
const { key, newSurface, value } = dItem;
if (val.ctitle === key) {
val.disable = value === '2';
curNewSurFace = newSurface;
}
});
val.curNewSurFace = curNewSurFace;
});
});
stepList.value = newStepList;
} else {
hasData.value = false;
}
} catch (e) {
console.error('获取增殖站运行状态失败', e);
hasData.value = false;
} finally {
loading.value = false;
}
};
// ==================== ====================
const onPanelChange = (data: any) => {
if (data.select) {
const option = stationOptions.value.find(
(item: any) => item.value === data.select
);
if (option) {
selectedStation.value = option.value;
selectedStcd.value = option.stcd;
fetchStepStatus(option.stcd);
}
}
};
const setActiveIndex = (index: number) => {
activeIndex.value = index;
};
// ==================== ====================
watch(apiId, newId => {
if (newId) {
fetchStationList(newId);
}
}, { immediate: true });
//
watch(selectedStcd, newStcd => {
if (newStcd) {
fetchStepStatus(newStcd);
}
});
</script>
<style scoped>
.spin-center,
.empty-center {
height: 197px;
}
.step-card-wrapper {
display: flex;
height: 197px;
position: relative;
padding: 0px 10px;
align-items: center;
justify-content: space-between;
}
.step-card {
position: relative;
transition: all 0.3s ease;
background: #ffffff40;
border: 1px solid #ddd;
height: 197px;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
&:hover {
box-shadow: 3px 3px 3px 3px #2f6b9840;
}
}
.step-card--active {
background: #fff;
border: 0px solid #00000000;
display: block;
&:hover {
box-shadow: 3px 3px 3px 3px #2f6b9840;
}
}
.step-card__header {
font-size: 14px;
color: #000000d9;
margin-bottom: 2px;
width: 126px;
text-align: center;
}
.step-card__icon {
width: 100%;
display: flex;
justify-content: center;
margin: 3px 0px;
img {
width: 45px;
height: 45px;
}
}
.step-card__arrow {
width: 100%;
display: flex;
justify-content: center;
img {
width: 20px;
height: 8px;
}
}
.step-card__list {
display: flex;
flex-direction: column;
align-items: center;
margin: 4px 0;
}
.step-card__list-item {
font-size: 14px;
color: #000000d9;
white-space: nowrap;
}
.step-card__list-item.is-disabled {
color: #bbb;
cursor: default;
}
.step-card__step {
position: absolute;
left: -25px;
bottom: 4px;
font-size: 14px;
color: #999;
margin: 0;
}
.step-card__fold-label {
font-size: 14px;
color: #000000d9;
writing-mode: vertical-lr;
letter-spacing: 2px;
}
</style>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="61px" height="64px" viewBox="0 0 61 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>arrowUpLg</title>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#2F6B98" stop-opacity="0.45" offset="0%"></stop>
<stop stop-color="#2F6B98" stop-opacity="0.25" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.7637128%" y1="0.23628715%" x2="49.7637128%" y2="99.7637128%" id="linearGradient-2">
<stop stop-color="#2F6B98" stop-opacity="0.25" offset="0%"></stop>
<stop stop-color="#2F6B98" stop-opacity="0.15" offset="100%"></stop>
</linearGradient>
<linearGradient x1="50%" y1="-2.48949813e-15%" x2="50%" y2="100%" id="linearGradient-3">
<stop stop-color="#2F6B98" stop-opacity="0.150950612" offset="0%"></stop>
<stop stop-color="#2F6B98" stop-opacity="0" offset="100%"></stop>
</linearGradient>
</defs>
<g id="5-生态调查" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="5.2-生态调查-陆生生态调查" transform="translate(-1560.000000, -662.000000)">
<g id="编组备份-4" transform="translate(1476.000000, 119.000000)">
<g id="编组-12" transform="translate(16.000000, 412.000000)">
<g id="ArrowUpLg" transform="translate(68.000000, 131.000000)">
<path d="M30.5,0.838886685 L59.7879803,34.4317691 L41.8358974,34.4317691 L41.8358974,63.4487179 L19.1641026,63.4487179 L19.1641026,34.4317691 L1.21201968,34.4317691 L30.5,0.838886685 Z" id="路径" stroke="url(#linearGradient-1)" stroke-width="1.1025641"></path>
<polygon id="路径备份-4" fill="url(#linearGradient-2)" points="56.9333333 33.1756019 30.5 2.75268817 4.06666667 33.1756019 20.3333333 33.1756019 20.3333333 61.9354839 40.6666667 61.9354839 40.6666667 33.1756019"></polygon>
<path d="M40.6666667,61.2473118 L40.6666667,61.9354839 L20.3333333,61.9354839 L20.3333333,61.2473118 L40.6666667,61.2473118 Z M40.6666667,59.8709677 L40.6666667,60.5591398 L20.3333333,60.5591398 L20.3333333,59.8709677 L40.6666667,59.8709677 Z M40.6666667,58.4946237 L40.6666667,59.1827957 L20.3333333,59.1827957 L20.3333333,58.4946237 L40.6666667,58.4946237 Z M40.6666667,57.1182796 L40.6666667,57.8064516 L20.3333333,57.8064516 L20.3333333,57.1182796 L40.6666667,57.1182796 Z M40.6666667,55.7419355 L40.6666667,56.4301075 L20.3333333,56.4301075 L20.3333333,55.7419355 L40.6666667,55.7419355 Z M40.6666667,54.3655914 L40.6666667,55.0537634 L20.3333333,55.0537634 L20.3333333,54.3655914 L40.6666667,54.3655914 Z M40.6666667,52.9892473 L40.6666667,53.6774194 L20.3333333,53.6774194 L20.3333333,52.9892473 L40.6666667,52.9892473 Z M40.6666667,51.6129032 L40.6666667,52.3010753 L20.3333333,52.3010753 L20.3333333,51.6129032 L40.6666667,51.6129032 Z M40.6666667,50.2365591 L40.6666667,50.9247312 L20.3333333,50.9247312 L20.3333333,50.2365591 L40.6666667,50.2365591 Z M40.6666667,48.8602151 L40.6666667,49.5483871 L20.3333333,49.5483871 L20.3333333,48.8602151 L40.6666667,48.8602151 Z M40.6666667,47.483871 L40.6666667,48.172043 L20.3333333,48.172043 L20.3333333,47.483871 L40.6666667,47.483871 Z M40.6666667,46.1075269 L40.6666667,46.7956989 L20.3333333,46.7956989 L20.3333333,46.1075269 L40.6666667,46.1075269 Z M40.6666667,44.7311828 L40.6666667,45.4193548 L20.3333333,45.4193548 L20.3333333,44.7311828 L40.6666667,44.7311828 Z M40.6666667,43.3548387 L40.6666667,44.0430108 L20.3333333,44.0430108 L20.3333333,43.3548387 L40.6666667,43.3548387 Z M40.6666667,41.9784946 L40.6666667,42.6666667 L20.3333333,42.6666667 L20.3333333,41.9784946 L40.6666667,41.9784946 Z M40.6666667,40.6021505 L40.6666667,41.2903226 L20.3333333,41.2903226 L20.3333333,40.6021505 L40.6666667,40.6021505 Z M40.6666667,39.2258065 L40.6666667,39.9139785 L20.3333333,39.9139785 L20.3333333,39.2258065 L40.6666667,39.2258065 Z M40.6666667,37.8494624 L40.6666667,38.5376344 L20.3333333,38.5376344 L20.3333333,37.8494624 L40.6666667,37.8494624 Z M40.6666667,36.4731183 L40.6666667,37.1612903 L20.3333333,37.1612903 L20.3333333,36.4731183 L40.6666667,36.4731183 Z M40.6666667,35.0967742 L40.6666667,35.7849462 L20.3333333,35.7849462 L20.3333333,35.0967742 L40.6666667,35.0967742 Z" id="形状结合" fill="url(#linearGradient-3)"></path>
<polyline id="路径-5备份" transform="translate(30.500000, 13.075269) scale(-1, 1) rotate(-90.000000) translate(-30.500000, -13.075269) " points="20.8655914 -3.86917563 40.1344086 13.0752688 20.8655914 30.0197133"></polyline>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,445 @@
<template>
<SidePanelItem
title="电站放流统计"
:datetimePicker="datePickerConfig"
@update-values="onDateChange"
>
<div class="release-statistics">
<a-spin
v-show="hasData"
:spinning="loading"
tip="加载中..."
class="chart-container"
>
<div ref="chartRef" class="chart-container"></div>
</a-spin>
<a-empty
v-show="!hasData && !loading"
description="暂无数据"
class="empty-wrapper"
/>
</div>
</SidePanelItem>
<!-- 弹窗 -->
<a-modal
v-model:open="showModal"
title="电站放流详情"
width="80vw"
:footer="null"
:destroyOnClose="true"
>
<PowerStationReleasetaStisticsDetail
v-if="showModal"
:year="modalPageData.year"
:stcdSelectOptions="modalPageData.stcdSelectOptions"
:stcd="modalPageData.stcd"
/>
</a-modal>
</template>
<script lang="ts" setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
inject
} from 'vue';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import PowerStationReleasetaStisticsDetail from './PowerStationReleasetaStisticsDetail.vue';
import {
getPowerStationReleaseYear,
getPowerStationReleaseData
} from '@/api/dianZhanZhuanTi';
defineOptions({ name: 'PowerStationReleasetaStistics' });
// ==================== ====================
const dianZhanStation = inject<any>('dianZhanStation', ref(null));
const stcd = computed(() => dianZhanStation.value?.stcd || '');
// ==================== ====================
const currentBaseId = (window as any).__lyConfigs?.baseId ?? '02';
const UNIT = '万尾';
const COLORS = ['#6ca4f7', '#91cc75'];
// ==================== ====================
const loading = ref(false);
const hasData = ref(false);
const searchDate = ref<dayjs.Dayjs | null>(null);
const resData = ref<any[]>([]);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
// ==================== ====================
const showModal = ref(false);
const modalPageData = ref({
year: '',
stcdSelectOptions: [] as { label: string; value: string }[],
stcd: ''
});
// ==================== ====================
const datePickerConfig = computed(() => ({
show: true,
picker: 'year' as const,
value: searchDate.value ? searchDate.value.format('YYYY') : undefined
}));
// ==================== ====================
interface ChartData {
stationList: string[];
planList: number[];
actualList: number[];
}
const chartData = computed<ChartData>(() => {
if (!resData.value?.length)
return { stationList: [], planList: [], actualList: [] };
return {
stationList: resData.value.map((item: any) => item.ennm),
planList: resData.value.map((item: any) =>
parseFloat(((item.fcntjh ?? 0) / 10000).toFixed(4))
),
actualList: resData.value.map((item: any) =>
parseFloat(((item.fcntjc ?? 0) / 10000).toFixed(4))
)
};
});
// ==================== ====================
const stcdSelectOptions = computed(() => {
if (!resData.value?.length) return [];
const options = resData.value.map((item: any) => ({
label: item.ennm,
value: item.stcd
}));
return [{ label: '全部', value: '' }, ...options];
});
// ==================== ECharts ====================
const buildOption = (data: ChartData) => {
const allValues = [...data.planList, ...data.actualList];
const maxVal = Math.max(...allValues, 1);
const minVal = Math.min(0, ...allValues);
// y 4
const rawRange = maxVal - minVal;
const rawStep = rawRange / 4;
const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
const normalizedStep = rawStep / magnitude;
let niceStep: number;
if (normalizedStep <= 1) niceStep = 1;
else if (normalizedStep <= 2) niceStep = 2;
else if (normalizedStep <= 5) niceStep = 5;
else niceStep = 10;
niceStep *= magnitude;
const yMin = minVal < 0 ? 0 : minVal;
return {
tooltip: {
trigger: 'axis' as const,
formatter: (params: any) => {
if (!params || params.length === 0) return '';
let html = `<div style="font-size:14px;margin-bottom:6px;">${params[0].name}</div>`;
params.forEach((p: any) => {
const val = p.value ?? 0;
html += `
<div style="display:flex;align-items:center;margin:2px 0;">
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:${p.color};margin-right:6px;"></span>
<span>${p.seriesName}: <strong>${val}</strong> ${UNIT}</span>
</div>`;
});
return html;
}
},
legend: {
top: 0,
left: 'right',
orient: 'horizontal' as const,
icon: 'roundRect' as const,
itemGap: 10,
data: [
{ name: '计划放流', itemStyle: { color: COLORS[0] } },
{ name: '实际放流', itemStyle: { color: COLORS[1] } }
],
textStyle: { fontSize: 12 }
},
color: COLORS,
grid: {
top: '15%',
left: '8%',
right: '8%',
bottom: '0%',
containLabel: true
},
xAxis: {
type: 'category' as const,
data: data.stationList,
axisLabel: {
rotate: 45,
fontSize: 12
}
},
yAxis: {
type: 'value' as const,
name: `数量(${UNIT})`,
nameGap: 15,
nameTextStyle: { fontSize: 12 },
splitNumber: 4,
min: yMin,
max: maxVal + niceStep,
interval: niceStep
},
series: [
{
name: '计划放流',
type: 'bar',
data: data.planList,
barMaxWidth: 15,
barGap: '40%',
label: {
show: true,
position: 'bottom',
distance: -25,
rotate: 90,
fontSize: 12,
color: '#333',
align: 'top',
verticalAlign: 'middle'
}
},
{
name: '实际放流',
type: 'bar',
data: data.actualList,
barMaxWidth: 15,
barGap: '40%',
label: {
show: true,
position: 'bottom',
distance: -25,
rotate: 90,
fontSize: 13,
color: '#333',
align: 'top',
verticalAlign: 'middle'
}
}
]
};
};
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
nextTick(() => {
chartInstance = echarts.init(chartRef.value);
chartInstance.on('click', handleChartClick);
if (chartData.value.stationList.length > 0) {
chartInstance.setOption(buildOption(chartData.value), true);
}
});
};
const handleResize = () => {
if (chartInstance) chartInstance.resize();
};
const destroyChart = () => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
};
// ==================== API ====================
/** 获取数据年份(第一次加载时确定默认年份) */
const fetchYear = () => {
if (!stcd.value) return;
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: currentBaseId
},
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: stcd.value
}
]
}
};
getPowerStationReleaseYear(params)
.then((res: any) => {
// debugger
const list = res?.data?.data?.[0] ?? [];
// debugger
if (list.length > 0) {
searchDate.value = dayjs(list[0].plansd);
} else {
searchDate.value = dayjs();
}
})
.catch(() => {
searchDate.value = dayjs();
});
};
/** 获取明细数据 */
const fetchData = () => {
if (!stcd.value || !searchDate.value) return;
loading.value = true;
const startTime = searchDate.value
.startOf('year')
.format('YYYY-MM-DD HH:mm:ss');
const endTime = searchDate.value.endOf('year').format('YYYY-MM-DD HH:mm:ss');
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: currentBaseId
},
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: startTime
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: endTime
},
{
field: 'stcd',
operator: 'in',
dataType: 'string',
value: stcd.value
}
]
}
};
getPowerStationReleaseData(params)
.then((res: any) => {
loading.value = false;
const list = res?.data?.data ?? [];
resData.value = list;
hasData.value = list.length > 0;
if (chartInstance) {
chartInstance.setOption(buildOption(chartData.value), true);
}
nextTick(() => {
chartInstance?.resize();
});
})
.catch(() => {
loading.value = false;
resData.value = [];
hasData.value = false;
});
};
// ==================== ====================
const onDateChange = (payload: any) => {
if (payload.datetime) {
searchDate.value = dayjs(payload.datetime, 'YYYY');
}
};
/** 图表点击 → 打开弹窗 */
const handleChartClick = (params: any) => {
const name = params?.name;
if (!name) return;
const matched = stcdSelectOptions.value.find(item => item.label === name);
modalPageData.value = {
year: searchDate.value ? searchDate.value.format('YYYY') : '',
stcdSelectOptions: stcdSelectOptions.value,
stcd: matched?.value ?? ''
};
showModal.value = true;
};
// ==================== ====================
watch(stcd, newStcd => {
if (newStcd) {
fetchYear();
}
});
watch(searchDate, newDate => {
if (newDate) {
fetchData();
}
});
watch(
chartData,
newData => {
if (chartInstance && newData.stationList.length > 0) {
chartInstance.setOption(buildOption(newData), true);
nextTick(() => chartInstance?.resize());
}
},
{ deep: true }
);
// ==================== ====================
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
if (stcd.value) {
fetchYear();
}
});
onUnmounted(() => {
destroyChart();
});
</script>
<style scoped>
.release-statistics {
width: 100%;
height: 196px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-container {
width: 100%;
height: 100%;
}
.empty-wrapper {
width: 100%;
height: 100%;
}
:deep(.ant-spin-nested-loading) {
width: 100%;
height: 100%;
}
:deep(.ant-spin-container) {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,5 +1,381 @@
<template> <template>
<div> <div class="dianzhanzhuanti-page dianzhanzhuanti-hbb">
<h2>电站专题 </h2> <!-- 左上角电站选择器 -->
<div class="mapTopRight">
<a-tree-select
v-model:value="treeSelectValue"
:tree-data="treeSelectData"
:field-names="{
label: 'title',
value: 'stcd',
children: 'children'
}"
show-search
tree-node-filter-prop="title"
style="width: 200px"
placeholder="请选择电站"
:tree-default-expanded-keys="treeDefaultExpandedKeys"
@select="onTreeSelect"
/>
</div>
<!-- Grid 布局容器 -->
<div class="layout-grid" v-if="showLayout" :style="gridStyle">
<div
v-for="item in normalizedItems"
:key="item.code"
:style="{ gridArea: `${item.position}-${item.areaIdx}` }"
class="panel-item"
>
<component :is="getModuleComponent(item.code)" />
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick, provide } from 'vue';
import { useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
import { getTreeConfiguredps, getPowerSubjectLayout } from '@/api/dianZhanZhuanTi';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useUiStore } from '@/store/modules/ui';
import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import { getModuleComponent } from './moduleMap';
import { LAYOUT_GRID_SKELETONS } from './layoutGridSkeletons';
import { setMapLegendPos } from '@/components/gis/gisUtils';
defineOptions({
name: 'DianZhanZhuanTi'
});
// ==================== ====================
const route = useRoute();
const { stcd: pageStcd } = route.query as Record<string, string>;
// ==================== Store ====================
const jidiStore = useJidiSelectEventStore();
const uiStore = useUiStore();
const mapViewStore = useMapViewStore();
// ==================== 3D ====================
const showLayout = ref(true);
// ==================== provide ====================
interface DianZhanStation {
wbsCode: string;
stcd: string;
ennm: string;
lgtd: number;
lttd: number;
}
const dianZhanStation = ref<DianZhanStation | null>(null);
provide('dianZhanStation', dianZhanStation);
// ==================== ====================
const treeSelectData = ref<any[]>([]);
const treeSelectValue = ref<string>();
const treeDefaultExpandedKeys = ref<string[]>([]);
const defaultApiId = ref<string>('');
const layoutData = ref<{
type: string;
data: Array<{ code: string; name: string; position: string; index: number }>;
} | null>(null);
// ==================== ====================
/**
* bclData position 各自从 0 重新编号得到 grid-area 所需的 areaIdx
*/
const normalizedItems = computed(() => {
const items = layoutData.value?.data || [];
const counter: Record<string, number> = { left: 0, right: 0, bottom: 0 };
return items.map((item: any) => {
const areaIdx = counter[item.position]++;
return { ...item, areaIdx };
});
});
/** 根据 layoutId 从骨架配置取出 CSS Grid 属性 */
const gridStyle = computed(() => {
const skeleton = LAYOUT_GRID_SKELETONS[layoutData.value?.type || ''];
if (!skeleton) return { display: 'none' };
return {
display: 'grid',
gridTemplateAreas: skeleton.gridTemplateAreas,
gridTemplateColumns: skeleton.gridTemplateColumns,
gridTemplateRows: skeleton.gridTemplateRows,
gap: '10px',
padding: '10px',
};
});
// ==================== ====================
/** 处理树形数据格式 */
function mapFuc(dataArray: any[]): any[] {
return (dataArray || []).map((item: any) => {
const hasChildren = item?.children?.length > 0;
return {
key: item?.wbsCode || item?.stcd,
value: item?.wbsCode || item?.stcd,
stcd: item?.wbsCode || item?.stcd,
title: item?.wbsName || item?.ennm,
ennm: item?.wbsName || item?.ennm,
lgtd: item?.lgtd,
lttd: item?.lttd,
selectable: hasChildren ? false : true,
children: hasChildren ? mapFuc(item.children) : undefined
};
});
}
/** 加载布局配置 */
function getLayout(stcd: string) {
getPowerSubjectLayout(stcd)
.then((res: any) => {
const layoutConfig = res?.data?.[0];
if (layoutConfig) {
const _data = JSON.parse(layoutConfig.bclData ?? '[]').filter(
(el: any) => el.code
);
layoutData.value = { type: layoutConfig.layoutId, data: _data };
} else {
layoutData.value = { type: '', data: [] };
}
})
.catch(() => {
layoutData.value = { type: '', data: [] };
});
}
/** TreeSelect 选择事件 */
function onTreeSelect(value: any, item: any) {
if (!item?.stcd) return;
defaultApiId.value = item.stcd;
const parentCode = findParentCode(item.stcd);
dianZhanStation.value = {
wbsCode: parentCode,
stcd: item.stcd,
ennm: item.ennm,
lgtd: item.lgtd,
lttd: item.lttd
};
getLayout(item.stcd);
treeSelectValue.value = item.stcd;
treeDefaultExpandedKeys.value = [item.stcd];
//
waitForMapReady(() => {
setTimeout(() => {
if ((window as any).mapClass) {
(window as any).mapClass.flyTopanto([item.lgtd, item.lttd], 14);
}
}, 0);
});
}
/** 在树数据中查找叶子节点对应的父节点基地级code */
function findParentCode(leafStcd: string): string {
for (const parent of treeSelectData.value) {
if (parent.children?.some((c: any) => c.stcd === leafStcd)) {
return parent.stcd;
}
}
return leafStcd;
}
/** 等待地图就绪后执行回调 */
function waitForMapReady(cb: () => void) {
const mc = (window as any).mapClass;
if (mc?.view) {
cb();
} else {
nextTick(() => waitForMapReady(cb));
}
}
// ==================== ====================
function switchToSatellite(retryCount = 0) {
const mc = (window as any).mapClass;
if (mc?.service?.activeBaseLayerKey === undefined) {
setTimeout(() => switchToSatellite(0), 200);
return;
}
try {
mc.baseLayerSwitcher('BASEMAP-img', true);
mapViewStore.setActiveBaseLayerKey('BASEMAP-img');
} catch {
if (retryCount < 30) {
setTimeout(() => switchToSatellite(retryCount + 1), 200);
}
}
}
function switchToVector() {
try {
(window as any).mapClass?.baseLayerSwitcher?.('s_province_boundaries', true);
mapViewStore.setActiveBaseLayerKey('s_province_boundaries');
} catch {}
}
//
const getModuleComponenturl = (url: string) => {
return defineAsyncComponent(
() => import(url)
)
};
// ==================== jidiSelectorMod ====================
watch(
() => jidiStore.selectedItem?.wbsCode,
(newWbsCode) => {
if (!newWbsCode || newWbsCode === dianZhanStation.value?.wbsCode) return;
const parentNode = treeSelectData.value.find(
(el: any) => el.stcd === newWbsCode
);
const targetNode = parentNode?.children?.[0];
const leafStcd = targetNode?.stcd || newWbsCode;
treeSelectValue.value = leafStcd;
treeDefaultExpandedKeys.value = [leafStcd];
defaultApiId.value = leafStcd;
getLayout(leafStcd);
dianZhanStation.value = {
wbsCode: newWbsCode,
stcd: leafStcd,
ennm: targetNode?.ennm || '',
lgtd: targetNode?.lgtd || 0,
lttd: targetNode?.lttd || 0
};
}
);
// ==================== ====================
watch(
() => layoutData.value,
(val) => {
if (val?.type) {
const sk = LAYOUT_GRID_SKELETONS[val.type];
const rows = sk?.gridTemplateRows?.split(' ') || [];
const bottomRowHeight = rows[rows.length - 1];
nextTick(() => setMapLegendPos(val.type, val, 460, bottomRowHeight));
}
},
{ deep: true }
);
// ==================== ====================
function resetMapControlPositions() {
['#qgc-legendtl', '#map-filter-container', '#map-compassControl', '#map-controller', '#map-monitor', '#map-baselayer'].forEach(sel => {
const el = document.querySelector(sel) as HTMLElement;
if (el) {
el.style.left = '';
el.style.right = '';
el.style.bottom = '';
}
});
}
// ==================== 3D/2D ====================
watch(
() => uiStore.mapType,
(newType) => {
showLayout.value = newType !== '3D';
}
);
// ==================== ====================
watch(
() => uiStore.mapSwitchCompletedTick,
() => {
if (uiStore.mapType === '3D') {
// 2D 3D +
resetMapControlPositions();
const station = dianZhanStation.value;
if (station) {
const mc = (window as any).mapClass;
mc?.flyTopanto?.([station.lgtd, station.lttd], 14);
}
} else if (showLayout.value) {
// 3D 2D +
if (layoutData.value?.type) {
const sk = LAYOUT_GRID_SKELETONS[layoutData.value.type];
const rows = sk?.gridTemplateRows?.split(' ') || [];
nextTick(() => setMapLegendPos(layoutData.value!.type, layoutData.value, 460, rows[rows.length - 1]));
}
switchToSatellite();
}
}
);
// ==================== ====================
onMounted(() => {
getTreeConfiguredps()
.then((res: any) => {
const result = res?.data ?? [];
const treeData = mapFuc(result);
treeSelectData.value = treeData;
const allChildren = treeData.flatMap((el: any) => el.children || []) as any[];
const targetStcd = pageStcd || treeData?.[0]?.children?.[0]?.stcd;
const targetNode = allChildren.find((el: any) => el.stcd === targetStcd)
|| treeData?.[0]?.children?.[0];
if (targetNode?.stcd) {
treeSelectValue.value = targetNode.stcd;
treeDefaultExpandedKeys.value = [targetNode.stcd];
onTreeSelect(targetNode.stcd, targetNode);
}
})
.catch((err: any) => {
console.error('获取电站树形数据失败', err);
message.error('获取电站列表失败');
});
switchToSatellite();
});
onUnmounted(() => {
switchToVector();
layoutData.value = null;
resetMapControlPositions();
});
</script>
<style lang="scss">
.dianzhanzhuanti-page {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index:999;
.mapTopRight {
position: absolute;
top: 10px;
right: 480px;
z-index: 100;
pointer-events: auto;
}
.layout-grid {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
.panel-item {
pointer-events: auto;
border-radius: 4px;
overflow: auto;
}
}
}
</style>

View File

@ -0,0 +1,133 @@
/**
* CSS Grid
*
*
* bclData position + per-position-index grid-area
*
* {position}-{areaIdx}
* areaIdx = position 0 index
* left-0 = 1 right-2 = 3
*
* = 1
* "." =
*/
export interface GridSkeleton {
/** CSS grid-template-areas 字符串 */
gridTemplateAreas: string;
/** CSS grid-template-columns 字符串 */
gridTemplateColumns: string;
/** CSS grid-template-rows 字符串 */
gridTemplateRows: string;
}
export const LAYOUT_GRID_SKELETONS: Record<string, GridSkeleton> = {
// 左3右3加底
layout1: {
gridTemplateAreas:
'"left-0 . right-0" "left-1 . right-1" "left-2 bottom-0 right-2"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
//左4右4加底
layout2: {
gridTemplateAreas:
'"left-0 . right-0" "left-1 . right-1" "left-2 . right-2" "left-3 bottom-0 right-3"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr 1fr'
},
// 左三右三(无底部)
layout3: {
gridTemplateAreas:
'"left-0 . right-0" "left-1 . right-1" "left-2 . right-2"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
//左4右4无底
layout4: {
gridTemplateAreas:
'"left-0 . right-0" "left-1 . right-1" "left-2 . right-2" "left-3 . right-3"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr 1fr'
},
// 右二(无左侧、无底部)
layout5: {
gridTemplateAreas: '". . right-0" ". . right-1"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr'
},
// 左2右1加底
layout6: {
gridTemplateAreas:
'"left-0 . right-0" "left-1 . right-0" "bottom-0 bottom-0 bottom-0"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
//仅左3
layout7: {
gridTemplateAreas: '"left-0 . ." "left-1 . ." "left-2 . ."',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
//左2右3加底
layout8: {
gridTemplateAreas:
'"left-0 . right-0" "left-1 . right-1" "bottom-0 bottom-0 right-2"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
//左3加底
layout9: {
gridTemplateAreas: '"left-0 . ." "left-1 . ." "left-2 bottom-0 bottom-0"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
// 左二右二 + 底部(底部撑满全宽)
layout10: {
gridTemplateAreas:
'"left-0 . right-0" "left-1 . right-1" "bottom-0 bottom-0 bottom-0"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
// 左3右1无底
layout11: {
gridTemplateAreas: '"left-0 . right-0" "left-1 . right-0" "left-2 . ."',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
//右一无底
layout12: {
gridTemplateAreas: '". . right-0" ". . right-0" ". . ." ". . ."',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr 1fr'
},
//右1(2/3高)无底
layout13: {
gridTemplateAreas: '". . right-0" ". . right-0" ". . ." ',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr '
},
//左一右一无底
layout14: {
gridTemplateAreas: '". left-0 . right-0" "left-0 . right-0" ". . ."',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
//左4右3满底
layout15: {
gridTemplateAreas:
' "left-0 . right-0" "left-1 . right-1" "left-2 . right-2" "left-3 bottom-0 bottom-0"',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
//左(2/3+1/3)右3加底
layout16: {
gridTemplateAreas:
'"left-0 . right-0" "left-0 . right-1" "bottom-0 bottom-0 right-2" ',
gridTemplateColumns: '440px 1fr 440px',
gridTemplateRows: '1fr 1fr 1fr'
},
// TODO: 后续补充其余布局的骨架配置
};

View File

@ -0,0 +1,120 @@
import { defineAsyncComponent } from 'vue';
/**
*
* key: 后端 bclData code
* value: Vue
*/
export const moduleMap: Record<string, any> = {
// === 已有 Vue 模块映射 ===
// 垂向水温变化
'dianzhanZhuantiMod/DZChuiXiangShuiWenBianHua': defineAsyncComponent(
() => import('@/modules/chuixiangshuiwenChangeMod/index.vue')
),
// 水质达标率
'dianzhanZhuantiMod/DZShuiZhiDaBiaoLv': defineAsyncComponent(
() => import('@/modules/EnvironmentalQuality/index.vue')
),
// 环保设施运行情况
'dianzhanZhuantiMod/HuanBaoSheShiYunxingQK': defineAsyncComponent(
() => import('@/modules/huanbaoMod/index.vue')
),
// 增殖站运行评估
'dianzhanZhuantiMod/ZenZhiZhanYunXingPG': defineAsyncComponent(
() => import('./components/ZenZhiZhanYunXingPG.vue')
),
// 水质监测趋势图 → 沿程水质变化
'dianzhanZhuantiMod/DZShuiZhiJianCeQuShiTu': defineAsyncComponent(
() => import('@/modules/waterQuality/index.vue')
),
// 监测点水温变化 → 水温监测
'dianzhanZhuantiMod/JianCeDianShuiWenBianHua': defineAsyncComponent(
() => import('@/modules/qixidishuiwenbianhua/index.vue')
),
// 水生生态监测 → 水生生态调查情况
'dianzhanZhuantiMod/SSST_MONITOR': defineAsyncComponent(
() => import('@/modules/shuishengshengtaijiance/index.vue')
),
// 陆生生态监测 → 陆生生态监测情况
'dianzhanZhuantiMod/LSST_MONITOR': defineAsyncComponent(
() => import('@/modules/lushengshengtaijiance/index.vue')
),
// 生态流量实时数据 → 生态流量达标情况
'dianzhanZhuantiMod/EcologicalDataMod': defineAsyncComponent(
() => import('@/modules/shengtaidabiaoMod/index.vue')
),
// 珍稀植物园运行数据
'dianzhanZhuantiMod/ZXZWY_DATA': defineAsyncComponent(
() =>
import(
'@/modules/ZhenXiZhiWuYuanMod/ZhiWuYuanJianSheJiJieRuQingKuangBar/index.vue'
)
),
// === 以下为需要新建的模块(临时占位) ===
// 水电站介绍(待实现)
'dianzhanZhuantiMod/ShuiDianZhanJieShao': defineAsyncComponent(
() => import('./components/ShuiDianZhanJieShao.vue')
),
// 社会投资扶贫就业(待实现)
'dianzhanZhuantiMod/DZSheHuiTouZiFP': defineAsyncComponent(
() => import('./components/DZSheHuiTouZiFP.vue')
),
// 水电站视频监控
'dianzhanZhuantiMod/DZShuiDianZhanSPJK': defineAsyncComponent(
() => import('./components/DZShuiDianZhanSPJK.vue')
),
// 电站放流统计
'dianzhanZhuantiMod/powerStationReleasetaStistics': defineAsyncComponent(
() => import('./components/powerStationReleasetaStistics.vue')
),
//生态流量数据分析
'ecologicalDataMod': defineAsyncComponent(
() =>
import(
'./components/EcologicalFlow.vue'
)
),
//过鱼总量
'liuyu/guoyuzongliang': defineAsyncComponent(
() =>
import(
'@/modules/GYZLLB/index.vue'
)
),
//电站放流统计
'liuyu/zengZhiFangLiu/powerStationReleasetaStistics': defineAsyncComponent(
() =>
import(
'./components/powerStationReleasetaStistics.vue'
)
),
//珍稀植物园运行数据
'luSSTDiaoCha/ZXZWYYunXingShuJu': defineAsyncComponent(
() =>
import(
'./components/ZXZWYYunXingShuJu.vue'
)
),
};
/**
* code
*/
export function getModuleComponent(code: string): any {
return moduleMap[code] || null;
}

View File

@ -9,8 +9,11 @@
@values-change="onValuesChange" @values-change="onValuesChange"
> >
<template #actions> <template #actions>
<a-tooltip title="新增配置"> <a-switch v-model:checked="checked" @change ="switchChange" />
<a-button @click="props.handleAdd" type="primary"> 新增沿程配置 </a-button> <a-tooltip title="新增预警配置">
<a-button @click="props.handleAdd" type="primary">
新增预警配置</a-button
>
</a-tooltip> </a-tooltip>
</template> </template>
</BasicSearch> </BasicSearch>
@ -18,46 +21,51 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted } from 'vue';
import BasicSearch from "@/components/BasicSearch/index.vue"; import BasicSearch from '@/components/BasicSearch/index.vue';
import {
updateShow
} from '@/api/system/alertRules';
interface Props { interface Props {
handleAdd: () => void; handleAdd: () => void;
riverOptions: any[];
riverOptionsLoad: boolean;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "reset", values: any): void; (e: 'reset', values: any): void;
(e: "searchFinish", values: any): void; (e: 'searchFinish', values: any): void;
}>(); }>();
const checked = ref(false);
const initSearchData = { const initSearchData = {
configName: "", ruleName: '',
ruleType: ''
}; };
const searchData = ref<any>({ ...initSearchData }); const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [ const searchList: any = computed(() => [
{ {
type: "Input", type: 'Select',
name: "name", name: 'ruleType',
label: "沿程配置名称", label: '预警类型',
fieldProps: { width: 120,
allowClear: true, options: props.riverOptions,
}, loading: props.riverOptionsLoad
}, },
{ {
type: "Input", type: 'Input',
name: "rvnm", name: 'ruleName',
label: "所在河段", label: '预警名称',
fieldProps: { fieldProps: {
allowClear: true, allowClear: true
}, }
}, }
]); ]);
const onSearchFinish = (values: any) => { const onSearchFinish = (values: any) => {
emit("searchFinish", values); emit('searchFinish', values);
}; };
const onValuesChange = (changedValues: any, allValues: any) => { const onValuesChange = (changedValues: any, allValues: any) => {
@ -65,11 +73,13 @@ const onValuesChange = (changedValues: any, allValues: any) => {
}; };
const handleReset = () => { const handleReset = () => {
emit("reset", initSearchData); emit('reset', initSearchData);
};
const switchChange = (checked: boolean) => {
const res = updateShow({isShow:checked?1:0})
}; };
onMounted(() => { onMounted(() => {
emit("searchFinish", initSearchData); emit('searchFinish', initSearchData);
}); });
</script> </script>
<style lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -1,9 +1,10 @@
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\ConfigManagement\index.vue --> <!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\TiltPhotoManagement\index.vue -->
<template> <template>
<div class="content"> <div class="content">
预警配置管理 <ConfigManagementSearch
<!-- <ConfigManagementSearch ref="tiltPhotoManagementSearch"
ref="configManagementSearch" :river-options="riverOptions"
:river-options-load="riverOptionsLoad"
:handle-add="handleAdd" :handle-add="handleAdd"
@reset="handleReset" @reset="handleReset"
@search-finish="onSearchFinish" @search-finish="onSearchFinish"
@ -11,7 +12,7 @@
<BasicTable <BasicTable
ref="basicTable" ref="basicTable"
:columns="columns" :columns="columns"
:list-url="getAllConfigTree" :list-url="bindGetKendoList"
:search-params="searchParams" :search-params="searchParams"
> >
<template #action="{ column, record }"> <template #action="{ column, record }">
@ -35,12 +36,14 @@
</template> </template>
</BasicTable> </BasicTable>
<ConfigManagementForm <ConfigManagementForm
ref="configManagementForm" ref="tiltPhotoManagementForm"
v-model:visible="editModalVisible" v-model:visible="editModalVisible"
:river-options="riverOptions"
:river-options-load="riverOptionsLoad"
:initial-values="currentRecord" :initial-values="currentRecord"
@cancel="editModalCancel" @cancel="editModalCancel"
@ok="handleEditSubmit" @ok="handleEditSubmit"
/> --> />
</div> </div>
</template> </template>
@ -51,13 +54,11 @@ import ConfigManagementSearch from './ConfigManagementSearch.vue';
import ConfigManagementForm from './ConfigManagementForm.vue'; import ConfigManagementForm from './ConfigManagementForm.vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { import {
getAllConfigTree, bindGetKendoList,
deleteBaseWbsb, warnruleAddOrUpdate,
saveBaseWbsb warnruleBindDelete
} from '@/api/system/map/ConfigManagement'; } from '@/api/system/alertRules';
//
const basicTable = ref<any>(null);
// //
const columns = [ const columns = [
{ {
@ -69,32 +70,24 @@ const columns = [
customRender: ({ text, record, index }) => index + 1 customRender: ({ text, record, index }) => index + 1
}, },
{ {
title: '沿程配置名称', title: '预警名称',
dataIndex: 'name', dataIndex: 'ruleName',
key: 'name', key: 'ruleName'
width: 150
}, },
{ {
title: '配置编码', title: '预警类型',
key: 'code', dataIndex: 'ruleTypeName',
dataIndex: 'code', key: 'ruleTypeName'
width: 120
}, },
{ {
title: '所在河段', title: '规则类型',
dataIndex: 'rvnm', dataIndex: 'ruleTypeName',
key: 'rvnm', key: 'ruleTypeName'
ellipsis: true
}, },
{ {
title: '创建人', title: '创建人',
key: 'recordUser', key: 'recordUserName',
dataIndex: 'recordUser' dataIndex: 'recordUserName',
},
{
title: '创建时间',
key: 'recordTime',
dataIndex: 'recordTime',
width: 150 width: 150
}, },
{ {
@ -105,9 +98,8 @@ const columns = [
}, },
{ {
title: '备注', title: '备注',
dataIndex: 'remark', key: 'description',
key: 'remark', dataIndex: 'description'
width: 360
}, },
{ {
title: '操作', title: '操作',
@ -118,13 +110,15 @@ const columns = [
} }
]; ];
//
const basicTable = ref<any>(null);
// //
const searchParams = ref({}); const searchParams = ref({});
// //
const currentRecord = ref<any | null>(null); const currentRecord = ref<any | null>(null);
const editModalVisible = ref(false); const editModalVisible = ref(false);
// //
const handleAdd = () => { const handleAdd = () => {
currentRecord.value = null; currentRecord.value = null;
editModalVisible.value = true; editModalVisible.value = true;
@ -144,11 +138,11 @@ const handleDelete = (record: any) => {
zIndex: 2002, zIndex: 2002,
onOk: async () => { onOk: async () => {
try { try {
let res = await deleteBaseWbsb([record.id]); let res: any = await warnruleBindDelete({ id: record.bindId });
message.success('删除成功'); if (res.code == 0) message.success('删除成功');
basicTable.value.refresh(); basicTable.value.refresh();
} catch (error) { } catch (error) {
message.error('删除失败');
return; return;
} }
} }
@ -160,20 +154,26 @@ const onSearchFinish = (values: any) => {
const params = { const params = {
logic: 'and', logic: 'and',
filters: [ filters: [
values.parentId {
field: 'ruleCode',
operator: 'in',
dataType: 'string',
value: ['common', 'custom']
},
values.ruleName
? { ? {
field: 'parentId', field: 'ruleName',
operator: 'eq',
dataType: 'string',
value: values.parentId
}
: null,
values.name
? {
field: 'name',
operator: 'contains', operator: 'contains',
dataType: 'string', dataType: 'string',
value: values.name value: values.ruleName
}
: null,
values.ruleType
? {
field: 'ruleType',
operator: 'contains',
dataType: 'string',
value: values.ruleType
} }
: null : null
].filter(Boolean) ].filter(Boolean)
@ -183,9 +183,11 @@ const onSearchFinish = (values: any) => {
// //
const handleReset = () => { const handleReset = () => {
basicTable.value.getList({ nextTick(() => {
logic: 'and', basicTable.value.getList({
filters: [] logic: 'and',
filters: []
});
}); });
}; };
@ -198,22 +200,25 @@ const editModalCancel = () => {
// //
const handleEditSubmit = async (values: any) => { const handleEditSubmit = async (values: any) => {
try { try {
let res = await saveBaseWbsb({ let res:any = await warnruleAddOrUpdate(values);
...currentRecord.value, if(res.code == 0) message.success(`保存成功`);
...values
});
message.success(`保存成功`);
editModalVisible.value = false; editModalVisible.value = false;
basicTable.value.refresh(); basicTable.value.refresh();
} catch (error) { } catch (error) {
message.error(`保存失败`);
editModalVisible.value = false; editModalVisible.value = false;
return; return;
} }
}; };
const riverOptions = ref<any>([
{ value: 'RZ_RULE', label: '水位' },
{ value: 'QGC_RULE', label: '生态流量' },
{ value: 'WT_RULE', label: '水温' },
{ value: 'WQ_RULE', label: '水质' }
]);
const riverOptionsLoad = ref(false);
const initOption = () => {}; const initOption = () => {};
onMounted(() => { onMounted(() => {
initOption(); initOption();
@ -221,8 +226,22 @@ onMounted(() => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.config-management { .tilt-photo-management {
height: 100%; height: 100%;
padding: 16px; padding: 16px;
} }
.content {
position: relative;
z-index: 900;
pointer-events: all;
width: 100%;
height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
gap: 10px;
box-sizing: border-box;
overflow: hidden;
padding: 16px;
}
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<a-modal <a-modal
:title="isEdit ? '编辑倾斜摄影' : '新增倾斜摄影'" :title="isEdit ? '编辑预警规则' : '新增预警规则'"
v-model:open="modalVisible" v-model:open="modalVisible"
:confirm-loading="localLoading" :confirm-loading="localLoading"
width="1536px" width="1536px"
@ -36,14 +36,14 @@
:options="riverOptions" :options="riverOptions"
:filter-option="filterOption" :filter-option="filterOption"
:disabled="isEdit" :disabled="isEdit"
@change="fetchWarnLevelTabs" @change="loadStaticRuleData"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="8" v-if="isWaterQuality"> <a-col :span="8" v-if="isWaterQuality">
<a-form-item label="水质等级" name="warnLevel"> <a-form-item label="水质等级" name="lvl">
<a-select <a-select
v-model:value="formData.warnLevel" v-model:value="formData.lvl"
show-search show-search
style="width: 100%" style="width: 100%"
placeholder="请选择水质等级" placeholder="请选择水质等级"
@ -55,15 +55,33 @@
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
<a-tabs v-if="!warnLevelTabsLoading && warnLevelTabs.length > 0" v-model:activeKey="activeTabKey"> <a-tabs
v-if="!warnLevelTabsLoading && warnLevelTabs.length > 0"
v-model:activeKey="activeTabKey"
>
<a-tab-pane v-for="tab in warnLevelTabs" :key="tab.key" :tab="tab.label"> <a-tab-pane v-for="tab in warnLevelTabs" :key="tab.key" :tab="tab.label">
<a-row :gutter="16"> <a-row :gutter="16">
<a-col v-for="item in detailData[tab.key] || []" :key="item.ys" :span="8"> <a-col
<a-form-item :label="item.ysName + 'm'"> v-for="item in detailData[tab.key] || []"
:key="item.ys"
:span="8"
>
<div class="mb-1 font-medium">
{{ item.ysName }}{{ unitSuffix }}:
</div>
<a-form-item>
<div class="flex gap-2"> <div class="flex gap-2">
<a-input-number v-model:value="item.minVal" placeholder="最小值" style="width: 100%" /> <a-input-number
v-model:value="item.minVal"
placeholder="最小值"
style="width: 100%"
/>
<span class="flex items-center px-1">~</span> <span class="flex items-center px-1">~</span>
<a-input-number v-model:value="item.maxVal" placeholder="最大值" style="width: 100%" /> <a-input-number
v-model:value="item.maxVal"
placeholder="最大值"
style="width: 100%"
/>
</div> </div>
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -80,10 +98,8 @@ import { ref, reactive, computed, watch } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form'; import type { Rule } from 'ant-design-vue/es/form';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { import { getDictItemsByCode } from "@/api/dict";
dictGetRemoteDictValue, import { ruleysList } from '@/api/system/alertRules';
ruleysList
} from '@/api/system/alertRules';
interface Props { interface Props {
visible: boolean; visible: boolean;
initialValues?: any | null; initialValues?: any | null;
@ -115,7 +131,7 @@ const defaultFormData = reactive({
id: undefined, id: undefined,
ruleName: undefined, ruleName: undefined,
ruleType: undefined, ruleType: undefined,
warnLevel: undefined, lvl: undefined
}); });
const formData: any = reactive({ ...defaultFormData }); const formData: any = reactive({ ...defaultFormData });
@ -124,7 +140,7 @@ const rules: Record<string, Rule[]> = {
{ required: true, message: '请输入倾斜影像名称', trigger: 'blur' } { required: true, message: '请输入倾斜影像名称', trigger: 'blur' }
], ],
ruleType: [{ required: true, message: '请输入所在河段', trigger: 'blur' }], ruleType: [{ required: true, message: '请输入所在河段', trigger: 'blur' }],
warnLevel: [{ required: true, message: '请输入所属电站', trigger: 'blur' }], lvl: [{ required: true, message: '请输入所属电站', trigger: 'blur' }],
url: [{ required: true, message: '请输入倾斜影像URL', trigger: 'blur' }], url: [{ required: true, message: '请输入倾斜影像URL', trigger: 'blur' }],
location: [{ required: true, message: '请输入模型坐标', trigger: 'blur' }] location: [{ required: true, message: '请输入模型坐标', trigger: 'blur' }]
}; };
@ -133,21 +149,47 @@ const isEdit = computed(() => !!props.initialValues);
// //
const isWaterQuality = computed(() => { const isWaterQuality = computed(() => {
const selectedOption = props.riverOptions?.find(opt => opt.value === formData.ruleType); const selectedOption = props.riverOptions?.find(
opt => opt.value === formData.ruleType
);
return selectedOption?.label === '水质'; return selectedOption?.label === '水质';
}); });
// ruleType
const unitSuffix = computed(() => {
const unitMap: Record<string, string> = {
RZ_RULE: 'm',
QGC_RULE: 'm³/s',
WQ_RULE: 'mg/L'
};
return unitMap[formData.ruleType] || '';
});
const handleOk = async () => { const handleOk = async () => {
try { try {
await formRef.value.validate(); await formRef.value.validate();
const submitValues = {
...formData,
captureDate: formData.captureDate
? dayjs(formData.captureDate).format('YYYY-MM-DD')
: undefined
};
const isRz = formData.ruleType === 'RZ_RULE';
const isWq = formData.ruleType === 'WQ_RULE';
const submitValues: any = {
ruleName: formData.ruleName,
ruleType: formData.ruleType,
ruleCode: 'common',
detail: [
{
...(isWq ? { lvl: formData.lvl } : { warnLevel: activeTabKey.value }),
ysList: (detailData.value[activeTabKey.value] || []).map((item: any) => ({
ys: item.ys,
minVal: item.minVal,
maxVal: item.maxVal,
tbId: item.tbId,
...(isRz ? { mark: item.mark } : {})
}))
}
],
...(formData.id ? { id: formData.id } : {})
};
emit('ok', submitValues); emit('ok', submitValues);
} catch (error) { } catch (error) {
console.error('Validate Failed:', error); console.error('Validate Failed:', error);
@ -195,11 +237,13 @@ const filterOption = (inputValue: string, option: any) => {
}; };
const ennmOptions = ref([]); const ennmOptions = ref([]);
const handleRiverSelect = async (value: any) => { const handleRiverSelect = async (value: any) => {
let res: any = await dictGetRemoteDictValue({ dictCode: 'wwqtg' }); //
let res: any = await getDictItemsByCode({ dictCode: 'WWQTG' });
// debugger
if (res?.data?.length > 0) { if (res?.data?.length > 0) {
res.data.forEach((item: any) => { res.data.forEach((item: any) => {
item.value = item.dictValue; item.value = item.itemCode;
item.label = item.dictMeaning; item.label = item.dictName;
}); });
ennmOptions.value = res.data; ennmOptions.value = res.data;
} else { } else {
@ -207,15 +251,27 @@ const handleRiverSelect = async (value: any) => {
} }
}; };
// // ruleType
const ruleTypeToDictCodeMap: Record<string, string> = { const staticTabsData: Record<string, { key: string; label: string }[]> = {
'水位': 'RSVRFSR_WARN_LEVEL', RZ_RULE: [
'生态流量': 'EQMN_WARN_LEVEL', { key: '1', label: '一级预警' },
'水温': 'WTMN_WARN_LEVEL', { key: '2', label: '二级预警' },
'水质': 'WQLVL_WARN_LEVEL' { key: '3', label: '三级预警' }
],
QGC_RULE: [
{ key: '1', label: '一级预警' },
{ key: '2', label: '二级预警' }
],
WT_RULE: [
{ key: '1', label: '一级预警' },
{ key: '2', label: '二级预警' },
{ key: '3', label: '三级预警' }
],
WQ_RULE: [
{ key: '0', label: '通用预警' }
]
}; };
// //
const activeTabKey = ref<string>(''); const activeTabKey = ref<string>('');
const warnLevelTabs = ref<{ key: string; label: string }[]>([]); const warnLevelTabs = ref<{ key: string; label: string }[]>([]);
@ -224,57 +280,103 @@ const warnLevelTabsLoading = ref(false);
// //
const detailData = ref<Record<string, any[]>>({}); const detailData = ref<Record<string, any[]>>({});
// detailData // ruleType tabs + ysList
const fetchYsList = async () => { const loadStaticRuleData = async () => {
const res: any = await ruleysList({ const tabs = staticTabsData[formData.ruleType];
filter: { if (tabs) {
logic: 'and', warnLevelTabs.value = tabs;
filters: [ activeTabKey.value = tabs.length > 0 ? tabs[0].key : '';
warnLevelTabsLoading.value = true;
try {
const filters: any[] = [
{ {
field: 'ruleType', field: 'ruleType',
operator: 'eq', operator: 'eq',
dataType: 'string', dataType: 'string',
value: formData.ruleType value: formData.ruleType
} }
] ];
if (isEdit.value && formData.id) {
filters.push({
field: 'ruleId',
operator: 'eq',
dataType: 'string',
value: formData.id
});
}
const res: any = await ruleysList({
filter: {
logic: 'and',
filters
}
});
const list = res?.data || [];
// API tab /
if (isEdit.value && list.length > 0) {
if (formData.ruleType === 'WQ_RULE') {
if (list[0].lvl != null) formData.lvl = list[0].lvl;
} else {
if (list[0].warnLevel != null) activeTabKey.value = String(list[0].warnLevel);
}
}
const details: Record<string, any[]> = {};
// warnLevel warnLevel
const hasWarnLevel = list.some((item: any) => item.warnLevel != null);
// tbId minVal/maxVal tab tab
const fallbackItems = (() => {
const seen = new Set();
return list
.filter((item: any) => {
if (seen.has(item.tbId)) return false;
seen.add(item.tbId);
return true;
})
.map((item: any) => ({ ...item, minVal: undefined, maxVal: undefined }));
})();
tabs.forEach(tab => {
const filtered = formData.ruleType === 'WQ_RULE'
? list
: hasWarnLevel
? list.filter((item: any) => String(item.warnLevel) === tab.key)
: list;
if (formData.ruleType === 'RZ_RULE') {
const template = filtered.length > 0 ? filtered : fallbackItems;
details[tab.key] = [];
// filteredmark使fallbackItemsmarktab
const hasMark = filtered.length > 0 && filtered.some((item: any) => item.mark);
if (hasMark) {
template.forEach((item: any) => {
const ysName = item.ysName || (item.mark === 'low' ? '坝上水位' : '坝下水位');
details[tab.key].push({ ys: item.ys, ysName, tbId: item.tbId, minVal: item.minVal, maxVal: item.maxVal, mark: item.mark });
});
} else {
template.forEach((item: any) => {
details[tab.key].push({ ys: item.ys, ysName: '坝上水位', tbId: item.tbId, minVal: item.minVal, maxVal: item.maxVal, mark: 'low' });
details[tab.key].push({ ys: item.ys, ysName: '坝下水位', tbId: item.tbId, minVal: item.minVal, maxVal: item.maxVal, mark: 'high' });
});
}
} else {
const source = filtered.length > 0 ? filtered : fallbackItems;
details[tab.key] = source.map((item: any) => ({
ys: item.ys,
ysName: item.ysName,
tbId: item.tbId,
minVal: item.minVal,
maxVal: item.maxVal,
...(item.mark ? { mark: item.mark } : {})
}));
}
});
detailData.value = details;
} catch {
detailData.value = {};
} finally {
warnLevelTabsLoading.value = false;
} }
}); } else {
const list = res?.data?.data || []; warnLevelTabs.value = [];
const details: Record<string, any[]> = {}; detailData.value = {};
warnLevelTabs.value.forEach(tab => {
details[tab.key] = list.map((item: any) => ({
ys: item.ys,
ysName: item.ysName,
tbId: item.tbId,
minVal: null,
maxVal: null
}));
});
detailData.value = details;
};
const fetchWarnLevelTabs = async () => {
warnLevelTabsLoading.value = true;
try {
const selectedOption = props.riverOptions?.find(opt => opt.value === formData.ruleType);
const dictCode = selectedOption ? ruleTypeToDictCodeMap[selectedOption.label] : undefined;
if (!dictCode) {
warnLevelTabs.value = [];
return;
}
const res: any = await dictGetRemoteDictValue({ dictCode });
if (res?.data?.length > 0) {
warnLevelTabs.value = res.data.map((item: any) => ({
key: item.dictValue,
label: item.dictMeaning
}));
activeTabKey.value = res.data[0].dictValue;
} else {
warnLevelTabs.value = [];
}
await fetchYsList();
} finally {
warnLevelTabsLoading.value = false;
} }
}; };
@ -288,7 +390,7 @@ watch(
if (!props.initialValues && props.riverOptions?.length > 0) { if (!props.initialValues && props.riverOptions?.length > 0) {
formData.ruleType = props.riverOptions[0].value; formData.ruleType = props.riverOptions[0].value;
} }
fetchWarnLevelTabs(); loadStaticRuleData();
} }
}, },
{ immediate: false } { immediate: false }

View File

@ -9,7 +9,7 @@
@values-change="onValuesChange" @values-change="onValuesChange"
> >
<template #actions> <template #actions>
<a-tooltip title="新增倾斜摄影"> <a-tooltip title="新增预警">
<a-button @click="props.handleAdd" type="primary"> 新增预警</a-button> <a-button @click="props.handleAdd" type="primary"> 新增预警</a-button>
</a-tooltip> </a-tooltip>
</template> </template>

View File

@ -55,8 +55,10 @@ import LayerManagementForm from './LayerManagementForm.vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { import {
warnruleGetKendoList, warnruleGetKendoList,
dictGetRemoteDictValue warnruleAddOrUpdate,
warnruleDelete
} from '@/api/system/alertRules'; } from '@/api/system/alertRules';
// //
const columns = [ const columns = [
{ {
@ -69,23 +71,23 @@ const columns = [
}, },
{ {
title: '预警名称', title: '预警名称',
dataIndex: 'stnm', dataIndex: 'ruleName',
key: 'stnm' key: 'ruleName'
}, },
{ {
title: '预警类型', title: '预警类型',
dataIndex: 'url', dataIndex: 'ruleTypeName',
key: 'url' key: 'ruleTypeName'
}, },
{ {
title: '规则类型', title: '规则类型',
dataIndex: 'hbrvcdName', dataIndex: 'ruleTypeName',
key: 'hbrvcdName' key: 'ruleTypeName'
}, },
{ {
title: '创建人', title: '创建人',
key: 'recordUser', key: 'recordUserName',
dataIndex: 'recordUser', dataIndex: 'recordUserName',
width: 150 width: 150
}, },
{ {
@ -96,8 +98,8 @@ const columns = [
}, },
{ {
title: '备注', title: '备注',
key: 'recordUser', key: 'description',
dataIndex: 'recordUser' dataIndex: 'description'
}, },
{ {
title: '操作', title: '操作',
@ -132,15 +134,14 @@ const handleEdit = (record: any) => {
const handleDelete = (record: any) => { const handleDelete = (record: any) => {
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: '确定要删除选中的记录吗?', content: '注意,删除此规则后,绑定他的预警配置将会同步删除,确定要删除选中的记录吗?',
zIndex: 2002, zIndex: 2002,
onOk: async () => { onOk: async () => {
try { try {
// let res = await deleteTiltPhoto({ id: record.id }); let res:any = await warnruleDelete({ id: record.id });
message.success('删除成功'); if(res.code == 0) message.success('删除成功');
basicTable.value.refresh(); basicTable.value.refresh();
} catch (error) { } catch (error) {
message.error('删除失败');
return; return;
} }
} }
@ -197,38 +198,25 @@ const editModalCancel = () => {
// //
const handleEditSubmit = async (values: any) => { const handleEditSubmit = async (values: any) => {
// debugger
try { try {
// let res = await saveTiltPhoto({ let res:any = await warnruleAddOrUpdate(values);
// ...currentRecord.value, if(res.code == 0) message.success(`保存成功`);
// ...values
// });
message.success(`保存成功`);
editModalVisible.value = false; editModalVisible.value = false;
basicTable.value.refresh(); basicTable.value.refresh();
} catch (error) { } catch (error) {
message.error(`保存失败`);
editModalVisible.value = false; editModalVisible.value = false;
return; return;
} }
}; };
const riverOptions = ref<any>([]); const riverOptions = ref<any>([
{ value: 'RZ_RULE', label: '水位' },
{ value: 'QGC_RULE', label: '生态流量' },
{ value: 'WT_RULE', label: '水温' },
{ value: 'WQ_RULE', label: '水质' },
]);
const riverOptionsLoad = ref(false); const riverOptionsLoad = ref(false);
const initOption = () => { const initOption = () => {};
const params = {};
riverOptionsLoad.value = true;
dictGetRemoteDictValue({ dictCode: 'WARN_RULE_TYPE' }).then((res: any) => {
res.data.forEach((item: any) => {
item.value = item.dictValue;
item.label = item.dictMeaning;
});
riverOptions.value = res.data;
riverOptionsLoad.value = false;
});
};
onMounted(() => { onMounted(() => {
initOption(); initOption();
}); });

View File

@ -5,7 +5,7 @@ export default {
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref, nextTick } from 'vue'; import { onMounted, reactive, ref, nextTick, watch } from 'vue';
import { ElMessage, ElMessageBox, FormRules } from 'element-plus'; import { ElMessage, ElMessageBox, FormRules } from 'element-plus';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
@ -31,6 +31,14 @@ const treedata: any = ref([]);
const treeRef = ref(); const treeRef = ref();
const treeId = ref(''); const treeId = ref('');
const defaultProps = { label: 'dictName' }; const defaultProps = { label: 'dictName' };
const filterText = ref('');
function filterNode(value: string, data: any) {
if (!value) return true;
return data.dictName?.includes(value);
}
watch(filterText, val => {
treeRef.value?.filter(val);
});
// //
const title = ref(''); const title = ref('');
const dialogdict = ref(false); const dialogdict = ref(false);
@ -432,6 +440,13 @@ const total = ref();
/> />
新增字典</el-button 新增字典</el-button
> >
<el-input
v-model="filterText"
placeholder="搜索字典"
clearable
style="margin-bottom: 10px"
size="small"
/>
<el-tree <el-tree
v-loading="treeloading" v-loading="treeloading"
ref="treeRef" ref="treeRef"
@ -444,6 +459,7 @@ const total = ref();
:allow-drop="allowDrop" :allow-drop="allowDrop"
:data="treedata" :data="treedata"
draggable draggable
:filter-node-method="filterNode"
:highlight-current="true" :highlight-current="true"
:props="defaultProps" :props="defaultProps"
@node-click="handleNodeClick" @node-click="handleNodeClick"