Compare commits

..

1 Commits

Author SHA1 Message Date
Tim
615832f112 feat: add floating new message container 2025-09-07 23:49:28 +08:00
3 changed files with 75 additions and 66 deletions

View File

@@ -0,0 +1,33 @@
<template>
<div class="new-message-container" :style="{ bottom: bottom + 'px' }" @click="$emit('click')">
{{ count }} 条新消息点击查看
</div>
</template>
<script setup>
const props = defineProps({
count: {
type: Number,
default: 0,
},
bottom: {
type: Number,
default: 0,
},
})
</script>
<style scoped>
.new-message-container {
position: absolute;
left: 50%;
transform: translateX(-50%);
background-color: var(--primary-color);
color: #fff;
padding: 6px 16px;
border-radius: 20px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 50;
}
</style>

View File

@@ -36,11 +36,7 @@
</div>
</div>
<div v-if="item.replyTo" class="reply-preview info-content-text">
<div class="reply-header">
<next class="reply-icon" />
<BaseImage class="reply-avatar" :src="item.replyTo.sender.avatar" alt="avatar" />
<div class="reply-author">{{ item.replyTo.sender.username }}:</div>
</div>
<div class="reply-author">{{ item.replyTo.sender.username }}</div>
<div class="reply-content" v-html="renderMarkdown(item.replyTo.content)"></div>
</div>
<div class="message-content">
@@ -66,22 +62,19 @@
</template>
</div>
<div class="message-input-area">
<div
v-if="newMessagesCount > 0 && !isUserNearBottom"
class="new-message-container"
@click="handleScrollToBottom"
>
<double-down />
<div class="new-message-count">{{ newMessagesCount }}条新消息</div>
</div>
<NewMessageContainer
v-if="showNewMessageContainer"
:count="newMessagesCount"
:bottom="inputAreaHeight + 20"
@click="handleNewMessagesClick"
/>
<div class="message-input-area" ref="messageInputAreaEl">
<div v-if="replyTo" class="active-reply">
正在回复 {{ replyTo.sender.username }}:
{{ stripMarkdownLength(replyTo.content, 50) }}
<close-icon class="close-reply" @click="replyTo = null" />
</div>
<MessageEditor :loading="sending" @submit="sendMessage" />
</div>
</div>
@@ -110,6 +103,7 @@ import { useChannelsUnreadCount } from '~/composables/useChannelsUnreadCount'
import TimeManager from '~/utils/time'
import BaseTimeline from '~/components/BaseTimeline.vue'
import BasePlaceholder from '~/components/BasePlaceholder.vue'
import NewMessageContainer from '~/components/NewMessageContainer.vue'
const config = useRuntimeConfig()
const route = useRoute()
@@ -126,6 +120,7 @@ const error = ref(null)
const conversationId = route.params.id
const currentUser = ref(null)
const messagesListEl = ref(null)
const messageInputAreaEl = ref(null)
const currentPage = ref(0)
const totalPages = ref(0)
const loadingMore = ref(false)
@@ -135,6 +130,20 @@ const isFloatMode = computed(() => route.query.float !== undefined)
const floatRoute = useState('messageFloatRoute')
const replyTo = ref(null)
const newMessagesCount = ref(0)
const inputAreaHeight = ref(0)
const showNewMessageContainer = computed(
() => newMessagesCount.value > 0 && !isUserNearBottom.value,
)
function updateInputAreaHeight() {
if (!messageInputAreaEl.value) return
inputAreaHeight.value = messageInputAreaEl.value.offsetHeight
}
function handleNewMessagesClick() {
scrollToBottomSmooth()
newMessagesCount.value = 0
}
const isUserNearBottom = ref(true)
function updateNearBottom() {
@@ -142,9 +151,6 @@ function updateNearBottom() {
if (!el) return
const threshold = 40 // px
isUserNearBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold
if (isUserNearBottom.value) {
newMessagesCount.value = 0
}
}
const hasMoreMessages = computed(() => currentPage.value < totalPages.value - 1)
@@ -188,11 +194,6 @@ function scrollToBottomInstant() {
el.scrollTop = el.scrollHeight
}
function handleScrollToBottom() {
scrollToBottomSmooth()
newMessagesCount.value = 0
}
async function fetchMessages(page = 0) {
if (page === 0) {
loading.value = true
@@ -324,7 +325,6 @@ async function sendMessage(content, clearInput) {
await nextTick()
// 仅“发送消息成功后”才平滑滚动到底部
scrollToBottomSmooth()
newMessagesCount.value = 0
} catch (e) {
toast.error(e.message)
} finally {
@@ -353,6 +353,10 @@ onMounted(async () => {
messagesListEl.value.addEventListener('scroll', updateNearBottom, { passive: true })
}
window.addEventListener('resize', updateInputAreaHeight)
await nextTick()
updateInputAreaHeight()
currentUser.value = await fetchCurrentUser()
if (currentUser.value) {
await fetchMessages(0)
@@ -394,7 +398,6 @@ const subscribeToConversation = () => {
await markConversationAsRead()
await nextTick()
if (isUserNearBottom.value) {
scrollToBottomSmooth()
} else {
@@ -412,6 +415,14 @@ watch(isConnected, (newValue) => {
}
})
watch(isUserNearBottom, (val) => {
if (val) newMessagesCount.value = 0
})
watch(replyTo, () => {
nextTick(updateInputAreaHeight)
})
onActivated(async () => {
// 返回页面时:刷新数据与已读,并滚动到底部
if (currentUser.value) {
@@ -444,6 +455,7 @@ onUnmounted(() => {
if (messagesListEl.value) {
messagesListEl.value.removeEventListener('scroll', updateNearBottom)
}
window.removeEventListener('resize', updateInputAreaHeight)
})
function minimize() {
@@ -581,25 +593,6 @@ function goBack() {
gap: 10px;
}
.new-message-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
cursor: pointer;
border: 1px solid var(--normal-border-color);
border-radius: 20px;
padding: 3px 6px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
width: fit-content;
position: absolute;
bottom: calc(100% + 20px);
left: 50%;
transform: translateX(-50%);
z-index: 10;
background-color: var(--background-color);
}
.user-name {
font-size: 14px;
font-weight: 600;
@@ -630,6 +623,11 @@ function goBack() {
border-bottom-left-radius: 4px;
}
.message-input-area {
margin-left: 20px;
margin-right: 20px;
}
.loading-container {
display: flex;
justify-content: center;
@@ -646,19 +644,6 @@ function goBack() {
.message-input-area {
margin-left: 10px;
margin-right: 10px;
position: relative;
}
.reply-icon {
color: var(--primary-color);
margin-right: 5px;
}
.reply-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 5px;
}
.reply-preview {
@@ -670,16 +655,9 @@ function goBack() {
background-color: var(--normal-light-background-color);
}
.reply-header {
display: flex;
flex-direction: row;
align-items: center;
}
.reply-author {
font-weight: bold;
margin-bottom: 2px;
opacity: 0.5;
}
.reply-btn {

View File

@@ -73,7 +73,6 @@ import {
RobotOne,
Server,
Protection,
DoubleDown,
} from '@icon-park/vue-next'
export default defineNuxtPlugin((nuxtApp) => {
@@ -150,5 +149,4 @@ export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('RobotOne', RobotOne)
nuxtApp.vueApp.component('ServerIcon', Server)
nuxtApp.vueApp.component('Protection', Protection)
nuxtApp.vueApp.component('DoubleDown', DoubleDown)
})