|
@@ -3,62 +3,148 @@
|
|
|
<!-- 上半部分:两个卡片 -->
|
|
|
<div class="top-section">
|
|
|
<!-- 本月 -->
|
|
|
- <div class="card">
|
|
|
- <h3>本月</h3>
|
|
|
+ <div class="card month-card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h3>本月能耗</h3>
|
|
|
+ <span class="date-info">{{ currentMonth }}</span>
|
|
|
+ </div>
|
|
|
<div class="data-container">
|
|
|
<div class="data-row">
|
|
|
<div class="data-item">
|
|
|
- <span class="value">8.405</span><span class="unit">t</span>
|
|
|
+ <span class="value">{{ monthData.current }}</span><span class="unit">t</span>
|
|
|
<span class="label">当月</span>
|
|
|
</div>
|
|
|
<div class="data-item">
|
|
|
- <span class="value">8.477</span><span class="unit">t</span>
|
|
|
+ <span class="value">{{ monthData.lastMonth }}</span><span class="unit">t</span>
|
|
|
<span class="label">上月同期</span>
|
|
|
</div>
|
|
|
<div class="data-item trend">
|
|
|
- <span class="value" style="color: #00c853;">-0.072</span><span class="unit">↓</span>
|
|
|
- <span class="label">趋势</span>
|
|
|
+ <span class="value" :style="{ color: monthTrend.color }">
|
|
|
+ {{ monthTrend.value }}
|
|
|
+ </span>
|
|
|
+ <span class="unit">{{ monthTrend.icon }}</span>
|
|
|
+ <span class="label">{{ monthTrend.percentage }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="progress-bar">
|
|
|
+ <div class="progress-fill" :style="{ width: monthProgress + '%' }"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 本年 -->
|
|
|
- <div class="card">
|
|
|
- <h3>本年</h3>
|
|
|
+ <div class="card year-card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h3>本年能耗</h3>
|
|
|
+ <span class="date-info">{{ currentYear }}年</span>
|
|
|
+ </div>
|
|
|
<div class="data-container">
|
|
|
<div class="data-row">
|
|
|
<div class="data-item">
|
|
|
- <span class="value">144.747</span><span class="unit">t</span>
|
|
|
+ <span class="value">{{ yearData.current }}</span><span class="unit">t</span>
|
|
|
<span class="label">当年</span>
|
|
|
</div>
|
|
|
<div class="data-item">
|
|
|
- <span class="value">123.812</span><span class="unit">t</span>
|
|
|
+ <span class="value">{{ yearData.lastYear }}</span><span class="unit">t</span>
|
|
|
<span class="label">去年同期</span>
|
|
|
</div>
|
|
|
<div class="data-item trend">
|
|
|
- <span class="value" style="color: #d50000;">20.935</span><span class="unit">↑</span>
|
|
|
- <span class="label">趋势</span>
|
|
|
+ <span class="value" :style="{ color: yearTrend.color }">
|
|
|
+ {{ yearTrend.value }}
|
|
|
+ </span>
|
|
|
+ <span class="unit">{{ yearTrend.icon }}</span>
|
|
|
+ <span class="label">{{ yearTrend.percentage }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="progress-bar">
|
|
|
+ <div class="progress-fill year" :style="{ width: yearProgress + '%' }"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 筛选区域 -->
|
|
|
- <div class="filter-container">
|
|
|
- <span>分类能耗</span>
|
|
|
- <select v-model="selectedCategory">
|
|
|
- <option value="电">电</option>
|
|
|
- <option value="水">水</option>
|
|
|
- <option value="天然气">天然气</option>
|
|
|
- </select>
|
|
|
- <input type="text" v-model="selectedYear" placeholder="输入年份" />
|
|
|
- <button @click="fetchData">查询</button>
|
|
|
+ <div class="filter-section">
|
|
|
+ <div class="filter-container">
|
|
|
+ <div class="filter-group">
|
|
|
+ <label>能源类型</label>
|
|
|
+ <select v-model="selectedCategory">
|
|
|
+ <option value="all">全部</option>
|
|
|
+ <option value="electricity">电力</option>
|
|
|
+ <option value="water">水</option>
|
|
|
+ <option value="gas">天然气</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="filter-group">
|
|
|
+ <label>年份</label>
|
|
|
+ <select v-model="selectedYear">
|
|
|
+ <option v-for="year in yearOptions" :key="year" :value="year">
|
|
|
+ {{ year }}年
|
|
|
+ </option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="filter-group">
|
|
|
+ <label>时间范围</label>
|
|
|
+ <select v-model="timeRange">
|
|
|
+ <option value="month">按月</option>
|
|
|
+ <option value="quarter">按季度</option>
|
|
|
+ <option value="year">按年</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button @click="handleQuery" class="query-btn" :disabled="loading">
|
|
|
+ <i class="icon-search"></i>
|
|
|
+ {{ loading ? '查询中...' : '查询' }}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button @click="exportData" class="export-btn" :disabled="!hasData">
|
|
|
+ <i class="icon-export"></i> 导出数据
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="summary-info">
|
|
|
+ <span class="info-item">
|
|
|
+ <i class="icon-info"></i>
|
|
|
+ 总能耗: <strong>{{ totalConsumption }}</strong> t
|
|
|
+ </span>
|
|
|
+ <span class="info-item">
|
|
|
+ 同比: <strong :class="yearOverYearClass">{{ yearOverYear }}</strong>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 图表区域 -->
|
|
|
- <div id="chart" ref="chartRef" style="width: 100%; height: 400px;"></div>
|
|
|
+ <div class="chart-section">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3>能耗趋势分析</h3>
|
|
|
+ <div class="chart-controls">
|
|
|
+ <button
|
|
|
+ v-for="type in chartTypes"
|
|
|
+ :key="type.value"
|
|
|
+ @click="switchChartType(type.value)"
|
|
|
+ :class="['chart-type-btn', { active: currentChartType === type.value }]"
|
|
|
+ >
|
|
|
+ {{ type.label }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 无数据提示 -->
|
|
|
+ <div v-if="!hasData" class="no-data">
|
|
|
+ <i class="icon-empty"></i>
|
|
|
+ <p>请选择查询条件并点击查询按钮</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图表 -->
|
|
|
+ <div v-show="hasData" ref="chartRef" class="chart-container"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 加载状态 -->
|
|
|
+ <div v-if="loading" class="loading-overlay">
|
|
|
+ <div class="spinner"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
@@ -68,62 +154,609 @@ import * as echarts from 'echarts';
|
|
|
export default {
|
|
|
data() {
|
|
|
return {
|
|
|
- selectedCategory: '电',
|
|
|
- selectedYear: '',
|
|
|
+ selectedCategory: 'all',
|
|
|
+ selectedYear: new Date().getFullYear(),
|
|
|
+ timeRange: 'month',
|
|
|
+ currentChartType: 'bar',
|
|
|
chartInstance: null,
|
|
|
+ loading: false,
|
|
|
+ hasData: false,
|
|
|
+ resizeObserver: null,
|
|
|
+
|
|
|
+ // 数据
|
|
|
+ monthData: {
|
|
|
+ current: 0,
|
|
|
+ lastMonth: 0
|
|
|
+ },
|
|
|
+ yearData: {
|
|
|
+ current: 0,
|
|
|
+ lastYear: 0
|
|
|
+ },
|
|
|
+
|
|
|
+ // 图表类型
|
|
|
+ chartTypes: [
|
|
|
+ { value: 'bar', label: '柱状图' },
|
|
|
+ { value: 'line', label: '折线图' },
|
|
|
+ { value: 'pie', label: '饼图' }
|
|
|
+ ],
|
|
|
+
|
|
|
+ // 模拟数据
|
|
|
+ chartData: {
|
|
|
+ month: [],
|
|
|
+ values: []
|
|
|
+ }
|
|
|
};
|
|
|
},
|
|
|
+
|
|
|
+ computed: {
|
|
|
+ currentMonth() {
|
|
|
+ const months = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
|
|
|
+ return months[new Date().getMonth()];
|
|
|
+ },
|
|
|
+
|
|
|
+ currentYear() {
|
|
|
+ return new Date().getFullYear();
|
|
|
+ },
|
|
|
+
|
|
|
+ yearOptions() {
|
|
|
+ const currentYear = new Date().getFullYear();
|
|
|
+ return Array.from({ length: 5 }, (_, i) => currentYear - i);
|
|
|
+ },
|
|
|
+
|
|
|
+ monthTrend() {
|
|
|
+ const diff = this.monthData.current - this.monthData.lastMonth;
|
|
|
+ const percentage = this.monthData.lastMonth > 0
|
|
|
+ ? ((Math.abs(diff) / this.monthData.lastMonth) * 100).toFixed(1)
|
|
|
+ : '0.0';
|
|
|
+ return {
|
|
|
+ value: Math.abs(diff).toFixed(3),
|
|
|
+ color: diff < 0 ? '#00c853' : '#ff5252',
|
|
|
+ icon: diff < 0 ? '↓' : '↑',
|
|
|
+ percentage: `${percentage}%`
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ yearTrend() {
|
|
|
+ const diff = this.yearData.current - this.yearData.lastYear;
|
|
|
+ const percentage = this.yearData.lastYear > 0
|
|
|
+ ? ((Math.abs(diff) / this.yearData.lastYear) * 100).toFixed(1)
|
|
|
+ : '0.0';
|
|
|
+ return {
|
|
|
+ value: Math.abs(diff).toFixed(3),
|
|
|
+ color: diff < 0 ? '#00c853' : '#ff5252',
|
|
|
+ icon: diff < 0 ? '↓' : '↑',
|
|
|
+ percentage: `${percentage}%`
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ monthProgress() {
|
|
|
+ const total = this.monthData.current + this.monthData.lastMonth;
|
|
|
+ return total > 0 ? (this.monthData.current / total) * 100 : 0;
|
|
|
+ },
|
|
|
+
|
|
|
+ yearProgress() {
|
|
|
+ const total = this.yearData.current + this.yearData.lastYear;
|
|
|
+ return total > 0 ? (this.yearData.current / total) * 100 : 0;
|
|
|
+ },
|
|
|
+
|
|
|
+ totalConsumption() {
|
|
|
+ return this.chartData.values.reduce((sum, val) => sum + val, 0).toFixed(2);
|
|
|
+ },
|
|
|
+
|
|
|
+ yearOverYear() {
|
|
|
+ if (this.yearData.lastYear === 0) return '0.0%';
|
|
|
+ const percentage = ((this.yearData.current - this.yearData.lastYear) / this.yearData.lastYear * 100).toFixed(1);
|
|
|
+ return percentage > 0 ? `+${percentage}%` : `${percentage}%`;
|
|
|
+ },
|
|
|
+
|
|
|
+ yearOverYearClass() {
|
|
|
+ return this.yearData.current > this.yearData.lastYear ? 'increase' : 'decrease';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
mounted() {
|
|
|
- this.initChart();
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initChart();
|
|
|
+ this.setupResizeObserver();
|
|
|
+ });
|
|
|
+ window.addEventListener('resize', this.handleResize);
|
|
|
},
|
|
|
+
|
|
|
+ beforeUnmount() {
|
|
|
+ window.removeEventListener('resize', this.handleResize);
|
|
|
+ if (this.resizeObserver) {
|
|
|
+ this.resizeObserver.disconnect();
|
|
|
+ }
|
|
|
+ if (this.chartInstance) {
|
|
|
+ this.chartInstance.dispose();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
methods: {
|
|
|
initChart() {
|
|
|
- const chartDom = this.$refs.chartRef;
|
|
|
- this.chartInstance = echarts.init(chartDom);
|
|
|
+ if (!this.$refs.chartRef) {
|
|
|
+ console.error('Chart container not found');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 如果已存在实例,先销毁
|
|
|
+ if (this.chartInstance) {
|
|
|
+ this.chartInstance.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ this.chartInstance = echarts.init(this.$refs.chartRef);
|
|
|
+ console.log('Chart initialized successfully');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to initialize chart:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设置ResizeObserver来监听容器尺寸变化
|
|
|
+ setupResizeObserver() {
|
|
|
+ if (!this.$refs.chartRef || !window.ResizeObserver) return;
|
|
|
+
|
|
|
+ this.resizeObserver = new ResizeObserver((entries) => {
|
|
|
+ for (let entry of entries) {
|
|
|
+ if (entry.target === this.$refs.chartRef && this.chartInstance) {
|
|
|
+ // 延迟执行resize,避免频繁调用
|
|
|
+ clearTimeout(this.resizeTimer);
|
|
|
+ this.resizeTimer = setTimeout(() => {
|
|
|
+ this.chartInstance.resize();
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.resizeObserver.observe(this.$refs.chartRef);
|
|
|
+ },
|
|
|
+
|
|
|
+ updateChart() {
|
|
|
+ if (!this.chartInstance || !this.hasData) {
|
|
|
+ console.log('Chart instance not ready or no data');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let option = {};
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (this.currentChartType === 'bar') {
|
|
|
+ option = this.getBarOption();
|
|
|
+ } else if (this.currentChartType === 'line') {
|
|
|
+ option = this.getLineOption();
|
|
|
+ } else if (this.currentChartType === 'pie') {
|
|
|
+ option = this.getPieOption();
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('Setting chart option:', option);
|
|
|
+ this.chartInstance.setOption(option, true);
|
|
|
+
|
|
|
+ // 使用requestAnimationFrame确保在下一帧渲染
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ if (this.chartInstance) {
|
|
|
+ this.chartInstance.resize();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to update chart:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 强制重新渲染图表 - 改进版本
|
|
|
+ forceResizeChart() {
|
|
|
+ if (!this.chartInstance) return;
|
|
|
+
|
|
|
+ // 使用requestAnimationFrame和多次尝试
|
|
|
+ const attemptResize = (attempts = 0) => {
|
|
|
+ if (attempts >= 5) return; // 最多尝试5次
|
|
|
+
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ if (this.chartInstance) {
|
|
|
+ try {
|
|
|
+ this.chartInstance.resize();
|
|
|
+ console.log(`Chart resize attempt ${attempts + 1} completed`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`Chart resize attempt ${attempts + 1} failed:`, error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果还有尝试次数,继续尝试
|
|
|
+ if (attempts < 4) {
|
|
|
+ setTimeout(() => attemptResize(attempts + 1), 100 * (attempts + 1));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ attemptResize();
|
|
|
+ },
|
|
|
+
|
|
|
+ getBarOption() {
|
|
|
+ return {
|
|
|
+ title: {
|
|
|
+ text: `${this.selectedYear}年${this.getCategoryLabel(this.selectedCategory)}能耗分布`,
|
|
|
+ left: 'center',
|
|
|
+ textStyle: { fontSize: 16, fontWeight: 'normal' }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow',
|
|
|
+ shadowStyle: {
|
|
|
+ color: 'rgba(150,150,150,0.1)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ borderColor: '#ddd',
|
|
|
+ borderWidth: 1,
|
|
|
+ padding: [10, 15],
|
|
|
+ textStyle: {
|
|
|
+ color: '#333',
|
|
|
+ fontSize: 14
|
|
|
+ },
|
|
|
+ formatter: function(params) {
|
|
|
+ const data = params[0];
|
|
|
+ return `
|
|
|
+ <div style="font-weight: 600; margin-bottom: 5px;">${data.name}</div>
|
|
|
+ <div style="display: flex; align-items: center;">
|
|
|
+ <span style="display: inline-block; width: 10px; height: 10px; background: ${data.color}; border-radius: 50%; margin-right: 8px;"></span>
|
|
|
+ <span>能耗:${data.value} t</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: this.chartData.month,
|
|
|
+ axisLabel: {
|
|
|
+ color: '#666',
|
|
|
+ rotate: this.chartData.month.length > 12 ? 45 : 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '能耗 (t)',
|
|
|
+ axisLabel: { color: '#666' }
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ name: '能耗',
|
|
|
+ type: 'bar',
|
|
|
+ data: this.chartData.values,
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ position: 'top',
|
|
|
+ color: '#666',
|
|
|
+ fontSize: 12,
|
|
|
+ formatter: '{c}'
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: '#2196f3' },
|
|
|
+ { offset: 1, color: '#1976d2' }
|
|
|
+ ]),
|
|
|
+ borderRadius: [4, 4, 0, 0]
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: '#42a5f5' },
|
|
|
+ { offset: 1, color: '#2196f3' }
|
|
|
+ ])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }],
|
|
|
+ grid: {
|
|
|
+ top: 60,
|
|
|
+ bottom: 60,
|
|
|
+ left: 60,
|
|
|
+ right: 40
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
|
|
|
- const option = {
|
|
|
- title: { text: '能耗分布', left: 'center' },
|
|
|
+ getLineOption() {
|
|
|
+ return {
|
|
|
+ title: {
|
|
|
+ text: `${this.selectedYear}年${this.getCategoryLabel(this.selectedCategory)}能耗趋势`,
|
|
|
+ left: 'center',
|
|
|
+ textStyle: { fontSize: 16, fontWeight: 'normal' }
|
|
|
+ },
|
|
|
tooltip: {
|
|
|
trigger: 'axis',
|
|
|
- axisPointer: { type: 'shadow' }
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ borderColor: '#ddd',
|
|
|
+ borderWidth: 1,
|
|
|
+ padding: [10, 15],
|
|
|
+ textStyle: {
|
|
|
+ color: '#333',
|
|
|
+ fontSize: 14
|
|
|
+ },
|
|
|
+ formatter: function(params) {
|
|
|
+ const data = params[0];
|
|
|
+ return `
|
|
|
+ <div style="font-weight: 600; margin-bottom: 5px;">${data.name}</div>
|
|
|
+ <div style="display: flex; align-items: center;">
|
|
|
+ <span style="display: inline-block; width: 10px; height: 10px; background: ${data.color}; border-radius: 50%; margin-right: 8px;"></span>
|
|
|
+ <span>能耗:${data.value} t</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
},
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
- data: ['1月', '2月', '3月', '4月', '5月'],
|
|
|
- axisLabel: { color: '#333' }
|
|
|
+ data: this.chartData.month,
|
|
|
+ boundaryGap: false,
|
|
|
+ axisLabel: { color: '#666' }
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: 'value',
|
|
|
- axisLabel: { color: '#333' }
|
|
|
+ name: '能耗 (t)',
|
|
|
+ axisLabel: { color: '#666' }
|
|
|
},
|
|
|
- series: [
|
|
|
- {
|
|
|
- name: '能耗',
|
|
|
- type: 'bar',
|
|
|
- data: [39, 30, 28, 22, 6],
|
|
|
- itemStyle: { color: '#2196f3' }
|
|
|
+ series: [{
|
|
|
+ name: '能耗',
|
|
|
+ type: 'line',
|
|
|
+ data: this.chartData.values,
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 8,
|
|
|
+ showSymbol: true,
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ color: '#666',
|
|
|
+ fontSize: 12,
|
|
|
+ formatter: '{c}'
|
|
|
+ },
|
|
|
+ lineStyle: {
|
|
|
+ color: '#2196f3',
|
|
|
+ width: 3
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: 'rgba(33, 150, 243, 0.3)' },
|
|
|
+ { offset: 1, color: 'rgba(33, 150, 243, 0.05)' }
|
|
|
+ ])
|
|
|
}
|
|
|
- ],
|
|
|
+ }],
|
|
|
grid: {
|
|
|
- top: 30,
|
|
|
- bottom: 30,
|
|
|
+ top: 60,
|
|
|
+ bottom: 60,
|
|
|
left: 60,
|
|
|
- right: 20
|
|
|
+ right: 40
|
|
|
}
|
|
|
};
|
|
|
+ },
|
|
|
+
|
|
|
+ getPieOption() {
|
|
|
+ const pieData = this.chartData.month.map((month, index) => ({
|
|
|
+ name: month,
|
|
|
+ value: this.chartData.values[index]
|
|
|
+ }));
|
|
|
+
|
|
|
+ return {
|
|
|
+ title: {
|
|
|
+ text: `${this.selectedYear}年${this.getCategoryLabel(this.selectedCategory)}能耗占比`,
|
|
|
+ left: 'center',
|
|
|
+ textStyle: { fontSize: 16, fontWeight: 'normal' }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ borderColor: '#ddd',
|
|
|
+ borderWidth: 1,
|
|
|
+ padding: [10, 15],
|
|
|
+ textStyle: {
|
|
|
+ color: '#333',
|
|
|
+ fontSize: 14
|
|
|
+ },
|
|
|
+ formatter: function(params) {
|
|
|
+ const total = pieData.reduce((sum, item) => sum + item.value, 0);
|
|
|
+ const percent = ((params.value / total) * 100).toFixed(1);
|
|
|
+ return `
|
|
|
+ <div style="font-weight: 600; margin-bottom: 5px;">${params.name}</div>
|
|
|
+ <div style="display: flex; align-items: center; margin-bottom: 3px;">
|
|
|
+ <span style="display: inline-block; width: 10px; height: 10px; background: ${params.color}; border-radius: 50%; margin-right: 8px;"></span>
|
|
|
+ <span>能耗:${params.value} t</span>
|
|
|
+ </div>
|
|
|
+ <div style="margin-left: 18px; color: #666;">
|
|
|
+ 占比:${percent}%
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ type: 'scroll',
|
|
|
+ bottom: 10,
|
|
|
+ data: this.chartData.month
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ name: '能耗',
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['40%', '70%'],
|
|
|
+ center: ['50%', '45%'],
|
|
|
+ data: pieData,
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ shadowBlur: 10,
|
|
|
+ shadowOffsetX: 0,
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ fontSize: 14,
|
|
|
+ fontWeight: 'bold'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ position: 'outside',
|
|
|
+ formatter: '{b}: {c}t',
|
|
|
+ fontSize: 12
|
|
|
+ },
|
|
|
+ labelLine: {
|
|
|
+ show: true,
|
|
|
+ length: 15,
|
|
|
+ length2: 10
|
|
|
+ }
|
|
|
+ }]
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ async handleQuery() {
|
|
|
+ await this.fetchData();
|
|
|
+ },
|
|
|
+
|
|
|
+ async fetchData() {
|
|
|
+ this.loading = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 模拟API调用
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 800));
|
|
|
+
|
|
|
+ // 根据选择更新数据
|
|
|
+ if (this.timeRange === 'month') {
|
|
|
+ this.chartData.month = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
|
|
|
+ this.chartData.values = this.generateRandomData(12);
|
|
|
+ } else if (this.timeRange === 'quarter') {
|
|
|
+ this.chartData.month = ['Q1', 'Q2', 'Q3', 'Q4'];
|
|
|
+ this.chartData.values = this.generateRandomData(4, 80, 120);
|
|
|
+ } else if (this.timeRange === 'year') {
|
|
|
+ this.chartData.month = this.yearOptions.slice(0, 5).reverse().map(y => `${y}年`);
|
|
|
+ this.chartData.values = this.generateRandomData(5, 100, 200);
|
|
|
+ }
|
|
|
|
|
|
- this.chartInstance.setOption(option);
|
|
|
+ // 更新月度和年度数据
|
|
|
+ this.updateSummaryData();
|
|
|
+
|
|
|
+ // 标记已有数据
|
|
|
+ this.hasData = true;
|
|
|
+
|
|
|
+ console.log('Data fetched:', this.chartData);
|
|
|
+
|
|
|
+ // 等待DOM更新
|
|
|
+ await this.$nextTick();
|
|
|
+
|
|
|
+ // 确保图表容器存在且可见
|
|
|
+ if (this.$refs.chartRef && this.hasData) {
|
|
|
+ // 如果图表实例不存在,重新初始化
|
|
|
+ if (!this.chartInstance) {
|
|
|
+ this.initChart();
|
|
|
+ await this.$nextTick(); // 等待初始化完成
|
|
|
+ }
|
|
|
+
|
|
|
+ // 延迟更新图表,确保容器完全显示
|
|
|
+ setTimeout(() => {
|
|
|
+ this.updateChart();
|
|
|
+ // 强制重新调整图表尺寸
|
|
|
+ setTimeout(() => {
|
|
|
+ this.forceResizeChart();
|
|
|
+ }, 200);
|
|
|
+ }, 200);
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('数据获取失败:', error);
|
|
|
+ if (this.$message) {
|
|
|
+ this.$message.error('数据加载失败,请重试');
|
|
|
+ } else {
|
|
|
+ alert('数据加载失败,请重试');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ this.loading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ generateRandomData(count, min = 20, max = 50) {
|
|
|
+ return Array.from({ length: count }, () =>
|
|
|
+ Math.floor(Math.random() * (max - min + 1)) + min
|
|
|
+ );
|
|
|
+ },
|
|
|
+
|
|
|
+ updateSummaryData() {
|
|
|
+ // 模拟更新汇总数据
|
|
|
+ const baseMonth = 8.5;
|
|
|
+ const baseYear = 140;
|
|
|
+
|
|
|
+ this.monthData.current = (baseMonth + Math.random() * 2 - 1).toFixed(3);
|
|
|
+ this.monthData.lastMonth = (baseMonth + Math.random() * 2 - 1).toFixed(3);
|
|
|
+
|
|
|
+ this.yearData.current = (baseYear + Math.random() * 20 - 10).toFixed(3);
|
|
|
+ this.yearData.lastYear = (baseYear + Math.random() * 20 - 10).toFixed(3);
|
|
|
+ },
|
|
|
+
|
|
|
+ switchChartType(type) {
|
|
|
+ this.currentChartType = type;
|
|
|
+ // 延迟更新图表,确保状态已更新
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.updateChart();
|
|
|
+ // 切换图表类型后也强制resize
|
|
|
+ setTimeout(() => {
|
|
|
+ this.forceResizeChart();
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ exportData() {
|
|
|
+ if (!this.hasData) {
|
|
|
+ if (this.$message) {
|
|
|
+ this.$message.warning('请先查询数据');
|
|
|
+ } else {
|
|
|
+ alert('请先查询数据');
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 准备导出数据
|
|
|
+ const data = this.chartData.month.map((month, index) => ({
|
|
|
+ 时间: month,
|
|
|
+ 能耗: this.chartData.values[index],
|
|
|
+ 单位: 't',
|
|
|
+ 类型: this.getCategoryLabel(this.selectedCategory)
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 转换为CSV
|
|
|
+ const csv = this.convertToCSV(data);
|
|
|
+
|
|
|
+ // 下载文件
|
|
|
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
|
+ const link = document.createElement('a');
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+
|
|
|
+ link.setAttribute('href', url);
|
|
|
+ link.setAttribute('download', `能耗数据_${this.selectedYear}年_${new Date().toLocaleDateString()}.csv`);
|
|
|
+ link.style.visibility = 'hidden';
|
|
|
+
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
},
|
|
|
- fetchData() {
|
|
|
- console.log('查询:', this.selectedCategory, this.selectedYear);
|
|
|
- // 模拟数据更新
|
|
|
- this.updateChart([40, 32, 29, 23, 7]);
|
|
|
+
|
|
|
+ convertToCSV(data) {
|
|
|
+ const headers = Object.keys(data[0]);
|
|
|
+ const csvHeaders = headers.join(',');
|
|
|
+ const csvRows = data.map(row =>
|
|
|
+ headers.map(header => `"${row[header]}"`).join(',')
|
|
|
+ );
|
|
|
+
|
|
|
+ return `\ufeff${csvHeaders}\n${csvRows.join('\n')}`;
|
|
|
+ },
|
|
|
+
|
|
|
+ getCategoryLabel(category) {
|
|
|
+ const labels = {
|
|
|
+ 'all': '全部',
|
|
|
+ 'electricity': '电力',
|
|
|
+ 'water': '水',
|
|
|
+ 'gas': '天然气'
|
|
|
+ };
|
|
|
+ return labels[category] || category;
|
|
|
},
|
|
|
- updateChart(data) {
|
|
|
+
|
|
|
+ handleResize() {
|
|
|
if (this.chartInstance) {
|
|
|
- this.chartInstance.setOption({
|
|
|
- series: [{ data: data }]
|
|
|
- });
|
|
|
+ // 延迟执行resize,确保容器尺寸已更新
|
|
|
+ clearTimeout(this.resizeTimer);
|
|
|
+ this.resizeTimer = setTimeout(() => {
|
|
|
+ this.chartInstance.resize();
|
|
|
+ }, 100);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -132,115 +765,505 @@ export default {
|
|
|
|
|
|
<style scoped>
|
|
|
.app {
|
|
|
- background-color: #fff;
|
|
|
+ background-color: #f5f7fa;
|
|
|
color: #333;
|
|
|
- font-family: "Segoe UI", sans-serif;
|
|
|
- padding: 30px;
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
|
+ padding: 24px;
|
|
|
+ min-height: 90vh;
|
|
|
}
|
|
|
|
|
|
-/* 上方两个卡片并列 */
|
|
|
+/* 上方卡片区域 */
|
|
|
.top-section {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 30px;
|
|
|
- margin-bottom: 30px;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 24px;
|
|
|
+ margin-bottom: 24px;
|
|
|
}
|
|
|
|
|
|
.card {
|
|
|
- flex: 1;
|
|
|
- background-color: #f8f9fa;
|
|
|
- border-radius: 10px;
|
|
|
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
|
- border: 1px solid #e0e0e0;
|
|
|
- height: 300px; /* 固定高度 */
|
|
|
- position: relative; /* 用于绝对定位标题 */
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ padding: 24px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.card:hover {
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 24px;
|
|
|
}
|
|
|
|
|
|
.card h3 {
|
|
|
- margin: 15px;
|
|
|
+ margin: 0;
|
|
|
font-size: 18px;
|
|
|
font-weight: 600;
|
|
|
- color: #212121;
|
|
|
- position: absolute;
|
|
|
- top: 15px;
|
|
|
- left: 15px; /* 标题固定在左上角 */
|
|
|
+ color: #1a1a1a;
|
|
|
+}
|
|
|
+
|
|
|
+.date-info {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+ background: #f0f2f5;
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 16px;
|
|
|
}
|
|
|
|
|
|
.data-container {
|
|
|
- width: 100%;
|
|
|
- display: flex;
|
|
|
- justify-content: center; /* 水平居中 */
|
|
|
- align-items: center; /* 垂直居中 */
|
|
|
- height: calc(100% - 40px); /* 减去标题的高度 */
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
.data-row {
|
|
|
display: flex;
|
|
|
- justify-content: space-around;
|
|
|
+ justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- width: 80%; /* 调整宽度以适应居中 */
|
|
|
}
|
|
|
|
|
|
.data-item {
|
|
|
text-align: center;
|
|
|
+ flex: 1;
|
|
|
}
|
|
|
|
|
|
.value {
|
|
|
- font-size: 24px;
|
|
|
- font-weight: bold;
|
|
|
- color: #000;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1a1a1a;
|
|
|
+ display: inline-block;
|
|
|
}
|
|
|
|
|
|
.unit {
|
|
|
font-size: 14px;
|
|
|
margin-left: 4px;
|
|
|
color: #666;
|
|
|
+ font-weight: normal;
|
|
|
}
|
|
|
|
|
|
.label {
|
|
|
- font-size: 12px;
|
|
|
+ font-size: 13px;
|
|
|
color: #999;
|
|
|
display: block;
|
|
|
- margin-top: 4px;
|
|
|
+ margin-top: 8px;
|
|
|
}
|
|
|
|
|
|
.trend .value {
|
|
|
- font-size: 20px;
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 进度条 */
|
|
|
+.progress-bar {
|
|
|
+ height: 6px;
|
|
|
+ background: #e8eaf0;
|
|
|
+ border-radius: 3px;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-fill {
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient(90deg, #2196f3 0%, #1976d2 100%);
|
|
|
+ border-radius: 3px;
|
|
|
+ transition: width 0.6s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-fill.year {
|
|
|
+ background: linear-gradient(90deg, #4caf50 0%, #388e3c 100%);
|
|
|
}
|
|
|
|
|
|
/* 筛选区域 */
|
|
|
+.filter-section {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px 24px;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
.filter-container {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 25px;
|
|
|
- gap: 15px;
|
|
|
+ gap: 20px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
-.filter-container span {
|
|
|
+.filter-group label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
font-weight: 500;
|
|
|
- min-width: 70px;
|
|
|
}
|
|
|
|
|
|
-.filter-container select,
|
|
|
-.filter-container input {
|
|
|
- padding: 8px 12px;
|
|
|
- border-radius: 6px;
|
|
|
- border: 1px solid #ccc;
|
|
|
+.filter-group select {
|
|
|
+ padding: 8px 32px 8px 12px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ background: white;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ appearance: none;
|
|
|
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg width='14' height='8' viewBox='0 0 14 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M1 1L7 7L13 1' stroke='%23909399' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e");
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-position: right 12px center;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-group select:hover {
|
|
|
+ border-color: #2196f3;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-group select:focus {
|
|
|
outline: none;
|
|
|
+ border-color: #2196f3;
|
|
|
+ box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+/* 查询按钮 */
|
|
|
+.query-btn {
|
|
|
+ padding: 8px 24px;
|
|
|
+ background: #2196f3;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.query-btn:hover:not(:disabled) {
|
|
|
+ background: #1976d2;
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.query-btn:disabled {
|
|
|
+ background: #b0bec5;
|
|
|
+ cursor: not-allowed;
|
|
|
+ transform: none;
|
|
|
}
|
|
|
|
|
|
-.filter-container button {
|
|
|
- padding: 8px 16px;
|
|
|
- background-color: #2196f3;
|
|
|
+.icon-search::before {
|
|
|
+ content: "🔍";
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.export-btn {
|
|
|
+ margin-left: auto;
|
|
|
+ padding: 8px 20px;
|
|
|
+ background: #4caf50;
|
|
|
color: white;
|
|
|
border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.export-btn:hover:not(:disabled) {
|
|
|
+ background: #388e3c;
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.export-btn:disabled {
|
|
|
+ background: #b0bec5;
|
|
|
+ cursor: not-allowed;
|
|
|
+ transform: none;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-export::before {
|
|
|
+ content: "⬇";
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-info {
|
|
|
+ margin-top: 16px;
|
|
|
+ padding-top: 16px;
|
|
|
+ border-top: 1px solid #e8eaf0;
|
|
|
+ display: flex;
|
|
|
+ gap: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item strong {
|
|
|
+ color: #1a1a1a;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item .increase {
|
|
|
+ color: #ff5252;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item .decrease {
|
|
|
+ color: #00c853;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-info::before {
|
|
|
+ content: "ℹ";
|
|
|
+ font-size: 16px;
|
|
|
+ color: #2196f3;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图表区域 */
|
|
|
+.chart-section {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 24px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ min-height: 500px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-header h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1a1a1a;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-controls {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-type-btn {
|
|
|
+ padding: 6px 16px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ background: white;
|
|
|
border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
cursor: pointer;
|
|
|
- transition: background-color 0.3s ease;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-type-btn:hover {
|
|
|
+ border-color: #2196f3;
|
|
|
+ color: #2196f3;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-type-btn.active {
|
|
|
+ background: #2196f3;
|
|
|
+ color: white;
|
|
|
+ border-color: #2196f3;
|
|
|
+}
|
|
|
+
|
|
|
+/* 无数据提示 */
|
|
|
+.no-data {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 400px;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.no-data .icon-empty::before {
|
|
|
+ content: "📊";
|
|
|
+ font-size: 64px;
|
|
|
+ opacity: 0.3;
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.no-data p {
|
|
|
+ font-size: 16px;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图表容器样式 - 关键修复 */
|
|
|
+.chart-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 400px;
|
|
|
+ min-height: 400px;
|
|
|
+ position: relative;
|
|
|
+ background: transparent;
|
|
|
+ /* 确保容器有明确的尺寸 */
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保图表在显示时有正确的尺寸 */
|
|
|
+.chart-container > div {
|
|
|
+ width: 100% !important;
|
|
|
+ height: 100% !important;
|
|
|
+ position: absolute !important;
|
|
|
+ top: 0 !important;
|
|
|
+ left: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图表容器可见性控制 */
|
|
|
+.chart-container[style*="display: none"] {
|
|
|
+ display: block !important;
|
|
|
+ visibility: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container:not([style*="display: none"]) {
|
|
|
+ visibility: visible;
|
|
|
+}
|
|
|
+
|
|
|
+/* 加载状态 */
|
|
|
+.loading-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.spinner {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border: 3px solid #f3f3f3;
|
|
|
+ border-top: 3px solid #2196f3;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .top-section {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-container {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-group {
|
|
|
+ width: 100%;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+
|
|
|
+ .query-btn,
|
|
|
+ .export-btn {
|
|
|
+ margin-left: 0;
|
|
|
+ width: 100%;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-header {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .data-item .value {
|
|
|
+ font-size: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container {
|
|
|
+ height: 300px;
|
|
|
+ min-height: 300px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 动画效果 */
|
|
|
+@keyframes fadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(10px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.card {
|
|
|
+ animation: fadeIn 0.5s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.card:nth-child(2) {
|
|
|
+ animation-delay: 0.1s;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-section {
|
|
|
+ animation: fadeIn 0.5s ease-out 0.2s both;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-section {
|
|
|
+ animation: fadeIn 0.5s ease-out 0.3s both;
|
|
|
+}
|
|
|
+
|
|
|
+/* 数据更新动画 */
|
|
|
+@keyframes pulse {
|
|
|
+ 0% {
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ transform: scale(1.05);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.value {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.value.updating {
|
|
|
+ animation: pulse 0.6s ease;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图表显示优化 */
|
|
|
+.chart-section[data-loading="false"] .chart-container {
|
|
|
+ opacity: 1;
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-section[data-loading="true"] .chart-container {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+/* 强制图表容器正确显示 */
|
|
|
+.chart-container {
|
|
|
+ display: block !important;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
-.filter-container button:hover {
|
|
|
- background-color: #1976d2;
|
|
|
+/* 确保ECharts容器正确渲染 */
|
|
|
+.chart-container canvas {
|
|
|
+ display: block !important;
|
|
|
}
|
|
|
</style>
|