WholeProcessPlatform/frontend/src/modules/EnvironmentalQuality/index.vue
2026-09-02 16:19:28 +08:00

1049 lines
28 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="
chartData.length === 0 && !loading && baseid != 'all'
? 'height: 300px'
: '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"
:style="
chartData.length === 0 && !loading && baseid != 'all'
? 'height: 300px'
: 'height: 650px'
"
>
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
</div>
</SidePanelItem>
<!-- 环境质量满足度弹框 -->
<a-modal
v-model:open="modalVisible"
title="环境质量满足度"
width="1536px"
:footer="null"
centered
>
<EnvironmentalQualityTwoLayers
:datas="
baseid == 'all' ? JidiSelectEventStore.jidiData : stationListForModal
"
:type="select.value"
:dateValue="datetimePicker.value"
:dataDimensionVal="clickedBarData?.selectedId || baseid"
:buildType="tabs"
:typeOne="fieldOne"
:baseid="baseid"
/>
</a-modal>
</template>
<script lang="ts" setup>
import {
ref,
onMounted,
onUnmounted,
watch,
nextTick,
computed,
inject,
defineOptions
} 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';
import { useDraggable } from '@/utils/drag';
// 定义组件名(便于调试和递归)
defineOptions({
name: 'ShuiZhiDaBiaoQingKuang'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const tabs: any = 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 stationListForModal = ref<Array<{ baseid: string; basename: string }>>(
[]
);
// 新增:存储图表数据索引与 baseid/rstcd 的映射关系
const chartDataIdMap = ref<Array<string>>([]);
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 fieldOne = ref('');
// 解析API返回的嵌套数据支持两种结构
const parseApiResponse = (
items: any[],
currentValueForApi: string,
lastYearValue: string
) => {
const result: Array<{
name: string;
current: number | null;
lastYear: number | null;
}> = [];
fieldOne.value = '';
// 新增:清空 ID 映射数组
chartDataIdMap.value = [];
// 新增:提取电站列表(仅当 baseid != 'all' 时)
const stations: Array<{ baseid: string; basename: string }> = [];
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" 或 "2026" 或 "2025"
const dataItem = monthGroup.items?.[0];
if (dataItem) {
// 兼容月度数据COUNT_DRMONTH/wqRate和年度数据COUNT_DRYEAR
const value =
dataItem.COUNT_DRMONTH ??
dataItem.COUNT_DRYEAR ??
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}}`
);
// 新增:记录 ID 映射关系
if (field === 'baseId') {
fieldOne.value = 'all';
// baseid == 'all' 时,记录基地 ID
chartDataIdMap.value.push(mainGroup.key); // "02", "03" 等
console.log(' 记录基地ID:', mainGroup.key);
} else if (field === 'ennm') {
fieldOne.value = 'other';
// baseid != 'all' 时,记录电站编码
const stationCode = subGroup?.key; // "008660306300000001"
chartDataIdMap.value.push(stationCode || '');
console.log(' 记录电站编码:', stationCode);
// 同时提取电站列表
if (displayName && stationCode) {
stations.push({
baseid: stationCode,
basename: displayName
});
}
}
result.push({
name: finalDisplayName,
current: currentPeriodValue,
lastYear: lastYearPeriodValue
});
}
});
});
// 新增:如果提取到了电站列表,更新响应式变量
if (stations.length > 0) {
stationListForModal.value = stations;
console.log('提取到的电站列表:', stations);
} else {
stationListForModal.value = [];
}
console.log('ID 映射关系:', chartDataIdMap.value);
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);
});
}
});
// 监听电站变化(优先使用父组件 provide 的电站上下文,兜底 jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(
() => injectedStation?.value || JidiSelectEventStore.selectedItem
);
watch(
() => stationSource.value,
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;
//
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. 构建请求参数
console.log(baseid.value);
if (!baseid.value) {
return;
}
//
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'
},
baseid.value == 'all'
? {
field: 'dtinType',
operator: 'eq',
value: tabs.value
}
: {
field: 'rstcd',
operator: 'isnotnull'
},
,
{
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: [
baseid.value == 'all'
? {
dir: 'des',
field: 'baseStepSort'
}
: {
dir: 'asc',
field: 'rstcdStepSort'
},
,
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'
}
].filter(Boolean),
groupResultFlat: false,
aggregate: [
{
aggregate: 'sum',
field: 'wqRate'
}
]
};
console.log('请求参数:', params);
//
// 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) : '-';
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 ? Math.floor(Number(item.current)) : 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 ? Math.floor(Number(item.lastYear)) : 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) {
// 新增:获取对应的 baseid 或 rstcd
const selectedId = chartDataIdMap.value[dataIndex] || '';
//
clickedBarData.value = {
name: barData.name.replace('\n', ''), // 去除换行符
current: barData.current,
lastYear: barData.lastYear,
selectedId: selectedId // 新增:存储选中的 ID
};
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);
// 当选择器或日期变化时,重新加载图表数据
if (data.select || data.datetime) {
await (select.value.value = data.select);
// 根据 select 的值决定 datetime 的格式
let formattedDatetime = data.datetime;
if (data.select === 'year') {
// 如果是年度模式,将 datetime 转换为 YYYY 格式
if (data.datetime && typeof data.datetime === 'string') {
// 提取年份部分(假设原格式为 YYYY-MM
const yearMatch = data.datetime.match(/^(\d{4})/);
if (yearMatch) {
formattedDatetime = yearMatch[1];
}
}
}
// 如果是 month 模式,保持 datetime 原值不变
await (datetimePicker.value.value = formattedDatetime);
await (datetimePicker.value.picker = data.select);
await (datetimePicker.value.format =
data.select == 'month' ? 'YYYY-MM' : 'YYYY');
getEchartsData();
}
};
//获取图表数据
// 页面加载时执行
onMounted(() => {
// 延迟初始化,确保容器已渲染
setTimeout(() => {
initChart();
// 如果已有 selectedItem触发数据加载
const currentStation =
injectedStation?.value || JidiSelectEventStore.selectedItem;
if (currentStation?.wbsCode) {
baseid.value = currentStation.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;
selectedId?: string;
} | null>(null);
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
</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 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: 300px;
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>