style: 更新样式选项

This commit is contained in:
ViperEkura 2026-04-26 14:33:13 +08:00
parent ef78196b8c
commit a0e2ae794f
4 changed files with 88 additions and 50 deletions

View File

@ -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>

View File

@ -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
}

View File

@ -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 = ''">&times;</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>

View File

@ -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 })