640 lines
17 KiB
Vue
640 lines
17 KiB
Vue
<template>
|
||
<view class="pet-chat-container">
|
||
<u-navbar :title="`与${petInfo.name}聊天`" left-icon="arrow-left" @left-click="goBack">
|
||
<template #right>
|
||
<u-icon name="more-circle" size="20" @click="showChatMenu"></u-icon>
|
||
</template>
|
||
</u-navbar>
|
||
|
||
<!-- 宠物信息卡片 -->
|
||
<view class="pet-info-card">
|
||
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="40" shape="circle"></u-avatar>
|
||
<view class="pet-info-text">
|
||
<u-text :text="petInfo.name" type="primary" size="14" bold></u-text>
|
||
<u-text :text="petInfo.personality" type="tips" size="12"></u-text>
|
||
</view>
|
||
<view class="chat-status">
|
||
<u-tag text="在线" type="success" size="mini"></u-tag>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 聊天消息列表 -->
|
||
<scroll-view
|
||
class="chat-messages"
|
||
scroll-y
|
||
:scroll-top="scrollTop"
|
||
scroll-with-animation
|
||
@scrolltolower="loadMoreMessages"
|
||
>
|
||
<view class="message-list">
|
||
<!-- 加载更多提示 -->
|
||
<view class="load-more" v-if="hasMoreMessages">
|
||
<u-loading-icon v-if="loadingMore"></u-loading-icon>
|
||
<u-text text="上拉加载更多历史消息" type="tips" size="12" v-else></u-text>
|
||
</view>
|
||
|
||
<!-- 消息项 -->
|
||
<view
|
||
class="message-item"
|
||
v-for="message in messageList"
|
||
:key="message.id"
|
||
:class="message.type"
|
||
>
|
||
<view class="message-avatar" v-if="message.type === 'ai'">
|
||
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="32" shape="circle"></u-avatar>
|
||
</view>
|
||
|
||
<view class="message-content">
|
||
<view class="message-bubble" :class="message.type">
|
||
<u-text :text="message.content" size="14" :color="message.type === 'user' ? '#ffffff' : '#333333'"></u-text>
|
||
</view>
|
||
<view class="message-time">
|
||
<u-text :text="formatTime(message.timestamp)" type="tips" size="10"></u-text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="message-avatar" v-if="message.type === 'user'">
|
||
<u-avatar src="/static/user-avatar.png" size="32" shape="circle"></u-avatar>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- AI正在输入提示 -->
|
||
<view class="message-item ai" v-if="aiTyping">
|
||
<view class="message-avatar">
|
||
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="32" shape="circle"></u-avatar>
|
||
</view>
|
||
<view class="message-content">
|
||
<view class="message-bubble ai typing">
|
||
<view class="typing-dots">
|
||
<view class="dot"></view>
|
||
<view class="dot"></view>
|
||
<view class="dot"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 输入区域 -->
|
||
<view class="chat-input-area">
|
||
<view class="input-container">
|
||
<u-input
|
||
v-model="inputMessage"
|
||
placeholder="和你的宠物说点什么..."
|
||
:border="false"
|
||
:custom-style="inputStyle"
|
||
@keyboardheightchange="onKeyboardHeightChange"
|
||
@confirm="sendMessage"
|
||
confirm-type="send"
|
||
></u-input>
|
||
<view class="input-actions">
|
||
<u-icon name="mic" size="20" color="#999" @click="startVoiceInput" v-if="!inputMessage"></u-icon>
|
||
<u-button
|
||
type="primary"
|
||
size="mini"
|
||
text="发送"
|
||
@click="sendMessage"
|
||
:disabled="!inputMessage.trim()"
|
||
v-else
|
||
></u-button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 聊天菜单弹窗 -->
|
||
<u-popup v-model="showMenu" mode="bottom" border-radius="20">
|
||
<view class="chat-menu">
|
||
<view class="menu-title">
|
||
<u-text text="聊天设置" type="primary" size="16" bold></u-text>
|
||
</view>
|
||
<view class="menu-items">
|
||
<view class="menu-item" @click="clearHistory">
|
||
<u-icon name="delete" size="20" color="#ff6b6b"></u-icon>
|
||
<u-text text="清空聊天记录" size="14"></u-text>
|
||
</view>
|
||
<view class="menu-item" @click="exportHistory">
|
||
<u-icon name="download" size="20" color="#64B5F6"></u-icon>
|
||
<u-text text="导出聊天记录" size="14"></u-text>
|
||
</view>
|
||
<view class="menu-item" @click="togglePersonality">
|
||
<u-icon name="setting" size="20" color="#81C784"></u-icon>
|
||
<u-text text="个性化设置" size="14"></u-text>
|
||
</view>
|
||
</view>
|
||
<view class="menu-cancel" @click="showMenu = false">
|
||
<u-text text="取消" type="info" size="14"></u-text>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||
|
||
export default {
|
||
name: 'PetChat',
|
||
setup() {
|
||
// 响应式数据
|
||
const state = reactive({
|
||
petId: '',
|
||
petInfo: {},
|
||
messageList: [],
|
||
inputMessage: '',
|
||
scrollTop: 0,
|
||
aiTyping: false,
|
||
showMenu: false,
|
||
hasMoreMessages: true,
|
||
loadingMore: false,
|
||
keyboardHeight: 0
|
||
})
|
||
|
||
// 样式配置
|
||
const inputStyle = {
|
||
backgroundColor: '#f8f9fa',
|
||
borderRadius: '20px',
|
||
padding: '10px 15px'
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
initPage()
|
||
})
|
||
|
||
// 初始化页面
|
||
const initPage = () => {
|
||
// 获取宠物ID(从路由参数)
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
state.petId = currentPage.options.petId || '1'
|
||
|
||
loadPetInfo()
|
||
loadChatHistory()
|
||
}
|
||
|
||
// 加载宠物信息
|
||
const loadPetInfo = () => {
|
||
// 模拟从本地存储获取宠物信息
|
||
const mockPets = [
|
||
{
|
||
id: '1',
|
||
name: '小橘',
|
||
breed: '橘猫',
|
||
personality: '活泼好动,喜欢撒娇',
|
||
avatar: '/static/cat-avatar.jpg'
|
||
},
|
||
{
|
||
id: '2',
|
||
name: '小白',
|
||
breed: '金毛',
|
||
personality: '温顺友好,聪明伶俐',
|
||
avatar: '/static/dog-avatar.jpg'
|
||
}
|
||
]
|
||
|
||
state.petInfo = mockPets.find(pet => pet.id === state.petId) || mockPets[0]
|
||
}
|
||
|
||
// 加载聊天历史
|
||
const loadChatHistory = () => {
|
||
// 模拟聊天历史数据
|
||
const mockMessages = [
|
||
{
|
||
id: '1',
|
||
type: 'ai',
|
||
content: `你好主人!我是${state.petInfo.name},今天感觉特别开心呢!有什么想和我聊的吗?`,
|
||
timestamp: Date.now() - 3600000
|
||
},
|
||
{
|
||
id: '2',
|
||
type: 'user',
|
||
content: '小橘今天吃饭怎么样?',
|
||
timestamp: Date.now() - 3500000
|
||
},
|
||
{
|
||
id: '3',
|
||
type: 'ai',
|
||
content: '今天的小鱼干特别香!我吃得可开心了,还想要更多呢~主人你也要记得按时吃饭哦!',
|
||
timestamp: Date.now() - 3400000
|
||
},
|
||
{
|
||
id: '4',
|
||
type: 'user',
|
||
content: '那你今天有没有调皮捣蛋?',
|
||
timestamp: Date.now() - 3300000
|
||
},
|
||
{
|
||
id: '5',
|
||
type: 'ai',
|
||
content: '嘿嘿,我今天把你的拖鞋藏起来了!不过是因为太想你了,想要你的味道陪着我~',
|
||
timestamp: Date.now() - 3200000
|
||
}
|
||
]
|
||
|
||
state.messageList = mockMessages
|
||
scrollToBottom()
|
||
}
|
||
|
||
// 发送消息
|
||
const sendMessage = () => {
|
||
if (!state.inputMessage.trim()) return
|
||
|
||
const userMessage = {
|
||
id: Date.now().toString(),
|
||
type: 'user',
|
||
content: state.inputMessage.trim(),
|
||
timestamp: Date.now()
|
||
}
|
||
|
||
state.messageList.push(userMessage)
|
||
const messageContent = state.inputMessage.trim()
|
||
state.inputMessage = ''
|
||
|
||
// 滚动到底部
|
||
scrollToBottom()
|
||
|
||
// 模拟AI回复
|
||
simulateAIReply(messageContent)
|
||
}
|
||
|
||
// 模拟AI回复
|
||
const simulateAIReply = (userMessage) => {
|
||
state.aiTyping = true
|
||
scrollToBottom()
|
||
|
||
setTimeout(() => {
|
||
const aiResponse = generateAIResponse(userMessage)
|
||
const aiMessage = {
|
||
id: (Date.now() + 1).toString(),
|
||
type: 'ai',
|
||
content: aiResponse,
|
||
timestamp: Date.now()
|
||
}
|
||
|
||
state.messageList.push(aiMessage)
|
||
state.aiTyping = false
|
||
scrollToBottom()
|
||
}, 1500 + Math.random() * 1000) // 1.5-2.5秒随机延迟
|
||
}
|
||
|
||
// 生成AI回复内容
|
||
const generateAIResponse = (userMessage) => {
|
||
const petName = state.petInfo.name
|
||
const responses = {
|
||
// 问候类
|
||
greeting: [
|
||
`${petName}最喜欢和主人聊天了!今天过得怎么样呀?`,
|
||
`主人好!${petName}今天心情特别好呢~`,
|
||
`哇!主人来找我聊天了,${petName}好开心!`
|
||
],
|
||
// 食物相关
|
||
food: [
|
||
`说到吃的,${petName}就兴奋了!今天的小鱼干特别香呢~`,
|
||
`${petName}最爱吃主人准备的美食了!什么时候再给我加餐呀?`,
|
||
`嘿嘿,${petName}今天偷偷多吃了一点,不要告诉其他人哦!`
|
||
],
|
||
// 健康相关
|
||
health: [
|
||
`${petName}今天身体棒棒的!谢谢主人这么关心我~`,
|
||
`${petName}觉得自己越来越健康了,都是主人照顾得好!`,
|
||
`主人真贴心!${petName}会好好保护自己的身体的!`
|
||
],
|
||
// 玩耍相关
|
||
play: [
|
||
`${petName}最喜欢和主人一起玩了!我们什么时候再玩游戏呀?`,
|
||
`玩耍时间到!${petName}已经准备好了,主人快来陪我!`,
|
||
`${petName}今天学会了新的小把戏,想不想看看?`
|
||
],
|
||
// 情感相关
|
||
emotion: [
|
||
`${petName}最爱主人了!每天都想和你在一起~`,
|
||
`主人不在的时候,${petName}会想念你的味道呢!`,
|
||
`${petName}觉得自己是世界上最幸福的小宝贝!`
|
||
],
|
||
// 默认回复
|
||
default: [
|
||
`${petName}听不太懂,但是很开心能和主人聊天!`,
|
||
`主人说的话${petName}都会认真听的!`,
|
||
`${petName}觉得主人说得很有道理呢!`,
|
||
`虽然${petName}不太明白,但是感觉很厉害的样子!`
|
||
]
|
||
}
|
||
|
||
// 简单的关键词匹配
|
||
const message = userMessage.toLowerCase()
|
||
let category = 'default'
|
||
|
||
if (message.includes('你好') || message.includes('hi') || message.includes('hello')) {
|
||
category = 'greeting'
|
||
} else if (message.includes('吃') || message.includes('食') || message.includes('饭') || message.includes('零食')) {
|
||
category = 'food'
|
||
} else if (message.includes('健康') || message.includes('身体') || message.includes('生病') || message.includes('医生')) {
|
||
category = 'health'
|
||
} else if (message.includes('玩') || message.includes('游戏') || message.includes('运动') || message.includes('散步')) {
|
||
category = 'play'
|
||
} else if (message.includes('爱') || message.includes('想') || message.includes('喜欢') || message.includes('亲亲')) {
|
||
category = 'emotion'
|
||
}
|
||
|
||
const categoryResponses = responses[category]
|
||
return categoryResponses[Math.floor(Math.random() * categoryResponses.length)]
|
||
}
|
||
|
||
// 滚动到底部
|
||
const scrollToBottom = () => {
|
||
nextTick(() => {
|
||
state.scrollTop = 99999
|
||
})
|
||
}
|
||
|
||
// 格式化时间
|
||
const formatTime = (timestamp) => {
|
||
const date = new Date(timestamp)
|
||
const now = new Date()
|
||
const diff = now - date
|
||
|
||
if (diff < 60000) { // 1分钟内
|
||
return '刚刚'
|
||
} else if (diff < 3600000) { // 1小时内
|
||
return `${Math.floor(diff / 60000)}分钟前`
|
||
} else if (diff < 86400000) { // 24小时内
|
||
return `${Math.floor(diff / 3600000)}小时前`
|
||
} else {
|
||
return `${date.getMonth() + 1}月${date.getDate()}日`
|
||
}
|
||
}
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
// 显示聊天菜单
|
||
const showChatMenu = () => {
|
||
state.showMenu = true
|
||
}
|
||
|
||
// 加载更多消息
|
||
const loadMoreMessages = () => {
|
||
if (state.loadingMore || !state.hasMoreMessages) return
|
||
|
||
state.loadingMore = true
|
||
|
||
// 模拟加载历史消息
|
||
setTimeout(() => {
|
||
const moreMessages = [
|
||
{
|
||
id: `history_${Date.now()}`,
|
||
type: 'ai',
|
||
content: `主人,${state.petInfo.name}想你了!`,
|
||
timestamp: Date.now() - 7200000
|
||
}
|
||
]
|
||
|
||
state.messageList.unshift(...moreMessages)
|
||
state.loadingMore = false
|
||
|
||
// 模拟没有更多消息
|
||
if (state.messageList.length > 20) {
|
||
state.hasMoreMessages = false
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
// 清空聊天记录
|
||
const clearHistory = () => {
|
||
uni.showModal({
|
||
title: '确认清空',
|
||
content: '确定要清空所有聊天记录吗?此操作不可恢复。',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
state.messageList = []
|
||
state.showMenu = false
|
||
uni.showToast({
|
||
title: '已清空聊天记录',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 导出聊天记录
|
||
const exportHistory = () => {
|
||
uni.showToast({
|
||
title: '导出功能开发中',
|
||
icon: 'none'
|
||
})
|
||
state.showMenu = false
|
||
}
|
||
|
||
// 个性化设置
|
||
const togglePersonality = () => {
|
||
uni.showToast({
|
||
title: '个性化设置开发中',
|
||
icon: 'none'
|
||
})
|
||
state.showMenu = false
|
||
}
|
||
|
||
// 语音输入
|
||
const startVoiceInput = () => {
|
||
uni.showToast({
|
||
title: '语音功能开发中',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
// 键盘高度变化
|
||
const onKeyboardHeightChange = (e) => {
|
||
state.keyboardHeight = e.detail.height
|
||
if (e.detail.height > 0) {
|
||
scrollToBottom()
|
||
}
|
||
}
|
||
|
||
return {
|
||
...state,
|
||
inputStyle,
|
||
sendMessage,
|
||
formatTime,
|
||
goBack,
|
||
showChatMenu,
|
||
loadMoreMessages,
|
||
clearHistory,
|
||
exportHistory,
|
||
togglePersonality,
|
||
startVoiceInput,
|
||
onKeyboardHeightChange
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.pet-chat-container {
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.pet-info-card {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 15px 20px;
|
||
background-color: #ffffff;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
.pet-info-text {
|
||
flex: 1;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.chat-status {
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
|
||
.chat-messages {
|
||
flex: 1;
|
||
padding: 0 15px;
|
||
}
|
||
|
||
.message-list {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.load-more {
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.message-item {
|
||
display: flex;
|
||
margin-bottom: 20px;
|
||
|
||
&.user {
|
||
flex-direction: row-reverse;
|
||
|
||
.message-content {
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.message-bubble {
|
||
background-color: #FF8A80;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
|
||
&.ai {
|
||
.message-bubble {
|
||
background-color: #ffffff;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.message-avatar {
|
||
margin: 0 10px;
|
||
}
|
||
|
||
.message-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-width: 70%;
|
||
}
|
||
|
||
.message-bubble {
|
||
padding: 12px 16px;
|
||
border-radius: 18px;
|
||
word-wrap: break-word;
|
||
|
||
&.typing {
|
||
padding: 16px;
|
||
}
|
||
}
|
||
|
||
.message-time {
|
||
margin-top: 5px;
|
||
text-align: center;
|
||
}
|
||
|
||
.typing-dots {
|
||
display: flex;
|
||
gap: 4px;
|
||
|
||
.dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background-color: #ccc;
|
||
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 {
|
||
background-color: #ffffff;
|
||
border-top: 1px solid #f0f0f0;
|
||
padding: 15px 20px;
|
||
padding-bottom: calc(15px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.input-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.input-actions {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.chat-menu {
|
||
padding: 20px;
|
||
|
||
.menu-title {
|
||
text-align: center;
|
||
padding-bottom: 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.menu-items {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
padding: 15px 0;
|
||
border-bottom: 1px solid #f8f9fa;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.menu-cancel {
|
||
text-align: center;
|
||
padding: 15px 0;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
}
|
||
</style>
|