WholeProcessPlatform/frontend/src/modules/EnvironmentalQuality/index.vue
2026-06-01 08:37:38 +08:00

1014 lines
32 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.

<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="地表水水质达标率" :select="select" :datetimePicker="datetimePicker"
@update-values="handlePanelChange1" style="height: 650px;">
<div class="body_item">
<div v-if="baseid == 'all'" class="tabs_all">
<div :class="tabs == 0 ? 'zhong_tabs' : 'no_tabs'" @click="handleTabChange(0)">自建水质站</div>
<div :class="tabs == 1 ? 'zhong_tabs' : 'no_tabs'" @click="handleTabChange(1)">国家水质站</div>
</div>
<div v-show="tabs == 0 || tabs == 1" class="tabs_body">
<a-spin :spinning="loading">
<!-- 始终渲染图表容器确保有固定宽高 -->
<div ref="chartRef" class="chart-container"></div>
<!-- 无数据时显示Empty但不影响容器尺寸 -->
<div v-if="chartData.length === 0 && !loading" class="empty-overlay">
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
</div>
</SidePanelItem>
<!-- 环境质量满足度弹框 -->
<a-modal
v-model:open="modalVisible"
title="环境质量满足度"
width="1536px"
:footer="null"
centered
>
<EnvironmentalQualityTwoLayers
:datas="jiDiList"
:type="select.value "
:dateValue="datetimePicker.value"
:dataDimensionVal="baseid"
:buildType="tabs"
/>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue';
import * as echarts from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { wqGetKendoListCust } from '@/api/sz'
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
import EnvironmentalQualityTwoLayers from "./TwoLayers/EnvironmentalQualityTwoLayers.vue"
// 定义组件名(便于调试和递归)
defineOptions({
name: 'EnvironmentalQuality'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const tabs = ref(0);
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const loading = ref(false);
// 响应式图表数据
const chartData = ref<Array<{ name: string; current: number | null; lastYear: number | null }>>([]);
const select = ref({
show: true,
value: 'month',
options: [
{ value: 'month', label: '月度' },
{ value: 'year', label: '年度' }
],
picker: undefined,
format: undefined,
});
// 日期选择器配置
const datetimePicker = ref({
show: true,
value: (() => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
return `${year}-${month}`;
})(),
format: "YYYY-MM",
picker: "month" as const,
options: [],
});
const baseid = ref("")
const jiDiList: any = ref([
{
"baseid": "all",
"basename": "当前全部",
"id": "9BFEC848-83EA-AD22-6DE2-10E969476693"
},
{
"baseid": "01",
"basename": "金沙江干流",
"id": "A33040B7-8977-D9F1-5E02-D7F0241AB8AA"
},
{
"baseid": "02",
"basename": "雅砻江干流",
"id": "C63D6020-995D-FE97-2F4A-F619A7142C79"
},
{
"baseid": "03",
"basename": "大渡河干流",
"id": "CCB0766B-F1D4-7DD6-650F-5C556B7231B3"
},
{
"baseid": "04",
"basename": "乌江干流",
"id": "E8A66641-B4F4-CC85-0815-38C5B2F93DBD"
},
{
"baseid": "05",
"basename": "长江上游干流",
"id": "E02C11E9-CEA5-A030-202F-3BFE84465D03"
},
{
"baseid": "10",
"basename": "湘西",
"id": "B1D1D52D-CEEF-6DC4-27DF-9990EB572F8D"
},
{
"baseid": "08",
"basename": "黄河上游干流",
"id": "BC6AF135-263D-09A9-D5CA-C99B2598FE6E"
},
{
"baseid": "09",
"basename": "黄河中游干流",
"id": "60EEEC28-128F-A2A8-44F3-6EAAC8FD8BB6"
},
{
"baseid": "06",
"basename": "南盘江-红水河",
"id": "7BB9A8F4-34B5-42B4-A7FC-CE910AD7F203"
},
{
"baseid": "12",
"basename": "东北",
"id": "47F8EF06-924E-E161-FCAF-62A66BBF252D"
},
{
"baseid": "07",
"basename": "澜沧江干流",
"id": "A966A4C9-278C-B0DA-2B97-2D10B7A6E96A"
},
{
"baseid": "13",
"basename": "怒江干流",
"id": "FA89E8CB-67A8-76DA-DC1E-23AE4C54F9E4"
},
{
"baseid": "11",
"basename": "闽浙赣",
"id": "CD98F995-EEB2-1021-D807-DA1B1AD9E49A"
},
{
"baseid": "other",
"basename": "其他",
"id": "AFBDFC67-B955-4EFD-959A-014CFB59EBFC"
}
])
// 基地列表(从 Store 或其他来源获取)
const baseList = computed(() => {
// TODO: 根据实际情况返回基地列表
// 这里需要根据项目实际情况从 Store 或 API 获取
// 示例结构:
// return [
// { baseid: 'all', basename: '全部' },
// { baseid: '01', basename: '金沙江干流' },
// ...
// ]
// 临时返回空数组,待后续补充实际数据源
return []
})
// 解析API返回的嵌套数据支持两种结构
const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValue: string) => {
const result: Array<{ name: string; current: number | null; lastYear: number | null }> = [];
console.log('=== parseApiResponse 开始解析 ===');
console.log('currentValueForApi:', currentValueForApi);
console.log('lastYearValue:', lastYearValue);
console.log('items数量:', items?.length);
items.forEach((group, groupIndex) => {
console.log(`\n--- 处理第${groupIndex + 1}个group ---`);
console.log('group.key:', group.key);
console.log('group.items数量:', group.items?.length);
group.items?.forEach((mainGroup, mainIndex) => {
console.log(` → 第${mainIndex + 1}个mainGroup:`);
console.log(' field:', mainGroup.field);
console.log(' key:', mainGroup.key);
// 判断是哪种结构检查第二层的field字段
const field = mainGroup.field;
if (field === 'ennm' || field === 'baseId') {
// 提取中文名称从mainGroup.key获取站点名或基地ID
let displayName = '';
// mainGroup.key 可能是站点名(如"两河口"或基地ID如"02"
if (mainGroup.key && typeof mainGroup.key === 'string') {
displayName = mainGroup.key;
console.log(' 从mainGroup.key获取名称:', displayName);
}
// 如果mainGroup.key不是有效的中文名称尝试从subGroup.key获取
if (!displayName || !/[\u4e00-\u9fa5]/.test(displayName)) {
const subGroup = mainGroup.items?.[0];
console.log(' subGroup.key:', subGroup?.key);
if (subGroup && subGroup.key && typeof subGroup.key === 'string') {
displayName = subGroup.key; // 这里应该是中文名(如"雅砻江干流"
console.log(' 从subGroup.key获取名称:', displayName);
}
}
// 如果还是没有,尝试从最内层数据获取
if (!displayName) {
const subGroup = mainGroup.items?.[0];
const firstMonthGroup = subGroup?.items?.[0];
const dataItem = firstMonthGroup?.items?.[0];
if (dataItem) {
if (field === 'ennm') {
displayName = dataItem.ENNM || '';
} else if (field === 'baseId') {
displayName = dataItem.BASENAME || '';
}
console.log(' 从最内层数据获取名称:', displayName);
}
}
if (!displayName) {
console.log(' ⚠️ 跳过:没有名称');
return; // 跳过没有名称的数据
}
let currentPeriodValue: number | null = null;
let lastYearPeriodValue: number | null = null;
// 遍历时间分组需要深入到第3层
const subGroup = mainGroup.items?.[0];
console.log(' subGroup.items数量:', subGroup?.items?.length);
subGroup?.items?.forEach(monthGroup => {
const periodKey = monthGroup.key; // "2026-05" 或 "2025-05"
const dataItem = monthGroup.items?.[0];
if (dataItem) {
const value = dataItem.COUNT_DRMONTH ?? dataItem.wqRate;
console.log(` 时间:${periodKey}, 值:${value}`);
// 判断是当前期还是去年同期
if (periodKey === currentValueForApi) {
currentPeriodValue = value != null ? Number(value) : null;
console.log(` ✓ 当前期值: ${currentPeriodValue}`);
} else if (periodKey === lastYearValue) {
lastYearPeriodValue = value != null ? Number(value) : null;
console.log(` ✓ 去年同期值: ${lastYearPeriodValue}`);
}
}
});
// 添加换行符:在"干流"前添加\n
const finalDisplayName = displayName.replace(/干流$/, '\n干流');
console.log(` → 最终数据: {name: "${finalDisplayName}", current: ${currentPeriodValue}, lastYear: ${lastYearPeriodValue}}`);
result.push({
name: finalDisplayName,
current: currentPeriodValue,
lastYear: lastYearPeriodValue
});
}
});
});
console.log('\n=== parseApiResponse 解析完成 ===');
console.log('result数量:', result.length);
console.log('result:', result);
return result;
};
// 处理tab切换
const handleTabChange = (tab: number) => {
tabs.value = tab;
getEchartsData()
};
//监听tabs变化处理图表刷新
watch(tabs, (newVal) => {
if (newVal === 0 || newVal === 1) {
nextTick(() => {
setTimeout(() => {
getEchartsData();
}, 50);
});
}
});
watch(
() => JidiSelectEventStore.selectedItem,
(newVal) => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getEchartsData();
}
},
{ deep: true }
);
// 初始化图表实例仅创建实例不设置option
const initChart = (): Promise<void> => {
return new Promise((resolve, reject) => {
if (!chartRef.value) {
console.error('图表容器ref不存在');
reject(new Error('图表容器不存在'));
return;
}
if (chartInstance) {
resolve();
return;
}
const domElement = chartRef.value;
// 检查 DOM 尺寸
if (domElement.clientWidth === 0 || domElement.clientHeight === 0) {
console.warn('图表容器尺寸为0等待DOM布局完成...', {
clientWidth: domElement.clientWidth,
clientHeight: domElement.clientHeight,
offsetWidth: domElement.offsetWidth,
offsetHeight: domElement.offsetHeight
});
// 使用多次重试机制
let retryCount = 0;
const maxRetries = 10;
const tryInit = () => {
retryCount++;
if (domElement.clientWidth > 0 && domElement.clientHeight > 0) {
chartInstance = echarts.init(domElement);
console.log('图表实例已创建(第' + retryCount + '次尝试)');
resolve();
} else if (retryCount < maxRetries) {
console.log(`第${retryCount}次尝试容器尺寸仍为0继续等待...`);
requestAnimationFrame(tryInit);
} else {
console.error('图表容器尺寸仍为0已达到最大重试次数', {
clientWidth: domElement.clientWidth,
clientHeight: domElement.clientHeight
});
reject(new Error('图表容器尺寸为0'));
}
};
// 开始重试
requestAnimationFrame(tryInit);
} else {
// 尺寸正常,直接初始化
chartInstance = echarts.init(domElement);
console.log('图表实例已创建');
resolve();
}
});
};
//获取图表数据
const getEchartsData = async () => {
// 1. 格式验证
const currentValue = datetimePicker.value.value;
// debugger
if (select.value.value === 'year') {
// 年度模式:验证是否为 YYYY 格式4位数字
if (!/^\d{4}$/.test(currentValue)) {
console.warn('年度模式下日期格式不正确,应为 YYYY 格式');
return;
}
} else if (select.value.value === 'month') {
// 月度模式:验证是否为 YYYY-MM 格式
if (!/^\d{4}-\d{2}$/.test(currentValue)) {
console.warn('月度模式下日期格式不正确,应为 YYYY-MM 格式');
return;
}
} else {
console.warn('未知的选择器类型');
return;
}
// 2. 计算当前值和上一年的值
let currentValueForApi: string; // 第一个值:当前选择
let lastYearValue: string; // 第二个值:上一年同期
if (select.value.value === 'year') {
// 年模式
currentValueForApi = currentValue; // "2025"
lastYearValue = String(Number(currentValue) - 1); // "2024"
} else {
// 月模式
currentValueForApi = currentValue; // "2025-03"
// 提取年份和月份
const [year, month] = currentValue.split('-');
// 年份减1月份保持不变
const lastYear = String(Number(year) - 1);
lastYearValue = `${lastYear}-${month}`; // "2024-03"
}
// 3. 构建请求参数
let params = {
"filter": {
"logic": "and",
"filters": [
baseid.value != 'all' ? {
"field": "baseId",
"operator": "eq",
"value": baseid.value
} : null,
{
"field": "type",
"operator": "eq",
"value": select.value.value == "year" ? "YEAR" : "MON"
},
{
"field": "dtinType",
"operator": "eq",
"value": tabs.value
},
{
"logic": "or",
"filters": [
{
"field": select.value.value == "year" ? "drYear" : "drMonth",
"operator": "eq",
"value": currentValueForApi, // 当前值(年或年月)
"dataType": "string"
},
{
"field": select.value.value == "year" ? "drYear" : "drMonth",
"operator": "eq",
"value": lastYearValue, // 上一年同期
"dataType": "string"
}
]
}
].filter(Boolean)
},
"group": [
{
"dir": "des",
"field": "baseStepSort"
},
baseid.value == 'all' ? {
"dir": "des",
"field": "baseId"
} : {
"dir": "asc",
"field": "ennm"
},
baseid.value == 'all' ? {
"dir": "des",
"field": "baseName"
} : {
"dir": "des",
"field": "rstcd"
},
{
"dir": "desc",
"field": select.value.value == "year" ? "drYear" : "drMonth"
}
],
"groupResultFlat": false,
"aggregate": [
{
"aggregate": "sum",
"field": "wqRate"
}
]
};
// 4. 调用API并处理数据
loading.value = true;
try {
const res = await wqGetKendoListCust(params);
console.log('=== API返回数据调试 ===');
console.log('res:', res);
console.log('res.data:', res?.data);
console.log('res.data.data:', res?.data?.data);
console.log('currentValueForApi:', currentValueForApi);
console.log('lastYearValue:', lastYearValue);
if (!res?.data?.data) {
console.warn('数据获取失败或数据为空');
chartData.value = [];
// 清空图表中原有的数据
if (chartInstance) {
chartInstance.clear();
console.log('图表已清空(数据获取失败)');
}
return;
}
// 解析数据
const parsedData = parseApiResponse(res.data.data, currentValueForApi, lastYearValue);
console.log('解析后的数据 parsedData:', parsedData);
// 只有当有数据时才更新
if (parsedData.length > 0) {
chartData.value = parsedData;
console.log('chartData.value:', chartData.value);
// 等待 DOM 更新后,再初始化和更新图表
await nextTick();
try {
// 如果图表实例不存在,先初始化(等待完成)
if (!chartInstance) {
await initChart();
}
// 更新图表
if (chartInstance) {
updateChartData(currentValueForApi, lastYearValue);
console.log('图表已更新');
} else {
console.error('图表实例创建失败');
}
} catch (error) {
console.error('图表初始化失败:', error);
}
} else {
chartData.value = [];
console.warn('解析后数据为空');
// 清空图表中原有的数据
if (chartInstance) {
chartInstance.clear();
console.log('图表已清空(解析后数据为空)');
}
}
} catch (error) {
console.error('获取图表数据失败:', error);
chartData.value = [];
// 清空图表中原有的数据
if (chartInstance) {
chartInstance.clear();
console.log('图表已清空(发生错误)');
}
} finally {
loading.value = false;
}
};
// 更新图表数据
const updateChartData = (currentLabel: string, lastYearLabel: string) => {
if (!chartInstance) return;
// 动态生成图例标签
const modeText = select.value.value === 'year' ? '' : ' 月度';
const legendCurrentLabel = currentLabel + modeText;
const legendLastYearLabel = ' 去年同期';
const option = {
// 图例配置
legend: {
top: 10,
itemWidth: 18,
itemHeight: 12,
itemGap: 20,
icon: 'roundRect',
data: [
{ name: legendCurrentLabel, itemStyle: { color: '#4A8BC2' } },
{ name: legendLastYearLabel, itemStyle: { color: '#9B59B6' } }
],
textStyle: {
fontSize: 12,
color: '#333'
}
},
// 提示框配置
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(200, 200, 200, 0.2)'
},
label: {
show: false
}
},
backgroundColor: '#fff',
borderColor: '#e8e8e8',
borderWidth: 1,
padding: 12,
textStyle: {
color: '#333',
fontSize: 12
},
formatter: (params: any) => {
const name = params[0].name.replace('\n', '');
let result = `<div style="font-weight: bold; margin-bottom: 8px;">${name}</div>`;
params.forEach((item: any) => {
const value = item.value != null ? Number(item.value).toFixed(1) : '-';
result += `<div style="display: flex; align-items: center; margin-bottom: 4px;">
<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: ${item.color}; margin-right: 8px;"></span>
<span>${item.seriesName}:</span>
<span style="margin-left: auto; font-weight: bold;">${value}%</span>
</div>`;
});
return result;
}
},
// 网格配置
grid: {
left: 60,
right: 10,
top: 60,
bottom: 60
},
// X轴配置
xAxis: {
type: 'value',
min: 0,
max: 100,
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: true,
lineStyle: {
color: '#e8e8e8',
type: 'solid'
}
},
axisLabel: {
fontSize: 12,
color: '#666'
}
},
// Y轴配置
yAxis: {
type: 'category',
data: chartData.value.map(item => item.name),
inverse: true,
axisLine: {
show: true,
lineStyle: {
color: '#333',
width: 1
}
},
axisTick: {
show: true,
length: 4,
lineStyle: {
color: '#333',
width: 1
}
},
axisLabel: {
fontSize: 12,
color: '#333',
margin: 12
},
// 在Y轴底部添加"达标率(%)"标签
name: '达标率(%)',
nameLocation: 'end',
nameGap: 20,
nameTextStyle: {
fontSize: 12,
color: '#666',
align: 'left'
}
},
// 系列配置
series: [
{
name: legendCurrentLabel,
type: 'bar',
data: chartData.value.map(item => item.current != null ? Number(item.current).toFixed(1) : null),
barWidth: 10,
barGap: '20%',
itemStyle: {
color: '#4A8BC2',
borderRadius: [0, 3, 3, 0]
}
},
{
name: legendLastYearLabel,
type: 'bar',
data: chartData.value.map(item => item.lastYear != null ? Number(item.lastYear).toFixed(1) : null),
barWidth: 10,
barGap: '20%',
itemStyle: {
color: '#9B59B6',
borderRadius: [0, 3, 3, 0]
}
}
]
};
chartInstance.setOption(option, { notMerge: true });
// 移除旧的点击事件监听
chartInstance.off('click');
// 绑定点击事件
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series' && params.seriesType === 'bar') {
// 获取点击的柱子对应的数据
const dataIndex = params.dataIndex;
const barData = chartData.value[dataIndex];
if (barData) {
clickedBarData.value = {
name: barData.name.replace('\n', ''), // 去除换行符
current: barData.current,
lastYear: barData.lastYear
};
modalVisible.value = true;
}
}
});
};
// 监听 datetimePicker.value.value 的变化
watch(
() => datetimePicker.value.value,
async (newValue, oldValue) => {
// 确保新值和旧值不同,且图表已初始化
if (newValue !== oldValue && chartInstance) {
await nextTick();
getEchartsData();
}
}
);
//监听子组件的数据变化
const handlePanelChange1 = async (data) => {
console.log('当前所有控件状态:', data);
// debugger
// 当选择器或日期变化时,重新加载图表数据
if (data.select || data.datetime) {
await (select.value.value = data.select)
await (datetimePicker.value.value = data.datetime)
await (datetimePicker.value.picker = data.select)
await (datetimePicker.value.format = data.select == 'month' ? 'YYYY-MM' : 'YYYY')
getEchartsData()
}
}
//获取图表数据
// 页面加载时执行
onMounted(() => {
// 延迟初始化,确保容器已渲染
setTimeout(() => {
initChart();
// 如果已有 selectedItem触发数据加载
if (JidiSelectEventStore.selectedItem?.wbsCode) {
baseid.value = JidiSelectEventStore.selectedItem.wbsCode;
getEchartsData();
}
}, 100);
// 监听窗口resize
window.addEventListener('resize', () => {
chartInstance?.resize();
});
});
// 组件卸载时清理
onUnmounted(() => {
chartInstance?.dispose();
chartInstance = null;
chartData.value = [];
window.removeEventListener('resize', () => {
chartInstance?.resize();
});
});
// 弹框相关状态
const modalVisible = ref(false);
const clickedBarData = ref<{ name: string; current: number | null; lastYear: number | null } | null>(null);
// 计算弹框中的时间信息
const modalTimeInfo = computed(() => {
if (!datetimePicker.value.value) return '';
const currentValue = datetimePicker.value.value;
const modeText = select.value.value === 'year' ? '年度' : '月度';
if (select.value.value === 'year') {
return `${currentValue}${modeText}`;
} else {
const [year, month] = currentValue.split('-');
return `${year}${month}${modeText}`;
}
});
// 计算同比变化值
const getChangeValue = () => {
if (!clickedBarData.value || clickedBarData.value.current == null || clickedBarData.value.lastYear == null) {
return '-';
}
const change = Number(clickedBarData.value.current) - Number(clickedBarData.value.lastYear);
const symbol = change > 0 ? '+' : '';
return `${symbol}${change.toFixed(1)}%`;
};
// 获取变化值的样式类
const getChangeClass = () => {
if (!clickedBarData.value || clickedBarData.value.current == null || clickedBarData.value.lastYear == null) {
return '';
}
const change = Number(clickedBarData.value.current) - Number(clickedBarData.value.lastYear);
if (change > 0) return 'positive';
if (change < 0) return 'negative';
return 'neutral';
};
</script>
<style lang="scss" scoped>
.body_item {
width: 100%;
height: 600px;
display: flex;
align-items: center;
justify-content: space-between;
.tabs_all {
width: 28px;
height: 500px;
box-sizing: border-box;
border: 2px solid #2f6b98;
border-radius: 5px;
.zhong_tabs {
width: 100%;
height: 50%;
background: #2f6b98;
color: #fff;
font-size: 14px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
text-shadow: 0 0 .25px currentcolor;
cursor: pointer;
}
.no_tabs {
width: 100%;
height: 50%;
font-size: 14px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
}
.no_tabs:hover {
color: #40a9ff;
}
}
.tabs_body {
width: 368px;
height: 600px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
/* 为empty-overlay提供定位上下文 */
.chart-container {
width: 100% !important;
height: 100% !important;
min-width: 368px;
/* 强制最小宽度 */
min-height: 600px;
/* 强制最小高度 */
}
.empty-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
z-index: 10;
}
}
}
.modal-content {
padding: 20px;
.detail-info {
.info-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #e8e8e8;
h3 {
margin: 0 0 8px 0;
font-size: 20px;
color: #333;
font-weight: bold;
}
.time-info {
margin: 0;
font-size: 14px;
color: #666;
}
}
.info-body {
.info-item {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: #f5f7fa;
border-radius: 4px;
.label {
font-size: 14px;
color: #666;
min-width: 140px;
}
.value {
font-size: 16px;
font-weight: bold;
&.current {
color: #4A8BC2;
}
&.last-year {
color: #9B59B6;
}
&.change {
&.positive {
color: #52c41a;
}
&.negative {
color: #ff4d4f;
}
&.neutral {
color: #666;
}
}
}
}
.custom-section {
margin-top: 24px;
padding: 16px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #e8e8e8;
h4 {
margin: 0 0 12px 0;
font-size: 16px;
color: #333;
}
p {
margin: 0;
font-size: 14px;
color: #666;
line-height: 1.6;
}
}
}
}
}
</style>