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.

404 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="chat-container">
<div class="chat-list" ref="scrollContainer">
<AiHint></AiHint>
<div
v-for="(item, index) in messageList"
: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 v-show="item.type == 'text'">{{ item.text }}</span>
<AddIssue v-show="item.add"></AddIssue>
</div>
<!-- 图表 -->
<section v-if="ehcatrsType.includes(item.type)">
<div class="echarts-box" :id="item.type + 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>
<!-- 表格 -->
<AiTable v-if="!item.type" :obj="item"></AiTable>
<!-- -->
<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.loading">
<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>
</div>
</div>
</div>
</div>
</div>
<!-- 搜索 -->
<n-input
class="chat-input"
round
v-model:value="keyWord"
type="textarea"
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>
</div>
</template>
</n-input>
</div>
</template>
<script setup>
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, AiTable,AddIssue } from '../components'
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()
//输入框加载
let loading = ref(false)
//搜索关键字
let keyWord = ref('')
//聊天记录list
let messageList = reactive([])
//聊天室容器
const scrollContainer = ref(null)
//聊天记录最后一个索引
let lastIndex = ref(0)
//主题
const echartsTheme = ['简约风', '商务风', '科技风']
let stopWatch = null
let isWatching = ref(false)
const ehcatrsType = ['pie', 'line', 'bar']
/**
* 页面初始完成
*/
onMounted(() => {
startWatching()
getMessageInfo()
})
/**
* 获取聊天详情
*/
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)
console.log(res.data)
useMessageRoom.resetList(res.data)
res.data.content.map((item, index) => {
item.chartInstanceActiveIndex = 0
messageList.push(item)
initEcharts(item, index)
})
}
} catch {
messageDialog.error('获取聊天室记录失败!')
}
}
const checkLastPartIsTimestamp = str => {
// 找到最后一个 / 的位置
const lastSlashIndex = str.lastIndexOf('/')
if (lastSlashIndex === -1) {
return false
}
// 截取最后一个 / 后面的值
const lastPart = str.slice(lastSlashIndex + 1)
return lastPart
}
/**
* 问答
*/
const handleSend = async () => {
if (!keyWord.value.trim()) {
messageDialog.warning('请先输入需要统计的数据!')
return
}
if (useMessageRoom.messageRoom.content.length >= 35) {
messageDialog.warning('每个聊天室上限问题为15条')
return
}
loading.value = true
//存储用户回答
setMessageItem({
from: 'user',
text: keyWord.value,
type:'text',
time: moment().format('YYYY/MM/DD HH:mm:ss'),
add: true
})
return
//先存储AI回答作为交互提示接口响应后再做更新
setMessageItem(
{
from: 'ai',
text: '',
time: moment().format('YYYY/MM/DD HH:mm:ss'),
loading: true, //ai思考中
chartInstanceItem: null,
chartInstanceActiveIndex: 0,
type:'text',
xData: [],
yData: []
},
false
)
try {
const res = await getAiMsg({ prompt: keyWord.value })
// 更新
updataMessageItem({
from: 'ai',
text: '',
time: moment().format('YYYY/MM/DD HH:mm:ss'),
loading: false, //ai思考中
chartInstanceItem: null,
chartInstanceActiveIndex: 0,
type: res.type,
xData: [],
data: [],
title: res.title,
unit: res.unit
})
nextTick(() => {
const itemData = messageList[lastIndex.value]
//饼图
if (res.type === 'pie') {
itemData.chartInstanceItem = new PieChart(`${res.type}${lastIndex.value}`, {
title: res.title,
unit: res.unit,
data: res.data
})
} else if (res.type === 'bar') {
itemData.chartInstanceItem = new BarChart(`${res.type}${lastIndex.value}`, {
title: res.title,
unit: res.unit,
xData: res.xData,
data: res.data
})
} else if (res.type === 'line') {
itemData.chartInstanceItem = new LineChart(`${res.type}${lastIndex.value}`, {
title: res.title,
unit: res.unit,
xData: res.xData,
data: res.data
})
} else {
//纯文本
updataMessageItem({
from: 'ai',
title: res.title,
type: res.type,
text: res.data,
time: moment().format('YYYY/MM/DD HH:mm:ss'),
loading: false
})
}
})
} catch {
updataMessageItem({
from: 'ai',
type: 'text',
text: '服务器繁忙,请稍后再试。',
time: moment().format('YYYY/MM/DD HH:mm:ss'),
loading: false
})
}
}
/**
* 同意渲染图表
*/
const initEcharts = (item, index) => {
nextTick(() => {
//饼图
if (item.type === 'pie') {
item.chartInstanceItem = new PieChart(`${item.type}${index}`, {
title: item.title,
unit: item.unit,
data: item.data
})
} else if (item.type === 'bar') {
item.chartInstanceItem = new BarChart(`${item.type}${index}`, {
title: item.title,
unit: item.unit,
xData: item.xData,
data: item.yData
})
} else if (item.type === 'line') {
item.chartInstanceItem = new LineChart(`${item.type}${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(obj)
}
}
/**
* ai回答后更新对应数据
* @param data
*/
const updataMessageItem = data => {
messageList[lastIndex.value] = data
keyWord.value = ''
loading.value = false
useMessageRoom.setMessage(data)
// useMessageRoom.setMessage({
// from: data.from,
// text: data.text,
// type: data.type,
// 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) => {
if (isWatching.value) {
pauseWatching()
isWatching.value = false
console.log('暂停')
}
item.chartInstanceActiveIndex = index
item.chartInstanceItem.changeColorStyle(index)
setTimeout(() => {
startWatching()
console.log('监听')
}, 5000)
}
/**
* 下载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
}
})
},{deep:true})
isWatching.value = true
}
const pauseWatching = () => {
if (stopWatch) {
stopWatch() // 调用停止函数
stopWatch = null
}
}
onUnmounted(() => {
//清除
messageList.forEach(instance => {
if (ehcatrsType.includes( instance.type) && instance.type) {
instance.chartInstanceItem.dispose()
}
})
})
</script>
<style lang="scss" scoped>
/* 设置滚动条整体样式 */
::-webkit-scrollbar {
width: 0px;
}
</style>