|
|
|
@ -30,11 +30,13 @@ |
|
|
|
placeholder="请输入您的问题..." |
|
|
|
placeholder="请输入您的问题..." |
|
|
|
@keyup.enter.native="sendMessage" |
|
|
|
@keyup.enter.native="sendMessage" |
|
|
|
></el-input> |
|
|
|
></el-input> |
|
|
|
<el-button |
|
|
|
<el-button |
|
|
|
type="primary" |
|
|
|
|
|
|
|
@click="sendMessage" |
|
|
|
@click="sendMessage" |
|
|
|
:disabled="!newMessage.trim()" |
|
|
|
v-if="!isTyping" type="primary" icon="el-icon-position" circle></el-button> |
|
|
|
>发送</el-button> |
|
|
|
<div class="stop-btn " v-if="isTyping" |
|
|
|
|
|
|
|
@click="showFullAnswer"> |
|
|
|
|
|
|
|
<div class="el-icon-stop"></div> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -57,24 +59,32 @@ export default { |
|
|
|
data() { |
|
|
|
data() { |
|
|
|
return { |
|
|
|
return { |
|
|
|
newMessage: '', |
|
|
|
newMessage: '', |
|
|
|
messages: [ |
|
|
|
messages: [], |
|
|
|
{ |
|
|
|
|
|
|
|
text: '您好!我是AI智能助手,请问有什么可以帮您?', |
|
|
|
|
|
|
|
sender: 'service', |
|
|
|
|
|
|
|
time: this.formatTime(new Date()) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
], |
|
|
|
|
|
|
|
faqs: [ |
|
|
|
faqs: [ |
|
|
|
{ question: '如何查询订单状态?', answer: '您可以在"我的订单"页面查看订单的当前状态和物流信息。' }, |
|
|
|
{ question: '如何查询订单状态?', answer: '您可以在"我的订单"页面查看订单的当前状态和物流信息。' }, |
|
|
|
{ question: '如何办理退货?', answer: '请在订单详情页点击"申请退货",填写相关信息后提交申请。' }, |
|
|
|
{ question: '如何办理退货?', answer: '请在订单详情页点击"申请退货",填写相关信息后提交申请。' }, |
|
|
|
{ question: '支付方式有哪些?', answer: '我们支持支付宝、微信支付、银联等多种支付方式。' }, |
|
|
|
{ question: '支付方式有哪些?', answer: '我们支持支付宝、微信支付、银联等多种支付方式。' }, |
|
|
|
{ question: '会员有哪些优惠?', answer: '会员可享受购物折扣、专属优惠券、生日特权等多重优惠。' }, |
|
|
|
{ question: '会员有哪些优惠?', answer: '会员可享受购物折扣、专属优惠券、生日特权等多重优惠。' }, |
|
|
|
{ question: '商品何时发货?', answer: '一般情况下,我们会在订单支付成功后24小时内安排发货。' } |
|
|
|
{ question: '商品何时发货?', answer: '一般情况下,我们会在订单支付成功后24小时内安排发货。' } |
|
|
|
] |
|
|
|
], |
|
|
|
|
|
|
|
STORAGE_KEY: { |
|
|
|
|
|
|
|
permanent: 'ai_assistant_messages', |
|
|
|
|
|
|
|
temporary: 'ai_assistant_messages_temp' |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
storageType: 'temporary', // 默认使用临时存储 |
|
|
|
|
|
|
|
typewriterInterval: null, // 存储打字效果的定时器引用 |
|
|
|
|
|
|
|
isTyping: false, // 标记是否正在打字 |
|
|
|
|
|
|
|
currentMessageIndex: -1 // 当前正在打字的消息索引 |
|
|
|
}; |
|
|
|
}; |
|
|
|
},mounted() { |
|
|
|
},mounted() { |
|
|
|
|
|
|
|
// 从localStorage加载历史消息 |
|
|
|
|
|
|
|
this.loadMessages(); |
|
|
|
// 初始化时滚动到底部,符合大模型聊天体验 |
|
|
|
// 初始化时滚动到底部,符合大模型聊天体验 |
|
|
|
this.scrollToBottom(); |
|
|
|
this.scrollToBottom(); |
|
|
|
|
|
|
|
// 监听页面卸载事件,可以在这里清除数据 |
|
|
|
|
|
|
|
window.addEventListener('beforeunload', this.handleBeforeUnload); |
|
|
|
|
|
|
|
// 初始化时使用默认存储类型加载消息 |
|
|
|
|
|
|
|
this.loadMessages(this.storageType); |
|
|
|
}, |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
methods: { |
|
|
|
sendMessage() { |
|
|
|
sendMessage() { |
|
|
|
@ -149,12 +159,22 @@ export default { |
|
|
|
typeWriter(messageIndex, fullText) { |
|
|
|
typeWriter(messageIndex, fullText) { |
|
|
|
let currentIndex = 0; |
|
|
|
let currentIndex = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 设置打字状态 |
|
|
|
|
|
|
|
this.isTyping = true; |
|
|
|
|
|
|
|
this.currentMessageIndex = messageIndex; |
|
|
|
|
|
|
|
|
|
|
|
// 优化滚动:先滚动到底部 |
|
|
|
// 优化滚动:先滚动到底部 |
|
|
|
this.$nextTick(() => { |
|
|
|
this.$nextTick(() => { |
|
|
|
this.scrollToBottom(); |
|
|
|
this.scrollToBottom(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const typeInterval = setInterval(() => { |
|
|
|
// 清除之前可能存在的定时器 |
|
|
|
|
|
|
|
if (this.typewriterInterval) { |
|
|
|
|
|
|
|
clearInterval(this.typewriterInterval); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 存储定时器引用 |
|
|
|
|
|
|
|
this.typewriterInterval = setInterval(() => { |
|
|
|
if (currentIndex < fullText.length) { |
|
|
|
if (currentIndex < fullText.length) { |
|
|
|
// 优化打字速度:根据文本长度动态调整速度 |
|
|
|
// 优化打字速度:根据文本长度动态调整速度 |
|
|
|
let typeSpeed = 15; // 加快速度到15毫秒/字符 |
|
|
|
let typeSpeed = 15; // 加快速度到15毫秒/字符 |
|
|
|
@ -184,12 +204,34 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
// 打字完成 |
|
|
|
// 打字完成 |
|
|
|
clearInterval(typeInterval); |
|
|
|
this.stopTyping(); |
|
|
|
// 最终滚动到底部 |
|
|
|
|
|
|
|
this.scrollToBottom(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
}, 15); // 加快打字速度到15毫秒/字符 |
|
|
|
}, 15); // 加快打字速度到15毫秒/字符 |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
// 停止打字效果 |
|
|
|
|
|
|
|
stopTyping() { |
|
|
|
|
|
|
|
if (this.typewriterInterval) { |
|
|
|
|
|
|
|
clearInterval(this.typewriterInterval); |
|
|
|
|
|
|
|
this.typewriterInterval = null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
this.isTyping = false; |
|
|
|
|
|
|
|
this.currentMessageIndex = -1; |
|
|
|
|
|
|
|
// 最终滚动到底部 |
|
|
|
|
|
|
|
this.scrollToBottom(); |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 立即显示完整答案并停止打字效果 |
|
|
|
|
|
|
|
showFullAnswer() { |
|
|
|
|
|
|
|
if (this.isTyping && this.currentMessageIndex >= 0 && this.currentMessageIndex < this.messages.length) { |
|
|
|
|
|
|
|
// 获取当前正在处理的service消息 |
|
|
|
|
|
|
|
const currentMessage = this.messages[this.currentMessageIndex]; |
|
|
|
|
|
|
|
if (currentMessage && currentMessage.sender === 'service') { |
|
|
|
|
|
|
|
// 停止打字效果 |
|
|
|
|
|
|
|
this.stopTyping(); |
|
|
|
|
|
|
|
// 这里我们无法直接获取完整文本,所以只保留已有的部分 |
|
|
|
|
|
|
|
// 在实际应用中,你可能需要将完整文本存储在某个地方以便快速显示 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
selectFaq(faq) { |
|
|
|
selectFaq(faq) { |
|
|
|
// 添加用户选择的常见问题 |
|
|
|
// 添加用户选择的常见问题 |
|
|
|
this.messages.push({ |
|
|
|
this.messages.push({ |
|
|
|
@ -315,12 +357,95 @@ export default { |
|
|
|
const hours = date.getHours().toString().padStart(2, '0'); |
|
|
|
const hours = date.getHours().toString().padStart(2, '0'); |
|
|
|
const minutes = date.getMinutes().toString().padStart(2, '0'); |
|
|
|
const minutes = date.getMinutes().toString().padStart(2, '0'); |
|
|
|
return `${hours}:${minutes}`; |
|
|
|
return `${hours}:${minutes}`; |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 处理页面卸载前的事件 |
|
|
|
|
|
|
|
handleBeforeUnload() { |
|
|
|
|
|
|
|
// 只在临时存储时清除记录 |
|
|
|
|
|
|
|
if (this.storageType === 'temporary') { |
|
|
|
|
|
|
|
this.clearChatHistory('temporary'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 清除聊天记录 |
|
|
|
|
|
|
|
clearChatHistory() { |
|
|
|
|
|
|
|
// 保留第一条欢迎消息 |
|
|
|
|
|
|
|
this.messages = [{...this.messages[0]}]; |
|
|
|
|
|
|
|
// 清空输入框 |
|
|
|
|
|
|
|
this.newMessage = ''; |
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
|
|
|
|
this.scrollToBottom(); |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 从存储加载消息 |
|
|
|
|
|
|
|
loadMessages(type = this.storageType) { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const storage = type === 'permanent' ? localStorage : sessionStorage; |
|
|
|
|
|
|
|
const savedMessages = storage.getItem(this.STORAGE_KEY[type]); |
|
|
|
|
|
|
|
if (savedMessages) { |
|
|
|
|
|
|
|
this.messages = JSON.parse(savedMessages); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// 如果没有保存的消息,显示欢迎消息 |
|
|
|
|
|
|
|
this.messages = [{ |
|
|
|
|
|
|
|
text: '您好!我是AI智能助手,请问有什么可以帮您?', |
|
|
|
|
|
|
|
sender: 'service', |
|
|
|
|
|
|
|
time: this.formatTime(new Date()) |
|
|
|
|
|
|
|
}]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error('加载消息失败:', error); |
|
|
|
|
|
|
|
// 加载失败时显示默认消息 |
|
|
|
|
|
|
|
this.messages = [{ |
|
|
|
|
|
|
|
text: '您好!我是AI智能助手,请问有什么可以帮您?', |
|
|
|
|
|
|
|
sender: 'service', |
|
|
|
|
|
|
|
time: this.formatTime(new Date()) |
|
|
|
|
|
|
|
}]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 保存消息到存储 |
|
|
|
|
|
|
|
saveMessages(type = this.storageType) { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const storage = type === 'permanent' ? localStorage : sessionStorage; |
|
|
|
|
|
|
|
storage.setItem(this.STORAGE_KEY[type], JSON.stringify(this.messages)); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error('保存消息失败:', error); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 清除对话记录 |
|
|
|
|
|
|
|
clearChatHistory(type = this.storageType) { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const storage = type === 'permanent' ? localStorage : sessionStorage; |
|
|
|
|
|
|
|
storage.removeItem(this.STORAGE_KEY[type]); |
|
|
|
|
|
|
|
this.messages = [{ |
|
|
|
|
|
|
|
text: '您好!我是AI智能助手,请问有什么可以帮您?', |
|
|
|
|
|
|
|
sender: 'service', |
|
|
|
|
|
|
|
time: this.formatTime(new Date()) |
|
|
|
|
|
|
|
}]; |
|
|
|
|
|
|
|
this.scrollToBottom(); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error('清除消息失败:', error); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 设置存储类型 |
|
|
|
|
|
|
|
setStorageType(type) { |
|
|
|
|
|
|
|
if (['permanent', 'temporary'].includes(type)) { |
|
|
|
|
|
|
|
this.storageType = type; |
|
|
|
|
|
|
|
// 切换存储类型时重新加载消息 |
|
|
|
|
|
|
|
this.loadMessages(type); |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
// 处理页面卸载前的事件 |
|
|
|
|
|
|
|
handleBeforeUnload() { |
|
|
|
|
|
|
|
// 这里可以根据需求决定是否在关闭浏览器时清除数据 |
|
|
|
|
|
|
|
// 如果需要清除,可以取消下面这行代码的注释 |
|
|
|
|
|
|
|
// this.clearChatHistory(); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
watch: { |
|
|
|
watch: { |
|
|
|
messages: { |
|
|
|
messages: { |
|
|
|
handler() { |
|
|
|
handler() { |
|
|
|
// 当消息数组发生变化时,滚动到底部,确保最新消息可见 |
|
|
|
// 当消息数组发生变化时,保存到localStorage |
|
|
|
|
|
|
|
this.saveMessages(); |
|
|
|
|
|
|
|
// 滚动到底部,确保最新消息可见 |
|
|
|
this.scrollToBottom(); |
|
|
|
this.scrollToBottom(); |
|
|
|
}, |
|
|
|
}, |
|
|
|
deep: true |
|
|
|
deep: true |
|
|
|
@ -347,6 +472,13 @@ export default { |
|
|
|
border-top-left-radius: 4px; |
|
|
|
border-top-left-radius: 4px; |
|
|
|
border-top-right-radius: 4px; |
|
|
|
border-top-right-radius: 4px; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.service-header h2 { |
|
|
|
|
|
|
|
margin: 0 0 5px 0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
.service-header p { |
|
|
|
|
|
|
|
margin: 0; |
|
|
|
|
|
|
|
opacity: 0.9; |
|
|
|
|
|
|
|
} |
|
|
|
.service-container { |
|
|
|
.service-container { |
|
|
|
display: flex; |
|
|
|
display: flex; |
|
|
|
min-height: 400px; |
|
|
|
min-height: 400px; |
|
|
|
@ -403,9 +535,35 @@ export default { |
|
|
|
padding: 15px; |
|
|
|
padding: 15px; |
|
|
|
border-top: 1px solid #e6e6e6; |
|
|
|
border-top: 1px solid #e6e6e6; |
|
|
|
display: flex; |
|
|
|
display: flex; |
|
|
|
|
|
|
|
gap: 10px; |
|
|
|
} |
|
|
|
} |
|
|
|
.chat-input .el-input { |
|
|
|
.chat-input .el-input { |
|
|
|
margin-right: 10px; |
|
|
|
margin-right: 10px; |
|
|
|
|
|
|
|
flex: 1; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
.chat-input .el-button { |
|
|
|
|
|
|
|
white-space: nowrap; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stop-btn { |
|
|
|
|
|
|
|
align-items: center; |
|
|
|
|
|
|
|
border: 2px solid rgba(73, 113, 245, .5); |
|
|
|
|
|
|
|
border-radius: 20px; |
|
|
|
|
|
|
|
display: flex; |
|
|
|
|
|
|
|
height: 36px; |
|
|
|
|
|
|
|
justify-content: center; |
|
|
|
|
|
|
|
width: 36px; |
|
|
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
|
|
transition: all .2s; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stop-btn .el-icon-stop { |
|
|
|
|
|
|
|
background: linear-gradient(0deg, #4971f5, #4971f5), #fff; |
|
|
|
|
|
|
|
border-radius: 4px; |
|
|
|
|
|
|
|
flex-shrink: 0; |
|
|
|
|
|
|
|
height: 13px; |
|
|
|
|
|
|
|
width: 13px; |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
.faq-item { |
|
|
|
.faq-item { |
|
|
|
padding: 10px; |
|
|
|
padding: 10px; |
|
|
|
|