Files
system-file/web-vue/src/components/TopNavbar.vue

279 lines
8.2 KiB
Vue

<template>
<div class="top-navbar">
<div class="navbar-left">
<div class="logo-icon">
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 主色调渐变 -->
<linearGradient id="hex-face1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00bcd4"/>
<stop offset="100%" stop-color="#0097a7"/>
</linearGradient>
<linearGradient id="hex-face2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#26c6da"/>
<stop offset="100%" stop-color="#00acc1"/>
</linearGradient>
<linearGradient id="hex-face3" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#4dd0e1"/>
<stop offset="100%" stop-color="#26c6da"/>
</linearGradient>
<!-- 高光效果 -->
<linearGradient id="hex-shine" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="rgba(255,255,255,0.6)"/>
<stop offset="100%" stop-color="rgba(255,255,255,0)"/>
</linearGradient>
<!-- 阴影滤镜 -->
<filter id="dropShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="rgba(0,0,0,0.3)"/>
</filter>
</defs>
<!-- 3D 六边形主体 -->
<g filter="url(#dropShadow)">
<!-- 左侧面 -->
<polygon points="24,4 8,12 8,32 24,40" fill="url(#hex-face3)"/>
<!-- 右侧面 -->
<polygon points="24,4 40,12 40,32 24,40" fill="url(#hex-face1)"/>
<!-- 顶面 -->
<polygon points="8,12 24,4 40,12 24,20" fill="url(#hex-face2)"/>
</g>
<!-- 高光边缘 -->
<polygon points="8,12 24,4 40,12" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
<polygon points="8,12 8,32 24,40" fill="none" stroke="rgba(0,0,0,0.1)" stroke-width="0.5"/>
<!-- 文件图标 -->
<g transform="translate(14, 16)">
<!-- 文件夹 -->
<path d="M2 4h6l2 2h8v10H2V4z" fill="rgba(255,255,255,0.95)"/>
<path d="M2 6h18v10H2V6z" fill="rgba(255,255,255,0.85)"/>
<!-- 文件角标 -->
<rect x="12" y="2" width="8" height="10" rx="1" fill="#fff"/>
<rect x="13" y="3" width="6" height="1" fill="#00bcd4"/>
<rect x="13" y="5" width="4" height="1" fill="#b2ebf2"/>
<rect x="13" y="7" width="5" height="1" fill="#b2ebf2"/>
</g>
<!-- 聊天气泡 -->
<g transform="translate(26, 22)">
<ellipse cx="8" cy="8" rx="8" ry="6" fill="#fff"/>
<path d="M6 14l2-2 2 2-2 2z" fill="#fff"/>
<!-- 消息点 -->
<circle cx="5" cy="8" r="1.2" fill="#00bcd4"/>
<circle cx="8" cy="8" r="1.2" fill="#00bcd4"/>
<circle cx="11" cy="8" r="1.2" fill="#00bcd4"/>
</g>
</svg>
</div>
<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 command="changePassword">
<el-icon><Lock /></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" />
<PasswordDialog v-model="passwordVisible" />
</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, Lock } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/user'
import ChatDialog from './ChatDialog.vue'
import ProfileDialog from './ProfileDialog.vue'
import PasswordDialog from './PasswordDialog.vue'
import { chatService } from '@/services/chat'
import { getUnreadCount } from '@/api/message'
import request from '@/api/request'
const router = useRouter()
const userStore = useUserStore()
const chatVisible = ref(false)
const profileVisible = ref(false)
const passwordVisible = 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 === 'changePassword') {
passwordVisible.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;
}
.logo-icon {
width: 28px;
height: 28px;
color: #fff;
}
.logo-icon svg {
width: 100%;
height: 100%;
}
.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>