mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-04-21 11:27:27 +08:00
Merge pull request #722 from nagisa77/codex/add-floating-window-support-for-message-box-a7msu4
feat: add floating message box window
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="header-container">
|
<div v-if="!isFloatMode" class="header-container">
|
||||||
<HeaderComponent
|
<HeaderComponent
|
||||||
ref="header"
|
ref="header"
|
||||||
@toggle-menu="menuVisible = !menuVisible"
|
@toggle-menu="menuVisible = !menuVisible"
|
||||||
@@ -9,19 +9,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="menu-container" v-click-outside="handleMenuOutside">
|
<div v-if="!isFloatMode" class="menu-container" v-click-outside="handleMenuOutside">
|
||||||
<MenuComponent :visible="!hideMenu && menuVisible" @item-click="menuVisible = false" />
|
<MenuComponent :visible="!hideMenu && menuVisible" @item-click="menuVisible = false" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content" :class="{ 'menu-open': menuVisible && !hideMenu }">
|
<div
|
||||||
|
class="content"
|
||||||
|
:class="{ 'menu-open': menuVisible && !hideMenu && !isFloatMode }"
|
||||||
|
:style="isFloatMode ? { paddingTop: '0px', minHeight: '100vh' } : {}"
|
||||||
|
>
|
||||||
<NuxtPage keepalive />
|
<NuxtPage keepalive />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showNewPostIcon && isMobile" class="app-new-post-icon" @click="goToNewPost">
|
<div
|
||||||
|
v-if="showNewPostIcon && isMobile && !isFloatMode"
|
||||||
|
class="app-new-post-icon"
|
||||||
|
@click="goToNewPost"
|
||||||
|
>
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GlobalPopups />
|
<GlobalPopups />
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
|
<MessageFloatWindow v-if="!isFloatMode" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -30,6 +39,7 @@ import HeaderComponent from '~/components/HeaderComponent.vue'
|
|||||||
import MenuComponent from '~/components/MenuComponent.vue'
|
import MenuComponent from '~/components/MenuComponent.vue'
|
||||||
import GlobalPopups from '~/components/GlobalPopups.vue'
|
import GlobalPopups from '~/components/GlobalPopups.vue'
|
||||||
import ConfirmDialog from '~/components/ConfirmDialog.vue'
|
import ConfirmDialog from '~/components/ConfirmDialog.vue'
|
||||||
|
import MessageFloatWindow from '~/components/MessageFloatWindow.vue'
|
||||||
import { useIsMobile } from '~/utils/screen'
|
import { useIsMobile } from '~/utils/screen'
|
||||||
|
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
@@ -52,6 +62,7 @@ const hideMenu = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const header = useTemplateRef('header')
|
const header = useTemplateRef('header')
|
||||||
|
const isFloatMode = computed(() => useRoute().query.float !== undefined)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|||||||
65
frontend_nuxt/components/MessageFloatWindow.vue
Normal file
65
frontend_nuxt/components/MessageFloatWindow.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="floatRoute" class="message-float-window">
|
||||||
|
<iframe :src="iframeSrc" frameborder="0"></iframe>
|
||||||
|
<div class="float-actions">
|
||||||
|
<i class="fas fa-expand" @click="expand"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const floatRoute = useState('messageFloatRoute')
|
||||||
|
|
||||||
|
const iframeSrc = computed(() => {
|
||||||
|
if (!floatRoute.value) return ''
|
||||||
|
return floatRoute.value + (floatRoute.value.includes('?') ? '&' : '?') + 'float=1'
|
||||||
|
})
|
||||||
|
|
||||||
|
function expand() {
|
||||||
|
if (!floatRoute.value) return
|
||||||
|
const target = floatRoute.value
|
||||||
|
floatRoute.value = null
|
||||||
|
navigateTo(target)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.message-float-window {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 400px;
|
||||||
|
height: 60vh;
|
||||||
|
max-height: 90vh;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 1px solid var(--normal-border-color);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-float-window iframe {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-actions i {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.message-float-window {
|
||||||
|
width: 100%;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-container">
|
<div class="chat-container" :class="{ float: isFloatMode }">
|
||||||
<div v-if="!loading" class="chat-header">
|
<div v-if="!loading" class="chat-header">
|
||||||
<NuxtLink to="/message-box" class="back-button">
|
<div class="header-main">
|
||||||
<i class="fas fa-arrow-left"></i>
|
<NuxtLink to="/message-box" class="back-button">
|
||||||
</NuxtLink>
|
<i class="fas fa-arrow-left"></i>
|
||||||
<h2 class="participant-name">
|
</NuxtLink>
|
||||||
{{ isChannel ? conversationName : otherParticipant?.username }}
|
<h2 class="participant-name">
|
||||||
</h2>
|
{{ isChannel ? conversationName : otherParticipant?.username }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isFloatMode" class="header-actions" @click="minimize">
|
||||||
|
<i class="fas fa-window-minimize"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="messages-list" ref="messagesListEl">
|
<div class="messages-list" ref="messagesListEl">
|
||||||
@@ -115,6 +120,8 @@ const loadingMore = ref(false)
|
|||||||
let scrollInterval = null
|
let scrollInterval = null
|
||||||
const conversationName = ref('')
|
const conversationName = ref('')
|
||||||
const isChannel = ref(false)
|
const isChannel = ref(false)
|
||||||
|
const isFloatMode = computed(() => route.query.float !== undefined)
|
||||||
|
const floatRoute = useState('messageFloatRoute')
|
||||||
const replyTo = ref(null)
|
const replyTo = ref(null)
|
||||||
|
|
||||||
const hasMoreMessages = computed(() => currentPage.value < totalPages.value - 1)
|
const hasMoreMessages = computed(() => currentPage.value < totalPages.value - 1)
|
||||||
@@ -179,7 +186,7 @@ async function fetchMessages(page = 0) {
|
|||||||
...item,
|
...item,
|
||||||
src: item.sender.avatar,
|
src: item.sender.avatar,
|
||||||
iconClick: () => {
|
iconClick: () => {
|
||||||
navigateTo(`/users/${item.sender.id}`, { replace: true })
|
openUser(item.sender.id)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -260,7 +267,7 @@ async function sendMessage(content, clearInput) {
|
|||||||
...newMessage,
|
...newMessage,
|
||||||
src: newMessage.sender.avatar,
|
src: newMessage.sender.avatar,
|
||||||
iconClick: () => {
|
iconClick: () => {
|
||||||
navigateTo(`/users/${newMessage.sender.id}`, { replace: true })
|
openUser(newMessage.sender.id)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
clearInput()
|
clearInput()
|
||||||
@@ -347,7 +354,7 @@ watch(isConnected, (newValue) => {
|
|||||||
...message,
|
...message,
|
||||||
src: message.sender.avatar,
|
src: message.sender.avatar,
|
||||||
iconClick: () => {
|
iconClick: () => {
|
||||||
navigateTo(`/users/${message.sender.id}`, { replace: true })
|
openUser(message.sender.id)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// 实时收到消息时自动标记为已读
|
// 实时收到消息时自动标记为已读
|
||||||
@@ -401,6 +408,19 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
disconnect()
|
disconnect()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function minimize() {
|
||||||
|
floatRoute.value = route.fullPath
|
||||||
|
navigateTo('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUser(id) {
|
||||||
|
if (isFloatMode.value && typeof window !== 'undefined') {
|
||||||
|
window.top.location.href = `/users/${id}`
|
||||||
|
} else {
|
||||||
|
navigateTo(`/users/${id}`, { replace: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -413,8 +433,13 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-container.float {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
@@ -425,6 +450,15 @@ onUnmounted(() => {
|
|||||||
backdrop-filter: var(--blur-10);
|
backdrop-filter: var(--blur-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions i {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: var(--text-color-primary);
|
color: var(--text-color-primary);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="messages-container">
|
<div class="messages-container">
|
||||||
|
<div v-if="!isFloatMode" class="float-control">
|
||||||
|
<i class="fas fa-window-minimize" @click="minimize"></i>
|
||||||
|
</div>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div :class="['tab', { active: activeTab === 'messages' }]" @click="activeTab = 'messages'">
|
<div :class="['tab', { active: activeTab === 'messages' }]" @click="activeTab = 'messages'">
|
||||||
站内信
|
站内信
|
||||||
@@ -114,8 +117,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, onUnmounted, watch, onActivated, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ref, onUnmounted, watch, onActivated } from 'vue'
|
|
||||||
import { getToken, fetchCurrentUser } from '~/utils/auth'
|
import { getToken, fetchCurrentUser } from '~/utils/auth'
|
||||||
import { toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { useWebSocket } from '~/composables/useWebSocket'
|
import { useWebSocket } from '~/composables/useWebSocket'
|
||||||
@@ -143,6 +146,9 @@ let subscription = null
|
|||||||
const activeTab = ref('messages')
|
const activeTab = ref('messages')
|
||||||
const channels = ref([])
|
const channels = ref([])
|
||||||
const loadingChannels = ref(false)
|
const loadingChannels = ref(false)
|
||||||
|
const route = useRoute()
|
||||||
|
const isFloatMode = computed(() => route.query.float !== undefined)
|
||||||
|
const floatRoute = useState('messageFloatRoute')
|
||||||
|
|
||||||
async function fetchConversations() {
|
async function fetchConversations() {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
@@ -275,12 +281,26 @@ onUnmounted(() => {
|
|||||||
function goToConversation(id) {
|
function goToConversation(id) {
|
||||||
navigateTo(`/message-box/${id}`)
|
navigateTo(`/message-box/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function minimize() {
|
||||||
|
floatRoute.value = route.fullPath
|
||||||
|
navigateTo('/')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.messages-container {
|
.messages-container {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.float-control {
|
||||||
|
text-align: right;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-control i {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid var(--normal-border-color);
|
border-bottom: 1px solid var(--normal-border-color);
|
||||||
|
|||||||
Reference in New Issue
Block a user