feat: ai对答

main
许宏杰 1 month ago
parent 6ca6ceeb30
commit 66b0a45740

7926
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -38,6 +38,7 @@
"iconify-icon": "^1.0.8", "iconify-icon": "^1.0.8",
"keymaster": "^1.6.2", "keymaster": "^1.6.2",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"moment": "^2.30.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"naive-ui": "2.40.3", "naive-ui": "2.40.3",
"pinia": "^2.0.13", "pinia": "^2.0.13",

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getAiMsg(data) {
return request({
url: '',
method: 'post',
data
})
}

@ -11,6 +11,7 @@ import uimIcons from '@iconify/json/json/uim.json'
import lineMdIcons from '@iconify/json/json/line-md.json' import lineMdIcons from '@iconify/json/json/line-md.json'
import wiIcons from '@iconify/json/json/wi.json' import wiIcons from '@iconify/json/json/wi.json'
// 引入全局样式 // 引入全局样式
import '@/styles/pages/index.scss' import '@/styles/pages/index.scss'
// 引入动画 // 引入动画

@ -1,4 +1,4 @@
import { PieCommonConfig } from './PieCommon/index' import { PieCommonConfig } from './PieCommon/index'
import { PieCircleConfig } from './PieCircle/index' import { PieCircleConfig } from './PieCircle/index'
export default [PieCommonConfig, PieCircleConfig] export default [PieCommonConfig ]//PieCircleConfig

@ -0,0 +1,79 @@
const sessionCache = {
set (key, value) {
if (!sessionStorage) {
return
}
if (key != null && value != null) {
sessionStorage.setItem(key, value)
}
},
get (key) {
if (!sessionStorage) {
return null
}
if (key == null) {
return null
}
return sessionStorage.getItem(key)
},
setJSON (key, jsonValue) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue))
}
},
getJSON (key) {
const value = this.get(key)
if (value != null) {
return JSON.parse(value)
}
return null
},
remove (key) {
sessionStorage.removeItem(key);
}
}
const localCache = {
set (key, value) {
if (!localStorage) {
return
}
if (key != null && value != null) {
localStorage.setItem(key, value)
}
},
get (key) {
if (!localStorage) {
return null
}
if (key == null) {
return null
}
return localStorage.getItem(key)
},
setJSON (key, jsonValue) {
if (jsonValue != null) {
this.set(key, JSON.stringify(jsonValue))
}
},
getJSON (key) {
const value = this.get(key)
if (value != null) {
return JSON.parse(value)
}
return null
},
remove (key) {
localStorage.removeItem(key);
}
}
export default {
/**
* 会话级缓存
*/
session: sessionCache,
/**
* 本地缓存
*/
local: localCache
}

@ -0,0 +1,6 @@
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
'default': '系统未知错误,请反馈给管理员'
}

@ -0,0 +1,123 @@
import axios from 'axios'
import { useMessage } from 'naive-ui'
import errorCode from '@/utils/errorCode'
import { tansParams, } from '@/utils/ruoyi'
import cache from '@/utils/cache'
const message = useMessage()
// 是否显示重新登录
export let isRelogin = { show: false };
axios.defaults.headers['Content-Type'] = 'multipart/form-data'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: '/ai',
// 超时60秒
timeout: 60000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
if (requestSize >= limitSize) {
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制无法进行防重复提交验证。')
return config;
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
message.error(
msg
)
return Promise.reject(new Error(msg))
} else if (code === 601) {
message.error(
msg
)
return Promise.reject(new Error(msg))
} else if (code !== 200) {
message.error(
msg
)
return Promise.reject('error')
} else {
return Promise.resolve(res.data)
}
},
error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
message.error(
message
)
return Promise.reject(error)
}
)
export default service

@ -0,0 +1,228 @@
/**
* 通用js方法封装处理
* Copyright (c) 2019 ruoyi
*/
// 日期格式化
export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) {
return null
}
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
// 表单重置
export function resetForm(refName) {
if (this.$refs[refName]) {
this.$refs[refName].resetFields();
}
}
// 添加日期范围
export function addDateRange(params, dateRange, propName) {
let search = params;
search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
dateRange = Array.isArray(dateRange) ? dateRange : [];
if (typeof (propName) === 'undefined') {
search.params['beginTime'] = dateRange[0];
search.params['endTime'] = dateRange[1];
} else {
search.params['begin' + propName] = dateRange[0];
search.params['end' + propName] = dateRange[1];
}
return search;
}
// 回显数据字典
export function selectDictLabel(datas, value) {
if (value === undefined) {
return "";
}
var actions = [];
Object.keys(datas).some((key) => {
if (datas[key].value == ('' + value)) {
actions.push(datas[key].label);
return true;
}
})
if (actions.length === 0) {
actions.push(value);
}
return actions.join('');
}
// 回显数据字典(字符串、数组)
export function selectDictLabels(datas, value, separator) {
if (value === undefined || value.length ===0) {
return "";
}
if (Array.isArray(value)) {
value = value.join(",");
}
var actions = [];
var currentSeparator = undefined === separator ? "," : separator;
var temp = value.split(currentSeparator);
Object.keys(value.split(currentSeparator)).some((val) => {
var match = false;
Object.keys(datas).some((key) => {
if (datas[key].value == ('' + temp[val])) {
actions.push(datas[key].label + currentSeparator);
match = true;
}
})
if (!match) {
actions.push(temp[val] + currentSeparator);
}
})
return actions.join('').substring(0, actions.join('').length - 1);
}
// 字符串格式化(%s )
export function sprintf(str) {
var args = arguments, flag = true, i = 1;
str = str.replace(/%s/g, function () {
var arg = args[i++];
if (typeof arg === 'undefined') {
flag = false;
return '';
}
return arg;
});
return flag ? str : '';
}
// 转换字符串undefined,null等转化为""
export function parseStrEmpty(str) {
if (!str || str == "undefined" || str == "null") {
return "";
}
return str;
}
// 数据合并
export function mergeRecursive(source, target) {
for (var p in target) {
try {
if (target[p].constructor == Object) {
source[p] = mergeRecursive(source[p], target[p]);
} else {
source[p] = target[p];
}
} catch (e) {
source[p] = target[p];
}
}
return source;
};
/**
* 构造树型结构数据
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
*/
export function handleTree(data, id, parentId, children) {
let config = {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children'
};
var childrenListMap = {};
var tree = [];
for (let d of data) {
let id = d[config.id];
childrenListMap[id] = d;
if (!d[config.childrenList]) {
d[config.childrenList] = [];
}
}
for (let d of data) {
let parentId = d[config.parentId]
let parentObj = childrenListMap[parentId]
if (!parentObj) {
tree.push(d);
} else {
parentObj[config.childrenList].push(d)
}
}
return tree;
}
/**
* 参数处理
* @param {*} params 参数
*/
export function tansParams(params) {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && value !== "" && typeof (value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
let params = propName + '[' + key + ']';
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
}
} else {
result += part + encodeURIComponent(value) + "&";
}
}
}
return result
}
// 返回项目路径
export function getNormalPath(p) {
if (p.length === 0 || !p || p == 'undefined') {
return p
};
let res = p.replace('//', '/')
if (res[res.length - 1] === '/') {
return res.slice(0, res.length - 1)
}
return res
}
// 验证是否为blob格式
export function blobValidate(data) {
return data.type !== 'application/json'
}

