You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

427 lines
12 KiB

1 month ago
<template>
<div class="chat-container">
1 month ago
<div class="chat-list" ref="scrollContainer">
<!-- <div>
<div class="echarts-box" id="barEcharts"></div>
<div class="echatrs-theme">
<div class="theme-title">风格切换</div>
<div class="theme-list">
<div class="theme-item" @click="handlerTheme(index)" v-for="(item, index) in echartsTheme" :key="index">
{{ item }}
</div>
1 month ago
</div>
</div>
</div> -->
<AiHint></AiHint>
1 month ago
<div
v-for="(item, index) in messageList"
1 month ago
:key="index"
:class="['chat-item', item.from == 'user' ? 'user-message' : 'ai-message']"
>
<!-- 头像 -->
<img :src="item.from == 'user' ? userIcon : aiIcon" class="user-icon" alt="" />
<!-- 内容 -->
<div class="message-content">
<div class="message-time">{{ item.time }}</div>
<div class="message-text" :id="`box${index}`">
<!-- 纯文本 -->
<div class="text-ros">
<span>{{ item.text }}</span>
<div v-show="item.add">
<n-tooltip placement="bottom" trigger="hover">
<template #trigger>
<n-icon size="14" style="cursor: pointer">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 12 12"
>
<g fill="none">
<path
d="M6.5 1.75a.75.75 0 0 0-1.5 0V5H1.75a.75.75 0 0 0 0 1.5H5v3.25a.75.75 0 0 0 1.5 0V6.5h3.25a.75.75 0 0 0 0-1.5H6.5V1.75z"
fill="currentColor"
></path>
</g>
</svg>
</n-icon>
</template>
<span> 添加为常用问题 </span>
</n-tooltip>
</div>
</div>
<!-- 图表 -->
<section v-if="item.chartType">
<div class="echarts-box" :id="item.chartType + index"></div>
<div class="echatrs-theme">
<div class="theme-title">风格切换</div>
<div class="theme-list">
<div
class="theme-item"
@click="handlerTheme(item, themeIndex)"
v-for="(themeItem, themeIndex) in echartsTheme"
:key="themeIndex"
:class="item.chartInstanceActiveIndex == themeIndex ? 'activeTheme' : ''"
>
{{ themeItem }}
</div>
</div>
</div>
</section>
<!-- 思考 -->
<div v-show="item.from === 'ai' && item.loading" class="ai-loading">
<span>思考中</span> <n-spin size="small" content-class="spin-class" />
</div>
<!-- 免责 -->
<div class="ai-hint" v-show="item.from === 'ai' && (item.text || item.chartType)">
<span>本回答由AI生成内容仅供参考</span>
<div class="upload-text" @click="uploadImage(index)">
<n-icon size="14">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 16 16"
>
<g fill="none">
<path
d="M3.5 13h9a.75.75 0 0 1 .102 1.493l-.102.007h-9a.75.75 0 0 1-.102-1.493L3.5 13h9h-9zM7.898 1.007L8 1a.75.75 0 0 1 .743.648l.007.102v7.688l2.255-2.254a.75.75 0 0 1 .977-.072l.084.072a.75.75 0 0 1 .072.977l-.072.084L8.53 11.78a.75.75 0 0 1-.976.073l-.084-.073l-3.536-3.535a.75.75 0 0 1 .977-1.133l.084.072L7.25 9.44V1.75a.75.75 0 0 1 .648-.743L8 1l-.102.007z"
fill="currentColor"
></path>
</g>
</svg>
</n-icon>
<span>下载图片</span>
</div>
1 month ago
</div>
</div>
1 month ago
</div>
</div>
</div>
<!-- 搜索 -->
1 month ago
<n-input
class="chat-input"
round
v-model:value="keyWord"
type="textarea"
1 month ago
placeholder="请输入需要统计的数据"
1 month ago
:autosize="{ minRows: 5 }"
>
<template #suffix>
<div class="chat-suffix">
<span>常用问题</span>
1 month ago
<n-button :loading="loading" size="small" :class="['chat-send']" :bordered="false" @click="handleSend">
</n-button>
1 month ago
</div>
</template>
</n-input>
</div>
</template>
<script setup>
1 month ago
import { ref, reactive, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { getAiMsg } from '@/api/ai.js'
import moment from 'moment'
import { LineChart, BarChart, PieChart } from './class/index'
import { AiHint } from '../components'
1 month ago
import userIcon from '@/assets/images/ai/user-icon.png'
import aiIcon from '@/assets/images/ai/ai-icon.png'
import { useMessage } from 'naive-ui'
import useMessageRoomStore from '@/store/modules/messageRoom'
import { canvasCut } from '@/utils'
import { historyMessageRoomById } from '@/api/path'
import { useRoute } from 'vue-router'
const route = useRoute()
const useMessageRoom = useMessageRoomStore()
const messageDialog = useMessage()
//输入框加载
1 month ago
let loading = ref(false)
//搜索关键字
1 month ago
let keyWord = ref('')
//聊天记录list
let messageList = reactive([])
//聊天室容器
const scrollContainer = ref(null)
//聊天记录最后一个索引
let lastIndex = ref(0)
//主题
const echartsTheme = ['简约风', '商务风', '科技风']
let stopWatch = null
1 month ago
/**
* 页面初始完成
*/
onMounted(() => {
startWatching()
getMessageInfo()
})
1 month ago
/**
* 获取聊天详情
*/
const getMessageInfo = async () => {
const id = checkLastPartIsTimestamp(route.path)
try {
const res = await historyMessageRoomById(id)
if (res.code == 200 && res.data) {
res.data.content = JSON.parse(res.data.content)
useMessageRoom.resetList(res.data)
res.data.content.map((item, index) => {
messageList.push(item)
initEcharts(item, index)
})
console.log(res.data)
}
} catch {
messageDialog.error('获取聊天室记录失败!')
}
}
const checkLastPartIsTimestamp = str => {
// 找到最后一个 / 的位置
const lastSlashIndex = str.lastIndexOf('/')
if (lastSlashIndex === -1) {
return false
}
// 截取最后一个 / 后面的值
const lastPart = str.slice(lastSlashIndex + 1)
return lastPart
}
/**
* 问答
*/
1 month ago
const handleSend = async () => {
if (!keyWord.value.trim()) {
messageDialog.warning('请先输入需要统计的数据!')
return
}
if (useMessageRoom.messageRoom.content.length >= 35) {
messageDialog.warning('每个聊天室上限问题为15条')
return
}
loading.value = true
//存储用户回答
setMessageItem({
1 month ago
from: 'user',
text: keyWord.value,
time: moment().format('YYYY/MM/DD HH:mm:ss'),
add: true
1 month ago
})
//先存储AI回答作为交互提示接口响应后再做更新
setMessageItem(
{
from: 'ai',
text: '',
time: moment().format('YYYY/MM/DD HH:mm:ss'),
loading: true, //ai思考中
chartInstanceItem: null,
chartInstanceActiveIndex: 0,
chartType: null,
xData: [],
yData: []
},
false
)
try {
const res = await getAiMsg({ prompt: keyWord.value })
if (res.type) {
// 更新
updataMessageItem({
from: 'ai',
text: '',
time: moment().format('YYYY/MM/DD HH:mm:ss'),
loading: false, //ai思考中
chartInstanceItem: null,
chartInstanceActiveIndex: 0,
chartType: res.chartType,
xData: res.xData,
yData: res.yData,
title: res.title,
unit: res.unit
})
nextTick(() => {
const itemData = messageList[lastIndex.value]
//饼图
if (res.chartType === 'pie') {
itemData.chartInstanceItem = new PieChart(`${res.chartType}${lastIndex.value}`, {
title: res.title,
unit: res.unit,
xData: res.xData,
data: res.yData
})
} else if (res.chartType === 'bar') {
itemData.chartInstanceItem = new BarChart(`${res.chartType}${lastIndex.value}`, {
title: res.title,
unit: res.unit,
xData: res.xData,
data: res.yData
})
} else if (res.chartType === 'line') {
itemData.chartInstanceItem = new LineChart(`${res.chartType}${lastIndex.value}`, {
title: res.title,
unit: res.unit,
xData: res.xData,
data: res.yData
})
}
})
} else {
//纯文本
updataMessageItem({
from: 'ai',
text: res,
time: moment().format('YYYY/MM/DD HH:mm:ss'),
loading: false
})
}
} catch {
updataMessageItem({
from: 'ai',
text: '服务器繁忙,请稍后再试。',
time: moment().format('YYYY/MM/DD HH:mm:ss'),
loading: false
})
}
1 month ago
}
/**
* 同意渲染图表
*/
const initEcharts = (item, index) => {
nextTick(() => {
//饼图
if (item.chartType === 'pie') {
item.chartInstanceItem = new PieChart(`${item.chartType}${index}`, {
title: item.title,
unit: item.unit,
xData: item.xData,
data: item.yData
})
} else if (item.chartType === 'bar') {
item.chartInstanceItem = new BarChart(`${item.chartType}${index}`, {
title: item.title,
unit: item.unit,
xData: item.xData,
data: item.yData
})
} else if (item.chartType === 'line') {
item.chartInstanceItem = new LineChart(`${item.chartType}${index}`, {
title: item.title,
unit: item.unit,
xData: item.xData,
data: item.yData
})
}
})
}
/**
* 存储聊天室数据
* @param obj
*/
const setMessageItem = (obj, set = true) => {
messageList.push(obj)
lastIndex.value = messageList.length - 1
if (set) {
useMessageRoom.setMessage({
from: obj.from,
text: obj.text,
time: obj.time,
add: true
})
}
1 month ago
}
/**
* ai回答后更新对应数据
* @param data
*/
const updataMessageItem = data => {
messageList[lastIndex.value] = data
keyWord.value = ''
loading.value = false
useMessageRoom.setMessage({
from: data.from,
text: data.text,
chartType: data.chartType,
loading: false, //ai思考中
time: data.time,
chartInstanceItem: data.chartInstanceItem,
chartInstanceActiveIndex: 0,
xData: data.xData,
yData: data.yData,
title: data.title,
unit: data.unit
})
}
/**
* 图表切换主题
* @param index
*/
const handlerTheme = (item, index) => {
pauseWatching()
item.chartInstanceActiveIndex = index
item.chartInstanceItem.changeColorStyle(index)
setTimeout(() => {
startWatching()
}, 1000)
}
/**
* 下载AI回答为图片
*/
const uploadImage = index => {
// 导出图片
const range = document.getElementById(`box${index}`)
// 隐藏边距线
if (!range) {
window['$message'].error('下载失败!')
return
}
canvasCut(range)
}
// 聊天室自动滚动条下拉
const startWatching = () => {
stopWatch = watch(messageList, newVal => {
nextTick(() => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
}
})
})
}
const pauseWatching = () => {
if (stopWatch) {
stopWatch() // 调用停止函数
stopWatch = null
}
}
onUnmounted(() => {
//清除
messageList.forEach(instance => {
if (instance.chartType) {
instance.chartInstanceItem.dispose()
}
})
})
1 month ago
</script>
<style lang="scss" scoped>
/* 设置滚动条整体样式 */
::-webkit-scrollbar {
width: 0px;
}
</style>