页面模块增加

This commit is contained in:
王兴凯 2026-05-09 17:06:31 +08:00
commit abe7721236
18 changed files with 3540 additions and 21 deletions

View File

@ -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;
}
}
}

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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.0m14.0m,最大高度 29.85m37.00m。鱼道由诱鱼系统、入口、池室、休息池、观察室和出口等组成,其中诱鱼系统和入口布置于下游厂房尾水侧,出口布置于上游泄洪闸进口侧。鱼道采取连续“绕弯”方式布置,全长 597m平均坡降为 3.85%。沙坪二级鱼道位于泄洪闸与厂房之间,鱼道上下游方向长 224.42m,宽 10.0m14.0m,最大高度 29.85m37.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>

View 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>

View 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;
}
// Y1
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>

View 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-81-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'
}
},
// XaxisPointer
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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>