feat(AI): 优化聊天组件并添加新功能

- 移除 request 和 response 拦截器中的 console.log 语句- 优化消息组件样式和逻辑,支持渐进式消息显示- 更新 TabBox 组件,支持动态列表数据
- 重构 treasureBox 组件,展示产品详情和知识库
- 优化 AI聊天流程,支持流式响应和消息动画
- 添加 markdown 渲染支持,包括数学公式和流程图
This commit is contained in:
陈昱达
2025-06-05 16:39:45 +08:00
parent 979fde4c85
commit 15ddd03c7a
8 changed files with 3296 additions and 2110 deletions

View File

@@ -24,8 +24,10 @@
"eruda": "^2.11.3",
"fastclick": "^1.0.6",
"markdown-it": "^12.3.2",
"markdown-it-katex": "^2.0.3",
"markdown-it-mermaid": "^0.2.5",
"mermaid": "^10.9.3",
"mermaid-it-markdown": "^1.0.8",
"sass": "^1.69.3",
"svg-sprite-loader": "^6.0.11",
"swiper": "^5.4.5",
@@ -45,13 +47,18 @@
"@vue/cli-service": "^3.9.0",
"@vue/eslint-config-prettier": "^4.0.1",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "^10.4.21",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-plugin-import": "^1.12.0",
"browserslist": "^4.25.0",
"caniuse-lite": "^1.0.30001721",
"eslint": "^5.16.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-vue": "^5.0.0",
"postcss": "^8.5.4",
"postcss-loader": "^4.0.3",
"postcss-px-to-viewport": "^1.1.1",
"sass-loader": "^7.1.0",
"vee-validate": "^2.0.0-rc.25",

View File