@ -0,0 +1,149 @@
import * as echarts from 'echarts';
interface ChartData {
xData: string[]; // X轴分类数据如['一月', '二月', '三月']
yData: number[]; // 单一数值数组对应X轴每个分类的值
seriesName?: string; // 系列名称(默认'数据系列'
color?: string; // 柱状图颜色(默认主题蓝色)
}
export class BarChart {
private chart: echarts.ECharts | null = null;
private dom: HTMLElement;
private xData: string[];
private yData: number[];
private seriesName: string = '数据系列';
private currentColor: string = '#1890ff'; // 默认颜色
constructor(containerId: string, chartData: ChartData) {
this.dom = document.getElementById(containerId)!;
this.xData = chartData.xData;
this.yData = chartData.yData;
this.seriesName = chartData.seriesName || this.seriesName;
this.currentColor = chartData.color || this.currentColor;
this.initChart();
}
private initChart(): void {
this.chart = echarts.init(this.dom);
this.setChartOptions();
window.addEventListener('resize', () => this.chart?.resize());
}
private setChartOptions(): void {
const option= {
// tooltip: {
// trigger: 'axis',
// formatter: (params) => `
// <div style="font-size: 14px; font-weight: 500">${params[0].name}</div>
// <div style="display: flex; align-items: center; gap: 4px; margin: 4px 0">
// <span style="width: 8px; height: 8px; border-radius: 2px; background: ${this.currentColor}"></span>
// <span>${this.seriesName}: ${params[0].value}</span>
// </div>
// `
// },
// legend: {
// data: [this.seriesName],
// top: '16px',
// left: 'center',
// textStyle: { color: '#666', fontSize: 14 }
// },
grid: {
left: '0%',
right: '0%',
bottom: '5%',
containLabel: true
},
xAxis: {
type: 'category',
data: this.xData,
axisTick: { alignWithLabel: true, length: 8, color: '#e0e0e0' },
axisLine: { lineStyle: { color: '#C3CAD9', width: 1 } },
textStyle: { color: '#C3CAD9', fontSize: 12 }
},
yAxis: {
type: 'value',
name: '',
// nameTextStyle: { color: 'red', fontSize: 14, margin: 20 },
splitLine: { lineStyle: { type: 'dashed', color: '#f0f0f0' } },
// textStyle: { color: '#666', fontSize: 14 }
axisLabel:{
color: '#C3CAD9',
fontSize: 12,
}
},
series: [{
name: this.seriesName,
type: 'bar',
data: this.yData,
barWidth: '25%', // 柱体宽度
barRadius: 4, // 柱体圆角
itemStyle: {
color: this.currentColor,
emphasis: {
shadowBlur: 8,
shadowColor: 'rgba(0, 0, 0, 0.2)'
}
},
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 12,
formatter: '{c}' // 显示数值
}
}]
};
this.chart?.setOption(option);
}
/**
*
* @param index /RGB/HSL
*/
changeColorStyle(index: number): void {
if (!this.chart) {
console.warn('未读取到元素!')
return
}
if (index === 0) {
this.currentColor = '#3071EC'
} else if (index === 1) {
this.currentColor = '#6790EA'
} else if (index === 2) {
this.currentColor = '#0A50FF'
}
this.chart.setOption({
series: [
{
itemStyle: { color: this.currentColor }
}
]
});
}
/**
*
* @param newData
*/
updateData(newData: ChartData): void {
if (!this.chart) return;
this.xData = newData.xData;
this.yData = newData.yData;
this.seriesName = newData.seriesName || this.seriesName;
this.currentColor = newData.color || this.currentColor;
this.setChartOptions();
}
dispose(): void {
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
window.removeEventListener('resize', () => this.chart?.resize());
}
}

