新增 CLAUDE.md 文件以提供项目指导,添加 .claudeignore 文件以排除不必要的文件,更新 pom.xml 版本至 3.9.2,修复多个路径遍历和 SQL 注入漏洞,优化字典翻译切面逻辑,增强文件上传和下载的安全性,新增音频文件类型支持,改进动态数据源的安全校验。
This commit is contained in:
@@ -131,7 +131,7 @@ export const generateMemoryByAppId = (params) => {
|
||||
url: Api.generateMemoryByAppId+'?variables='+ params.variables + '&memoryId='+ params.memoryId,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
timeout: 5 * 60 * 1000,
|
||||
timeout: 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
|
||||
@@ -333,6 +333,7 @@
|
||||
initChartData(params.appId);
|
||||
} else {
|
||||
initChartData();
|
||||
appData.value.metadata = { izDraw: '1', defaultSelect: '0' }
|
||||
quickCommandData.value = [
|
||||
{ name: '请介绍一下JeecgBoot', descr: "请介绍一下JeecgBoot" },
|
||||
{ name: 'JEECG有哪些优势?', descr: "JEECG有哪些优势?" },
|
||||
|
||||
@@ -360,8 +360,8 @@
|
||||
const isThinking = ref<boolean>(false);
|
||||
//是否开启网络搜索
|
||||
const enableSearch = ref<boolean>(false);
|
||||
//是否显示网络搜索按钮(只有千问模型支持)
|
||||
const showWebSearch = ref<boolean>(false);
|
||||
//是否显示网络搜索按钮(默认显示)
|
||||
const showWebSearch = ref<boolean>(true);
|
||||
//模型provider信息
|
||||
const modelProvider = ref<string>('');
|
||||
//是否显示深度思考( 只有deepsee-reason支持 )
|
||||
@@ -565,7 +565,7 @@
|
||||
|
||||
// 停止响应
|
||||
const handleStop = () => {
|
||||
console.log('ai 聊天:::---停止响应');
|
||||
console.log('ai 聊天:::---停止响应, 当前loading:', loading.value, ', 调用栈:', new Error().stack?.split('\n').slice(1,4).join(' <- '));
|
||||
if (loading.value) {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -666,7 +666,7 @@
|
||||
params: param,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
timeout: 5 * 60 * 1000,
|
||||
timeout: 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
@@ -1037,59 +1037,34 @@
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
|
||||
let result = decoder.decode(value, { stream: true });
|
||||
result = buffer + result;
|
||||
const lines = result.split('\n\n');
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
let content = line.replace('data:', '').trim();
|
||||
if(!content){
|
||||
continue;
|
||||
}
|
||||
if(!content.endsWith('}')){
|
||||
buffer = buffer + content;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
try {
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
if(content.indexOf(":::card:::") !== -1){
|
||||
content = content.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(content);
|
||||
await renderText(parse,conversationId,text,options).then((res)=>{
|
||||
text = res.returnText;
|
||||
conversationId = res.conversationId;
|
||||
});
|
||||
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
} catch (error) {
|
||||
console.log('Error parsing update:', error);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
|
||||
}else{
|
||||
if(!line){
|
||||
continue;
|
||||
}
|
||||
if(!line.endsWith('}')){
|
||||
buffer = buffer + line;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
try {
|
||||
if(line.indexOf(":::card:::") !== -1){
|
||||
line = line.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(line);
|
||||
await renderText(parse, conversationId, text, options).then((res) => {
|
||||
text = res.returnText;
|
||||
conversationId = res.conversationId;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error parsing update:', error);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
buffer += result;
|
||||
// 按SSE协议用 \n\n 分割完整事件,最后一个元素可能不完整需保留在buffer中
|
||||
const parts = buffer.split('\n\n');
|
||||
buffer = parts.pop() || '';
|
||||
for (let part of parts) {
|
||||
if (!part || !part.trim()) {
|
||||
continue;
|
||||
}
|
||||
let content = part.startsWith('data:') ? part.replace('data:', '').trim() : part.trim();
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
try {
|
||||
if(content.indexOf(":::card:::") !== -1){
|
||||
content = content.replace(/\s+/g, '');
|
||||
}
|
||||
let parse = JSON.parse(content);
|
||||
await renderText(parse, conversationId, text, options).then((res) => {
|
||||
text = res.returnText;
|
||||
conversationId = res.conversationId;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('JSON解析失败, content长度:', content.length, ', error:', error);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-11-05---for: 如果是断线重连并且文本为空,需要移出前面两条会话---
|
||||
if(!text && isReConnect && chatData.value.length >1){
|
||||
@@ -1118,7 +1093,7 @@
|
||||
const result = await defHttp.get({ url: '/airag/chat/receive/' + requestId ,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
timeout: 5 * 60 * 1000
|
||||
timeout: 60 * 60 * 1000
|
||||
}, { isTransformResponse: false }).catch(async (err)=>{
|
||||
loading.value = false;
|
||||
localStorage.removeItem('chat_requestId_' + uuid.value);
|
||||
@@ -1240,29 +1215,28 @@
|
||||
|
||||
//是否显示绘图工具
|
||||
showDraw.value = metadata.izDraw === '1';
|
||||
//是否选中生成图片
|
||||
enableDraw.value = metadata.izDraw === '1';
|
||||
//是否选中生成图片(defaultSelect 为 0 时默认不选中)
|
||||
const defaultSelect = metadata.defaultSelect || metadata.izDraw;
|
||||
enableDraw.value = defaultSelect === '1';
|
||||
|
||||
drawModelId.value = metadata.drawModelId;
|
||||
|
||||
if (metadata && metadata.modelInfo) {
|
||||
modelProvider.value = metadata.modelInfo.provider || '';
|
||||
modelName.value = metadata.modelInfo.modelName || '';
|
||||
// 只有千问模型支持网络搜索
|
||||
showWebSearch.value = modelProvider.value === 'QWEN';
|
||||
showThink.value = modelName.value === 'deepseek-reasoner';
|
||||
} else {
|
||||
showWebSearch.value = false;
|
||||
showThink.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析模型信息失败', e);
|
||||
showWebSearch.value = false;
|
||||
showThink.value = false;
|
||||
enableDraw.value = false;
|
||||
}
|
||||
} else {
|
||||
showWebSearch.value = false;
|
||||
showThink.value = false;
|
||||
showDraw.value = false;
|
||||
enableDraw.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || (props.presetQuestion && props.presetQuestion.length>0)">
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || props.error || (props.presetQuestion && props.presetQuestion.length>0)">
|
||||
<div class="avatar" v-if="showAvatar !== 'no'">
|
||||
<img v-if="inversion === 'user'" :src="avatar()" />
|
||||
<img v-else :src="getAiImg()" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="parsedText != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : (isOnlyImage ? 'chatgpt-image' : 'chatgpt')]" ref="textRef">
|
||||
<div v-if="parsedText != '' || error" class="textWrap" :class="[inversion === 'user' ? 'self' : (isOnlyImage ? 'chatgpt-image' : 'chatgpt')]" ref="textRef">
|
||||
<div v-if="inversion != 'user'" :style="{ width: getIsMobile? screenWidth : 'auto' }">
|
||||
<div ref="markdownBodyRef" class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="parsedText" />
|
||||
<template v-if="showRefKnow">
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
sqlPageExecute = '/airag/mcp/database/sqlPageExecute',
|
||||
sqlExportXls = '/airag/mcp/database/sqlExportXls',
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页执行 SQL 查询
|
||||
*/
|
||||
export function sqlPageExecute(params: { sql: string; dbSource?: string; pageNo: number; pageSize: number }) {
|
||||
return defHttp.post<Recordable>(
|
||||
{
|
||||
url: Api.sqlPageExecute,
|
||||
params: {
|
||||
sql: params.sql,
|
||||
dbSourceKey: params.dbSource || '',
|
||||
pageNo: params.pageNo,
|
||||
pageSize: params.pageSize,
|
||||
},
|
||||
},
|
||||
{ isTransformResponse: false }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出图表原始数据为 Excel
|
||||
*/
|
||||
export function sqlExportXls(params: { sql: string; dbSource?: string; columns?: Recordable }) {
|
||||
return defHttp.post(
|
||||
{
|
||||
url: Api.sqlExportXls,
|
||||
params: {
|
||||
sql: params.sql,
|
||||
dbSourceKey: params.dbSource || '',
|
||||
columns: params.columns || {},
|
||||
},
|
||||
responseType: 'blob',
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
{ isTransformResponse: false, isReturnNativeResponse: true }
|
||||
);
|
||||
}
|
||||
@@ -9,32 +9,42 @@
|
||||
<span v-else>模型返回的图表渲染格式不正确,请优化提示词或重新尝试。</span>
|
||||
</div>
|
||||
<div v-else class="ai-chat-chart__body">
|
||||
<!-- 折线图 -->
|
||||
<LineMulti v-if="resolvedType === 'line'" v-bind="lineProps"/>
|
||||
<!-- 柱状图 -->
|
||||
<BarMulti v-else-if="resolvedType === 'bar'" v-bind="barProps"/>
|
||||
<!-- 饼图 -->
|
||||
<Pie v-else-if="resolvedType === 'pie'" v-bind="pieProps"/>
|
||||
<!-- 多列柱状图 -->
|
||||
<BarMulti v-else-if="resolvedType === 'multibar'" v-bind="multiBarProps"/>
|
||||
<!-- 多行折线图 -->
|
||||
<LineMulti v-else-if="resolvedType === 'multiline'" v-bind="multiLineProps"/>
|
||||
<!-- 折柱图 -->
|
||||
<BarAndLine v-else-if="resolvedType === 'barline'" v-bind="barLineProps"/>
|
||||
<!-- 面积图 -->
|
||||
<SingleLine v-else-if="resolvedType === 'area'" v-bind="areaLineProps"/>
|
||||
<!-- 雷达图 -->
|
||||
<Radar v-else-if="resolvedType === 'radar'" v-bind="radarProps"/>
|
||||
<!-- 仪表盘 -->
|
||||
<Gauge v-else-if="resolvedType === 'gauge'" v-bind="gaugeProps"/>
|
||||
|
||||
<Tabs v-model:activeKey="activeTab" size="small" class="ai-chat-chart__tabs">
|
||||
<!-- 主图表 Tab -->
|
||||
<TabPane :key="resolvedType" :tab="getChartLabel(resolvedType)">
|
||||
<ChartBody :chartType="resolvedType" />
|
||||
</TabPane>
|
||||
<!-- 可替代图表 Tabs -->
|
||||
<TabPane v-for="alt in validAltTypes" :key="alt" :tab="getChartLabel(alt)">
|
||||
<ChartBody :chartType="alt" />
|
||||
</TabPane>
|
||||
<!-- 数据 Tab -->
|
||||
<TabPane v-if="hasSql" key="__data__" tab="数据">
|
||||
<div class="ai-chat-chart__data-toolbar">
|
||||
<a-button size="small" :loading="exportLoading" @click="handleExport">
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
</div>
|
||||
<BasicTable
|
||||
v-bind="tableBindings"
|
||||
:loading="tableLoading"
|
||||
size="small"
|
||||
class="ai-chat-chart__table"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ChartType } from './types';
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
import type { BasicColumn } from '/@/components/Table';
|
||||
import { computed, defineComponent, h, ref, watch, watchEffect } from 'vue';
|
||||
import { Tabs, TabPane, message } from 'ant-design-vue';
|
||||
import { DownloadOutlined } from '@ant-design/icons-vue';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import LineMulti from '/@/components/chart/LineMulti.vue';
|
||||
import BarMulti from '/@/components/chart/BarMulti.vue';
|
||||
import Pie from '/@/components/chart/Pie.vue';
|
||||
@@ -42,11 +52,12 @@ import Radar from '/@/components/chart/Radar.vue';
|
||||
import Gauge from '/@/components/chart/Gauge.vue';
|
||||
import BarAndLine from '/@/components/chart/BarAndLine.vue';
|
||||
import SingleLine from '/@/components/chart/SingleLine.vue';
|
||||
import { sqlPageExecute, sqlExportXls } from './ChartRender.api';
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 图表配置字符串,示例:
|
||||
* {"type":"bar","data":[{"x":"数据项1","y":100},{"x":"数据项2","y":80}]}
|
||||
* {"type":"bar","altTypes":["line","pie"],"data":[...],"sql":"SELECT ...","dbSource":"","columns":{"field1":"标题1"}}
|
||||
*/
|
||||
data: {
|
||||
type: String,
|
||||
@@ -58,6 +69,39 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
/** 图表类型中文名映射 */
|
||||
const chartTypeLabels: Record<string, string> = {
|
||||
bar: '柱状图',
|
||||
line: '折线图',
|
||||
pie: '饼图',
|
||||
radar: '雷达图',
|
||||
gauge: '仪表盘',
|
||||
barline: '折柱图',
|
||||
multibar: '多列柱状图',
|
||||
multiline: '多行折线图',
|
||||
area: '面积图',
|
||||
};
|
||||
|
||||
/** 获取图表类型的中文标签 */
|
||||
function getChartLabel(type: string): string {
|
||||
return chartTypeLabels[type] || type;
|
||||
}
|
||||
|
||||
/** 当前激活的 Tab */
|
||||
const activeTab = ref<string>('');
|
||||
/** 表格加载状态 */
|
||||
const tableLoading = ref(false);
|
||||
/** 导出加载状态 */
|
||||
const exportLoading = ref(false);
|
||||
/** 表格数据 */
|
||||
const tableData = ref<Recordable[]>([]);
|
||||
/** 表格列定义 */
|
||||
const tableColumns = ref<BasicColumn[]>([]);
|
||||
/** 数据总条数 */
|
||||
const tableTotal = ref(0);
|
||||
/** 是否已首次加载过数据 */
|
||||
const dataLoaded = ref(false);
|
||||
|
||||
/**
|
||||
* 解析失败或类型错误的提示文本。
|
||||
*/
|
||||
@@ -85,7 +129,6 @@ const parsedConfig = computed<Recordable>(() => {
|
||||
/**
|
||||
* 支持的图表类型集合。
|
||||
*/
|
||||
// 支持的类型覆盖常见图表,名称与提示词保持宽松映射
|
||||
const supportedTypes: ChartType[] = ['bar', 'line', 'pie', 'radar', 'gauge', 'barline', 'multibar', 'multiline', 'area'];
|
||||
|
||||
/**
|
||||
@@ -115,6 +158,25 @@ const resolvedType = computed<ChartType>(() => {
|
||||
return '';
|
||||
});
|
||||
|
||||
/** 解析可替代图表类型列表(过滤掉无效类型和主类型重复) */
|
||||
const validAltTypes = computed<ChartType[]>(() => {
|
||||
const altTypes = (parsedConfig.value as any).altTypes;
|
||||
if (!Array.isArray(altTypes)) {
|
||||
return [];
|
||||
}
|
||||
const primary = resolvedType.value;
|
||||
return altTypes
|
||||
.map((t: string) => String(t).toLowerCase() as ChartType)
|
||||
.filter((t: ChartType) => t !== primary && supportedTypes.includes(t));
|
||||
});
|
||||
|
||||
/** 初始化 activeTab 为主图表类型 */
|
||||
watchEffect(() => {
|
||||
if (resolvedType.value && !activeTab.value) {
|
||||
activeTab.value = resolvedType.value;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 当类型不被支持时,给出错误提示。
|
||||
*/
|
||||
@@ -149,6 +211,171 @@ const hasData = computed<boolean>(() => {
|
||||
return Array.isArray(rawData.value) && rawData.value.length > 0;
|
||||
});
|
||||
|
||||
/** 是否包含可查询的 SQL */
|
||||
const hasSql = computed<boolean>(() => {
|
||||
const sql = (parsedConfig.value as any).sql;
|
||||
return typeof sql === 'string' && sql.trim().length > 0;
|
||||
});
|
||||
|
||||
/** 从配置中提取 SQL */
|
||||
const chartSql = computed<string>(() => {
|
||||
return ((parsedConfig.value as any).sql || '').trim();
|
||||
});
|
||||
|
||||
/** 从配置中提取数据源 */
|
||||
const chartDbSource = computed<string>(() => {
|
||||
return ((parsedConfig.value as any).dbSource || '').trim();
|
||||
});
|
||||
|
||||
/** 从配置中提取列标题映射 */
|
||||
const chartColumns = computed<Recordable>(() => {
|
||||
return (parsedConfig.value as any).columns || {};
|
||||
});
|
||||
|
||||
/**
|
||||
* 使用 BasicTable 组件
|
||||
*/
|
||||
const [registerTable, { setPagination }] = useTable({
|
||||
columns: tableColumns,
|
||||
dataSource: tableData,
|
||||
pagination: {
|
||||
pageSize: 10,
|
||||
current: 1,
|
||||
total: 0,
|
||||
},
|
||||
showIndexColumn: false,
|
||||
canResize: false,
|
||||
bordered: true,
|
||||
onChange: handleTableChange,
|
||||
});
|
||||
|
||||
/** 表格绑定属性 */
|
||||
const tableBindings = computed(() => ({
|
||||
onRegister: registerTable,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 加载表格数据
|
||||
*/
|
||||
async function loadTableData(pageNo = 1, pageSize = 10) {
|
||||
if (!chartSql.value) {
|
||||
return;
|
||||
}
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const res = await sqlPageExecute({
|
||||
sql: chartSql.value,
|
||||
dbSource: chartDbSource.value,
|
||||
pageNo,
|
||||
pageSize,
|
||||
});
|
||||
if (res.success && res.result) {
|
||||
const { records = [], total = 0 } = res.result;
|
||||
tableData.value = records;
|
||||
tableTotal.value = total;
|
||||
// 根据返回数据动态生成列(使用 columns 映射中文标题)
|
||||
if (records.length > 0) {
|
||||
tableColumns.value = buildColumns(records[0]);
|
||||
}
|
||||
setPagination({
|
||||
current: pageNo,
|
||||
pageSize,
|
||||
total,
|
||||
});
|
||||
} else {
|
||||
tableData.value = [];
|
||||
tableTotal.value = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载图表原始数据失败', e);
|
||||
tableData.value = [];
|
||||
tableTotal.value = 0;
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据行的 key 动态生成 BasicTable 列定义,优先使用 columns 映射的中文标题
|
||||
*/
|
||||
function buildColumns(row: Recordable): BasicColumn[] {
|
||||
const columnMap = chartColumns.value;
|
||||
return Object.keys(row).map((key) => ({
|
||||
title: columnMap[key] || key,
|
||||
dataIndex: key,
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格分页变更处理
|
||||
*/
|
||||
function handleTableChange(pagination: any) {
|
||||
loadTableData(pagination.current, pagination.pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出全部数据
|
||||
*/
|
||||
async function handleExport() {
|
||||
if (!chartSql.value) {
|
||||
return;
|
||||
}
|
||||
exportLoading.value = true;
|
||||
try {
|
||||
const response = await sqlExportXls({
|
||||
sql: chartSql.value,
|
||||
dbSource: chartDbSource.value,
|
||||
columns: chartColumns.value,
|
||||
});
|
||||
if (!response || !response.data) {
|
||||
message.warning('导出失败');
|
||||
return;
|
||||
}
|
||||
const data = response.data;
|
||||
// 检查是否为错误响应(JSON 格式)
|
||||
if (data.type && data.type.indexOf('json') !== -1) {
|
||||
const text = await data.text();
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (!json.success) {
|
||||
message.warning('导出失败:' + (json.message || '未知错误'));
|
||||
return;
|
||||
}
|
||||
} catch (_e) {
|
||||
// 非 JSON,继续下载
|
||||
}
|
||||
}
|
||||
const url = window.URL.createObjectURL(new Blob([data], { type: 'application/vnd.ms-excel' }));
|
||||
const link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
link.setAttribute('download', '数据导出.xls');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error('导出失败', e);
|
||||
message.error('导出失败');
|
||||
} finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到数据 Tab 时首次加载
|
||||
*/
|
||||
watch(activeTab, (val) => {
|
||||
if (val === '__data__' && !dataLoaded.value && hasSql.value) {
|
||||
dataLoaded.value = true;
|
||||
loadTableData();
|
||||
}
|
||||
});
|
||||
|
||||
// ======================== 图表数据转换 ========================
|
||||
|
||||
/**
|
||||
* 将原始数据标准化为多序列图表所需的结构。
|
||||
*/
|
||||
@@ -201,7 +428,7 @@ function buildMultiSeriesItem(item: Recordable): Recordable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析系列名称,优先使用 series,其次使用 type,最后回退为“数据”。
|
||||
* 解析系列名称,优先使用 series,其次使用 type,最后回退为"数据"。
|
||||
*/
|
||||
function resolveSeriesName(item: Recordable): string {
|
||||
if (item && item.series !== undefined) {
|
||||
@@ -295,114 +522,84 @@ const barLineSeriesData = computed<Recordable[]>(() => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 折线图的渲染属性。
|
||||
*/
|
||||
const lineProps = computed(() => {
|
||||
return {
|
||||
type: 'line',
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: multiSeriesData.value,
|
||||
};
|
||||
});
|
||||
// ======================== 根据类型获取图表 props ========================
|
||||
|
||||
/**
|
||||
* 柱状图的渲染属性。
|
||||
* 根据图表类型返回对应的渲染属性
|
||||
*/
|
||||
const barProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: multiSeriesData.value,
|
||||
};
|
||||
});
|
||||
function getChartPropsForType(type: ChartType): Recordable {
|
||||
switch (type) {
|
||||
case 'line':
|
||||
return { type: 'line', height: '360px', width: '100%', chartData: multiSeriesData.value };
|
||||
case 'bar':
|
||||
return { height: '360px', width: '100%', chartData: multiSeriesData.value };
|
||||
case 'pie':
|
||||
return { height: '360px', width: '100%', chartData: pieSeriesData.value };
|
||||
case 'multibar':
|
||||
return { height: '360px', width: '100%', chartData: multiSeriesData.value, option: parsedConfig.value.option || {} };
|
||||
case 'multiline':
|
||||
return { type: 'line', height: '360px', width: '100%', chartData: multiSeriesData.value, option: parsedConfig.value.option || {} };
|
||||
case 'barline':
|
||||
return { height: '360px', width: '100%', chartData: barLineSeriesData.value, customColor: (parsedConfig.value as any).colors || [], option: parsedConfig.value.option || {} };
|
||||
case 'area':
|
||||
return { type: 'line', height: '360px', width: '100%', chartData: areaSeriesData.value, option: { ...(parsedConfig.value.option || {}), areaStyle: {} } };
|
||||
case 'radar':
|
||||
return { height: '420px', width: '100%', chartData: radarSeriesData.value, option: parsedConfig.value.option || {} };
|
||||
case 'gauge':
|
||||
return { height: '360px', width: '100%', chartData: gaugeData.value, option: parsedConfig.value.option || {}, seriesColor: (parsedConfig.value as any).seriesColor || undefined };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 饼图的渲染属性。
|
||||
* 根据图表类型返回对应的组件
|
||||
*/
|
||||
const pieProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: pieSeriesData.value,
|
||||
};
|
||||
});
|
||||
function getChartComponentForType(type: ChartType) {
|
||||
switch (type) {
|
||||
case 'line':
|
||||
return LineMulti;
|
||||
case 'bar':
|
||||
return BarMulti;
|
||||
case 'pie':
|
||||
return Pie;
|
||||
case 'multibar':
|
||||
return BarMulti;
|
||||
case 'multiline':
|
||||
return LineMulti;
|
||||
case 'barline':
|
||||
return BarAndLine;
|
||||
case 'area':
|
||||
return SingleLine;
|
||||
case 'radar':
|
||||
return Radar;
|
||||
case 'gauge':
|
||||
return Gauge;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多列柱状图配置,与 bar 相同但允许区分类型前缀。
|
||||
* 内部图表渲染组件,根据 chartType 动态渲染对应图表
|
||||
*/
|
||||
const multiBarProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: multiSeriesData.value,
|
||||
option: parsedConfig.value.option || {},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 多行折线图配置。
|
||||
*/
|
||||
const multiLineProps = computed(() => {
|
||||
return {
|
||||
type: 'line',
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: multiSeriesData.value,
|
||||
option: parsedConfig.value.option || {},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 面积折线图配置,开启面积样式。
|
||||
*/
|
||||
const areaLineProps = computed(() => {
|
||||
return {
|
||||
type: 'line',
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: areaSeriesData.value,
|
||||
option: { ...(parsedConfig.value.option || {}), areaStyle: {} },
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 雷达图配置。
|
||||
*/
|
||||
const radarProps = computed(() => {
|
||||
return {
|
||||
height: '420px',
|
||||
width: '100%',
|
||||
chartData: radarSeriesData.value,
|
||||
option: parsedConfig.value.option || {},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 仪表盘配置。
|
||||
*/
|
||||
const gaugeProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: gaugeData.value,
|
||||
option: parsedConfig.value.option || {},
|
||||
seriesColor: (parsedConfig.value as any).seriesColor || undefined,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 折柱图配置,支持自定义颜色。
|
||||
*/
|
||||
const barLineProps = computed(() => {
|
||||
return {
|
||||
height: '360px',
|
||||
width: '100%',
|
||||
chartData: barLineSeriesData.value,
|
||||
customColor: (parsedConfig.value as any).colors || [],
|
||||
option: parsedConfig.value.option || {},
|
||||
};
|
||||
const ChartBody = defineComponent({
|
||||
props: {
|
||||
chartType: {
|
||||
type: String as () => ChartType,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(bodyProps) {
|
||||
return () => {
|
||||
const comp = getChartComponentForType(bodyProps.chartType as ChartType);
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
const chartProps = getChartPropsForType(bodyProps.chartType as ChartType);
|
||||
return h(comp, chartProps);
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -410,7 +607,7 @@ const barLineProps = computed(() => {
|
||||
|
||||
.ai-chat-chart {
|
||||
width: 100%;
|
||||
min-width: 360px;
|
||||
min-width: 420px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
@@ -424,4 +621,22 @@ const barLineProps = computed(() => {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ai-chat-chart__tabs {
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-chat-chart__data-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ai-chat-chart__table {
|
||||
:deep(.ant-table-wrapper) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -232,6 +232,7 @@ function formatData(value: unknown): string {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.tool-exec-loading,
|
||||
@@ -371,6 +372,7 @@ function formatData(value: unknown): string {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 340px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,13 @@
|
||||
prologue: '你好,我是 AI绘图智能体。',
|
||||
presetQuestion: '[{"key":1,"descr":"请生成一张具有日本风格的动漫成年女孩。","update":true}, {"key":2,"descr":"请生成一幅中国神话故事中,手持武器的哪吒形象。","update":true}]',
|
||||
metadata:"{\"izDraw\":\"1\"}"
|
||||
},
|
||||
{
|
||||
id: '1993651187913981953',
|
||||
name: '商品导购',
|
||||
icon: 'https://minio.jeecg.com/otatest/shoppingGuide_1769754188966.png',
|
||||
prologue: '向要了解哪一款产品,我可以给你介绍。',
|
||||
presetQuestion: '[{"key":1,"sort":1,"descr":"HIP 0603T Series","update":false},{"key":2,"sort":2,"descr":"CHIP 1206HC Series","update":false},{"key":3,"sort":3,"descr":"BRICK 1032ST Series","update":true}]',
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
resetField();
|
||||
await getPromptList();
|
||||
setModalProps({
|
||||
title: '选择提示词',
|
||||
height: 600,
|
||||
bodyStyle: { padding: '24px' },
|
||||
});
|
||||
@@ -200,6 +201,8 @@
|
||||
*/
|
||||
async function handleOk() {
|
||||
if (selectedPrompt.value) {
|
||||
// select 传递完整对象,供调用方按需取用 id/name/content
|
||||
emit('select', selectedPrompt.value);
|
||||
emit('ok', selectedPrompt.value.content);
|
||||
} else {
|
||||
emit('ok');
|
||||
|
||||
@@ -428,6 +428,20 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol">
|
||||
<div style="display: flex;margin-top: 10px">
|
||||
<div style="margin-left: 2px">显示工具调用过程:</div>
|
||||
<a-switch
|
||||
v-model:checked="showToolProcessChecked"
|
||||
:disabled="isRelease"
|
||||
checked-children="开"
|
||||
un-checked-children="关"
|
||||
@change="handleShowToolProcessChange"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-row>
|
||||
<a-row v-if="izDrawChecked" class="mt-10">
|
||||
<a-col :span="24">
|
||||
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.drawModelId">
|
||||
@@ -622,6 +636,8 @@
|
||||
const multiSessionChecked = ref<boolean>(true);
|
||||
//开启会话能力
|
||||
const izDrawChecked = ref<boolean>(false);
|
||||
//显示工具调用过程
|
||||
const showToolProcessChecked = ref<boolean>(true);
|
||||
// 是否已发布
|
||||
const isRelease = ref<boolean>(false);
|
||||
//对话设置弹窗ref
|
||||
@@ -1208,6 +1224,11 @@
|
||||
}else{
|
||||
izDrawChecked.value = false;
|
||||
}
|
||||
if(metadata.value?.showToolProcess != null){
|
||||
showToolProcessChecked.value = metadata.value.showToolProcess === '1';
|
||||
}else{
|
||||
showToolProcessChecked.value = true;
|
||||
}
|
||||
if(metadata.value?.drawModelId){
|
||||
formState.drawModelId = metadata.value.drawModelId;
|
||||
}
|
||||
@@ -1611,6 +1632,20 @@
|
||||
}
|
||||
//================================================ end 开启绘画 ========================================================
|
||||
|
||||
//================================================ begin 显示工具调用过程 =========================================================
|
||||
/**
|
||||
* 显示工具调用过程开关回调
|
||||
*/
|
||||
function handleShowToolProcessChange(checked){
|
||||
if(checked){
|
||||
metadata.value.showToolProcess = "1";
|
||||
}else{
|
||||
metadata.value.showToolProcess = "0";
|
||||
}
|
||||
formState.metadata = JSON.stringify(metadata.value);
|
||||
}
|
||||
//================================================ end 显示工具调用过程 ========================================================
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
@@ -1705,6 +1740,8 @@
|
||||
izDrawChecked,
|
||||
handleDrawChange,
|
||||
handleDrawModelChange,
|
||||
showToolProcessChecked,
|
||||
handleShowToolProcessChange,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
import { Icon } from '/src/components/Icon';
|
||||
import { Button, Checkbox, Switch, Popconfirm } from 'ant-design-vue';
|
||||
import { JVxeTypes, JVxeColumn, JVxeTableInstance } from '/src/components/jeecg/JVxeTable/types';
|
||||
import { JVxeTable } from '/src/components/jeecg/JVxeTable';
|
||||
import { useMessage } from '/src/hooks/web/useMessage';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -47,7 +46,6 @@
|
||||
components: {
|
||||
BasicModal,
|
||||
Icon,
|
||||
JVxeTable,
|
||||
AButton: Button,
|
||||
ACheckbox: Checkbox,
|
||||
ASwitch: Switch,
|
||||
|
||||
Reference in New Issue
Block a user