页面模块增加
This commit is contained in:
commit
abe7721236
@ -3,7 +3,7 @@
|
||||
<div class="qgc-side-pannel-item">
|
||||
<div class="qgc_title">
|
||||
<div class="title_left">
|
||||
<span>{{ title }}</span>
|
||||
<span class="texttitle">{{ title }}</span>
|
||||
<span v-if="prompt.show" class="title_icon">
|
||||
<a-tooltip placement="top" :title="prompt.value" :get-popup-container="getPopupContainer">
|
||||
<QuestionCircleOutlined />
|
||||
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<div class="title_right">
|
||||
<div v-if="select.show">
|
||||
<a-select v-model:value="selectValue" show-search placeholder="请选择" style="width: 142px"
|
||||
<a-select v-model:value="selectValue" show-search placeholder="请选择" :size="'small'"
|
||||
:options="select.options" :filter-option="filterOption" @focus="handleFocus" @blur="handleBlur"
|
||||
@change="handleChange"></a-select>
|
||||
</div>
|
||||
@ -27,7 +27,7 @@
|
||||
<img v-else src="@/assets/components/arrow-down.png" alt="">
|
||||
</div>
|
||||
<div v-if="moreSelect.show">
|
||||
<a-tree-select v-model:value="moreSelectValue" show-search style="width: 170px"
|
||||
<a-tree-select v-model:value="moreSelectValue" show-search
|
||||
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" placeholder="Please select"
|
||||
allow-clear tree-default-expand-all :tree-data="moreSelect.options"
|
||||
tree-node-filter-prop="label">
|
||||
@ -37,8 +37,8 @@
|
||||
<!-- 添加 locale 属性来设置语言 -->
|
||||
<a-date-picker v-model:value="datetimeValue" show-time
|
||||
:format="datetimePicker.format !== null ? datetimePicker.format : undefined"
|
||||
:picker="datetimePicker.picker" placeholder="请选择时间" style="width: 180px"
|
||||
@change="handleDateTimeChange" />
|
||||
:picker="datetimePicker.picker" placeholder="请选择时间"
|
||||
@change="handleDateTimeChange" :size="'small'" />
|
||||
<!-- 修改为 locale 变量 -->
|
||||
</div>
|
||||
<div v-if="scopeDate.show">
|
||||
@ -212,17 +212,25 @@ onMounted(() => {
|
||||
.title_left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.texttitle{
|
||||
display: inline-block;
|
||||
width:180px;
|
||||
}
|
||||
.title_icon {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.title_right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
div{
|
||||
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
487
frontend/src/modules/GYZLLB/index.vue
Normal file
487
frontend/src/modules/GYZLLB/index.vue
Normal file
@ -0,0 +1,487 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem :title="title" :datetimePicker="datetimePicker">
|
||||
<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 renderChartData"
|
||||
:key="index"
|
||||
class="carousel-item"
|
||||
>
|
||||
<div class="pie-chart-container">
|
||||
<!-- 图表区域 -->
|
||||
<div :ref="el => setChartRef(el, index)" class="chart"></div>
|
||||
<!-- 标题 -->
|
||||
<div class="title">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'GYZLLB'
|
||||
});
|
||||
const props = defineProps({
|
||||
title: { // 标题
|
||||
type: String,
|
||||
default: '过鱼总量'
|
||||
},
|
||||
});
|
||||
const datetimePicker = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
format: 'YYYY',
|
||||
picker: 'year' as const,
|
||||
options: []
|
||||
});
|
||||
|
||||
// 图表相关
|
||||
interface ChartDataItem {
|
||||
title: string;
|
||||
data: Array<{ name: string; value: number }>;
|
||||
}
|
||||
|
||||
// 原始图表数据(多组数据用于跑马灯)
|
||||
const originalChartData = ref<ChartDataItem[]>([
|
||||
{
|
||||
title: '过鱼总量统计 - 2025年',
|
||||
data: [
|
||||
{ name: '草鱼', value: 1250 },
|
||||
{ name: '鲢鱼', value: 980 },
|
||||
{ name: '鳙鱼', value: 760 },
|
||||
{ name: '青鱼', value: 540 },
|
||||
{ name: '鲤鱼', value: 430 },
|
||||
{ name: '鲫鱼', value: 320 },
|
||||
{ name: '其他', value: 280 }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '过鱼总量统计 - 2024年',
|
||||
data: [
|
||||
{ name: '草鱼1', value: 1250 },
|
||||
{ name: '鲢鱼1', value: 980 },
|
||||
{ name: '鳙鱼1', value: 760 },
|
||||
{ name: '青鱼1', value: 540 },
|
||||
{ name: '鲤鱼1', value: 430 },
|
||||
{ name: '鲫鱼1', value: 320 },
|
||||
{ name: '其他1', value: 280 }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '过鱼总量统计 - 2023年',
|
||||
data: [
|
||||
{ name: '草鱼2', value: 1250 },
|
||||
{ name: '鲢鱼2', value: 980 },
|
||||
{ name: '鳙鱼2', value: 760 },
|
||||
{ name: '青鱼2', value: 540 },
|
||||
{ name: '鲤鱼2', value: 430 },
|
||||
{ name: '鲫鱼2', value: 320 },
|
||||
{ name: '其他2', value: 280 }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// 克隆首尾项后的渲染数组(用于无缝循环)
|
||||
const renderChartData = ref<ChartDataItem[]>([]);
|
||||
|
||||
// 当前显示索引(指向renderChartData)
|
||||
const currentIndex = ref(1); // 从1开始,跳过克隆的首项
|
||||
|
||||
// 定时器引用
|
||||
let timer: any = null;
|
||||
|
||||
// 鼠标悬停状态
|
||||
const isHovering = ref(false);
|
||||
|
||||
// 是否正在切换动画中(用于禁用transition)
|
||||
const isTransitioning = ref(false);
|
||||
|
||||
// 图表实例数组
|
||||
const chartInstances = ref<(ECharts | null)[]>([]);
|
||||
const chartRefs = ref<(HTMLElement | null)[]>([]);
|
||||
|
||||
// 设置图表ref
|
||||
const setChartRef = (el: any, index: number) => {
|
||||
if (el) {
|
||||
chartRefs.value[index] = el;
|
||||
}
|
||||
};
|
||||
|
||||
// 单位配置(模拟函数)
|
||||
const getUnitConfigByCode = (code: string, type: string): { unit: string } => {
|
||||
if (type === 'FCNT') {
|
||||
return { unit: '尾' };
|
||||
} else if (type === 'SL') {
|
||||
return { unit: '万尾' };
|
||||
}
|
||||
return { unit: '尾' };
|
||||
};
|
||||
|
||||
// 生成随机颜色(根据名称)- 生成适中亮度颜色
|
||||
const generateRandomColor = (names: string[]): string[] => {
|
||||
const colors: string[] = [];
|
||||
|
||||
names.forEach(() => {
|
||||
// 生成 HSL 颜色,控制饱和度和亮度在适中范围
|
||||
// H: 0-360 (色相)
|
||||
// S: 40-70% (饱和度,避免过于鲜艳或灰暗)
|
||||
// L: 45-65% (亮度,避免过亮或过暗)
|
||||
const hue = Math.floor(Math.random() * 360);
|
||||
const saturation = 40 + Math.floor(Math.random() * 30); // 40-70%
|
||||
const lightness = 45 + Math.floor(Math.random() * 20); // 45-65%
|
||||
|
||||
// 将 HSL 转换为 HEX
|
||||
const h = hue / 360;
|
||||
const s = saturation / 100;
|
||||
const l = lightness / 100;
|
||||
|
||||
let r, g, b;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l;
|
||||
} else {
|
||||
const hue2rgb = (p: number, q: number, t: number) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||
if (t < 1/2) return q;
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
|
||||
r = hue2rgb(p, q, h + 1/3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1/3);
|
||||
}
|
||||
|
||||
const toHex = (x: number) => {
|
||||
const hex = Math.round(x * 255).toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
};
|
||||
|
||||
colors.push(`#${toHex(r)}${toHex(g)}${toHex(b)}`);
|
||||
});
|
||||
|
||||
return colors;
|
||||
};
|
||||
|
||||
// 计算总量
|
||||
const getTotalAmount = (data: Array<{ name: string; value: number }>) => {
|
||||
return data.reduce((sum, item) => sum + item.value, 0);
|
||||
};
|
||||
|
||||
// 获取单位
|
||||
const getUnit = () => {
|
||||
const fyunit = getUnitConfigByCode('FPSSRL_R', 'FCNT')?.unit ?? '尾';
|
||||
return fyunit;
|
||||
};
|
||||
|
||||
// 初始化单个图表
|
||||
const initChart = (index: number) => {
|
||||
const chartRef = chartRefs.value[index];
|
||||
if (!chartRef) return;
|
||||
|
||||
const rect = chartRef.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
console.warn(`图表容器尺寸为0,延迟重试... 索引: ${index}`);
|
||||
setTimeout(() => initChart(index), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const chartInstance = echarts.init(chartRef);
|
||||
chartInstances.value[index] = chartInstance;
|
||||
updateChart(index);
|
||||
};
|
||||
|
||||
// 更新单个图表配置
|
||||
const updateChart = (index: number) => {
|
||||
const chartInstance = chartInstances.value[index];
|
||||
const chartData = renderChartData.value[index];
|
||||
|
||||
if (!chartInstance || !chartData) return;
|
||||
|
||||
const colorNames = chartData.data.map(item => item.name);
|
||||
const colors = generateRandomColor(colorNames);
|
||||
const totalAmount = getTotalAmount(chartData.data);
|
||||
const unit = getUnit();
|
||||
|
||||
const option: echarts.EChartsOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: `{b}: {c} ${unit} ({d}%)`,
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
title: [
|
||||
{
|
||||
text: `${totalAmount}`,
|
||||
subtext: `总量(${unit})`,
|
||||
left: '34%',
|
||||
top: '45%',
|
||||
textAlign: 'center',
|
||||
textVerticalAlign: 'middle',
|
||||
textStyle: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
},
|
||||
subtextStyle: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
align: 'center',
|
||||
}
|
||||
}
|
||||
],
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 5,
|
||||
top: 'middle',
|
||||
bottom: 25,
|
||||
itemWidth: 25,
|
||||
itemHeight: 13,
|
||||
itemGap: 8,
|
||||
pageIconColor: '#333',
|
||||
pageIconInactiveColor: '#ccc',
|
||||
pageIconSize: 12,
|
||||
pageFormatter: '{current}/{total}',
|
||||
formatter: function(name: string) {
|
||||
const found = chartData.data.find(item => item.name === name);
|
||||
const value = found ? found.value : '-';
|
||||
const maxLength = 7;
|
||||
const truncatedName = name.length > maxLength ? name.substring(0, maxLength) + '...' : name;
|
||||
return `${truncatedName} ${value}${unit}`;
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 13,
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['35%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData.data.map((item, idx) => ({
|
||||
...item,
|
||||
itemStyle: {
|
||||
color: colors[idx]
|
||||
}
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chartInstances.value.forEach(instance => {
|
||||
if (instance) {
|
||||
instance.resize();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化渲染数组(克隆首尾项)
|
||||
const initRenderData = () => {
|
||||
const length = originalChartData.value.length;
|
||||
if (length === 0) return;
|
||||
|
||||
renderChartData.value = [
|
||||
originalChartData.value[length - 1], // 克隆最后一项
|
||||
...originalChartData.value, // 原始数据
|
||||
originalChartData.value[0] // 克隆第一项
|
||||
];
|
||||
|
||||
// 初始化图表实例数组
|
||||
chartInstances.value = new Array(renderChartData.value.length).fill(null);
|
||||
chartRefs.value = new Array(renderChartData.value.length).fill(null);
|
||||
};
|
||||
|
||||
// 启动自动轮播
|
||||
const startAutoPlay = () => {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
if (!isHovering.value && !isTransitioning.value) {
|
||||
nextSlide();
|
||||
}
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
// 切换到下一张
|
||||
const nextSlide = () => {
|
||||
currentIndex.value++;
|
||||
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
};
|
||||
|
||||
// 检查是否需要无缝跳转(克隆项处理)
|
||||
const checkSeamlessJump = () => {
|
||||
const realLength = originalChartData.value.length;
|
||||
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
};
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(async () => {
|
||||
initRenderData();
|
||||
|
||||
// 等待DOM渲染完成
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
// 初始化所有图表
|
||||
renderChartData.value.forEach((_, index) => {
|
||||
initChart(index);
|
||||
});
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// 启动自动轮播
|
||||
startAutoPlay();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
|
||||
// 销毁所有图表实例
|
||||
chartInstances.value.forEach(instance => {
|
||||
if (instance) {
|
||||
instance.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 290px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
|
||||
// 跑马灯轨道
|
||||
.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; // 防止被压缩
|
||||
|
||||
.pie-chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
221
frontend/src/modules/ZhenXiZhiWuYuanMod/Dwjzqk/index.vue
Normal file
221
frontend/src/modules/ZhenXiZhiWuYuanMod/Dwjzqk/index.vue
Normal file
@ -0,0 +1,221 @@
|
||||
<!-- 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
|
||||
v-if="item.type === 'image'"
|
||||
:src="item.url"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<!-- 视频 -->
|
||||
<video
|
||||
v-else
|
||||
:src="item.url"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
></video>
|
||||
|
||||
<!-- 说明文字 -->
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
|
||||
});
|
||||
|
||||
// 媒体类型定义
|
||||
interface MediaItem {
|
||||
type: 'image' | 'video';
|
||||
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: 'video',
|
||||
url: 'https://example.com/video1.mp4', // 视频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 renderMediaData = ref<MediaItem[]>([]);
|
||||
|
||||
// 当前显示索引(指向renderMediaData)
|
||||
const currentIndex = ref(1); // 从1开始,跳过克隆的首项
|
||||
|
||||
// 定时器引用
|
||||
let timer: any = null;
|
||||
|
||||
// 鼠标悬停状态
|
||||
const isHovering = ref(false);
|
||||
|
||||
// 是否正在切换动画中(用于禁用transition)
|
||||
const isTransitioning = ref(false);
|
||||
|
||||
// 初始化渲染数组(克隆首尾项)
|
||||
const initRenderData = () => {
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// 切换到下一张
|
||||
const nextSlide = () => {
|
||||
currentIndex.value++;
|
||||
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
};
|
||||
|
||||
// 检查是否需要无缝跳转(克隆项处理)
|
||||
const checkSeamlessJump = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
};
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 290px;
|
||||
border: 1px solid #7fd6ff;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
|
||||
// 跑马灯轨道
|
||||
.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; // 防止被压缩
|
||||
|
||||
img, video {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,135 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="植物园情况">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
|
||||
});
|
||||
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
|
||||
// 颜色配置
|
||||
const jdColor = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'];
|
||||
|
||||
// 模拟数据
|
||||
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 }
|
||||
];
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
chartInstance = echarts.init(chartRef.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: 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);
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 延迟初始化,确保容器已渲染
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
}, 50);
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize();
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 310px;
|
||||
}
|
||||
</style>
|
||||
134
frontend/src/modules/guoyujiance/index.vue
Normal file
134
frontend/src/modules/guoyujiance/index.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="过鱼监测">
|
||||
<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: 16px;"> <span class="facility-count">{{ facility.count
|
||||
}}</span><span>座</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'guoyusheshijiansheqingkuang'
|
||||
});
|
||||
|
||||
// 设施数据
|
||||
const facilities = ref([
|
||||
{
|
||||
name: '视频监控',
|
||||
count: 56,
|
||||
icon: 'icon iconfont icon-shipinjiankongshebei'
|
||||
},
|
||||
{
|
||||
name: 'AI摄像头',
|
||||
count: 1722,
|
||||
icon: 'icon iconfont icon-jiankong'
|
||||
},
|
||||
{
|
||||
name: '声纳',
|
||||
count: 135,
|
||||
icon: 'icon iconfont icon-dwsjhQianzhidangqiang'
|
||||
},
|
||||
{
|
||||
name: '水质设备',
|
||||
count: 135,
|
||||
icon: 'icon iconfont icon-shuizhijiancezhan'
|
||||
},
|
||||
{
|
||||
name: '水文设备',
|
||||
count: 56,
|
||||
icon: 'icon iconfont icon-guojiashuizhizhan'
|
||||
},
|
||||
{
|
||||
name: '气象设备',
|
||||
count: 1722,
|
||||
icon: 'icon iconfont icon-qixiangzhan'
|
||||
},
|
||||
|
||||
]);
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 延迟初始化,确保容器已渲染
|
||||
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
|
||||
});
|
||||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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%;
|
||||
|
||||
.anticon {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.facility-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.facility-name {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
// margin-bottom: 4px;
|
||||
// font-weight: 500;
|
||||
}
|
||||
|
||||
.facility-count {
|
||||
font-size: 18px;
|
||||
color: #2f6b98;
|
||||
// font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
124
frontend/src/modules/guoyusheshijiansheqingkuang/index.vue
Normal file
124
frontend/src/modules/guoyusheshijiansheqingkuang/index.vue
Normal file
@ -0,0 +1,124 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="过鱼设施">
|
||||
<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: 16px;"> <span class="facility-count">{{ facility.count
|
||||
}}</span><span>座</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'guoyusheshijiansheqingkuang'
|
||||
});
|
||||
|
||||
// 设施数据
|
||||
const facilities = ref([
|
||||
{
|
||||
name: '鱼道',
|
||||
count: 56,
|
||||
icon: 'icon iconfont icon-map-gyssYudao'
|
||||
},
|
||||
{
|
||||
name: '集运鱼系统',
|
||||
count: 1722,
|
||||
icon: 'icon iconfont icon-map-gyssJiyunyuxitong'
|
||||
},
|
||||
{
|
||||
name: '升鱼机',
|
||||
count: 135,
|
||||
icon: 'icon iconfont icon-map-gyssShengyuji'
|
||||
},
|
||||
{
|
||||
name: '其他',
|
||||
count: 135,
|
||||
icon: 'icon iconfont icon-map-gyssQita'
|
||||
},
|
||||
|
||||
]);
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 延迟初始化,确保容器已渲染
|
||||
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
|
||||
});
|
||||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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%;
|
||||
|
||||
.anticon {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.facility-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.facility-name {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
// margin-bottom: 4px;
|
||||
// font-weight: 500;
|
||||
}
|
||||
|
||||
.facility-count {
|
||||
font-size: 18px;
|
||||
color: #2f6b98;
|
||||
// font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
286
frontend/src/modules/guoyusheshijieshao/index.vue
Normal file
286
frontend/src/modules/guoyusheshijieshao/index.vue
Normal file
@ -0,0 +1,286 @@
|
||||
<!-- 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>
|
||||
</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>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
|
||||
});
|
||||
|
||||
// 媒体类型定义
|
||||
interface MediaItem {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// 原始媒体数据(3条测试数据)
|
||||
const originalMediaData = ref<MediaItem[]>([
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20240328104745462272538202651107&view=jpg&token=bearer 50b652db-035b-43a9-becd-48b7d54aa941',
|
||||
text: '铜街子鱼道',
|
||||
description: '鱼道布置在河床右岸,全长 1388.32m,由鱼道进口、进口段、绕过木阀闸段、坝后开挖段、过坝段、出口明渠段、鱼道出口组成。鱼道布置 1 个进口和 1 个出口,其中进口位于新华电站尾水渠下游约15m 处。出口位于库区右岸,距离坝轴线约 150m。'
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
url: 'https://211.99.26.225:12125/?20240328104800052128627215562160&view=jpg&token=bearer 50b652db-035b-43a9-becd-48b7d54aa941', // 视频URL示例,您可替换为真实地址
|
||||
text: '枕头坝一级鱼道',
|
||||
description: '枕头坝一级鱼道建筑物主要由鱼道进口、梯身、鱼道出口等组成。全长1300m、池室坡度i=0.033、鱼道净宽2.4m、池室长度为3m,每隔10个水池设立一个长6m的休息池,池室隔墙厚度为0.20m,高3.00m;设置3个进口,进口断面尺寸为2.40×3.00m(宽×高)。'
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
url: 'https://211.99.26.225:12125/?20240328104800052128627215562160&view=jpg&token=bearer 50b652db-035b-43a9-becd-48b7d54aa941',
|
||||
text: '沙坪二级鱼道',
|
||||
description: '沙坪二级鱼道位于泄洪闸与厂房之间,鱼道上下游方向长 224.42m,宽 10.0m~14.0m,最大高度 29.85m~37.00m。鱼道由诱鱼系统、入口、池室、休息池、观察室和出口等组成,其中诱鱼系统和入口布置于下游厂房尾水侧,出口布置于上游泄洪闸进口侧。鱼道采取连续“绕弯”方式布置,全长 597m,平均坡降为 3.85%。沙坪二级鱼道位于泄洪闸与厂房之间,鱼道上下游方向长 224.42m,宽 10.0m~14.0m,最大高度 29.85m~37.00m。鱼道由诱鱼系统、入口、池室、休息池、观察室和出口等组成,其中诱鱼系统和入口布置于下游厂房尾水侧,出口布置于上游泄洪闸进口侧。鱼道采取连续“绕弯”方式布置,全长 597m,平均坡降为 3.85%。'
|
||||
}
|
||||
]);
|
||||
|
||||
// 克隆首尾项后的渲染数组(用于无缝循环)
|
||||
// 结构:[最后一项克隆, 原始数据..., 第一项克隆]
|
||||
const renderMediaData = ref<MediaItem[]>([]);
|
||||
|
||||
// 当前显示索引(指向renderMediaData)
|
||||
const currentIndex = ref(1); // 从1开始,跳过克隆的首项
|
||||
|
||||
// 定时器引用
|
||||
let timer: any = null;
|
||||
|
||||
// 鼠标悬停状态
|
||||
const isHovering = ref(false);
|
||||
|
||||
// 是否正在切换动画中(用于禁用transition)
|
||||
const isTransitioning = ref(false);
|
||||
|
||||
// 初始化渲染数组(克隆首尾项)
|
||||
const initRenderData = () => {
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// 切换到下一张
|
||||
const nextSlide = () => {
|
||||
currentIndex.value++;
|
||||
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
};
|
||||
|
||||
// 检查是否需要无缝跳转(克隆项处理)
|
||||
const checkSeamlessJump = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
};
|
||||
|
||||
// 计算当前显示的描述文字
|
||||
const currentDescription = computed(() => {
|
||||
const realIndex = getCurrentRealIndex();
|
||||
return originalMediaData.value[realIndex]?.description || '';
|
||||
});
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
|
||||
// 获取当前真实的索引(排除克隆项的影响)
|
||||
const getCurrentRealIndex = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
||||
|
||||
// 处理边界情况(无缝循环时的克隆项)
|
||||
if (realIndex < 0) realIndex = realLength - 1;
|
||||
if (realIndex >= realLength) realIndex = 0;
|
||||
|
||||
return realIndex;
|
||||
};
|
||||
|
||||
// 跳转到指定幻灯片
|
||||
const goToSlide = (targetIndex: number) => {
|
||||
if (isTransitioning.value) return;
|
||||
|
||||
// 计算目标索引(考虑克隆项偏移)
|
||||
currentIndex.value = targetIndex + 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
// border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
|
||||
// 跑马灯轨道
|
||||
.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; // 防止被压缩
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
// 多行文本溢出省略
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
292
frontend/src/modules/qixidijchuXx/index.vue
Normal file
292
frontend/src/modules/qixidijchuXx/index.vue
Normal file
@ -0,0 +1,292 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="主要栖息地基本信息" :select="select">
|
||||
<div class="table-container">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:pagination="pagination"
|
||||
:scroll="{ y: 450 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 仅栖息地列需要自定义渲染 -->
|
||||
<template v-if="column.key === 'fhstnm'">
|
||||
<a-tooltip :title="record.fhstnm">
|
||||
<span
|
||||
class="habitat-cell"
|
||||
@click="handleHabitatClick(record)"
|
||||
>
|
||||
{{ record.fhstnm }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'qixidijchuXx'
|
||||
});
|
||||
|
||||
const select = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
options: [],
|
||||
picker: undefined,
|
||||
format: undefined
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
key: 'fhstnm',
|
||||
title: '栖息地',
|
||||
dataIndex: 'fhstnm',
|
||||
width: 136,
|
||||
fixed: 'left' as const,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'bhhl',
|
||||
title: '保护河流',
|
||||
dataIndex: 'bhhl',
|
||||
width: 106,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'qxdbhdx',
|
||||
title: '保护对象',
|
||||
dataIndex: 'qxdbhdx',
|
||||
width: 96,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'qxdbhcd',
|
||||
title: '保护总长(km)',
|
||||
dataIndex: 'qxdbhcd',
|
||||
width: 96,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
key: 'qxdbhmj',
|
||||
title: '保护面积(km²)',
|
||||
dataIndex: 'qxdbhmj',
|
||||
width: 96,
|
||||
ellipsis: true
|
||||
}
|
||||
]
|
||||
|
||||
// 分页配置
|
||||
const pagination = {
|
||||
pageSize: 20,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
showSizeChanger: false
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const tableData = ref([
|
||||
{
|
||||
key: '1',
|
||||
fhstnm: '长江上游珍稀鱼类栖息地A区',
|
||||
bhhl: '金沙江',
|
||||
qxdbhdx: '中华鲟',
|
||||
qxdbhcd: 120.5,
|
||||
qxdbhmj: 85.3
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
fhstnm: '岷江特有鱼类保护区',
|
||||
bhhl: '岷江',
|
||||
qxdbhdx: '胭脂鱼',
|
||||
qxdbhcd: 95.2,
|
||||
qxdbhmj: 62.7
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
fhstnm: '雅砻江冷水鱼栖息地',
|
||||
bhhl: '雅砻江',
|
||||
qxdbhdx: '齐口裂腹鱼',
|
||||
qxdbhcd: 110.8,
|
||||
qxdbhmj: 78.4
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
fhstnm: '大渡河高原鳅保护区',
|
||||
bhhl: '大渡河',
|
||||
qxdbhdx: '高原鳅',
|
||||
qxdbhcd: 88.6,
|
||||
qxdbhmj: 55.9
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
fhstnm: '乌江珍稀鱼类核心栖息地',
|
||||
bhhl: '乌江',
|
||||
qxdbhdx: '岩原鲤',
|
||||
qxdbhcd: 135.4,
|
||||
qxdbhmj: 92.1
|
||||
},
|
||||
{
|
||||
key: '6',
|
||||
fhstnm: '嘉陵江鱼类自然保护区',
|
||||
bhhl: '嘉陵江',
|
||||
qxdbhdx: '白甲鱼',
|
||||
qxdbhcd: 102.3,
|
||||
qxdbhmj: 71.5
|
||||
},
|
||||
{
|
||||
key: '7',
|
||||
fhstnm: '沱江特有鱼类栖息地B段',
|
||||
bhhl: '沱江',
|
||||
qxdbhdx: '圆口铜鱼',
|
||||
qxdbhcd: 76.9,
|
||||
qxdbhmj: 48.2
|
||||
},
|
||||
{
|
||||
key: '8',
|
||||
fhstnm: '赤水河珍稀鱼类保护区',
|
||||
bhhl: '赤水河',
|
||||
qxdbhdx: '赤水河独鱼',
|
||||
qxdbhcd: 118.7,
|
||||
qxdbhmj: 82.6
|
||||
},
|
||||
{
|
||||
key: '9',
|
||||
fhstnm: '涪江高原鱼类栖息地',
|
||||
bhhl: '涪江',
|
||||
qxdbhdx: '松潘裸鲤',
|
||||
qxdbhcd: 92.4,
|
||||
qxdbhmj: 64.8
|
||||
},
|
||||
{
|
||||
key: '10',
|
||||
fhstnm: '渠江特有鱼类核心保护区',
|
||||
bhhl: '渠江',
|
||||
qxdbhdx: '华鳊',
|
||||
qxdbhcd: 105.6,
|
||||
qxdbhmj: 73.9
|
||||
},
|
||||
{
|
||||
key: '11',
|
||||
fhstnm: '汉江上游珍稀鱼类栖息地',
|
||||
bhhl: '汉江',
|
||||
qxdbhdx: '多鳞铲颌鱼',
|
||||
qxdbhcd: 128.3,
|
||||
qxdbhmj: 89.7
|
||||
},
|
||||
{
|
||||
key: '12',
|
||||
fhstnm: '丹江口库区鱼类保护区',
|
||||
bhhl: '丹江',
|
||||
qxdbhdx: '青鱼',
|
||||
qxdbhcd: 142.5,
|
||||
qxdbhmj: 98.4
|
||||
},
|
||||
{
|
||||
key: '13',
|
||||
fhstnm: '清江土著鱼类栖息地',
|
||||
bhhl: '清江',
|
||||
qxdbhdx: '清江银鱼',
|
||||
qxdbhcd: 85.7,
|
||||
qxdbhmj: 59.3
|
||||
},
|
||||
{
|
||||
key: '14',
|
||||
fhstnm: '湘江珍稀鱼类核心栖息地C区',
|
||||
bhhl: '湘江',
|
||||
qxdbhdx: '湘鲫',
|
||||
qxdbhcd: 112.9,
|
||||
qxdbhmj: 79.6
|
||||
},
|
||||
{
|
||||
key: '15',
|
||||
fhstnm: '赣江特有鱼类保护区',
|
||||
bhhl: '赣江',
|
||||
qxdbhdx: '赣江鮰',
|
||||
qxdbhcd: 98.4,
|
||||
qxdbhmj: 68.2
|
||||
},
|
||||
{
|
||||
key: '16',
|
||||
fhstnm: '闽江珍稀鱼类栖息地D段',
|
||||
bhhl: '闽江',
|
||||
qxdbhdx: '闽江小鳔鮈',
|
||||
qxdbhcd: 106.8,
|
||||
qxdbhmj: 74.5
|
||||
},
|
||||
{
|
||||
key: '17',
|
||||
fhstnm: '珠江三角洲鱼类核心保护区',
|
||||
bhhl: '珠江',
|
||||
qxdbhdx: '唐鱼',
|
||||
qxdbhcd: 132.6,
|
||||
qxdbhmj: 91.8
|
||||
},
|
||||
{
|
||||
key: '18',
|
||||
fhstnm: '西江上游珍稀鱼类栖息地',
|
||||
bhhl: '西江',
|
||||
qxdbhdx: '光倒刺鲃',
|
||||
qxdbhcd: 115.3,
|
||||
qxdbhmj: 80.7
|
||||
}
|
||||
])
|
||||
|
||||
// 点击事件处理(预留功能)
|
||||
const handleHabitatClick = (record: any) => {
|
||||
console.log('点击栖息地:', record)
|
||||
// TODO: 后续实现具体功能
|
||||
}
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 延迟初始化,确保容器已渲染
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-container {
|
||||
height: 531px;
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table-body {
|
||||
max-height: 450px;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
// 表头字体加粗
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.habitat-cell {
|
||||
color: #2f6b98;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
489
frontend/src/modules/qixidiliuliangbianhua/index.vue
Normal file
489
frontend/src/modules/qixidiliuliangbianhua/index.vue
Normal file
@ -0,0 +1,489 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="水文监测" :select="select">
|
||||
<div class="chart-container">
|
||||
<div ref="chartRef" class="echarts-chart"></div>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名
|
||||
defineOptions({
|
||||
name: 'qixidiliuliangbianhua'
|
||||
});
|
||||
|
||||
// ==================== 选择器配置 ====================
|
||||
const select = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
options: [],
|
||||
picker: undefined,
|
||||
format: undefined
|
||||
});
|
||||
|
||||
// ==================== 图表相关响应式数据 ====================
|
||||
const chartRef = ref<HTMLElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
|
||||
// 模拟图表数据(水温监测 - 12个月)
|
||||
const echartsData = ref<any[]>([
|
||||
{ dt: '2024-01', wt: 8.5, q: 120.5, z: 45.2 },
|
||||
{ dt: '2024-02', wt: 9.2, q: 135.8, z: 46.1 },
|
||||
{ dt: '2024-03', wt: 12.8, q: 180.3, z: 47.5 },
|
||||
{ dt: '2024-04', wt: 16.3, q: 220.6, z: 48.8 },
|
||||
{ dt: '2024-05', wt: 20.5, q: 280.2, z: 50.3 },
|
||||
{ dt: '2024-06', wt: 24.8, q: 350.5, z: 52.1 },
|
||||
{ dt: '2024-07', wt: 28.3, q: 420.8, z: 53.6 },
|
||||
{ dt: '2024-08', wt: 27.5, q: 390.3, z: 52.9 },
|
||||
{ dt: '2024-09', wt: 23.2, q: 310.7, z: 51.2 },
|
||||
{ dt: '2024-10', wt: 18.6, q: 240.5, z: 49.5 },
|
||||
{ dt: '2024-11', wt: 13.4, q: 165.2, z: 47.8 },
|
||||
{ dt: '2024-12', wt: 9.8, q: 130.6, z: 46.3 }
|
||||
]);
|
||||
|
||||
// 图表配置引用(用于 legendselectchanged 时读取最新配置)
|
||||
const echartOptionRef = ref<EChartsOption>({});
|
||||
|
||||
// ==================== 工具函数:omit ====================
|
||||
const omit = (obj: any, key: string) => {
|
||||
const newObj = { ...obj };
|
||||
delete newObj[key];
|
||||
return newObj;
|
||||
};
|
||||
|
||||
// ==================== 多Y轴动态布局算法 ====================
|
||||
/**
|
||||
* 根据图例选中状态动态调整 Y 轴布局和 grid 边距
|
||||
* @param selected - 图例选中状态对象 { '系列名': boolean }
|
||||
* @param options - ECharts 配置对象
|
||||
*
|
||||
* 规则:
|
||||
* - 左侧最多 1 个 Y 轴(第一个可见系列)
|
||||
* - 右侧可以显示多个 Y 轴(其余可见系列)
|
||||
*/
|
||||
const yAxisShowDynamic = (selected: Record<string, boolean>, options: any) => {
|
||||
const allShow = options.yAxis?.filter((item: any) => item.show);
|
||||
const showCount = allShow?.length || 0;
|
||||
|
||||
// 没有显示的Y轴
|
||||
if (showCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有一个Y轴:统一置于左侧
|
||||
if (showCount === 1) {
|
||||
options.grid = omit(options.grid, 'right');
|
||||
options.grid.left = '80px';
|
||||
|
||||
options.yAxis = options.yAxis.map((item: any) => {
|
||||
if (item.show) {
|
||||
return {
|
||||
...item,
|
||||
position: 'left',
|
||||
offset: 0
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 两个及以上Y轴:左侧1个,右侧其余
|
||||
if (showCount >= 2) {
|
||||
let leftIndex = 0; // 左侧Y轴索引
|
||||
let rightIndex = 0; // 右侧Y轴计数
|
||||
|
||||
options.yAxis = options.yAxis.map((item: any) => {
|
||||
if (!item.show) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// 第一个可见的Y轴放在左侧
|
||||
if (leftIndex === 0) {
|
||||
leftIndex++;
|
||||
options.grid = omit(options.grid, 'right');
|
||||
options.grid.left = '80px';
|
||||
|
||||
return {
|
||||
...item,
|
||||
position: 'left',
|
||||
offset: 0
|
||||
};
|
||||
} else {
|
||||
// 其余Y轴放在右侧
|
||||
rightIndex++;
|
||||
|
||||
// 右侧有多个Y轴时,需要增加右边距并设置偏移
|
||||
if (rightIndex > 1) {
|
||||
options.grid.right = '100px';
|
||||
return {
|
||||
...item,
|
||||
position: 'right',
|
||||
offset: 60
|
||||
};
|
||||
} else {
|
||||
options.grid.right = '60px';
|
||||
return {
|
||||
...item,
|
||||
position: 'right',
|
||||
offset: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 图表初始化 ====================
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) {
|
||||
console.warn('图表容器未就绪');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化 ECharts 实例
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
|
||||
// 监听图例选择变化事件
|
||||
chartInstance.on('legendselectchanged', (params: any) => {
|
||||
const { selected } = params;
|
||||
|
||||
// 深拷贝当前配置
|
||||
const options = JSON.parse(JSON.stringify(echartOptionRef.value));
|
||||
|
||||
// 同步图例选中状态
|
||||
options.legend.selected = selected;
|
||||
|
||||
// 根据图例状态更新 Y 轴显隐
|
||||
const newYAxis = options.yAxis.map((item: any) => {
|
||||
let isShow = true;
|
||||
for (const key in selected) {
|
||||
if (key === item.name) {
|
||||
isShow = selected[key];
|
||||
}
|
||||
}
|
||||
return { ...item, show: isShow };
|
||||
});
|
||||
|
||||
options.yAxis = newYAxis;
|
||||
|
||||
// 触发布局调整
|
||||
yAxisShowDynamic(selected, options);
|
||||
|
||||
// 完全替换模式更新配置
|
||||
chartInstance?.setOption(options, true);
|
||||
echartOptionRef.value = options;
|
||||
});
|
||||
|
||||
// 监听数据点点击事件
|
||||
chartInstance.on('click', (params: any) => {
|
||||
console.log('点击数据点:', {
|
||||
date: params.name,
|
||||
seriesName: params.seriesName,
|
||||
value: params.value
|
||||
});
|
||||
// TODO: 可扩展为打开详情弹窗
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 图表配置生成 ====================
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return;
|
||||
|
||||
// 单位和颜色配置(可根据实际需求从配置系统获取)
|
||||
const Color = ['#5470c6', '#91cc75', '#fac858'];
|
||||
|
||||
const _legendData = ['水温(℃)', '流量(m³/s)', '水位(m)'];
|
||||
|
||||
const setting: EChartsOption = {
|
||||
// Tooltip 配置
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#ffffff'
|
||||
},
|
||||
valueFormatter: (value: any) => (value === undefined ? '-' : value),
|
||||
formatter: (params: any) => {
|
||||
let res = `${params[0].name}<br/>`;
|
||||
params.forEach((item: any) => {
|
||||
const seriesName = item.seriesName ?? '';
|
||||
const regx = /\(([^()]+?)\)/;
|
||||
const unit = seriesName.match(regx);
|
||||
const finalValue = item.value !== undefined && item.value !== null ? item.value : '-';
|
||||
|
||||
if (item.value !== undefined && item.value !== null) {
|
||||
res += `<span style="background: ${item.color}; height:10px; width: 10px; border-radius: 50%; display: inline-block; margin-right:10px;"></span>${item.seriesName} ${finalValue}${unit?.[1] || ''}<br/>`;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
},
|
||||
|
||||
// 颜色配置
|
||||
color: Color,
|
||||
|
||||
// 图例配置
|
||||
legend: {
|
||||
data: _legendData,
|
||||
selectedMode: 'multiple',
|
||||
top: 0,
|
||||
left: 'center',
|
||||
itemGap: 15,
|
||||
// 默认只选中"水温",其他系列隐藏
|
||||
selected: {
|
||||
'水温(℃)': true,
|
||||
'流量(m³/s)': false,
|
||||
'水位(m)': false
|
||||
}
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
shadowStyle: {
|
||||
color: 'rgba(84, 112, 198, 0.2)'
|
||||
}
|
||||
},
|
||||
// 网格配置 - 为三个Y轴预留空间
|
||||
grid: {
|
||||
left: '0px',
|
||||
right: '60px',
|
||||
bottom: '10%',
|
||||
top: '65px'
|
||||
},
|
||||
|
||||
// 数据缩放配置
|
||||
dataZoom: {
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100
|
||||
},
|
||||
|
||||
// X轴配置
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: echartsData.value.map(item => item.dt),
|
||||
boundaryGap: true,
|
||||
axisLabel: {
|
||||
show: true
|
||||
},
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
color: '#e0e0e0'
|
||||
}
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
shadowStyle: {
|
||||
color: 'rgba(84, 112, 198, 0.2)'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Y轴配置 - 三个独立Y轴
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '水温(℃)',
|
||||
position: 'left',
|
||||
offset: 0,
|
||||
show: true,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
color: '#e0e0e0'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
formatter: '{value}',
|
||||
color: '#5470c6'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#6e7079',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
nameTextStyle: {
|
||||
color: '#5470c6',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '流量(m³/s)',
|
||||
position: 'left',
|
||||
offset: 60,
|
||||
show: false,
|
||||
splitLine: { show: false },
|
||||
axisTick: {
|
||||
show: true
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
formatter: '{value}',
|
||||
color: '#91cc75'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#6e7079',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
nameTextStyle: {
|
||||
color: '#91cc75',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '水位(m)',
|
||||
position: 'right',
|
||||
offset: 0,
|
||||
show: false,
|
||||
splitLine: { show: false },
|
||||
axisTick: {
|
||||
show: true
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
formatter: '{value}',
|
||||
color: '#fac858'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#6e7079',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
nameTextStyle: {
|
||||
color: '#fac858',
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// 系列配置
|
||||
series: [
|
||||
{
|
||||
name: '水温(℃)',
|
||||
data: echartsData.value.map(item => item.wt),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
connectNulls: true,
|
||||
symbolSize: 4,
|
||||
symbol: 'circle',
|
||||
itemStyle: {
|
||||
color: '#5470c6'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2.5
|
||||
},
|
||||
yAxisIndex: 0
|
||||
},
|
||||
{
|
||||
name: '流量(m³/s)',
|
||||
data: echartsData.value.map(item => item.q),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
connectNulls: true,
|
||||
symbolSize: 4,
|
||||
symbol: 'circle',
|
||||
itemStyle: {
|
||||
color: '#91cc75'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2.5
|
||||
},
|
||||
yAxisIndex: 1
|
||||
},
|
||||
{
|
||||
name: '水位(m)',
|
||||
data: echartsData.value.map(item => item.z),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
connectNulls: true,
|
||||
symbolSize: 4,
|
||||
symbol: 'circle',
|
||||
itemStyle: {
|
||||
color: '#fac858'
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2.5
|
||||
},
|
||||
yAxisIndex: 2
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 保存配置引用
|
||||
echartOptionRef.value = setting;
|
||||
|
||||
// 完全替换模式更新图表
|
||||
chartInstance.setOption(setting, true);
|
||||
|
||||
// ✅ 初始化时应用动态布局算法,确保只有水温Y轴显示
|
||||
yAxisShowDynamic({
|
||||
'水温(℃)': true,
|
||||
'流量(m³/s)': false,
|
||||
'水位(m)': false
|
||||
}, setting);
|
||||
|
||||
// 再次应用调整后的配置
|
||||
chartInstance.setOption(setting, true);
|
||||
};
|
||||
|
||||
// ==================== 生命周期钩子 ====================
|
||||
onMounted(async () => {
|
||||
// 双重等待机制:nextTick + setTimeout
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
updateChart();
|
||||
}, 50);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 销毁图表实例,防止内存泄漏
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
height: 231px;
|
||||
min-height: 231px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.echarts-chart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
316
frontend/src/modules/qixidishuiwenbianhua/index.vue
Normal file
316
frontend/src/modules/qixidishuiwenbianhua/index.vue
Normal file
@ -0,0 +1,316 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="水温监测" :select="select" :datetimePicker="datetimePicker">
|
||||
<div ref="chartRef" class="water-temp-chart"></div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名
|
||||
defineOptions({
|
||||
name: 'qixidishuiwenbianhua'
|
||||
});
|
||||
|
||||
// ==================== 响应式变量定义 ====================
|
||||
|
||||
// 图表容器引用
|
||||
const chartRef = ref<HTMLDivElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
|
||||
// 选择器配置
|
||||
const select = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
options: [],
|
||||
picker: undefined,
|
||||
format: undefined
|
||||
});
|
||||
|
||||
// 日期选择器配置
|
||||
const datetimePicker = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
format: 'YYYY',
|
||||
picker: 'year' as const,
|
||||
options: []
|
||||
});
|
||||
|
||||
// 图表数据
|
||||
const echartsData = ref<any[]>([]);
|
||||
|
||||
// 单位配置
|
||||
const unita = ref('℃');
|
||||
|
||||
// 颜色配置 - 使用HSL色彩空间生成舒适的颜色
|
||||
const Color = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'];
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
* 生成模拟数据 - 季节性水温变化
|
||||
*/
|
||||
const generateMockData = () => {
|
||||
const data = [];
|
||||
const baseTemp = 15; // 基础温度
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const month = String(i + 1).padStart(2, '0');
|
||||
// 模拟季节性变化:夏季高,冬季低
|
||||
// 正弦波模拟:峰值在7-8月,谷值在1-2月
|
||||
const temp = baseTemp + Math.sin((i / 12) * Math.PI * 2 - Math.PI / 2) * 10 + Math.random() * 2;
|
||||
|
||||
data.push({
|
||||
dt: `2024-${month}`,
|
||||
wt: parseFloat(temp.toFixed(1))
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化图表
|
||||
*/
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) {
|
||||
console.warn('图表容器未找到');
|
||||
return;
|
||||
}
|
||||
|
||||
// 销毁已存在的实例
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
|
||||
// 创建新实例
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
|
||||
// 更新图表
|
||||
updateChart();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新图表配置
|
||||
*/
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return;
|
||||
|
||||
const _legendData = [`水温(${unita.value})`];
|
||||
|
||||
const option: EChartsOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#ffffff'
|
||||
},
|
||||
formatter: function (params: any) {
|
||||
if (!params || params.length === 0) return '';
|
||||
|
||||
let res = `${params[0].name} <br/>`;
|
||||
|
||||
for (const item of params) {
|
||||
const seriesName = item.seriesName ?? '';
|
||||
let regx = /\(([^()]+?)\)/;
|
||||
let unit = seriesName.match(regx);
|
||||
const finalValue = item.value !== undefined && item.value !== null ? item.value : '-';
|
||||
|
||||
if (item.value !== undefined && item.value !== null) {
|
||||
res += `<span style="background: ${item.color}; height:10px; width: 10px; border-radius: 50%;display: inline-block;margin-right:10px;"></span> ${item.seriesName} ${finalValue}${unit?.[1] || ''} <br/>`;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
// axisPointer 配置:鼠标经过时显示阴影块
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
shadowStyle: {
|
||||
color: 'rgba(84, 112, 198, 0.2)'
|
||||
}
|
||||
},
|
||||
color: Color,
|
||||
legend: {
|
||||
data: _legendData,
|
||||
top: 10,
|
||||
left: 'center',
|
||||
selectedMode: 'multiple'
|
||||
},
|
||||
grid: {
|
||||
top: 45,
|
||||
left: '10%',
|
||||
right: '10px',
|
||||
bottom: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
dataZoom: {
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: echartsData.value?.map((item: any) => item.dt) || [],
|
||||
boundaryGap: true,
|
||||
// X轴网格线(垂直方向)
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
color: '#e0e0e0'
|
||||
}
|
||||
},
|
||||
// X轴的axisPointer配置:鼠标经过时显示阴影块
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
shadowStyle: {
|
||||
color: 'rgba(84, 112, 198, 0.2)'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: _legendData[0],
|
||||
// 确保显示刻度和标签
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: '#333' // 刻度标签颜色
|
||||
},
|
||||
axisTick: {
|
||||
show: true
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#333' // Y轴轴线颜色,与刻度标签一致
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'solid' // 虚线改为实线
|
||||
}
|
||||
},
|
||||
splitNumber: 3
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: _legendData[0],
|
||||
data: echartsData.value?.map((item: any) => item.wt) || [],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
width: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 使用完全替换模式更新配置
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理窗口大小变化
|
||||
*/
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载模拟数据
|
||||
*/
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// 生成模拟数据
|
||||
echartsData.value = generateMockData();
|
||||
|
||||
// 数据加载完成后更新图表
|
||||
await nextTick();
|
||||
updateChart();
|
||||
} catch (error) {
|
||||
console.error('加载水温数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 生命周期钩子 ====================
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 双重等待确保DOM和样式计算完成
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// 校验容器尺寸
|
||||
if (chartRef.value) {
|
||||
const rect = chartRef.value.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
initChart();
|
||||
loadData();
|
||||
} else {
|
||||
console.warn('图表容器尺寸为0,启动重试机制');
|
||||
// 指数退避重试
|
||||
let retryCount = 0;
|
||||
const maxRetries = 5;
|
||||
const retryInit = () => {
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++;
|
||||
setTimeout(() => {
|
||||
const newRect = chartRef.value?.getBoundingClientRect();
|
||||
if (newRect && newRect.width > 0 && newRect.height > 0) {
|
||||
initChart();
|
||||
loadData();
|
||||
} else {
|
||||
retryInit();
|
||||
}
|
||||
}, 50 * Math.pow(2, retryCount));
|
||||
} else {
|
||||
console.error('图表容器初始化失败,已达到最大重试次数');
|
||||
}
|
||||
};
|
||||
retryInit();
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
// 移除事件监听
|
||||
window.removeEventListener('resize', handleResize);
|
||||
|
||||
// 销毁图表实例
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.water-temp-chart {
|
||||
width: 100%;
|
||||
height: 231px;
|
||||
min-height: 231px;
|
||||
}
|
||||
</style>
|
||||
339
frontend/src/modules/zengZhiJiHuaWanChengQingKuang/index.vue
Normal file
339
frontend/src/modules/zengZhiJiHuaWanChengQingKuang/index.vue
Normal file
@ -0,0 +1,339 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="增殖计划完成情况" :select="select" :datetimePicker="datetimePicker">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'zengZhiJiHuaWanChengQingKuang'
|
||||
});
|
||||
|
||||
// 图表容器引用
|
||||
const chartRef = ref<HTMLElement | null>(null);
|
||||
let chartInstance: ECharts | null = null;
|
||||
|
||||
// 选择器和日期选择器配置
|
||||
const select = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
options: [],
|
||||
picker: undefined,
|
||||
format: undefined
|
||||
});
|
||||
|
||||
const datetimePicker = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
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 unit = ref('万尾');
|
||||
|
||||
// 数值过滤函数(模拟 filterNumberByConfig)
|
||||
const filterNumberByConfig = (value: number, config: any) => {
|
||||
if (value < 0) return 0;
|
||||
return value;
|
||||
};
|
||||
|
||||
// 计算图表配置
|
||||
const chartOption = computed(() => {
|
||||
let wcval = wangCheng.value?.[0]?.unfinished || 0;
|
||||
|
||||
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 // 间距的宽度
|
||||
},
|
||||
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
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// 初始化图表
|
||||
const initChart = (retryCount = 0) => {
|
||||
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;
|
||||
}
|
||||
|
||||
// 销毁旧实例
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
|
||||
// 创建新实例
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
|
||||
// 设置图表配置
|
||||
chartInstance.setOption(chartOption.value);
|
||||
|
||||
console.log('图表初始化成功');
|
||||
};
|
||||
|
||||
// 更新图表数据
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return;
|
||||
|
||||
// 使用完全替换模式,防止状态残留
|
||||
chartInstance.setOption(chartOption.value, true);
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize();
|
||||
};
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 使用 nextTick + setTimeout 双重等待确保DOM和样式计算完成
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
// 初始化后再 resize 一次,确保尺寸正确
|
||||
setTimeout(() => {
|
||||
chartInstance?.resize();
|
||||
}, 50);
|
||||
}, 50);
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
min-height: 280px;
|
||||
}
|
||||
</style>
|
||||
286
frontend/src/modules/zengZhiZhanJieShaoMod/index.vue
Normal file
286
frontend/src/modules/zengZhiZhanJieShaoMod/index.vue
Normal file
@ -0,0 +1,286 @@
|
||||
<!-- 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>
|
||||
</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>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'zengZhiZhanJieShaoMod'
|
||||
});
|
||||
|
||||
// 媒体类型定义
|
||||
interface MediaItem {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
text: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// 原始媒体数据(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 renderMediaData = ref<MediaItem[]>([]);
|
||||
|
||||
// 当前显示索引(指向renderMediaData)
|
||||
const currentIndex = ref(1); // 从1开始,跳过克隆的首项
|
||||
|
||||
// 定时器引用
|
||||
let timer: any = null;
|
||||
|
||||
// 鼠标悬停状态
|
||||
const isHovering = ref(false);
|
||||
|
||||
// 是否正在切换动画中(用于禁用transition)
|
||||
const isTransitioning = ref(false);
|
||||
|
||||
// 初始化渲染数组(克隆首尾项)
|
||||
const initRenderData = () => {
|
||||
const length = originalMediaData.value.length;
|
||||
if (length === 0) return;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// 切换到下一张
|
||||
const nextSlide = () => {
|
||||
currentIndex.value++;
|
||||
|
||||
// 延迟检查是否需要无缝跳转
|
||||
setTimeout(() => {
|
||||
checkSeamlessJump();
|
||||
}, 500); // 与transition时间一致
|
||||
};
|
||||
|
||||
// 检查是否需要无缝跳转(克隆项处理)
|
||||
const checkSeamlessJump = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
|
||||
// 如果到达克隆的最后一项(索引 = realLength + 1)
|
||||
if (currentIndex.value >= realLength + 1) {
|
||||
// 1. 禁用过渡动画,实现瞬间跳转
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 2. 瞬间跳转到真实的第二项(索引1),用户看不到跳变
|
||||
currentIndex.value = 1;
|
||||
|
||||
// 3. 等待两帧后恢复过渡动画(确保DOM已更新)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isTransitioning.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入
|
||||
const handleMouseEnter = () => {
|
||||
isHovering.value = true;
|
||||
};
|
||||
|
||||
// 处理鼠标离开
|
||||
const handleMouseLeave = () => {
|
||||
isHovering.value = false;
|
||||
};
|
||||
|
||||
// 计算当前显示的描述文字
|
||||
const currentDescription = computed(() => {
|
||||
const realIndex = getCurrentRealIndex();
|
||||
return originalMediaData.value[realIndex]?.description || '';
|
||||
});
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
initRenderData();
|
||||
startAutoPlay();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
|
||||
// 获取当前真实的索引(排除克隆项的影响)
|
||||
const getCurrentRealIndex = () => {
|
||||
const realLength = originalMediaData.value.length;
|
||||
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
||||
|
||||
// 处理边界情况(无缝循环时的克隆项)
|
||||
if (realIndex < 0) realIndex = realLength - 1;
|
||||
if (realIndex >= realLength) realIndex = 0;
|
||||
|
||||
return realIndex;
|
||||
};
|
||||
|
||||
// 跳转到指定幻灯片
|
||||
const goToSlide = (targetIndex: number) => {
|
||||
if (isTransitioning.value) return;
|
||||
|
||||
// 计算目标索引(考虑克隆项偏移)
|
||||
currentIndex.value = targetIndex + 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
// border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden; // 隐藏超出容器的内容
|
||||
|
||||
// 跑马灯轨道
|
||||
.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; // 防止被压缩
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
// 多行文本溢出省略
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
128
frontend/src/modules/zengzhizhanjiansheyunxing/index.vue
Normal file
128
frontend/src/modules/zengzhizhanjiansheyunxing/index.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<!-- 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>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'zengzhizhanjiansheyunxing'
|
||||
});
|
||||
const datetimePicker = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
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'
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
// 延迟初始化,确保容器已渲染
|
||||
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
|
||||
});
|
||||
</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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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%;
|
||||
|
||||
.anticon {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.facility-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.facility-name {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
// margin-bottom: 4px;
|
||||
// font-weight: 500;
|
||||
}
|
||||
|
||||
.facility-count {
|
||||
font-size: 18px;
|
||||
color: #2f6b98;
|
||||
// font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
214
frontend/src/modules/zengzhizhanyunxingsjtj/index.vue
Normal file
214
frontend/src/modules/zengzhizhanyunxingsjtj/index.vue
Normal file
@ -0,0 +1,214 @@
|
||||
<!-- SidePanelItem.vue -->
|
||||
<template>
|
||||
<SidePanelItem title="增殖站运行数据统计" :datetimePicker="datetimePicker">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
// 定义组件名(便于调试和递归)
|
||||
defineOptions({
|
||||
name: 'ZengZhiZhanYunXingSJ'
|
||||
});
|
||||
|
||||
const datetimePicker = ref({
|
||||
show: true,
|
||||
value: undefined,
|
||||
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 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)
|
||||
};
|
||||
};
|
||||
|
||||
// 单位转换(简化版)
|
||||
const transUnit = (value: number, type: string, unit: string) => {
|
||||
// 这里可以根据实际需求实现单位转换逻辑
|
||||
return value;
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
|
||||
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 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: mockData.map(el => el.hbrvcdName),
|
||||
},
|
||||
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'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
// 页面加载时执行
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 延时50ms确保容器尺寸已完全渲染
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize();
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
min-height: 300px;
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
|
||||
import RightDrawer from "@/components/RightDrawer/index.vue";
|
||||
import GuoYuSheShiJieShao from "@/modules/guoyusheshijieshao/index.vue"
|
||||
import GuoYuSheShiJianSheQingKuang from "@/modules/guoyusheshijiansheqingkuang/index.vue"
|
||||
import GuoYuJianCeTJ from "@/modules/guoyujiance/index.vue"
|
||||
import GYZLLB from "@/modules/GYZLLB/index.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>过鱼设施监测</h2>
|
||||
<div class="moduleContent">
|
||||
<div class="leftContent">
|
||||
<JidiSelectorMod />
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<!-- <GuoYuSheShiJieShao /> -->
|
||||
<GuoYuSheShiJianSheQingKuang />
|
||||
<GuoYuJianCeTJ />
|
||||
<GYZLLB title="过鱼总量" />
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
|
||||
import RightDrawer from "@/components/RightDrawer/index.vue";
|
||||
import QiXiDiBaoHuGongZuoKaiZhan from "@/modules/qixidibaohugongzuokaizhanQK/index.vue"
|
||||
// import QixidijchuXx from "@/modules/qixidijchuXx"
|
||||
// import QiXiDiShuiWenBianHua from "@/modules/qixidishuiwenbianhua"
|
||||
// import QiXiDiLiuLiangBianHua from "@/modules/qixidiliuliangbianhua"
|
||||
import QixidijchuXx from "@/modules/qixidijchuXx/index.vue"
|
||||
import QiXiDiShuiWenBianHua from "@/modules/qixidishuiwenbianhua/index.vue"
|
||||
import QiXiDiLiuLiangBianHua from "@/modules/qixidiliuliangbianhua/index.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -15,9 +15,9 @@ import QiXiDiBaoHuGongZuoKaiZhan from "@/modules/qixidibaohugongzuokaizhanQK/ind
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<QiXiDiBaoHuGongZuoKaiZhan />
|
||||
<!-- <QiXiDiShuiWenBianHua /> -->
|
||||
<!-- <QiXiDiLiuLiangBianHua /> -->
|
||||
<!-- <QixidijchuXx /> -->
|
||||
<QiXiDiShuiWenBianHua />
|
||||
<QiXiDiLiuLiangBianHua />
|
||||
<QixidijchuXx />
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
|
||||
import RightDrawer from "@/components/RightDrawer/index.vue";
|
||||
|
||||
import ZengZhiJiHuaWanChengQingKuang from "@/modules/zengZhiJiHuaWanChengQingKuang/index.vue"
|
||||
import ZengZhiZhanJieShaoMod from "@/modules/zengZhiZhanJieShaoMod/index.vue"
|
||||
import ZZZJSYXQKTT from "@/modules/zengzhizhanjiansheyunxing/index.vue"
|
||||
import ZZZYXSJTJ from "@/modules/zengzhizhanyunxingsjtj/index.vue"
|
||||
import FLZLLB from "@/modules/GYZLLB/index.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>增殖放流情况</h2>
|
||||
<div class="moduleContent">
|
||||
<div class="leftContent">
|
||||
<JidiSelectorMod />
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<ZZZJSYXQKTT />
|
||||
<ZengZhiZhanJieShaoMod />
|
||||
<ZengZhiJiHuaWanChengQingKuang />
|
||||
<FLZLLB title="放流总量" />
|
||||
<ZZZYXSJTJ />
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@ -1,5 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
|
||||
import RightDrawer from "@/components/RightDrawer/index.vue";
|
||||
import ZhiWuYuanJianSheJiJieRuQingKuangBar from "@/modules/ZhenXiZhiWuYuanMod/ZhiWuYuanJianSheJiJieRuQingKuangBar/index.vue"; // 植物园建设及接入情况
|
||||
import Dwjzqk from "@/modules/ZhenXiZhiWuYuanMod/Dwjzqk/index.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2>动植物保护</h2>
|
||||
<div class="moduleContent">
|
||||
<div class="leftContent">
|
||||
<JidiSelectorMod />
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<ZhiWuYuanJianSheJiJieRuQingKuangBar />
|
||||
<Dwjzqk />
|
||||
<!-- <JZZQKJS /> -->
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user