|
|
@@ -56,21 +56,21 @@ import * as echarts from 'echarts';
|
|
|
import { api8000 } from '@/utils/request'; // 导入 api8000 实例
|
|
|
|
|
|
// 图表容器 & 实例
|
|
|
-const initialCdChart = ref(null); // 初始Cd图表
|
|
|
-const otherIndicatorsChart = ref(null); // 其他指标图表
|
|
|
-const chartInstanceInitial = ref(null); // 初始Cd实例
|
|
|
-const chartInstanceOther = ref(null); // 其他指标实例
|
|
|
+const initialCdChart = ref(null);
|
|
|
+const otherIndicatorsChart = ref(null);
|
|
|
+const chartInstanceInitial = ref(null);
|
|
|
+const chartInstanceOther = ref(null);
|
|
|
|
|
|
// 响应式状态
|
|
|
const isLoading = ref(true);
|
|
|
const error = ref(null);
|
|
|
const stats = ref({ samples: 0 });
|
|
|
|
|
|
-// 统计数据(拆分两组)
|
|
|
-const initialCdStats = ref([]); // 初始Cd统计
|
|
|
-const otherIndicatorsStats = ref([]); // 其他指标统计
|
|
|
+// 统计数据
|
|
|
+const initialCdStats = ref([]);
|
|
|
+const otherIndicatorsStats = ref([]);
|
|
|
|
|
|
-// 字段配置(拆分初始Cd和其他指标)
|
|
|
+// 字段配置
|
|
|
const fieldConfig = {
|
|
|
initialCd: [
|
|
|
{ key: 'Initial_Cd', name: '土壤初始Cd总量', color: '#5470c6' }
|
|
|
@@ -85,208 +85,239 @@ const fieldConfig = {
|
|
|
]
|
|
|
};
|
|
|
|
|
|
-// 数据请求
|
|
|
+// 数据请求 - 增强错误处理和调试
|
|
|
const fetchData = async () => {
|
|
|
try {
|
|
|
- const apiUrl = '/api/vector/stats/FluxCd_input_data'; // 相对路径
|
|
|
- const response = await api8000.get(apiUrl); // 使用 api8000
|
|
|
- const rawData = response.data.features
|
|
|
- ? response.data.features.map(f => f.properties)
|
|
|
- : response.data.data;
|
|
|
- return rawData;
|
|
|
+ isLoading.value = true;
|
|
|
+ const apiUrl = '/api/vector/stats/FluxCd_input_data';
|
|
|
+ console.log('正在请求数据:', apiUrl);
|
|
|
+
|
|
|
+ const response = await api8000.get(apiUrl);
|
|
|
+ console.log('API响应:', response);
|
|
|
+
|
|
|
+ // 调试:输出响应结构
|
|
|
+ console.log('响应数据:', response.data);
|
|
|
+
|
|
|
+ // 处理不同的响应格式
|
|
|
+ let processedData;
|
|
|
+ if (response.data && typeof response.data === 'object') {
|
|
|
+ // 情况1: 直接返回统计对象
|
|
|
+ if (response.data.Initial_Cd || response.data.DQCJ_Cd) {
|
|
|
+ processedData = response.data;
|
|
|
+ }
|
|
|
+ // 情况2: 包含features数组
|
|
|
+ else if (response.data.features && Array.isArray(response.data.features)) {
|
|
|
+ processedData = response.data.features.map(f => f.properties);
|
|
|
+ }
|
|
|
+ // 情况3: 包含data数组
|
|
|
+ else if (response.data.data && Array.isArray(response.data.data)) {
|
|
|
+ processedData = response.data.data;
|
|
|
+ }
|
|
|
+ // 情况4: 数组直接返回
|
|
|
+ else if (Array.isArray(response.data)) {
|
|
|
+ processedData = response.data;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!processedData) {
|
|
|
+ throw new Error('无法解析API返回的数据结构');
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('处理后的数据:', processedData);
|
|
|
+ return processedData;
|
|
|
} catch (err) {
|
|
|
- throw new Error('数据加载失败: ' + err.message);
|
|
|
+ console.error('数据请求失败:', err);
|
|
|
+ throw new Error(`数据加载失败: ${err.message || '网络或服务器错误'}`);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 计算单个字段的统计量
|
|
|
-const calculateFieldStats = (statsData, fieldKey, fieldName) => {
|
|
|
- // 从接口数据中获取当前字段的统计结果
|
|
|
- const fieldStats = statsData[fieldKey];
|
|
|
- if (!fieldStats) {
|
|
|
- return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
|
|
|
+// 计算统计量 - 支持原始数据和预统计数据
|
|
|
+const calculateFieldStats = (data, fieldKey, fieldName) => {
|
|
|
+ // 如果已经是预统计好的数据
|
|
|
+ if (data[fieldKey] && typeof data[fieldKey] === 'object') {
|
|
|
+ const fieldStats = data[fieldKey];
|
|
|
+ return {
|
|
|
+ key: fieldKey,
|
|
|
+ name: fieldName,
|
|
|
+ min: fieldStats.min || null,
|
|
|
+ q1: fieldStats.q1 || null,
|
|
|
+ median: fieldStats.median || null,
|
|
|
+ q3: fieldStats.q3 || null,
|
|
|
+ max: fieldStats.max || null,
|
|
|
+ count: fieldStats.count || 0
|
|
|
+ };
|
|
|
}
|
|
|
-
|
|
|
- // 提取预统计值
|
|
|
- let min = fieldStats.min;
|
|
|
- let q1 = fieldStats.q1;
|
|
|
- let median = fieldStats.median;
|
|
|
- let q3 = fieldStats.q3;
|
|
|
- let max = fieldStats.max;
|
|
|
-
|
|
|
- // 强制校正统计量顺序(确保 min ≤ q1 ≤ median ≤ q3 ≤ max)
|
|
|
- const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
|
|
|
- return {
|
|
|
- key: fieldKey,
|
|
|
- name: fieldName,
|
|
|
- min: sortedStats[0],
|
|
|
- q1: sortedStats[1],
|
|
|
- median: sortedStats[2],
|
|
|
- q3: sortedStats[3],
|
|
|
- max: sortedStats[4]
|
|
|
- };
|
|
|
+
|
|
|
+ // 如果是原始数据数组,需要手动计算统计量
|
|
|
+ if (Array.isArray(data)) {
|
|
|
+ const values = data
|
|
|
+ .map(item => parseFloat(item[fieldKey]))
|
|
|
+ .filter(val => !isNaN(val) && val !== null);
|
|
|
+
|
|
|
+ if (values.length === 0) {
|
|
|
+ return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null, count: 0 };
|
|
|
+ }
|
|
|
+
|
|
|
+ values.sort((a, b) => a - b);
|
|
|
+ const min = values[0];
|
|
|
+ const max = values[values.length - 1];
|
|
|
+ const median = values[Math.floor(values.length / 2)];
|
|
|
+ const q1 = values[Math.floor(values.length / 4)];
|
|
|
+ const q3 = values[Math.floor(values.length * 3 / 4)];
|
|
|
+
|
|
|
+ return { key: fieldKey, name: fieldName, min, q1, median, q3, max, count: values.length };
|
|
|
+ }
|
|
|
+
|
|
|
+ return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null, count: 0 };
|
|
|
};
|
|
|
|
|
|
-// 计算所有统计数据(拆分两组)
|
|
|
-const calculateAllStats = (statsData) => {
|
|
|
- // 1. 初始Cd统计(与配置顺序一致)
|
|
|
+// 计算所有统计数据
|
|
|
+const calculateAllStats = (data) => {
|
|
|
+ console.log('计算统计数据,输入数据:', data);
|
|
|
+
|
|
|
+ // 初始Cd统计
|
|
|
initialCdStats.value = fieldConfig.initialCd.map(indicator =>
|
|
|
- calculateFieldStats(statsData, indicator.key, indicator.name)
|
|
|
+ calculateFieldStats(data, indicator.key, indicator.name)
|
|
|
);
|
|
|
|
|
|
- // 2. 其他指标统计(与配置顺序一致)
|
|
|
+ // 其他指标统计
|
|
|
otherIndicatorsStats.value = fieldConfig.otherIndicators.map(indicator =>
|
|
|
- calculateFieldStats(statsData, indicator.key, indicator.name)
|
|
|
+ calculateFieldStats(data, indicator.key, indicator.name)
|
|
|
);
|
|
|
|
|
|
- // 3. 更新样本数(从预统计数据中取第一个字段的count)
|
|
|
- const firstFieldKey = fieldConfig.initialCd[0]?.key || fieldConfig.otherIndicators[0]?.key;
|
|
|
- stats.value.samples = statsData[firstFieldKey]?.count || 0;
|
|
|
+ // 更新样本数
|
|
|
+ const firstStat = initialCdStats.value[0] || otherIndicatorsStats.value[0];
|
|
|
+ stats.value.samples = firstStat?.count || 0;
|
|
|
+
|
|
|
+ console.log('计算后的统计数据:', {
|
|
|
+ initialCdStats: initialCdStats.value,
|
|
|
+ otherIndicatorsStats: otherIndicatorsStats.value,
|
|
|
+ sampleCount: stats.value.samples
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
-// 构建箱线图数据(通用函数)
|
|
|
+// 构建箱线图数据
|
|
|
const buildBoxplotData = (statsArray) => {
|
|
|
return statsArray.map(stat => {
|
|
|
- if (!stat.min) return [null, null, null, null, null];
|
|
|
+ if (stat.min === null || stat.min === undefined) {
|
|
|
+ return [null, null, null, null, null];
|
|
|
+ }
|
|
|
return [stat.min, stat.q1, stat.median, stat.q3, stat.max];
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-// 初始化【初始Cd】图表(独立箱线图)
|
|
|
+// 初始化【初始Cd】图表
|
|
|
const initInitialCdChart = () => {
|
|
|
- // 容器存在性检查
|
|
|
if (!initialCdChart.value) {
|
|
|
- console.error('initialCdChart容器未找到');
|
|
|
- error.value = new Error('初始Cd图表容器未找到,请刷新页面重试');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 容器尺寸检查
|
|
|
- const { offsetWidth, offsetHeight } = initialCdChart.value;
|
|
|
- if (offsetWidth === 0 || offsetHeight === 0) {
|
|
|
- console.error('initialCdChart容器尺寸异常', { offsetWidth, offsetHeight });
|
|
|
- error.value = new Error('初始Cd图表容器尺寸异常,请检查页面样式');
|
|
|
+ error.value = new Error('初始Cd图表容器未找到');
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- // 销毁旧实例
|
|
|
- if (chartInstanceInitial.value) {
|
|
|
- chartInstanceInitial.value.dispose();
|
|
|
- }
|
|
|
|
|
|
- // 初始化图表
|
|
|
try {
|
|
|
+ if (chartInstanceInitial.value) {
|
|
|
+ chartInstanceInitial.value.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
chartInstanceInitial.value = echarts.init(initialCdChart.value);
|
|
|
const xAxisData = fieldConfig.initialCd.map(ind => ind.name);
|
|
|
const boxData = buildBoxplotData(initialCdStats.value);
|
|
|
|
|
|
- chartInstanceInitial.value.setOption({
|
|
|
- title: { text: '初始Cd分布箱线图', left: 'center', textStyle: { fontSize: 14 } },
|
|
|
+ const option = {
|
|
|
+ title: { text: '初始Cd分布箱线图', left: 'center' },
|
|
|
tooltip: {
|
|
|
trigger: "item",
|
|
|
- formatter: (params) => formatTooltip(initialCdStats.value[params.dataIndex])
|
|
|
+ formatter: (params) => {
|
|
|
+ const stat = initialCdStats.value[params.dataIndex];
|
|
|
+ return formatTooltip(stat);
|
|
|
+ }
|
|
|
},
|
|
|
grid: { top: 60, right: 30, bottom: 25, left: 60 },
|
|
|
xAxis: {
|
|
|
type: "category",
|
|
|
- data: xAxisData,
|
|
|
- axisLabel: { fontSize: 12 }
|
|
|
+ data: xAxisData
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: "value",
|
|
|
- name: 'g/ha',
|
|
|
- nameTextStyle: { fontSize: 12 },
|
|
|
- axisLabel: { fontSize: 11 },
|
|
|
- scale: true
|
|
|
+ name: 'g/ha'
|
|
|
},
|
|
|
series: [{
|
|
|
- name: '初始Cd',
|
|
|
type: "boxplot",
|
|
|
itemStyle: {
|
|
|
- color: (p) => fieldConfig.initialCd[p.dataIndex].color,
|
|
|
+ color: fieldConfig.initialCd[0].color,
|
|
|
borderWidth: 2
|
|
|
},
|
|
|
data: boxData
|
|
|
}]
|
|
|
- });
|
|
|
+ };
|
|
|
+
|
|
|
+ chartInstanceInitial.value.setOption(option);
|
|
|
} catch (err) {
|
|
|
console.error('初始Cd图表初始化失败', err);
|
|
|
error.value = new Error(`初始Cd图表初始化失败: ${err.message}`);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 初始化【其他指标】合并图表
|
|
|
+// 初始化【其他指标】图表
|
|
|
const initOtherIndicatorsChart = () => {
|
|
|
- // 容器存在性检查
|
|
|
if (!otherIndicatorsChart.value) {
|
|
|
- console.error('otherIndicatorsChart容器未找到');
|
|
|
- error.value = new Error('其他指标图表容器未找到,请刷新页面重试');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 容器尺寸检查
|
|
|
- const { offsetWidth, offsetHeight } = otherIndicatorsChart.value;
|
|
|
- if (offsetWidth === 0 || offsetHeight === 0) {
|
|
|
- console.error('otherIndicatorsChart容器尺寸异常', { offsetWidth, offsetHeight });
|
|
|
- error.value = new Error('其他指标图表容器尺寸异常,请检查页面样式');
|
|
|
+ error.value = new Error('其他指标图表容器未找到');
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- // 销毁旧实例
|
|
|
- if (chartInstanceOther.value) {
|
|
|
- chartInstanceOther.value.dispose();
|
|
|
- }
|
|
|
|
|
|
- // 初始化图表
|
|
|
try {
|
|
|
+ if (chartInstanceOther.value) {
|
|
|
+ chartInstanceOther.value.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
chartInstanceOther.value = echarts.init(otherIndicatorsChart.value);
|
|
|
const xAxisData = fieldConfig.otherIndicators.map(ind => ind.name);
|
|
|
const boxData = buildBoxplotData(otherIndicatorsStats.value);
|
|
|
|
|
|
- chartInstanceOther.value.setOption({
|
|
|
- title: { text: '其他通量Cd指标分布对比', left: 'center', textStyle: { fontSize: 14 } },
|
|
|
+ const option = {
|
|
|
+ title: { text: '其他通量Cd指标分布对比', left: 'center' },
|
|
|
tooltip: {
|
|
|
trigger: "item",
|
|
|
- formatter: (params) => formatTooltip(otherIndicatorsStats.value[params.dataIndex])
|
|
|
+ formatter: (params) => {
|
|
|
+ const stat = otherIndicatorsStats.value[params.dataIndex];
|
|
|
+ return formatTooltip(stat);
|
|
|
+ }
|
|
|
},
|
|
|
grid: { top: 60, right: 30, bottom: 70, left: 60 },
|
|
|
xAxis: {
|
|
|
type: "category",
|
|
|
data: xAxisData,
|
|
|
axisLabel: {
|
|
|
- fontSize: 11,
|
|
|
rotate: 45,
|
|
|
- interval: 0, // 强制显示所有标签
|
|
|
formatter: (value) => value.length > 8 ? value.substring(0, 8) + '...' : value
|
|
|
}
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: "value",
|
|
|
- name: 'g/ha/a',
|
|
|
- nameTextStyle: { fontSize: 12 },
|
|
|
- axisLabel: { fontSize: 11 }
|
|
|
+ name: 'g/ha/a'
|
|
|
},
|
|
|
series: [{
|
|
|
- name: '其他指标',
|
|
|
type: "boxplot",
|
|
|
itemStyle: {
|
|
|
- color: (p) => fieldConfig.otherIndicators[p.dataIndex].color,
|
|
|
+ color: (params) => fieldConfig.otherIndicators[params.dataIndex].color,
|
|
|
borderWidth: 2
|
|
|
},
|
|
|
data: boxData
|
|
|
}]
|
|
|
- });
|
|
|
+ };
|
|
|
+
|
|
|
+ chartInstanceOther.value.setOption(option);
|
|
|
} catch (err) {
|
|
|
console.error('其他指标图表初始化失败', err);
|
|
|
error.value = new Error(`其他指标图表初始化失败: ${err.message}`);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// Tooltip格式化(通用逻辑)
|
|
|
+// Tooltip格式化
|
|
|
const formatTooltip = (stat) => {
|
|
|
- if (!stat || !stat.min) {
|
|
|
+ if (!stat || stat.min === null) {
|
|
|
return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || '未知'}</div><div>无有效数据</div>`;
|
|
|
}
|
|
|
+
|
|
|
return `<div style="font-weight:bold">${stat.name}</div>
|
|
|
<div style="margin-top:8px">
|
|
|
<div>最小值:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
|
|
|
@@ -294,57 +325,39 @@ const formatTooltip = (stat) => {
|
|
|
<div>中位数:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
|
|
|
<div>上四分位:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
|
|
|
<div>最大值:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
|
|
|
+ <div>样本数:<span style="color:#909399;">${stat.count}</span></div>
|
|
|
</div>`;
|
|
|
};
|
|
|
|
|
|
-// 初始化图表主流程
|
|
|
+// 主初始化函数
|
|
|
const initCharts = async () => {
|
|
|
try {
|
|
|
isLoading.value = true;
|
|
|
error.value = null;
|
|
|
- chartInstanceInitial.value = null;
|
|
|
- chartInstanceOther.value = null;
|
|
|
|
|
|
- // 1. 获取数据
|
|
|
+ console.log('开始初始化图表...');
|
|
|
+
|
|
|
+ // 获取数据
|
|
|
const data = await fetchData();
|
|
|
- if (!data || data.length === 0) {
|
|
|
+ console.log('获取到的数据:', data);
|
|
|
+
|
|
|
+ if (!data || (Array.isArray(data) && data.length === 0)) {
|
|
|
throw new Error('未获取到有效数据');
|
|
|
}
|
|
|
|
|
|
- // 2. 计算统计数据
|
|
|
+ // 计算统计数据
|
|
|
calculateAllStats(data);
|
|
|
|
|
|
- // 3. 等待DOM更新
|
|
|
- await nextTick();
|
|
|
-
|
|
|
- // 4. 轮询检查容器尺寸(最多等待3秒)
|
|
|
- const checkContainers = () => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- let checkCount = 0;
|
|
|
- const interval = setInterval(() => {
|
|
|
- // 检查两个容器的宽度是否有效
|
|
|
- const initialWidth = initialCdChart.value?.offsetWidth || 0;
|
|
|
- const otherWidth = otherIndicatorsChart.value?.offsetWidth || 0;
|
|
|
-
|
|
|
- if (initialWidth > 0 && otherWidth > 0) {
|
|
|
- clearInterval(interval);
|
|
|
- resolve();
|
|
|
- } else if (checkCount >= 30) { // 30 * 100ms = 3秒
|
|
|
- clearInterval(interval);
|
|
|
- reject(new Error('图表容器尺寸异常,准备超时,请检查样式'));
|
|
|
- }
|
|
|
- checkCount++;
|
|
|
- }, 100);
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- await checkContainers();
|
|
|
+ // 等待DOM更新
|
|
|
+ await nextTick();
|
|
|
|
|
|
- // 5. 初始化图表
|
|
|
+ // 初始化图表
|
|
|
initInitialCdChart();
|
|
|
initOtherIndicatorsChart();
|
|
|
|
|
|
isLoading.value = false;
|
|
|
+ console.log('图表初始化完成');
|
|
|
+
|
|
|
} catch (err) {
|
|
|
isLoading.value = false;
|
|
|
error.value = err;
|
|
|
@@ -352,32 +365,21 @@ const initCharts = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 组件挂载 & 销毁
|
|
|
+// 组件生命周期
|
|
|
onMounted(() => {
|
|
|
initCharts();
|
|
|
|
|
|
- // 窗口resize响应
|
|
|
const handleResize = () => {
|
|
|
- if (chartInstanceInitial.value) chartInstanceInitial.value.resize();
|
|
|
- if (chartInstanceOther.value) chartInstanceOther.value.resize();
|
|
|
+ chartInstanceInitial.value?.resize();
|
|
|
+ chartInstanceOther.value?.resize();
|
|
|
};
|
|
|
- window.addEventListener('resize', handleResize);
|
|
|
|
|
|
- return () => {
|
|
|
- window.removeEventListener('resize', handleResize);
|
|
|
- if (chartInstanceInitial.value) chartInstanceInitial.value.dispose();
|
|
|
- if (chartInstanceOther.value) chartInstanceOther.value.dispose();
|
|
|
- };
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.flux-cd-dashboard {
|
|
|
- font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
|
- max-width: 1200px;
|
|
|
- margin: 0 auto;
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
+/* 样式保持不变 */
|
|
|
.chart-container {
|
|
|
background: white;
|
|
|
border-radius: 6px;
|
|
|
@@ -387,32 +389,7 @@ onMounted(() => {
|
|
|
min-height: 400px;
|
|
|
position: relative;
|
|
|
}
|
|
|
-[ref="initialCdChart"],
|
|
|
-[ref="otherIndicatorsChart"] {
|
|
|
- min-height: 400px;
|
|
|
- background-color: #f9f9f9;
|
|
|
- border: 1px dashed #eee;
|
|
|
-}
|
|
|
|
|
|
-.section-title {
|
|
|
- color: #2c3e50;
|
|
|
- border-left: 3px solid #3498db;
|
|
|
- padding-left: 10px;
|
|
|
- margin-bottom: 12px;
|
|
|
-}
|
|
|
-.legend-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- margin-right: 12px;
|
|
|
- margin-bottom: 6px;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.legend-color {
|
|
|
- width: 10px;
|
|
|
- height: 10px;
|
|
|
- border-radius: 50%;
|
|
|
- margin-right: 5px;
|
|
|
-}
|
|
|
.spinner {
|
|
|
width: 30px;
|
|
|
height: 30px;
|
|
|
@@ -421,20 +398,8 @@ onMounted(() => {
|
|
|
border-left-color: #3498db;
|
|
|
animation: spin 1s linear infinite;
|
|
|
}
|
|
|
-@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
-.stat-card {
|
|
|
- background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
|
|
|
- border-radius: 6px;
|
|
|
- padding: 8px 12px;
|
|
|
- box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
|
-}
|
|
|
-.stat-value {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: bold;
|
|
|
- color: #2c3e50;
|
|
|
-}
|
|
|
-.stat-label {
|
|
|
- font-size: 12px;
|
|
|
- color: #7f8c8d;
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
}
|
|
|
</style>
|