pet/pages/assistant/assistant.vue

1051 lines
25 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="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>