chore(build): 调整ai-chat-message组件的位置,减少对主包尺寸的影响
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"component": true,
|
|
||||||
"usingComponents": {
|
|
||||||
"ns-loading": "../ns-loading/ns-loading"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -109,6 +109,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化多语言配置
|
* 初始化多语言配置
|
||||||
*/
|
*/
|
||||||
@@ -130,12 +131,14 @@ export default {
|
|||||||
this.currentLangIndex = 0;
|
this.currentLangIndex = 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 电话联系客服
|
* 电话联系客服
|
||||||
*/
|
*/
|
||||||
call() {
|
call() {
|
||||||
this.customerService.makePhoneCall(this.tel);
|
this.customerService.makePhoneCall(this.tel);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换中英文语言,并刷新当前页面(保留所有参数)
|
* 切换中英文语言,并刷新当前页面(保留所有参数)
|
||||||
*/
|
*/
|
||||||
@@ -153,12 +156,12 @@ export default {
|
|||||||
openAIChat() {
|
openAIChat() {
|
||||||
this.$util.redirectTo(this.$util.AI_CHAT_PAGE_URL);
|
this.$util.redirectTo(this.$util.AI_CHAT_PAGE_URL);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开客服选择对话框
|
||||||
|
*/
|
||||||
openCustomerSelectPopup() {
|
openCustomerSelectPopup() {
|
||||||
if (this.customerService) {
|
this.customerService.openCustomerSelectPopupDialog();
|
||||||
this.customerService.openCustomerSelectPopupDialog();
|
|
||||||
} else {
|
|
||||||
uni.showToast({ title: '客服初始化中,请稍后重试', icon: 'none' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="ai-chat-container">
|
<view class="ai-chat-container">
|
||||||
<!-- 聊天消息列表 -->
|
<!-- 聊天消息列表 -->
|
||||||
<scroll-view
|
<scroll-view class="chat-messages" scroll-y :scroll-top="scrollTop" @scroll="onScroll"
|
||||||
class="chat-messages"
|
|
||||||
scroll-y
|
|
||||||
:scroll-top="scrollTop"
|
|
||||||
@scroll="onScroll"
|
|
||||||
:scroll-with-animation="false">
|
:scroll-with-animation="false">
|
||||||
|
|
||||||
<!-- 加载更多历史消息 -->
|
<!-- 加载更多历史消息 -->
|
||||||
@@ -14,11 +10,8 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<view
|
<view v-for="(message, index) in messages" :key="`msg-${message.id || message.timestamp}-${index}`"
|
||||||
v-for="(message, index) in messages"
|
class="message-item" :class="[message.role, { 'first-message': index === 0 }]">
|
||||||
:key="`msg-${message.id || message.timestamp}-${index}`"
|
|
||||||
class="message-item"
|
|
||||||
:class="[message.role, { 'first-message': index === 0 }]">
|
|
||||||
|
|
||||||
<!-- 用户消息 -->
|
<!-- 用户消息 -->
|
||||||
<view v-if="message.role === 'user'" class="user-message">
|
<view v-if="message.role === 'user'" class="user-message">
|
||||||
@@ -32,7 +25,7 @@
|
|||||||
<view class="message-bubble">
|
<view class="message-bubble">
|
||||||
<text class="message-text">{{ message.content }}</text>
|
<text class="message-text">{{ message.content }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- AI消息 -->
|
<!-- AI消息 -->
|
||||||
@@ -85,21 +78,12 @@
|
|||||||
<text class="audio-duration">{{ formatDuration(message.duration) }}</text>
|
<text class="audio-duration">{{ formatDuration(message.duration) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="audio-controls">
|
<view class="audio-controls">
|
||||||
<button
|
<button class="play-btn" :class="{ playing: message.playing }" @click="toggleAudio(message)">
|
||||||
class="play-btn"
|
|
||||||
:class="{ playing: message.playing }"
|
|
||||||
@click="toggleAudio(message)">
|
|
||||||
<text class="iconfont" :class="message.playing ? 'icon-pause' : 'icon-play'"></text>
|
<text class="iconfont" :class="message.playing ? 'icon-pause' : 'icon-play'"></text>
|
||||||
</button>
|
</button>
|
||||||
<slider
|
<slider class="audio-slider" :value="message.currentTime || 0" :max="message.duration || 0"
|
||||||
class="audio-slider"
|
@changing="onAudioSliderChange" @change="onAudioSliderChangeEnd" activeColor="#8a9fb8"
|
||||||
:value="message.currentTime || 0"
|
backgroundColor="#e8edf3" block-size="12" />
|
||||||
:max="message.duration || 0"
|
|
||||||
@changing="onAudioSliderChange"
|
|
||||||
@change="onAudioSliderChangeEnd"
|
|
||||||
activeColor="#8a9fb8"
|
|
||||||
backgroundColor="#e8edf3"
|
|
||||||
block-size="12" />
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -107,14 +91,8 @@
|
|||||||
<!-- 视频消息 -->
|
<!-- 视频消息 -->
|
||||||
<view v-else-if="message.type === 'video'" class="video-content">
|
<view v-else-if="message.type === 'video'" class="video-content">
|
||||||
<view class="video-player">
|
<view class="video-player">
|
||||||
<video
|
<video :src="message.url" :poster="message.cover" :controls="true" :autoplay="false"
|
||||||
:src="message.url"
|
class="video-element" @play="onVideoPlay(message)" @pause="onVideoPause(message)">
|
||||||
:poster="message.cover"
|
|
||||||
:controls="true"
|
|
||||||
:autoplay="false"
|
|
||||||
class="video-element"
|
|
||||||
@play="onVideoPlay(message)"
|
|
||||||
@pause="onVideoPause(message)">
|
|
||||||
</video>
|
</video>
|
||||||
<view class="video-info">
|
<view class="video-info">
|
||||||
<text class="video-title">{{ message.title || '视频消息' }}</text>
|
<text class="video-title">{{ message.title || '视频消息' }}</text>
|
||||||
@@ -163,12 +141,8 @@
|
|||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<view class="message-actions" v-if="message.actions && message.actions.length > 0">
|
<view class="message-actions" v-if="message.actions && message.actions.length > 0">
|
||||||
<button
|
<button v-for="action in message.actions" :key="action.id" class="action-btn"
|
||||||
v-for="action in message.actions"
|
:class="[`iconfont`, action.icon, action.type]" @click="handleAction(action, message)">
|
||||||
:key="action.id"
|
|
||||||
class="action-btn"
|
|
||||||
:class="[`iconfont`, action.icon, action.type]"
|
|
||||||
@click="handleAction(action, message)">
|
|
||||||
{{ action.text }}
|
{{ action.text }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -199,21 +173,11 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="input-container">
|
<view class="input-container">
|
||||||
<textarea
|
<textarea v-model="inputText" class="message-input" placeholder="请输入您的问题..." :maxlength="500"
|
||||||
v-model="inputText"
|
:auto-height="true" :show-confirm-bar="false" @confirm="sendMessage" @input="onInput" />
|
||||||
class="message-input"
|
|
||||||
placeholder="请输入您的问题..."
|
|
||||||
:maxlength="500"
|
|
||||||
:auto-height="true"
|
|
||||||
:show-confirm-bar="false"
|
|
||||||
@confirm="sendMessage"
|
|
||||||
@input="onInput" />
|
|
||||||
|
|
||||||
<!-- 发送按钮 -->
|
<!-- 发送按钮 -->
|
||||||
<button
|
<button class="send-btn" :class="{ disabled: !inputText.trim() }" @click="sendMessage"
|
||||||
class="send-btn"
|
|
||||||
:class="{ disabled: !inputText.trim() }"
|
|
||||||
@click="sendMessage"
|
|
||||||
:disabled="!inputText.trim()">
|
:disabled="!inputText.trim()">
|
||||||
<text>发送</text>
|
<text>发送</text>
|
||||||
</button>
|
</button>
|
||||||
@@ -271,11 +235,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="voice-content">
|
<view class="voice-content">
|
||||||
<view class="voice-wave" :class="{ recording: voiceInputing }">
|
<view class="voice-wave" :class="{ recording: voiceInputing }">
|
||||||
<view
|
<view v-for="i in 8" :key="i" class="wave-bar" :style="{ animationDelay: (i * 0.1) + 's' }"></view>
|
||||||
v-for="i in 8"
|
|
||||||
:key="i"
|
|
||||||
class="wave-bar"
|
|
||||||
:style="{ animationDelay: (i * 0.1) + 's' }"></view>
|
|
||||||
</view>
|
</view>
|
||||||
<text class="voice-tip">{{ voiceInputing ? '正在录音,松开结束' : '点击开始录音' }}</text>
|
<text class="voice-tip">{{ voiceInputing ? '正在录音,松开结束' : '点击开始录音' }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -331,23 +291,15 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="nickname-editor-content">
|
<view class="nickname-editor-content">
|
||||||
<view class="nickname-input-container">
|
<view class="nickname-input-container">
|
||||||
<input
|
<input v-model="tempNickname" class="nickname-input" placeholder="请输入您的新昵称" :maxlength="12" type="text" />
|
||||||
v-model="tempNickname"
|
|
||||||
class="nickname-input"
|
|
||||||
placeholder="请输入您的新昵称"
|
|
||||||
:maxlength="12"
|
|
||||||
type="text" />
|
|
||||||
</view>
|
</view>
|
||||||
<view class="nickname-tip">昵称长度限制1-12个字符,仅自己可见</view>
|
<view class="nickname-tip">昵称长度限制1-12个字符,仅自己可见</view>
|
||||||
<view class="nickname-editor-actions">
|
<view class="nickname-editor-actions">
|
||||||
<button class="nickname-action-btn cancel" @click="closeNicknameEditor">
|
<button class="nickname-action-btn cancel" @click="closeNicknameEditor">
|
||||||
<text>取消</text>
|
<text>取消</text>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="nickname-action-btn confirm" :class="{ disabled: !tempNickname.trim() }"
|
||||||
class="nickname-action-btn confirm"
|
:disabled="!tempNickname.trim()" @click="saveNickname">
|
||||||
:class="{ disabled: !tempNickname.trim() }"
|
|
||||||
:disabled="!tempNickname.trim()"
|
|
||||||
@click="saveNickname">
|
|
||||||
<text>保存</text>
|
<text>保存</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -398,7 +350,7 @@ export default {
|
|||||||
default: 20
|
default: 20
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data(){
|
data() {
|
||||||
return {
|
return {
|
||||||
messages: [],
|
messages: [],
|
||||||
inputText: '',
|
inputText: '',
|
||||||
@@ -432,34 +384,34 @@ export default {
|
|||||||
currentConversationId: null,
|
currentConversationId: null,
|
||||||
isAIServiceAvailable: true,
|
isAIServiceAvailable: true,
|
||||||
aiServiceError: null,
|
aiServiceError: null,
|
||||||
isLoadingHistory: false, // 新增:标记历史加载状态
|
isLoadingHistory: false, // 新增:标记历史加载状态
|
||||||
shouldScrollToBottom: false,
|
shouldScrollToBottom: false,
|
||||||
scrollTimer: null,
|
scrollTimer: null,
|
||||||
lastScrollTop: 0,
|
lastScrollTop: 0,
|
||||||
historyOffset: 0,
|
historyOffset: 0,
|
||||||
isFetchingHistory: false
|
isFetchingHistory: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
// 优先读取本地缓存的会话 ID
|
// 优先读取本地缓存的会话 ID
|
||||||
const localConvId = this.getConversationIdFromLocal();
|
const localConvId = this.getConversationIdFromLocal();
|
||||||
if (localConvId) {
|
if (localConvId) {
|
||||||
this.currentConversationId = localConvId;
|
this.currentConversationId = localConvId;
|
||||||
aiService.setConversationId(localConvId);
|
aiService.setConversationId(localConvId);
|
||||||
} else {
|
} else {
|
||||||
this.currentConversationId = aiService.getConversationId();
|
this.currentConversationId = aiService.getConversationId();
|
||||||
}
|
}
|
||||||
// 加载初始历史后,同步historyOffset
|
// 加载初始历史后,同步historyOffset
|
||||||
this.loadChatHistoryIfExist().then(() => {
|
this.loadChatHistoryIfExist().then(() => {
|
||||||
// 初始历史条数作为偏移量
|
// 初始历史条数作为偏移量
|
||||||
this.historyOffset = this.messages.length;
|
this.historyOffset = this.messages.length;
|
||||||
});
|
});
|
||||||
// 移除:this.historyOffset = 0;(避免覆盖初始历史的偏移量)
|
// 移除:this.historyOffset = 0;(避免覆盖初始历史的偏移量)
|
||||||
},
|
},
|
||||||
// 新增:组件销毁时清除防抖定时器
|
// 新增:组件销毁时清除防抖定时器
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.messages = [...this.initialMessages]
|
this.messages = [...this.initialMessages]
|
||||||
this.messageId = this.messages.length
|
this.messageId = this.messages.length
|
||||||
@@ -468,42 +420,42 @@ export default {
|
|||||||
this.initUserAvatarData()
|
this.initUserAvatarData()
|
||||||
// 初始化用户昵称(缓存优先)
|
// 初始化用户昵称(缓存优先)
|
||||||
this.initUserNicknameData()
|
this.initUserNicknameData()
|
||||||
this.currentConversationId = aiService.getConversationId();
|
this.currentConversationId = aiService.getConversationId();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 初始加载时滚底(仅一次)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
// 初始加载时滚底(仅一次)
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToBottom()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
messages: {
|
messages: {
|
||||||
handler() {
|
handler() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
// 仅当不是加载历史、且需要滚底时,才执行滚底
|
// 仅当不是加载历史、且需要滚底时,才执行滚底
|
||||||
if (!this.isLoadingHistory && this.shouldScrollToBottom) {
|
if (!this.isLoadingHistory && this.shouldScrollToBottom) {
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
// 滚底后重置状态,避免重复触发
|
// 滚底后重置状态,避免重复触发
|
||||||
this.shouldScrollToBottom = false
|
this.shouldScrollToBottom = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 【新增:会话 ID 本地缓存方法】
|
// 【新增:会话 ID 本地缓存方法】
|
||||||
saveConversationIdToLocal(convId) {
|
saveConversationIdToLocal(convId) {
|
||||||
if (convId) {
|
if (convId) {
|
||||||
uni.setStorageSync('dify_conversation_id', convId);
|
uni.setStorageSync('dify_conversation_id', convId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getConversationIdFromLocal() {
|
getConversationIdFromLocal() {
|
||||||
return uni.getStorageSync('dify_conversation_id') || null;
|
return uni.getStorageSync('dify_conversation_id') || null;
|
||||||
},
|
},
|
||||||
clearConversationIdFromLocal() {
|
clearConversationIdFromLocal() {
|
||||||
uni.removeStorageSync('dify_conversation_id');
|
uni.removeStorageSync('dify_conversation_id');
|
||||||
},
|
},
|
||||||
// 初始化音频上下文
|
// 初始化音频上下文
|
||||||
initAudioContext() {
|
initAudioContext() {
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
@@ -512,18 +464,18 @@ export default {
|
|||||||
}
|
}
|
||||||
// #endif
|
// #endif
|
||||||
},
|
},
|
||||||
onScroll(e) {
|
onScroll(e) {
|
||||||
const currentScrollTop = e.detail.scrollTop;
|
const currentScrollTop = e.detail.scrollTop;
|
||||||
this.lastScrollTop = currentScrollTop;
|
this.lastScrollTop = currentScrollTop;
|
||||||
// 防抖处理:50ms内只执行一次,避免频繁触发
|
// 防抖处理:50ms内只执行一次,避免频繁触发
|
||||||
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
||||||
this.scrollTimer = setTimeout(() => {
|
this.scrollTimer = setTimeout(() => {
|
||||||
// 滚动到顶部附近(<20px)、不在加载中、显示加载更多时,触发加载
|
// 滚动到顶部附近(<20px)、不在加载中、显示加载更多时,触发加载
|
||||||
if (currentScrollTop < 20 && !this.isLoadingHistory && this.showLoadMore) {
|
if (currentScrollTop < 20 && !this.isLoadingHistory && this.showLoadMore) {
|
||||||
this.loadMoreHistory();
|
this.loadMoreHistory();
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
},
|
},
|
||||||
// 初始化用户头像(支持缓存持久化)
|
// 初始化用户头像(支持缓存持久化)
|
||||||
initUserAvatarData() {
|
initUserAvatarData() {
|
||||||
// 1. 优先读取本地缓存的头像
|
// 1. 优先读取本地缓存的头像
|
||||||
@@ -544,42 +496,42 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 👇 新增:加载历史记录
|
// 👇 新增:加载历史记录
|
||||||
async loadChatHistoryIfExist() {
|
async loadChatHistoryIfExist() {
|
||||||
const convId = aiService.getConversationId();
|
const convId = aiService.getConversationId();
|
||||||
if (convId) {
|
if (convId) {
|
||||||
try {
|
try {
|
||||||
const history = await aiService.getChatHistory();
|
const history = await aiService.getChatHistory();
|
||||||
if (!history || !Array.isArray(history.messages)) {
|
if (!history || !Array.isArray(history.messages)) {
|
||||||
console.warn('历史记录为空或格式无效');
|
console.warn('历史记录为空或格式无效');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messages = history.messages.map(msg => {
|
this.messages = history.messages.map(msg => {
|
||||||
let content = '';
|
let content = '';
|
||||||
if (msg.role === 'user') {
|
if (msg.role === 'user') {
|
||||||
content = msg.query || (typeof msg.inputs === 'string' ? msg.inputs : JSON.stringify(msg.inputs || '')) || '';
|
content = msg.query || (typeof msg.inputs === 'string' ? msg.inputs : JSON.stringify(msg.inputs || '')) || '';
|
||||||
} else {
|
} else {
|
||||||
content = msg.answer || '';
|
content = msg.answer || '';
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: msg.message_id || (Date.now() + Math.random() * 10000),
|
id: msg.message_id || (Date.now() + Math.random() * 10000),
|
||||||
role: msg.role === 'user' ? 'user' : 'assistant',
|
role: msg.role === 'user' ? 'user' : 'assistant',
|
||||||
content: content,
|
content: content,
|
||||||
timestamp: msg.created_at,
|
timestamp: msg.created_at,
|
||||||
actions: msg.role !== 'user' ? [
|
actions: msg.role !== 'user' ? [
|
||||||
{ id: 1, text: '有帮助', type: 'like' },
|
{ id: 1, text: '有帮助', type: 'like' },
|
||||||
{ id: 2, text: '没帮助', type: 'dislike' }
|
{ id: 2, text: '没帮助', type: 'dislike' }
|
||||||
] : []
|
] : []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载历史记录失败:', error);
|
console.error('加载历史记录失败:', error);
|
||||||
aiService.clearConversationId();
|
aiService.clearConversationId();
|
||||||
this.clearConversationIdFromLocal();
|
this.clearConversationIdFromLocal();
|
||||||
this.currentConversationId = null;
|
this.currentConversationId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 新增:初始化用户昵称(支持缓存持久化)
|
// 新增:初始化用户昵称(支持缓存持久化)
|
||||||
initUserNicknameData() {
|
initUserNicknameData() {
|
||||||
// 1. 优先读取本地缓存的昵称
|
// 1. 优先读取本地缓存的昵称
|
||||||
@@ -717,7 +669,7 @@ export default {
|
|||||||
content: this.inputText.trim(),
|
content: this.inputText.trim(),
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
}
|
}
|
||||||
this.shouldScrollToBottom = true
|
this.shouldScrollToBottom = true
|
||||||
this.messages.push(userMessage)
|
this.messages.push(userMessage)
|
||||||
this.inputText = ''
|
this.inputText = ''
|
||||||
|
|
||||||
@@ -787,8 +739,8 @@ export default {
|
|||||||
// 更新会话ID
|
// 更新会话ID
|
||||||
if (response.conversationId) {
|
if (response.conversationId) {
|
||||||
this.currentConversationId = response.conversationId;
|
this.currentConversationId = response.conversationId;
|
||||||
aiService.setConversationId(response.conversationId);
|
aiService.setConversationId(response.conversationId);
|
||||||
this.saveConversationIdToLocal(response.conversationId);
|
this.saveConversationIdToLocal(response.conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const aiMessage = {
|
const aiMessage = {
|
||||||
@@ -823,52 +775,52 @@ export default {
|
|||||||
|
|
||||||
// 移除加载状态,添加流式消息
|
// 移除加载状态,添加流式消息
|
||||||
this.messages = this.messages.filter(msg => msg.type !== 'loading')
|
this.messages = this.messages.filter(msg => msg.type !== 'loading')
|
||||||
this.shouldScrollToBottom = true
|
this.shouldScrollToBottom = true
|
||||||
this.messages.push(streamMessage)
|
this.messages.push(streamMessage)
|
||||||
|
|
||||||
// 开始流式响应
|
// 开始流式响应
|
||||||
await aiService.sendStreamMessage(
|
await aiService.sendStreamMessage(
|
||||||
userMessage,
|
userMessage,
|
||||||
// 流式数据回调
|
// 流式数据回调
|
||||||
(chunk) => {
|
(chunk) => {
|
||||||
streamMessage.content += chunk
|
streamMessage.content += chunk
|
||||||
this.$forceUpdate() // 或 this.$nextTick()
|
this.$forceUpdate() // 或 this.$nextTick()
|
||||||
},
|
},
|
||||||
// 完成回调:处理对象或字符串
|
// 完成回调:处理对象或字符串
|
||||||
(completeResult) => {
|
(completeResult) => {
|
||||||
let finalContent = '';
|
let finalContent = '';
|
||||||
let convId = '';
|
let convId = '';
|
||||||
|
|
||||||
// 判断是对象还是字符串
|
// 判断是对象还是字符串
|
||||||
if (typeof completeResult === 'string') {
|
if (typeof completeResult === 'string') {
|
||||||
finalContent = completeResult;
|
finalContent = completeResult;
|
||||||
} else {
|
} else {
|
||||||
finalContent = completeResult.content || '';
|
finalContent = completeResult.content || '';
|
||||||
convId = completeResult.conversation_id || ''; // 👈 关键:提取 conversation_id
|
convId = completeResult.conversation_id || ''; // 👈 关键:提取 conversation_id
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新消息状态
|
// 更新消息状态
|
||||||
streamMessage.isStreaming = false;
|
streamMessage.isStreaming = false;
|
||||||
streamMessage.content = finalContent;
|
streamMessage.content = finalContent;
|
||||||
|
|
||||||
// 添加操作按钮
|
// 添加操作按钮
|
||||||
streamMessage.actions = [
|
streamMessage.actions = [
|
||||||
{ id: 1, text: '有帮助', type: 'like' },
|
{ id: 1, text: '有帮助', type: 'like' },
|
||||||
{ id: 2, text: '没帮助', type: 'dislike' }
|
{ id: 2, text: '没帮助', type: 'dislike' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 保存 conversation_id 到本地
|
// 保存 conversation_id 到本地
|
||||||
if (convId) {
|
if (convId) {
|
||||||
aiService.setConversationId(convId);
|
aiService.setConversationId(convId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发事件
|
// 触发事件
|
||||||
this.$emit('ai-response', streamMessage);
|
this.$emit('ai-response', streamMessage);
|
||||||
this.saveConversationIdToLocal(convId);
|
this.saveConversationIdToLocal(convId);
|
||||||
this.currentConversationId = convId;
|
this.currentConversationId = convId;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
generateAIResponse(userMessage) {
|
generateAIResponse(userMessage) {
|
||||||
const responses = {
|
const responses = {
|
||||||
'你好': '您好!我是AI智能客服,很高兴为您服务!有什么可以帮助您的吗?',
|
'你好': '您好!我是AI智能客服,很高兴为您服务!有什么可以帮助您的吗?',
|
||||||
@@ -915,8 +867,8 @@ export default {
|
|||||||
return Math.floor(diff / 3600000) + '小时前'
|
return Math.floor(diff / 3600000) + '小时前'
|
||||||
} else {
|
} else {
|
||||||
return date.getMonth() + 1 + '月' + date.getDate() + '日 ' +
|
return date.getMonth() + 1 + '月' + date.getDate() + '日 ' +
|
||||||
date.getHours().toString().padStart(2, '0') + ':' +
|
date.getHours().toString().padStart(2, '0') + ':' +
|
||||||
date.getMinutes().toString().padStart(2, '0')
|
date.getMinutes().toString().padStart(2, '0')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -945,76 +897,76 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 加载更多历史消息
|
// 加载更多历史消息
|
||||||
async loadMoreHistory() {
|
async loadMoreHistory() {
|
||||||
if (!this.currentConversationId) {
|
if (!this.currentConversationId) {
|
||||||
this.currentConversationId = this.getConversationIdFromLocal();
|
this.currentConversationId = this.getConversationIdFromLocal();
|
||||||
aiService.setConversationId(this.currentConversationId);
|
aiService.setConversationId(this.currentConversationId);
|
||||||
}
|
}
|
||||||
// 防止重复加载
|
// 防止重复加载
|
||||||
if (!this.showLoadMore || !this.currentConversationId || this.isLoadingHistory) {
|
if (!this.showLoadMore || !this.currentConversationId || this.isLoadingHistory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isFetchingHistory = true;
|
this.isFetchingHistory = true;
|
||||||
this.isLoadingHistory = true;
|
this.isLoadingHistory = true;
|
||||||
this.loadingText = '加载历史消息中...';
|
this.loadingText = '加载历史消息中...';
|
||||||
this.hasMoreHistory = true;
|
this.hasMoreHistory = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 关键修改:用独立的historyOffset作为偏移量,不再依赖messages.length
|
// 关键修改:用独立的historyOffset作为偏移量,不再依赖messages.length
|
||||||
const response = await aiService.getConversationHistory({
|
const response = await aiService.getConversationHistory({
|
||||||
conversation_id: this.currentConversationId,
|
conversation_id: this.currentConversationId,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
offset: this.historyOffset // 改用独立偏移量
|
offset: this.historyOffset // 改用独立偏移量
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success && Array.isArray(response.messages) && response.messages.length > 0) {
|
if (response.success && Array.isArray(response.messages) && response.messages.length > 0) {
|
||||||
const historyMessages = response.messages.map(msg => ({
|
const historyMessages = response.messages.map(msg => ({
|
||||||
id: msg.id || `history-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
id: msg.id || `history-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
role: msg.role === 'user' ? 'user' : 'ai',
|
role: msg.role === 'user' ? 'user' : 'ai',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
timestamp: msg.create_time * 1000
|
timestamp: msg.create_time * 1000
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 1. 记录新增消息数量
|
// 1. 记录新增消息数量
|
||||||
const newMsgCount = historyMessages.length;
|
const newMsgCount = historyMessages.length;
|
||||||
// 2. 添加到消息列表开头
|
// 2. 添加到消息列表开头
|
||||||
this.messages = [...historyMessages, ...this.messages];
|
this.messages = [...historyMessages, ...this.messages];
|
||||||
// 3. 更新历史偏移量(累加,不是覆盖)
|
// 3. 更新历史偏移量(累加,不是覆盖)
|
||||||
this.historyOffset += newMsgCount;
|
this.historyOffset += newMsgCount;
|
||||||
|
|
||||||
// 4. 等待DOM渲染后,精准计算滚动位置
|
// 4. 等待DOM渲染后,精准计算滚动位置
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
// 获取新增消息的实际DOM高度
|
// 获取新增消息的实际DOM高度
|
||||||
const query = uni.createSelectorQuery().in(this);
|
const query = uni.createSelectorQuery().in(this);
|
||||||
const heightPromises = [];
|
const heightPromises = [];
|
||||||
for (let i = 0; i < newMsgCount; i++) {
|
for (let i = 0; i < newMsgCount; i++) {
|
||||||
heightPromises.push(new Promise(resolve => {
|
heightPromises.push(new Promise(resolve => {
|
||||||
query.select(`.message-item:nth-child(${i + 1})`).boundingClientRect(rect => {
|
query.select(`.message-item:nth-child(${i + 1})`).boundingClientRect(rect => {
|
||||||
resolve(rect ? rect.height + 32 : 0); // 加上消息间距(32rpx)
|
resolve(rect ? rect.height + 32 : 0); // 加上消息间距(32rpx)
|
||||||
}).exec();
|
}).exec();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// 计算总高度
|
// 计算总高度
|
||||||
const heights = await Promise.all(heightPromises);
|
const heights = await Promise.all(heightPromises);
|
||||||
const totalNewHeight = heights.reduce((sum, h) => sum + h, 0);
|
const totalNewHeight = heights.reduce((sum, h) => sum + h, 0);
|
||||||
// 5. 设置scrollTop,保持滚动条在顶部附近
|
// 5. 设置scrollTop,保持滚动条在顶部附近
|
||||||
this.scrollTop = totalNewHeight;
|
this.scrollTop = totalNewHeight;
|
||||||
} else {
|
} else {
|
||||||
// 无更多历史
|
// 无更多历史
|
||||||
this.showLoadMore = false;
|
this.showLoadMore = false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载历史失败:', error);
|
console.error('加载历史失败:', error);
|
||||||
uni.showToast({ title: '加载历史失败', icon: 'none' });
|
uni.showToast({ title: '加载历史失败', icon: 'none' });
|
||||||
this.showLoadMore = false;
|
this.showLoadMore = false;
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoadingHistory = false;
|
this.isLoadingHistory = false;
|
||||||
this.isFetchingHistory = false;
|
this.isFetchingHistory = false;
|
||||||
this.hasMoreHistory = false;
|
this.hasMoreHistory = false;
|
||||||
this.loadingText = '加载更多历史消息';
|
this.loadingText = '加载更多历史消息';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 显示更多工具
|
// 显示更多工具
|
||||||
showMoreTools() {
|
showMoreTools() {
|
||||||
this.showToolsPanel = true
|
this.showToolsPanel = true
|
||||||
@@ -1402,7 +1354,8 @@ $radius-lg: 36rpx;
|
|||||||
padding: 24rpx 32rpx;
|
padding: 24rpx 32rpx;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 0; /* 重要:防止flex元素溢出 */
|
min-height: 0;
|
||||||
|
/* 重要:防止flex元素溢出 */
|
||||||
|
|
||||||
.load-more {
|
.load-more {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -1417,7 +1370,8 @@ $radius-lg: 36rpx;
|
|||||||
margin-top: 24rpx;
|
margin-top: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-message, .ai-message {
|
.user-message,
|
||||||
|
.ai-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
@@ -1448,17 +1402,20 @@ $radius-lg: 36rpx;
|
|||||||
margin-left: 24rpx;
|
margin-left: 24rpx;
|
||||||
max-width: calc(100% - 112rpx);
|
max-width: calc(100% - 112rpx);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 0; /* 添加这个属性防止flex元素溢出 */
|
width: 0;
|
||||||
|
/* 添加这个属性防止flex元素溢出 */
|
||||||
|
|
||||||
.message-nickname {
|
.message-nickname {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: $color-text-light;
|
color: $color-text-light;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 8rpx;
|
||||||
letter-spacing: 0.5rpx;
|
letter-spacing: 0.5rpx;
|
||||||
|
|
||||||
// 用户昵称右对齐,AI昵称左对齐
|
// 用户昵称右对齐,AI昵称左对齐
|
||||||
&:not(.ai-message .message-nickname) {
|
&:not(.ai-message .message-nickname) {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI昵称专属样式 - 左移到气泡上方
|
// AI昵称专属样式 - 左移到气泡上方
|
||||||
&.ai-nickname {
|
&.ai-nickname {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -1898,20 +1855,27 @@ $radius-lg: 36rpx;
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
background: linear-gradient(135deg, #ffadd2, #f783ac) !important; /* 粉色渐变 */
|
background: linear-gradient(135deg, #ffadd2, #f783ac) !important;
|
||||||
color: white !important;
|
/* 粉色渐变 */
|
||||||
border-radius: 16rpx 16rpx 4rpx 16rpx; /* 右对齐气泡尖角适配 */
|
color: white !important;
|
||||||
box-shadow: 0 8rpx 20rpx rgba(247, 131, 172, 0.3) !important;
|
border-radius: 16rpx 16rpx 4rpx 16rpx;
|
||||||
border: none !important;
|
/* 右对齐气泡尖角适配 */
|
||||||
/* ✅ 关键:允许内容撑开高度 */
|
box-shadow: 0 8rpx 20rpx rgba(247, 131, 172, 0.3) !important;
|
||||||
min-height: auto;
|
border: none !important;
|
||||||
height: auto;
|
/* ✅ 关键:允许内容撑开高度 */
|
||||||
padding: 24rpx 32rpx; /* 保留内边距 */
|
min-height: auto;
|
||||||
display: inline-block; /* 让宽度也随内容收缩(可选) */
|
height: auto;
|
||||||
max-width: 80%; /* 防止过宽 */
|
padding: 24rpx 32rpx;
|
||||||
word-break: break-word;
|
/* 保留内边距 */
|
||||||
white-space: pre-wrap; /* 保留用户输入的换行符 */
|
display: inline-block;
|
||||||
line-height: 1.6;
|
/* 让宽度也随内容收缩(可选) */
|
||||||
|
max-width: 80%;
|
||||||
|
/* 防止过宽 */
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
/* 保留用户输入的换行符 */
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1919,7 +1883,8 @@ $radius-lg: 36rpx;
|
|||||||
right: -14rpx;
|
right: -14rpx;
|
||||||
width: 24rpx;
|
width: 24rpx;
|
||||||
height: 24rpx;
|
height: 24rpx;
|
||||||
background: #ffffff !important; /* 菱形改为白色 */
|
background: #ffffff !important;
|
||||||
|
/* 菱形改为白色 */
|
||||||
border-radius: 6rpx;
|
border-radius: 6rpx;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
box-shadow: 4rpx -4rpx 4rpx rgba(0, 0, 0, 0.05);
|
box-shadow: 4rpx -4rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||||
@@ -1932,11 +1897,9 @@ $radius-lg: 36rpx;
|
|||||||
left: -100%;
|
left: -100%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(
|
background: linear-gradient(to right,
|
||||||
to right,
|
rgba(255, 255, 255, 0) 0%,
|
||||||
rgba(255, 255, 255, 0) 0%,
|
rgba(255, 255, 255, 0.15) 100%);
|
||||||
rgba(255, 255, 255, 0.15) 100%
|
|
||||||
);
|
|
||||||
animation: shine 4s infinite linear;
|
animation: shine 4s infinite linear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1952,19 +1915,22 @@ $radius-lg: 36rpx;
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe) !important; /* 蓝色浅渐变 */
|
background: linear-gradient(135deg, #dbeafe, #bfdbfe) !important;
|
||||||
color: #2c3e50 !important;
|
/* 蓝色浅渐变 */
|
||||||
border-radius: 16rpx 16rpx 16rpx 4rpx; /* 左对齐气泡尖角适配 */
|
color: #2c3e50 !important;
|
||||||
box-shadow: 0 8rpx 20rpx rgba(191, 219, 254, 0.4) !important;
|
border-radius: 16rpx 16rpx 16rpx 4rpx;
|
||||||
border: 1rpx solid #dbeafe !important;
|
/* 左对齐气泡尖角适配 */
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(191, 219, 254, 0.4) !important;
|
||||||
|
border: 1rpx solid #dbeafe !important;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 24rpx 32rpx;
|
padding: 24rpx 32rpx;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1972,7 +1938,8 @@ $radius-lg: 36rpx;
|
|||||||
left: -14rpx;
|
left: -14rpx;
|
||||||
width: 24rpx;
|
width: 24rpx;
|
||||||
height: 24rpx;
|
height: 24rpx;
|
||||||
background: #ffffff !important; /* 菱形改为白色 */
|
background: #ffffff !important;
|
||||||
|
/* 菱形改为白色 */
|
||||||
border-radius: 6rpx;
|
border-radius: 6rpx;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
box-shadow: -4rpx 4rpx 4rpx rgba(0, 0, 0, 0.05);
|
box-shadow: -4rpx 4rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||||
@@ -2072,10 +2039,13 @@ $radius-lg: 36rpx;
|
|||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border: none;
|
border: none;
|
||||||
text-align: center; /* 兼容多端文字居中 */
|
text-align: center;
|
||||||
white-space: nowrap; /* 强制文字单行横向显示 */
|
/* 兼容多端文字居中 */
|
||||||
line-height: 1; /* 重置行高,避免文字垂直偏移 */
|
white-space: nowrap;
|
||||||
top:-10px;
|
/* 强制文字单行横向显示 */
|
||||||
|
line-height: 1;
|
||||||
|
/* 重置行高,避免文字垂直偏移 */
|
||||||
|
top: -10px;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
background-color: $color-primary-light;
|
background-color: $color-primary-light;
|
||||||
@@ -2098,19 +2068,21 @@ $radius-lg: 36rpx;
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
pointer-events: none; /* 让鼠标事件穿透容器,不影响主内容交互 */
|
pointer-events: none;
|
||||||
z-index: 999; /* 基础弹窗层级 */
|
/* 让鼠标事件穿透容器,不影响主内容交互 */
|
||||||
|
z-index: 999;
|
||||||
|
/* 基础弹窗层级 */
|
||||||
|
|
||||||
/* 子弹窗需要开启 pointer-events,否则点击无效 */
|
/* 子弹窗需要开启 pointer-events,否则点击无效 */
|
||||||
> .tools-popup,
|
>.tools-popup,
|
||||||
> .voice-popup,
|
>.voice-popup,
|
||||||
> .avatar-selector-popup,
|
>.avatar-selector-popup,
|
||||||
> .nickname-editor-popup {
|
>.nickname-editor-popup {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 昵称编辑弹窗层级最高,避免被其他弹窗遮挡 */
|
/* 昵称编辑弹窗层级最高,避免被其他弹窗遮挡 */
|
||||||
> .nickname-editor-popup {
|
>.nickname-editor-popup {
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2470,14 +2442,45 @@ $radius-lg: 36rpx;
|
|||||||
border-radius: 4rpx;
|
border-radius: 4rpx;
|
||||||
animation: none;
|
animation: none;
|
||||||
|
|
||||||
&:nth-child(1) { height: 60rpx; animation-delay: 0s; }
|
&:nth-child(1) {
|
||||||
&:nth-child(2) { height: 90rpx; animation-delay: 0.1s; }
|
height: 60rpx;
|
||||||
&:nth-child(3) { height: 50rpx; animation-delay: 0.2s; }
|
animation-delay: 0s;
|
||||||
&:nth-child(4) { height: 120rpx; animation-delay: 0.3s; }
|
}
|
||||||
&:nth-child(5) { height: 70rpx; animation-delay: 0.4s; }
|
|
||||||
&:nth-child(6) { height: 100rpx; animation-delay: 0.5s; }
|
&:nth-child(2) {
|
||||||
&:nth-child(7) { height: 80rpx; animation-delay: 0.6s; }
|
height: 90rpx;
|
||||||
&:nth-child(8) { height: 40rpx; animation-delay: 0.7s; }
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
height: 50rpx;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
height: 120rpx;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(5) {
|
||||||
|
height: 70rpx;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(6) {
|
||||||
|
height: 100rpx;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(7) {
|
||||||
|
height: 80rpx;
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(8) {
|
||||||
|
height: 40rpx;
|
||||||
|
animation-delay: 0.7s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2645,10 +2648,13 @@ $radius-lg: 36rpx;
|
|||||||
|
|
||||||
/* 动画定义 */
|
/* 动画定义 */
|
||||||
@keyframes dotPulse {
|
@keyframes dotPulse {
|
||||||
0%, 100% {
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
@@ -2656,10 +2662,13 @@ $radius-lg: 36rpx;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes wave {
|
@keyframes wave {
|
||||||
0%, 100% {
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
transform: scaleY(0.5);
|
transform: scaleY(0.5);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: scaleY(1);
|
transform: scaleY(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -2670,6 +2679,7 @@ $radius-lg: 36rpx;
|
|||||||
0% {
|
0% {
|
||||||
left: -100%;
|
left: -100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
left: 200%;
|
left: 200%;
|
||||||
}
|
}
|
||||||
@@ -2679,9 +2689,11 @@ $radius-lg: 36rpx;
|
|||||||
0% {
|
0% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
background-position: 100% 50%;
|
background-position: 100% 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
}
|
}
|
||||||
@@ -2718,7 +2730,8 @@ $radius-lg: 36rpx;
|
|||||||
.message-item {
|
.message-item {
|
||||||
margin-bottom: 24rpx;
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
.user-message, .ai-message {
|
.user-message,
|
||||||
|
.ai-message {
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 72rpx;
|
width: 72rpx;
|
||||||
height: 72rpx;
|
height: 72rpx;
|
||||||
@@ -52,7 +52,12 @@ import { mapGetters, mapMutations } from 'vuex'
|
|||||||
import navigationHelper from '@/common/js/navigation';
|
import navigationHelper from '@/common/js/navigation';
|
||||||
import { EventSafety } from '@/common/js/event-safety';
|
import { EventSafety } from '@/common/js/event-safety';
|
||||||
|
|
||||||
|
import aiChatMessage from './ai-chat-message.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
aiChatMessage,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
initialMessages: [
|
initialMessages: [
|
||||||
@@ -86,7 +91,6 @@ export default {
|
|||||||
safeEventHandlers: new Map()
|
safeEventHandlers: new Map()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters([
|
...mapGetters([
|
||||||
'globalAIKefuConfig'
|
'globalAIKefuConfig'
|
||||||
@@ -142,9 +146,6 @@ export default {
|
|||||||
this.cleanup()
|
this.cleanup()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
// ========== 安全事件处理 ==========
|
// ========== 安全事件处理 ==========
|
||||||
setupSafeEventListeners() {
|
setupSafeEventListeners() {
|
||||||
|
|||||||
Reference in New Issue
Block a user