2026-04-02 12:06:51 +08:00
|
|
|
|
<template>
|
2026-04-01 22:39:11 +08:00
|
|
|
|
<div class="top-navbar">
|
|
|
|
|
|
<div class="navbar-left">
|
2026-04-02 12:06:51 +08:00
|
|
|
|
<div class="logo-icon">
|
|
|
|
|
|
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
|
<defs>
|
|
|
|
|
|
<linearGradient id="hex-top" x1="24" y1="2" x2="24" y2="46" gradientUnits="userSpaceOnUse">
|
|
|
|
|
|
<stop offset="0%" stop-color="#4db6ac"/>
|
|
|
|
|
|
<stop offset="50%" stop-color="#009688"/>
|
|
|
|
|
|
<stop offset="100%" stop-color="#004d40"/>
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
<linearGradient id="hex-front" x1="10" y1="22" x2="38" y2="46" gradientUnits="userSpaceOnUse">
|
|
|
|
|
|
<stop offset="0%" stop-color="#26a69a"/>
|
|
|
|
|
|
<stop offset="100%" stop-color="#00796b"/>
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
<linearGradient id="hex-side" x1="4" y1="14" x2="24" y2="44" gradientUnits="userSpaceOnUse">
|
|
|
|
|
|
<stop offset="0%" stop-color="#00796b"/>
|
|
|
|
|
|
<stop offset="100%" stop-color="#004d40"/>
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
<linearGradient id="hex-highlight" x1="24" y1="4" x2="24" y2="26" gradientUnits="userSpaceOnUse">
|
|
|
|
|
|
<stop offset="0%" stop-color="rgba(255,255,255,0.5)"/>
|
|
|
|
|
|
<stop offset="100%" stop-color="rgba(255,255,255,0)"/>
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
<filter id="glow">
|
|
|
|
|
|
<feGaussianBlur stdDeviation="0.5" result="blur"/>
|
|
|
|
|
|
<feMerge>
|
|
|
|
|
|
<feMergeNode in="blur"/>
|
|
|
|
|
|
<feMergeNode in="SourceGraphic"/>
|
|
|
|
|
|
</feMerge>
|
|
|
|
|
|
</filter>
|
|
|
|
|
|
</defs>
|
|
|
|
|
|
<!-- 3D Hexagon: side (left) -->
|
|
|
|
|
|
<polygon points="4,14 10,10 10,34 4,38" fill="url(#hex-side)" opacity="0.85"/>
|
|
|
|
|
|
<!-- 3D Hexagon: front face -->
|
|
|
|
|
|
<polygon points="10,10 38,10 44,14 44,38 38,42 10,42 4,38 4,14" fill="url(#hex-front)"/>
|
|
|
|
|
|
<!-- 3D Hexagon: top face -->
|
|
|
|
|
|
<polygon points="10,10 24,3 38,10 10,10" fill="url(#hex-top)" opacity="0.9"/>
|
|
|
|
|
|
<!-- Highlight on top -->
|
|
|
|
|
|
<polygon points="12,10 24,5 36,10" fill="url(#hex-highlight)" opacity="0.6"/>
|
|
|
|
|
|
<!-- Inner subtle line for depth -->
|
|
|
|
|
|
<polygon points="10,10 38,10 44,14 4,14" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="0.5"/>
|
|
|
|
|
|
<!-- Center icon: stylized folder/cloud -->
|
|
|
|
|
|
<path d="M19 22h-1a3 3 0 00-3 3v6a3 3 0 003 3h12a3 3 0 003-3v-4a3 3 0 00-3-3h-2l-1.5-2H19z" fill="rgba(255,255,255,0.9)" filter="url(#glow)"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
2026-04-01 22:39:11 +08:00
|
|
|
|
<span class="system-title">云文件管理系统</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="navbar-right">
|
|
|
|
|
|
<!-- 消息图标 -->
|
|
|
|
|
|
<el-badge :value="totalUnread" :hidden="totalUnread === 0" class="msg-badge" :class="{ 'has-unread': totalUnread > 0 }">
|
|
|
|
|
|
<el-button link class="navbar-icon-btn" @click="openChat">
|
|
|
|
|
|
<el-icon :size="20" color="#ffffff"><ChatDotRound /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-badge>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 用户头像下拉 -->
|
|
|
|
|
|
<el-dropdown trigger="click" @command="handleUserCommand">
|
|
|
|
|
|
<div class="user-info">
|
|
|
|
|
|
<el-avatar :size="30" :src="userStore.avatar || undefined" style="background:#409eff;font-size:13px">
|
|
|
|
|
|
{{ (userStore.nickname || userStore.username)?.[0]?.toUpperCase() || 'U' }}
|
|
|
|
|
|
</el-avatar>
|
|
|
|
|
|
<span class="username">{{ userStore.nickname || userStore.username }}</span>
|
|
|
|
|
|
<el-icon :size="12" color="#ffffff"><ArrowDown /></el-icon>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<template #dropdown>
|
|
|
|
|
|
<el-dropdown-menu>
|
|
|
|
|
|
<el-dropdown-item command="profile">
|
|
|
|
|
|
<el-icon><User /></el-icon> 个人信息
|
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item divided command="logout">
|
|
|
|
|
|
<el-icon><SwitchButton /></el-icon> 退出系统
|
|
|
|
|
|
</el-dropdown-item>
|
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 聊天弹窗 -->
|
|
|
|
|
|
<ChatDialog v-model="chatVisible" @unread-change="updateUnread" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 个人信息弹窗 -->
|
|
|
|
|
|
<ProfileDialog v-model="profileVisible" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
|
|
|
|
|
import { Folder, ChatDotRound, ArrowDown, User, SwitchButton } from '@element-plus/icons-vue'
|
|
|
|
|
|
import { useUserStore } from '@/store/user'
|
|
|
|
|
|
import ChatDialog from './ChatDialog.vue'
|
|
|
|
|
|
import ProfileDialog from './ProfileDialog.vue'
|
|
|
|
|
|
import { chatService } from '@/services/chat'
|
|
|
|
|
|
import { getUnreadCount } from '@/api/message'
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
|
|
|
|
|
|
const chatVisible = ref(false)
|
|
|
|
|
|
const profileVisible = ref(false)
|
|
|
|
|
|
const totalUnread = ref(0)
|
|
|
|
|
|
|
|
|
|
|
|
const updateUnread = () => {
|
|
|
|
|
|
checkUnread()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openChat = () => {
|
|
|
|
|
|
chatVisible.value = true
|
|
|
|
|
|
// 打开聊天时清零未读数
|
|
|
|
|
|
totalUnread.value = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 定期检查未读消息
|
|
|
|
|
|
let pollTimer = null
|
|
|
|
|
|
let wsUnsubscribe = null
|
|
|
|
|
|
|
|
|
|
|
|
const checkUnread = () => {
|
|
|
|
|
|
getUnreadCount().then(res => {
|
|
|
|
|
|
totalUnread.value = res.data.count || 0
|
|
|
|
|
|
}).catch(() => {})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
checkUnread()
|
|
|
|
|
|
pollTimer = setInterval(checkUnread, 30000)
|
|
|
|
|
|
|
|
|
|
|
|
// 监听 WebSocket 消息,增加未读计数
|
|
|
|
|
|
wsUnsubscribe = chatService.onMessage((data) => {
|
|
|
|
|
|
if (data.type === 'chat') {
|
|
|
|
|
|
const isSelf = Number(data.message.fromUserId) === Number(userStore.userId)
|
|
|
|
|
|
// 只在对方发来消息且聊天窗口未打开时增加未读
|
|
|
|
|
|
if (!isSelf && !chatVisible.value) {
|
|
|
|
|
|
totalUnread.value++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (pollTimer) clearInterval(pollTimer)
|
|
|
|
|
|
if (wsUnsubscribe) wsUnsubscribe()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleUserCommand = (command) => {
|
|
|
|
|
|
if (command === 'profile') {
|
|
|
|
|
|
profileVisible.value = true
|
|
|
|
|
|
} else if (command === 'logout') {
|
|
|
|
|
|
ElMessageBox.confirm('确定退出登录吗?', '提示', { type: 'warning' }).then(() => {
|
|
|
|
|
|
userStore.logout()
|
|
|
|
|
|
router.push('/login')
|
|
|
|
|
|
}).catch(() => {})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.top-navbar {
|
|
|
|
|
|
height: 50px;
|
|
|
|
|
|
background: #009688;
|
|
|
|
|
|
border-bottom: 1px solid #00796b;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.navbar-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:06:51 +08:00
|
|
|
|
.logo-icon {
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-icon svg {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 22:39:11 +08:00
|
|
|
|
.system-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.navbar-right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.msg-badge :deep(.el-badge__content) {
|
|
|
|
|
|
transform: scale(0.8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.msg-badge.has-unread :deep(.el-badge__content) {
|
|
|
|
|
|
animation: flash 1s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes flash {
|
|
|
|
|
|
0%, 100% { opacity: 1; }
|
|
|
|
|
|
50% { opacity: 0.5; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.navbar-icon-btn {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.navbar-icon-btn:hover {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-info:hover {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.username {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
max-width: 80px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|