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