563 lines
16 KiB
Vue
563 lines
16 KiB
Vue
<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>
|