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