@@ -8,7 +8,6 @@ const service = axios.create({
// request拦截器
service.interceptors.request.use(
(config) => {
console.log(config)
if (!config.headers.noLoading) {
Toast.loading({
duration: 0,
@@ -30,7 +29,6 @@ service.interceptors.request.use(
// respone拦截器
service.interceptors.response.use(
(response) => {
console.log(response, 23)
if (response.config.sts) {
return res
}
@@ -85,7 +83,6 @@ service.interceptors.response.use(
}
},
(error) => {
console.log(error)
Toast(error.message ? error.message : '未知异常')
return Promise.reject(error)
}

View File

@@ -1,12 +1,12 @@
<template>
<div >
<div class='tab-box' v-for='item in 2'>
<div class='tab-box' v-for='item in list'>
<div class='box-title'>
这个标题
{{item.title}}
</div>
<div class='box-container'>
<div v-for='item in 5'>
内容
<div v-for='list in item.list'>
{{list.title}}
</div>
</div>
</div>
@@ -18,7 +18,12 @@ export default {
data() {
return {}
},
props: {},
props: {
list: {
type: Array,
default: () => []
}
},
watch: {},
components: {},
filters: {},

View File

@@ -1,12 +1,12 @@
<template>
<section>
<div v-for="(message, index) in messagesList" :key="index" class="message-item">
<div v-for="(message, index) in messagesList" :key="index" class="message-item ">
<!--用户消息-->
<div v-if="message.type === 'user'" class="user-message">
<div v-if="message.type === 'user'" class="user-message mb10">
<p>{{ message.text }}</p>
</div>
<!--机器人消息-->
<div v-else-if="message.type === 'bot'" class="bot-message">
<div v-else-if="message.type === 'bot'" class="bot-message mb10">
<span v-if="message.isThink">
<!-- loading-->
<span class="speakLoadingToast pv10">
@@ -18,18 +18,18 @@
<p v-html="md.render(message.think)" v-if="message.think && message.showThink" class="thinkText" />
</span>
<div>
<p v-html="md.render(message.text)" v-if="message.text"></p>
<span class="speakLoadingToast pv10" v-else>
<p v-html="md.render(message.text)" v-if="message.text "></p>
<span class="speakLoadingToast pv10" v-else-if='!message.text && !thinkOk'>
<van-loading type="spinner" color="#2e5ca9" size="20px" v-if="!message.text" />
</span>
</div>
</div>
<!--百宝箱-->
<div v-else>
<div v-else class='mb10'>
<treasure-box :item="message"></treasure-box>
</div>
<!-- 新增点赞和踩按钮 -->
<div class="reaction-buttons" v-if="message.type !== 'user'">
<div class="reaction-buttons mb10" v-if="message.type !== 'user'">
<button @click="handleReaction(message, 'like')" class="like">
<svg-icon :icon-class="message.isLike ? 'fillLike' : 'like'" class-name="chat-icon"></svg-icon> 有用
</button>
@@ -47,16 +47,12 @@ import { Icon } from 'vant'
import TreasureBox from '@/views/AI/components/treasureBox.vue'
import MarkdownIt from 'markdown-it'
import markdownItKatex from 'markdown-it-katex'
// import mermaidItMarkdown from 'mermaid-it-markdown'
console.log(mermaidItMarkdown)
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
})
.use(markdownItKatex)
// .use(mermaidItMarkdown)
export default {
name: 'message',
components: { TreasureBox, [Icon.name]: Icon },
@@ -65,6 +61,10 @@ export default {
type: Array,
default: () => [],
},
thinkOk: {
type: Boolean,
default: false,
},
isDeep: {
type: Boolean,
default: false,
@@ -133,6 +133,8 @@ $primary-trans-color: rgba(135, 162, 208, 0.5); // 使用rgba定义颜色
.user-message {
justify-content: end;
max-width: 80%;
margin-left: 20%;
}
.bot-message {
justify-content: start;

View File

@@ -2,14 +2,15 @@
<div class="treasure-box">
<h3>{{ item.text.indexOf('工具箱') !== -1 ? item.text : item.text + '工具箱' }}</h3>
<van-tabs v-model="active" color="#2E5CA9" title-active-color="#2E5CA9" line-width="20">
<van-tab title="常用工具">
<TabBox></TabBox>
</van-tab>
<van-tab title="爆款图文">
<TabBox></TabBox>
</van-tab>
<!-- <van-tab title="爆款图文">-->
<!-- <TabBox></TabBox>-->
<!-- </van-tab>-->
<van-tab title="产品知识">
<TabBox></TabBox>
<TabBox :list='knowledge'></TabBox>
</van-tab>
<van-tab title="常用工具">
<TabBox :list='tools'></TabBox>
</van-tab>
</van-tabs>
<!-- 在这里添加百宝箱的具体内容 -->
@@ -37,22 +38,56 @@ export default {
return {
active: 0,
// 可以在这里添加百宝箱相关的数据
// 工具列表
tools: [],
knowledge: [],
}
},
watch: {
'item.text': {
handler(newValue, oldValue) {
console.log(this.item)
if(!this.item.detail){
this.getTreasureBox()
} else {
this.setList(this.item.detail)
}
},
immediate: true,
},
},
methods: {
setList(){
this.tools = [
{title: '工具',
list:[
{value:this.item.detail.instructionUrl,title:'产品说明书' },
{value:this.item.detail.clauseUrl,title:'条款' },
]},
]
this.knowledge = []
for(let i in this.item.detail.knowledge){
this.knowledge.push({
title:i,
list:this.item.detail.knowledge[i].split(',').map(item=>{
return {
title:item,
value:item
}
})
})
}
},
// 可以在这里添加百宝箱相关的功能方法
async getTreasureBox() {
productDetail({ query: this.item.text }).then((res) => {
console.log(res)
productDetail({ productName: this.item.text }).then((res) => {
if(res){
this.$set(this.item,'detail',res.content)
this.setList()
}
})
},
},

View File

@@ -4,7 +4,7 @@
<div class="chat-content">
<div class="message-area" ref="messageArea" @scroll="handleScroll">
<HotProducts class="mb10" :messagesList.sync="messages"></HotProducts>
<messageComponent :messagesList="messages" :is-deep="isDeep" :is-search="isSearching"></messageComponent>
<messageComponent :messagesList="messages" :is-deep="isDeep" :is-search="isSearching" :think-ok='isThink'></messageComponent>
</div>
</div>
</main>
@@ -46,6 +46,9 @@ export default {
data() {
return {
answerMap:'',
timer:null,
answerIndex:0,
conversationId: '',
currentMessage: null,
messageStatus: 'stop',
@@ -75,13 +78,16 @@ export default {
chatProduct({ query: this.newMessage }).then((res) => {
if (res) {
this.messageStatus = 'stop'
this.messages.push({ type: 'box', text: this.newMessage })
this.messages.push({ type: 'box', text: this.newMessage,detail:res.content })
this.newMessage = ''
}
})
},
sendMessage() {
if(this.messageStatus === 'send'){
return
}
if (this.newMessage.trim() === '') return
this.messages.push({ type: 'user', text: this.newMessage })
@@ -157,7 +163,10 @@ export default {
signal: abortController.signal,
body: JSON.stringify(params),
})
.then((res) => this.processStreamResponse(res))
.then(async (res) =>{
this.newMessage = ''
await this.processStreamResponse(res)
})
.catch((err) => console.error('请求错误:', err))
},
@@ -198,7 +207,11 @@ export default {
const cleanLine = line.replace(/^data:\s*/, '')
if (!cleanLine) return null
const data = JSON.parse(cleanLine)
console.log(data)
if(data.answer){
this.answerMap+=data.answer
}
this.updateConversationState(data)
return data
} catch (error) {
@@ -216,20 +229,34 @@ export default {
this.isThink = false
}
},
updateMessageContent({ answer }) {
if (!this.currentMessage || !answer) return
// 使用 requestAnimationFrame 控制文本逐步显示
if (answer) {
if (this.isThink) {
this.currentMessage.think += answer
} else {
this.currentMessage.text += answer
}
updateMessageContent({ answer, event }) {
if(event === 'message_end'){
this.messageStatus = 'stop'
}
this.scrollToBottom()
},
if (!this.currentMessage || !answer) return;
const mode = this.isThink ? 'think' : 'text';
// 清除之前的动画任务
if (this.timer) {
cancelAnimationFrame(this.timer);
}
const renderChar = () => {
if (this.answerIndex < this.answerMap.length) {
this.currentMessage[mode] += this.answerMap[this.answerIndex++];
// this.$nextTick(() => {
this.timer = requestAnimationFrame(renderChar);
// });
} else {
this.scrollToBottom();
}
};
this.timer = requestAnimationFrame(renderChar);
}
},
watch: {

View File

@@ -1,42 +1,48 @@
const autoprefixer = require('autoprefixer')
const pxtoviewport = require('postcss-px-to-viewport')
const path = require('path')
const autoprefixer = require('autoprefixer');
const pxtoviewport = require('postcss-px-to-viewport');
const path = require('path');
function resolve(dir) {
return path.join(__dirname, dir)
return path.join(__dirname, dir);
}
module.exports = {
publicPath: '/',
lintOnSave: false, //是否开启代码检查
outputDir: 'dist', //打包输出目录
lintOnSave: false,
outputDir: 'dist',
productionSourceMap: false,
devServer: {
https: false,
host: "0.0.0.0",
disableHostCheck: true
host: '0.0.0.0',
disableHostCheck: true,
},
css: {
sourceMap: true, // 查看css属于哪个css文件
loaderOptions: {
sourceMap: true,
loaderOptions: {
postcss: {
plugins: [
autoprefixer(),
pxtoviewport({
viewportWidth: 375,
// 该项仅在使用 Circle 组件时需要
// 原因参见 https://github.com/youzan/vant/issues/1948
selectorBlackList: ['van-circle__layer']
})
]
postcssOptions: {
plugins: [
autoprefixer(), // 自动加浏览器前缀
pxtoviewport({ // px 转 viewport
viewportWidth: 375,
selectorBlackList: ['van-circle__layer'] // 排除vant circle组件样式转换
})
]
}
}
}
},
chainWebpack: (config) => {
// 移除 prefetch 插件
config.resolve.alias.set('@utils', resolve('./src/assets/js/utils'))
// 设置路径别名
config.resolve.alias
.set('@', resolve('src'))
.set('@utils', resolve('src/assets/js/utils'));
// config.plugins.delete('prefetch')
/* 配置svg图标自动加载 begin */
config.module.rule('svg').exclude.add(resolve('src/icons')).end()
// 移除 prefetch 插件(减少初始加载体积)
config.plugins.delete('prefetch');
// SVG 图标配置
config.module.rule('svg').exclude.add(resolve('src/icons')).end();
config.module
.rule('icons')
.test(/\.svg$/)
@@ -45,8 +51,10 @@ module.exports = {
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
symbolId: 'icon-[name]',
});
// 处理 .mjs 文件(用于 mermaid 等模块)
config.module
.rule('mjs')
.test(/\.mjs$/)
@@ -54,20 +62,37 @@ module.exports = {
.end()
.use('babel-loader')
.loader('babel-loader');
//设置路径别名
// 添加对现代 JS 的支持(如 ?? 和 ?.
config.module
.rule('js')
.test(/\.js$/)
.use('babel-loader')
.loader('babel-loader')
.options({
presets: ['@babel/preset-env'],
});
},
configureWebpack: (config) => {
;(config.devtool = 'source-map'), // 调试js
(config.performance = {
hints: 'error',
//入口起点的最大体积 700kb
maxEntrypointSize: 7168000,
//生成文件的最大体积 700kb
maxAssetSize: 7168000,
//只给出 js 文件的性能提示
assetFilter: function (assetFilename) {
return assetFilename.endsWith('.js')
}
})
}
}
// 强制使用 CommonJS 模块加载 mermaid
config.resolve = config.resolve || {};
config.resolve.alias = config.resolve.alias || {};
// config.resolve.alias.mermaid$ = path.resolve(
// __dirname,
// './node_modules/mermaid/dist/mermaid.common.js'
// );
// Webpack devtool 设置
config.devtool = 'source-map';
// 打包性能提示设置
config.performance = {
hints: 'error',
maxEntrypointSize: 7168000, // 7MB
maxAssetSize: 7168000, // 7MB
assetFilter: function (assetFilename) {
return assetFilename.endsWith('.js');
},
};
},
};

5132
yarn.lock

File diff suppressed because it is too large Load Diff