增殖放流和珍稀动植物
This commit is contained in:
commit
4af0c79ac5
35
frontend/src/api/zxdzw/index.ts
Normal file
35
frontend/src/api/zxdzw/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
|
||||
export function vpcGetKendoListCust(data: any) {
|
||||
return request({
|
||||
url: '/api/wmp-env-server/env/vpc/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 植物园建设及接入情况 - 表格数据
|
||||
export function vmsstbprptGetKendoList(data: any) {
|
||||
return request({
|
||||
url: '/api/dec-lygk-base-server/base/vmsstbprpt/GetKendoList',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 动物救助情况
|
||||
export function wvaGetKendoListCust(data: any) {
|
||||
return request({
|
||||
url: '/api/wmp-env-server/env/wva/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
//植物园情况介绍
|
||||
// export function vpcGetKendoListCust(data: any) {
|
||||
// return request({
|
||||
// url: '/api/wmp-env-server/env/vpc/GetKendoListCust',
|
||||
// method: 'post',
|
||||
// data
|
||||
// });
|
||||
// }
|
||||
76
frontend/src/api/zzfl/index.ts
Normal file
76
frontend/src/api/zzfl/index.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 增殖站建设运行情况 - 概览统计
|
||||
export function overviewGetKendoListCust(data: any) {
|
||||
return request({
|
||||
url: '/fb/station/overview/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 增殖站概况详情 - 全国概览接口(all=true时使用)
|
||||
export function qgcoverviewGetOverviewSecond(data: any) {
|
||||
return request({
|
||||
url: '/fb/station/qgcoverview/getOverviewSecond',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 增殖站概况详情 - 普通接口(all=false时使用)
|
||||
export function overviewGetOverviewSecond(data: any) {
|
||||
return request({
|
||||
url: '/fb/station/overview/getOverviewSecond',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
//增殖站介绍 /api/dec-lygk-base-server/base/msstbprpt/GetKendoList
|
||||
export function msstbprptGetKendoList(data: any) {
|
||||
return request({
|
||||
url: '/fb/msstbprpt/GetKendoList',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 增殖站运行数据统计
|
||||
export function qgcgetFbStaticsData(data: any) {
|
||||
return request({
|
||||
url: '/fb/station/qgc/getFbStaticsData',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 增殖计划完成情况,下拉框 ///////
|
||||
export function fishTypegetStcdList(data: any) {
|
||||
return request({
|
||||
url: '/fb/fishType/getStcdList',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
//增殖计划完成情况-数量完成情况//////////////
|
||||
export function fishCountGetKendoListCust(data: any) {
|
||||
return request({
|
||||
url: '/fb/fishCount/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
//增殖计划完成情况-种类完成情况///////////
|
||||
export function fishTypeGetKendoListCust(data: any) {
|
||||
return request({
|
||||
url: '/fb/fishType/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
// 增殖站运行情况统计列表 (TwoLayers 弹窗表格) ///////////////
|
||||
export function msfbrdmGetKendoListCust(data: any) {
|
||||
return request({
|
||||
url: '/fb/msfbrdm/qgc/GetKendoListCust',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
@ -215,7 +215,7 @@ const getList = async (filter?: Record<string, any>) => {
|
||||
let finalTotal = 0;
|
||||
|
||||
if (props.transformData) {
|
||||
const result = props.transformData(res);
|
||||
const result:any = props.transformData(res);
|
||||
finalRecords = result.records || result.data || [];
|
||||
finalTotal = result.total || 0;
|
||||
} else {
|
||||
@ -392,8 +392,11 @@ const enhancedColumns = computed(() => {
|
||||
}
|
||||
|
||||
// 处理 merge 配置
|
||||
if (enhancedCol.merge && enhancedCol.dataIndex) {
|
||||
enhancedCol.customCell = createMergeCellHandler(enhancedCol.dataIndex);
|
||||
if (enhancedCol.merge) {
|
||||
const mergeField = enhancedCol.dataIndex || enhancedCol.mergeWith;
|
||||
if (mergeField) {
|
||||
enhancedCol.customCell = createMergeCellHandler(mergeField);
|
||||
}
|
||||
|
||||
// 如果有 slots.customRender,移除 slots
|
||||
if (enhancedCol.slots?.customRender) {
|
||||
|
||||
@ -295,7 +295,14 @@ watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (active && !hasLoaded.value) {
|
||||
getYearList();
|
||||
console.log('isActive', modelStore.filter.year);
|
||||
if (modelStore.filter.year) {
|
||||
searchParams.value.tm = modelStore.filter.year;
|
||||
getFtpOptions();
|
||||
} else {
|
||||
getYearList();
|
||||
}
|
||||
|
||||
hasLoaded.value = true;
|
||||
}
|
||||
},
|
||||
|
||||
@ -150,7 +150,13 @@ watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (active && !hasLoaded.value) {
|
||||
getYearList();
|
||||
// getYearList();
|
||||
if (modelStore.filter.year) {
|
||||
searchParams.value.tm = modelStore.filter.year;
|
||||
refreshTable();
|
||||
} else {
|
||||
getYearList();
|
||||
}
|
||||
hasLoaded.value = true;
|
||||
}
|
||||
},
|
||||
|
||||
@ -209,7 +209,7 @@
|
||||
v-show="currentActiveKey === 'ProcessDiagram'"
|
||||
:is-active="currentActiveKey === 'ProcessDiagram'"
|
||||
/>
|
||||
<!-- 增值放流情况 -->
|
||||
<!-- 增殖放流情况 -->
|
||||
<NormalAddedSituation
|
||||
v-show="currentActiveKey === 'NormalAddedSituation'"
|
||||
:is-active="currentActiveKey === 'NormalAddedSituation'"
|
||||
@ -405,63 +405,6 @@ const isLCJ = env?.startsWith('lcj');
|
||||
const sttpList = ['ENG', 'WT', 'WQ', 'FP', 'FB', 'VP', 'DW', 'EQ', 'VA'];
|
||||
|
||||
// ==================== 异步 Tab 过滤逻辑 ====================
|
||||
|
||||
// 功能6: 垂向水温与 AI 识别动态过滤
|
||||
const checkVerticalWaterAndAI = async (stcd: string) => {
|
||||
try {
|
||||
const res = await getStInfoByStcd({ stcd });
|
||||
const data = res?.data || res;
|
||||
if (data && data.relList) {
|
||||
const jianCei = data.relList.find(
|
||||
(item: any) => item.sttpCode === 'WTVT'
|
||||
);
|
||||
const aiShiBie = data.relList.filter((item: any) =>
|
||||
item?.sttpCode?.startsWith('AI')
|
||||
);
|
||||
|
||||
if (!jianCei) {
|
||||
// 无垂向水温子站点,过滤掉垂向水温 Tab
|
||||
tabsConfig.value = tabsConfig.value.filter(
|
||||
(item: any) => item.type !== 'VerticalWaterTemperature'
|
||||
);
|
||||
} else {
|
||||
modelStore.verticalWaterStcd = jianCei.stcd;
|
||||
}
|
||||
|
||||
if (!aiShiBie.length) {
|
||||
// 无 AI 识别子站点,过滤掉 AI 识别 Tab
|
||||
tabsConfig.value = tabsConfig.value.filter(
|
||||
(item: any) => item.type !== 'AIYXSB'
|
||||
);
|
||||
} else {
|
||||
modelStore.aiSites = aiShiBie;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('垂向水温/AI 检查失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 功能7: 视频存在性检查
|
||||
const checkVideoExists = async (stcd: string) => {
|
||||
try {
|
||||
const res = await getVideoList({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [{ field: 'stcd', operator: 'eq', value: stcd }]
|
||||
}
|
||||
});
|
||||
if (!res?.data?.data?.length && !res?.data?.records?.length) {
|
||||
// 无视频数据,过滤掉视频 Tab
|
||||
tabsConfig.value = tabsConfig.value.filter(
|
||||
(item: any) => item.type !== 'video'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频检查失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 功能8: 鱼类栖息地数据类型检查 (React: isLCJ && sttpCode === 'WE')
|
||||
const checkFishTab = async (stcd: string) => {
|
||||
try {
|
||||
@ -604,7 +547,7 @@ const setQGCTabList = async (_tabs: any, stcd: string) => {
|
||||
// 2. 垂向水温 & AI 识别检查(对所有类型执行,与 React 一致)
|
||||
const res4 = await getStInfoByStcd({ stcd });
|
||||
let newTabs = [...tabs];
|
||||
|
||||
// debugger
|
||||
if (res4 && res4.data && res4.data.relList) {
|
||||
const relList = res4.data.relList;
|
||||
const jianCei = relList.find((item: any) => item.sttpCode === 'WTVT');
|
||||
@ -722,14 +665,14 @@ const applyTabFilters = async (params: any) => {
|
||||
// LCJ 鱼类栖息地数据类型检查
|
||||
await checkFishTab(stcd);
|
||||
} else if (
|
||||
((baseId === '07' || baseId === '02') && sttpCode === 'WQ') ||
|
||||
( sttpCode === 'WQ') ||
|
||||
sttpCode === 'WQFB' ||
|
||||
sttpCode === 'wq'
|
||||
) {
|
||||
// 水质 AI 预测检查(React 逻辑:WQ 需要条件,WQFB/wq 无条件)
|
||||
await checkAnalysisAvailability(stcd);
|
||||
} else if (
|
||||
((baseId === '07' || baseId === '02') && sttpCode === 'WT') ||
|
||||
( sttpCode === 'WT') ||
|
||||
sttpCode === 'wt'
|
||||
) {
|
||||
// 水温站特殊处理(React 逻辑:WT 需要条件,wt 无条件)
|
||||
|
||||
@ -64,6 +64,7 @@
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@change="handleChange"
|
||||
@select="handleTreeSelect1"
|
||||
></a-select>
|
||||
</div>
|
||||
<div v-if="shrink" class="title_shrink" @click="isExpand = !isExpand">
|
||||
@ -668,7 +669,16 @@ const handleTreeSelect = (selectedKeys: string | string[], info: any) => {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleTreeSelect1 = (selectedKeys: string | string[], info: any) => {
|
||||
// debugger
|
||||
// 保存选中节点的额外属性(如 lgtd、lttd 等)
|
||||
if (info && typeof info === 'object') {
|
||||
selectedNodeExtra.value = {
|
||||
lgtd: info.lgtd,
|
||||
lttd: info.lttd
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 处理树节点展开/收起事件
|
||||
* @param keys 展开的节点键值数组
|
||||
@ -679,6 +689,7 @@ const handleTreeExpand = (keys: string[]) => {
|
||||
|
||||
// 下拉选择框事件处理
|
||||
const handleChange = (value: string) => {
|
||||
|
||||
// Handle change
|
||||
};
|
||||
|
||||
|
||||
@ -1,76 +1,94 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="植物园情况介绍">
|
||||
<div class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
|
||||
<!-- 跑马灯轨道容器 -->
|
||||
<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">
|
||||
<!-- 图片 -->
|
||||
<img :src="item.url" alt="" />
|
||||
<!-- 说明文字(随媒体项移动) -->
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
<SidePanelItem title="植物园情况介绍">
|
||||
<a-spin :spinning="loading">
|
||||
<template v-if="originalMediaData.length > 0">
|
||||
<div
|
||||
class="container"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<!-- 跑马灯轨道容器 -->
|
||||
<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="handleItemClick"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<img :src="item.url" alt="" />
|
||||
<!-- 说明文字(随媒体项移动) -->
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 面板指示器(固定在底部右侧) -->
|
||||
<div class="pagination-dots-fixed">
|
||||
<span
|
||||
v-for="(dot, index) in originalMediaData"
|
||||
:key="index"
|
||||
class="dot"
|
||||
:class="{ active: getCurrentRealIndex() === index }"
|
||||
@click="goToSlide(index)"
|
||||
></span>
|
||||
</div>
|
||||
<!-- 面板指示器(固定在底部右侧) -->
|
||||
<div class="pagination-dots-fixed">
|
||||
<span
|
||||
v-for="(dot, index) in originalMediaData"
|
||||
:key="index"
|
||||
class="dot"
|
||||
:class="{ active: getCurrentRealIndex() === index }"
|
||||
@click="goToSlide(index)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的文字说明区域(随跑马灯切换而变化) -->
|
||||
<div class="description-text">
|
||||
{{ currentDescription }}
|
||||
<div class="description-text" @click="handleItemClick">
|
||||
{{ currentDescription }}
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
<a-empty v-else description="暂无数据" />
|
||||
</a-spin>
|
||||
|
||||
<!-- 设施详情弹框 -->
|
||||
<a-modal v-model:open="modalVisible" :title="'植物园详情'" width="1536px" :footer="null">
|
||||
<div v-if="currentItem">
|
||||
<ArtsDetail :dataSource="currentItem" :index="getCurrentRealIndex()" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { vpcGetKendoListCust } from '@/api/zxdzw';
|
||||
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'zengZhiZhanJieShaoMod'
|
||||
name: 'zengZhiZhanJieShaoMod'
|
||||
});
|
||||
|
||||
// 媒体类型定义
|
||||
interface MediaItem {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
description: string;
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
description: string;
|
||||
}
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const baseid = ref('');
|
||||
|
||||
// 原始媒体数据(3条测试数据)
|
||||
const originalMediaData = ref<MediaItem[]>([
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20230814205611342377136845462200&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920',
|
||||
text: '松岗鱼类增殖站',
|
||||
description: '松岗鱼类增殖放流站位于四川省阿坝藏族羌族自治州马尔康市松岗镇,主要服务于大渡河上游的双江口和金川两座水电站,同时还承担多种珍稀特有鱼类的救护和科研任务,实现工程建设与生态环境共同推进、相互促进。'
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
url: 'https://211.99.26.225:12125/?20230805205848575430105387253710&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920', // 视频URL示例,您可替换为真实地址
|
||||
text: '猴子岩鱼类增殖站',
|
||||
description: '猴子岩水电站鱼类增殖放流站位于猴子岩水电站坝址下游约7.0km(业主营地下游约1.5km),大渡河左岸桃花渣场顶部平台上,紧邻枢纽桃花大桥下游侧,占地面积47.3亩,其中一期工程27.0亩,预留二期工程用地20.3亩(二期工程目前为丹巴、巴底水电站预留工程)增殖放流站工作流程为:亲鱼收集购买、亲鱼驯养培育、人工催产和授精、人工孵化、苗种培育和放流。 猴子岩鱼类增殖放流站近期放流对象中齐口裂腹鱼、重口裂腹鱼、大渡软刺裸裂尻鱼增殖放流技术水平已趋于熟化,中期放流对象大渡软刺裸裂尻鱼人工繁殖技术逐渐趋于熟化'
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20230805205924378504010675106305&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920',
|
||||
text: '黑马鱼类增殖站',
|
||||
description: '大渡河黑马鱼类增殖放流站位于四川省甘洛县黑马乡黑马业主营地内,距离甘洛县城45km,区域交通路况较好。主要承担瀑布沟、深溪沟、大岗山、枕头坝一级、沙坪二级等五座水电站鱼类增殖放流的重任。放流鱼类包含:齐口裂腹鱼、重口裂腹鱼、鲈鲤、长薄鳅、白甲鱼、中华倒刺?、长吻脆、稀有鮊鲫、华鲮、侧沟爬岩鳅等10个种类,共计约918.07万尾珍稀特有鱼苗。'
|
||||
}
|
||||
]);
|
||||
// 弹框控制
|
||||
const modalVisible = ref(false);
|
||||
const currentItem = ref<any>(null);
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 原始媒体数据
|
||||
const originalMediaData = ref<MediaItem[]>([]);
|
||||
|
||||
// 克隆首尾项后的渲染数组(用于无缝循环)
|
||||
// 结构:[最后一项克隆, 原始数据..., 第一项克隆]
|
||||
@ -90,197 +108,257 @@ const isTransitioning = ref(false);
|
||||
|
||||
// 初始化渲染数组(克隆首尾项)
|
||||
const initRenderData = () => {
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
|
||||
renderMediaData.value = [
|
||||
originalMediaData.value[length - 1], // 克隆最后一项
|
||||
...originalMediaData.value, // 原始数据
|
||||
originalMediaData.value[0] // 克隆第一项
|
||||
];
|
||||
renderMediaData.value = [
|
||||
originalMediaData.value[length - 1], // 克隆最后一项
|
||||
...originalMediaData.value, // 原始数据
|
||||
originalMediaData.value[0] // 克隆第一项
|
||||
];
|
||||
};
|
||||
|
||||
// 启动自动轮播
|
||||
const startAutoPlay = () => {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 4000);
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// 切换到下一张
|
||||
const nextSlide = () => {
|
||||
currentIndex.value++;
|
||||
currentIndex.value++;
|
||||
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
};
|
||||
|
||||
// 检查是否需要无缝跳转(克隆项处理)
|
||||
const checkSeamlessJump = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
const realLength = originalMediaData.value.length;
|
||||
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
isHovering.value = false;
|
||||
};
|
||||
|
||||
// 计算当前显示的描述文字
|
||||
const currentDescription = computed(() => {
|
||||
const realIndex = getCurrentRealIndex();
|
||||
return originalMediaData.value[realIndex]?.description || '';
|
||||
});
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
const realIndex = getCurrentRealIndex();
|
||||
return originalMediaData.value[realIndex]?.description || '';
|
||||
});
|
||||
|
||||
// 获取当前真实的索引(排除克隆项的影响)
|
||||
const getCurrentRealIndex = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
||||
const realLength = originalMediaData.value.length;
|
||||
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
||||
|
||||
// 处理边界情况(无缝循环时的克隆项)
|
||||
if (realIndex < 0) realIndex = realLength - 1;
|
||||
if (realIndex >= realLength) realIndex = 0;
|
||||
// 处理边界情况(无缝循环时的克隆项)
|
||||
if (realIndex < 0) realIndex = realLength - 1;
|
||||
if (realIndex >= realLength) realIndex = 0;
|
||||
|
||||
return realIndex;
|
||||
return realIndex;
|
||||
};
|
||||
|
||||
// 跳转到指定幻灯片
|
||||
const goToSlide = (targetIndex: number) => {
|
||||
if (isTransitioning.value) return;
|
||||
if (isTransitioning.value) return;
|
||||
|
||||
// 计算目标索引(考虑克隆项偏移)
|
||||
currentIndex.value = targetIndex + 1;
|
||||
// 计算目标索引(考虑克隆项偏移)
|
||||
currentIndex.value = targetIndex + 1;
|
||||
};
|
||||
|
||||
// 处理项目点击事件
|
||||
const handleItemClick = () => {
|
||||
currentItem.value = originalMediaData.value.map((item: any) => ({
|
||||
url: item.url || '',
|
||||
description: item.description || '',
|
||||
title: item.text || ''
|
||||
}));
|
||||
modalVisible.value = true;
|
||||
};
|
||||
|
||||
const getData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
let params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: baseid.value
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
let res = await vpcGetKendoListCust(params);
|
||||
let data = res?.data?.data;
|
||||
if (data && data.length > 0) {
|
||||
originalMediaData.value = data.map((item: any) => {
|
||||
return {
|
||||
type: 'image',
|
||||
url: baseUrl + '?' + item.logo + '&view=jpg',
|
||||
text: item.stnm,
|
||||
description: item.introduce
|
||||
};
|
||||
});
|
||||
} else {
|
||||
originalMediaData.value = [];
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
async newVal => {
|
||||
if (newVal && newVal.wbsCode) {
|
||||
baseid.value = newVal.wbsCode;
|
||||
await getData();
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
// border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
// border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
|
||||
// 跑马灯轨道
|
||||
.carousel-track {
|
||||
display: flex; // 横向排列所有媒体项
|
||||
// 跑马灯轨道
|
||||
.carousel-track {
|
||||
display: flex; // 横向排列所有媒体项
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease-in-out; // 0.5秒平滑过渡
|
||||
|
||||
// 禁用过渡动画(用于无缝跳转)
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 单个媒体项
|
||||
.carousel-item {
|
||||
min-width: 100%; // 每个项目占满容器宽度
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
cursor: pointer;
|
||||
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease-in-out; // 0.5秒平滑过渡
|
||||
object-fit: cover; // 保持比例填充
|
||||
}
|
||||
|
||||
// 禁用过渡动画(用于无缝跳转)
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 单个媒体项
|
||||
.carousel-item {
|
||||
min-width: 100%; // 每个项目占满容器宽度
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; // 保持比例填充
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 固定的面板指示器
|
||||
.pagination-dots-fixed {
|
||||
.text {
|
||||
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;
|
||||
}
|
||||
}
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 固定的面板指示器
|
||||
.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 {
|
||||
// padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
// min-height: 40px;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
// padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
// min-height: 40px;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
// 多行文本溢出省略
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
// 多行文本溢出省略
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@ -1,56 +1,64 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="动物救助情况">
|
||||
<div class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
|
||||
<!-- 跑马灯轨道容器 -->
|
||||
<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">
|
||||
<!-- 图片 -->
|
||||
<a-image :src="item.url" />
|
||||
<!-- 说明文字 -->
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidePanelItem title="动物救助情况">
|
||||
<!-- Loading 状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<a-spin />
|
||||
</div>
|
||||
|
||||
<!-- 暂无数据 -->
|
||||
<div v-else-if="originalMediaData.length === 0" class="empty-container">
|
||||
<a-empty description="暂无数据" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="container"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<!-- 跑马灯轨道容器 -->
|
||||
<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"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<a-image :src="item.url" />
|
||||
<!-- 说明文字 -->
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { wvaGetKendoListCust } from '@/api/zxdzw';
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
|
||||
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
|
||||
});
|
||||
|
||||
// 媒体类型定义
|
||||
interface MediaItem {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
url: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
// 原始媒体数据(3条测试数据)
|
||||
const originalMediaData = ref<MediaItem[]>([
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20250709001636453909987812803850&view=jpg&token=bearer 0a2549fe-9bc6-4bef-b098-1344e569c395',
|
||||
text: '糯扎渡动物救护站 2025-04-11 15'
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20250508094042468361071230016027&view=jpg&token=bearer fee092f0-b748-4019-bcbb-1386fa8c4fa4', // 视频URL示例,您可替换为真实地址
|
||||
text: '野生动物救助中心 2025-04-12 10'
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20250709001636453909987812803850&view=jpg&token=bearer 0a2549fe-9bc6-4bef-b098-1344e569c395',
|
||||
text: '珍稀动物保护基地 2025-04-13 14'
|
||||
}
|
||||
]);
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const baseid = ref('');
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
const loading = ref(false);
|
||||
// 原始媒体数据
|
||||
const originalMediaData = ref<MediaItem[]>([]);
|
||||
|
||||
// 克隆首尾项后的渲染数组(用于无缝循环)
|
||||
// 结构:[最后一项克隆, 原始数据..., 第一项克隆]
|
||||
@ -70,139 +78,214 @@ const isTransitioning = ref(false);
|
||||
|
||||
// 初始化渲染数组(克隆首尾项)
|
||||
const initRenderData = () => {
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
|
||||
renderMediaData.value = [
|
||||
originalMediaData.value[length - 1], // 克隆最后一项
|
||||
...originalMediaData.value, // 原始数据
|
||||
originalMediaData.value[0] // 克隆第一项
|
||||
];
|
||||
renderMediaData.value = [
|
||||
originalMediaData.value[length - 1], // 克隆最后一项
|
||||
...originalMediaData.value, // 原始数据
|
||||
originalMediaData.value[0] // 克隆第一项
|
||||
];
|
||||
};
|
||||
|
||||
// 启动自动轮播
|
||||
const startAutoPlay = () => {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 2000);
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// 切换到下一张
|
||||
const nextSlide = () => {
|
||||
currentIndex.value++;
|
||||
currentIndex.value++;
|
||||
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
};
|
||||
|
||||
// 检查是否需要无缝跳转(克隆项处理)
|
||||
const checkSeamlessJump = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
const realLength = originalMediaData.value.length;
|
||||
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
isHovering.value = false;
|
||||
};
|
||||
const getData = async () => {
|
||||
loading.value = true;
|
||||
let params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'recordUser',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: '1'
|
||||
},
|
||||
baseid.value != 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: baseid.value
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
}
|
||||
};
|
||||
const res = await wvaGetKendoListCust(params);
|
||||
if (res?.data?.data?.length > 0) {
|
||||
// 过滤掉 imgPath 为 null 的对象
|
||||
const filteredData = res.data.data
|
||||
originalMediaData.value = filteredData.map((item: any) => {
|
||||
return {
|
||||
url: `${baseUrl}?${item.fid}&view=jpg`,
|
||||
text: item.flnm
|
||||
};
|
||||
});
|
||||
} else {
|
||||
originalMediaData.value = [];
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
newVal => {
|
||||
if (newVal && newVal.wbsCode) {
|
||||
baseid.value = newVal.wbsCode;
|
||||
getData();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 监听原始数据变化,重新初始化渲染数据
|
||||
watch(originalMediaData, () => {
|
||||
initRenderData();
|
||||
currentIndex.value = 1; // 重置索引
|
||||
});
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
// Loading 容器
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height:290px;
|
||||
}
|
||||
|
||||
// 暂无数据容器
|
||||
.empty-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 290px;
|
||||
border: 1px solid #7fd6ff;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 290px;
|
||||
border: 1px solid #7fd6ff;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
|
||||
// 跑马灯轨道
|
||||
.carousel-track {
|
||||
display: flex; // 横向排列所有媒体项
|
||||
|
||||
|
||||
// 跑马灯轨道
|
||||
.carousel-track {
|
||||
display: flex; // 横向排列所有媒体项
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease-in-out; // 0.5秒平滑过渡
|
||||
|
||||
// 禁用过渡动画(用于无缝跳转)
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 单个媒体项
|
||||
.carousel-item {
|
||||
width: 100% !important; // 每个项目占满容器宽度
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease-in-out; // 0.5秒平滑过渡
|
||||
object-fit: cover; // 保持比例填充
|
||||
}
|
||||
|
||||
// 禁用过渡动画(用于无缝跳转)
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
// Ant Design Vue 的 a-image 组件样式
|
||||
:deep(.ant-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
// 单个媒体项
|
||||
.carousel-item {
|
||||
width: 100% !important; // 每个项目占满容器宽度
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; // 保持比例填充
|
||||
}
|
||||
|
||||
// Ant Design Vue 的 a-image 组件样式
|
||||
:deep(.ant-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
line-height: 40px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
line-height: 40px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.ant-empty-description){
|
||||
text-indent:0em !important;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="zwy-detail-table">
|
||||
<!-- 搜索表单 -->
|
||||
<a-form layout="inline" class="search-form">
|
||||
<a-form-item label="所属基地">
|
||||
<a-select
|
||||
v-model:value="jidiValue"
|
||||
placeholder="请选择"
|
||||
style="width: 150px"
|
||||
:options="jidiOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="建设状态">
|
||||
<a-select
|
||||
v-model:value="buildStatus"
|
||||
placeholder="请选择"
|
||||
style="width: 120px"
|
||||
:options="buildStatusOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="">
|
||||
<a-input
|
||||
v-model:value="searchInput"
|
||||
placeholder="请输入设施名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">查询</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-wrapper">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:scroll-y="500"
|
||||
:scroll-x="800"
|
||||
:list-url="listUrl"
|
||||
:search-params="searchParams"
|
||||
:transform-data="customTransform"
|
||||
>
|
||||
<template #stnm="{ record }">
|
||||
<!-- 操作列 -->
|
||||
<a @click="handleViewDetail(record, 'VP')" class="text-link">
|
||||
{{ record.stnm }}
|
||||
</a>
|
||||
</template>
|
||||
<template #ennm="{ record }">
|
||||
<!-- 操作列 -->
|
||||
<a @click="handleViewDetail(record, 'ENG')" class="text-link">
|
||||
{{ record.ennm }}
|
||||
</a>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { vmsstbprptGetKendoList } from '@/api/zxdzw';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
const modelStore = useModelStore();
|
||||
// ==================== Props ====================
|
||||
const props = defineProps<{
|
||||
jidi?: string; // 基地ID
|
||||
jidiList?: Array<{ label: string; value: string }>; // 基地列表
|
||||
}>();
|
||||
|
||||
// ==================== 搜索状态 ====================
|
||||
const jidiValue = ref(props.jidi || 'all');
|
||||
const buildStatus = ref(''); // 建设状态
|
||||
const searchInput = ref('');
|
||||
|
||||
// 建设状态选项(写死)
|
||||
const buildStatusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '未建', value: '0' },
|
||||
{ label: '在建', value: '1' },
|
||||
{ label: '已建', value: '2' }
|
||||
];
|
||||
|
||||
// 基地选项
|
||||
const jidiOptions = computed(() => {
|
||||
if (props.jidiList && props.jidiList.length > 0) {
|
||||
return [{ label: '全部', value: 'all' }, ...props.jidiList];
|
||||
}
|
||||
return [{ label: '全部', value: 'all' }];
|
||||
});
|
||||
|
||||
// ==================== 接口URL ====================
|
||||
const listUrl = vmsstbprptGetKendoList;
|
||||
|
||||
// ==================== 搜索参数 ====================
|
||||
const searchParams = computed(() => ({
|
||||
sort: [
|
||||
{
|
||||
"field": "rvcdStepSort",
|
||||
"dir": "asc"
|
||||
},
|
||||
{
|
||||
"field": "rstcdStepSort",
|
||||
"dir": "asc"
|
||||
}
|
||||
],
|
||||
group: [],
|
||||
select: [],
|
||||
groupResultFlat: false
|
||||
}));
|
||||
|
||||
// ==================== 表格列配置 ====================
|
||||
const columns = [
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '设施名称',
|
||||
dataIndex: 'stnm',
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
slots: { customRender: 'stnm' }
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所在基地',
|
||||
dataIndex: 'baseName',
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
slots: { customRender: 'ennm' }
|
||||
},
|
||||
{
|
||||
key: 'bldsttCcodeName',
|
||||
title: '建设状态',
|
||||
dataIndex: 'bldsttCcodeName',
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ststdt',
|
||||
title: '开工日期',
|
||||
dataIndex: 'ststdt',
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'jcdt',
|
||||
title: '建成日期',
|
||||
dataIndex: 'jcdt',
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'dtinName',
|
||||
title: '接入状态',
|
||||
dataIndex: 'dtinName',
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
}
|
||||
];
|
||||
|
||||
// ==================== 表格引用 ====================
|
||||
const tableRef = ref();
|
||||
|
||||
// ==================== 过滤条件构建 ====================
|
||||
const getFilter = () => {
|
||||
const filters: any[] = [
|
||||
{
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'VP'
|
||||
}
|
||||
];
|
||||
|
||||
// 基地筛选
|
||||
if (jidiValue.value && jidiValue.value !== 'all') {
|
||||
filters.push({
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: jidiValue.value
|
||||
});
|
||||
}
|
||||
|
||||
// 建设状态筛选
|
||||
if (buildStatus.value !== '') {
|
||||
filters.push({
|
||||
field: 'bldsttCcode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: buildStatus.value
|
||||
});
|
||||
}
|
||||
|
||||
// 设施名称搜索
|
||||
if (searchInput.value) {
|
||||
filters.push({
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: searchInput.value
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
logic: 'and',
|
||||
filters
|
||||
};
|
||||
};
|
||||
|
||||
// ==================== 数据转换 ====================
|
||||
const customTransform = (res: any) => {
|
||||
return {
|
||||
records: res?.data?.data || [],
|
||||
total: res?.data?.total || 0
|
||||
};
|
||||
};
|
||||
|
||||
// ==================== 搜索与重置 ====================
|
||||
const handleSearch = () => {
|
||||
const filter = getFilter();
|
||||
tableRef.value?.getList(filter);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
jidiValue.value = props.jidi || 'all';
|
||||
buildStatus.value = '';
|
||||
searchInput.value = '';
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (record: any, type: any) => {
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params = record;
|
||||
modelStore.title = type == 'ENG' ? record.ennm : record.stnm;
|
||||
modelStore.params.sttp = type;
|
||||
};
|
||||
// ==================== 监听 jidi prop 变化 ====================
|
||||
watch(
|
||||
() => props.jidi,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
jidiValue.value = newVal;
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
// ==================== 挂载时执行 ====================
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zwy-detail-table {
|
||||
width: 100%;
|
||||
|
||||
.search-form {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.text-link {
|
||||
color: #2f6b98;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,135 +1,275 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="植物园情况">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</SidePanelItem>
|
||||
<SidePanelItem title="植物园情况">
|
||||
<a-spin :spinning="loading" tip="加载中...">
|
||||
<div class="chart-wrapper">
|
||||
<div ref="chartRef" class="chart-container" v-show="chartData.length > 0"></div>
|
||||
<a-empty v-show="chartData.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
title="植物园建设情况"
|
||||
width="1536px"
|
||||
:footer="null"
|
||||
|
||||
>
|
||||
<ZhiWuYuanDetailTable
|
||||
v-if="modalVisible"
|
||||
:jidi="modalJidi"
|
||||
:jidi-list="jidiOptions"
|
||||
/>
|
||||
</a-modal>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import { vpcGetKendoListCust } from '@/api/zxdzw';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import ZhiWuYuanDetailTable from './ZhiWuYuanDetailTable.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
|
||||
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
|
||||
});
|
||||
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
|
||||
// 颜色配置
|
||||
const jdColor = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'];
|
||||
// 图表数据
|
||||
const chartData = ref<any[]>([]);
|
||||
|
||||
// 模拟数据
|
||||
const barData = [
|
||||
{ basename: '栖息地A', count: 45 },
|
||||
{ basename: '栖息地B', count: 32 },
|
||||
{ basename: '栖息地C', count: 68 },
|
||||
{ basename: '栖息地D', count: 25 },
|
||||
{ basename: '栖息地E', count: 53 },
|
||||
{ basename: '栖息地F', count: 41 }
|
||||
// loading 状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 弹窗状态
|
||||
const modalVisible = ref(false);
|
||||
const modalJidi = ref('all');
|
||||
|
||||
// 基地选项(用于弹窗中的下拉)
|
||||
const jidiOptions = ref<any[]>([]);
|
||||
|
||||
// 颜色配置
|
||||
const jdColor = [
|
||||
'#5470c6',
|
||||
'#91cc75',
|
||||
'#fac858',
|
||||
'#ee6666',
|
||||
'#73c0de',
|
||||
'#3ba272',
|
||||
'#fc8452',
|
||||
'#9a60b4'
|
||||
];
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (!chartRef.value) return;
|
||||
|
||||
if (!chartInstance) {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
} else {
|
||||
chartInstance.resize();
|
||||
}
|
||||
|
||||
const option: any = {
|
||||
color: jdColor,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: barData.map((v: any) => v.basename),
|
||||
axisLabel: {
|
||||
rotate: 30, // 标签旋转角度
|
||||
interval: 0 // 显示所有标签
|
||||
},
|
||||
axisTick: {
|
||||
alignWithLabel: true // 刻度与标签对齐
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '数量(个)',
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#eee',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: '46%',
|
||||
barGap: '60%',
|
||||
data: barData.map(item => item.count),
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 12,
|
||||
position: 'top',
|
||||
formatter: (params: any) => {
|
||||
return Math.round(params.value);
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
const option: any = {
|
||||
color: jdColor,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.value.map((v: any) => v.basename),
|
||||
axisLabel: {
|
||||
rotate: 30, // 标签旋转角度
|
||||
interval: 0 // 显示所有标签
|
||||
},
|
||||
axisTick: {
|
||||
alignWithLabel: true // 刻度与标签对齐
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '数量(个)',
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#eee',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: '46%',
|
||||
barGap: '60%',
|
||||
data: chartData.value.map(item => item.count),
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 12,
|
||||
position: 'top',
|
||||
formatter: (params: any) => {
|
||||
return Math.round(params.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
chartInstance.setOption(option);
|
||||
|
||||
// 添加点击事件
|
||||
chartInstance.on('click', handleChartClick);
|
||||
};
|
||||
|
||||
// 处理图表点击事件
|
||||
const handleChartClick = (params: any) => {
|
||||
// 只在点击柱状图数据时触发,忽略坐标轴、图例等其他区域
|
||||
if (params.componentType !== 'series') return;
|
||||
|
||||
// 根据柱状图名称(基地名称)查找对应的基地代码
|
||||
const jidiItem = JidiSelectEventStore.jidiData.find(
|
||||
(item: any) => item.wbsName === params.name
|
||||
);
|
||||
modalJidi.value = jidiItem?.wbsCode || 'all';
|
||||
modalVisible.value = true;
|
||||
};
|
||||
const getData = async () => {
|
||||
loading.value = true;
|
||||
let params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'isNationwide',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
field: 'isBotanicalGarden',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
group: [
|
||||
{
|
||||
dir: 'asc',
|
||||
field: 'bldstt'
|
||||
},
|
||||
{
|
||||
dir: 'asc',
|
||||
field: 'dtin'
|
||||
},
|
||||
{
|
||||
dir: 'asc',
|
||||
field: 'baseId'
|
||||
}
|
||||
]
|
||||
};
|
||||
try {
|
||||
let res = await vpcGetKendoListCust(params);
|
||||
|
||||
// 解析接口数据
|
||||
const apiData = res?.data?.data?.[0]?.items || [];
|
||||
|
||||
// 按基地代码汇总数量
|
||||
const baseCountMap: Record<string, number> = {};
|
||||
apiData.forEach((dtinGroup: any) => {
|
||||
dtinGroup.items?.forEach((baseItem: any) => {
|
||||
const baseId = baseItem.key;
|
||||
const count = baseItem.count || 0;
|
||||
baseCountMap[baseId] = (baseCountMap[baseId] || 0) + count;
|
||||
});
|
||||
});
|
||||
|
||||
// 按 jidiData 顺序整合数据(过滤掉没有数据的基地,跳过 'all')
|
||||
chartData.value = JidiSelectEventStore.jidiData
|
||||
.filter((item: any) => item.wbsCode !== 'all' && baseCountMap[item.wbsCode])
|
||||
.map((item: any) => ({
|
||||
basename: item.wbsName,
|
||||
count: baseCountMap[item.wbsCode]
|
||||
}));
|
||||
|
||||
// 更新图表
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
});
|
||||
} catch (error) {
|
||||
chartData.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 延迟初始化,确保容器已渲染
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
}, 50);
|
||||
// 初始化基地选项(用于弹窗下拉)
|
||||
jidiOptions.value = JidiSelectEventStore.jidiData
|
||||
.filter((item: any) => item.wbsCode !== 'all')
|
||||
.map((item: any) => ({
|
||||
label: item.wbsName,
|
||||
value: item.wbsCode
|
||||
}));
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
getData();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize();
|
||||
chartInstance?.resize();
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 310px !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 310px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@ -4,13 +4,9 @@
|
||||
:datetimePicker="datetimePicker"
|
||||
@update-values="handlePanelChange"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div v-show="!sourceData.length && !loading" class="empty-wrapper">
|
||||
<a-empty description="暂无数据" />
|
||||
</div>
|
||||
<div v-show="sourceData.length" class="chart-wrapper">
|
||||
<div ref="chartContainer" class="chart-container"></div>
|
||||
</div>
|
||||
<a-spin :spinning="loading" style="width: 100%;">
|
||||
<div ref="chartContainer" class="chart-container" v-show="sourceData.length > 0"></div>
|
||||
<a-empty v-show="sourceData.length === 0" description="暂无数据" class="empty-container" />
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
@ -18,6 +14,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import { sdFprdRGetFtpChangeInfo } from '@/api/yldc';
|
||||
@ -50,8 +47,7 @@ const loading = ref(false);
|
||||
|
||||
// 图表实例
|
||||
const chartContainer = ref<HTMLDivElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let chartInstance: ECharts | null = null;
|
||||
|
||||
// 生成从1月到当前月份的月份列表
|
||||
function getMonthsUntilNow(year: number) {
|
||||
@ -119,10 +115,10 @@ const option = computed<EChartsOption>(() => {
|
||||
textStyle: { fontSize: baseFontSize }
|
||||
},
|
||||
grid: {
|
||||
left: '50px',
|
||||
right: '60px',
|
||||
top: '45px',
|
||||
bottom: '40px'
|
||||
left: 50,
|
||||
right: 60,
|
||||
top: 45,
|
||||
bottom: 40
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
@ -220,6 +216,11 @@ const getData = async () => {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
nextTick(() => {
|
||||
if (sourceData.value.length > 0) {
|
||||
initChart();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
@ -252,9 +253,7 @@ const handlePanelChange = (data: any) => {
|
||||
|
||||
// 窗口大小变化时重新调整图表
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
chartInstance?.resize();
|
||||
};
|
||||
let baseid = ref('');
|
||||
watch(
|
||||
@ -266,73 +265,39 @@ watch(
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 延时确保容器尺寸已完全渲染
|
||||
setTimeout(() => {
|
||||
if (chartContainer.value) {
|
||||
chartInstance = echarts.init(chartContainer.value);
|
||||
chartInstance.setOption(option.value);
|
||||
}
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, 50);
|
||||
});
|
||||
getData();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 监听数据变化,数据到达后初始化图表
|
||||
watch(sourceData, newData => {
|
||||
if (newData && newData.length > 0) {
|
||||
// 先销毁旧的 observer
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
// 使用 ResizeObserver 等待容器有实际尺寸后再初始化图表
|
||||
resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
if (width > 0 && height > 0) {
|
||||
nextTick(() => {
|
||||
if (!chartInstance && chartContainer.value) {
|
||||
initChart();
|
||||
} else {
|
||||
updateChart();
|
||||
}
|
||||
});
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
if (chartContainer.value) {
|
||||
resizeObserver.observe(chartContainer.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
}
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,588 @@
|
||||
<template>
|
||||
<div class="zengzhi-zhan-qingkuang-two-layers">
|
||||
<!-- 搜索表单 -->
|
||||
<a-form layout="inline" class="search-form">
|
||||
<a-form-item label="年份">
|
||||
<a-date-picker
|
||||
v-model:value="years"
|
||||
picker="year"
|
||||
:allow-clear="false"
|
||||
format="YYYY"
|
||||
value-format="YYYY"
|
||||
/>
|
||||
</a-form-item>
|
||||
<!-- 当 baseId 为 'all' 时显示基地选择,否则显示设施名称 -->
|
||||
<a-form-item v-if="baseId === 'all'" label="所属基地">
|
||||
<a-select
|
||||
v-model:value="dataDimensionVal"
|
||||
placeholder="请选择基地"
|
||||
style="width: 180px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in datas"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-else label="设施名称">
|
||||
<a-select
|
||||
v-model:value="dzstcd"
|
||||
placeholder="请选择设施"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in DZOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="运行状态">
|
||||
<a-select
|
||||
v-model:value="runState"
|
||||
placeholder="请选择状态"
|
||||
style="width: 160px"
|
||||
>
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="1">正常运行</a-select-option>
|
||||
<a-select-option value="3">未正常运行</a-select-option>
|
||||
<a-select-option value="2">暂无数据</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="">
|
||||
<a-input
|
||||
v-model:value="search"
|
||||
placeholder="请输入电站或设施名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">查询</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-wrapper">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:scroll-y="500"
|
||||
:scroll-x="1000"
|
||||
:list-url="listUrl"
|
||||
:search-params="searchParams"
|
||||
:transform-data="customTransform"
|
||||
>
|
||||
<template #ennm="{ record }">
|
||||
<template v-if="record.ennm && record.rstcd">
|
||||
<span
|
||||
v-for="(item, index) in parseStationList(record)"
|
||||
:key="index"
|
||||
class="station-item"
|
||||
>
|
||||
<a @click="handleViewDetail(item, 'eng')" class="text-link">{{
|
||||
item.name
|
||||
}}</a>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else>{{ record.ennm || '-' }}</span>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a @click="handleViewDetail(record, 'FB')" class="text-link"
|
||||
>查看详情</a
|
||||
>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
|
||||
<!-- 嵌套详情弹窗 - 后续补充 -->
|
||||
<!-- <a-modal v-model:open="modalVisible" :title="modalTitle" width="800px" :footer="null">
|
||||
详情内容
|
||||
</a-modal> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, h, onMounted } from 'vue';
|
||||
import { msfbrdmGetKendoListCust, fishTypegetStcdList } from '@/api/zzfl';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
const modelStore = useModelStore();
|
||||
// 单位配置模拟
|
||||
const unitVal = '万尾';
|
||||
|
||||
// 数值渲染函数
|
||||
const transUnitRender = (val: any): string | number => {
|
||||
if (val === null || val === undefined || val === '') return '-';
|
||||
const num = Number(val);
|
||||
if (isNaN(num) || num < 0) return '-';
|
||||
// 返回原始数字,由调用方处理格式化
|
||||
return num / 10000;
|
||||
};
|
||||
|
||||
// ==================== Props ====================
|
||||
const props = defineProps<{
|
||||
datas?: any[]; // 基地下拉选项列表
|
||||
nian?: string; // 年份,格式 YYYY
|
||||
data?: any; // 图表点击数据
|
||||
baseId?: string; // 基地ID
|
||||
selectData?: any; // 选中的站点对象
|
||||
}>();
|
||||
|
||||
// ==================== 搜索状态 ====================
|
||||
const years = ref(props.nian || `${new Date().getFullYear()}`);
|
||||
const dataDimensionVal = ref(props.baseId || 'all');
|
||||
const runState = ref(props.data?.dictValue ?? '');
|
||||
const search = ref('');
|
||||
const dzstcd = ref(props.selectData?.value ?? '');
|
||||
const DZOptions = ref<any[]>([]);
|
||||
|
||||
// ==================== 接口URL ====================
|
||||
const listUrl = msfbrdmGetKendoListCust;
|
||||
|
||||
// ==================== 表格列配置 ====================
|
||||
const unit = unitVal;
|
||||
|
||||
const columns = computed(() => [
|
||||
// {
|
||||
// key: 'stcd',
|
||||
// title: '站码',
|
||||
// dataIndex: 'stcd',
|
||||
// visible: false,
|
||||
// width: 100
|
||||
// },
|
||||
// {
|
||||
// key: 'sttpName',
|
||||
// title: '站类',
|
||||
// dataIndex: 'sttpName',
|
||||
// visible: false,
|
||||
// width: 100
|
||||
// },
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '设施名称',
|
||||
dataIndex: 'stnm',
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '所属电站',
|
||||
dataIndex: 'ennm',
|
||||
// width: 180,
|
||||
ellipsis: true,
|
||||
merge: true,
|
||||
slots: { customRender: 'ennm' }
|
||||
},
|
||||
{
|
||||
key: 'baseName',
|
||||
title: '所在基地',
|
||||
dataIndex: 'baseName',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
key: 'ftpName',
|
||||
title: '养殖鱼类',
|
||||
dataIndex: 'ftpName',
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'fcntjh',
|
||||
title: `计划养殖规模(${unit})`,
|
||||
dataIndex: 'fcntjh',
|
||||
width: 160,
|
||||
sort: true,
|
||||
customRender: ({ text, record }: { text: number; record: any }) => {
|
||||
const rendered = transUnitRender(text);
|
||||
const displayVal =
|
||||
typeof rendered === 'number' ? rendered.toFixed(4) : rendered;
|
||||
if (typeof rendered === 'number' && rendered > 0) {
|
||||
return h('div', { style: { color: 'red' } }, displayVal);
|
||||
}
|
||||
return h('div', {}, displayVal);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'fcntjc',
|
||||
title: '实际完成数量(万尾)',
|
||||
dataIndex: 'fcntjc',
|
||||
width: 160,
|
||||
sort: true,
|
||||
customRender: ({ value }: { value: number }) => {
|
||||
const rendered = transUnitRender(value);
|
||||
const displayVal =
|
||||
typeof rendered === 'number' ? rendered.toFixed(4) : rendered;
|
||||
if (typeof rendered === 'number' && rendered > 0) {
|
||||
return h('div', { style: { color: 'red' } }, displayVal);
|
||||
}
|
||||
return h('div', {}, displayVal);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'runStateName',
|
||||
title: '运行状态',
|
||||
dataIndex: 'runStateName',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
merge: true
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
dataIndex: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
merge: true,
|
||||
slots: { customRender: 'action' }
|
||||
}
|
||||
]);
|
||||
|
||||
// ==================== 表格相关 ====================
|
||||
const tableRef = ref();
|
||||
|
||||
const searchParams = computed(() => {
|
||||
const startTime = `${years.value}-01-01 00:00:00`;
|
||||
const endTime = `${years.value}-12-31 23:59:59`;
|
||||
|
||||
const filters: any[] = [
|
||||
{
|
||||
field: 'startTime',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: startTime
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: endTime
|
||||
}
|
||||
];
|
||||
|
||||
if (props.baseId === 'all') {
|
||||
filters.push({
|
||||
field: 'dtin',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 1
|
||||
});
|
||||
if (dataDimensionVal.value !== 'all') {
|
||||
filters.push({
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: dataDimensionVal.value
|
||||
});
|
||||
}
|
||||
} else {
|
||||
filters.push({
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: props.baseId
|
||||
});
|
||||
if (dzstcd.value) {
|
||||
filters.push({
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: dzstcd.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (search.value) {
|
||||
filters.push({
|
||||
field: 'ennm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: search.value
|
||||
});
|
||||
}
|
||||
|
||||
if (!runState.value) {
|
||||
filters.push({
|
||||
field: 'runState',
|
||||
operator: 'in',
|
||||
dataType: 'string',
|
||||
value: ['1', '2', '3']
|
||||
});
|
||||
} else {
|
||||
filters.push({
|
||||
field: 'runState',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: runState.value
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sort: [],
|
||||
group: [],
|
||||
select: [],
|
||||
groupResultFlat: false
|
||||
};
|
||||
});
|
||||
|
||||
const customTransform = (res: any) => {
|
||||
return {
|
||||
records: res?.data?.data || [],
|
||||
total: res?.data?.total || 0
|
||||
};
|
||||
};
|
||||
|
||||
// ==================== 搜索与重置 ====================
|
||||
const getFilter = () => {
|
||||
const startTime = `${years.value}-01-01 00:00:00`;
|
||||
const endTime = `${years.value}-12-31 23:59:59`;
|
||||
|
||||
const filters: any[] = [
|
||||
{
|
||||
field: 'startTime',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: startTime
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: endTime
|
||||
}
|
||||
];
|
||||
|
||||
if (props.baseId === 'all') {
|
||||
filters.push({
|
||||
field: 'dtin',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 1
|
||||
});
|
||||
if (dataDimensionVal.value !== 'all') {
|
||||
filters.push({
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: dataDimensionVal.value
|
||||
});
|
||||
}
|
||||
} else {
|
||||
filters.push({
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: props.baseId
|
||||
});
|
||||
if (dzstcd.value) {
|
||||
filters.push({
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: dzstcd.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (search.value) {
|
||||
filters.push({
|
||||
field: 'ennm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: search.value
|
||||
});
|
||||
}
|
||||
|
||||
if (!runState.value) {
|
||||
filters.push({
|
||||
field: 'runState',
|
||||
operator: 'in',
|
||||
dataType: 'string',
|
||||
value: ['1', '2', '3']
|
||||
});
|
||||
} else {
|
||||
filters.push({
|
||||
field: 'runState',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: runState.value
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
logic: 'and',
|
||||
filters
|
||||
};
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
const filter = getFilter();
|
||||
tableRef.value?.getList(filter);
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
years: `${new Date().getFullYear()}`,
|
||||
dataDimensionVal: props.baseId || 'all',
|
||||
runState: props.data?.dictValue ?? '',
|
||||
search: '',
|
||||
dzstcd: props.selectData?.value ?? ''
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
years.value = initialState.years;
|
||||
dataDimensionVal.value = initialState.dataDimensionVal;
|
||||
runState.value = initialState.runState;
|
||||
search.value = initialState.search;
|
||||
dzstcd.value = initialState.dzstcd;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// ==================== 获取设施选项 ====================
|
||||
const fetchDZOptions = async () => {
|
||||
if (props.baseId === 'all') return;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: props.baseId
|
||||
},
|
||||
{
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'FB'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const res = await fishTypegetStcdList(params);
|
||||
if (res?.data?.data) {
|
||||
DZOptions.value = res.data.data.map((item: any) => ({
|
||||
value: item.stcd,
|
||||
label: item.stnm
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设施选项失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 查看详情 ====================
|
||||
const handleViewDetail = (record: any, type: any) => {
|
||||
// 后续补充:打开详情弹窗
|
||||
if (type == 'FB') {
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params = record;
|
||||
modelStore.title = record.stnm;
|
||||
modelStore.params.sttp = 'FB';
|
||||
modelStore.filter.year = years.value;
|
||||
} else {
|
||||
// debugger
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params.sttp = 'ENG';
|
||||
modelStore.title = record.name;
|
||||
modelStore.params.stcd = record.code;
|
||||
}
|
||||
};
|
||||
|
||||
// 解析电站列表,将 ennm 和 rstcd 按顺序一一对应
|
||||
const parseStationList = (record: any) => {
|
||||
const names = (record.ennm || '').split(',').filter(Boolean);
|
||||
const codes = (record.rstcd || '').split(',').filter(Boolean);
|
||||
return names.map((name, index) => ({
|
||||
name,
|
||||
code: codes[index] || '',
|
||||
baseId: record.baseId,
|
||||
stcd: record.stcd,
|
||||
stnm: record.stnm
|
||||
}));
|
||||
};
|
||||
|
||||
const handleViewStationDetail = (station: any) => {
|
||||
// 点击单个电站,获取该电站的详细数据
|
||||
console.log('查看电站详情:', station);
|
||||
// 后续补充:使用 station.code (rstcd) 请求该电站的具体数据
|
||||
};
|
||||
|
||||
// ==================== 监听 props 变化 ====================
|
||||
watch(
|
||||
() => props.nian,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
years.value = newVal;
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.baseId,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
dataDimensionVal.value = newVal;
|
||||
fetchDZOptions();
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
fetchDZOptions();
|
||||
// 延迟确保 DOM 渲染完成后初始化表格
|
||||
setTimeout(() => {
|
||||
handleSearch();
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zengzhi-zhan-qingkuang-two-layers {
|
||||
width: 100%;
|
||||
|
||||
.search-form {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: #2f6b98;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
.station-item {
|
||||
&:not(:last-child)::after {
|
||||
content: ', ';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,339 +1,606 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="增殖计划完成情况" :select="select" :datetimePicker="datetimePicker">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</SidePanelItem>
|
||||
<SidePanelItem
|
||||
title="增殖计划完成情况"
|
||||
:select="select"
|
||||
:datetimePicker="datetimePicker"
|
||||
@update-values="handlePanelChange1"
|
||||
>
|
||||
<div
|
||||
v-show="!noData && !loading"
|
||||
ref="chartRef"
|
||||
class="chart-container"
|
||||
></div>
|
||||
<div v-show="loading" class="center-container">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<div v-show="noData" class="center-container">
|
||||
<a-empty description="暂无数据" />
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="dialogVisible"
|
||||
title="增殖计划完成情况"
|
||||
width="1536px"
|
||||
:footer="null"
|
||||
>
|
||||
<ZengZhiZhanQingKuangTwoLayers
|
||||
v-if="dialogVisible"
|
||||
:selectData="selectedOption"
|
||||
:baseId="baseid"
|
||||
:data="dialogData"
|
||||
:datas="JidiSelectEventStore.jidiData"
|
||||
:nian="datetimePicker.value"
|
||||
></ZengZhiZhanQingKuangTwoLayers>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import {
|
||||
fishTypegetStcdList,
|
||||
fishCountGetKendoListCust,
|
||||
fishTypeGetKendoListCust
|
||||
} from '@/api/zzfl';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { MapClass } from '@/components/gis/map.class';
|
||||
import ZengZhiZhanQingKuangTwoLayers from './TwoLayers/ZengZhiZhanQingKuangTwoLayers.vue';
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
|
||||
const baseid = ref('');
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'zengZhiJiHuaWanChengQingKuang'
|
||||
name: 'zengZhiJiHuaWanChengQingKuang'
|
||||
});
|
||||
|
||||
// 图表容器引用
|
||||
const chartRef = ref<HTMLElement | null>(null);
|
||||
let chartInstance: ECharts | null = null;
|
||||
|
||||
// 弹窗控制
|
||||
const dialogVisible = ref(false);
|
||||
const dialogData = ref<any>(null);
|
||||
|
||||
// 当前选中的站点选项
|
||||
const selectedOption = computed(() => {
|
||||
return select.value.options.find(
|
||||
(item: any) => item.value === select.value.value
|
||||
);
|
||||
});
|
||||
|
||||
// 选择器和日期选择器配置
|
||||
const select = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
options: [],
|
||||
picker: undefined,
|
||||
format: undefined
|
||||
show: true,
|
||||
value: undefined,
|
||||
options: [],
|
||||
picker: undefined,
|
||||
format: undefined,
|
||||
width: '120px'
|
||||
});
|
||||
|
||||
const datetimePicker = ref({
|
||||
show: true,
|
||||
value: `${new Date().getFullYear() }`,
|
||||
format: 'YYYY', // YYYY-MM-DD HH
|
||||
picker: 'year' as const, // date | week | month | quarter | year
|
||||
options: []
|
||||
show: true,
|
||||
value: `${new Date().getFullYear()}`,
|
||||
format: 'YYYY', // YYYY-MM-DD HH
|
||||
picker: 'year' as const, // date | week | month | quarter | year
|
||||
options: []
|
||||
});
|
||||
|
||||
// 模拟数据 - 实际项目中应从 API 获取
|
||||
const wangCheng = ref([
|
||||
{ unfinished: 35.2468, finished: 64.7532 }
|
||||
]);
|
||||
|
||||
const zhongLei = ref([
|
||||
{ unfinished: 3, finished: 7 }
|
||||
]);
|
||||
|
||||
const jihua = ref([10, 20, 30, 40]);
|
||||
// 数据状态
|
||||
const wangCheng = ref([]);
|
||||
const zhongLei = ref([]);
|
||||
const jihua = ref([]);
|
||||
const loading = ref(false);
|
||||
const noData = ref(false);
|
||||
|
||||
const unit = ref('万尾');
|
||||
|
||||
// 数值过滤函数(模拟 filterNumberByConfig)
|
||||
const filterNumberByConfig = (value: number, config: any) => {
|
||||
if (value < 0) return 0;
|
||||
return value;
|
||||
if (value < 0) return 0;
|
||||
return value;
|
||||
};
|
||||
|
||||
// 计算图表配置
|
||||
const chartOption = computed(() => {
|
||||
let wcval = wangCheng.value?.[0]?.unfinished || 0;
|
||||
let wcval = wangCheng.value?.[0]?.unfinished || 0;
|
||||
|
||||
let datapie = [
|
||||
{
|
||||
value: filterNumberByConfig(wcval, { tbCode: 'Other', ys: 'SL' }),
|
||||
name: "未完成数量"
|
||||
let datapie = [
|
||||
{
|
||||
value: filterNumberByConfig(wcval, { tbCode: 'Other', ys: 'SL' }),
|
||||
name: '未完成数量'
|
||||
},
|
||||
{
|
||||
value: filterNumberByConfig(wangCheng.value?.[0]?.finished || 0, {
|
||||
tbCode: 'Other',
|
||||
ys: 'SL'
|
||||
}),
|
||||
name: '已完成数量'
|
||||
}
|
||||
];
|
||||
|
||||
let datas = [
|
||||
{ value: zhongLei.value?.[0]?.unfinished || 0, name: '未完成种类' },
|
||||
{ value: zhongLei.value?.[0]?.finished || 0, name: '已完成种类' }
|
||||
];
|
||||
|
||||
const wcStr = jihua.value
|
||||
.reduce((pre: any, i: any) => pre + i, 0)
|
||||
?.toFixed(4);
|
||||
const zlStr = datas.reduce((pre, i: any) => pre + i.value, 0);
|
||||
|
||||
// 种类完成情况是否显示白线
|
||||
const isShowdatas =
|
||||
datas.filter((i: any) => i.value).length !== 1
|
||||
? {
|
||||
itemStyle: {
|
||||
normal: {
|
||||
borderWidth: 3,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
}
|
||||
}
|
||||
: {};
|
||||
|
||||
// 数量完成情况是否显示白线
|
||||
const isShowdatapie =
|
||||
datapie.filter((i: any) => i.value).length !== 1
|
||||
? {
|
||||
itemStyle: {
|
||||
normal: {
|
||||
borderWidth: 3,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
}
|
||||
}
|
||||
: {};
|
||||
|
||||
const textStyle = {
|
||||
rich: {
|
||||
a: {
|
||||
color: '#2F6B98',
|
||||
fontSize: 18,
|
||||
height: 30
|
||||
},
|
||||
c: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(0,0,0)',
|
||||
padding: [0, 2]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
title: [
|
||||
{
|
||||
text:
|
||||
'{a|' +
|
||||
wcStr +
|
||||
' ' +
|
||||
'}\n{c|' +
|
||||
'计划完成总数' +
|
||||
'\n\n' +
|
||||
'(' +
|
||||
unit.value +
|
||||
')' +
|
||||
'}',
|
||||
x: '108',
|
||||
y: '98',
|
||||
textAlign: 'center',
|
||||
textStyle
|
||||
},
|
||||
{
|
||||
text:
|
||||
'{a|' +
|
||||
zlStr +
|
||||
' ' +
|
||||
'}\n{c|' +
|
||||
'计划完成种类' +
|
||||
'\n\n' +
|
||||
'(种)' +
|
||||
'}',
|
||||
x: '298',
|
||||
y: '98',
|
||||
textAlign: 'center',
|
||||
textStyle
|
||||
},
|
||||
{
|
||||
text: '数量完成情况',
|
||||
textAlign: 'center',
|
||||
left: '24%',
|
||||
top: '5',
|
||||
textStyle: {
|
||||
fontSize: '14',
|
||||
color: 'rgba(0,0,0,0.8)'
|
||||
}
|
||||
},
|
||||
{
|
||||
textAlign: 'center',
|
||||
text: '种类完成情况',
|
||||
left: '71%',
|
||||
top: '5',
|
||||
textStyle: {
|
||||
fontSize: '14',
|
||||
color: 'rgba(0,0,0,0.8)'
|
||||
}
|
||||
}
|
||||
],
|
||||
legend: [
|
||||
{
|
||||
data: datapie,
|
||||
bottom: '5%',
|
||||
x: '10%',
|
||||
height: 60,
|
||||
width: 160,
|
||||
orient: 'vertical',
|
||||
selectedMode: 'multiple',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: ' rgba(0, 0, 0, 0.85)',
|
||||
rich: {
|
||||
name: {
|
||||
verticalAlign: 'right',
|
||||
align: 'left',
|
||||
width: 60,
|
||||
fontSize: 14,
|
||||
color: ' rgba(0, 0, 0, 0.85)'
|
||||
},
|
||||
percent: { fontSize: 14, padding: [0, 0, 0, 8], color: '#2F6B98 ' }
|
||||
},
|
||||
borderWidth: 53 // 间距的宽度
|
||||
},
|
||||
{
|
||||
value: filterNumberByConfig(wangCheng.value?.[0]?.finished || 0, { tbCode: 'Other', ys: 'SL' }),
|
||||
name: "已完成数量"
|
||||
}
|
||||
];
|
||||
|
||||
let datas = [
|
||||
{ value: zhongLei.value?.[0]?.unfinished || 0, name: "未完成种类" },
|
||||
{ value: zhongLei.value?.[0]?.finished || 0, name: "已完成种类" }
|
||||
];
|
||||
|
||||
const wcStr = jihua.value.reduce((pre: any, i: any) => pre + i, 0)?.toFixed(4);
|
||||
const zlStr = datas.reduce((pre, i: any) => pre + i.value, 0);
|
||||
|
||||
// 种类完成情况是否显示白线
|
||||
const isShowdatas = datas.filter((i: any) => i.value).length !== 1
|
||||
? {
|
||||
itemStyle: {
|
||||
normal: {
|
||||
borderWidth: 3,
|
||||
borderColor: "#fff"
|
||||
}
|
||||
formatter(name: string) {
|
||||
let data = datapie;
|
||||
let value = '';
|
||||
data.forEach((item, index) => {
|
||||
if (item.name == name) {
|
||||
value =
|
||||
item.value < 0
|
||||
? ' ' + 0
|
||||
: item.value < 10
|
||||
? ' ' + item.value
|
||||
: (item.value as any);
|
||||
}
|
||||
});
|
||||
return (
|
||||
'{name|' + name + '}' + '{percent|' + value + '}' + ' ' + unit.value
|
||||
);
|
||||
}
|
||||
: {};
|
||||
|
||||
// 数量完成情况是否显示白线
|
||||
const isShowdatapie = datapie.filter((i: any) => i.value).length !== 1
|
||||
? {
|
||||
itemStyle: {
|
||||
normal: {
|
||||
borderWidth: 3,
|
||||
borderColor: "#fff"
|
||||
}
|
||||
}
|
||||
}
|
||||
: {};
|
||||
|
||||
const textStyle = {
|
||||
rich: {
|
||||
a: {
|
||||
color: "#2F6B98",
|
||||
fontSize: 18,
|
||||
height: 30
|
||||
},
|
||||
{
|
||||
data: datas,
|
||||
bottom: '5%',
|
||||
left: '232',
|
||||
height: 60,
|
||||
width: 200,
|
||||
orient: 'vertical',
|
||||
selectedMode: 'multiple',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: ' rgba(0, 0, 0, 0.85)',
|
||||
rich: {
|
||||
name: {
|
||||
verticalAlign: 'right',
|
||||
align: 'left',
|
||||
width: 70,
|
||||
fontSize: 14,
|
||||
color: ' rgba(0, 0, 0, 0.85)'
|
||||
},
|
||||
c: {
|
||||
fontSize: 12,
|
||||
color: "rgba(0,0,0)",
|
||||
padding: [0, 2]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: "item"
|
||||
percent: { fontSize: 14, padding: [0, 0, 0, 8], color: '#2F6B98 ' }
|
||||
},
|
||||
borderWidth: 53 // 间距的宽度
|
||||
},
|
||||
title: [
|
||||
{
|
||||
text: "{a|" + wcStr + " " + "}\n{c|" + "计划完成总数" + "\n\n" + "(" + unit.value + ")" + "}",
|
||||
x: "108",
|
||||
y: "98",
|
||||
textAlign: "center",
|
||||
textStyle,
|
||||
},
|
||||
{
|
||||
text: "{a|" + zlStr + " " + "}\n{c|" + "计划完成种类" + "\n\n" + "(种)" + "}",
|
||||
x: "298",
|
||||
y: "98",
|
||||
textAlign: "center",
|
||||
textStyle,
|
||||
},
|
||||
{
|
||||
text: "数量完成情况",
|
||||
textAlign: "center",
|
||||
left: "24%",
|
||||
top: "5",
|
||||
textStyle: {
|
||||
fontSize: "14",
|
||||
color: "rgba(0,0,0,0.8)"
|
||||
}
|
||||
},
|
||||
{
|
||||
textAlign: "center",
|
||||
text: "种类完成情况",
|
||||
left: "71%",
|
||||
top: "5",
|
||||
textStyle: {
|
||||
fontSize: "14",
|
||||
color: "rgba(0,0,0,0.8)"
|
||||
}
|
||||
formatter(name: string) {
|
||||
let data = datas;
|
||||
let value = '';
|
||||
data.forEach((item, index) => {
|
||||
if (item.name == name) {
|
||||
value = item.value < 10 ? ' ' + item.value : (item.value as any);
|
||||
}
|
||||
],
|
||||
legend: [
|
||||
{
|
||||
data: datapie,
|
||||
bottom: "5%",
|
||||
x: "10%",
|
||||
height: 60,
|
||||
width: 160,
|
||||
orient: "vertical",
|
||||
selectedMode: 'multiple',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: " rgba(0, 0, 0, 0.85)",
|
||||
rich: {
|
||||
name: {
|
||||
verticalAlign: "right",
|
||||
align: "left",
|
||||
width: 60,
|
||||
fontSize: 14,
|
||||
color: " rgba(0, 0, 0, 0.85)"
|
||||
},
|
||||
percent: { fontSize: 14, padding: [0, 0, 0, 8], color: "#2F6B98 " }
|
||||
},
|
||||
borderWidth: 53 // 间距的宽度
|
||||
},
|
||||
formatter(name: string) {
|
||||
let data = datapie;
|
||||
let value = "";
|
||||
data.forEach((item, index) => {
|
||||
if (item.name == name) {
|
||||
value = item.value < 0 ? " " + 0 : item.value < 10 ? " " + item.value : (item.value as any);
|
||||
}
|
||||
});
|
||||
return "{name|" + name + "}" + "{percent|" + value + "}" + " " + unit.value;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: datas,
|
||||
bottom: "5%",
|
||||
left: "232",
|
||||
height: 60,
|
||||
width: 200,
|
||||
orient: "vertical",
|
||||
selectedMode: 'multiple',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: " rgba(0, 0, 0, 0.85)",
|
||||
rich: {
|
||||
name: {
|
||||
verticalAlign: "right",
|
||||
align: "left",
|
||||
width: 70,
|
||||
fontSize: 14,
|
||||
color: " rgba(0, 0, 0, 0.85)"
|
||||
},
|
||||
percent: { fontSize: 14, padding: [0, 0, 0, 8], color: "#2F6B98 " }
|
||||
},
|
||||
borderWidth: 53 // 间距的宽度
|
||||
},
|
||||
formatter(name: string) {
|
||||
let data = datas;
|
||||
let value = "";
|
||||
data.forEach((item, index) => {
|
||||
if (item.name == name) {
|
||||
value = item.value < 10 ? " " + item.value : (item.value as any);
|
||||
}
|
||||
});
|
||||
return "{name|" + name + "}" + "{percent|" + value + "}" + " " + "种";
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "数量完成情况",
|
||||
id: "parentpie",
|
||||
type: "pie",
|
||||
radius: ["62", "80"],
|
||||
center: ["110", "130"],
|
||||
data: datapie,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
...isShowdatapie
|
||||
},
|
||||
{
|
||||
name: "种类完成情况",
|
||||
id: "pie",
|
||||
type: "pie",
|
||||
radius: ["62", "80"],
|
||||
center: ["300", "130"],
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
data: datas,
|
||||
...isShowdatas
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
return '{name|' + name + '}' + '{percent|' + value + '}' + ' ' + '种';
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '数量完成情况',
|
||||
id: 'parentpie',
|
||||
type: 'pie',
|
||||
radius: ['62', '80'],
|
||||
center: ['110', '130'],
|
||||
data: datapie,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
...isShowdatapie
|
||||
},
|
||||
{
|
||||
name: '种类完成情况',
|
||||
id: 'pie',
|
||||
type: 'pie',
|
||||
radius: ['62', '80'],
|
||||
center: ['300', '130'],
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
data: datas,
|
||||
...isShowdatas
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// 初始化图表
|
||||
const initChart = (retryCount = 0) => {
|
||||
if (!chartRef.value) {
|
||||
console.warn('图表容器不存在');
|
||||
return;
|
||||
if (!chartRef.value) {
|
||||
console.warn('图表容器不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验容器尺寸
|
||||
const rect = chartRef.value.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
// 最多重试5次,避免无限递归
|
||||
if (retryCount < 5) {
|
||||
console.warn(`图表容器尺寸无效,延迟重试... (${retryCount + 1}/5)`, {
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
});
|
||||
setTimeout(() => {
|
||||
initChart(retryCount + 1);
|
||||
}, 100 * Math.pow(2, retryCount)); // 指数退避
|
||||
} else {
|
||||
console.error('图表初始化失败:达到最大重试次数');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验容器尺寸
|
||||
const rect = chartRef.value.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
// 最多重试5次,避免无限递归
|
||||
if (retryCount < 5) {
|
||||
console.warn(`图表容器尺寸无效,延迟重试... (${retryCount + 1}/5)`, {
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
});
|
||||
setTimeout(() => {
|
||||
initChart(retryCount + 1);
|
||||
}, 100 * Math.pow(2, retryCount)); // 指数退避
|
||||
} else {
|
||||
console.error('图表初始化失败:达到最大重试次数');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 销毁旧实例
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
|
||||
// 销毁旧实例
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
// 创建新实例
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
|
||||
// 创建新实例
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
// 设置图表配置
|
||||
chartInstance.setOption(chartOption.value);
|
||||
|
||||
// 设置图表配置
|
||||
chartInstance.setOption(chartOption.value);
|
||||
// 绑定点击事件
|
||||
bindChartClick();
|
||||
|
||||
console.log('图表初始化成功');
|
||||
console.log('图表初始化成功');
|
||||
};
|
||||
|
||||
// 更新图表数据
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return;
|
||||
if (!chartInstance) return;
|
||||
|
||||
// 使用完全替换模式,防止状态残留
|
||||
chartInstance.setOption(chartOption.value, true);
|
||||
// 使用完全替换模式,防止状态残留
|
||||
chartInstance.setOption(chartOption.value, true);
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize();
|
||||
chartInstance?.resize();
|
||||
};
|
||||
|
||||
// 处理图表点击事件
|
||||
const handleChartClick = (params: any) => {
|
||||
dialogData.value = params;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 监听图表点击
|
||||
const bindChartClick = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.off('click');
|
||||
chartInstance.on('click', handleChartClick);
|
||||
}
|
||||
};
|
||||
const getSelectData = async () => {
|
||||
let params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: baseid.value
|
||||
},
|
||||
{
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'FB'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
const res = await fishTypegetStcdList(params);
|
||||
if (res.data.data.length == 0) {
|
||||
noData.value = true;
|
||||
return;
|
||||
}
|
||||
select.value.options = res.data.data.map(item => {
|
||||
return {
|
||||
value: item.stcd,
|
||||
label: item.stnm,
|
||||
lgtd: item.lgtd,
|
||||
lttd: item.lttd
|
||||
};
|
||||
});
|
||||
select.value.value = select.value.options[0].value;
|
||||
// 获取完站点列表后,立即获取图表数据
|
||||
getEcharts();
|
||||
// debugger;
|
||||
};
|
||||
const getEcharts = async () => {
|
||||
loading.value = true;
|
||||
noData.value = false;
|
||||
|
||||
// 立即清空图表,显示 loading
|
||||
if (chartInstance) {
|
||||
chartInstance.clear();
|
||||
}
|
||||
|
||||
let parmas = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: baseid.value
|
||||
},
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: select.value.value
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: datetimePicker.value.value + '-12-31 23:59:59'
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: datetimePicker.value.value + '-01-01 00:00:00'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fishCountGetKendoListCust(parmas);
|
||||
const res2 = await fishTypeGetKendoListCust(parmas);
|
||||
|
||||
// 更新数量完成情况(转换为万尾)
|
||||
if (res.data.data?.length > 0) {
|
||||
const countData = res.data.data[0];
|
||||
wangCheng.value = [
|
||||
{
|
||||
finished: countData.finished / 10000,
|
||||
unfinished: countData.unfinished / 10000
|
||||
}
|
||||
];
|
||||
// 计算总数
|
||||
jihua.value = [countData.finished / 10000, countData.unfinished / 10000];
|
||||
} else {
|
||||
wangCheng.value = [];
|
||||
jihua.value = [];
|
||||
}
|
||||
|
||||
// 更新种类完成情况
|
||||
if (res2.data.data?.length > 0) {
|
||||
const typeData = res2.data.data[0];
|
||||
zhongLei.value = [
|
||||
{
|
||||
finished: typeData.finished,
|
||||
unfinished: typeData.unfinished
|
||||
}
|
||||
];
|
||||
} else {
|
||||
zhongLei.value = [];
|
||||
}
|
||||
|
||||
// 判断是否有数据
|
||||
noData.value = wangCheng.value.length === 0 && zhongLei.value.length === 0;
|
||||
console.log('图表数据:', noData.value);
|
||||
// debugger;
|
||||
// 只有有数据时才渲染图表
|
||||
if (!noData.value) {
|
||||
updateChart();
|
||||
} else {
|
||||
chartInstance?.clear();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取图表数据失败:', error);
|
||||
noData.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 使用 nextTick + setTimeout 双重等待确保DOM和样式计算完成
|
||||
// 使用 nextTick + setTimeout 双重等待确保DOM和样式计算完成
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
// 初始化后再 resize 一次,确保尺寸正确
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
// 初始化后再 resize 一次,确保尺寸正确
|
||||
setTimeout(() => {
|
||||
chartInstance?.resize();
|
||||
}, 50);
|
||||
chartInstance?.resize();
|
||||
}, 50);
|
||||
}, 50);
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
});
|
||||
//监听子组件的数据变化
|
||||
const handlePanelChange1 = async data => {
|
||||
console.log('当前所有控件状态:', data);
|
||||
// debugger;
|
||||
// 当选择器或日期变化时,重新加载图表数据
|
||||
if (data.datetime && data.select) {
|
||||
await (datetimePicker.value.value = data.datetime);
|
||||
select.value.value = data.select;
|
||||
getEcharts();
|
||||
}
|
||||
if (data?.selectedNodeExtra?.lttd && data?.selectedNodeExtra?.lgtd) {
|
||||
const mapClass = MapClass.getInstance();
|
||||
mapClass.flyTopanto(
|
||||
[
|
||||
Number(data.selectedNodeExtra.lgtd),
|
||||
Number(data.selectedNodeExtra.lttd)
|
||||
],
|
||||
12
|
||||
);
|
||||
}
|
||||
};
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
newVal => {
|
||||
if (newVal && newVal.wbsCode) {
|
||||
baseid.value = newVal.wbsCode;
|
||||
getSelectData();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
min-height: 280px;
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.center-container {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,76 +1,92 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="增殖站介绍">
|
||||
<div class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
|
||||
<!-- 跑马灯轨道容器 -->
|
||||
<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">
|
||||
<!-- 图片 -->
|
||||
<img :src="item.url" alt="" />
|
||||
<!-- 说明文字(随媒体项移动) -->
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
<SidePanelItem title="增殖站介绍">
|
||||
<a-spin :spinning="loading">
|
||||
<template v-if="originalMediaData.length > 0">
|
||||
<div
|
||||
class="container"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<!-- 跑马灯轨道容器 -->
|
||||
<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="handleItemClick"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<img :src="item.url" alt="" />
|
||||
<!-- 说明文字(随媒体项移动) -->
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 面板指示器(固定在底部右侧) -->
|
||||
<div class="pagination-dots-fixed">
|
||||
<span
|
||||
v-for="(dot, index) in originalMediaData"
|
||||
:key="index"
|
||||
class="dot"
|
||||
:class="{ active: getCurrentRealIndex() === index }"
|
||||
@click="goToSlide(index)"
|
||||
></span>
|
||||
</div>
|
||||
<!-- 面板指示器(固定在底部右侧) -->
|
||||
<div class="pagination-dots-fixed">
|
||||
<span
|
||||
v-for="(dot, index) in originalMediaData"
|
||||
:key="index"
|
||||
class="dot"
|
||||
:class="{ active: getCurrentRealIndex() === index }"
|
||||
@click="goToSlide(index)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的文字说明区域(随跑马灯切换而变化) -->
|
||||
<div class="description-text">
|
||||
{{ currentDescription }}
|
||||
<div class="description-text" @click="handleItemClick">
|
||||
{{ currentDescription }}
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
<a-empty v-else description="暂无数据" />
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
|
||||
<!-- 设施详情弹框 -->
|
||||
<a-modal v-model:open="modalVisible" :title="'设施类型介绍'" width="1536px" :footer="null">
|
||||
<div v-if="currentItem">
|
||||
<ArtsDetail :dataSource="currentItem" :index="getCurrentRealIndex()" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { msstbprptGetKendoList } from '@/api/zzfl';
|
||||
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'zengZhiZhanJieShaoMod'
|
||||
name: 'zengZhiZhanJieShaoMod'
|
||||
});
|
||||
|
||||
const baseid = ref('');
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
// 媒体类型定义
|
||||
interface MediaItem {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
description: string;
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
description: string;
|
||||
}
|
||||
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
||||
|
||||
// 原始媒体数据(3条测试数据)
|
||||
const originalMediaData = ref<MediaItem[]>([
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20230814205611342377136845462200&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920',
|
||||
text: '松岗鱼类增殖站',
|
||||
description: '松岗鱼类增殖放流站位于四川省阿坝藏族羌族自治州马尔康市松岗镇,主要服务于大渡河上游的双江口和金川两座水电站,同时还承担多种珍稀特有鱼类的救护和科研任务,实现工程建设与生态环境共同推进、相互促进。'
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
url: 'https://211.99.26.225:12125/?20230805205848575430105387253710&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920', // 视频URL示例,您可替换为真实地址
|
||||
text: '猴子岩鱼类增殖站',
|
||||
description: '猴子岩水电站鱼类增殖放流站位于猴子岩水电站坝址下游约7.0km(业主营地下游约1.5km),大渡河左岸桃花渣场顶部平台上,紧邻枢纽桃花大桥下游侧,占地面积47.3亩,其中一期工程27.0亩,预留二期工程用地20.3亩(二期工程目前为丹巴、巴底水电站预留工程)增殖放流站工作流程为:亲鱼收集购买、亲鱼驯养培育、人工催产和授精、人工孵化、苗种培育和放流。 猴子岩鱼类增殖放流站近期放流对象中齐口裂腹鱼、重口裂腹鱼、大渡软刺裸裂尻鱼增殖放流技术水平已趋于熟化,中期放流对象大渡软刺裸裂尻鱼人工繁殖技术逐渐趋于熟化'
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20230805205924378504010675106305&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920',
|
||||
text: '黑马鱼类增殖站',
|
||||
description: '大渡河黑马鱼类增殖放流站位于四川省甘洛县黑马乡黑马业主营地内,距离甘洛县城45km,区域交通路况较好。主要承担瀑布沟、深溪沟、大岗山、枕头坝一级、沙坪二级等五座水电站鱼类增殖放流的重任。放流鱼类包含:齐口裂腹鱼、重口裂腹鱼、鲈鲤、长薄鳅、白甲鱼、中华倒刺?、长吻脆、稀有鮊鲫、华鲮、侧沟爬岩鳅等10个种类,共计约918.07万尾珍稀特有鱼苗。'
|
||||
}
|
||||
]);
|
||||
// 弹框控制
|
||||
const modalVisible = ref(false);
|
||||
const currentItem = ref<any>(null);
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 原始媒体数据(从API获取)
|
||||
const originalMediaData = ref<MediaItem[]>([]);
|
||||
|
||||
// 克隆首尾项后的渲染数组(用于无缝循环)
|
||||
// 结构:[最后一项克隆, 原始数据..., 第一项克隆]
|
||||
@ -90,197 +106,283 @@ const isTransitioning = ref(false);
|
||||
|
||||
// 初始化渲染数组(克隆首尾项)
|
||||
const initRenderData = () => {
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
|
||||
renderMediaData.value = [
|
||||
originalMediaData.value[length - 1], // 克隆最后一项
|
||||
...originalMediaData.value, // 原始数据
|
||||
originalMediaData.value[0] // 克隆第一项
|
||||
];
|
||||
renderMediaData.value = [
|
||||
originalMediaData.value[length - 1], // 克隆最后一项
|
||||
...originalMediaData.value, // 原始数据
|
||||
originalMediaData.value[0] // 克隆第一项
|
||||
];
|
||||
};
|
||||
|
||||
// 启动自动轮播
|
||||
const startAutoPlay = () => {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 4000);
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// 切换到下一张
|
||||
const nextSlide = () => {
|
||||
currentIndex.value++;
|
||||
currentIndex.value++;
|
||||
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
};
|
||||
|
||||
// 检查是否需要无缝跳转(克隆项处理)
|
||||
const checkSeamlessJump = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
const realLength = originalMediaData.value.length;
|
||||
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
isHovering.value = false;
|
||||
};
|
||||
|
||||
// 计算当前显示的描述文字
|
||||
const currentDescription = computed(() => {
|
||||
const realIndex = getCurrentRealIndex();
|
||||
return originalMediaData.value[realIndex]?.description || '';
|
||||
const realIndex = getCurrentRealIndex();
|
||||
return originalMediaData.value[realIndex]?.description || '';
|
||||
});
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
|
||||
// 获取当前真实的索引(排除克隆项的影响)
|
||||
const getCurrentRealIndex = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
||||
const realLength = originalMediaData.value.length;
|
||||
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
||||
|
||||
// 处理边界情况(无缝循环时的克隆项)
|
||||
if (realIndex < 0) realIndex = realLength - 1;
|
||||
if (realIndex >= realLength) realIndex = 0;
|
||||
// 处理边界情况(无缝循环时的克隆项)
|
||||
if (realIndex < 0) realIndex = realLength - 1;
|
||||
if (realIndex >= realLength) realIndex = 0;
|
||||
|
||||
return realIndex;
|
||||
return realIndex;
|
||||
};
|
||||
|
||||
// 跳转到指定幻灯片
|
||||
const goToSlide = (targetIndex: number) => {
|
||||
if (isTransitioning.value) return;
|
||||
if (isTransitioning.value) return;
|
||||
|
||||
// 计算目标索引(考虑克隆项偏移)
|
||||
currentIndex.value = targetIndex + 1;
|
||||
// 计算目标索引(考虑克隆项偏移)
|
||||
currentIndex.value = targetIndex + 1;
|
||||
};
|
||||
const getData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
let params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'FB'
|
||||
},
|
||||
{
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: baseid.value
|
||||
},
|
||||
{
|
||||
logic: 'or',
|
||||
filters: [
|
||||
{
|
||||
field: 'logo',
|
||||
dataType: 'string',
|
||||
operator: 'isnotnull'
|
||||
},
|
||||
{
|
||||
field: 'introduce',
|
||||
dataType: 'string',
|
||||
operator: 'isnotnull'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
select: ['introduce', 'logo', 'stnm', 'precis']
|
||||
};
|
||||
let res = await msstbprptGetKendoList(params);
|
||||
let data = res?.data?.data;
|
||||
originalMediaData.value = data.map((item: any) => {
|
||||
return {
|
||||
url: baseUrl + '?' + item.logo + '&view=jpg',
|
||||
text: item.stnm,
|
||||
description: item.introduce,
|
||||
stcd: item.stcd
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(async () => {
|
||||
// 数据会在watch中获取
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
|
||||
// 处理项目点击事件
|
||||
const handleItemClick = () => {
|
||||
currentItem.value = originalMediaData.value.map((item: any) => ({
|
||||
url: item.url || '',
|
||||
description: item.description || '',
|
||||
title: item.text || ''
|
||||
}));
|
||||
modalVisible.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
async newVal => {
|
||||
if (newVal && newVal.wbsCode) {
|
||||
baseid.value = newVal.wbsCode;
|
||||
await getData();
|
||||
initRenderData();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
// border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
// border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
|
||||
// 跑马灯轨道
|
||||
.carousel-track {
|
||||
display: flex; // 横向排列所有媒体项
|
||||
// 跑马灯轨道
|
||||
.carousel-track {
|
||||
display: flex; // 横向排列所有媒体项
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease-in-out; // 0.5秒平滑过渡
|
||||
|
||||
// 禁用过渡动画(用于无缝跳转)
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 单个媒体项
|
||||
.carousel-item {
|
||||
min-width: 100%; // 每个项目占满容器宽度
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.5s ease-in-out; // 0.5秒平滑过渡
|
||||
object-fit: cover; // 保持比例填充
|
||||
}
|
||||
|
||||
// 禁用过渡动画(用于无缝跳转)
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 单个媒体项
|
||||
.carousel-item {
|
||||
min-width: 100%; // 每个项目占满容器宽度
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
|
||||
img,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; // 保持比例填充
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 固定的面板指示器
|
||||
.pagination-dots-fixed {
|
||||
.text {
|
||||
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;
|
||||
}
|
||||
}
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 固定的面板指示器
|
||||
.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 {
|
||||
// padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
// min-height: 40px;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
// padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
// min-height: 40px;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 20px;
|
||||
|
||||
// 多行文本溢出省略
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
// 多行文本溢出省略
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div class="whole-valued-station-detail">
|
||||
<!-- 搜索表单 -->
|
||||
<a-form layout="inline" class="search-form">
|
||||
<a-form-item label="年份">
|
||||
<a-date-picker
|
||||
v-model:value="yearValue"
|
||||
picker="year"
|
||||
:allow-clear="false"
|
||||
format="YYYY"
|
||||
value-format="YYYY"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="">
|
||||
<a-input
|
||||
v-model:value="search"
|
||||
placeholder="请输入设施名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">查询</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-wrapper">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:scroll-y="500"
|
||||
:scroll-x="800"
|
||||
:list-url="listUrl"
|
||||
:search-params="searchParams"
|
||||
:transform-data="customTransform"
|
||||
>
|
||||
<template #action="{ record }">
|
||||
<!-- 操作列 -->
|
||||
<a @click="handleViewDetail(record)" class="text-link"> 查看详情 </a>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, h, onMounted } from 'vue';
|
||||
import {
|
||||
qgcoverviewGetOverviewSecond,
|
||||
overviewGetOverviewSecond
|
||||
} from '@/api/zzfl';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { List } from 'ant-design-vue';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
const modelStore = useModelStore();
|
||||
// 渲染逗号分隔的文本为列表
|
||||
const renderList = (text: string) => {
|
||||
if (!text) return '-';
|
||||
const items = text.split(',');
|
||||
return h(
|
||||
List,
|
||||
{ size: 'small' },
|
||||
{
|
||||
renderItem: (item: string) => h(List.Item, {}, () => item),
|
||||
default: () => items.map(item => h(List.Item, {}, () => item))
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== Props ====================
|
||||
const props = defineProps<{
|
||||
time?: string; // 年份,格式 YYYY
|
||||
all?: boolean; // 是否为全国概览模式
|
||||
baseId?: string; // 基地ID
|
||||
}>();
|
||||
|
||||
// ==================== 搜索状态 ====================
|
||||
const yearValue = ref(props.time || `${new Date().getFullYear()}`);
|
||||
const search = ref('');
|
||||
|
||||
// ==================== 接口URL ====================
|
||||
const listUrl = computed(() => {
|
||||
return props.all ? qgcoverviewGetOverviewSecond : overviewGetOverviewSecond;
|
||||
});
|
||||
|
||||
// ==================== 搜索参数 ====================
|
||||
const searchParams = computed(() => ({
|
||||
sort: [],
|
||||
group: [],
|
||||
select: [],
|
||||
groupResultFlat: false
|
||||
}));
|
||||
|
||||
// ==================== 表格列配置 ====================
|
||||
const columns = [
|
||||
{
|
||||
key: 'stnm',
|
||||
title: '设施名称',
|
||||
dataIndex: 'stnm',
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'ennm',
|
||||
title: '服务电站',
|
||||
dataIndex: 'ennm',
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'totalInv',
|
||||
title: '总投资',
|
||||
dataIndex: 'totalInv',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'planFtp',
|
||||
title: '计划放流种类',
|
||||
dataIndex: 'planFtp',
|
||||
width: 160,
|
||||
customRender: ({ text }: { text: string }) => renderList(text)
|
||||
},
|
||||
{
|
||||
key: 'fcntjh',
|
||||
title: '计划放流数量',
|
||||
dataIndex: 'fcntjh',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
key: 'realftp',
|
||||
title: '实际放流种类',
|
||||
dataIndex: 'realftp',
|
||||
width: 160,
|
||||
customRender: ({ text }: { text: string }) => renderList(text)
|
||||
},
|
||||
{
|
||||
key: 'realfcntStr',
|
||||
title: '实际放流数量',
|
||||
dataIndex: 'realfcntStr',
|
||||
width: 160,
|
||||
customRender: ({ text }: { text: string }) => renderList(text)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
slots: { customRender: 'action' }
|
||||
}
|
||||
];
|
||||
|
||||
// ==================== 过滤条件构建 ====================
|
||||
const tableRef = ref();
|
||||
|
||||
// 构建查询过滤条件
|
||||
const getFilter = () => {
|
||||
return {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'year',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: yearValue.value
|
||||
},
|
||||
props.all && props.baseId && props.baseId !== 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: props.baseId
|
||||
}
|
||||
: null,
|
||||
search.value
|
||||
? {
|
||||
field: 'stnm',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: search.value
|
||||
}
|
||||
: null
|
||||
].filter(Boolean)
|
||||
};
|
||||
};
|
||||
|
||||
// ==================== 数据转换 ====================
|
||||
const customTransform = (res: any) => {
|
||||
return {
|
||||
records: res?.data?.data || [],
|
||||
total: res?.data?.total || 0
|
||||
};
|
||||
};
|
||||
|
||||
// ==================== 搜索与重置 ====================
|
||||
const handleSearch = () => {
|
||||
// debugger;
|
||||
const filter = getFilter();
|
||||
tableRef.value?.getList(filter);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
yearValue.value = props.time || `${new Date().getFullYear()}`;
|
||||
search.value = '';
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (record: any) => {
|
||||
// debugger;
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params = record;
|
||||
modelStore.title = record.stnm;
|
||||
modelStore.params.sttp = 'FB';
|
||||
modelStore.filter.year = yearValue.value;
|
||||
// modelStore.params.s = record.stnm;
|
||||
};
|
||||
// ==================== 监听 time prop 变化 ====================
|
||||
watch(
|
||||
() => props.time,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
yearValue.value = newVal;
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.whole-valued-station-detail {
|
||||
width: 100%;
|
||||
|
||||
.search-form {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.text-link {
|
||||
color: #2f6b98;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,128 +1,232 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="增殖站建设运行情况" :datetimePicker="datetimePicker" >
|
||||
<div class="facility-grid">
|
||||
<div v-for="facility in facilities" :key="facility.name" class="facility-card">
|
||||
<div style="width: 70px;height: 62px;display: flex;align-items: center;justify-content: center;">
|
||||
<div class="facility-icon">
|
||||
<i style="color: #fff;" :class="facility.icon" type="icon-shengtailiuliang2"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="facility-info">
|
||||
<div class="facility-name">{{ facility.name }}</div>
|
||||
<div style="font-size: 14px;"> <span class="facility-count">{{ facility.count }}</span>
|
||||
<span v-if="facility.name == '增殖站数量'">座</span>
|
||||
<span v-if="facility.name == '放流量'">万尾</span>
|
||||
<span v-if="facility.name == '放流种类'">种</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidePanelItem
|
||||
title="增殖站建设运行情况"
|
||||
:datetimePicker="datetimePicker"
|
||||
@update-values="handlePanelChange1"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="facility-grid">
|
||||
<div
|
||||
v-for="facility in facilities"
|
||||
:key="facility.name"
|
||||
class="facility-card"
|
||||
@click="openDialog(facility)"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
width: 70px;
|
||||
height: 62px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
>
|
||||
<div class="facility-icon">
|
||||
<i
|
||||
style="color: #fff"
|
||||
:class="facility.icon"
|
||||
type="icon-shengtailiuliang2"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
|
||||
<div class="facility-info">
|
||||
<div class="facility-name">{{ facility.name }}</div>
|
||||
<div style="font-size: 14px">
|
||||
<span class="facility-count">{{ facility.nowNum }}</span>
|
||||
<span v-if="facility.name == '增殖站数量'">座</span>
|
||||
<span v-if="facility.name == '放流量'">万尾</span>
|
||||
<span v-if="facility.name == '放流种类'">种</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
<a-modal
|
||||
v-model:visible="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
:width="1536"
|
||||
:footer="null"
|
||||
>
|
||||
<WholeValuedStationOverviewDetail v-if="dialogVisible" :time="datetimePicker.value" all :baseId="baseid" />
|
||||
</a-modal>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
import { overviewGetKendoListCust } from '@/api/zzfl';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import WholeValuedStationOverviewDetail from "./WholeValuedStationOverviewDetail/index.vue"
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'zengzhizhanjiansheyunxing'
|
||||
name: 'zengzhizhanjiansheyunxing'
|
||||
});
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const baseid = ref('');
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('增殖站概况');
|
||||
const datetimePicker = ref({
|
||||
show: true,
|
||||
value: `${new Date().getFullYear() - 1}`, // 上一年
|
||||
format: 'YYYY',
|
||||
picker: 'year' as const,
|
||||
options: []
|
||||
show: true,
|
||||
value: `${new Date().getFullYear() - 1}`, // 上一年
|
||||
format: 'YYYY',
|
||||
picker: 'year' as const,
|
||||
options: []
|
||||
});
|
||||
// 设施数据
|
||||
const facilities = ref([
|
||||
{
|
||||
name: '增殖站数量',
|
||||
count: "56",
|
||||
icon: 'icon iconfont icon-yuleizengzhizhan1'
|
||||
},
|
||||
{
|
||||
name: '放流量',
|
||||
count: '1722',
|
||||
icon: 'icon iconfont icon-fangliushuliang'
|
||||
},
|
||||
{
|
||||
name: '放流种类',
|
||||
count: '135',
|
||||
icon: 'icon iconfont icon-yuzhongshuliang'
|
||||
}
|
||||
|
||||
const facilities:any = ref([
|
||||
{
|
||||
name: '增殖站数量',
|
||||
nowNum: '56',
|
||||
icon: 'icon iconfont icon-yuleizengzhizhan1'
|
||||
},
|
||||
{
|
||||
name: '放流量',
|
||||
nowNum: '1722',
|
||||
icon: 'icon iconfont icon-fangliushuliang'
|
||||
},
|
||||
{
|
||||
name: '放流种类',
|
||||
nowNum: '135',
|
||||
icon: 'icon iconfont icon-yuzhongshuliang'
|
||||
}
|
||||
]);
|
||||
|
||||
const getData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const year = datetimePicker.value.value;
|
||||
let params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
baseid.value != 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
value: baseid.value
|
||||
}
|
||||
: null,
|
||||
{
|
||||
field: 'startTime',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: `${year}-01-01 00:00:00`
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: `${year}-12-31 23:59:59`
|
||||
}
|
||||
].filter(Boolean)
|
||||
}
|
||||
};
|
||||
let res = await overviewGetKendoListCust(params);
|
||||
if (res.data?.data?.length > 0) {
|
||||
const item = res.data.data[0];
|
||||
facilities.value[0].nowNum = item.count ?? '-';
|
||||
facilities.value[1].nowNum = item.realfcnt
|
||||
? (item.realfcnt / 10000).toFixed(4)
|
||||
: '0.0000';
|
||||
facilities.value[2].nowNum = item.realftpCount ?? '0';
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 延迟初始化,确保容器已渲染
|
||||
|
||||
// 延迟初始化,确保容器已渲染
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
onUnmounted(() => {});
|
||||
|
||||
});
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
newVal => {
|
||||
if (newVal && newVal.wbsCode) {
|
||||
baseid.value = newVal.wbsCode;
|
||||
getData();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
//监听子组件的数据变化
|
||||
const handlePanelChange1 = async data => {
|
||||
console.log('当前所有控件状态:', data);
|
||||
// 当选择器或日期变化时,重新加载图表数据
|
||||
if (data.datetime) {
|
||||
await (datetimePicker.value.value = data.datetime);
|
||||
getData();
|
||||
}
|
||||
};
|
||||
|
||||
const openDialog = (facility) => {
|
||||
dialogTitle.value = `增殖站概况`;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.facility-grid {
|
||||
width: 406px;
|
||||
flex-flow: wrap;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
width: 406px;
|
||||
flex-flow: wrap;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji,
|
||||
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
}
|
||||
|
||||
.facility-card {
|
||||
width: 200px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 4px 0px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
width: 200px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 4px 0px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.facility-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
// margin-right: 8px;
|
||||
background: rgb(47, 107, 152);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
// margin-right: 8px;
|
||||
background: rgb(47, 107, 152);
|
||||
border-radius: 50%;
|
||||
|
||||
.anticon {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
.anticon {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.facility-info {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.facility-name {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
// margin-bottom: 4px;
|
||||
// font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
// margin-bottom: 4px;
|
||||
// font-weight: 500;
|
||||
}
|
||||
|
||||
.facility-count {
|
||||
font-size: 18px;
|
||||
color: #2f6b98;
|
||||
// font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: #2f6b98;
|
||||
// font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@ -1,214 +1,326 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="增殖站运行数据统计" :datetimePicker="datetimePicker">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</SidePanelItem>
|
||||
<SidePanelItem
|
||||
title="增殖站运行数据统计"
|
||||
:datetimePicker="datetimePicker"
|
||||
@update-values="handlePanelChange1"
|
||||
>
|
||||
|
||||
<a-spin :spinning="loading" tip="加载中...">
|
||||
<div class="chart-wrapper">
|
||||
<div ref="chartRef" class="chart-container" v-show="chartData.length > 0"></div>
|
||||
<a-empty v-show="chartData.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { qgcgetFbStaticsData } from '@/api/zzfl';
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const baseid = ref('');
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'ZengZhiZhanYunXingSJ'
|
||||
name: 'ZengZhiZhanYunXingSJ'
|
||||
});
|
||||
|
||||
const datetimePicker = ref({
|
||||
show: true,
|
||||
value: `${new Date().getFullYear() - 1}`, // 上一年
|
||||
format: 'YYYY',
|
||||
picker: 'year' as const,
|
||||
options: []
|
||||
show: true,
|
||||
value: `${new Date().getFullYear() - 1}`, // 上一年
|
||||
format: 'YYYY',
|
||||
picker: 'year' as const,
|
||||
options: []
|
||||
});
|
||||
|
||||
// 图表相关
|
||||
const chartRef = ref<HTMLElement | null>(null);
|
||||
let chartInstance: ECharts | null = null;
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{ hbrvcdName: '长江上游', fcnt: 1200, ftp: 15 },
|
||||
{ hbrvcdName: '长江中游', fcnt: 980, ftp: 12 },
|
||||
{ hbrvcdName: '长江下游', fcnt: 1500, ftp: 18 },
|
||||
{ hbrvcdName: '珠江流域', fcnt: 750, ftp: 10 },
|
||||
{ hbrvcdName: '黄河流域', fcnt: 600, ftp: 8 },
|
||||
{ hbrvcdName: '淮河流域', fcnt: 450, ftp: 6 },
|
||||
];
|
||||
// 图表数据
|
||||
const chartData = ref<any[]>([]);
|
||||
|
||||
// loading 状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 计算坐标轴步长
|
||||
const getChartStep = (data: number[]) => {
|
||||
if (!data || data.length === 0) return {};
|
||||
const maxVal = Math.max(...data.filter(v => v !== null && v !== undefined));
|
||||
const minVal = Math.min(...data.filter(v => v !== null && v !== undefined));
|
||||
const range = maxVal - minVal;
|
||||
const step = range / 5;
|
||||
return {
|
||||
min: Math.floor(minVal),
|
||||
max: Math.ceil(maxVal),
|
||||
interval: Math.ceil(step)
|
||||
};
|
||||
if (!data || data.length === 0) return {};
|
||||
const maxVal = Math.max(...data.filter(v => v !== null && v !== undefined));
|
||||
const range = maxVal; // 从0开始
|
||||
const step = range / 5;
|
||||
return {
|
||||
min: 0, // y轴从0开始
|
||||
max: Math.ceil(maxVal * 1.1), // 留10%余量
|
||||
interval: Math.ceil(step * 1.1)
|
||||
};
|
||||
};
|
||||
|
||||
// 单位转换(简化版)
|
||||
// 单位转换:尾转万尾
|
||||
const transUnit = (value: number, type: string, unit: string) => {
|
||||
// 这里可以根据实际需求实现单位转换逻辑
|
||||
return value;
|
||||
if (unit === 'SL') {
|
||||
// 尾转万尾,不四舍五入
|
||||
return value / 10000;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (!chartRef.value) return;
|
||||
|
||||
if (!chartInstance) {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
} else {
|
||||
chartInstance.resize();
|
||||
}
|
||||
|
||||
const fcntData = mockData.map(el => el.fcnt ? transUnit(el.fcnt, 'Other', 'SL') : el.fcnt);
|
||||
const ftpData = mockData.map(el => el.ftp);
|
||||
const fcntStep = getChartStep(fcntData);
|
||||
const ftpStep = getChartStep(ftpData);
|
||||
const fcntData = chartData.value.map(el =>
|
||||
el.fcnt ? transUnit(el.fcnt, 'Other', 'SL') : el.fcnt
|
||||
);
|
||||
const ftpData = chartData.value.map(el => el.ftp);
|
||||
const fcntStep = getChartStep(fcntData);
|
||||
const ftpStep = getChartStep(ftpData);
|
||||
|
||||
const option: any = {
|
||||
grid: {
|
||||
left: 50,
|
||||
right: 50,
|
||||
bottom: 60,
|
||||
top: 60,
|
||||
const option: any = {
|
||||
grid: {
|
||||
left: 50,
|
||||
right: 50,
|
||||
bottom: 60,
|
||||
top: 60
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['放流量(万尾)', '放流种类(种)'],
|
||||
top: 10,
|
||||
itemWidth: 12,
|
||||
itemHeight: 8,
|
||||
itemGap: 8
|
||||
},
|
||||
xAxis: {
|
||||
show: true,
|
||||
type: 'category',
|
||||
axisLine: { show: true },
|
||||
splitLine: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#686868',
|
||||
rotate: 37,
|
||||
interval: 0
|
||||
},
|
||||
data: chartData.value.map(el => el.baseName)
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
show: true,
|
||||
type: 'value',
|
||||
name: '放流量(万尾)',
|
||||
nameTextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#848484',
|
||||
align: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
axisTick: { show: true },
|
||||
axisLine: { show: true },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['放流量(万尾)', '放流种类(种)'],
|
||||
top: 10,
|
||||
itemWidth: 12,
|
||||
itemHeight: 8,
|
||||
itemGap: 8
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#343434'
|
||||
},
|
||||
xAxis: {
|
||||
show: true,
|
||||
type: 'category',
|
||||
axisLine: { show: true },
|
||||
splitLine: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#686868',
|
||||
rotate: 37,
|
||||
interval: 0
|
||||
},
|
||||
data: mockData.map(el => el.hbrvcdName),
|
||||
position: 'left',
|
||||
...fcntStep
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
type: 'value',
|
||||
name: '种类(种)',
|
||||
nameTextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#848484',
|
||||
align: 'left'
|
||||
},
|
||||
yAxis: [{
|
||||
show: true,
|
||||
type: 'value',
|
||||
name: '放流量(万尾)',
|
||||
nameTextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#848484',
|
||||
align: 'center'
|
||||
},
|
||||
axisTick: { show: true },
|
||||
axisLine: { show: true },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#343434'
|
||||
},
|
||||
position: 'left',
|
||||
...fcntStep,
|
||||
}, {
|
||||
show: true,
|
||||
type: 'value',
|
||||
name: '种类(种)',
|
||||
nameTextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#848484',
|
||||
align: 'left'
|
||||
},
|
||||
axisTick: { show: true },
|
||||
axisLine: { show: true },
|
||||
splitLine: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#343434'
|
||||
},
|
||||
position: 'right',
|
||||
...ftpStep,
|
||||
}],
|
||||
series: [{
|
||||
name: '放流量(万尾)',
|
||||
type: 'bar',
|
||||
barGap: 0,
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: fcntData,
|
||||
yAxisIndex: 0,
|
||||
barMaxWidth: 20,
|
||||
itemStyle: {
|
||||
color: '#5470C6'
|
||||
}
|
||||
}, {
|
||||
name: '放流种类(种)',
|
||||
type: 'bar',
|
||||
barGap: 0,
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: ftpData,
|
||||
yAxisIndex: 1,
|
||||
barMaxWidth: 20,
|
||||
itemStyle: {
|
||||
color: '#91CC75'
|
||||
}
|
||||
}]
|
||||
};
|
||||
axisTick: { show: true },
|
||||
axisLine: { show: true },
|
||||
splitLine: { show: false },
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#343434'
|
||||
},
|
||||
position: 'right',
|
||||
...ftpStep
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '放流量(万尾)',
|
||||
type: 'bar',
|
||||
barGap: 0,
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: fcntData,
|
||||
yAxisIndex: 0,
|
||||
barMaxWidth: 20,
|
||||
itemStyle: {
|
||||
color: '#5470C6'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '放流种类(种)',
|
||||
type: 'bar',
|
||||
barGap: 0,
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: ftpData,
|
||||
yAxisIndex: 1,
|
||||
barMaxWidth: 20,
|
||||
itemStyle: {
|
||||
color: '#91CC75'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 延时50ms确保容器尺寸已完全渲染
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
nextTick(() => {
|
||||
// 延时50ms确保容器尺寸已完全渲染
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, 50);
|
||||
});
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
const getData = async () => {
|
||||
loading.value = true;
|
||||
let params = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
baseid.value != 'all'
|
||||
? {
|
||||
field: 'baseId',
|
||||
operator: 'eq',
|
||||
value: baseid.value
|
||||
}
|
||||
: null,
|
||||
{
|
||||
field: 'startTime',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: datetimePicker.value.value + '-01-01 00:00:00'
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: datetimePicker.value.value + '-12-31 23:59:59'
|
||||
}
|
||||
].filter(Boolean)
|
||||
}
|
||||
};
|
||||
try {
|
||||
let res = await qgcgetFbStaticsData(params);
|
||||
|
||||
// 使用接口返回的实际数据
|
||||
if (res?.data?.data) {
|
||||
chartData.value = res.data.data;
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
});
|
||||
} else {
|
||||
chartData.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
chartData.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize();
|
||||
chartInstance?.resize();
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
//监听子组件的数据变化
|
||||
const handlePanelChange1 = async data => {
|
||||
console.log('当前所有控件状态:', data);
|
||||
// 当选择器或日期变化时,重新加载图表数据
|
||||
if (data.datetime) {
|
||||
await (datetimePicker.value.value = data.datetime);
|
||||
getData();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
newVal => {
|
||||
if (newVal && newVal.wbsCode) {
|
||||
baseid.value = newVal.wbsCode;
|
||||
getData();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
min-height: 300px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -53,7 +53,8 @@ export const useModelStore = defineStore('model', () => {
|
||||
stllgzlx: '', // 生态流量 - 规则类型
|
||||
stllTmType: '', // 生态流量 - 时间类型
|
||||
tm: '',
|
||||
rangeTm: []
|
||||
rangeTm: [],
|
||||
year:'',
|
||||
});
|
||||
return {
|
||||
params,
|
||||
|
||||
@ -34,7 +34,7 @@ service.interceptors.request.use(
|
||||
config.headers._sysid = '10EC2E0B-AEA9-4757-83A2-201BA1BC54E9';
|
||||
|
||||
config.headers.authorization =
|
||||
'bearer 0952e56d-8a77-4c43-abe4-a44367a22d73';
|
||||
'bearer cb68557d-2861-497d-b708-6b73ae68ba95';
|
||||
config.baseURL = '/';
|
||||
} else {
|
||||
const user = useUserStoreHook();
|
||||
|
||||
@ -21,7 +21,7 @@ watch(
|
||||
wbsCode.value = newVal.wbsCode;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user