Commit 403f09ee authored by 水玉婷's avatar 水玉婷
Browse files

feat:图表改为页面,支持分页及排序

parent 59fea6c1
...@@ -106,9 +106,8 @@ ...@@ -106,9 +106,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { tableTemplate } from './utils/tableTemplate';
import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './utils/markdownTemplate'; import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './utils/markdownTemplate';
import { SendOutlined, UserOutlined, UpOutlined, DownOutlined } from '@ant-design/icons-vue'; import { SendOutlined, UserOutlined, UpOutlined, DownOutlined } from '@ant-design/icons-vue';
import defaultAvatar from '@/assets/logo.png'; import defaultAvatar from '@/assets/logo.png';
...@@ -216,7 +215,6 @@ const handleSSEMessage = (data: SSEData) => { ...@@ -216,7 +215,6 @@ const handleSSEMessage = (data: SSEData) => {
currentBlockIndex.value, currentBlockIndex.value,
false, false,
{ {
tableTemplate,
markdownTemplate, markdownTemplate,
isLastBlockMarkdown, isLastBlockMarkdown,
getLastMarkdownBlockIndex, getLastMarkdownBlockIndex,
...@@ -493,7 +491,6 @@ onBeforeUnmount(() => { ...@@ -493,7 +491,6 @@ onBeforeUnmount(() => {
// 处理历史记录数据 // 处理历史记录数据
const processHistoryData = (dataArray: any[]) => { const processHistoryData = (dataArray: any[]) => {
return templateService.processHistoryData(dataArray, { return templateService.processHistoryData(dataArray, {
tableTemplate,
markdownTemplate, markdownTemplate,
isLastBlockMarkdown, isLastBlockMarkdown,
getLastMarkdownBlockIndex, getLastMarkdownBlockIndex,
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
<!-- 图表类型选择器 --> <!-- 图表类型选择器 -->
<div class="chart-controls"> <div class="chart-controls">
<div class="chart-type-selector"> <div class="chart-type-selector">
<!-- <span class="control-label">图表类型</span> -->
<a-select <a-select
v-model:value="selectedChartType" v-model:value="selectedChartType"
:options="chartTypeOptions" :options="chartTypeOptions"
...@@ -81,7 +80,10 @@ ...@@ -81,7 +80,10 @@
<div class="chart-render-area"> <div class="chart-render-area">
<!-- 表格渲染 --> <!-- 表格渲染 -->
<template v-if="selectedChartType === CHART_TYPES.TABLE"> <template v-if="selectedChartType === CHART_TYPES.TABLE">
<div v-html="tableHtml" class="message-chart"></div> <TableComponent
:table-data="props.chartData"
:config="{ title: title }"
/>
</template> </template>
<!-- 柱状图渲染 --> <!-- 柱状图渲染 -->
...@@ -136,9 +138,7 @@ export type ChartType = typeof CHART_TYPES[keyof typeof CHART_TYPES]; ...@@ -136,9 +138,7 @@ export type ChartType = typeof CHART_TYPES[keyof typeof CHART_TYPES];
import { ref, computed, watch, onMounted, defineAsyncComponent, type Ref } from '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 { Select as ASelect, SelectOption as ASelectOption, Tooltip as ATooltip } from 'ant-design-vue';
import { QuestionCircleOutlined } from '@ant-design/icons-vue'; import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { generateTableHTML } from './utils/tableTemplate'; import TableComponent from './TableComponent.vue';
// 类型定义 // 类型定义
interface ChartData { interface ChartData {
...@@ -194,14 +194,6 @@ const availableIndexes = computed(() => { ...@@ -194,14 +194,6 @@ const availableIndexes = computed(() => {
return props.chartData?.indexFields ?? []; return props.chartData?.indexFields ?? [];
}); });
const tableHtml = computed(() => {
return props.chartData ? generateTableHTML({
...props.chartData,
dimFields: props.chartData.dimFields || [],
indexFields: props.chartData.indexFields || []
}) : '';
});
// 修改chartTypeOptions以包含饼图选项 // 修改chartTypeOptions以包含饼图选项
const chartTypeOptions = computed<ChartTypeOption[]>(() => { const chartTypeOptions = computed<ChartTypeOption[]>(() => {
return [ return [
...@@ -415,7 +407,6 @@ onMounted(() => { ...@@ -415,7 +407,6 @@ onMounted(() => {
.chart-render-area { .chart-render-area {
padding: 0px 8px; padding: 0px 8px;
min-height: 300px;
font-size: 0; font-size: 0;
width: 100% !important; width: 100% !important;
display: flex; display: flex;
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, shallowRef, onMounted, onUnmounted, watch, nextTick } from 'vue'; import { ref, shallowRef, onMounted, onUnmounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { formatNumber, getColors } from './utils/utils';
// 定义组件属性 // 定义组件属性
interface Props { interface Props {
...@@ -59,42 +60,6 @@ const checkDataEmpty = (data: any): boolean => { ...@@ -59,42 +60,6 @@ const checkDataEmpty = (data: any): boolean => {
return !hasValidData; 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'
];
};
/** /**
* 数据格式化工具 * 数据格式化工具
*/ */
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, shallowRef, watch, onMounted, onUnmounted, nextTick } from 'vue'; import { ref, shallowRef, watch, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { formatNumber, getColors } from './utils/utils';
// 定义组件属性 - 与ChartComponent保持一致 // 定义组件属性 - 与ChartComponent保持一致
interface Props { interface Props {
...@@ -59,42 +60,6 @@ const checkDataEmpty = (data: any): boolean => { ...@@ -59,42 +60,6 @@ const checkDataEmpty = (data: any): boolean => {
return !hasValidData; 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'
];
};
/** /**
* 数据格式化工具 * 数据格式化工具
*/ */
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, shallowRef, watch, onMounted, onUnmounted, nextTick } from 'vue'; import { ref, shallowRef, watch, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { formatNumber, getColors } from './utils/utils';
// 定义组件属性 - 与ChartComponent保持一致 // 定义组件属性 - 与ChartComponent保持一致
interface Props { interface Props {
...@@ -59,42 +60,6 @@ const checkDataEmpty = (data: any): boolean => { ...@@ -59,42 +60,6 @@ const checkDataEmpty = (data: any): boolean => {
return !hasValidData; 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'
];
};
/** /**
* 数据格式化工具 * 数据格式化工具
*/ */
......
<template>
<div class="message-chart">
<!-- 表格标题 -->
<div class="table-title">{{ tableTitle }}</div>
<!-- Ant Design Table -->
<div class="table-container">
<a-table
:columns="tableColumns"
:data-source="processedTableData"
size="middle"
:scroll="{ x: 'max-content' }"
:pagination="{
pageSize: 15,
total: processedTableData.value?.length || 0
}"
class="data-table"
>
<!-- 自定义单元格渲染 -->
<template #bodyCell="{ column, text, record }">
<div :class="getCellClass(column.dataIndex)">
{{ renderCellContent(column.dataIndex, text) }}
</div>
</template>
</a-table>
</div>
<!-- 表格页脚 -->
<div v-if="showFooter" class="table-footer">
<span>{{ processedTableData?.length || 0 }}</span> 条记录
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Table as ATable } from 'ant-design-vue';
import { formatNumber } from './utils/utils';
// 类型定义
interface TableData {
[key: string]: any;
}
interface TableConfig {
showFooter?: boolean;
title?: string;
}
interface Props {
tableData: TableData[] | any;
config?: TableConfig;
}
// 组件属性定义
const props = withDefaults(defineProps<Props>(), {
config: () => ({
showFooter: true,
title: '数据表格',
dimFields: [],
indexFields: []
})
});
// 处理表格数据
const processedTableData = computed(() => {
if (!props.tableData) return [];
// 处理传入完整数据结构的情况
if (Array.isArray(props.tableData.rows)) {
return props.tableData.rows;
} else if (Array.isArray(props.tableData)) {
return props.tableData;
}
return [];
});
// 表格标题
const tableTitle = computed(() => {
if (props.tableData?.title) {
return props.tableData.title;
}
return props.config.title || '数据表格';
});
// 是否显示页脚
const showFooter = computed(() => {
return props.config.showFooter !== false;
});
// 获取表头列配置
const tableColumns = computed(() => {
if (processedTableData.value.length === 0) return [];
const headers = Object.keys(processedTableData.value[0]);
return headers.map(header => ({
title: header,
dataIndex: header,
key: header,
align: getColumnAlign(header),
width: getColumnWidth(header),
ellipsis: true,
sorter: isNumericColumn(header) ? (a, b) => {
const aValue = parseFloat(a[header]);
const bValue = parseFloat(b[header]);
return aValue - bValue;
} : undefined
}));
});
// 判断列是否为数字列
const isNumericColumn = (header: string) => {
// 直接从传入的数据中获取indexFields
const indexFields = props.tableData?.indexFields || [];
// 只通过indexFields来判断数字列
return indexFields.includes(header);
};
// 判断列是否为趋势列
const isTrendColumn = (header: string) => {
return header.toLowerCase().includes('趋势') || header.toLowerCase().includes('trend');
};
// 获取列对齐方式
const getColumnAlign = (header: string) => {
if (isTrendColumn(header)) {
return 'center';
} else if (isNumericColumn(header)) {
return 'right';
} else {
return 'left';
}
};
// 获取列宽度
const getColumnWidth = (header: string) => {
if (isNumericColumn(header)) {
return 120;
} else if (isTrendColumn(header)) {
return 80;
} else {
return 'auto';
}
};
// 获取单元格样式类
const getCellClass = (header: string) => {
if (isTrendColumn(header)) {
return 'trend-cell';
} else if (isNumericColumn(header)) {
return 'numeric-cell';
} else {
return 'text-cell';
}
};
// 渲染单元格内容
const renderCellContent = (header: string, value: any) => {
// 如果是趋势列,根据up/down值显示箭头图标
if (isTrendColumn(header)) {
const trendValue = String(value).toLowerCase().trim();
if (trendValue === 'up' || trendValue === '上升' || trendValue === '上涨') {
return '';
} else if (trendValue === 'down' || trendValue === '下降' || trendValue === '下跌') {
return '';
}
}
// 如果是数字列,进行格式化
if (isNumericColumn(header)) {
return formatNumber(value);
}
// 其他列保持原样
return value;
};
</script>
<style scoped>
.message-chart {
width: 100%;
max-width: 100%;
margin: 24px 0 16px;
}
.table-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #323232;
width: 100%;
box-sizing: border-box;
text-align: center;
}
.table-container {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-bottom: 1px solid #f0f0f0;
}
.data-table {
width: auto;
min-width: 100%;
border-collapse: collapse;
background-color: white;
table-layout: auto;
}
.data-table {
th {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
height: 35px;
vertical-align: middle;
}
td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
height: 35px;
vertical-align: middle;
}
}
.table-footer {
margin-top: 12px;
font-size: 14px;
text-align: right;
margin-right: 8px;
span {
color: #5B8AFE;
font-weight: bold;
}
}
/* 单元格样式 */
.trend-cell {
text-align: center;
font-weight: bold;
}
.numeric-cell {
text-align: right;
}
.text-cell {
text-align: left;
}
/* 趋势箭头样式 */
.trend-cell:has(span:contains('↑')) {
color: #f5222d; /* 红色表示上涨 */
}
.trend-cell:has(span:contains('↓')) {
color: #52c41a; /* 绿色表示下跌 */
}
/* 表格样式 */
:deep(.ant-table) {
width: 100%;
.ant-table-thead > tr > th {
background: linear-gradient(135deg, #5B8AFE 0%, #4a7df5 100%);
color: white;
font-weight: 400;
padding: 6px 8px;
font-size: 14px;
border-right: 1px solid rgba(255, 255, 255, 0.2);
height: 35px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
&:last-child {
border-right: none;
}
.ant-table-column-sorter-up.active, .ant-table-column-sorter-down.active {
color: white;
}
}
.ant-table-tbody > tr > td {
padding: 10px 8px;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
color: #323232;
height: 35px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
min-width: 80px;
}
/* 奇偶行样式 */
.ant-table-tbody > tr:nth-child(odd) > td {
background-color: #f8faff;
}
.ant-table-tbody > tr:nth-child(even) > td {
background-color: white;
}
.ant-table-tbody > tr:hover > td {
background-color: #f0f5ff;
}
.ant-table-tbody > tr:last-child > td {
border-bottom: none;
}
}
</style>
\ No newline at end of file
...@@ -539,163 +539,7 @@ li { ...@@ -539,163 +539,7 @@ li {
} }
} }
// =============================================
// 表格消息样式
// =============================================
:deep(.message-table) {
width: 100%;
max-width: 100%;
margin: 24px 0 16px;
// 表格容器
.table-container {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-bottom: 1px solid #f0f0f0;
// 滚动条样式
&::-webkit-scrollbar {
height: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a8a8a8;
}
}
}
// 确保表头不换行
.data-table {
th {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
height: 35px;
vertical-align: middle;
}
td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
height: 35px;
vertical-align: middle;
}
}
.data-table {
width: auto;
min-width: 100%;
border-collapse: collapse;
background-color: @white;
table-layout: auto;
// 列类型样式
.text-cell {
text-align: left;
padding-left: 12px;
padding-right: 8px;
}
.numeric-cell {
text-align: right;
padding-left: 8px;
padding-right: 12px;
}
.trend-cell {
text-align: center;
padding-left: 8px;
padding-right: 8px;
}
th {
background: linear-gradient(135deg, @primary-color 0%, @primary-hover 100%);
color: @white;
font-weight: 400;
padding: 6px 8px;
font-size: 14px;
border-right: 1px solid rgba(255, 255, 255, 0.2);
height: 35px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
&:last-child {
border-right: none;
}
}
td {
padding: 10px 8px;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
color: @gray-7;
height: 35px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
min-width: 80px;
}
// 奇偶行样式
tr:nth-child(odd) td {
background-color: @blue-light-2;
}
tr:nth-child(even) td {
background-color: @white;
}
tr:hover td {
background-color: @blue-light-1;
}
tr:last-child td {
border-bottom: none;
}
}
// 趋势箭头样式
.trend-up {
color: @success-color;
font-weight: bold;
font-size: 16px;
}
.trend-down {
color: @error-color;
font-weight: bold;
font-size: 16px;
}
.table-footer {
margin-top: 12px;
font-size: 14px;
text-align: right;
margin-right: 8px;
span {
color:@primary-color;
font-weight: bold;
}
}
}
// ============================================= // =============================================
// iframe消息样式 // iframe消息样式
......
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { tableTemplate } from './tableTemplate';
import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './markdownTemplate'; import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './markdownTemplate';
// 图表类型常量定义 // 图表类型常量定义
...@@ -15,7 +14,6 @@ const CHART_TYPES = { ...@@ -15,7 +14,6 @@ const CHART_TYPES = {
text: (content: string) => string; text: (content: string) => string;
thinking: (content: string) => string; thinking: (content: string) => string;
error: (content: string) => string; error: (content: string) => string;
table: (tableData: any) => string;
markdown: (content: any, isStreaming?: boolean) => string; markdown: (content: any, isStreaming?: boolean) => string;
option: (optionData: any) => string; option: (optionData: any) => string;
iframe: (iframeData: any) => string; iframe: (iframeData: any) => string;
...@@ -147,21 +145,6 @@ export class ContentTemplateService { ...@@ -147,21 +145,6 @@ export class ContentTemplateService {
return `<div class="message-success">${content}</div>`; return `<div class="message-success">${content}</div>`;
}, },
// 表格模板 - 使用独立的表格模板工具
table: (tableData: any) => {
// 如果tableData包含dimFields和indexFields,则传递这些配置
if (tableData && tableData.dimFields && tableData.indexFields) {
return tableTemplate({
rows: tableData.rows || [],
title: tableData.title || '',
dimFields: tableData.dimFields || [],
indexFields: tableData.indexFields || []
});
}
// 否则使用默认调用
return tableTemplate(tableData);
},
// Markdown模板 - 使用独立的markdown模板工具 // Markdown模板 - 使用独立的markdown模板工具
markdown: (content: any, isStreaming: boolean = false) => { markdown: (content: any, isStreaming: boolean = false) => {
return markdownTemplate(content, isStreaming); return markdownTemplate(content, isStreaming);
...@@ -262,7 +245,6 @@ export class ContentTemplateService { ...@@ -262,7 +245,6 @@ export class ContentTemplateService {
currentBlockIndex: number, currentBlockIndex: number,
isHistoryData = false, isHistoryData = false,
templateTools?: { templateTools?: {
tableTemplate: (tableData: any) => string;
markdownTemplate: (content: any, isStreaming?: boolean) => string; markdownTemplate: (content: any, isStreaming?: boolean) => string;
isLastBlockMarkdown: (blocks: MessageBlock[]) => boolean; isLastBlockMarkdown: (blocks: MessageBlock[]) => boolean;
getLastMarkdownBlockIndex: (blocks: MessageBlock[]) => number; getLastMarkdownBlockIndex: (blocks: MessageBlock[]) => number;
...@@ -302,24 +284,7 @@ export class ContentTemplateService { ...@@ -302,24 +284,7 @@ export class ContentTemplateService {
case 3: // 图表数据 case 3: // 图表数据
if (updatedResponse) { if (updatedResponse) {
switch (contentType) { switch (contentType) {
case 2: // 表格数据 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({ updatedResponse.contentBlocks.push({
content: '', content: '',
...@@ -571,7 +536,6 @@ export class ContentTemplateService { ...@@ -571,7 +536,6 @@ export class ContentTemplateService {
public processHistoryData( public processHistoryData(
dataArray: any[], dataArray: any[],
templateTools?: { templateTools?: {
tableTemplate: (tableData: any) => string;
markdownTemplate: (content: any, isStreaming?: boolean) => string; markdownTemplate: (content: any, isStreaming?: boolean) => string;
isLastBlockMarkdown: (blocks: MessageBlock[]) => boolean; isLastBlockMarkdown: (blocks: MessageBlock[]) => boolean;
getLastMarkdownBlockIndex: (blocks: MessageBlock[]) => number; getLastMarkdownBlockIndex: (blocks: MessageBlock[]) => number;
......
/**
* 表格模板工具类
* 用于生成和渲染数据表格
*/
// 表格数据接口
export interface TableData {
[key: string]: any;
}
// 表格配置接口
export interface TableConfig {
showFooter?: boolean;
title?: string; // 表格标题
dimFields?: string[]; // 维度字段列表,与数据结构中的dimFields保持一致
indexFields?: string[]; // 指标字段列表,这些字段需要特殊数字处理
}
/**
* 生成表格HTML
* @param tableData 表格数据数组或完整数据结构
* @param config 表格配置
* @returns 表格HTML字符串
*/
export const generateTableHTML = (tableData: TableData[] | any, config: TableConfig = {}): string => {
// 处理传入完整数据结构的情况
let rows: TableData[] = [];
let finalConfig: TableConfig = { ...config };
if (tableData && typeof tableData === 'object') {
// 如果传入的是完整数据结构(包含rows、dimFields、indexFields等)
if (Array.isArray(tableData.rows)) {
rows = tableData.rows;
// 从数据结构中提取配置
if (tableData.title) {
finalConfig.title = tableData.title;
}
if (tableData.dimFields) {
finalConfig.dimFields = tableData.dimFields;
}
if (tableData.indexFields) {
finalConfig.indexFields = tableData.indexFields;
}
} else if (Array.isArray(tableData)) {
// 如果传入的是纯数组,直接使用
rows = tableData;
}
}
const { showFooter = true, title = '数据表格', indexFields = [], dimFields = [] } = finalConfig;
// 处理空数据
if (!Array.isArray(rows) || rows.length === 0) {
return `
<div class="message-table">
<div class="table-title">${title}</div>
<div class="table-empty">暂无数据</div>
</div>`;
}
// 动态生成表头 - 使用第一条数据的键名
const headers = Object.keys(rows[0]);
// 判断列是否为数字列
const isNumericColumn = (header: string) => {
// 如果指定了indexFields,优先使用indexFields判断
if (indexFields.length > 0) {
return indexFields.includes(header);
}
// 默认逻辑:根据数据内容判断
return rows.some(row => {
const value = row[header];
return typeof value === 'number' || (!isNaN(parseFloat(value)) && isFinite(value));
});
};
// 数字格式化函数 - 万/亿格式化,保留两位小数,四舍五入
const formatNumber = (value: any) => {
if (value === null || value === undefined || value === '') return '';
const num = parseFloat(value);
if (isNaN(num)) return value;
if (num >= 100000000) {
// 亿级别
const result = Math.round((num / 100000000) * 100) / 100;
return result.toFixed(2) + '亿';
} else if (num >= 10000) {
// 万级别
const result = Math.round((num / 10000) * 100) / 100;
return result.toFixed(2) + '';
} else {
// 小于万
return Math.round(num).toString();
}
};
// 处理单元格内容,特别处理趋势列和数字列
const renderCellContent = (header: string, value: any) => {
// 如果是趋势列,根据up/down值显示箭头图标
if (header.toLowerCase().includes('趋势') || header.toLowerCase().includes('trend')) {
const trendValue = String(value).toLowerCase().trim();
if (trendValue === 'up' || trendValue === '上升' || trendValue === '上涨') {
return `<span class="trend-up">↑</span>`;
} else if (trendValue === 'down' || trendValue === '下降' || trendValue === '下跌') {
return `<span class="trend-down">↓</span>`;
}
}
// 如果是数字列,进行格式化
if (isNumericColumn(header)) {
return formatNumber(value);
}
// 其他列保持原样
return value;
};
// 判断列是否为趋势列
const isTrendColumn = (header: string) => {
return header.toLowerCase().includes('趋势') || header.toLowerCase().includes('trend');
};
// 为不同列类型添加对应的样式类
const getCellClass = (header: string) => {
if (isTrendColumn(header)) {
return 'trend-cell'; // 趋势列居中
} else if (isNumericColumn(header)) {
return 'numeric-cell'; // 数字列右对齐
} else {
return 'text-cell'; // 文字列左对齐
}
};
return `
<div class="message-table">
<div class="table-title">${title}</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
${headers.map(header => `<th class="${getCellClass(header)}">${header}</th>`).join('')}
</tr>
</thead>
<tbody>
${rows.map(row => `
<tr>
${headers.map(header => `
<td class="${getCellClass(header)}">
${renderCellContent(header, row[header])}
</td>
`).join('')}
</tr>
`).join('')}
</tbody>
</table>
</div>
${showFooter ? `<div class="table-footer">共 <span>${rows.length}</span> 条记录</div>` : ''}
</div>
`;
};
/**
* 简化的表格生成函数(兼容旧版本)
* @param tableData 表格数据
* @returns 表格HTML字符串
*/
export const tableTemplate = (tableData: any): string => {
return generateTableHTML(tableData);
};
export default {
generateTableHTML,
tableTemplate
};
\ No newline at end of file
/**
* 数字格式化工具函数
* 功能:
* 1. 直接截断而非四舍五入
* 2. 最多保留两位小数,自动移除末尾0
* 3. 智能判断整数和小数显示方式
* 4. 支持亿、万单位自动转换
*/
export const formatNumber = (value: any): string => {
if (value === null || value === undefined || value === '') return '';
const num = parseFloat(value);
if (isNaN(num)) return value;
if (num >= 100000000) {
// 亿级别
const result = num / 100000000;
const decimalPart = result % 1;
if (decimalPart === 0) {
return result.toString() + '亿';
} else {
// 最多保留两位小数
const fixedResult = result.toFixed(2);
// 移除末尾的0
return fixedResult.replace(/\.?0+$/, '') + '亿';
}
} else if (num >= 10000) {
// 万级别
const result = num / 10000;
const decimalPart = result % 1;
if (decimalPart === 0) {
return result.toString() + '';
} else {
// 最多保留两位小数
const fixedResult = result.toFixed(2);
// 移除末尾的0
return fixedResult.replace(/\.?0+$/, '') + '';
}
} else {
// 小于万
const decimalPart = num % 1;
if (decimalPart === 0) {
return num.toString();
} else {
// 最多保留两位小数
const fixedResult = num.toFixed(2);
// 移除末尾的0
return fixedResult.replace(/\.?0+$/, '');
}
}
};
/**
* 获取颜色方案 - 自定义颜色方案
*/
export 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'
];
};
\ No newline at end of file
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