1051 lines
25 KiB
Vue
1051 lines
25 KiB
Vue
<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="action-buttons">
|
||
<view class="action-btn" @click="clearHistory" title="清空历史">
|
||
<text class="icon-text">🗑️</text>
|
||
</view>
|
||
<view class="action-btn" @click="openKnowledge" title="知识库">
|
||
<text class="icon-text">📚</text>
|
||
</view>
|
||
</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>
|
||
import {
|
||
askPetAssistant,
|
||
streamAskPetAssistant,
|
||
getAssistantHistory,
|
||
clearAssistantHistory,
|
||
validateMessage,
|
||
formatChatMessage,
|
||
formatMessageTime
|
||
} from '@/http/api/assistant.js'
|
||
// WebSocket功能已移除,使用HTTP API替代
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
inputMessage: '',
|
||
scrollTop: 0,
|
||
isThinking: false,
|
||
userAvatar: '/static/user-avatar.png',
|
||
messageList: []
|
||
}
|
||
},
|
||
|
||
onLoad(options) {
|
||
// 加载历史记录
|
||
this.loadChatHistory()
|
||
},
|
||
|
||
onShow() {
|
||
// 页面显示时滚动到底部
|
||
this.$nextTick(() => {
|
||
this.scrollToBottom()
|
||
})
|
||
},
|
||
|
||
methods: {
|
||
getCurrentTime() {
|
||
const now = new Date()
|
||
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
|
||
},
|
||
|
||
|
||
|
||
async sendMessage() {
|
||
if (!this.inputMessage.trim() || this.isThinking) return
|
||
|
||
// 验证消息内容
|
||
const validation = validateMessage(this.inputMessage)
|
||
if (!validation.valid) {
|
||
uni.showToast({
|
||
title: validation.error,
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
return
|
||
}
|
||
|
||
// 保存用户输入内容
|
||
const userInput = this.inputMessage.trim()
|
||
|
||
// 添加用户消息
|
||
const userMessage = {
|
||
type: 'user',
|
||
content: userInput,
|
||
time: this.getCurrentTime()
|
||
}
|
||
this.messageList.push(userMessage)
|
||
|
||
// 清空输入框
|
||
this.inputMessage = ''
|
||
|
||
// 开始AI思考
|
||
this.isThinking = true
|
||
|
||
// 用于存储当前AI消息的引用
|
||
let currentAiMessage = null
|
||
|
||
// 确保DOM更新后再滚动到思考状态
|
||
this.$nextTick(() => {
|
||
this.forceScrollToBottom()
|
||
// 在思考过程中持续滚动,确保思考动画可见
|
||
setTimeout(() => {
|
||
this.forceScrollToBottom()
|
||
}, 200)
|
||
setTimeout(() => {
|
||
this.forceScrollToBottom()
|
||
}, 500)
|
||
})
|
||
|
||
try {
|
||
// 使用HTTP API流式响应
|
||
const response = await this.handleStreamResponse({
|
||
message: userInput,
|
||
temperature: 0.7,
|
||
maxTokens: 1000
|
||
}, (aiMessage) => {
|
||
// 保存AI消息引用,用于fallback时更新
|
||
currentAiMessage = aiMessage
|
||
})
|
||
|
||
// 检查是否包含敏感词
|
||
if (response && response.data && response.data.isSensitive) {
|
||
uni.showToast({
|
||
title: '消息包含敏感内容',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
|
||
// 结束思考状态
|
||
this.isThinking = false
|
||
} catch (error) {
|
||
console.error('SSE流式请求失败,尝试普通请求:', error)
|
||
|
||
// 降级处理:使用普通API
|
||
try {
|
||
const fallbackResponse = await askPetAssistant({
|
||
message: userInput,
|
||
temperature: 0.7,
|
||
maxTokens: 1000
|
||
})
|
||
|
||
if (fallbackResponse && fallbackResponse.data) {
|
||
// 更新已存在的AI消息,而不是创建新的
|
||
if (currentAiMessage) {
|
||
currentAiMessage.content = fallbackResponse.data.message || fallbackResponse.data.content || '抱歉,我现在无法回答您的问题。'
|
||
currentAiMessage.isSensitive = fallbackResponse.data.isSensitive || false
|
||
currentAiMessage.tokenCount = fallbackResponse.data.tokenCount || 0
|
||
currentAiMessage.responseTime = fallbackResponse.data.responseTime || 0
|
||
} else {
|
||
// 如果没有AI消息引用,创建新的(兜底逻辑)
|
||
const aiMessage = {
|
||
type: 'ai',
|
||
content: fallbackResponse.data.message || fallbackResponse.data.content || '抱歉,我现在无法回答您的问题。',
|
||
time: this.getCurrentTime(),
|
||
isSensitive: fallbackResponse.data.isSensitive || false,
|
||
tokenCount: fallbackResponse.data.tokenCount || 0,
|
||
responseTime: fallbackResponse.data.responseTime || 0
|
||
}
|
||
this.messageList.push(aiMessage)
|
||
}
|
||
|
||
// 检查敏感词
|
||
if (fallbackResponse.data.isSensitive) {
|
||
uni.showToast({
|
||
title: '消息包含敏感内容',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
|
||
this.forceScrollToBottom()
|
||
} else {
|
||
this.handleApiError('API响应格式异常', currentAiMessage)
|
||
}
|
||
} catch (fallbackError) {
|
||
console.error('普通请求也失败:', fallbackError)
|
||
this.handleApiError(fallbackError.message || '网络请求失败', currentAiMessage)
|
||
}
|
||
} finally {
|
||
// 确保思考状态结束(防止异常情况)
|
||
this.isThinking = false
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 处理API错误
|
||
* @param {string} errorMessage 错误信息
|
||
* @param {Object} existingAiMessage 已存在的AI消息对象(可选)
|
||
*/
|
||
handleApiError(errorMessage, existingAiMessage = null) {
|
||
const errorContent = `抱歉,我遇到了一些问题:${errorMessage}\n\n请稍后再试,或者检查网络连接。`
|
||
|
||
if (existingAiMessage) {
|
||
// 更新已存在的AI消息
|
||
existingAiMessage.content = errorContent
|
||
existingAiMessage.isError = true
|
||
} else {
|
||
// 创建新的错误消息(兜底逻辑)
|
||
const errorMessage_display = {
|
||
type: 'ai',
|
||
content: errorContent,
|
||
time: this.getCurrentTime(),
|
||
isError: true
|
||
}
|
||
this.messageList.push(errorMessage_display)
|
||
}
|
||
|
||
this.forceScrollToBottom()
|
||
},
|
||
|
||
/**
|
||
* 加载聊天历史记录
|
||
*/
|
||
async loadChatHistory() {
|
||
console.log('开始加载聊天历史记录...')
|
||
try {
|
||
const response = await getAssistantHistory({
|
||
page: 1,
|
||
pageSize: 20
|
||
})
|
||
|
||
console.log('历史记录API响应:', response)
|
||
|
||
if (response && Array.isArray(response) && response.length > 0) {
|
||
// 格式化并设置历史消息
|
||
const historyMessages = response.map(msg => formatChatMessage(msg))
|
||
console.log('格式化后的历史消息:', historyMessages)
|
||
this.messageList = historyMessages
|
||
|
||
// 滚动到底部
|
||
this.$nextTick(() => {
|
||
this.scrollToBottom()
|
||
})
|
||
|
||
console.log('历史记录加载成功,共', historyMessages.length, '条消息')
|
||
} else {
|
||
// 没有历史记录,保持空列表
|
||
console.log('没有历史记录或数据格式异常:', response)
|
||
this.messageList = []
|
||
}
|
||
} catch (error) {
|
||
console.error('加载历史记录失败:', error)
|
||
|
||
// 显示用户友好的错误提示
|
||
uni.showToast({
|
||
title: '加载历史记录失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
|
||
// 保持空列表
|
||
this.messageList = []
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 清空聊天历史
|
||
*/
|
||
async clearHistory() {
|
||
try {
|
||
const result = await uni.showModal({
|
||
title: '确认清空',
|
||
content: '确定要清空聊天记录吗?此操作不可恢复。',
|
||
confirmText: '清空',
|
||
cancelText: '取消'
|
||
})
|
||
|
||
if (!result.confirm) return
|
||
|
||
uni.showLoading({
|
||
title: '正在清空...'
|
||
})
|
||
|
||
await clearAssistantHistory()
|
||
|
||
// 清空消息列表
|
||
this.messageList = []
|
||
|
||
uni.showToast({
|
||
title: '历史记录已清空',
|
||
icon: 'success',
|
||
duration: 1500
|
||
})
|
||
} catch (error) {
|
||
console.error('清空历史记录失败:', error)
|
||
uni.showToast({
|
||
title: '清空失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
},
|
||
|
||
|
||
|
||
/**
|
||
* 处理HTTP流式响应
|
||
* @param {Object} messageData 消息数据
|
||
* @param {Function} onAiMessageCreated 创建AI消息后的回调函数
|
||
* @returns {Promise} 返回流式响应处理结果
|
||
*/
|
||
async handleStreamResponse(messageData, onAiMessageCreated) {
|
||
// 创建AI消息对象
|
||
const aiMessage = {
|
||
type: 'ai',
|
||
content: '',
|
||
time: this.getCurrentTime(),
|
||
isSensitive: false,
|
||
tokenCount: 0,
|
||
responseTime: 0
|
||
}
|
||
|
||
// 添加到消息列表
|
||
this.messageList.push(aiMessage)
|
||
|
||
// 通知调用者AI消息已创建
|
||
if (onAiMessageCreated) {
|
||
onAiMessageCreated(aiMessage)
|
||
}
|
||
|
||
// 使用HTTP API处理流式响应
|
||
return streamAskPetAssistant(messageData)
|
||
.then(result => {
|
||
console.log('HTTP流式响应完成:', result)
|
||
// 更新消息内容
|
||
aiMessage.content = result.message || result.content || ''
|
||
// 更新消息的元数据
|
||
if (result.isSensitive !== undefined) {
|
||
aiMessage.isSensitive = result.isSensitive
|
||
}
|
||
if (result.tokenCount !== undefined) {
|
||
aiMessage.tokenCount = result.tokenCount
|
||
}
|
||
if (result.responseTime !== undefined) {
|
||
aiMessage.responseTime = result.responseTime
|
||
}
|
||
// 强制触发视图更新
|
||
this.$forceUpdate()
|
||
return result
|
||
})
|
||
.catch(error => {
|
||
console.error('HTTP流式响应失败:', error)
|
||
// 显示错误提示
|
||
uni.showToast({
|
||
title: '消息发送失败',
|
||
icon: 'error',
|
||
duration: 2000
|
||
})
|
||
// 更新消息状态为错误
|
||
aiMessage.content = '抱歉,消息发送失败,请重试。'
|
||
aiMessage.isError = true
|
||
// 强制触发视图更新
|
||
this.$forceUpdate()
|
||
throw error
|
||
})
|
||
},
|
||
|
||
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
|
||
});
|
||
}
|
||
});
|
||
},
|
||
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;
|
||
}
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
|
||
.action-btn {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 28rpx;
|
||
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);
|
||
transition: all 0.3s ease;
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
background: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.icon-text {
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.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>
|