Commit 6c6b90e5 authored by 水玉婷's avatar 水玉婷
Browse files

feat:添加饼图并支持多维度过滤

parent 09b84c6f
# 开发环境配置
VITE_API_BASE_URL=http://peddev.cmic.com.cn
# 应用基础路径
VITE_BASE_PATH=/ai/
VITE_SSE_PATH=/agentService
# API基础路径
VITE_API_BASE_PATH=''
# 应用代码
VITE_APP_CODE=ped.qywx
# 环境标识
VITE_ENV=development
\ No newline at end of file
# 测试环境配置
NODE_ENV = 'test'
# API基础URL - 保持不变,使用原配置
VITE_API_BASE_URL=http://peddev.cmic.com.cn
# 应用基础路径
VITE_BASE_PATH=/ai/
VITE_SSE_PATH=/aiService
# API基础路径
VITE_API_BASE_PATH=/pedapi
# 应用代码
VITE_APP_CODE=ped.qywx
# 环境标识
VITE_ENV=test
\ No newline at end of file
......@@ -5,7 +5,12 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:dev": "vite --mode dev",
"dev:test": "vite --mode test",
"test": "vite test",
"build": "vite build",
"build:dev": "vite build --mode dev",
"build:test": "vite build --mode test",
"preview": "vite preview"
},
"dependencies": {
......@@ -23,4 +28,4 @@
"less": "^4.4.2",
"vite": "^4.3.0"
}
}
}
\ No newline at end of file
......@@ -435,7 +435,7 @@ const sendMessage = async (type: MessageType = 'text', params: MessageParams = {
...props.params,
};
const response = await fetch(`${props.apiBaseUrl}/aiService/ask/app/${props.params?.appId}`, {
const response = await fetch(`${import.meta.env.VITE_SSE_PATH}/ask/app/${props.params?.appId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
......@@ -592,32 +592,48 @@ onBeforeUnmount(() => {
const simulateLineChartMessage = () => {
console.log('模拟折线图消息');
// 使用真实医院数据创建折线图数据
// 使用真实医院数据创建折线图数据,包含多个维度和指标
const lineChartData = {
title:"2023年第三季度武汉各医院每月住院患者总数及费用汇总",
dimFields: ['月份', '地区'],
indexFields: ['用户总数', '费用汇总'],
title:"2023年各医院月度运营数据汇总",
dimFields: ['月份', '地区', '医院类型', '科室'],
indexFields: ['用户总数', '费用汇总', '平均住院天数', '手术台数', '门诊量'],
rows: [
{ '月份': '1月', '地区': '北京', '用户总数': 100, '费用汇总': 5000 },
{ '月份': '2月', '地区': '北京', '用户总数': 150, '费用汇总': 6000 },
{ '月份': '3月', '地区': '北京', '用户总数': 120, '费用汇总': 5500 },
{ '月份': '4月', '地区': '北京', '用户总数': 180, '费用汇总': 8000 },
{ '月份': '5月', '地区': '北京', '用户总数': 200, '费用汇总': 9000 },
{ '月份': '6月', '地区': '北京', '用户总数': 250, '费用汇总': 11000 },
{ '月份': '1月', '地区': '上海', '用户总数': 80, '费用汇总': 4000 },
{ '月份': '2月', '地区': '上海', '用户总数': 120, '费用汇总': 5000 },
{ '月份': '3月', '地区': '上海', '用户总数': 100, '费用汇总': 4500 },
{ '月份': '4月', '地区': '上海', '用户总数': 150, '费用汇总': 7000 },
{ '月份': '5月', '地区': '上海', '用户总数': 180, '费用汇总': 8000 },
{ '月份': '6月', '地区': '上海', '用户总数': 220, '费用汇总': 10000 },
{ '月份': '1月', '地区': '广州', '用户总数': 60, '费用汇总': 3000 },
{ '月份': '2月', '地区': '广州', '用户总数': 90, '费用汇总': 4000 },
{ '月份': '3月', '地区': '广州', '用户总数': 80, '费用汇总': 3500 },
{ '月份': '4月', '地区': '广州', '用户总数': 120, '费用汇总': 6000 },
{ '月份': '5月', '地区': '广州', '用户总数': 150, '费用汇总': 7000 },
{ '月份': '6月', '地区': '广州', '用户总数': 180, '费用汇总': 9000 }
{ '月份': '1月', '地区': '北京', '医院类型': '三甲医院', '科室': '内科', '用户总数': 100, '费用汇总': 5000, '平均住院天数': 7.2, '手术台数': 15, '门诊量': 1200 },
{ '月份': '2月', '地区': '北京', '医院类型': '三甲医院', '科室': '内科', '用户总数': 150, '费用汇总': 6000, '平均住院天数': 6.8, '手术台数': 18, '门诊量': 1400 },
{ '月份': '3月', '地区': '北京', '医院类型': '三甲医院', '科室': '内科', '用户总数': 120, '费用汇总': 5500, '平均住院天数': 7.5, '手术台数': 16, '门诊量': 1300 },
{ '月份': '4月', '地区': '北京', '医院类型': '三甲医院', '科室': '内科', '用户总数': 180, '费用汇总': 8000, '平均住院天数': 6.5, '手术台数': 22, '门诊量': 1600 },
{ '月份': '5月', '地区': '北京', '医院类型': '三甲医院', '科室': '内科', '用户总数': 200, '费用汇总': 9000, '平均住院天数': 6.2, '手术台数': 25, '门诊量': 1800 },
{ '月份': '6月', '地区': '北京', '医院类型': '三甲医院', '科室': '内科', '用户总数': 250, '费用汇总': 11000, '平均住院天数': 5.8, '手术台数': 30, '门诊量': 2000 },
{ '月份': '1月', '地区': '北京', '医院类型': '三甲医院', '科室': '外科', '用户总数': 80, '费用汇总': 8000, '平均住院天数': 8.5, '手术台数': 45, '门诊量': 800 },
{ '月份': '2月', '地区': '北京', '医院类型': '三甲医院', '科室': '外科', '用户总数': 120, '费用汇总': 9500, '平均住院天数': 8.2, '手术台数': 52, '门诊量': 900 },
{ '月份': '3月', '地区': '北京', '医院类型': '三甲医院', '科室': '外科', '用户总数': 100, '费用汇总': 8500, '平均住院天数': 8.8, '手术台数': 48, '门诊量': 850 },
{ '月份': '4月', '地区': '北京', '医院类型': '三甲医院', '科室': '外科', '用户总数': 150, '费用汇总': 12000, '平均住院天数': 7.8, '手术台数': 60, '门诊量': 1100 },
{ '月份': '5月', '地区': '北京', '医院类型': '三甲医院', '科室': '外科', '用户总数': 180, '费用汇总': 14000, '平均住院天数': 7.5, '手术台数': 68, '门诊量': 1200 },
{ '月份': '6月', '地区': '北京', '医院类型': '三甲医院', '科室': '外科', '用户总数': 220, '费用汇总': 16000, '平均住院天数': 7.2, '手术台数': 75, '门诊量': 1400 },
{ '月份': '1月', '地区': '上海', '医院类型': '三甲医院', '科室': '内科', '用户总数': 80, '费用汇总': 4000, '平均住院天数': 6.8, '手术台数': 12, '门诊量': 1000 },
{ '月份': '2月', '地区': '上海', '医院类型': '三甲医院', '科室': '内科', '用户总数': 120, '费用汇总': 5000, '平均住院天数': 6.5, '手术台数': 15, '门诊量': 1200 },
{ '月份': '3月', '地区': '上海', '医院类型': '三甲医院', '科室': '内科', '用户总数': 100, '费用汇总': 4500, '平均住院天数': 7.0, '手术台数': 13, '门诊量': 1100 },
{ '月份': '4月', '地区': '上海', '医院类型': '三甲医院', '科室': '内科', '用户总数': 150, '费用汇总': 7000, '平均住院天数': 6.2, '手术台数': 18, '门诊量': 1400 },
{ '月份': '5月', '地区': '上海', '医院类型': '三甲医院', '科室': '内科', '用户总数': 180, '费用汇总': 8000, '平均住院天数': 5.9, '手术台数': 22, '门诊量': 1600 },
{ '月份': '6月', '地区': '上海', '医院类型': '三甲医院', '科室': '内科', '用户总数': 220, '费用汇总': 10000, '平均住院天数': 5.6, '手术台数': 28, '门诊量': 1800 },
{ '月份': '1月', '地区': '上海', '医院类型': '二甲医院', '科室': '内科', '用户总数': 60, '费用汇总': 2500, '平均住院天数': 5.5, '手术台数': 8, '门诊量': 600 },
{ '月份': '2月', '地区': '上海', '医院类型': '二甲医院', '科室': '内科', '用户总数': 90, '费用汇总': 3200, '平均住院天数': 5.2, '手术台数': 10, '门诊量': 700 },
{ '月份': '3月', '地区': '上海', '医院类型': '二甲医院', '科室': '内科', '用户总数': 80, '费用汇总': 2800, '平均住院天数': 5.8, '手术台数': 9, '门诊量': 650 },
{ '月份': '4月', '地区': '上海', '医院类型': '二甲医院', '科室': '内科', '用户总数': 120, '费用汇总': 4500, '平均住院天数': 5.0, '手术台数': 12, '门诊量': 800 },
{ '月份': '5月', '地区': '上海', '医院类型': '二甲医院', '科室': '内科', '用户总数': 150, '费用汇总': 5500, '平均住院天数': 4.8, '手术台数': 15, '门诊量': 900 },
{ '月份': '6月', '地区': '上海', '医院类型': '二甲医院', '科室': '内科', '用户总数': 180, '费用汇总': 6500, '平均住院天数': 4.5, '手术台数': 18, '门诊量': 1000 },
{ '月份': '1月', '地区': '广州', '医院类型': '三甲医院', '科室': '内科', '用户总数': 60, '费用汇总': 3000, '平均住院天数': 6.0, '手术台数': 10, '门诊量': 800 },
{ '月份': '2月', '地区': '广州', '医院类型': '三甲医院', '科室': '内科', '用户总数': 90, '费用汇总': 4000, '平均住院天数': 5.8, '手术台数': 12, '门诊量': 900 },
{ '月份': '3月', '地区': '广州', '医院类型': '三甲医院', '科室': '内科', '用户总数': 80, '费用汇总': 3500, '平均住院天数': 6.2, '手术台数': 11, '门诊量': 850 },
{ '月份': '4月', '地区': '广州', '医院类型': '三甲医院', '科室': '内科', '用户总数': 120, '费用汇总': 6000, '平均住院天数': 5.5, '手术台数': 15, '门诊量': 1100 },
{ '月份': '5月', '地区': '广州', '医院类型': '三甲医院', '科室': '内科', '用户总数': 150, '费用汇总': 7000, '平均住院天数': 5.2, '手术台数': 18, '门诊量': 1200 },
{ '月份': '6月', '地区': '广州', '医院类型': '三甲医院', '科室': '内科', '用户总数': 180, '费用汇总': 9000, '平均住院天数': 4.9, '手术台数': 22, '门诊量': 1400 }
],
chartType: 'line' // 将chartType放在message对象内部
// chartType: 'line' // 设置默认图表类型为折线图
};
// 正确的SSE消息格式:status 3(图表数据),type 2(表格数据)
......
<template>
<div class="chart-component">
<ColumnChart
v-if="chartType === CHART_TYPES.COLUMN"
:chart-data="chartData"
:title="title"
:width="width"
:height="height"
/>
<LineChart
v-else-if="chartType === CHART_TYPES.LINE"
:chart-data="chartData"
:title="title"
:width="width"
:height="height"
/>
<div v-else class="chart-error">
不支持的图表类型: {{ chartType }}
<div class="chart-container">
<!-- 图表类型选择器 -->
<div class="chart-controls">
<div class="chart-type-selector">
<!-- <span class="control-label">图表类型</span> -->
<a-select
v-model:value="selectedChartType"
:options="chartTypeOptions"
:getPopupContainer="triggerNode => triggerNode.parentNode"
@change="handleChartTypeChange"
/>
</div>
<!-- 指标和维度选择器(非表格时显示) -->
<div v-if="showChartControls" class="chart-fields-selector">
<div class="field-selector">
<span class="control-label">维度</span>
<a-tooltip placement="top">
<template #title>
<span>维度用于对数据进行分类,如时间、地区等</span>
</template>
<question-circle-outlined/>
</a-tooltip>
<!-- 维度选择器 -->
<a-select
v-model:value="selectedDimensions"
:mode="selectedChartType === CHART_TYPES.PIE ? undefined : 'multiple'"
:maxTagCount="12"
:maxTagTextLength="6"
:dropdownMatchSelectWidth="false"
:getPopupContainer="triggerNode => triggerNode.parentNode"
placeholder="请选择维度"
@change="handleFieldChange"
>
<a-select-option
v-for="dim in availableDimensions"
:key="dim"
:value="dim"
:disabled="
(selectedDimensions.length >= 2 && !selectedDimensions.includes(dim))"
>
{{ dim }}
</a-select-option>
</a-select>
</div>
<div class="field-selector">
<span class="control-label">指标</span>
<a-tooltip placement="top">
<template #title>
<span>指标用于衡量数据,如销售额、用户数等</span>
</template>
<question-circle-outlined/>
</a-tooltip>
<!-- 指标选择器 -->
<a-select
v-model:value="selectedIndexes"
:mode="selectedChartType === CHART_TYPES.PIE ? undefined : 'multiple'"
:maxTagCount="2"
:maxTagTextLength="6"
:dropdownMatchSelectWidth="false"
:getPopupContainer="triggerNode => triggerNode.parentNode"
placeholder="请选择指标"
@change="handleFieldChange"
>
<a-select-option
v-for="index in availableIndexes"
:key="index"
:value="index"
:disabled="(selectedIndexes.length >= 2 && !selectedIndexes.includes(index))"
>
{{ index }}
</a-select-option>
</a-select>
</div>
</div>
</div>
<!-- 图表渲染区域 -->
<div class="chart-render-area">
<!-- 表格渲染 -->
<div v-if="selectedChartType === CHART_TYPES.TABLE" class="table-render">
<div v-html="tableHtml" class="message-table"></div>
</div>
<!-- 柱状图渲染 -->
<ColumnChart
v-else-if="selectedChartType === CHART_TYPES.COLUMN && processedChartData"
:chart-data="processedChartData"
:title="title"
:width="width"
:height="height"
/>
<!-- 折线图渲染 -->
<LineChart
v-else-if="selectedChartType === CHART_TYPES.LINE && processedChartData"
:chart-data="processedChartData"
:title="title"
:width="width"
:height="height"
/>
<!-- 饼图渲染 -->
<PieChart
v-else-if="selectedChartType === CHART_TYPES.PIE && processedChartData"
:chart-data="processedChartData"
:title="title"
:width="width"
:height="height"
/>
<!-- 空数据提示 -->
<div v-else-if="!processedChartData" class="chart-empty">
<div class="empty-icon">📊</div>
<div class="empty-text">请选择维度和指标</div>
</div>
</div>
</div>
</template>
<script lang="ts">
// 图表类型常量(需要在模块作用域中定义,以便在defineProps中使用)
export const CHART_TYPES = {
TABLE: 'table',
COLUMN: 'column',
LINE: 'line',
PIE: 'pie'
} as const;
export type ChartType = typeof CHART_TYPES[keyof typeof CHART_TYPES];
</script>
<script setup lang="ts">
import ColumnChart from './ColumnChart.vue';
import LineChart from './LineChart.vue';
import { ref, computed, watch, onMounted, defineAsyncComponent, type Ref } from 'vue';
import { Select as ASelect, SelectOption as ASelectOption, Tooltip as ATooltip } from 'ant-design-vue';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { generateTableHTML } from './utils/tableTemplate';
// 在模块作用域中定义图表类型常量
const CHART_TYPES = {
COLUMN: 'column',
LINE: 'line'
};
// 定义组件属性
// 类型定义
interface ChartData {
dimFields?: string[];
indexFields?: string[];
rows?: any[];
}
interface ChartTypeOption {
value: string;
label: string;
}
interface Props {
chartData: any;
chartData?: ChartData;
chartType?: number | string;
title?: string;
width?: number | string;
height?: number | string;
}
// 使用异步组件实现按需加载
const ColumnChart = defineAsyncComponent(() => import('./ColumnChart.vue'));
const LineChart = defineAsyncComponent(() => import('./LineChart.vue'));
const PieChart = defineAsyncComponent(() => import('./PieChart.vue'));
// 组件属性定义
const props = withDefaults(defineProps<Props>(), {
chartType: 'column',
chartType: CHART_TYPES.COLUMN,
title: '数据图表',
width: '100%',
height: 'auto'
});
// 响应式数据
const selectedChartType: Ref<ChartType> = ref(CHART_TYPES.TABLE);
const selectedDimensions = ref<string[]>([]);
const selectedIndexes = ref<string[]>([]);
const processedChartData = ref<ChartData | null>(null);
// 计算属性
const showChartControls = computed(() => {
return selectedChartType.value === CHART_TYPES.COLUMN ||
selectedChartType.value === CHART_TYPES.LINE ||
selectedChartType.value === CHART_TYPES.PIE;
});
const availableDimensions = computed(() => {
return props.chartData?.dimFields ?? [];
});
const availableIndexes = computed(() => {
return props.chartData?.indexFields ?? [];
});
const tableHtml = computed(() => {
return props.chartData?.rows ? generateTableHTML(props.chartData.rows) : '';
});
// 修改chartTypeOptions以包含饼图选项
const chartTypeOptions = computed<ChartTypeOption[]>(() => {
return [
{ value: CHART_TYPES.TABLE, label: '图表类型 : 表格' },
{ value: CHART_TYPES.COLUMN, label: '图表类型 : 柱状图' },
{ value: CHART_TYPES.LINE, label: '图表类型 : 折线图' },
{ value: CHART_TYPES.PIE, label: '图表类型 : 饼图' }
];
});
// 方法 - 处理图表类型变化
const handleChartTypeChange = () => {
// 重置选择
selectedDimensions.value = [];
selectedIndexes.value = [];
processedChartData.value = null;
// 如果是表格,直接使用原始数据
if (selectedChartType.value === CHART_TYPES.TABLE) {
processedChartData.value = props.chartData ?? null;
} else {
// 对于柱状图、折线图和饼图,设置默认的维度和指标
if (availableDimensions.value.length > 0) {
selectedDimensions.value = [availableDimensions.value[0]];
}
if (availableIndexes.value.length > 0) {
selectedIndexes.value = [availableIndexes.value[0]];
}
// 处理数据,初始化图表
handleFieldChange();
}
};
// 修改handleFieldChange以支持饼图的选择限制
const handleFieldChange = () => {
// 根据图表类型设置不同的选择限制
const maxDimensions = selectedChartType.value === CHART_TYPES.PIE ? 1 : 2;
const maxIndexes = selectedChartType.value === CHART_TYPES.PIE ? 1 : 2;
// 确保selectedDimensions和selectedIndexes始终是数组格式
if (!Array.isArray(selectedDimensions.value)) {
selectedDimensions.value = selectedDimensions.value ? [selectedDimensions.value] : [];
}
if (!Array.isArray(selectedIndexes.value)) {
selectedIndexes.value = selectedIndexes.value ? [selectedIndexes.value] : [];
}
// 限制最多选择数量
if (selectedDimensions.value.length > maxDimensions) {
selectedDimensions.value = selectedDimensions.value.slice(0, maxDimensions);
}
if (selectedIndexes.value.length > maxIndexes) {
selectedIndexes.value = selectedIndexes.value.slice(0, maxIndexes);
}
// 确保至少选择一个维度和指标(适用于所有图表类型)
if (selectedDimensions.value.length === 0 && availableDimensions.value.length > 0) {
selectedDimensions.value = [availableDimensions.value[0]];
}
if (selectedIndexes.value.length === 0 && availableIndexes.value.length > 0) {
selectedIndexes.value = [availableIndexes.value[0]];
}
// 处理数据,只保留选中的维度和指标
if (selectedDimensions.value.length > 0 && selectedIndexes.value.length > 0 && props.chartData) {
processedChartData.value = {
...props.chartData,
dimFields: selectedDimensions.value,
indexFields: selectedIndexes.value
};
} else {
processedChartData.value = null;
}
};
// 监听chartType属性变化
watch(() => props.chartType, (newType) => {
if (!newType) return;
// 直接使用传入的图表类型
if (Object.values(CHART_TYPES).includes(newType as ChartType)) {
selectedChartType.value = newType as ChartType;
}
// 如果是其他无效类型,保持当前选择
}, { immediate: true });
// 监听原始数据变化
watch(() => props.chartData, (newData) => {
if (!newData) return;
// 如果是表格类型,直接使用数据
if (selectedChartType.value === CHART_TYPES.TABLE) {
processedChartData.value = newData;
return;
}
// 对于图表类型,设置默认的维度和指标
if (newData.dimFields && newData.dimFields.length > 0) {
selectedDimensions.value = [newData.dimFields[0]];
}
if (newData.indexFields && newData.indexFields.length > 0) {
selectedIndexes.value = [newData.indexFields[0]];
}
// 处理数据
handleFieldChange();
}, { immediate: true });
// 组件挂载时设置默认值
onMounted(() => {
if (!props.chartData) return;
// 如果是表格类型,直接使用数据
if (selectedChartType.value === CHART_TYPES.TABLE) {
processedChartData.value = props.chartData;
return;
}
// 对于图表类型,设置默认的维度和指标
if (props.chartData.dimFields && props.chartData.dimFields.length > 0) {
selectedDimensions.value = [props.chartData.dimFields[0]];
}
if (props.chartData.indexFields && props.chartData.indexFields.length > 0) {
selectedIndexes.value = [props.chartData.indexFields[0]];
}
// 处理数据
handleFieldChange();
});
</script>
<style scoped>
.chart-component {
margin: 16px 0;
.chart-container {
border-radius: 8px;
background-color: #fff;
overflow: hidden;
width: 100% !important;
max-width: 100% !important;
}
.chart-controls {
padding: 8px 16px;
background-color: #fafafa;
border-bottom: 1px solid #e8e8e8;
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
min-height: auto;
}
.chart-type-selector {
display: flex;
align-items: center;
margin-right: 6px;
}
.chart-fields-selector {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.field-selector {
display: flex;
align-items: center;
margin-right: 6px;
}
.control-label {
font-size: 14px;
color: #323232;
white-space: nowrap;
line-height: 28px;
margin-right: 4px;
}
.anticon-question-circle{
color: #999;
margin-right: 6px;
}
.chart-error {
padding: 20px;
text-align: center;
color: #f5222d;
background-color: #fff2f0;
border: 1px solid #ffccc7;
/* Ant Design Select 样式调整 */
:deep(.ant-select) {
font-size: 14px;
min-width: 60px;
}
:deep(.ant-select-selector) {
border-radius: 4px;
}
:deep(.ant-select-multiple .ant-select-selector) {
min-height: 32px;
}
/* 移除选中和hover样式 */
:deep(.ant-select-focused:not(.ant-select-disabled).ant-select:not(.ant-select-customize-input) .ant-select-selector) {
box-shadow: none !important;
border-color: #d9d9d9 !important;
}
:deep(.ant-select:not(.ant-select-disabled):hover .ant-select-selector) {
border-color: #d9d9d9 !important;
}
.chart-render-area {
padding: 0px 8px;
min-height: 300px;
font-size: 0;
width: 100% !important;
display: flex;
flex-direction: column;
}
.table-render {
width: 100%;
overflow-x: auto;
}
.chart-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #999;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
}
</style>
\ No newline at end of file
......@@ -87,6 +87,7 @@ const formatNumber = (value: any): string => {
*/
const getColors = () => {
return [
'#5B90F9', '#73DEB3', '#8074FF', '#00CDFE', '#FDDD60', '#FF8B45', '#F478D1', '#FF6F77', '#8D48E4',
'#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2',
'#fa541c', '#eb2f96', '#a0d911', '#2f54eb', '#fa8c16', '#52c41a',
'#13c2c2', '#1890ff', '#722ed1', '#faad14', '#f5222d', '#eb2f96',
......@@ -148,11 +149,32 @@ const getBaseChartConfig = () => ({
left: '3%',
right: '3%',
bottom: '3%',
top: '70px', // 改为固定值,为legend留出空间
top: '70px', // 增加顶部间距,为legend留出更多空间
containLabel: true
}
});
/**
* 基础legend配置 - 始终放在顶部,与折线图保持一致
*/
const getBaseLegendConfig = (data: string[]) => ({
data,
orient: 'horizontal' as const,
left: 'center' as const,
top: '0' as const,
padding: [10, 20, 0, 20], // 调整padding,确保顶部有足够空间
itemGap: 15,
type: 'scroll' as const,
textStyle: {
fontSize: 12
},
pageTextStyle: {
fontSize: 10
},
pageIconSize: 12,
pageButtonItemGap: 5
});
/**
* 基础tooltip配置
*/
......@@ -168,7 +190,7 @@ const getBaseTooltipConfig = () => ({
show: true,
backgroundColor: '#1890ff',
color: '#fff',
fontSize: 12,
fontSize: 14,
padding: [4, 6],
borderRadius: 2
}
......@@ -178,7 +200,7 @@ const getBaseTooltipConfig = () => ({
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 12
fontSize: 14
},
confine: false,
triggerOn: 'mousemove',
......@@ -230,7 +252,7 @@ const getXAxisConfig = (xAxisData: string[]) => ({
interval: 0,
rotate: xAxisData.length > 6 ? 45 : 0,
margin: 8,
fontSize: 12
fontSize: 14
}
});
......@@ -241,7 +263,8 @@ const getYAxisConfig = (name?: string) => ({
type: 'value' as const,
...(name && { name }),
axisLabel: {
formatter: (value: number) => formatNumber(value)
formatter: (value: number) => formatNumber(value),
fontSize: 14
},
axisTick: {
show: true,
......@@ -253,7 +276,7 @@ const getYAxisConfig = (name?: string) => ({
axisLine: {
show: true,
lineStyle: {
width: 1
width: 1,
}
},
splitLine: {
......@@ -273,7 +296,10 @@ const getDualYAxisConfig = (yFields: string[]) => [
...getYAxisConfig(yFields[0]),
position: 'left' as const,
nameTextStyle: {
align: 'right'
align: 'center',
fontSize: 14,
fontWeight: 'bold',
color: '#323232'
},
splitLine: { lineStyle: { type: 'dashed' as const, color: '#ccc', width: 1 } }
},
......@@ -281,7 +307,10 @@ const getDualYAxisConfig = (yFields: string[]) => [
...getYAxisConfig(yFields[1]),
position: 'right' as const,
nameTextStyle: {
align: 'left'
align: 'center',
fontSize: 14,
fontWeight: 'bold',
color: '#323232'
},
splitLine: { lineStyle: { type: 'solid' as const, color: '#e8e8e8', width: 1 } }
}
......@@ -317,7 +346,7 @@ const createSingleColumnOption = (chartConfig: any): echarts.EChartsOption => {
.filter((item: any) => item[chartConfig.groupField] === group)
.map((item: any) => item[chartConfig.yField]);
return getBaseSeriesConfig(group, groupData, colors[index % colors.length]);
return getBaseSeriesConfig(group, groupData, colors[index % colors.length], 0, xAxisData.length);
});
return {
......@@ -325,23 +354,7 @@ const createSingleColumnOption = (chartConfig: any): echarts.EChartsOption => {
tooltip: {
...getBaseTooltipConfig()
},
legend: {
data: groups,
orient: 'horizontal',
left: 'center',
top: '0',
padding: [15, 20, 20, 25], // 增加四周的padding
itemGap: 15,
type: 'scroll',
textStyle: {
fontSize: 12
},
pageTextStyle: {
fontSize: 10
},
pageIconSize: 12,
pageButtonItemGap: 5
},
legend: getBaseLegendConfig(groups),
xAxis: getXAxisConfig(xAxisData),
yAxis: getYAxisConfig(),
series
......@@ -357,7 +370,7 @@ const createSingleColumnOption = (chartConfig: any): echarts.EChartsOption => {
legend: { show: false },
xAxis: getXAxisConfig(xAxisData),
yAxis: getYAxisConfig(),
series: [getBaseSeriesConfig(chartConfig.yField, seriesData, colors[0])]
series: [getBaseSeriesConfig(chartConfig.yField, seriesData, colors[0], 0, xAxisData.length)]
};
}
};
......@@ -377,7 +390,7 @@ const createDualColumnOption = (chartConfig: any): echarts.EChartsOption => {
.filter((item: any) => item[chartConfig.groupField] === group)
.map((item: any) => item[chartConfig.yFields[0]]);
return getBaseSeriesConfig(`${group} - ${chartConfig.yFields[0]}`, groupData, colors[index % colors.length], 0);
return getBaseSeriesConfig(`${group} - ${chartConfig.yFields[0]}`, groupData, colors[index % colors.length], 0, xAxisData.length);
});
const series2 = groups.map((group, index) => {
......@@ -385,7 +398,7 @@ const createDualColumnOption = (chartConfig: any): echarts.EChartsOption => {
.filter((item: any) => item[chartConfig.groupField] === group)
.map((item: any) => item[chartConfig.yFields[1]]);
return getBaseSeriesConfig(`${group} - ${chartConfig.yFields[1]}`, groupData, colors[(index + 3) % colors.length], 1);
return getBaseSeriesConfig(`${group} - ${chartConfig.yFields[1]}`, groupData, colors[(index + 3) % colors.length], 1, xAxisData.length);
});
return {
......@@ -393,23 +406,7 @@ const createDualColumnOption = (chartConfig: any): echarts.EChartsOption => {
tooltip: {
...getBaseTooltipConfig()
},
legend: {
data: [...series1, ...series2].map(s => s.name),
orient: 'horizontal',
left: 'center',
top: '0',
padding: [15, 20, 20, 25], // 增加四周的padding
itemGap: 15,
type: 'scroll',
textStyle: {
fontSize: 12
},
pageTextStyle: {
fontSize: 10
},
pageIconSize: 12,
pageButtonItemGap: 5
},
legend: getBaseLegendConfig([...series1, ...series2].map(s => s.name)),
xAxis: getXAxisConfig(xAxisData),
yAxis: getDualYAxisConfig(chartConfig.yFields),
series: [...series1, ...series2]
......@@ -423,12 +420,12 @@ const createDualColumnOption = (chartConfig: any): echarts.EChartsOption => {
tooltip: {
...getBaseTooltipConfig()
},
legend: { data: [chartConfig.yFields[0], chartConfig.yFields[1]] },
legend: getBaseLegendConfig([chartConfig.yFields[0], chartConfig.yFields[1]]),
xAxis: getXAxisConfig(xAxisData),
yAxis: getDualYAxisConfig(chartConfig.yFields),
series: [
getBaseSeriesConfig(chartConfig.yFields[0], series1Data, colors[0], 0),
getBaseSeriesConfig(chartConfig.yFields[1], series2Data, colors[1], 1)
getBaseSeriesConfig(chartConfig.yFields[0], series1Data, colors[0], 0, xAxisData.length),
getBaseSeriesConfig(chartConfig.yFields[1], series2Data, colors[1], 1, xAxisData.length)
]
};
}
......@@ -506,6 +503,12 @@ const createChart = () => {
return;
}
// 清理之前的图表实例,确保每次数据变化时创建全新实例
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
}
const chartConfig = formatData(props.chartData);
// 根据数据特征自动选择图表类型
......@@ -548,7 +551,7 @@ onUnmounted(() => {
<style scoped>
.message-chart {
margin: 16px 0;
margin: 24px 0 16px;
/* 完全自适应,不设置固定高度 */
/* 确保宽度不超过父容器 */
box-sizing: border-box; /* 包含边框和内边距在宽度计算中 */
......@@ -565,14 +568,13 @@ onUnmounted(() => {
}
.chart-container {
border: 1px solid #e8e8e8;
border-radius: 4px;
/* 正常图表保持宽高比自适应 */
aspect-ratio: 3 / 2; /* 宽高比 3:2 */
min-height: 200px; /* 最小高度确保显示效果 */
width: 100%; /* 确保宽度不超过父容器 */
box-sizing: border-box; /* 包含边框在宽度计算中 */
overflow: hidden; /* 防止内容溢出导致滚动条 */
border: none;
border-radius: 0;
aspect-ratio: 3 / 1.5;
min-height: 200px;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
/* 空状态样式 */
......
......@@ -87,10 +87,11 @@ const formatNumber = (value: any): string => {
*/
const getColors = () => {
return [
'#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272',
'#fc8452', '#9a60b4', '#ea7ccc', '#c23531', '#2f4554', '#61a0a8',
'#d48265', '#91c7ae', '#749f83', '#ca8622', '#bda29a', '#6e7074',
'#546570', '#c4ccd3', '#f4eccc', '#76bca0', '#e69d87', '#8dc1a9'
'#5B90F9', '#73DEB3', '#8074FF', '#00CDFE', '#FDDD60', '#FF8B45', '#F478D1', '#FF6F77', '#8D48E4',
'#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2',
'#fa541c', '#eb2f96', '#a0d911', '#2f54eb', '#fa8c16', '#52c41a',
'#13c2c2', '#1890ff', '#722ed1', '#faad14', '#f5222d', '#eb2f96',
'#a0d911', '#2f54eb', '#fa8c16', '#52c41a', '#13c2c2', '#1890ff'
];
};
......@@ -145,10 +146,10 @@ const getBaseChartConfig = () => ({
animationDuration: 500,
animationEasing: 'cubicOut' as const,
grid: {
left: '3%',
right: '3%',
left: '2%',
right: '2%',
bottom: '3%',
top: '80px', // 增加顶部间距,为legend留出更多空间
top: '70px', // 增加顶部间距,为legend留出更多空间
containLabel: true
}
});
......@@ -199,7 +200,7 @@ const getBaseTooltipConfig = () => ({
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 12
fontSize: 14
},
confine: false,
triggerOn: 'mousemove',
......@@ -251,7 +252,7 @@ const getXAxisConfig = (xAxisData: string[]) => ({
interval: 0,
rotate: xAxisData.length > 6 ? 45 : 0,
margin: 8,
fontSize: 12
fontSize: 14
}
});
......@@ -262,7 +263,8 @@ const getYAxisConfig = (name?: string) => ({
type: 'value' as const,
...(name && { name }),
axisLabel: {
formatter: (value: number) => formatNumber(value)
formatter: (value: number) => formatNumber(value),
fontSize: 14
},
splitLine: {
lineStyle: {
......@@ -281,7 +283,7 @@ const getYAxisConfig = (name?: string) => ({
axisLine: {
show: true,
lineStyle: {
width: 1
width: 1,
}
}
});
......@@ -294,7 +296,10 @@ const getDualYAxisConfig = (yFields: string[]) => [
...getYAxisConfig(yFields[0]),
position: 'left' as const,
nameTextStyle: {
align: 'right'
align: 'center',
fontSize: 14,
fontWeight: 'bold',
color: '#323232'
},
splitLine: { lineStyle: { type: 'dashed' as const, color: '#ccc', width: 1 } }
},
......@@ -302,7 +307,10 @@ const getDualYAxisConfig = (yFields: string[]) => [
...getYAxisConfig(yFields[1]),
position: 'right' as const,
nameTextStyle: {
align: 'left'
align: 'center',
fontSize: 14,
fontWeight: 'bold',
color: '#323232'
},
splitLine: { lineStyle: { type: 'solid' as const, color: '#e8e8e8', width: 1 } }
}
......@@ -497,6 +505,12 @@ const createChart = () => {
return;
}
// 清理之前的图表实例
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
}
const chartConfig = formatData(props.chartData);
// 根据数据特征自动选择图表类型
......@@ -539,7 +553,7 @@ onUnmounted(() => {
<style scoped>
.message-chart {
margin: 16px 0;
margin: 24px 0 16px;
box-sizing: border-box;
}
......@@ -554,16 +568,15 @@ onUnmounted(() => {
}
.chart-container {
border: 1px solid #e8e8e8;
border-radius: 4px;
aspect-ratio: 3 / 2;
border: none;
border-radius: 0;
aspect-ratio: 3 / 1.5;
min-height: 200px;
width: 100%;
box-sizing: border-box;
overflow: visible !important; /* 确保tooltip可以正常显示 */
overflow: visible !important;
position: relative;
z-index: 1;
/* 确保tooltip可以正常显示 */
isolation: isolate;
}
......
<template>
<div class="pie-chart-container">
<div class="chart-title">{{ title }}</div>
<div v-if="isEmpty" class="chart-empty">
<div class="empty-icon">📊</div>
<div class="empty-text">暂无数据</div>
<div class="empty-desc">当前查询条件下没有找到相关数据</div>
</div>
<div v-else ref="chartContainer" class="chart-container"></div>
<div v-if="error" class="chart-error">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef, watch, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
// 定义组件属性 - 与ChartComponent保持一致
interface Props {
chartData: any;
title?: string;
width?: number | string;
height?: number | string;
}
const props = withDefaults(defineProps<Props>(), {
title: '饼图',
width: '100%',
height: 'auto'
});
const chartContainer = ref<HTMLElement>();
// 使用shallowRef避免Vue 3 Proxy代理与ECharts的兼容性问题
const chartInstance = shallowRef<echarts.ECharts | null>(null);
const error = ref<string>('');
const isEmpty = ref<boolean>(false);
// ========== 工具函数抽离 ==========
/**
* 检查数据是否为空
*/
const checkDataEmpty = (data: any): boolean => {
if (!data) return true;
if (!data.rows || !Array.isArray(data.rows)) return true;
if (data.rows.length === 0) return true;
// 检查所有行是否都是空数据
const hasValidData = data.rows.some((row: any) => {
if (data.indexFields && Array.isArray(data.indexFields)) {
return data.indexFields.some((field: string) => {
const value = row[field];
return value !== null && value !== undefined && value !== '' && !isNaN(Number(value));
});
}
return false;
});
return !hasValidData;
};
/**
* 数字格式化工具
*/
const formatNumber = (value: any): string => {
if (value === null || value === undefined || isNaN(value) || value === '') {
return '0';
}
const numValue = Number(value);
if (isNaN(numValue)) return '0';
if (numValue === 0) return '0';
const roundedValue = Math.ceil(numValue * 100) / 100;
if (Math.abs(roundedValue) >= 100000000) {
return `${(roundedValue / 100000000).toFixed(2)}亿`;
} else if (Math.abs(roundedValue) >= 10000) {
return `${(roundedValue / 10000).toFixed(2)}万`;
} else {
return roundedValue.toFixed(2);
}
};
/**
* 获取颜色方案 - 使用ECharts内置颜色方案
*/
const getColors = () => {
return [
'#5B90F9', '#73DEB3', '#8074FF', '#00CDFE', '#FDDD60', '#FF8B45', '#F478D1', '#FF6F77', '#8D48E4',
'#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2',
'#fa541c', '#eb2f96', '#a0d911', '#2f54eb', '#fa8c16', '#52c41a',
'#13c2c2', '#1890ff', '#722ed1', '#faad14', '#f5222d', '#eb2f96',
'#a0d911', '#2f54eb', '#fa8c16', '#52c41a', '#13c2c2', '#1890ff'
];
};
/**
* 数据格式化工具
*/
const formatData = (data: any) => {
if (data && data.rows && Array.isArray(data.rows) && data.dimFields && data.indexFields) {
const { rows, dimFields, indexFields } = data;
if (rows.length === 0) {
return { isEmpty: true, data: [], chartType: 'pie' };
}
const chartConfig: any = { data: rows, chartType: 'pie' };
// 饼图只需要第一个维度字段
if (dimFields.length >= 1) {
chartConfig.dimField = dimFields[0];
}
// 饼图只需要第一个指标字段
if (indexFields.length >= 1) {
chartConfig.indexField = indexFields[0];
}
return chartConfig;
} else {
throw new Error('不支持的数据格式,请使用新的结构化数据格式');
}
};
// ========== 图表配置生成器 ==========
/**
* 基础图表配置
*/
const getBaseChartConfig = () => ({
responsive: true,
animation: true,
animationDuration: 500,
animationEasing: 'cubicOut' as const
});
/**
* 基础tooltip配置 - 简化版本,确保兼容性
*/
const getBaseTooltipConfig = () => ({
trigger: 'item',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#e8e8e8',
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 14
},
formatter: function(params: any) {
if (!params) return '';
const { name, value, percent } = params;
const formattedValue = formatNumber(value);
return `<b>${name}</b><br/>${formattedValue} (${percent}%)`;
}
});
/**
* 创建饼图配置
*/
const createPieOption = (chartConfig: any) => {
const colors = getColors();
const { data, dimField, indexField } = chartConfig;
// 数据聚合:按维度字段分组,对指标字段求和
const aggregatedData: { [key: string]: number } = {};
data.forEach((item: any) => {
const category = item[dimField] || '未知';
const value = Number(item[indexField]) || 0;
if (value > 0) {
if (!aggregatedData[category]) {
aggregatedData[category] = 0;
}
aggregatedData[category] += value;
}
});
// 转换为饼图数据格式
const pieData = Object.entries(aggregatedData).map(([name, value]) => ({
name,
value
}));
// 按值从大到小排序,确保大块在前
pieData.sort((a, b) => b.value - a.value);
// 如果数据项太多,只显示前10个,其他合并为"其他"
if (pieData.length > 10) {
const top10 = pieData.slice(0, 10);
const othersValue = pieData.slice(10).reduce((sum, item) => sum + item.value, 0);
if (othersValue > 0) {
top10.push({
name: '其他',
value: othersValue
});
}
return createPieChartOption(top10);
}
return createPieChartOption(pieData);
};
/**
* 创建实际的饼图配置
*/
const createPieChartOption = (pieData: any[]) => {
const colors = getColors();
if (pieData.length === 0) {
return {
...getBaseChartConfig(),
series: []
};
}
// 默认选中第一个数据项
const selectedData = pieData.length > 0 ? { [pieData[0].name]: true } : {};
// 根据数据项数量调整标签布局
const isManyItems = pieData.length > 6;
// 获取响应式布局配置
const responsiveLayout = getResponsiveLayout(isManyItems);
return {
...getBaseChartConfig(),
tooltip: getBaseTooltipConfig(),
legend: {
orient: 'horizontal',
top: 0,
left: 'center',
type: 'scroll',
textStyle: {
fontSize: 12
},
selected: selectedData,
// 数据项多时调整图例位置
...(isManyItems && {
orient: 'vertical',
right: 5,
top: 'middle',
height: '80%'
})
},
series: [{
name: props.title,
type: 'pie',
radius: responsiveLayout.radius, // 使用响应式半径
center: ['50%', '50%'], // 始终居中
avoidLabelOverlap: true,
itemStyle: {
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
fontSize: 14,
color: '#333',
fontWeight: 'normal',
// 优化标签位置
position: isManyItems ? 'outside' : 'outside',
alignTo: 'edge',
margin: 10,
formatter: (params: any) => {
const { percent } = params;
return `${percent}%`;
}
},
labelLine: {
show: true,
length: isManyItems ? 5 : 8, // 缩短标签线第一段长度
length2: isManyItems ? 5 : 8, // 缩短标签线第二段长度
smooth: true,
lineStyle: {
color: '#999',
width: 0.5, // 减小线宽
type: 'solid'
},
// 优化标签线角度
minTurnAngle: 10,
maxSurfaceAngle: 20
},
emphasis: {
label: {
show: true,
fontSize: isManyItems ? 12 : 14,
fontWeight: 'bold',
color: '#000'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
data: pieData,
color: colors,
selectedMode: 'single',
selectedOffset: 5, // 减小选中偏移量,避免庙边效果
// 使用响应式布局
top: '10%',
bottom: '5%',
left: responsiveLayout.left,
right: responsiveLayout.right
}]
};
};
// ========== 响应式处理逻辑 ==========
// 响应式定时器引用
let resizeTimer: NodeJS.Timeout | null = null;
/**
* 创建响应式处理函数
*/
const createResizeHandler = () => {
return () => {
if (resizeTimer) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(() => {
if (chartInstance.value && chartContainer.value) {
chartInstance.value.resize();
}
}, 100);
};
};
/**
* 初始化图表大小监听
*/
const initChartResizeListener = () => {
const handleResize = createResizeHandler();
// 窗口大小变化监听
window.addEventListener('resize', handleResize);
// ResizeObserver监听
if (typeof ResizeObserver !== 'undefined' && chartContainer.value) {
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(chartContainer.value);
onUnmounted(() => {
resizeObserver.disconnect();
});
}
// 清理函数
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
// 清理定时器
if (resizeTimer) {
clearTimeout(resizeTimer);
resizeTimer = null;
}
});
};
// ========== 响应式布局函数 ==========
/**
* 根据屏幕宽度获取布局配置
*/
const getResponsiveLayout = (isManyItems: boolean) => {
const screenWidth = window.innerWidth;
if (screenWidth < 768) { // 小屏幕
return {
left: '5%',
right: '5%',
radius: isManyItems ? ['25%', '50%'] : ['35%', '60%']
};
} else if (screenWidth < 1200) { // 中等屏幕
return {
left: '25%',
right: '25%',
radius: isManyItems ? ['30%', '55%'] : ['40%', '65%']
};
} else { // 大屏幕
return {
left: '30%',
right: '30%',
radius: isManyItems ? ['35%', '60%'] : ['45%', '70%']
};
}
};
// ========== 主图表创建函数 ==========
/**
* 创建ECharts图表
*/
const createChart = () => {
if (!chartContainer.value) return;
// 检查数据是否为空
isEmpty.value = checkDataEmpty(props.chartData);
if (isEmpty.value) {
// 清理之前的图表实例
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
}
error.value = '';
return;
}
// 清理之前的图表实例
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
}
const chartConfig = formatData(props.chartData);
// 创建ECharts实例
chartInstance.value = echarts.init(chartContainer.value);
// 生成图表配置
const option = createPieOption(chartConfig);
// 设置图表选项
chartInstance.value.setOption(option);
error.value = '';
// 初始化响应式监听
initChartResizeListener();
};
// ========== 生命周期和响应式 ==========
// 监听数据变化
watch(() => props.chartData, createChart);
onMounted(() => {
nextTick(createChart);
});
// 组件卸载时清理
onUnmounted(() => {
if (chartInstance.value) {
chartInstance.value.dispose();
}
});
</script>
<style scoped>
.pie-chart-container {
margin: 16px 0;
box-sizing: border-box;
width: 100% !important;
max-width: 100% !important;
display: flex;
flex-direction: column;
flex: 1;
}
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
width: 100%;
box-sizing: border-box;
text-align: center; /* 标题居中 */
}
.chart-container {
aspect-ratio: 3 / 1; /* 饼图保持正方形比例 */
min-height: 350px;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
overflow: visible !important; /* 确保tooltip可以正常显示 */
position: relative;
z-index: 1;
/* 确保tooltip可以正常显示 */
isolation: isolate;
flex: 1;
}
/* 空状态样式 */
.chart-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 350px;
border: 1px solid #e8e8e8;
border-radius: 4px;
background-color: #fafafa;
color: #999;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.empty-text {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #666;
text-align: center;
max-width: 100%;
}
.empty-desc {
font-size: 14px;
color: #999;
text-align: center;
max-width: 100%;
}
.chart-error {
padding: 10px;
text-align: center;
color: #f5222d;
background-color: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 4px;
margin-top: 10px;
}
</style>
\ No newline at end of file
......@@ -17,7 +17,7 @@
@gray-4: #cccccc;
@gray-5: #999999;
@gray-6: #666666;
@gray-7: #333333;
@gray-7: #323232;
@success-color: #52c41a;
@error-color: #f5222d;
@warning-color: #faad14;
......@@ -70,6 +70,8 @@ li {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 1000px;
margin: 0 auto;
// 居中介绍页面样式
.chat-intro-center {
......@@ -451,15 +453,14 @@ li {
:deep(.message-table) {
width: 100%;
max-width: 100%;
margin: 8px 0;
margin: 4px 0;
// 表格容器
.table-container {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid #f0f0f0;
// 滚动条样式
&::-webkit-scrollbar {
......@@ -488,7 +489,7 @@ li {
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
height: 40px;
height: 35px;
vertical-align: middle;
}
......@@ -520,8 +521,6 @@ li {
text-align: right;
padding-left: 8px;
padding-right: 12px;
font-family: 'Courier New', monospace;
font-weight: 500;
}
.trend-cell {
......@@ -533,11 +532,11 @@ li {
th {
background: linear-gradient(135deg, @primary-color 0%, @primary-hover 100%);
color: @white;
font-weight: 600;
padding: 12px 8px;
font-weight: 400;
padding: 6px 8px;
font-size: 14px;
border-right: 1px solid rgba(255, 255, 255, 0.2);
height: 40px;
height: 35px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
......@@ -595,6 +594,8 @@ li {
.table-footer {
margin-top: 12px;
font-size: 14px;
text-align: right;
margin-right: 8px;
span {
color:@primary-color;
......
......@@ -292,22 +292,22 @@ export class ContentTemplateService {
switch (contentType) {
case 2: // 表格数据
const { rows } = messageContent;
// 表格数据处理
updatedResponse.contentBlocks.push({
content: templateTools?.tableTemplate ? templateTools.tableTemplate(rows) : this.templates.table(rows),
hasThinkBox: false,
thinkContent: '',
thinkBoxExpanded: false,
});
// 图表数据处理
updatedResponse.contentBlocks.push({
content: '',
hasThinkBox: false,
thinkContent: '',
thinkBoxExpanded: false,
chartData: messageContent,
chartType: CHART_TYPES.COLUMN,
});
// // 表格数据处理
// updatedResponse.contentBlocks.push({
// content: templateTools?.tableTemplate ? templateTools.tableTemplate(rows) : this.templates.table(rows),
// hasThinkBox: false,
// thinkContent: '',
// thinkBoxExpanded: false,
// });
// // 图表数据处理
// updatedResponse.contentBlocks.push({
// content: '',
// hasThinkBox: false,
// thinkContent: '',
// thinkBoxExpanded: false,
// chartData: messageContent,
// chartType: CHART_TYPES.COLUMN,
// });
// 图表数据处理
updatedResponse.contentBlocks.push({
content: '',
......
......@@ -47,7 +47,7 @@ export class SSEService {
// 初始化SSE连接
public initSSE(dialogSessionId: string): void {
try {
const url = `${this.config.apiBaseUrl}/aiService/sse/join/${this.config.params?.stage || ''}?app-id=${this.config.params?.appId || ''}&dialog-session-id=${dialogSessionId || ''}`;
const url = `${this.config.apiBaseUrl}${import.meta.env.VITE_SSE_PATH}/sse/join/${this.config.params?.stage || ''}?app-id=${this.config.params?.appId || ''}&dialog-session-id=${dialogSessionId || ''}`;
this.eventSource = new EventSourcePolyfill(url, {
headers: {
Token: this.config.token || '',
......
......@@ -10,6 +10,7 @@ export default defineConfig(({ mode }) => {
// 清理API基础URL,移除末尾的斜杠
const apiBaseUrl = (env.VITE_API_BASE_URL).replace(/\/+$/, '');
const basePath = env.VITE_BASE_PATH;
const agentPath = env.VITE_SSE_PATH;
return {
base: basePath,
......@@ -28,21 +29,21 @@ export default defineConfig(({ mode }) => {
]
},
proxy: {
// 同时配置两种路径的代理
'/pedapi': {
target: apiBaseUrl,
changeOrigin: true,
secure: false,
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.log('代理错误:', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('发送请求到:', options.target);
});
}
},
'/aiService': {
// // 同时配置两种路径的代理
// '/pedapi': {
// target: apiBaseUrl,
// changeOrigin: true,
// secure: false,
// configure: (proxy, options) => {
// proxy.on('error', (err, req, res) => {
// console.log('代理错误:', err);
// });
// proxy.on('proxyReq', (proxyReq, req, res) => {
// console.log('发送请求到:', options.target);
// });
// }
// },
[agentPath]: {
target: apiBaseUrl,
changeOrigin: true,
secure: false,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment