style: 更新样式选项
This commit is contained in:
parent
ef78196b8c
commit
a0e2ae794f
|
|
@ -134,11 +134,7 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.attachment-item:hover {
|
||||
border-color: var(--attachment-color);
|
||||
box-shadow: 0 2px 8px rgba(202, 138, 4, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
.attachment-icon {
|
||||
background: var(--attachment-bg);
|
||||
|
|
@ -169,9 +165,7 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
|||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.message-bubble:hover .message-footer {
|
||||
border-top-color: var(--border-light);
|
||||
}
|
||||
|
||||
|
||||
.token-item {
|
||||
font-size: 11px;
|
||||
|
|
@ -183,10 +177,7 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.token-item:hover {
|
||||
background: var(--accent-primary-light);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
|
|
@ -209,10 +200,7 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sender-name:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
|
||||
.round-tag {
|
||||
font-size: 0.7rem;
|
||||
|
|
@ -226,10 +214,7 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.round-tag:hover {
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
|
||||
/* ============ Message Body Enhancements ============ */
|
||||
.message-body {
|
||||
|
|
@ -249,9 +234,7 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
|||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.message-bubble:hover .message-body::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* ============ Room Message Special Styles ============ */
|
||||
.message-bubble.room-msg {
|
||||
|
|
@ -275,10 +258,7 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
|||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.message-bubble:hover :deep(.avatar) {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
|
||||
/* ============ Content Styling ============ */
|
||||
.message-content {
|
||||
|
|
@ -292,20 +272,5 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
|||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
:deep(.ghost-btn:hover) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(.ghost-btn.success:hover) {
|
||||
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.2);
|
||||
}
|
||||
|
||||
:deep(.ghost-btn.danger:hover) {
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
:deep(.ghost-btn.accent:hover) {
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { conversationsAPI, messagesAPI, toolsAPI, providersAPI } from './api.js'
|
||||
import { streamManager } from './streamManager.js'
|
||||
import { useStreamStore } from './streamStore.js'
|
||||
|
|
@ -100,6 +100,12 @@ export function useConversations() {
|
|||
const res = await messagesAPI.list(convId)
|
||||
if (res.success) {
|
||||
convMessages.value = res.data?.messages || []
|
||||
// 加载完成后强制滚动到底部(初始加载总是显示最新消息)
|
||||
nextTick(() => {
|
||||
if (typeof onInitialScroll === 'function') {
|
||||
onInitialScroll()
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取消息失败:', e)
|
||||
|
|
@ -271,6 +277,13 @@ export function useConversations() {
|
|||
}
|
||||
}
|
||||
|
||||
// 初始滚动回调(由外部设置)
|
||||
let onInitialScroll = null
|
||||
|
||||
const setOnInitialScroll = (callback) => {
|
||||
onInitialScroll = callback
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
list,
|
||||
|
|
@ -298,6 +311,7 @@ export function useConversations() {
|
|||
deleteMessage,
|
||||
regenerateMessage,
|
||||
loadEnabledTools,
|
||||
setOnInitialScroll,
|
||||
init,
|
||||
cleanup
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ const streaming = ref(false)
|
|||
const streamingMessages = ref({}) // Track in-progress streaming messages
|
||||
const error = ref('')
|
||||
const messagesContainer = ref(null)
|
||||
const isNearBottom = ref(true) // 是否接近底部
|
||||
|
||||
// Room agent editing
|
||||
const editingRoomAgent = ref(null)
|
||||
|
|
@ -282,7 +283,7 @@ async function selectRoom(id) {
|
|||
messages.value = msgRes.data?.messages || []
|
||||
if (msgRes.data?.room) room.value = msgRes.data.room
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
forceScrollToBottom() // 选择聊天室时强制滚动到底部
|
||||
} catch (e) {
|
||||
console.error('Failed to load room:', e)
|
||||
error.value = '加载失败'
|
||||
|
|
@ -292,11 +293,35 @@ async function selectRoom(id) {
|
|||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (!isNearBottom.value) return
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 智能滚动:检测是否接近底部
|
||||
function checkNearBottom() {
|
||||
if (!messagesContainer.value) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = messagesContainer.value
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
|
||||
isNearBottom.value = distanceFromBottom <= 100
|
||||
}
|
||||
|
||||
// 滚动事件处理
|
||||
function handleScroll() {
|
||||
checkNearBottom()
|
||||
}
|
||||
|
||||
// 强制滚动到底部(启动聊天室时使用)
|
||||
function forceScrollToBottom() {
|
||||
nextTick(() => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
isNearBottom.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============ SSE Streaming ============
|
||||
|
||||
async function startRoom() {
|
||||
|
|
@ -381,8 +406,8 @@ function handleSSEEvent(event, data) {
|
|||
// Move from streaming to complete messages
|
||||
delete streamingMessages.value[data.id]
|
||||
messages.value.push(msg)
|
||||
forceScrollToBottom() // 聊天室消息结束时强制滚动
|
||||
}
|
||||
nextTick(scrollToBottom)
|
||||
break
|
||||
}
|
||||
// Legacy complete message event
|
||||
|
|
@ -391,7 +416,7 @@ function handleSSEEvent(event, data) {
|
|||
delete streamingMessages.value[data.id]
|
||||
// Add complete message
|
||||
messages.value.push(data)
|
||||
nextTick(scrollToBottom)
|
||||
forceScrollToBottom() // 聊天室消息结束时强制滚动
|
||||
break
|
||||
}
|
||||
case 'room_started': room.value = { ...room.value, status: 'running' }; break
|
||||
|
|
@ -480,6 +505,11 @@ function randomColor() {
|
|||
|
||||
watch(messages, () => { nextTick(scrollToBottom) }, { deep: true })
|
||||
|
||||
// 启动聊天室时强制滚动
|
||||
watch(streaming, (val) => {
|
||||
if (val) forceScrollToBottom()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadAgentPool()
|
||||
loadRooms()
|
||||
|
|
@ -650,7 +680,7 @@ onUnmounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="error" class="error-bar">{{ error }}<button @click="error = ''">×</button></div>
|
||||
<div class="chat-messages" ref="messagesContainer">
|
||||
<div class="chat-messages" ref="messagesContainer" @scroll="handleScroll">
|
||||
<div v-if="messagesLoading" class="loading-messages"><div class="spinner-small"></div><span>加载中...</span></div>
|
||||
<div v-else-if="messages.length === 0 && Object.keys(streamingMessages).length === 0" class="chat-empty"><p>点击「开始」启动多 Agent 对话</p></div>
|
||||
<div v-else>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
<p>选择一个会话查看</p>
|
||||
</div>
|
||||
<div v-else class="chat-view-container">
|
||||
<div class="chat-messages" ref="messagesContainer">
|
||||
<div class="chat-messages" ref="messagesContainer" @scroll="handleScroll">
|
||||
<div v-if="loadingMessages" class="loading-messages">
|
||||
<div class="spinner-small"></div>
|
||||
<span>加载中...</span>
|
||||
|
|
@ -191,10 +191,14 @@ const {
|
|||
updateConvTitle,
|
||||
deleteMessage,
|
||||
regenerateMessage,
|
||||
setOnInitialScroll,
|
||||
init,
|
||||
cleanup
|
||||
} = useConversations()
|
||||
|
||||
// 设置初始滚动回调(加载历史消息后强制滚动)
|
||||
setOnInitialScroll(forceScrollToBottom)
|
||||
|
||||
const showModal = ref(false)
|
||||
const creating = ref(false)
|
||||
const form = ref({ title: '', provider_id: null, model: '' })
|
||||
|
|
@ -202,6 +206,7 @@ const form = ref({ title: '', provider_id: null, model: '' })
|
|||
const newMessage = ref('')
|
||||
const messagesContainer = ref(null)
|
||||
const activeMessageId = ref(null)
|
||||
const isNearBottom = ref(true) // 是否接近底部
|
||||
|
||||
const editConv = ref(null)
|
||||
|
||||
|
|
@ -210,7 +215,7 @@ const handleSend = async () => {
|
|||
if (!newMessage.value.trim()) return
|
||||
const message = newMessage.value.trim()
|
||||
newMessage.value = '' // 先清空输入框,再发送
|
||||
scrollToBottom()
|
||||
forceScrollToBottom() // 发送消息时强制滚动到底部
|
||||
await sendMessage(message)
|
||||
}
|
||||
|
||||
|
|
@ -278,8 +283,17 @@ const onProviderChange = () => {
|
|||
if (p) form.value.model = p.default_model || ''
|
||||
}
|
||||
|
||||
// 自动滚动到底部
|
||||
// 检测是否接近底部(距离底部 100px 以内)
|
||||
const checkNearBottom = () => {
|
||||
if (!messagesContainer.value) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = messagesContainer.value
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
|
||||
isNearBottom.value = distanceFromBottom <= 100
|
||||
}
|
||||
|
||||
// 智能滚动到底部 - 只有在接近底部时才自动滚动
|
||||
const scrollToBottom = () => {
|
||||
if (!isNearBottom.value) return
|
||||
nextTick(() => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
|
|
@ -287,6 +301,21 @@ const scrollToBottom = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// 强制滚动到底部(发送消息时使用)
|
||||
const forceScrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
isNearBottom.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动事件处理
|
||||
const handleScroll = () => {
|
||||
checkNearBottom()
|
||||
}
|
||||
|
||||
watch(convMessages, () => {
|
||||
scrollToBottom()
|
||||
}, { deep: true })
|
||||
|
|
|
|||
Loading…
Reference in New Issue