683 lines
20 KiB
Vue
683 lines
20 KiB
Vue
<template>
|
||
<div class="body_one">
|
||
<div class="search_one">
|
||
<div class="search_left">
|
||
<div>鱼类选择:</div>
|
||
<a-select v-model:value="selectValue" mode="multiple" style="width: 260px;" placeholder=" "
|
||
:options="selectOption" @change="handleChange"></a-select>
|
||
</div>
|
||
<div>
|
||
<a-range-picker v-model:value="rangePicker" format="YYYY-MM-DD" :disabled-date="disabledDate"
|
||
:allow-clear="false" @change="handleDateChange">
|
||
<template #renderExtraFooter>
|
||
<div class="quick-date-options">
|
||
<a-button size="small" @click="setQuickRange(7)">最近七天</a-button>
|
||
<a-button size="small" @click="setQuickRange(30)">最近一个月</a-button>
|
||
<a-button size="small" @click="setQuickRange(90)">最近三个月</a-button>
|
||
<a-button size="small" @click="setQuickRange(180)">最近六个月</a-button>
|
||
</div>
|
||
</template>
|
||
</a-range-picker>
|
||
</div>
|
||
</div>
|
||
<div class="body_body">
|
||
<div class="echarts" ref="chartRef"></div>
|
||
<div class="table">
|
||
<BasicTable ref="tableRef" :scrollY="460" :columns="columns" :list-url="fishGetKendoListCust"
|
||
:search-params="{}" :transform-data="customTransform">
|
||
<!-- 同期对比列的自定义渲染 -->
|
||
<template #contrast="{ record }">
|
||
<span v-if="record.wt && record.beforeWt"
|
||
:style="{ color: (record.wt - record.beforeWt) > 0 ? 'rgb(255, 85, 0)' : 'rgb(135, 208, 104)' }">
|
||
{{ (record.wt - record.beforeWt) > 0 ? '+' : '' }}{{ (record.wt -
|
||
record.beforeWt).toFixed(2) }}
|
||
</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</BasicTable>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, watch, onMounted, onUnmounted, computed } from "vue";
|
||
import * as echarts from 'echarts';
|
||
import dayjs, { Dayjs } from 'dayjs';
|
||
import { useModelStore } from "@/store/modules/model";
|
||
import { infoGetKendoListCust, fishGetKendoListCust } from "@/api/sw";
|
||
import BasicTable from "@/components/BasicTable/index.vue";
|
||
const modelStore = useModelStore();
|
||
const selectOption = ref([])
|
||
const selectValue = ref([])
|
||
const tableRef = ref<any>(null);
|
||
const chartRef = ref<HTMLDivElement | null>(null);
|
||
let chartInstance: echarts.ECharts | null = null;
|
||
const chartData = ref<any[]>([]);
|
||
const columns = [
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'dt',
|
||
width: 180,
|
||
customRender: ({ text }: any) => text ? dayjs(text).format('YYYY-MM-DD') : '-'
|
||
},
|
||
{
|
||
title: '水温°C',
|
||
dataIndex: 'wt',
|
||
customRender: ({ text }: any) => text !== undefined && text !== null ? text : '-'
|
||
},
|
||
{
|
||
title: '上一年水温',
|
||
dataIndex: 'beforeWt',
|
||
customRender: ({ text }: any) => text !== undefined && text !== null ? text : '-'
|
||
},
|
||
{
|
||
title: '同期对比',
|
||
dataIndex: 'contrast',
|
||
width: 120,
|
||
slots: { customRender: 'contrast' }
|
||
},
|
||
{
|
||
title: '适宜产卵鱼',
|
||
dataIndex: 'spawnFish',
|
||
width: 200,
|
||
customRender: ({ record }: any) => {
|
||
if (record.fishList && Array.isArray(record.fishList) && record.fishList.length > 0) {
|
||
return record.fishList.map(fish => fish.name).join('、');
|
||
}
|
||
return '-';
|
||
}
|
||
},
|
||
];
|
||
// 初始化日期范围:7天前到当天
|
||
const initDateRange = () => {
|
||
const endDate = dayjs();
|
||
const startDate = dayjs().subtract(7, 'day');
|
||
return [startDate, endDate] as [Dayjs, Dayjs];
|
||
};
|
||
|
||
const rangePicker = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||
// 禁用超过当前日期的选项
|
||
const disabledDate = (current: Dayjs) => {
|
||
// 不能选择今天之后的日期
|
||
return current && current.isAfter(dayjs(), 'day');
|
||
};
|
||
|
||
// 设置快捷日期范围
|
||
const setQuickRange = (days: number) => {
|
||
const endDate = dayjs();
|
||
const startDate = dayjs().subtract(days, 'day');
|
||
rangePicker.value = [startDate, endDate];
|
||
// 手动触发变更事件
|
||
handleDateChange(rangePicker.value, [
|
||
startDate.format('YYYY-MM-DD'),
|
||
endDate.format('YYYY-MM-DD')
|
||
]);
|
||
};
|
||
|
||
// 日期改变时的处理方法(内容待定)
|
||
const handleDateChange = (dates: any, dateStrings: string[]) => {
|
||
console.log('日期改变:', dates, dateStrings);
|
||
getEchartsData()
|
||
gerTableData()
|
||
// TODO: 在此处添加日期改变后的业务逻辑
|
||
};
|
||
// 下拉框数据
|
||
const getSelectOption = async () => {
|
||
//
|
||
// modelStore.params.date
|
||
let params = {
|
||
"filter": {
|
||
"logic": "and",
|
||
"filters": [
|
||
{
|
||
"field": "stcd",
|
||
"operator": "eq",
|
||
"dataType": "string",
|
||
"value": modelStore.params.stcd
|
||
}
|
||
]
|
||
}
|
||
}
|
||
let res = await infoGetKendoListCust(params)
|
||
let data = res?.data?.data || res?.data
|
||
console.log(res)
|
||
selectOption.value = data.map(item => {
|
||
return {
|
||
label: item.name,
|
||
value: item.id,
|
||
pretemp: item.pretemp,
|
||
}
|
||
})
|
||
// 根据数据长度智能选择ID:2个及以上取前2个,少于2个取第1个
|
||
selectValue.value = data.length >= 2 ? data.slice(0, 2).map(item => item.id) : (data.length > 0 ? [data[0].id] : [])
|
||
}
|
||
//echarts 数据获取
|
||
const getEchartsData = async () => {
|
||
const params = {
|
||
"filter": {
|
||
"logic": "and",
|
||
"filters": [
|
||
{
|
||
"field": "stcd",
|
||
"operator": "eq",
|
||
"dataType": "string",
|
||
"value": modelStore.params.stcd
|
||
},
|
||
{
|
||
"field": "startTime",
|
||
"operator": "gte",
|
||
"dataType": "date",
|
||
"value": rangePicker.value[0].format('YYYY-MM-DD') + " 00:00:00"
|
||
},
|
||
{
|
||
"field": "endTime",
|
||
"operator": "lte",
|
||
"dataType": "date",
|
||
"value": rangePicker.value[1].format('YYYY-MM-DD') + " 23:59:59"
|
||
}
|
||
]
|
||
},
|
||
"sort": [
|
||
{
|
||
"field": "dt",
|
||
"dir": "desc"
|
||
}
|
||
]
|
||
}
|
||
let res = await fishGetKendoListCust(params)
|
||
console.log('ECharts原始数据:', res)
|
||
|
||
// 存储原始数据到响应式变量
|
||
const rawData = res?.data?.data || res?.data?.records || []
|
||
chartData.value = rawData
|
||
|
||
// 延迟更新图表,确保DOM已渲染
|
||
setTimeout(() => {
|
||
updateChart()
|
||
}, 100)
|
||
}
|
||
//table 数据获取
|
||
const gerTableData = async () => {
|
||
const filter = {
|
||
logic: 'and',
|
||
"filters": [
|
||
{
|
||
"field": "stcd",
|
||
"operator": "eq",
|
||
"dataType": "string",
|
||
"value": modelStore.params.stcd
|
||
},
|
||
{
|
||
"field": "startTime",
|
||
"operator": "gte",
|
||
"dataType": "date",
|
||
"value": rangePicker.value[0].format('YYYY-MM-DD') + " 00:00:00"
|
||
},
|
||
{
|
||
"field": "endTime",
|
||
"operator": "lte",
|
||
"dataType": "date",
|
||
"value": rangePicker.value[1].format('YYYY-MM-DD') + " 23:59:59"
|
||
}
|
||
]
|
||
};
|
||
tableRef.value?.getList(filter);
|
||
}
|
||
|
||
const handleChange = (value: any) => {
|
||
console.log(value)
|
||
getEchartsData()
|
||
// gerTableData()
|
||
}
|
||
const customTransform = (res: any) => {
|
||
console.log('表格数据:', res);
|
||
return {
|
||
records: res?.data?.data || [],
|
||
total: res?.data?.total || 0
|
||
};
|
||
};
|
||
|
||
// 过滤和处理图表数据
|
||
const processChartData = (rawData: any[]) => {
|
||
if (!rawData || !Array.isArray(rawData)) return [];
|
||
|
||
// 过滤掉 wt 或 beforeWt 为 null 的记录
|
||
const filtered = rawData.filter(item =>
|
||
item.wt !== null && item.wt !== undefined
|
||
);
|
||
// && item.beforeWt !== null && item.beforeWt !== undefined
|
||
|
||
// 按 dt 升序排序
|
||
return filtered.sort((a, b) =>
|
||
new Date(a.dt).getTime() - new Date(b.dt).getTime()
|
||
);
|
||
};
|
||
|
||
// 计算 Y 轴最大值(向上取整到个位)
|
||
const calculateYAxisMax = (data: any[]) => {
|
||
if (!data || data.length === 0) return 10;
|
||
|
||
const allValues = [
|
||
...data.map(item => item.wt),
|
||
...data.map(item => item.beforeWt)
|
||
].filter(v => v !== null && v !== undefined);
|
||
|
||
const max = Math.max(...allValues);
|
||
return Math.ceil(max);
|
||
};
|
||
|
||
// 生成随机颜色(HSL转HEX)
|
||
const generateRandomColor = () => {
|
||
const hue = Math.random() * 360; // 色相:0-360°
|
||
const saturation = 40 + Math.random() * 30; // 饱和度:40%-70%
|
||
const lightness = 45 + Math.random() * 20; // 亮度:45%-65%
|
||
|
||
// HSL转RGB
|
||
const c = (1 - Math.abs(2 * lightness / 100 - 1)) * saturation / 100;
|
||
const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
|
||
const m = lightness / 100 - c / 2;
|
||
|
||
let r = 0, g = 0, b = 0;
|
||
|
||
if (hue < 60) { r = c; g = x; b = 0; }
|
||
else if (hue < 120) { r = x; g = c; b = 0; }
|
||
else if (hue < 180) { r = 0; g = c; b = x; }
|
||
else if (hue < 240) { r = 0; g = x; b = c; }
|
||
else if (hue < 300) { r = x; g = 0; b = c; }
|
||
else { r = c; g = 0; b = x; }
|
||
|
||
// RGB转HEX
|
||
const toHex = (value: number) => {
|
||
const hex = Math.round((value + m) * 255).toString(16);
|
||
return hex.length === 1 ? '0' + hex : hex;
|
||
};
|
||
|
||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||
};
|
||
|
||
// 计算选中鱼类的温度范围数组
|
||
const calculateTempRanges = () => {
|
||
if (!selectValue.value || selectValue.value.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
// 从 selectOption 中找到选中的鱼类
|
||
const selectedFish = selectOption.value.filter(fish =>
|
||
selectValue.value.includes(fish.value)
|
||
);
|
||
|
||
if (selectedFish.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
// 为每个鱼类生成温度范围和随机颜色
|
||
return selectedFish
|
||
.map(fish => {
|
||
if (!fish.pretemp || !Array.isArray(fish.pretemp) || fish.pretemp.length !== 2) {
|
||
return null;
|
||
}
|
||
|
||
const min = parseFloat(fish.pretemp[0]);
|
||
const max = parseFloat(fish.pretemp[1]);
|
||
|
||
if (isNaN(min) || isNaN(max)) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
name: fish.label,
|
||
min,
|
||
max,
|
||
color: generateRandomColor()
|
||
};
|
||
})
|
||
.filter(item => item !== null);
|
||
};
|
||
|
||
// 生成 ECharts 配置
|
||
const getChartOption = computed(() => {
|
||
const data = processChartData(chartData.value);
|
||
|
||
if (data.length === 0) {
|
||
return {};
|
||
}
|
||
|
||
// const yAxisMax = calculateYAxisMax(data);
|
||
const tempRanges = calculateTempRanges();
|
||
const dataMax = calculateYAxisMax(data);
|
||
const tempMax = tempRanges.length > 0
|
||
? Math.max(...tempRanges.map(r => r.max))
|
||
: 0;
|
||
const yAxisMax = Math.max(dataMax, tempMax);
|
||
// 提取时间轴和数据
|
||
const xAxisData = data.map(item => dayjs(item.dt).format('YYYY-MM-DD'));
|
||
const wtData = data.map(item => item.wt);
|
||
const beforeWtData = data.map(item => item.beforeWt);
|
||
|
||
// 构建 markArea 配置(为每个鱼类生成独立的背景区域)
|
||
const markAreaConfig = tempRanges.length > 0 ? {
|
||
silent: true,
|
||
data: tempRanges.map(range => [
|
||
{
|
||
yAxis: range.min,
|
||
name: `${range.name}: ${range.min}-${range.max}°C`,
|
||
itemStyle: { color: `${range.color}26` }
|
||
},
|
||
{
|
||
yAxis: range.max
|
||
}
|
||
])
|
||
} : undefined;
|
||
|
||
return {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||
textStyle: {
|
||
color: '#fff',
|
||
fontSize: 14
|
||
},
|
||
axisPointer: {
|
||
type: 'cross',
|
||
label: {
|
||
backgroundColor: '#6a7985'
|
||
}
|
||
},
|
||
formatter: (params: any) => {
|
||
if (!params || params.length === 0) return '';
|
||
|
||
const date = params[0].axisValue;
|
||
let html = `<div style="font-weight:bold;margin-bottom:8px;">${date}</div>`;
|
||
|
||
params.forEach((param: any) => {
|
||
const dataIndex = param.dataIndex;
|
||
const record = data[dataIndex];
|
||
const value = param.value != null ? param.value : '-';
|
||
|
||
html += `
|
||
<div style="display:flex;align-items:center;margin:4px 0;">
|
||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
|
||
<span>${param.seriesName}: ${value}°C</span>
|
||
</div>
|
||
`;
|
||
|
||
|
||
});
|
||
|
||
return html;
|
||
}
|
||
},
|
||
legend: {
|
||
top: 10,
|
||
left: 'center',
|
||
data: ['现在水温', '上一年同期水温'],
|
||
textStyle: {
|
||
fontSize: 13
|
||
}
|
||
},
|
||
grid: {
|
||
left: 60,
|
||
right: 40,
|
||
top: 60,
|
||
bottom: 50
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: xAxisData,
|
||
boundaryGap: false,
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#000000'
|
||
}
|
||
},
|
||
axisTick: {
|
||
show: false
|
||
},
|
||
axisLabel: {
|
||
interval: (() => {
|
||
const dataLen = xAxisData.length;
|
||
if (dataLen <= 8) return 0; // 数据少时全部显示
|
||
|
||
// 计算最大允许的间隔,确保首尾都能显示
|
||
// 需要显示的首尾2个 + 中间最多6个 = 总共8个标签
|
||
const maxLabels = 8;
|
||
const minInterval = Math.ceil((dataLen - 1) / (maxLabels - 1));
|
||
|
||
// 如果原有计算的间隔更大,使用原有逻辑;否则使用最小间隔
|
||
const calculatedInterval = Math.floor((dataLen - 1) / 7);
|
||
return Math.max(calculatedInterval, minInterval);
|
||
})(),
|
||
fontSize: 12,
|
||
rotate: 0,
|
||
margin: 10
|
||
},
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: '#bfbfbf',
|
||
type: 'solid'
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
name: '水温(°C)',
|
||
min: 0,
|
||
max: yAxisMax,
|
||
scale: false,
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#000000',
|
||
width: 1
|
||
}
|
||
},
|
||
axisTick: {
|
||
show: true,
|
||
length: 4
|
||
},
|
||
axisLabel: {
|
||
fontSize: 12,
|
||
formatter: '{value}'
|
||
},
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: '#bfbfbf',
|
||
type: 'solid'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '现在水温',
|
||
type: 'line',
|
||
data: wtData,
|
||
smooth: true,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
lineStyle: { color: '#009dff', width: 2 },
|
||
itemStyle: { color: '#009dff' },
|
||
connectNulls: false,
|
||
// 移除 markArea
|
||
},
|
||
{
|
||
name: '上一年同期水温',
|
||
type: 'line',
|
||
data: beforeWtData,
|
||
smooth: true,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
lineStyle: { color: '#fee588', width: 2 },
|
||
itemStyle: { color: '#fee588' },
|
||
connectNulls: false
|
||
},
|
||
// 新增辅助系列,专门承载 markArea
|
||
{
|
||
name: '温度区间背景', // 名字不会被放入 legend,所以不显示
|
||
type: 'line',
|
||
data: new Array(xAxisData.length).fill(0), // 假数据,保证坐标匹配
|
||
showSymbol: false,
|
||
symbol: 'none',
|
||
lineStyle: { opacity: 0 },
|
||
itemStyle: { opacity: 0 },
|
||
legendHoverLink: false, // 图例悬停时不联动
|
||
silent: true, // 不响应鼠标事件
|
||
markArea: markAreaConfig // 温度区间背景绑定于此
|
||
}
|
||
],
|
||
dataZoom: [
|
||
{
|
||
type: 'inside',
|
||
xAxisIndex: [0],
|
||
throttle: 50,
|
||
start: 0,
|
||
end: 100
|
||
}
|
||
],
|
||
toolbox: {
|
||
show: true,
|
||
feature: {
|
||
saveAsImage: {
|
||
title: '导出图片',
|
||
type: 'png',
|
||
pixelRatio: 2
|
||
}
|
||
},
|
||
right: 20,
|
||
top: 10
|
||
}
|
||
};
|
||
});
|
||
|
||
// 初始化图表
|
||
const initChart = () => {
|
||
if (!chartRef.value) return;
|
||
|
||
if (chartInstance) {
|
||
chartInstance.dispose();
|
||
}
|
||
|
||
chartInstance = echarts.init(chartRef.value);
|
||
updateChart();
|
||
|
||
// 监听窗口resize
|
||
window.addEventListener('resize', handleResize);
|
||
};
|
||
|
||
// 更新图表
|
||
const updateChart = () => {
|
||
if (!chartInstance) return;
|
||
|
||
const option = getChartOption.value;
|
||
if (Object.keys(option).length > 0) {
|
||
chartInstance.setOption(option, true);
|
||
} else {
|
||
// 如果没有数据,清空图表或显示空状态
|
||
chartInstance.clear();
|
||
}
|
||
};
|
||
|
||
// 处理窗口resize
|
||
const handleResize = () => {
|
||
if (chartInstance) {
|
||
chartInstance.resize();
|
||
}
|
||
};
|
||
|
||
// 销毁图表
|
||
const destroyChart = () => {
|
||
if (chartInstance) {
|
||
chartInstance.dispose();
|
||
chartInstance = null;
|
||
}
|
||
window.removeEventListener('resize', handleResize);
|
||
};
|
||
|
||
onMounted(() => {
|
||
getSelectOption()
|
||
getEchartsData()
|
||
gerTableData()
|
||
// 初始化图表
|
||
setTimeout(() => {
|
||
initChart()
|
||
}, 200)
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
destroyChart()
|
||
});
|
||
watch(
|
||
() => modelStore.params.stcd,
|
||
(newStcd, oldStcd) => {
|
||
// 避免初始化时的无效触发
|
||
if (newStcd && newStcd !== oldStcd) {
|
||
console.log('【BasicInfo】stcd 变化,重新加载数据:', newStcd);
|
||
getSelectOption()
|
||
getEchartsData()
|
||
gerTableData()
|
||
}
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.body_one {
|
||
width: 100%;
|
||
min-height: 600px;
|
||
|
||
.search_one {
|
||
width: 100%;
|
||
height: 32px;
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.search_left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.body_body {
|
||
width: 100%;
|
||
display: flex;
|
||
height: 540px;
|
||
|
||
.echarts {
|
||
flex: 1;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
padding-top: 10px;
|
||
}
|
||
|
||
.table {
|
||
flex: 1;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
padding: 10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.quick-date-options {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 8px 0;
|
||
|
||
.ant-btn {
|
||
border-color: #1890ff !important;
|
||
color: #1890ff !important;
|
||
background-color: #e6f7ff !important;
|
||
font-size: 12px;
|
||
|
||
&:hover {
|
||
border-color: #40a9ff !important;
|
||
color: #40a9ff !important;
|
||
background-color: #bae7ff !important;
|
||
}
|
||
}
|
||
}
|
||
</style>
|