feat: 前端多语言支持(中/英)与品牌 Logo 接入

多语言:
- 新增 vue-i18n,创建 src/locales/zh.ts、en.ts 语言包,按功能模块组织 key
- 新增 src/i18n.ts,语言检测优先级:localStorage > navigator.language(zh→中文,其他→英文)> 兜底英文
- 导航栏新增语言切换按钮(中/EN 一键切换),选择持久化到 localStorage
- 全部 View/Component 硬编码中文替换为 t() 调用,stores/auth.ts 错误消息同样国际化
- index.html lang 属性运行时动态更新

Logo:
- 替换 Vue 默认 logo 为 LmVPN 品牌 Logo(盾牌+网络节点)
- Header 导航栏:Logo 图标 + "LmVPN" 文字
- 首页标题区:大尺寸 Logo 展示
- Favicon 替换为 SVG 格式
This commit is contained in:
2026-07-07 15:05:53 +08:00
parent 5bce78cdb4
commit 25c3ad685a
20 changed files with 721 additions and 185 deletions
+16 -14
View File
@@ -1,17 +1,19 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const authStore = useAuthStore()
const { t } = useI18n()
const stats = ref([
{ label: '运行时长', value: '--', unit: '', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', route: '' },
{ label: '活跃设备', value: '--', unit: '', icon: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z', route: '' },
{ label: '今日流量', value: '--', unit: 'GB', icon: 'M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z', route: '' },
{ label: '在线节点', value: '--', unit: '', icon: 'M5 12h14M12 5l7 7-7 7', route: '' },
{ label: '用户总数', value: '--', unit: '', icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z', route: '/admin/users' },
{ label: 'VPN 管理', value: '配置', unit: '', icon: 'M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 4.05A12.884 12.884 0 0015 11a4 4 0 10-8 0c0 1.017.07 2.019.203 3M3 3l18 18', route: '/admin/vpn' },
{ label: 'admin.uptime', value: '--', unit: '', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', route: '' },
{ label: 'admin.activeDevices', value: '--', unit: '', icon: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z', route: '' },
{ label: 'admin.todayTraffic', value: '--', unit: 'GB', icon: 'M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z', route: '' },
{ label: 'admin.onlineNodes', value: '--', unit: '', icon: 'M5 12h14M12 5l7 7-7 7', route: '' },
{ label: 'admin.totalUsers', value: '--', unit: '', icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z', route: '/admin/users' },
{ label: 'admin.vpnManage', value: 'admin.config', unit: '', icon: 'M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 4.05A12.884 12.884 0 0015 11a4 4 0 10-8 0c0 1.017.07 2.019.203 3M3 3l18 18', route: '/admin/vpn' },
])
const userCount = ref<number | null>(null)
@@ -24,7 +26,7 @@ async function fetchUserCount() {
if (res.ok) {
const data = await res.json()
userCount.value = data.count
const stat = stats.value.find(s => s.label === '用户总数')
const stat = stats.value.find(s => s.label === 'admin.totalUsers')
if (stat) stat.value = String(data.count)
}
} catch {}
@@ -47,7 +49,7 @@ function handleLogout() {
<template>
<div class="max-w-4xl mx-auto px-4 py-12">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">管理后台</h2>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">{{ t('admin.title') }}</h2>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-5 mb-8">
<div
@@ -64,9 +66,9 @@ function handleLogout() {
</svg>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ stat.label }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t(stat.label) }}</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">
{{ stat.value }}<span class="text-sm font-normal text-gray-500">{{ stat.unit }}</span>
{{ stat.label === 'admin.vpnManage' ? t(stat.value) : stat.value }}<span class="text-sm font-normal text-gray-500">{{ stat.unit }}</span>
</p>
</div>
</div>
@@ -74,10 +76,10 @@ function handleLogout() {
</div>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">用户信息</h3>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ t('admin.userInfo') }}</h3>
<div class="space-y-2 text-sm text-gray-700 dark:text-gray-300">
<p><span class="font-medium text-gray-900 dark:text-white">用户名</span>{{ authStore.user?.username }}</p>
<p><span class="font-medium text-gray-900 dark:text-white">角色</span>{{ authStore.user?.role === 'admin' ? '管理员' : '普通用户' }}</p>
<p><span class="font-medium text-gray-900 dark:text-white">{{ t('admin.usernameLabel') }}</span>{{ authStore.user?.username }}</p>
<p><span class="font-medium text-gray-900 dark:text-white">{{ t('admin.roleLabel') }}</span>{{ authStore.user?.role === 'admin' ? t('common.admin') : t('common.normalUser') }}</p>
</div>
</div>
@@ -85,7 +87,7 @@ function handleLogout() {
class="px-6 py-2.5 rounded-lg font-medium text-white bg-red-500 hover:bg-red-600 transition-colors"
@click="handleLogout"
>
退出登录
{{ t('admin.logoutButton') }}
</button>
</div>
</template>