@ -0,0 +1,149 @@
import * as echarts from 'echarts';
interface ChartData {
xData: string[]; // X轴分类数据如['周一', '周二', '周三']
yData: number[]; // 单一折线数据(数值数组)
seriesName?: string; // 系列名称(默认'数据趋势'
color?: string; // 折线颜色(默认主题蓝色)
}
export class LineChart {
private chart: echarts.ECharts | null = null;
private dom: HTMLElement;
private xData: string[];
private yData: number[];
private seriesName: string = '数据趋势';
private currentColor: string = '#1890ff'; // 默认颜色ECharts主题蓝
constructor(containerId: string, chartData: ChartData) {
this.dom = document.getElementById(containerId)!;
this.xData = chartData.xData;
this.yData = chartData.yData;
this.seriesName = chartData.seriesName || this.seriesName;
this.currentColor = chartData.color || this.currentColor;
this.initChart();
}
private initChart(): void {
this.chart = echarts.init(this.dom);
this.setChartOptions();
window.addEventListener('resize', () => this.chart?.resize());
}
private setChartOptions(): void {
const option= {
// tooltip: {
// trigger: 'axis',
// formatter: (params) => `
// <div style="font-size: 14px; font-weight: 500">${params[0].name}</div>
// <div style="display: flex; align-items: center; gap: 4px; margin: 4px 0">
// <span style="width: 8px; height: 8px; border-radius: 50%; background: ${this.currentColor}"></span>
// <span>${this.seriesName}: ${params[0].value}</span>
// </div>
// `
// },
// legend: {
// data: [this.seriesName],
// top: '16px',
// left: 'center',
// textStyle: { color: '#666', fontSize: 14 }
// },
grid: {
top:'5%',
left: '0%',
right: '0%',
bottom: '5%',
containLabel: true
},
xAxis: {
type: 'category',
data: this.xData,
axisTick: { alignWithLabel: true, color: '#e0e0e0' },
axisLine: { lineStyle: { color: '#C3CAD9', width: 1 } },
axisLabel:{
color: '#C3CAD9',
fontSize: 12,
}
},
yAxis: {
type: 'value',
name: '',
splitLine: { lineStyle: { type: 'dashed', color: '#f0f0f0' } },
axisLabel:{
color: '#C3CAD9',
fontSize: 12,
}
},
series: [{
name: this.seriesName,
type: 'line',
data: this.yData,
color: this.currentColor,
smooth: true, // 曲线平滑
symbol: 'circle', // 标记样式
symbolSize: 8, // 标记大小
itemStyle: {
emphasis: { scale: 1.2 } // 悬停时标记放大
},
lineStyle: { width: 2 }, // 线宽
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 12,
formatter: '{c}' // 显示数值
}
}]
};
this.chart?.setOption(option);
}
/**
* 线
* @param newColor /RGB/HSL
*/
changeColorStyle(index: number): void {
if (!this.chart) {
console.warn('未读取到元素!')
return
}
if (index === 0) {
this.currentColor = '#3071EC'
} else if (index === 1) {
this.currentColor = '#6790EA'
} else if (index === 2) {
this.currentColor = '#0A50FF'
}
this.chart.setOption({
series: [{
color: this.currentColor,
itemStyle: { color: this.currentColor },
lineStyle: { color: this.currentColor }
}]
});
}
/**
*
* @param newData
*/
updateData(newData: ChartData): void {
if (!this.chart) return;
this.xData = newData.xData;
this.yData = newData.yData;
this.seriesName = newData.seriesName || this.seriesName;
this.currentColor = newData.color || this.currentColor;
this.setChartOptions();
}
dispose(): void {
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
window.removeEventListener('resize', () => this.chart?.resize());
}
}

@ -0,0 +1,115 @@
import * as echarts from 'echarts'
interface ChartData {
xData: string[]
yData: number[]
}
export class PieChart {
private chart: echarts.ECharts | null = null
private dom: HTMLElement
private xData: string[]
private yData: number[]
private currentColors: string[] = ['#0A50FF', '#23CAF4', '#E96F69', '#F2F7F8', '#35C75D', '#35C75D', '#E6FA72']
private scienceColors: string[] = ['#3071EC', '#41B7FD', '#54D360', '#DAE8F7', '#F6A450', '#EA7261', '#CA68F5'] // 科技风
private businessColors: string[] = ['#6790EA', '#63C8EA', '#88DE95', '#CFEE5F', '#F3D088', '#F99774', '#F17E7E'] // 商务风
private simpleColors: string[] = ['#0A50FF', '#23CAF4', '#E96F69', '#F2F7F8', '#35C75D', '#35C75D', '#E6FA72'] // 简约风
constructor(containerId: string, { xData, yData }: ChartData) {
this.dom = document.getElementById(containerId)!
this.xData = xData
this.yData = yData
this.initChart()
}
private initChart(): void {
this.chart = echarts.init(this.dom)
this.setChartOptions()
window.addEventListener('resize', () => this.chart?.resize())
}
private setChartOptions(): void {
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
bottom: '10%',
left: 'center',
icon: 'rect',
itemWidth: 10,
itemHeight: 5,
textStyle: {
color: '#C3CAD9',
fontSize: 12
},
data: this.xData
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['30%', '50%'], // 设置内外半径,形成环形
center: ['50%', '50%'],
color: this.currentColors, // 应用颜色配置
data: this.xData.map((name, index) => ({
name,
value: this.yData[index]
})),
label: {
show: false,
position: 'center'
}
// labelLine: {
// show: false
// },
// emphasis: {
// itemStyle: {
// shadowBlur: 10,
// shadowOffsetX: 0,
// shadowColor: 'rgba(0, 0, 0, 0.5)'
// }
// }
}
]
}
this.chart?.setOption(option)
}
/**
*
* @param newColors
*/
changeColorStyle(index: number): void {
if (!this.chart) {
console.warn('未读取到元素!')
return
}
if (index === 0) {
this.currentColors = this.simpleColors
} else if (index === 1) {
this.currentColors = this.businessColors
} else if (index === 2) {
this.currentColors = this.scienceColors
}
// 仅更新颜色配置,避免重新初始化图表
this.chart.setOption({
series: [
{
color: this.currentColors
}
]
})
}
dispose(): void {
if (this.chart) {
this.chart.dispose()
this.chart = null
}
window.removeEventListener('resize', () => this.chart?.resize())
}
}

