pet/pages/assistant/assistant.vue

855 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="assistant-container page-container-unified">
<!-- 宠物助手信息卡片 -->
<view class="assistant-info-card">
<view class="assistant-avatar-wrapper">
<view class="assistant-avatar">
<text class="avatar-emoji">🤖</text>
</view>
</view>
<view class="assistant-info-text">
<text class="assistant-name">宠物助手</text>
<text class="assistant-status">专业的宠物护理顾问</text>
</view>
<view class="chat-status">
<view class="status-dot"></view>
<text class="status-text">在线</text>
</view>
<view class="knowledge-icon" @click="openKnowledge">
<text class="icon-text">📚</text>
</view>
</view>
<!-- 聊天消息列表 -->
<view class="chat-messages">
<scroll-view
class="chat-scroll"
scroll-y
:scroll-top="scrollTop"
scroll-with-animation
>
<view class="message-list">
<!-- 消息项 -->
<template v-for="(message, index) in messageList" :key="index">
<!-- AI消息 -->
<view class="message-item ai" v-if="message.type === 'ai'">
<view class="message-avatar">
<view class="assistant-avatar">
<text class="avatar-emoji">🤖</text>
</view>
</view>
<view class="message-content ai">
<view class="message-bubble ai" @longpress="copyMessage(message.content)">
<view class="message-text" v-if="!(isThinking && index === messageList.length - 1)">
<u-markdown :content="message.content" :showLine="false"></u-markdown>
</view>
<view class="typing-dots" v-else>
<view class="typing-dot"></view>
<view class="typing-dot"></view>
<view class="typing-dot"></view>
</view>
</view>
<text class="message-time" v-if="!(isThinking && index === messageList.length - 1)">{{ message.time }}</text>
</view>
</view>
<!-- 用户消息 -->
<view class="message-item user" v-if="message.type === 'user'">
<view class="message-content user">
<view class="message-bubble user" @longpress="copyMessage(message.content)">
<view class="message-text">
<u-markdown :content="message.content" :showLine="false"></u-markdown>
</view>
</view>
<text class="message-time">{{ message.time }}</text>
</view>
<view class="message-avatar">
<view class="user-avatar">
<text class="avatar-emoji">👤</text>
</view>
</view>
</view>
</template>
<!-- AI思考中状态 -->
<view class="message-item ai" v-if="isThinking">
<view class="message-avatar">
<view class="assistant-avatar">
<text class="avatar-emoji">🤖</text>
</view>
</view>
<view class="message-content ai">
<view class="message-bubble ai typing-bubble">
<view class="typing-dots">
<view class="typing-dot"></view>
<view class="typing-dot"></view>
<view class="typing-dot"></view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 输入区域 -->
<view class="chat-input-area">
<view class="input-container">
<view class="input-wrapper">
<input
v-model="inputMessage"
placeholder="和宠物助手说点什么..."
class="message-input"
@confirm="sendMessage"
confirm-type="send"
maxlength="500"
/>
</view>
<view class="send-button" @click="sendMessage" :class="{ active: inputMessage.trim() }">
<text class="send-text">发送</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
inputMessage: '',
scrollTop: 0,
isThinking: false,
userAvatar: '/static/user-avatar.png',
messageList: [
{
type: 'ai',
content: '您好我是您的宠物AI助手🐾\n\n我可以为您解答关于宠物饲养、健康、训练、营养等方面的问题。\n\n**我能帮你做什么:**\n- 🍖 **饮食建议**:营养搭配、食物选择\n- 🏥 **健康咨询**:症状分析、预防措施 \n- 🎾 **训练指导**:行为纠正、技能训练\n- 💡 **日常护理**:清洁、美容、环境\n\n有什么想了解的吗',
time: this.getCurrentTime()
}
],
quickQuestions: [
{ id: 1, text: '猫咪不吃饭怎么办?' },
{ id: 2, text: '狗狗需要多久洗一次澡?' },
{ id: 3, text: '宠物疫苗接种时间' },
{ id: 4, text: '如何训练宠物定点上厕所?' },
{ id: 5, text: '宠物发烧的症状' },
{ id: 6, text: '幼猫喂养注意事项' }
],
// 知识库数据
knowledgeBase: {
'喂食': {
keywords: ['喂食', '吃饭', '食物', '饮食', '营养'],
responses: [
'定时定量喂食很重要成年猫每天2-3次幼猫3-4次。',
'选择优质的宠物食品,避免给宠物吃人类食物。',
'确保宠物随时有清洁的饮用水。'
]
},
'健康': {
keywords: ['健康', '生病', '症状', '发烧', '呕吐', '腹泻'],
responses: [
'定期体检很重要,建议每年至少一次。',
'注意观察宠物的食欲、精神状态和排便情况。',
'如果出现异常症状,请及时就医。'
]
},
'疫苗': {
keywords: ['疫苗', '接种', '免疫', '预防'],
responses: [
'幼猫幼犬8-12周开始接种疫苗。',
'成年宠物每年需要加强免疫。',
'常见疫苗包括三联疫苗和狂犬疫苗。'
]
},
'训练': {
keywords: ['训练', '教育', '行为', '定点', '上厕所'],
responses: [
'训练需要耐心和一致性。',
'使用正向强化,奖励好行为。',
'避免体罚,这会让宠物产生恐惧。'
]
},
'洗澡': {
keywords: ['洗澡', '清洁', '卫生'],
responses: [
'狗狗一般1-2周洗一次澡猫咪通常不需要经常洗澡。',
'使用宠物专用洗浴用品。',
'洗澡后要及时吹干,避免感冒。'
]
}
}
}
},
methods: {
getCurrentTime() {
const now = new Date()
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
},
askQuestion(question) {
this.inputMessage = question
this.sendMessage()
},
sendMessage() {
if (!this.inputMessage.trim() || this.isThinking) return
// 保存用户输入内容
const userInput = this.inputMessage
// 添加用户消息
const userMessage = {
type: 'user',
content: userInput,
time: this.getCurrentTime()
}
this.messageList.push(userMessage)
// 清空输入框
this.inputMessage = ''
// 开始AI思考
this.isThinking = true
// 确保DOM更新后再滚动到思考状态
this.$nextTick(() => {
this.forceScrollToBottom()
// 在思考过程中持续滚动,确保思考动画可见
setTimeout(() => {
this.forceScrollToBottom()
}, 200)
setTimeout(() => {
this.forceScrollToBottom()
}, 500)
})
// 模拟AI回复
setTimeout(() => {
const aiMessage = {
type: 'ai',
content: this.getAIResponse(userInput),
time: this.getCurrentTime()
}
this.messageList.push(aiMessage)
this.isThinking = false
// AI回复完成后强制滚动到底部
this.$nextTick(() => {
this.forceScrollToBottom()
// 延迟再次滚动,确保消息完全显示
setTimeout(() => {
this.forceScrollToBottom()
}, 100)
})
}, 1500 + Math.random() * 1000) // 1.5-2.5秒随机延迟
},
getAIResponse(question) {
// 智能匹配知识库
for (let category in this.knowledgeBase) {
const { keywords, responses } = this.knowledgeBase[category]
if (keywords.some(keyword => question.includes(keyword))) {
const randomResponse = responses[Math.floor(Math.random() * responses.length)]
return `${randomResponse}\n\n💡 如果您需要更详细的建议,建议咨询专业的宠物医生。`
}
}
// 默认回复
const defaultResponses = [
'这是一个很好的问题!建议您咨询专业的宠物医生获得更准确的建议。',
'每只宠物的情况都不同,建议根据具体情况来处理。您可以查看我们的知识库了解更多信息。',
'感谢您的提问!这个问题比较复杂,建议您带宠物去专业的宠物医院检查。',
'我理解您的担心。建议您记录宠物的具体症状,然后咨询专业医生。'
]
return defaultResponses[Math.floor(Math.random() * defaultResponses.length)]
},
scrollToBottom() {
this.$nextTick(() => {
this.scrollTop = 999999
})
},
// 强制滚动到底部,用于确保消息可见
forceScrollToBottom() {
this.$nextTick(() => {
// 使用时间戳确保每次滚动都能触发
this.scrollTop = Date.now()
setTimeout(() => {
this.scrollTop = 999999
}, 50)
})
},
copyMessage(content) {
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({
title: '已复制到剪贴板',
icon: 'success',
duration: 1500
});
},
fail: () => {
uni.showToast({
title: '复制失败',
icon: 'none',
duration: 1500
});
}
});
},
startVoiceInput() {
uni.showToast({
title: '语音功能开发中',
icon: 'none'
})
},
openKnowledge() {
uni.navigateTo({
url: '/pages/assistant/knowledge'
})
}
}
}
</script>
<style lang="scss" scoped>
.assistant-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
position: relative;
}
.assistant-info-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 0 0 0;
border-radius: 24rpx;
padding: 24rpx;
display: flex;
align-items: center;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.assistant-avatar-wrapper {
margin-right: 24rpx;
.assistant-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.3);
.avatar-emoji {
font-size: 40rpx;
color: #ffffff;
}
}
}
.assistant-info-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.assistant-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.assistant-status {
font-size: 24rpx;
color: #666666;
}
}
.chat-status {
display: flex;
align-items: center;
gap: 12rpx;
margin-right: 20rpx;
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #4CAF50;
box-shadow: 0 0 0 4rpx rgba(76, 175, 80, 0.2);
}
.status-text {
font-size: 24rpx;
color: #4CAF50;
font-weight: 500;
}
}
.knowledge-icon {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.icon-text {
font-size: 28rpx;
}
}
}
.quick-questions {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 20rpx 0;
border-radius: 24rpx;
padding: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #FF8A80;
margin-bottom: 20rpx;
display: block;
}
.question-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.question-button {
background: rgba(255, 138, 128, 0.1);
border: 2rpx solid rgba(255, 138, 128, 0.3);
border-radius: 20rpx;
padding: 16rpx 24rpx;
transition: all 0.3s ease;
&:active {
background: rgba(255, 138, 128, 0.2);
transform: scale(0.98);
}
.question-text {
font-size: 26rpx;
color: #FF8A80;
font-weight: 500;
}
}
}
}
.chat-messages {
margin-top: 24rpx;
flex: 1;
overflow: hidden;
}
.chat-scroll {
width: 100%;
height: 100%;
}
.message-list {
padding: 20rpx 0;
padding-bottom: 80rpx;
min-height: 100%;
box-sizing: border-box;
}
.message-item {
display: flex;
margin-bottom: 24rpx;
align-items: flex-start;
}
.message-item.user {
justify-content: flex-end;
padding: 0 0 0 40rpx;
}
.message-item.ai {
justify-content: flex-start;
padding: 0 40rpx 0 0;
}
.message-avatar {
width: 64rpx;
height: 64rpx;
margin: 0;
flex-shrink: 0;
}
/* AI消息头像 - 右边距 */
.message-item.ai .message-avatar {
margin-right: 16rpx;
}
/* 用户消息头像 - 左边距 */
.message-item.user .message-avatar {
margin-left: 16rpx;
}
.assistant-avatar, .user-avatar {
width: 100%;
height: 100%;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
}
.assistant-avatar {
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.3);
}
.user-avatar {
background: linear-gradient(135deg, #81C784, #A5D6A7);
box-shadow: 0 8rpx 24rpx rgba(129, 199, 132, 0.3);
}
.avatar-emoji {
color: #ffffff;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 80%;
}
.message-content.user {
align-items: flex-end;
}
.message-content.ai {
align-items: flex-start;
}
.message-bubble {
padding: 20rpx 24rpx;
border-radius: 24rpx;
word-wrap: break-word;
position: relative;
}
.message-bubble.ai {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
border-bottom-left-radius: 8rpx;
}
.message-bubble.ai.typing-bubble {
padding: 24rpx;
}
.message-bubble.user {
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 100%);
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.3);
border-bottom-right-radius: 8rpx;
}
.message-text {
font-size: 26rpx;
line-height: 1.5;
color: #333333;
}
/* Markdown样式优化 */
.message-text .u-markdown {
font-size: 26rpx;
line-height: 1.5;
}
/* 重置Markdown内部样式 */
.message-text .u-markdown :deep(p) {
margin: 0;
padding: 0;
font-size: 26rpx;
line-height: 1.5;
color: #333333;
}
.message-text .u-markdown :deep(h1),
.message-text .u-markdown :deep(h2),
.message-text .u-markdown :deep(h3),
.message-text .u-markdown :deep(h4),
.message-text .u-markdown :deep(h5),
.message-text .u-markdown :deep(h6) {
margin: 8rpx 0 4rpx 0;
font-weight: 600;
color: #FF8A80;
}
.message-text .u-markdown :deep(ul),
.message-text .u-markdown :deep(ol) {
margin: 8rpx 0;
padding-left: 32rpx;
}
.message-text .u-markdown :deep(li) {
margin: 4rpx 0;
font-size: 26rpx;
line-height: 1.5;
}
.message-text .u-markdown :deep(code) {
background: rgba(255, 138, 128, 0.1);
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-size: 24rpx;
color: #FF8A80;
}
.message-text .u-markdown :deep(pre) {
background: rgba(255, 138, 128, 0.05);
padding: 16rpx;
border-radius: 12rpx;
margin: 8rpx 0;
overflow-x: auto;
}
.message-text .u-markdown :deep(blockquote) {
border-left: 6rpx solid #FF8A80;
padding-left: 16rpx;
margin: 8rpx 0;
color: #666;
font-style: italic;
}
.message-bubble.user .message-text {
color: #ffffff;
}
/* 用户消息的Markdown样式 */
.message-bubble.user .message-text .u-markdown :deep(p),
.message-bubble.user .message-text .u-markdown :deep(li) {
color: #ffffff;
}
.message-bubble.user .message-text .u-markdown :deep(h1),
.message-bubble.user .message-text .u-markdown :deep(h2),
.message-bubble.user .message-text .u-markdown :deep(h3),
.message-bubble.user .message-text .u-markdown :deep(h4),
.message-bubble.user .message-text .u-markdown :deep(h5),
.message-bubble.user .message-text .u-markdown :deep(h6) {
color: #ffffff;
}
.message-bubble.user .message-text .u-markdown :deep(code) {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
.message-bubble.user .message-text .u-markdown :deep(pre) {
background: rgba(255, 255, 255, 0.1);
}
.message-bubble.user .message-text .u-markdown :deep(blockquote) {
border-left-color: #ffffff;
color: rgba(255, 255, 255, 0.8);
}
.message-time {
margin-top: 8rpx;
font-size: 20rpx;
color: rgba(255, 255, 255, 0.7);
}
.message-content.user .message-time {
text-align: right;
}
.message-content.ai .message-time {
text-align: left;
}
.typing-dots {
display: flex;
gap: 8rpx;
justify-content: center;
.typing-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #FF8A80;
animation: typing 1.4s infinite ease-in-out;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input-area {
backdrop-filter: blur(20rpx);
padding: 16rpx 0;
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
}
.input-container {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 0 !important;
.input-wrapper {
flex: 1;
border-radius: 28rpx;
padding: 0 20rpx;
backdrop-filter: blur(15rpx);
border: 3rpx solid rgba(255, 255, 255, 0.98);
height: 56rpx;
display: flex;
align-items: center;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 0 0 1rpx rgba(255, 255, 255, 0.4),
0 2rpx 8rpx rgba(255, 255, 255, 0.15),
0 4rpx 16rpx rgba(255, 255, 255, 0.1),
inset 0 1rpx 0 rgba(255, 255, 255, 0.2);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 25rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%, rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
}
&:focus-within {
border-color: rgba(255, 255, 255, 1);
box-shadow:
0 0 0 2rpx rgba(255, 255, 255, 0.6),
0 0 20rpx rgba(255, 255, 255, 0.3),
0 4rpx 12rpx rgba(255, 255, 255, 0.2),
0 8rpx 24rpx rgba(255, 255, 255, 0.15),
inset 0 1rpx 0 rgba(255, 255, 255, 0.3);
transform: translateY(-2rpx);
}
.message-input {
width: 100%;
height: 100%;
font-size: 28rpx;
color: #ffffff;
border: none;
outline: none;
background: transparent;
position: relative;
z-index: 1;
&::placeholder {
color: rgba(255, 255, 255, 0.95);
font-weight: 400;
text-shadow: 0 1rpx 2rpx rgba(255, 255, 255, 0.1);
}
}
}
.send-button {
height: 56rpx;
padding: 0 20rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
border: 3rpx solid rgba(255, 255, 255, 0.85);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
position: relative;
backdrop-filter: blur(10rpx);
box-shadow:
0 0 0 1rpx rgba(255, 255, 255, 0.3),
0 2rpx 6rpx rgba(255, 255, 255, 0.12),
0 4rpx 12rpx rgba(255, 255, 255, 0.08),
inset 0 1rpx 0 rgba(255, 255, 255, 0.15);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 21rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, transparent 50%, rgba(255, 255, 255, 0.03) 100%);
pointer-events: none;
}
&:active {
transform: scale(0.96) translateY(1rpx);
}
.send-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
position: relative;
z-index: 1;
text-shadow: 0 1rpx 2rpx rgba(255, 255, 255, 0.1);
}
}
.send-button.active {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
border: 3rpx solid rgba(255, 255, 255, 0.98);
box-shadow:
0 0 0 2rpx rgba(255, 255, 255, 0.5),
0 0 16rpx rgba(255, 255, 255, 0.25),
0 4rpx 10rpx rgba(255, 255, 255, 0.18),
0 6rpx 20rpx rgba(255, 255, 255, 0.12),
inset 0 1rpx 0 rgba(255, 255, 255, 0.25);
transform: translateY(-2rpx);
&::before {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, transparent 50%, rgba(255, 255, 255, 0.06) 100%);
}
.send-text {
color: rgba(255, 255, 255, 0.98);
font-weight: 600;
text-shadow: 0 1rpx 3rpx rgba(255, 255, 255, 0.15);
}
}
}
</style>