643 lines
15 KiB
Vue
643 lines
15 KiB
Vue
<template>
|
||
<view class="pet-chat-container page-container-with-bg">
|
||
<!-- 宠物信息卡片 -->
|
||
<view class="pet-info-card">
|
||
<view class="pet-avatar-wrapper">
|
||
<view class="pet-avatar">
|
||
<text class="avatar-emoji">🐱</text>
|
||
</view>
|
||
</view>
|
||
<view class="pet-info-text">
|
||
<text class="pet-name">{{ petInfo.name || '小橘' }}</text>
|
||
<text class="pet-status">{{ petInfo.personality || '活泼好动,喜欢撒娇' }}</text>
|
||
</view>
|
||
<view class="chat-status">
|
||
<view class="status-dot"></view>
|
||
<text class="status-text">在线</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 聊天消息列表 -->
|
||
<view class="chat-messages">
|
||
<scroll-view
|
||
class="chat-scroll"
|
||
scroll-y
|
||
:scroll-top="scrollTop"
|
||
:scroll-into-view="scrollIntoView"
|
||
scroll-with-animation
|
||
>
|
||
<view class="message-list">
|
||
<!-- 消息项 -->
|
||
<template v-for="message in messageList" :key="message.id">
|
||
<!-- AI消息 -->
|
||
<view class="message-item ai" v-if="message.type === 'ai'">
|
||
<view class="message-avatar">
|
||
<view class="pet-avatar">
|
||
<text class="avatar-emoji">🐱</text>
|
||
</view>
|
||
</view>
|
||
<view class="message-content ai">
|
||
<view class="message-bubble ai">
|
||
<text class="message-text">{{ message.content }}</text>
|
||
</view>
|
||
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 用户消息 -->
|
||
<view class="message-item user" v-if="message.type === 'user'">
|
||
<view class="message-content user">
|
||
<view class="message-bubble user">
|
||
<text class="message-text">{{ message.content }}</text>
|
||
</view>
|
||
<text class="message-time">{{ formatTime(message.timestamp) }}</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="aiTyping">
|
||
<view class="message-avatar">
|
||
<view class="pet-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 id="scroll-bottom" style="height: 1rpx;"></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>
|
||
|
||
<!-- 聊天菜单弹窗 -->
|
||
<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>
|
||
<view class="menu-cancel" @click="showMenu = false">
|
||
<u-text text="取消" type="info" size="14"></u-text>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'PetChatSimple',
|
||
data() {
|
||
return {
|
||
petId: '',
|
||
petInfo: {},
|
||
messageList: [],
|
||
inputMessage: '',
|
||
scrollTop: 0,
|
||
scrollIntoView: '',
|
||
aiTyping: false,
|
||
showMenu: false,
|
||
inputStyle: {
|
||
backgroundColor: '#f8f9fa',
|
||
borderRadius: '20px',
|
||
padding: '10px 15px'
|
||
}
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.petId = options.petId || '1'
|
||
this.loadPetInfo()
|
||
this.loadChatHistory()
|
||
},
|
||
methods: {
|
||
// 加载宠物信息
|
||
loadPetInfo() {
|
||
const mockPets = [
|
||
{
|
||
id: '1',
|
||
name: '小橘',
|
||
breed: '橘猫',
|
||
personality: '活泼好动,喜欢撒娇',
|
||
avatar: '/static/cat-avatar.jpg'
|
||
},
|
||
{
|
||
id: '2',
|
||
name: '小白',
|
||
breed: '金毛',
|
||
personality: '温顺友好,聪明伶俐',
|
||
avatar: '/static/dog-avatar.jpg'
|
||
}
|
||
]
|
||
|
||
this.petInfo = mockPets.find(pet => pet.id === this.petId) || mockPets[0]
|
||
},
|
||
|
||
// 加载聊天历史
|
||
loadChatHistory() {
|
||
const mockMessages = [
|
||
{
|
||
id: '1',
|
||
type: 'ai',
|
||
content: `你好主人!我是${this.petInfo.name},今天感觉特别开心呢!有什么想和我聊的吗?`,
|
||
timestamp: Date.now() - 3600000
|
||
},
|
||
{
|
||
id: '2',
|
||
type: 'user',
|
||
content: '小橘今天吃饭怎么样?',
|
||
timestamp: Date.now() - 3500000
|
||
},
|
||
{
|
||
id: '3',
|
||
type: 'ai',
|
||
content: '今天的小鱼干特别香!我吃得可开心了,还想要更多呢~主人你也要记得按时吃饭哦!',
|
||
timestamp: Date.now() - 3400000
|
||
}
|
||
]
|
||
|
||
this.messageList = mockMessages
|
||
this.scrollToBottom()
|
||
},
|
||
|
||
// 发送消息
|
||
sendMessage() {
|
||
if (!this.inputMessage.trim()) return
|
||
|
||
const userMessage = {
|
||
id: Date.now().toString(),
|
||
type: 'user',
|
||
content: this.inputMessage.trim(),
|
||
timestamp: Date.now()
|
||
}
|
||
|
||
this.messageList.push(userMessage)
|
||
const messageContent = this.inputMessage.trim()
|
||
this.inputMessage = ''
|
||
|
||
this.scrollToBottom()
|
||
this.simulateAIReply(messageContent)
|
||
},
|
||
|
||
// 模拟AI回复
|
||
simulateAIReply(userMessage) {
|
||
this.aiTyping = true
|
||
this.scrollToBottom()
|
||
|
||
setTimeout(() => {
|
||
const aiResponse = this.generateAIResponse(userMessage)
|
||
const aiMessage = {
|
||
id: (Date.now() + 1).toString(),
|
||
type: 'ai',
|
||
content: aiResponse,
|
||
timestamp: Date.now()
|
||
}
|
||
|
||
this.messageList.push(aiMessage)
|
||
this.aiTyping = false
|
||
this.scrollToBottom()
|
||
}, 1500)
|
||
},
|
||
|
||
// 生成AI回复内容
|
||
generateAIResponse(userMessage) {
|
||
const petName = this.petInfo.name
|
||
const responses = [
|
||
`${petName}最喜欢和主人聊天了!今天过得怎么样呀?`,
|
||
`主人好!${petName}今天心情特别好呢~`,
|
||
`哇!主人来找我聊天了,${petName}好开心!`,
|
||
`${petName}听不太懂,但是很开心能和主人聊天!`,
|
||
`主人说的话${petName}都会认真听的!`
|
||
]
|
||
|
||
return responses[Math.floor(Math.random() * responses.length)]
|
||
},
|
||
|
||
// 滚动到底部
|
||
scrollToBottom() {
|
||
this.$nextTick(() => {
|
||
// 使用scroll-into-view滚动到底部锚点
|
||
this.scrollIntoView = 'scroll-bottom'
|
||
// 同时设置scrollTop作为备用方案
|
||
setTimeout(() => {
|
||
this.scrollTop = Math.random() * 1000 + 99999 // 每次不同的值确保触发
|
||
}, 100)
|
||
})
|
||
},
|
||
|
||
// 格式化时间
|
||
formatTime(timestamp) {
|
||
const date = new Date(timestamp)
|
||
const now = new Date()
|
||
const diff = now - date
|
||
|
||
if (diff < 60000) return '刚刚'
|
||
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
|
||
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
|
||
|
||
return `${date.getMonth() + 1}月${date.getDate()}日`
|
||
},
|
||
|
||
// 清空聊天记录
|
||
clearHistory() {
|
||
uni.showModal({
|
||
title: '确认清空',
|
||
content: '确定要清空所有聊天记录吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
this.messageList = []
|
||
this.showMenu = false
|
||
uni.showToast({
|
||
title: '已清空聊天记录',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
// 返回上一页
|
||
goBack() {
|
||
uni.navigateBack()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.pet-chat-container {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
}
|
||
|
||
.pet-info-card {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(20rpx);
|
||
margin: 20rpx 30rpx 0 30rpx;
|
||
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);
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 10;
|
||
|
||
.pet-avatar-wrapper {
|
||
margin-right: 24rpx;
|
||
|
||
.pet-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;
|
||
}
|
||
}
|
||
}
|
||
|
||
.pet-info-text {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
|
||
.pet-name {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333333;
|
||
}
|
||
|
||
.pet-status {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
}
|
||
}
|
||
|
||
.chat-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
|
||
.chat-messages {
|
||
position: absolute;
|
||
top: 140rpx; /* 宠物信息卡片的高度 */
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 120rpx; /* 输入框的高度 */
|
||
width: 100%;
|
||
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 10rpx 0 80rpx; /* 右边距30rpx,左边距80rpx给AI消息留空间 */
|
||
}
|
||
|
||
.message-item.ai {
|
||
justify-content: flex-start;
|
||
padding: 0 80rpx 0 10rpx; /* 左边距30rpx,右边距80rpx给用户消息留空间 */
|
||
}
|
||
|
||
.message-avatar {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
margin: 0 16rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.pet-avatar, .user-avatar {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.pet-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: 65%;
|
||
}
|
||
|
||
.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: 28rpx;
|
||
line-height: 1.4;
|
||
color: #333333;
|
||
}
|
||
|
||
.message-bubble.user .message-text {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.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 {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(20rpx);
|
||
padding: 16rpx 20rpx;
|
||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.input-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
|
||
.input-wrapper {
|
||
flex: 1;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 28rpx;
|
||
padding: 0 20rpx;
|
||
backdrop-filter: blur(10rpx);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.message-input {
|
||
width: 100%;
|
||
height: 100%;
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
}
|
||
}
|
||
|
||
.send-button {
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 24rpx;
|
||
background: rgba(200, 200, 200, 0.5);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.send-button.active {
|
||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 100%);
|
||
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.4);
|
||
}
|
||
|
||
.send-button:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.send-text {
|
||
font-size: 26rpx;
|
||
color: #ffffff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.send-button:not(.active) .send-text {
|
||
color: #cccccc;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.menu-cancel {
|
||
text-align: center;
|
||
padding: 15px 0;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
}
|
||
</style>
|