SourceTermAnalysisSystem_vue/src/pages/sjfx/two/index.vue
2026-07-22 17:33:00 +08:00

563 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang='ts'>
import { Download, Search } from '@element-plus/icons-vue';
import * as echarts from 'echarts';
import { ElMessage, type FormInstance, type FormRules, type UploadProps } from 'element-plus';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { getSampleGrade, getStationList, exportData } from '@/utils/axios/sjfx';
import { exportCharts } from '@/utils/index'; // 下载图表图片
const { t } = useI18n();
const sampleTypeList = ref<any>([
{ label: '颗粒物', value: 'P' },
{ label: '惰性气体', value: 'B' },
]);
const dataSourceList = ref<any>([
{ label: 'NDC自动处理', value: '1' },
{ label: 'NDC交互分析', value: '2' },
{ label: 'IDC自动处理', value: '3' },
{ label: 'IDC交互分析', value: '4' },
]);
const form = ref<any>({
// sampleType: 'P',
// station: 106,
// dataSource: '1',
// startDate: '2025-01-01',
// endDate: '2025-09-15',
// createTime: ['2025-01-01', '2025-09-15'],
});
const stationList = ref<any>([]);
function getStation() {
getStationList({ systemType: form.value.sampleType, dataSource: form.value.dataSource }, (res) => {
const list = res.result;
stationList.value = list.map((item: { stationCode: any; stationId: any }) => {
return {
label: item.stationCode,
value: item.stationId,
};
});
console.log(res, 'res');
});
}
const barData = ref<any>({
xAxisData: [],
seriesData: [
],
});
function handleSampleTypeChange() {
if (!form.value.dataSource) {
ElMessage.warning('请选择数据来源!!!');
return;
}
form.value.station = '';
getStation();
}
const formRef = ref<FormInstance>();
const rules = {
sampleType: [{ required: true, message: '请选择样品类型', trigger: 'change' }],
station: [{ required: true, message: '请选择站点', trigger: 'change' }],
dataSource: [{ required: true, message: '请选择数据源', trigger: 'change' }],
createTime: [{
required: true, message: '请选择创建时间范围', trigger: 'change', validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请选择创建时间范围'));
}
else {
callback();
}
}
}],
};
function setTime(val: any[]) {
form.value.startDate = val[0];
form.value.endDate = val[1];
console.log(val);
}
function handleSearch() {
getList();
}
function getList() {
const formEl = formRef.value;
if (!formEl)
return;
formEl.validate((valid, fields) => {
if (valid) {
form.value.stationIds = [form.value.station]
getSampleGrade(form.value, (res) => {
console.log(res, 'res');
const list = res.result?.sampleDataList;
if (!list || list.length === 0) {
barData.value = {
xAxisData: [],
seriesData: [],
};
initCharts();
ElMessage.warning('当前未查询到可用数据');
return;
}
// 固定等级颜色映射
const levelColorMap = {
1: '#67C23A',
2: '#409EFF',
3: '#ff9933',
4: '#ff0000',
5: '#909399',
default: '#c2c2c2',
};
// 1. 提取所有唯一的时间点(包含 category 为 null 的时间)
// 确保 X 轴的时间顺序是完整的
const allTimes = Array.from(new Set(list.map((item: { collectStop: any }) => item.collectStop)))
.sort((a, b) => new Date(a) - new Date(b));
// 2. 提取所有【有效】的等级(过滤掉 null用于图例
const validCategories = Array.from(new Set(
list.filter((item: { category: null | undefined }) => item.category !== null && item.category !== undefined)
.map((item: { category: any }) => item.category),
)).sort((a, b) => a - b);
// 3. 按时间对数据进行分组
const groupMap = new Map();
list.forEach((item: { collectStop: any }) => {
if (!groupMap.has(item.collectStop))
groupMap.set(item.collectStop, []);
groupMap.get(item.collectStop).push(item);
});
// 4. 为每个有效等级构建 Series
const seriesList: any[] = [];
validCategories.forEach((cat) => {
// 计算这个等级在单个时间点下出现的最大次数(处理同等级并排)
let maxCount = 0;
groupMap.forEach((samples) => {
const count = samples.filter(s => s.category === cat).length;
if (count > maxCount)
maxCount = count;
});
// 为该等级创建 N 个同名系列
for (let i = 0; i < maxCount; i++) {
seriesList.push({
name: `等级 ${cat}`,
type: 'bar',
barGap: '15%',
itemStyle: { color: levelColorMap[cat] || levelColorMap.default },
emphasis: { focus: 'series' },
// 遍历 X 轴所有时间点,确保时间轴完整
data: allTimes.map((time) => {
const matched = groupMap.get(time).filter(s => s.category === cat);
if (matched[i]) {
return {
value: matched[i].category,
sampleId: matched[i].sampleId,
};
}
return null; // 这里返回 null柱子不显示但占位和时间轴保留
}),
});
}
});
barData.value.xAxisData = allTimes;
barData.value.seriesData = seriesList;
barData.value.legendData = validCategories.map(cat => `等级 ${cat}`);
console.log('处理后的数据:', barData.value);
renderChart();
});
}
// else {
// console.log('表单验证失败:', fields);
// ElMessage.error('搜索信息未填写');
// }
});
}
function renderChart() {
if (!barData.value || !barData.value.seriesData || barData.value.seriesData.length === 0) {
console.warn('数据异常');
return;
}
const option = {
tooltip: {
trigger: 'axis', backgroundColor: '#03aa7f', borderColor: '#03aa7f', // 边框颜色
borderWidth: 1, // 边框宽度
borderRadius: 4, // 圆角
textStyle: {
color: '#fff', // 文字颜色
fontSize: 12,
}, formatter(params: any[]) {
let res = `<b>时间: ${params[0].name}</b><br/>`;
params.forEach((p) => {
if (p.data && p.data.sampleId) {
res += `${p.marker} ID: ${p.data.sampleId} | 等级: <b>${p.data.value}</b><br/>`;
}
});
return res;
}
},
legend: {
data: barData.value.legendData,
orient: 'horizontal',
top: 'top',
type: 'plain',
textStyle: { color: '#9ab1bc' },
icon: 'circle',
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
containLabel: true,
},
xAxis: {
type: 'category',
name: '时间',
nameLocation: 'center',
nameTextStyle: {
padding: 40,
color: '#9ab1bc',
},
axisLabel: {
color: '#9ab1bc',
rotate: 45,
fontSize: 10, // 减小字体大小
margin: 2, // 减小边距
formatter: (val: string) => val.split(' ')[0], // 轴上仅显示日期
},
axisTick: {
show: true,
alignWithLabel: true, // 刻度线是否与标签对齐
length: 10,
},
axisLine: {
lineStyle: {
color: '#406979',
width: 1,
type: 'solid',
},
},
data: barData.value.xAxisData || [],
},
yAxis: {
type: 'value',
name: '级别',
nameLocation: 'center',
minInterval: 1,
nameTextStyle: { color: '#9ab1bc' },
axisLabel: { color: '#9ab1bc' },
splitLine: {
lineStyle: {
color: '#406979',
width: 1,
type: 'solid',
},
},
},
dataZoom: [
{ type: 'slider', start: 0, end: 100 },
{ type: 'inside' },
],
series: barData.value.seriesData,
};
barChart.setOption(option);
}
// 柱状图相关
const barRef = ref<HTMLDivElement | null>(null);
let barChart: echarts.ECharts | null = null;
// 初始化图表
function initCharts() {
// 先销毁已存在的图表实例,防止内存泄漏
if (barChart) {
barChart.dispose();
}
nextTick(() => {
// 初始化柱状图
if (barRef.value) {
barChart = echarts.init(barRef.value);
barChart.setOption({
// tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
// legend: { type: 'plain', top: 0, textStyle: { color: '#9ab1bc' }, icon: 'circle' },
// grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
// xAxis: {
// type: 'category',
// nameTextStyle: { padding: 10, color: '#9ab1bc' },
// axisLabel: { color: '#9ab1bc' },
// name: '时间',
// nameLocation: 'center',
// axisLine: {
// lineStyle: {
// color: '#406979', // 线条颜色
// width: 1, // 线条宽度
// type: 'solid', // 线条类型: solid, dashed, dotted
// },
// },
// data: barData.value.xAxisData,
// },
// yAxis: {
// type: 'value',
// name: '级别',
// nameLocation: 'center',
// nameTextStyle: { color: '#9ab1bc' },
// axisLabel: { color: '#9ab1bc' },
// min: 0, // 最小值
// max: 5, // 最大值
// interval: 1, // 刻度间隔
// splitLine: {
// lineStyle: {
// color: '#406979', // 网格线颜色
// width: 1, // 网格线宽度
// type: 'solid', // 线条类型: solid, dashed, dotted
// },
// },
// },
// series: [
// {
// name: '',
// type: 'bar',
// itemStyle: {
// color: '#1569c7',
// borderWidth: 1,
// },
// // barWidth: '40%',
// // barGap: '90%',
// // barCategoryGap: '20%',
// data: [],
// },
// ],
});
}
});
}
const exportChartsData = async () => {
try {
console.log('开始下载...');
form.value.type = 'sampleLevelTime'
const { filename, blob } = await exportData(form.value);
// const filename = '模版'
console.log('文件信息:', {
filename,
blob,
blobType: blob.type,
blobSize: `${blob.size} bytes`,
});
// 覆盖 Blob 类型为 Excel 标准 MIME 类型(避免后端返回类型错误)
const excelBlob = new Blob([blob], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' // xlsx 标准类型
// 如果是 xls 格式,用:'application/vnd.ms-excel'
});
// 验证 Blob 是否有效
if (!blob || blob.size === 0) {
throw new Error('获取到的文件为空');
}
// 创建下载链接
const url = window.URL.createObjectURL(excelBlob);
const link = document.createElement('a');
link.href = url;
link.download = filename; // 使用从响应头获取的文件名
link.style.display = 'none';
document.body.appendChild(link);
link.click();
console.log('文件下载成功:', filename);
ElMessage.success('导出成功');
}
catch (error) {
console.error('文件下载失败:', error);
}
finally {
}
}
onMounted(async () => {
initCharts();
window.addEventListener('resize', () => {
barChart?.resize();
});
});
</script>
<template>
<div class="sjfx-two-content">
<div class="head">
<el-form ref="formRef" :model="form" label-width="80px" class="queryForm" :rules="rules">
<el-row :gutter="10">
<el-col :span="4">
<el-form-item :label="t('dataSource')" prop="dataSource">
<el-select v-model="form.dataSource" :placeholder="t('pleaseSelect')" style="width: 300px;">
<el-option v-for="item in dataSourceList" :key="item.value"
style="background-color: transparent; height: 30px;color: #fff;" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item :label="t('sampleType')" prop="sampleType">
<el-select v-model="form.sampleType" :placeholder="t('pleaseSelect')" style="width: 100%;"
popper-style="background-color: transparent;border: solid 1px #0da397;width:100px;overflow: hidden;border-radius: 0;"
@change="handleSampleTypeChange">
<el-option v-for="item in sampleTypeList" :key="item.value"
style="background-color: transparent; height: 30px;color: #fff;" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item :label="t('stationName')" prop="station">
<el-select-v2 v-model="form.station" :placeholder="t('pleaseSelect')" style="width: 160px;" filterable
:options="stationList" clearable />
</el-form-item>
</el-col>
<!-- <el-col :span="3">
<el-form-item :label="t('nuclideName')">
<el-input v-model="form.nuclideName" style="width: 100%;" />
</el-form-item>
</el-col> -->
<el-col :span="7">
<el-form-item :label="t('createTime')" prop="createTime">
<el-date-picker v-model="form.createTime" type="daterange" :default-time="new Date()" style="width: 100%;"
range-separator="--" :start-placeholder="t('startDate')" :end-placeholder="t('endDate')"
value-format="YYYY-MM-DD" @change="setTime" />
</el-form-item>
</el-col>
<el-col :span="2">
<el-form-item>
<el-button :icon="Search" @click="handleSearch">
{{ t('refresh') }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div class="echars">
<div class="echars-head">
<h3>{{ t('SampleGradeTimeSeriesAnalysis') }}</h3>
<div style="flex: 1; text-align: right;">
<el-button :icon="Download" @click="exportChartsData">
{{ t('exportData') }}
</el-button>
<el-button :icon="Download" @click="exportCharts(barChart, '样品等级时序分析')">
{{ t('download') }}
</el-button>
</div>
</div>
<div class="chart-card">
<div ref="barRef" class="chart-container" />
</div>
</div>
</div>
</template>
<style scoped lang='less'>
.head {
width: 100%;
height: 50px;
line-height: 50px;
display: flex;
justify-content: space-between;
margin: 10px 0 0 0;
padding-top: 5px;
.queryForm {
display: flex;
align-items: center;
:deep(.el-form) {
padding-top: 20px;
}
:deep(.el-form-item__content) {
&:nth-last-child(1) {
margin-left: 0 !important;
}
}
:deep(.el-range-separator) {
color: #fff;
}
:deep(.el-select__wrapper),
:deep(.el-date-editor) {
background-color: rgba(3, 53, 63, 0.51) !important;
border-radius: 0%;
border: solid 1px #0b8c82 !important;
}
}
:deep(.el-button) {
width: 90px;
height: 32px;
margin: 0 5px;
background-color: #1397a3;
font-size: 16px;
font-weight: normal;
color: #ffffff;
border-radius: 0;
border: 1px solid #1397a3;
}
}
.echars-head {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
height: 42px;
border-top: 0.5px solid #0cebc9;
margin-bottom: 6px;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 8px;
width: 100%;
transform: scaleY(0.5);
transform-origin: bottom;
background: rgba(12, 235, 201, 0.2);
}
h3 {
height: 40px;
padding-right: 20px;
font-size: 18px;
font-weight: bold;
font-stretch: normal;
line-height: 40px;
letter-spacing: 1px;
color: #0cebc9;
background: rgba(12, 235, 201, 0.05);
margin-left: 18px;
}
:deep(.el-button) {
width: 85px;
height: 26px;
background-color: #097c42;
color: #fff;
border: none;
border-radius: 0;
}
}
.chart-card {
background-color: transparent;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.chart-container {
width: 100%;
height: 800px;
}
}
</style>