fix: 优化样式
This commit is contained in:
parent
6f9bff1f1f
commit
4618012a9a
|
|
@ -16,11 +16,14 @@ const { isLoggedIn } = useAuth()
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#app {
|
#app {
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
.main-content {
|
.main-content {
|
||||||
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-height: 100vh;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="messages.length > 0" class="bookmark-rail">
|
||||||
|
<div
|
||||||
|
v-for="msg in userMessages"
|
||||||
|
:key="msg.id"
|
||||||
|
class="bookmark"
|
||||||
|
:class="{ active: activeId === msg.id }"
|
||||||
|
@click="$emit('scrollTo', msg.id)"
|
||||||
|
>
|
||||||
|
<div class="bookmark-dot"></div>
|
||||||
|
<div class="bookmark-label">{{ preview(msg) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
messages: { type: Array, required: true },
|
||||||
|
activeId: { type: String, default: null },
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['scrollTo'])
|
||||||
|
|
||||||
|
const userMessages = computed(() => props.messages.filter(m => m.role === 'user'))
|
||||||
|
|
||||||
|
function preview(msg) {
|
||||||
|
if (!msg.text) return '...'
|
||||||
|
// Clean markdown characters
|
||||||
|
const clean = msg.text.replace(/[#*`~>\-\[\]()]/g, '').replace(/\s+/g, ' ').trim()
|
||||||
|
const MAX_LENGTH = 30
|
||||||
|
return clean.length > MAX_LENGTH ? clean.slice(0, MAX_LENGTH) + '...' : clean
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bookmark-rail {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 10px;
|
||||||
|
height: 70vh;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 4px 0;
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-rail::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-rail:hover {
|
||||||
|
width: 10vw;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark.active {
|
||||||
|
background: var(--bg-active);
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-dot {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 1.5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #3b82f6;
|
||||||
|
opacity: 0.35;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark.active .bookmark-dot,
|
||||||
|
.bookmark-rail:hover .bookmark-dot {
|
||||||
|
opacity: 1;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-label {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(8px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-rail:hover .bookmark-label {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark.active .bookmark-label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="convMessages.length || streamingMessage">
|
<div v-else-if="convMessages.length || streamingMessage">
|
||||||
<!-- 历史消息 -->
|
<!-- 历史消息 -->
|
||||||
<div v-for="msg in convMessages" :key="msg.id" class="chat-message" :class="msg.role">
|
<div v-for="msg in convMessages" :key="msg.id" class="chat-message" :class="msg.role" :data-msg-id="msg.id">
|
||||||
<div class="message-avatar">{{ msg.role === 'user' ? '👤' : '🤖' }}</div>
|
<div class="message-avatar">{{ msg.role === 'user' ? '👤' : '🤖' }}</div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<!-- 工具调用步骤显示(包含思考和文本内容) -->
|
<!-- 工具调用步骤显示(包含思考和文本内容) -->
|
||||||
|
|
@ -133,15 +133,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息导航栏 -->
|
||||||
|
<MessageNav
|
||||||
|
v-if="selectedConv && convMessages.length > 0"
|
||||||
|
:messages="convMessages"
|
||||||
|
:active-id="activeMessageId"
|
||||||
|
@scroll-to="scrollToMessageById"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { conversationsAPI, providersAPI, messagesAPI, toolsAPI } from '../utils/api.js'
|
import { conversationsAPI, providersAPI, messagesAPI, toolsAPI } from '../utils/api.js'
|
||||||
import { renderMarkdown } from '../utils/markdown.js'
|
import { renderMarkdown } from '../utils/markdown.js'
|
||||||
import ProcessBlock from '../components/ProcessBlock.vue'
|
import ProcessBlock from '../components/ProcessBlock.vue'
|
||||||
|
import MessageNav from '../components/MessageNav.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
|
|
@ -209,9 +218,60 @@ const selectConv = async (c) => {
|
||||||
await fetchConvMessages(c.id)
|
await fetchConvMessages(c.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newMessage = ref('')
|
||||||
|
const sending = ref(false)
|
||||||
|
const messagesContainer = ref(null)
|
||||||
|
const streamingMessage = ref(null)
|
||||||
|
const activeMessageId = ref(null)
|
||||||
|
let scrollObserver = null
|
||||||
|
const observedElements = new Set()
|
||||||
|
|
||||||
|
// 初始化 IntersectionObserver 来跟踪可见消息
|
||||||
|
const initScrollObserver = () => {
|
||||||
|
if (!messagesContainer.value) return
|
||||||
|
scrollObserver?.disconnect()
|
||||||
|
scrollObserver = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
activeMessageId.value = entry.target.dataset.msgId || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ root: messagesContainer.value, threshold: 0.5 }
|
||||||
|
)
|
||||||
|
// 观察已有的消息元素
|
||||||
|
nextTick(() => {
|
||||||
|
if (!messagesContainer.value) return
|
||||||
|
const wrappers = messagesContainer.value.querySelectorAll('[data-msg-id]')
|
||||||
|
wrappers.forEach(el => {
|
||||||
|
if (!observedElements.has(el)) {
|
||||||
|
scrollObserver.observe(el)
|
||||||
|
observedElements.add(el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 观察新添加的消息元素
|
||||||
|
watch(() => convMessages.value.length, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (!scrollObserver || !messagesContainer.value) return
|
||||||
|
const wrappers = messagesContainer.value.querySelectorAll('[data-msg-id]')
|
||||||
|
wrappers.forEach(el => {
|
||||||
|
if (!observedElements.has(el)) {
|
||||||
|
scrollObserver.observe(el)
|
||||||
|
observedElements.add(el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const fetchConvMessages = async (convId) => {
|
const fetchConvMessages = async (convId) => {
|
||||||
loadingMessages.value = true
|
loadingMessages.value = true
|
||||||
convMessages.value = []
|
convMessages.value = []
|
||||||
|
// 重置观察的元素集合
|
||||||
|
observedElements.clear()
|
||||||
try {
|
try {
|
||||||
const res = await messagesAPI.list(convId)
|
const res = await messagesAPI.list(convId)
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
|
@ -221,13 +281,32 @@ const fetchConvMessages = async (convId) => {
|
||||||
console.error('获取消息失败:', e)
|
console.error('获取消息失败:', e)
|
||||||
} finally {
|
} finally {
|
||||||
loadingMessages.value = false
|
loadingMessages.value = false
|
||||||
|
// 加载完成后初始化 observer
|
||||||
|
nextTick(() => initScrollObserver())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMessage = ref('')
|
// 导航到指定消息
|
||||||
const sending = ref(false)
|
const scrollToMessage = (index) => {
|
||||||
const messagesContainer = ref(null)
|
if (!messagesContainer.value) return
|
||||||
const streamingMessage = ref(null)
|
const items = messagesContainer.value.querySelectorAll('.chat-message')
|
||||||
|
if (items[index]) {
|
||||||
|
items[index].scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航到指定消息(通过ID)
|
||||||
|
const scrollToMessageById = (msgId) => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (!messagesContainer.value) return
|
||||||
|
// 使用 data-msg-id 直接定位消息元素
|
||||||
|
const el = messagesContainer.value.querySelector(`[data-msg-id="${msgId}"]`)
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
activeMessageId.value = msgId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
watch(convMessages, () => {
|
watch(convMessages, () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|
@ -375,6 +454,10 @@ onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
loadEnabledTools()
|
loadEnabledTools()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
scrollObserver?.disconnect()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -382,13 +465,22 @@ onMounted(() => {
|
||||||
.page-container {
|
.page-container {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container.conversations {
|
||||||
|
height: calc(100vh - 80px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conv-layout {
|
.conv-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
height: calc(100vh - 80px);
|
height: 100%;
|
||||||
min-height: 400px;
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 左侧边栏 */
|
/* 左侧边栏 */
|
||||||
|
|
@ -463,7 +555,6 @@ onMounted(() => {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -544,6 +635,7 @@ onMounted(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-content {
|
.empty-content {
|
||||||
|
|
@ -574,6 +666,27 @@ onMounted(() => {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-toggle:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -616,7 +729,13 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
max-width: 70%;
|
max-width: 80%;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.user .message-content {
|
||||||
|
max-width: 80%;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue