feat(ai-chat): 优化加载状态动画与流式消息过渡
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<view
|
<view
|
||||||
v-for="(message, index) in messages.filter(msg => msg.content && msg.content.trim() !== '')"
|
v-for="(message, index) in messages.filter(msg => msg.type === 'loading' || (msg.content && msg.content.trim() !== ''))"
|
||||||
:key="getMessageKey(message, index)"
|
:key="getMessageKey(message, index)"
|
||||||
:id="getMessageKey(message, index)"
|
:id="getMessageKey(message, index)"
|
||||||
class="message-item"
|
class="message-item"
|
||||||
@@ -159,14 +159,23 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
||||||
<!-- 加载中状态 -->
|
<!-- 加载/处理中状态 -->
|
||||||
<view v-else-if="message.type === 'loading'" class="loading-content">
|
<view v-else-if="message.type === 'loading'" class="loading-content">
|
||||||
<view class="loading-dots">
|
<view class="loading-typing">
|
||||||
<text class="dot"></text>
|
<view class="typing-dots">
|
||||||
<text class="dot"></text>
|
<text class="dot"></text>
|
||||||
<text class="dot"></text>
|
<text class="dot"></text>
|
||||||
|
<text class="dot"></text>
|
||||||
|
</view>
|
||||||
|
<text class="loading-text">
|
||||||
|
<text class="loading-status">{{ message.loadingText || 'AI正在思考' }}</text>
|
||||||
|
<text class="loading-ellipsis">
|
||||||
|
<text class="ellipsis-dot">.</text>
|
||||||
|
<text class="ellipsis-dot">.</text>
|
||||||
|
<text class="ellipsis-dot">.</text>
|
||||||
|
</text>
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="loading-text">AI正在思考中...</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
@@ -558,9 +567,15 @@ export default {
|
|||||||
}
|
}
|
||||||
// 会话未变化且已有消息 → 跳过加载,保留当前消息
|
// 会话未变化且已有消息 → 跳过加载,保留当前消息
|
||||||
},
|
},
|
||||||
// 新增:组件销毁时清除防抖定时器
|
// 新增:组件销毁时清除所有定时器
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
||||||
|
// 清理所有加载状态定时器
|
||||||
|
this.messages.forEach(msg => {
|
||||||
|
if (msg._rotationTimer) {
|
||||||
|
clearInterval(msg._rotationTimer);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.messages = [...this.initialMessages]
|
this.messages = [...this.initialMessages]
|
||||||
@@ -900,17 +915,25 @@ async loadChatHistoryIfExist() {
|
|||||||
this.messages.push(userMessage)
|
this.messages.push(userMessage)
|
||||||
this.inputText = ''
|
this.inputText = ''
|
||||||
|
|
||||||
// 显示AI正在输入
|
// 显示AI正在处理 - 带状态轮换提示
|
||||||
const loadingMessage = {
|
const loadingMessage = {
|
||||||
id: ++this.messageId,
|
id: ++this.messageId,
|
||||||
role: 'ai',
|
role: 'ai',
|
||||||
type: 'loading',
|
type: 'loading',
|
||||||
content: '',
|
content: '',
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
|
loadingText: 'AI正在思考',
|
||||||
|
_loadingIndex: 0
|
||||||
}
|
}
|
||||||
this.shouldScrollToBottom = true
|
this.shouldScrollToBottom = true
|
||||||
this.messages.push(loadingMessage)
|
this.messages.push(loadingMessage)
|
||||||
|
|
||||||
|
// 启动处理状态文字轮换
|
||||||
|
this._startLoadingTextRotation(loadingMessage)
|
||||||
|
|
||||||
|
// 等待 Vue 先渲染 loading 动画,再发起请求
|
||||||
|
await this.$nextTick()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 直接发送消息(跳过健康检查,避免健康检查接口格式不匹配导致误判)
|
// 直接发送消息(跳过健康检查,避免健康检查接口格式不匹配导致误判)
|
||||||
// 如果服务不可用,实际发送时会抛出异常,由 catch 块处理
|
// 如果服务不可用,实际发送时会抛出异常,由 catch 块处理
|
||||||
@@ -925,7 +948,7 @@ async loadChatHistoryIfExist() {
|
|||||||
console.error('AI服务调用失败:', error)
|
console.error('AI服务调用失败:', error)
|
||||||
|
|
||||||
// 移除加载状态
|
// 移除加载状态
|
||||||
this.messages = this.messages.filter(msg => msg.type !== 'loading')
|
this._removeLoadingMessages()
|
||||||
|
|
||||||
// 显示错误消息
|
// 显示错误消息
|
||||||
const errorMessage = {
|
const errorMessage = {
|
||||||
@@ -955,7 +978,7 @@ async loadChatHistoryIfExist() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 移除加载状态
|
// 移除加载状态
|
||||||
this.messages = this.messages.filter(msg => msg.type !== 'loading')
|
this._removeLoadingMessages()
|
||||||
|
|
||||||
// 更新会话ID
|
// 更新会话ID
|
||||||
if (response.conversationId) {
|
if (response.conversationId) {
|
||||||
@@ -982,134 +1005,193 @@ async loadChatHistoryIfExist() {
|
|||||||
this.$emit('ai-response', aiMessage)
|
this.$emit('ai-response', aiMessage)
|
||||||
},
|
},
|
||||||
// 发送流式消息(自动适配 H5 / 微信小程序)
|
// 发送流式消息(自动适配 H5 / 微信小程序)
|
||||||
async sendStreamMessage(userMessage) {
|
async sendStreamMessage(userMessage) {
|
||||||
// 创建流式消息对象
|
// 找到已有的 loading 消息,复用它在 DOM 中的位置,实现无缝过渡
|
||||||
const streamMessage = {
|
const loadingIdx = this.messages.findIndex(msg => msg.type === 'loading')
|
||||||
id: ++this.messageId,
|
if (loadingIdx === -1) {
|
||||||
role: 'ai',
|
// 安全回退:没有 loading 消息时创建新消息
|
||||||
type: 'text',
|
const fallback = {
|
||||||
content: '',
|
id: ++this.messageId,
|
||||||
timestamp: Date.now(),
|
role: 'ai',
|
||||||
isStreaming: true
|
type: 'text',
|
||||||
};
|
content: '',
|
||||||
// 标记正在流式响应
|
timestamp: Date.now(),
|
||||||
this.isStreaming = true;
|
isStreaming: true
|
||||||
// 移除加载状态,添加流式消息
|
}
|
||||||
this.messages = this.messages.filter(msg => msg.type !== 'loading');
|
this.isStreaming = true
|
||||||
this.shouldScrollToBottom = true;
|
this.messages.push(fallback)
|
||||||
this.messages.push(streamMessage);
|
this.scrollToBottom()
|
||||||
|
|
||||||
// 添加消息后立即滚动一次
|
try {
|
||||||
this.$nextTick(() => {
|
// #ifdef H5
|
||||||
this.scrollToBottom();
|
await aiService.sendHttpStream(userMessage,
|
||||||
});
|
(chunk) => { fallback.content += chunk; this.scrollToBottom() },
|
||||||
|
(result) => this._finishStream(fallback, result))
|
||||||
try {
|
// #endif
|
||||||
// #ifdef H5
|
// #ifdef MP-WEIXIN
|
||||||
await aiService.sendHttpStream(
|
await aiService.sendStreamMessage(userMessage,
|
||||||
userMessage,
|
(chunk) => { fallback.content += chunk; this.scrollToBottom() },
|
||||||
(chunk) => {
|
(result) => this._finishStream(fallback, result))
|
||||||
// 实时更新内容
|
// #endif
|
||||||
streamMessage.content += chunk;
|
} catch (error) {
|
||||||
// 流式更新时滚动到底部显示最新内容(防抖)
|
this._handleStreamError(fallback)
|
||||||
this.scrollToBottom();
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isStreaming = true
|
||||||
|
const loadingMsg = this.messages[loadingIdx]
|
||||||
|
this._stopLoadingTextRotation(loadingMsg)
|
||||||
|
loadingMsg.loadingText = '正在接收回复'
|
||||||
|
|
||||||
|
let transformed = false
|
||||||
|
|
||||||
|
const transformOnFirstChunk = () => {
|
||||||
|
if (transformed) return this.messages[loadingIdx]
|
||||||
|
transformed = true
|
||||||
|
const msg = this.messages[loadingIdx]
|
||||||
|
msg.type = 'text'
|
||||||
|
msg.content = ''
|
||||||
|
this.$set(msg, 'isStreaming', true)
|
||||||
|
this.$set(msg, 'actions', [])
|
||||||
|
delete msg.loadingText
|
||||||
|
delete msg._loadingIndex
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// #ifdef H5
|
||||||
|
await aiService.sendHttpStream(
|
||||||
|
userMessage,
|
||||||
|
(chunk) => {
|
||||||
|
const msg = transformOnFirstChunk()
|
||||||
|
msg.content += chunk
|
||||||
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
(completeResult) => {
|
(completeResult) => {
|
||||||
// 流结束回调
|
const msg = this.messages[loadingIdx]
|
||||||
let convId = '';
|
if (!this._finishStream(msg, completeResult)) return
|
||||||
|
|
||||||
if (typeof completeResult !== 'string') {
|
|
||||||
convId = completeResult.conversation_id || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记流式结束
|
|
||||||
streamMessage.isStreaming = false;
|
|
||||||
this.isStreaming = false;
|
|
||||||
|
|
||||||
// 检查内容是否为空,如果为空则移除该消息
|
|
||||||
if (!streamMessage.content || streamMessage.content.trim() === '') {
|
|
||||||
this.messages = this.messages.filter(msg => msg.id !== streamMessage.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存会话 ID
|
|
||||||
if (convId) {
|
|
||||||
this.currentConversationId = convId;
|
|
||||||
aiService.setConversationId(convId);
|
|
||||||
this.saveConversationIdToLocal(convId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 流结束后再滚动到底部
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 触发事件
|
|
||||||
this.$emit('ai-response', streamMessage);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
// #ifdef MP-WEIXIN
|
|
||||||
// ===== 微信小程序: 使用 WebSocket =====
|
|
||||||
await aiService.sendStreamMessage(
|
|
||||||
userMessage,
|
|
||||||
(chunk) => {
|
|
||||||
// 实时更新内容
|
|
||||||
streamMessage.content += chunk;
|
|
||||||
// 流式更新时滚动到底部显示最新内容(防抖)
|
|
||||||
this.scrollToBottom();
|
|
||||||
},
|
|
||||||
(completeResult) => {
|
|
||||||
// 流结束回调
|
|
||||||
let convId = completeResult?.conversation_id || '';
|
|
||||||
|
|
||||||
// 标记流式结束
|
|
||||||
streamMessage.isStreaming = false;
|
|
||||||
this.isStreaming = false;
|
|
||||||
|
|
||||||
// 检查内容是否为空,如果为空则移除该消息
|
|
||||||
if (!streamMessage.content || streamMessage.content.trim() === '') {
|
|
||||||
this.messages = this.messages.filter(msg => msg.id !== streamMessage.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存会话 ID
|
|
||||||
if (convId) {
|
|
||||||
this.currentConversationId = convId;
|
|
||||||
aiService.setConversationId(convId);
|
|
||||||
this.saveConversationIdToLocal(convId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 流结束后再滚动到底部
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 触发事件
|
|
||||||
this.$emit('ai-response', streamMessage);
|
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
// #endif
|
// #endif
|
||||||
} catch (error) {
|
|
||||||
console.error('流式请求失败:', error);
|
// #ifdef MP-WEIXIN
|
||||||
// 标记流式结束
|
await aiService.sendStreamMessage(
|
||||||
this.isStreaming = false;
|
userMessage,
|
||||||
// 移除流式消息
|
(chunk) => {
|
||||||
this.messages = this.messages.filter(msg => msg.id !== streamMessage.id);
|
const msg = transformOnFirstChunk()
|
||||||
// 显示错误
|
msg.content += chunk
|
||||||
const errorMsg = {
|
this.scrollToBottom()
|
||||||
id: ++this.messageId,
|
},
|
||||||
role: 'ai',
|
(completeResult) => {
|
||||||
type: 'text',
|
const msg = this.messages[loadingIdx]
|
||||||
content: '抱歉,服务暂时不可用,请稍后重试。',
|
if (!this._finishStream(msg, completeResult)) return
|
||||||
timestamp: Date.now(),
|
}
|
||||||
actions: [{ id: 1, text: '重试', type: 'retry' }]
|
)
|
||||||
};
|
// #endif
|
||||||
this.messages.push(errorMsg);
|
} catch (error) {
|
||||||
this.$emit('ai-response', errorMsg);
|
console.error('流式请求失败:', error)
|
||||||
}
|
this.isStreaming = false
|
||||||
},
|
|
||||||
|
if (!transformed) {
|
||||||
|
// 在第一个 chunk 到达前出错 → 移除 loading 消息
|
||||||
|
this._removeLoadingMessages()
|
||||||
|
} else {
|
||||||
|
// 流传输中出错 → 移除空的流式消息
|
||||||
|
const msg = this.messages[loadingIdx]
|
||||||
|
if (msg && (!msg.content || msg.content.trim() === '')) {
|
||||||
|
this.messages.splice(loadingIdx, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMsg = {
|
||||||
|
id: ++this.messageId,
|
||||||
|
role: 'ai',
|
||||||
|
type: 'text',
|
||||||
|
content: '抱歉,服务暂时不可用,请稍后重试。',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
actions: [{ id: 1, text: '重试', type: 'retry' }]
|
||||||
|
}
|
||||||
|
this.messages.push(errorMsg)
|
||||||
|
this.$emit('ai-response', errorMsg)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 完成流式消息处理(共用逻辑)
|
||||||
|
_finishStream(msg, result) {
|
||||||
|
msg.isStreaming = false
|
||||||
|
this.isStreaming = false
|
||||||
|
|
||||||
|
if (!msg.content || msg.content.trim() === '') {
|
||||||
|
this._removeLoadingMessages()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const convId = typeof result === 'string' ? '' : (result.conversation_id || '')
|
||||||
|
if (convId) {
|
||||||
|
this.currentConversationId = convId
|
||||||
|
aiService.setConversationId(convId)
|
||||||
|
this.saveConversationIdToLocal(convId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.actions = [
|
||||||
|
{ id: 1, text: '有帮助', type: 'like' },
|
||||||
|
{ id: 2, text: '没帮助', type: 'dislike' }
|
||||||
|
]
|
||||||
|
|
||||||
|
this.$nextTick(() => this.scrollToBottom())
|
||||||
|
this.$emit('ai-response', msg)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
// 流式请求出错处理
|
||||||
|
_handleStreamError(msg) {
|
||||||
|
console.error('流式请求失败')
|
||||||
|
this.isStreaming = false
|
||||||
|
this.messages = this.messages.filter(m => m.id !== msg.id)
|
||||||
|
|
||||||
|
const errorMsg = {
|
||||||
|
id: ++this.messageId,
|
||||||
|
role: 'ai',
|
||||||
|
type: 'text',
|
||||||
|
content: '抱歉,服务暂时不可用,请稍后重试。',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
actions: [{ id: 1, text: '重试', type: 'retry' }]
|
||||||
|
}
|
||||||
|
this.messages.push(errorMsg)
|
||||||
|
this.$emit('ai-response', errorMsg)
|
||||||
|
},
|
||||||
|
// 加载状态文字轮换 - 让AI看起来在处理不同阶段
|
||||||
|
_startLoadingTextRotation(loadingMsg) {
|
||||||
|
const statusTexts = [
|
||||||
|
'AI正在思考',
|
||||||
|
'AI正在分析',
|
||||||
|
'AI正在检索',
|
||||||
|
'AI正在生成'
|
||||||
|
]
|
||||||
|
loadingMsg._rotationTimer = setInterval(() => {
|
||||||
|
loadingMsg._loadingIndex = (loadingMsg._loadingIndex + 1) % statusTexts.length
|
||||||
|
loadingMsg.loadingText = statusTexts[loadingMsg._loadingIndex]
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 停止加载状态文字轮换
|
||||||
|
_stopLoadingTextRotation(loadingMsg) {
|
||||||
|
if (loadingMsg && loadingMsg._rotationTimer) {
|
||||||
|
clearInterval(loadingMsg._rotationTimer)
|
||||||
|
loadingMsg._rotationTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除加载消息并清理定时器
|
||||||
|
_removeLoadingMessages() {
|
||||||
|
const loadingMsgs = this.messages.filter(msg => msg.type === 'loading');
|
||||||
|
loadingMsgs.forEach(msg => this._stopLoadingTextRotation(msg));
|
||||||
|
this.messages = this.messages.filter(msg => msg.type !== 'loading');
|
||||||
|
},
|
||||||
|
|
||||||
generateAIResponse(userMessage) {
|
generateAIResponse(userMessage) {
|
||||||
const responses = {
|
const responses = {
|
||||||
'你好': '您好!我是AI智能客服,很高兴为您服务!有什么可以帮助您的吗?',
|
'你好': '您好!我是AI智能客服,很高兴为您服务!有什么可以帮助您的吗?',
|
||||||
@@ -1713,11 +1795,17 @@ async loadChatHistoryIfExist() {
|
|||||||
role: 'ai',
|
role: 'ai',
|
||||||
type: 'loading',
|
type: 'loading',
|
||||||
content: '',
|
content: '',
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
|
loadingText: 'AI正在思考',
|
||||||
|
_loadingIndex: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messages.push(loadingMessage)
|
this.messages.push(loadingMessage)
|
||||||
|
this._startLoadingTextRotation(loadingMessage)
|
||||||
|
|
||||||
|
// 等待 Vue 渲染 loading 动画
|
||||||
|
await this.$nextTick()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用AI服务获取回复
|
// 使用AI服务获取回复
|
||||||
if (this.enableStreaming) {
|
if (this.enableStreaming) {
|
||||||
@@ -1732,7 +1820,7 @@ async loadChatHistoryIfExist() {
|
|||||||
console.error('AI服务调用失败:', error)
|
console.error('AI服务调用失败:', error)
|
||||||
|
|
||||||
// 移除加载状态
|
// 移除加载状态
|
||||||
this.messages = this.messages.filter(msg => msg.type !== 'loading')
|
this._removeLoadingMessages()
|
||||||
|
|
||||||
// 显示错误消息
|
// 显示错误消息
|
||||||
const errorMessage = {
|
const errorMessage = {
|
||||||
@@ -1900,6 +1988,13 @@ async loadChatHistoryIfExist() {
|
|||||||
|
|
||||||
// 清空聊天记录
|
// 清空聊天记录
|
||||||
clearMessages() {
|
clearMessages() {
|
||||||
|
// 清理所有加载状态定时器
|
||||||
|
this.messages.forEach(msg => {
|
||||||
|
if (msg._rotationTimer) {
|
||||||
|
clearInterval(msg._rotationTimer)
|
||||||
|
msg._rotationTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
// 清空消息数组
|
// 清空消息数组
|
||||||
this.messages = []
|
this.messages = []
|
||||||
// 重置消息 ID
|
// 重置消息 ID
|
||||||
@@ -2562,31 +2657,68 @@ $radius-lg: 36rpx;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 32rpx 0;
|
padding: 32rpx 0;
|
||||||
|
|
||||||
.loading-dots {
|
.loading-typing {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: 16rpx;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
.dot {
|
.typing-dots {
|
||||||
width: 14rpx;
|
display: flex;
|
||||||
height: 14rpx;
|
align-items: center;
|
||||||
border-radius: 50%;
|
|
||||||
background-color: $color-primary;
|
|
||||||
margin: 0 6rpx;
|
|
||||||
animation: dotPulse 1.5s infinite ease-in-out;
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
.dot {
|
||||||
animation-delay: 0.2s;
|
width: 14rpx;
|
||||||
}
|
height: 14rpx;
|
||||||
|
border-radius: 50%;
|
||||||
&:nth-child(3) {
|
background-color: $color-primary;
|
||||||
animation-delay: 0.4s;
|
margin: 0 6rpx;
|
||||||
|
animation: dotPulse 1.5s infinite ease-in-out;
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: $color-text-light;
|
||||||
|
font-size: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.loading-status {
|
||||||
|
color: $color-primary;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-ellipsis {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
|
||||||
|
.ellipsis-dot {
|
||||||
|
animation: ellipsisBlink 1.4s infinite ease-in-out;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
color: $color-text-light;
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3478,6 +3610,15 @@ $radius-lg: 36rpx;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes ellipsisBlink {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes wave {
|
@keyframes wave {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
transform: scaleY(0.5);
|
transform: scaleY(0.5);
|
||||||
|
|||||||
Reference in New Issue
Block a user