Files
Message-Push-Nest/web/src/components/Index.vue
T

358 lines
12 KiB
Vue

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
// 使用 defineAsyncComponent 导入没有默认导出的组件
// const Dashboard = defineAsyncComponent(() => import("@/components/pages/dashboard/Dashboard.vue"))
// const SendLogs = defineAsyncComponent(() => import("@/components/pages/sendLogs/SendLogs.vue"))
// 导入 constant 模块,使用命名导入匹配 constant.js 的导出方式
import { CONSTANT } from '../constant.js'
import { LocalStieConfigUtils } from '@/util/localSiteConfig'
import { usePageState } from '@/store/page_sate.js'
import { useRoute, useRouter } from 'vue-router'
// 定义标签接口
interface TabRoute {
name: string;
path: string;
}
const route = useRoute()
const router = useRouter()
const pageState = usePageState()
const isAuthenticated = ref(Boolean(localStorage.getItem(CONSTANT.STORE_TOKEN_NAME)));
const isMobileMenuOpen = ref(false)
const isUserMenuOpen = ref(false)
const userAccount = ref('管理员')
const siteConfig = ref<any>({})
// 从JWT中解析用户名
const parseJwtUsername = (token: string): string => {
try {
const payload = JSON.parse(atob(token.split('.')[1]))
return payload.username || payload.user || payload.name || '管理员'
} catch (error) {
console.error('解析JWT失败:', error)
return '管理员'
}
}
// 更新用户账号信息
const updateUserAccount = () => {
const token = localStorage.getItem(CONSTANT.STORE_TOKEN_NAME)
if (token) {
userAccount.value = parseJwtUsername(token)
} else {
userAccount.value = '管理员'
}
}
// 更新favicon
const updateFavicon = (logoSvg: string) => {
if (logoSvg) {
// 查找现有的favicon链接
let link = document.querySelector("link[rel*='icon']") as HTMLLinkElement
if (!link) {
// 如果不存在,创建新的favicon链接
link = document.createElement('link')
link.rel = 'icon'
document.head.appendChild(link)
}
// 将SVG转换为data URL
const svgBlob = new Blob([logoSvg], { type: 'image/svg+xml' })
const svgUrl = URL.createObjectURL(svgBlob)
link.href = svgUrl
link.type = 'image/svg+xml'
}
}
// 获取本地配置
const getLocalConfig = () => {
try {
const localConfig = LocalStieConfigUtils.getLocalConfig()
if (localConfig) {
siteConfig.value = localConfig
// 更新页面状态中的配置数据
if (pageState.setSiteConfigData) {
pageState.setSiteConfigData(localConfig)
}
// 更新网站标题
if (localConfig.title) {
document.title = localConfig.title
}
// 更新favicon
if (localConfig.logo) {
updateFavicon(localConfig.logo)
}
}
} catch (error) {
console.error('获取本地配置失败:', error)
}
}
// 获取最新配置并更新
const getLatestConfig = async () => {
try {
const latestConfig = await LocalStieConfigUtils.getLatestLocalConfig()
if (latestConfig) {
siteConfig.value = latestConfig
// 更新页面状态中的配置数据
if (pageState.setSiteConfigData) {
pageState.setSiteConfigData(latestConfig)
}
// 更新网站标题
if (latestConfig.title) {
document.title = latestConfig.title
}
// 更新favicon
if (latestConfig.logo) {
updateFavicon(latestConfig.logo)
}
}
} catch (error) {
console.error('获取最新配置失败:', error)
// 如果获取最新配置失败,尝试使用本地配置
getLocalConfig()
}
}
// 切换移动端菜单
const toggleMobileMenu = () => {
isMobileMenuOpen.value = !isMobileMenuOpen.value
}
// 切换用户菜单
const toggleUserMenu = () => {
isUserMenuOpen.value = !isUserMenuOpen.value
}
// 退出登录
const logout = () => {
localStorage.removeItem(CONSTANT.STORE_TOKEN_NAME)
isAuthenticated.value = false
isUserMenuOpen.value = false
router.push('/login')
}
// 监听localStorage变化
onMounted(() => {
// 初始化用户账号信息
updateUserAccount();
// 初始化配置信息
getLocalConfig();
// 如果已认证,获取最新配置
if (isAuthenticated.value) {
getLatestConfig();
}
// 定期检查token状态
const checkAuth = () => {
const wasAuthenticated = isAuthenticated.value;
isAuthenticated.value = Boolean(localStorage.getItem(CONSTANT.STORE_TOKEN_NAME));
// 如果认证状态发生变化,更新用户账号信息和配置
if (wasAuthenticated !== isAuthenticated.value) {
updateUserAccount();
if (isAuthenticated.value) {
// 用户刚登录,获取最新配置
getLatestConfig();
} else {
// 用户退出登录,使用本地配置
getLocalConfig();
}
}
};
// 监听storage事件
window.addEventListener('storage', checkAuth);
// 定期检查(处理同一页面内的变化)
const interval = setInterval(checkAuth, 1000);
// 点击外部关闭用户菜单
const handleClickOutside = (event: Event) => {
const target = event.target as Element;
if (!target.closest('.user-menu-container')) {
isUserMenuOpen.value = false;
}
};
document.addEventListener('click', handleClickOutside);
// 清理函数
return () => {
window.removeEventListener('storage', checkAuth);
clearInterval(interval);
document.removeEventListener('click', handleClickOutside);
};
});
// 定义标签及其对应的路由路径
const tabRoutes: TabRoute[] = [
{ name: '数据统计', path: '/' },
{ name: '发信日志', path: '/sendlogs' },
{ name: '托管消息', path: '/hostedmessage' },
{ name: '定时消息', path: '/cronmessages' },
{ name: '发信任务', path: '/sendtasks' },
{ name: '发信渠道', path: '/sendways' },
{ name: '设置', path: '/settings' }
];
const activeTab = ref('Dashboard');
// 处理标签点击事件,跳转到对应路由
const handleTabClick = (tab: TabRoute) => {
activeTab.value = tab.name;
// 使用 Vue Router 进行导航,保持单页应用状态
// 对于根路径,直接导航;对于其他路径,使用相对路径
if (tab.path === '/') {
router.push(tab.path);
} else {
// 去掉开头的斜杠,使用相对路径导航
const relativePath = tab.path.startsWith('/') ? tab.path.substring(1) : tab.path;
router.push(relativePath);
}
};
// 根据当前路由设置活动标签
const updateActiveTab = () => {
// 获取当前路径(去掉开头的斜杠)
const currentPath = route.path.startsWith('/') ? route.path.substring(1) : route.path;
// 查找当前路由对应的标签
const currentTab = tabRoutes.find(tab => {
// 对于根路径,需要精确匹配
if (tab.path === '/') {
return route.path === '/' || route.path === '';
}
// 对于其他路径,检查当前路由是否匹配
// 去掉开头的斜杠进行比较
const tabPath = tab.path.startsWith('/') ? tab.path.substring(1) : tab.path;
return currentPath === tabPath || route.path === tab.path;
});
// 如果找到对应标签,更新活动标签
if (currentTab) {
activeTab.value = currentTab.name;
}
};
// 初始化时设置活动标签
updateActiveTab();
// 监听路由变化并更新活动标签
watch(() => route.path, updateActiveTab);
// 计算属性:站点标题
const siteTitle = computed(() => {
return siteConfig.value?.title || '消息管理系统'
})
</script>
<template>
<router-view v-if="!isAuthenticated || route.path == '/login' || route.path == 'login'"></router-view>
<div class="min-h-screen bg-gray-50" v-else>
<!-- 顶部导航栏 -->
<nav class="bg-white shadow-sm border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo/品牌名称 -->
<div class="flex items-center">
<h1 class="text-lg sm:text-xl font-bold text-gray-900">{{ siteTitle }}</h1>
</div>
<!-- 桌面端导航 -->
<div class="hidden md:flex space-x-6 lg:space-x-8">
<button v-for="tab in tabRoutes" :key="tab.name" @click="handleTabClick(tab)" :class="[
'relative py-2 px-3 text-sm font-medium transition-all duration-200 rounded-md whitespace-nowrap',
activeTab === tab.name
? 'text-blue-600 bg-blue-50'
: 'text-gray-600 hover:text-blue-600 hover:bg-gray-50'
]">
{{ tab.name }}
<!-- 活动状态指示器 -->
<span v-if="activeTab === tab.name"
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-1 h-1 bg-blue-600 rounded-full"></span>
</button>
</div>
<!-- 右侧区域 -->
<div class="flex items-center space-x-4">
<!-- 用户账号下拉菜单 -->
<div class="relative user-menu-container">
<button @click="toggleUserMenu" class="flex items-center space-x-2 p-2 rounded-md hover:bg-gray-50 transition-colors">
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
</svg>
</div>
<span class="hidden sm:block text-sm font-medium text-gray-700">{{ userAccount }}</span>
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- 下拉菜单 -->
<div v-if="isUserMenuOpen" class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-50">
<div class="py-1">
<div class="px-4 py-2 text-sm text-gray-500 border-b border-gray-100">
<div class="font-medium text-gray-900">{{ userAccount }}</div>
<!-- <div class="text-xs">当前登录账号</div> -->
</div>
<button @click="logout" class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors">
<div class="flex items-center space-x-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
<span>退出登录</span>
</div>
</button>
</div>
</div>
</div>
<!-- 移动端菜单按钮 -->
<button @click="toggleMobileMenu"
class="md:hidden p-2 rounded-md text-gray-600 hover:text-blue-600 hover:bg-gray-50 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-if="!isMobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16" />
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- 移动端菜单 -->
<div v-if="isMobileMenuOpen" class="md:hidden border-t border-gray-200 py-2">
<div class="flex flex-col space-y-1">
<button v-for="tab in tabRoutes" :key="tab.name" @click="handleTabClick(tab); isMobileMenuOpen = false"
:class="[
'text-left py-3 px-4 text-sm font-medium transition-all duration-200 rounded-md',
activeTab === tab.name
? 'text-blue-600 bg-blue-50'
: 'text-gray-600 hover:text-blue-600 hover:bg-gray-50'
]">
{{ tab.name }}
</button>
</div>
</div>
</div>
</nav>
<!-- 主要内容区域 -->
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<!-- <div class="bg-white rounded-lg shadow-sm border border-gray-200"> -->
<div class="">
<!-- 使用 router-view 显示嵌套路由的内容 -->
<router-view></router-view>
</div>
</main>
</div>
</template>