@ -1,6 +1,15 @@
<template> <template>
<div class="chat-container"> <div class="chat-container">
<div class="chat-list"> <div class="chat-list" ref="scrollContainer">
<!-- <div class="echarts-box" id="test"></div>
<div class="echarts-style-row">
<div class="style-row-name">风格切换</div>
<div class="echarts-style-list">
<div class="style-item" v-for="(echartItem, echartIndex) in styleColor" :key="echartIndex">
{{ echartItem }}
</div>
</div>
</div> -->
<div <div
v-for="(item, index) in messages" v-for="(item, index) in messages"
:key="index" :key="index"
@ -11,7 +20,26 @@
<!-- 内容 --> <!-- 内容 -->
<div class="message-content"> <div class="message-content">
<div class="message-time">{{ item.time }}</div> <div class="message-time">{{ item.time }}</div>
<div class="message-text">{{ item.text }}</div> <div class="message-text">
<span>{{ item.text }}</span>
<div v-if="item.from === 'ai' && item.chartType">
<div :id="item.chartType + index" class="echarts-box"></div>
<div class="echarts-style-row">
<div class="style-row-name">风格切换</div>
<div class="echarts-style-list">
<div
class="style-item"
@click="hanlderStyleColor(item.chartType, index, echartIndex)"
:class="echartIndex == item.chartInstanceActiveIndex ? 'activeItem' : ''"
v-for="(echartItem, echartIndex) in styleColor"
:key="echartIndex"
>
{{ echartItem }}
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -20,44 +48,152 @@
round round
v-model:value="keyWord" v-model:value="keyWord"
type="textarea" type="textarea"
placeholder="请输入您的问题" placeholder="请输入需要统计的数据"
:autosize="{ minRows: 5 }" :autosize="{ minRows: 5 }"
> >
<template #suffix> <template #suffix>
<div class="chat-suffix"> <div class="chat-suffix">
<span>常用问题</span> <span>常用问题</span>
<img src="~@/assets/images/ai/chat-send.png" class="chat-send" alt="" /> <!-- <img src="~@/assets/images/ai/chat-send.png" class="chat-send" alt="" /> -->
<n-button :loading="loading" size="small" :class="['chat-send']" :bordered="false" @click="handleSend">
</n-button>
</div> </div>
</template> </template>
</n-input> </n-input>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { useRoute,useRouter } from 'vue-router' import moment from 'moment'
import { ref, reactive, onMounted, } from 'vue'
// @ts-ignore
import { getAiMsg } from '@/api/ai.js'
// @ts-ignore
import { AiChatroomType } from './types'
// @ts-ignore
import { PieChart } from './class/PieChart.ts'
// @ts-ignore
import { LineChart } from './class/LineChart.ts'
// @ts-ignore
import { BarChart } from './class/BarChart.ts'
import { ref, reactive, onMounted, onUnmounted, watch, nextTick } from 'vue'
import userIcon from '@/assets/images/ai/user-icon.png' import userIcon from '@/assets/images/ai/user-icon.png'
import aiIcon from '@/assets/images/ai/ai-icon.png' import aiIcon from '@/assets/images/ai/ai-icon.png'
const route = useRoute() import { useMessage } from 'naive-ui'
let keyWord = ref(null)
let messages = reactive([ const messageDialog = useMessage()
{ const scrollContainer = ref<HTMLDivElement | null>(null)
// const chartContainer = ref<HTMLDivElement | null>(null)
let loading = ref(false)
let keyWord = ref('')
let messages: AiChatroomType[] = reactive([])
let chartInstance: LineChart | null = null
//
const chartData = {
xData: ['Q1', 'Q2', 'Q3', 'Q4'],
yData: [120, 150, 130, 180],
seriesName: '年度营收', //
color: '#ff6b6b' //
}
const styleColor: string[] = ['简约风', '商务风', '科技风']
const handleSend = async () => {
if (!keyWord.value.trim()) {
messageDialog.warning('请先输入您想知道的问题!')
return
}
/**
*保存用户提问信息
*/
messages.push({
from: 'user', from: 'user',
text: '根据2024年度系统内所有业务类型签订的合同数据指标生成数据看板根据业务数据生成并且完整展示数据内容用饼图的 形式展现,图表颜色搭配要协调,整体美观', text: keyWord.value,
time: '2025/03/27 14:30:23' time: moment().format('YYYY/MM/DD HH:mm:ss')
}, })
{ loading.value = true
try {
const res = await getAiMsg({ prompt: keyWord.value })
//AI
if (res.type) {
messages.push({
from: 'ai', from: 'ai',
text: '根据用户要求的统计2024年系统每个业务类型产生签订的合同数量并用饼图展示生成的统计图是', text: '以图表形式展示',
time: '2025/03/27 14:30:23' chartType: res.chartType,
time: moment().format('YYYY/MM/DD HH:mm:ss'),
chartInstanceItem: null,
chartInstanceActiveIndex: 0,
xData: res.xData,
yData: res.yData
})
nextTick(() => {
const lastIndex = messages.length - 1
const itemData = messages[lastIndex]
const xData = res.xData
const yData = res.yData.map((str: string) => parseInt(str, 10))
if (itemData.chartType === 'pie')
itemData.chartInstanceItem = new PieChart(`${itemData.chartType}${lastIndex}`, { xData, yData })
if (itemData.chartType === 'line') {
itemData.chartInstanceItem = new LineChart(`${itemData.chartType}${lastIndex}`, { xData, yData })
}
if (itemData.chartType === 'bar') {
itemData.chartInstanceItem = new BarChart(`${itemData.chartType}${lastIndex}`, { xData, yData })
}
})
} else {
//
messages.push({
from: 'ai',
text: res,
time: moment().format('YYYY/MM/DD HH:mm:ss')
})
}
keyWord.value = '' //
loading.value = false
} catch (error) {
//
messages.push({
from: 'ai',
text: '服务器繁忙,请稍后再试。',
time: moment().format('YYYY/MM/DD HH:mm:ss')
})
keyWord.value = '' //
loading.value = false
}
}
const hanlderStyleColor = (type: string, fatherIndex: number, childIndex: number) => {
const messagesItem = messages[fatherIndex]
if (messagesItem.chartInstanceActiveIndex == childIndex) return
messagesItem.chartInstanceActiveIndex = childIndex
messagesItem.chartInstanceItem?.changeColorStyle(childIndex)
} }
])
onMounted(() => { onMounted(() => {
// console.log(route.path) // chartInstance = new LineChart('test', chartData)
}) })
// messages
watch(
messages,
() => {
nextTick(() => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
}
})
},
{ deep: true }
)
onUnmounted(() => {
//
messages.forEach(instance => {
if (instance.chartType) {
instance.chartInstanceItem?.dispose()
}
})
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -88,14 +224,18 @@ onMounted(()=>{
width: 46px; width: 46px;
height: 30px; height: 30px;
margin-bottom: 10px; margin-bottom: 10px;
background: url('@/assets/images/ai/chat-send.png');
background-size: 100% 100%;
} }
.chat-list { .chat-list {
width: 50%;
height: 100%;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 25px; gap: 25px;
width: 50%;
height: 100%;
// border: 1px solid red;
} }
.chat-item { .chat-item {
display: flex; display: flex;
@ -141,4 +281,45 @@ onMounted(()=>{
border-radius: 10px 10px 10px 10px; border-radius: 10px 10px 10px 10px;
} }
} }
::v-deep .n-input .n-input__suffix .n-base-loading {
color: #283e81 !important;
}
/* 设置滚动条整体样式 */
::-webkit-scrollbar {
width: 0px;
}
.echarts-box {
height: 300px;
width: 500px;
// border: 1px solid red;
}
.echarts-style-row {
display: flex;
align-items: center;
.style-row-name {
font-size: 14px;
color: #cfd5e5;
font-weight: 400;
}
.echarts-style-list {
display: flex;
align-items: center;
gap: 10px;
}
.style-item {
cursor: pointer;
font-size: 14px;
color: #cfd5e5;
font-weight: 400;
background: #31363e;
border-radius: 2px 2px 2px 2px;
padding: 3px 10px;
}
.activeItem {
background: rgba(63, 123, 248, 0.1);
color: #3f7bf8;
}
}
</style> </style>

@ -0,0 +1,8 @@
export interface AiChatroom {
from:string,//来自谁
time:string,//时间
test?:string,//纯文本
}
export type AiChatroomType = AiChatroom;

@ -0,0 +1,292 @@
<template>
<n-timeline class="go-chart-configurations-timeline">
<!-- 处理 echarts 的数据映射 -->
<n-timeline-item v-if="isCharts && dimensionsAndSource" type="info" :title="TimelineTitleEnum.MAPPING">
<n-table striped>
<thead>
<tr>
<th v-for="item in tableTitle" :key="item">{{ item }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in dimensionsAndSource" :key="index">
<td>{{ item.field }}</td>
<td>{{ item.mapping }}</td>
<td>
<n-space v-if="item.result === 0">
<n-badge dot type="success"></n-badge>
<n-text></n-text>
</n-space>
<n-space v-else>
<n-badge dot :type="item.result === 1 ? 'success' : 'error'"></n-badge>
<n-text>匹配{{ item.result === 1 ? '成功' : '失败' }}</n-text>
</n-space>
</td>
</tr>
</tbody>
</n-table>
</n-timeline-item>
<!-- 处理 vcharts 的数据映射 -->
<n-timeline-item v-if="isVChart" type="info" :title="TimelineTitleEnum.MAPPING">
<n-table striped>
<thead>
<tr>
<th v-for="item in vchartTableTitle" :key="item">{{ item }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in fieldList" :key="item.field">
<td>
<n-ellipsis style="width: 70px; max-width: 240px">
{{ item.field }}
</n-ellipsis>
</td>
<td v-if="isArray(item.mapping)">
<n-space :size="4" vertical>
<n-input
v-for="(mappingItem, index) in item.mapping"
:key="index"
v-model:value="item.mapping[index]"
type="tiny"
size="small"
placeholder="输入字段"
@change="() => (item.result = matchingHandle(item.mapping[index]))"
/>
</n-space>
</td>
<td v-else>
<n-input v-model:value="item.mapping" type="text" size="small" placeholder="小" />
</td>
<!-- <td>
<n-space style="width: 70px" :size="4">
<n-badge dot :type="item.result === 1 ? 'success' : 'error'"></n-badge>
<n-text>匹配{{ item.result === 1 ? '成功' : '失败' }}</n-text>
</n-space>
</td> -->
</tr>
</tbody>
</n-table>
</n-timeline-item>
<n-timeline-item v-show="filterShow" color="#97846c" :title="TimelineTitleEnum.FILTER">
<n-space :size="18" vertical>
<n-text depth="3">过滤器默认处理接口返回值的data字段</n-text>
<chart-data-monaco-editor></chart-data-monaco-editor>
</n-space>
</n-timeline-item>
<n-timeline-item type="success" :title="TimelineTitleEnum.CONTENT">
<n-space vertical>
<n-space class="source-btn-box">
<n-upload
v-model:file-list="uploadFileListRef"
:show-file-list="false"
:customRequest="customRequest"
@before-upload="beforeUpload"
>
<n-space>
<n-button v-if="!ajax" class="sourceBtn-item" :disabled="noData">
<template #icon>
<n-icon>
<document-add-icon />
</n-icon>
</template>
导入json / txt
</n-button>
</n-space>
</n-upload>
<div>
<n-button class="sourceBtn-item" :disabled="noData" @click="download">
<template #icon>
<n-icon>
<document-download-icon />
</n-icon>
</template>
下载
</n-button>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon class="go-ml-1" size="21" :depth="3">
<help-outline-icon></help-outline-icon>
</n-icon>
</template>
<span>点击下载查看完整数据</span>
</n-tooltip>
</div>
</n-space>
<n-card size="small">
<n-code :code="toString(source)" language="json"></n-code>
</n-card>
</n-space>
</n-timeline-item>
</n-timeline>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ChartFrameEnum } from '@/packages/index.d'
import { RequestDataTypeEnum } from '@/enums/httpEnum'
import { icon } from '@/plugins'
import { DataResultEnum, TimelineTitleEnum } from '../../index.d'
import { ChartDataMonacoEditor } from '../ChartDataMonacoEditor'
import { useFile } from '../../hooks/useFile.hooks'
import { useTargetData } from '../../../hooks/useTargetData.hook'
import { toString, isArray } from '@/utils'
const { targetData } = useTargetData()
defineProps({
show: {
type: Boolean,
required: false
},
ajax: {
type: Boolean,
required: true
}
})
//
const tableTitle = ['字段', '映射', '状态']
const vchartTableTitle = ['字段', '接口映射字段']
const { HelpOutlineIcon } = icon.ionicons5
const { DocumentAddIcon, DocumentDownloadIcon } = icon.carbon
const source = ref()
const dimensions = ref()
const dimensionsAndSource = ref()
const noData = ref(false)
// , mapping
const fieldList = ref<
Array<{
field: string
mapping: string[]
result: DataResultEnum
}>
>([])
const { uploadFileListRef, customRequest, beforeUpload, download } = useFile(targetData)
//
const filterShow = computed(() => {
return targetData.value.request.requestDataType !== RequestDataTypeEnum.STATIC
})
// dataset
const isCharts = computed(() => {
return targetData.value.chartConfig.chartFrame === ChartFrameEnum.ECHARTS
})
// vchart
const isVChart = computed(() => {
return targetData.value.chartConfig.chartFrame === ChartFrameEnum.VCHART
})
//
const matchingHandle = (mapping: string) => {
let res = DataResultEnum.SUCCESS
for (let i = 0; i < source.value.length; i++) {
if (source.value[i][mapping] === undefined) {
res = DataResultEnum.FAILURE
return res
}
}
return DataResultEnum.SUCCESS
}
//
const dimensionsAndSourceHandle = () => {
try {
//
return dimensions.value.map((dimensionsItem: string, index: number) => {
return index === 0
? {
//
field: '通用标识',
//
mapping: dimensionsItem,
//
result: DataResultEnum.NULL
}
: {
field: `数据项-${index}`,
mapping: dimensionsItem,
result: matchingHandle(dimensionsItem)
}
})
} catch (error) {
return []
}
}
// vchart
const initFieldListHandle = () => {
if (targetData.value?.option) {
fieldList.value = []
// Field key
for (const key in targetData.value.option) {
if (key.endsWith('Field')) {
const value = targetData.value.option[key]
targetData.value.option[key] = value
const item = {
field: key,
mapping: value,
result: DataResultEnum.SUCCESS
}
if (item.mapping === undefined) {
item.result = DataResultEnum.FAILURE
}
fieldList.value.push(item)
}
}
}
}
watch(
() => targetData.value?.option?.dataset,
(
newData?: {
source: any
dimensions: any
} | null
) => {
noData.value = false
if (newData && targetData?.value?.chartConfig?.chartFrame === ChartFrameEnum.ECHARTS) {
// DataSet
source.value = newData
if (isCharts.value) {
dimensions.value = newData.dimensions
dimensionsAndSource.value = dimensionsAndSourceHandle()
}
} else if (newData && targetData?.value?.chartConfig?.chartFrame === ChartFrameEnum.VCHART) {
source.value = newData
initFieldListHandle()
} else if (newData !== undefined && newData !== null) {
dimensionsAndSource.value = null
source.value = newData
fieldList.value = []
} else {
noData.value = true
source.value = '此组件无数据源'
}
if (isArray(newData)) {
dimensionsAndSource.value = null
}
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
@include go('chart-configurations-timeline') {
@include deep() {
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
}
.source-btn-box {
margin-top: 10px !important;
}
}
</style>

@ -1,292 +1,236 @@
<template> <template>
<n-timeline class="go-chart-configurations-timeline"> <div class="chat-container" ref="chatBxo">
<!-- 处理 echarts 的数据映射 --> <div
<n-timeline-item v-if="isCharts && dimensionsAndSource" type="info" :title="TimelineTitleEnum.MAPPING"> class="chat-list"
<n-table striped> ref="scrollContainer"
<thead> :style="{
<tr> height: chatRoom + 'px'
<th v-for="item in tableTitle" :key="item">{{ item }}</th> }"
</tr> >
</thead> <div
<tbody> v-for="(item, index) in messagesList"
<tr v-for="(item, index) in dimensionsAndSource" :key="index">
<td>{{ item.field }}</td>
<td>{{ item.mapping }}</td>
<td>
<n-space v-if="item.result === 0">
<n-badge dot type="success"></n-badge>
<n-text></n-text>
</n-space>
<n-space v-else>
<n-badge dot :type="item.result === 1 ? 'success' : 'error'"></n-badge>
<n-text>匹配{{ item.result === 1 ? '成功' : '失败' }}</n-text>
</n-space>
</td>
</tr>
</tbody>
</n-table>
</n-timeline-item>
<!-- 处理 vcharts 的数据映射 -->
<n-timeline-item v-if="isVChart" type="info" :title="TimelineTitleEnum.MAPPING">
<n-table striped>
<thead>
<tr>
<th v-for="item in vchartTableTitle" :key="item">{{ item }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in fieldList" :key="item.field">
<td>
<n-ellipsis style="width: 70px; max-width: 240px">
{{ item.field }}
</n-ellipsis>
</td>
<td v-if="isArray(item.mapping)">
<n-space :size="4" vertical>
<n-input
v-for="(mappingItem, index) in item.mapping"
:key="index" :key="index"
v-model:value="item.mapping[index]" :class="['chat-item', item.from == 'user' ? 'user-message' : 'ai-message']"
type="tiny"
size="small"
placeholder="输入字段"
@change="() => (item.result = matchingHandle(item.mapping[index]))"
/>
</n-space>
</td>
<td v-else>
<n-input v-model:value="item.mapping" type="text" size="small" placeholder="小" />
</td>
<!-- <td>
<n-space style="width: 70px" :size="4">
<n-badge dot :type="item.result === 1 ? 'success' : 'error'"></n-badge>
<n-text>匹配{{ item.result === 1 ? '成功' : '失败' }}</n-text>
</n-space>
</td> -->
</tr>
</tbody>
</n-table>
</n-timeline-item>
<n-timeline-item v-show="filterShow" color="#97846c" :title="TimelineTitleEnum.FILTER">
<n-space :size="18" vertical>
<n-text depth="3">过滤器默认处理接口返回值的data字段</n-text>
<chart-data-monaco-editor></chart-data-monaco-editor>
</n-space>
</n-timeline-item>
<n-timeline-item type="success" :title="TimelineTitleEnum.CONTENT">
<n-space vertical>
<n-space class="source-btn-box">
<n-upload
v-model:file-list="uploadFileListRef"
:show-file-list="false"
:customRequest="customRequest"
@before-upload="beforeUpload"
> >
<n-space> <!-- 头像 -->
<n-button v-if="!ajax" class="sourceBtn-item" :disabled="noData"> <img :src="item.from == 'user' ? userIcon : aiIcon" class="user-icon" alt="" />
<template #icon> <!-- 内容 -->
<n-icon> <div class="message-content">
<document-add-icon /> <div class="message-time">{{ item.time }}</div>
</n-icon> <div class="message-text">
</template> <span>{{ item.text }}</span>
导入json / txt </div>
</n-button> </div>
</n-space> </div>
</n-upload> </div>
<div>
<n-button class="sourceBtn-item" :disabled="noData" @click="download"> <div class="chat-input" ref="chatInput">
<template #icon> <n-input
<n-icon> round
<document-download-icon /> v-model:value="keyWord"
</n-icon> type="textarea"
</template> placeholder="请输入需要统计的数据"
下载 :autosize="{ minRows: 5 }"
>
<template #suffix>
<div class="chat-suffix">
<span></span>
<n-button :loading="loading" size="small" :class="['chat-send']" :bordered="false" @click="handleSend">
</n-button> </n-button>
<n-tooltip trigger="hover"> </div>
<template #trigger>
<n-icon class="go-ml-1" size="21" :depth="3">
<help-outline-icon></help-outline-icon>
</n-icon>
</template> </template>
<span>点击下载查看完整数据</span> </n-input>
</n-tooltip> </div>
</div> </div>
</n-space>
<n-card size="small">
<n-code :code="toString(source)" language="json"></n-code>
</n-card>
</n-space>
</n-timeline-item>
</n-timeline>
</template> </template>
<script setup lang="ts"> <script setup>
import { ref, computed, watch } from 'vue' import { useMessage } from 'naive-ui'
import { ChartFrameEnum } from '@/packages/index.d' import { ref, reactive, watch, nextTick, onMounted } from 'vue'
import { RequestDataTypeEnum } from '@/enums/httpEnum'
import { icon } from '@/plugins'
import { DataResultEnum, TimelineTitleEnum } from '../../index.d'
import { ChartDataMonacoEditor } from '../ChartDataMonacoEditor'
import { useFile } from '../../hooks/useFile.hooks'
import { useTargetData } from '../../../hooks/useTargetData.hook' import { useTargetData } from '../../../hooks/useTargetData.hook'
import { toString, isArray } from '@/utils' import moment from 'moment'
// @ts-ignore
const { targetData } = useTargetData() import { getAiMsg } from '@/api/ai.js'
defineProps({
show: {
type: Boolean,
required: false
},
ajax: {
type: Boolean,
required: true
}
})
//
const tableTitle = ['字段', '映射', '状态']
const vchartTableTitle = ['字段', '接口映射字段']
const { HelpOutlineIcon } = icon.ionicons5
const { DocumentAddIcon, DocumentDownloadIcon } = icon.carbon
const source = ref() import userIcon from '@/assets/images/ai/user-icon.png'
const dimensions = ref() import aiIcon from '@/assets/images/ai/ai-icon.png'
const dimensionsAndSource = ref()
const noData = ref(false)
// , mapping //
const fieldList = ref< const { targetData, chartEditStore } = useTargetData()
Array<{ let keyWord = ref('')
field: string let loading = ref(false)
mapping: string[] const messageDialog = useMessage()
result: DataResultEnum let messagesList = reactive([])
}> const scrollContainer = ref(null)
>([]) const chatBxo = ref(null)
const chatInput = ref(null)
const { uploadFileListRef, customRequest, beforeUpload, download } = useFile(targetData) let chatRoom = ref(0)
//
// const handleSend = async () => {
const filterShow = computed(() => { if (!keyWord.value.trim()) {
return targetData.value.request.requestDataType !== RequestDataTypeEnum.STATIC messageDialog.warning('请输入需要统计的数据!')
return
}
/**
*保存用户提问信息
*/
messagesList.push({
from: 'user',
text: keyWord.value,
time: moment().format('YYYY/MM/DD HH:mm:ss')
}) })
loading.value = true
// dataset try {
const isCharts = computed(() => { const res = await getAiMsg({ prompt: keyWord.value })
return targetData.value.chartConfig.chartFrame === ChartFrameEnum.ECHARTS let result = {}
if(res.chartType){
result.dimensions = [
'product','data1'
]
result.source = []
res.xData.map((item,index)=>{
result.source.push({
product:item,
data1:parseInt(res.yData[index])
}) })
// vchart
const isVChart = computed(() => {
return targetData.value.chartConfig.chartFrame === ChartFrameEnum.VCHART
}) })
// }else{
const matchingHandle = (mapping: string) => { result = res
let res = DataResultEnum.SUCCESS
for (let i = 0; i < source.value.length; i++) {
if (source.value[i][mapping] === undefined) {
res = DataResultEnum.FAILURE
return res
}
}
return DataResultEnum.SUCCESS
} }
// //AI
const dimensionsAndSourceHandle = () => { messagesList.push({
try { from: 'ai',
// text: result,
return dimensions.value.map((dimensionsItem: string, index: number) => { time: moment().format('YYYY/MM/DD HH:mm:ss')
return index === 0
? {
//
field: '通用标识',
//
mapping: dimensionsItem,
//
result: DataResultEnum.NULL
}
: {
field: `数据项-${index}`,
mapping: dimensionsItem,
result: matchingHandle(dimensionsItem)
}
}) })
targetData.value.option.dataset = result
keyWord.value = '' //
loading.value = false
} catch (error) { } catch (error) {
return [] //
messagesList.push({
from: 'ai',
text: '服务器繁忙,请稍后再试。',
time: moment().format('YYYY/MM/DD HH:mm:ss')
})
keyWord.value = '' //
loading.value = false
} }
} }
// vchart //watch
const initFieldListHandle = () => { // messages
if (targetData.value?.option) { watch(
fieldList.value = [] messagesList,
// Field key () => {
for (const key in targetData.value.option) { nextTick(() => {
if (key.endsWith('Field')) { if (scrollContainer.value) {
const value = targetData.value.option[key] scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
targetData.value.option[key] = value
const item = {
field: key,
mapping: value,
result: DataResultEnum.SUCCESS
}
if (item.mapping === undefined) {
item.result = DataResultEnum.FAILURE
}
fieldList.value.push(item)
} }
})
},
{ deep: true }
)
onMounted(() => {
if (chatBxo.value) {
// 使 offsetHeight
chatRoom.value = chatBxo.value.offsetHeight - (chatInput.value.offsetHeight+100 )
console.log(chatBxo.value.offsetHeight, '高度')
} }
})
</script>
<style lang="scss" scoped>
.chat-container {
// box-sizing: border-box;
// padding: 20px;
height: 100%;
gap: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
} }
.chat-list {
display: flex;
flex-direction: column;
gap: 25px;
width: 100%;
overflow-y: auto;
.chat-item {
display: flex;
gap: 6px;
}
.user-icon {
height: 35px;
width: 35px;
} }
watch( .user-message {
() => targetData.value?.option?.dataset, flex-direction: row-reverse;
( .message-content {
newData?: { display: flex;
source: any flex-direction: column;
dimensions: any align-items: flex-end;
} | null
) => {
noData.value = false
if (newData && targetData?.value?.chartConfig?.chartFrame === ChartFrameEnum.ECHARTS) {
// DataSet
source.value = newData
if (isCharts.value) {
dimensions.value = newData.dimensions
dimensionsAndSource.value = dimensionsAndSourceHandle()
} }
} else if (newData && targetData?.value?.chartConfig?.chartFrame === ChartFrameEnum.VCHART) {
source.value = newData
initFieldListHandle()
} else if (newData !== undefined && newData !== null) {
dimensionsAndSource.value = null
source.value = newData
fieldList.value = []
} else {
noData.value = true
source.value = '此组件无数据源'
} }
if (isArray(newData)) { .ai-message {
dimensionsAndSource.value = null flex-direction: row;
.message-content {
display: flex;
flex-direction: column;
align-items: flex-start;
} }
},
{
immediate: true
} }
)
</script>
<style lang="scss" scoped> .message-content {
@include go('chart-configurations-timeline') { font-size: 13px;
@include deep() { font-weight: 400;
pre { .message-time {
white-space: pre-wrap; color: #c3cad9;
word-wrap: break-word; }
} width: calc(100% - 70px);
padding-top: 8px;
.message-text {
width: auto;
color: #ffffff;
font-size: 13px;
margin-top: 10px;
padding: 10px;
background: #282a31;
border-radius: 10px 10px 10px 10px;
} }
.source-btn-box {
margin-top: 10px !important;
} }
::v-deep .n-input .n-input__suffix .n-base-loading {
color: #283e81 !important;
}
}
.chat-input {
width: 100%;
}
.chat-suffix {
font-size: 14px;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: space-between;
}
.chat-send {
cursor: pointer;
width: 46px;
height: 30px;
margin-bottom: 10px;
background: url('@/assets/images/ai/chat-send.png');
background-size: 100% 100%;
}
/* 设置滚动条整体样式 */
::-webkit-scrollbar {
width: 0px;
} }
</style> </style>

@ -1,5 +1,5 @@
<template> <template>
<div class="go-chart-configurations-data-static"> <div class="go-chart-configurations-data-static" style="height: 100%;">
<chart-data-matching-and-show :show="false" :ajax="false"></chart-data-matching-and-show> <chart-data-matching-and-show :show="false" :ajax="false"></chart-data-matching-and-show>
</div> </div>
</template> </template>

@ -23,6 +23,7 @@ export const useFile = (targetData: any) => {
nextTick(() => { nextTick(() => {
if (file.file) { if (file.file) {
readFile(file.file).then((fileData: any) => { readFile(file.file).then((fileData: any) => {
console.log(fileData,'ssss')
targetData.value.option.dataset = JSONParse(fileData) targetData.value.option.dataset = JSONParse(fileData)
}) })
} else { } else {

@ -1,8 +1,8 @@
<template> <template>
<div class="go-chart-configurations-data" v-if="targetData"> <div class="go-chart-configurations-data" v-if="targetData">
<setting-item-box name="请求方式" :alone="true"> <!-- <setting-item-box name="请求方式" :alone="true">
<n-select v-model:value="targetData.request.requestDataType" :disabled="isNotData" :options="selectOptions" /> <n-select v-model:value="targetData.request.requestDataType" :disabled="isNotData" :options="selectOptions" />
</setting-item-box> </setting-item-box> -->
<!-- 静态 --> <!-- 静态 -->
<chart-data-static v-if="targetData.request.requestDataType === RequestDataTypeEnum.STATIC"></chart-data-static> <chart-data-static v-if="targetData.request.requestDataType === RequestDataTypeEnum.STATIC"></chart-data-static>
<!-- 动态 --> <!-- 动态 -->

@ -146,7 +146,7 @@ const chartsTabList = [
// ...chartsDefaultTabList, // ...chartsDefaultTabList,
{ {
key: TabsEnum.CHART_DATA, key: TabsEnum.CHART_DATA,
title: '数据', title: '统计数据',
icon: FlashIcon, icon: FlashIcon,
render: ChartData render: ChartData
}, },
@ -169,4 +169,20 @@ const chartsTabList = [
} }
} }
} }
::v-deep .n-scrollbar-content{
height: 100%;
overflow: hidden;
}
.n-tabs{
height: 100%;
display: flex;
flex-direction: column;
.n-tab-pane{
flex: 1;
.go-chart-configurations-data{
height: 100%;
}
}
}
</style> </style>

@ -46,13 +46,20 @@ export default ({ mode }) => defineConfig({
open: true, open: true,
port: 3000, port: 3000,
proxy: { proxy: {
[axiosPre]: { [axiosPre]: {
// @ts-ignore // @ts-ignore
target: loadEnv(mode, process.cwd()).VITE_DEV_PATH, target: loadEnv(mode, process.cwd()).VITE_DEV_PATH,
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
secure: true, secure: true,
} },
'/ai': {
target: 'http://5179zv4ns298.vicp.fun/',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/ai/, '')
},
} }
}, },
define: { define: {

Loading…
Cancel
Save