feat: 实现AppHeader和LoginView完整国际化支持

- 完成AppHeader.vue全面国际化
  * 版本检查和更新通知系统多语言支持
  * 用户菜单和账户信息修改模态框国际化
  * 退出登录确认流程多语言化
  * 总计替换30+个硬编码中文字符串

- 实现LoginView.vue完整国际化
  * 登录表单所有文本支持多语言
  * 添加语言切换组件到登录页面
  * 确保用户可在登录前选择语言

- 扩展三语言翻译文件
  * zh-cn.js: 简体中文标准翻译
  * zh-tw.js: 繁体中文专业化翻译
  * en.js: 英文技术术语标准翻译
  * 新增header和login完整翻译组

- 提升用户体验
  * 登录页面右上角工具栏(语言+主题切换)
  * 响应式布局适配多设备
  * 完整的首次访问多语言体验
This commit is contained in:
Wangnov
2025-09-08 19:41:44 +08:00
parent 4aae4aaec0
commit e88e97b485
5 changed files with 218 additions and 45 deletions

View File

@@ -11,7 +11,7 @@
<LogoTitle
:loading="oemLoading"
:logo-src="oemSettings.siteIconData || oemSettings.siteIcon"
subtitle="管理后台"
:subtitle="t('header.adminPanel')"
:title="oemSettings.siteName"
title-class="text-white dark:text-gray-100"
>
@@ -27,10 +27,10 @@
class="inline-flex animate-pulse items-center gap-1 rounded-full border border-green-600 bg-green-500 px-2 py-0.5 text-xs text-white transition-colors hover:bg-green-600"
:href="versionInfo.releaseInfo?.htmlUrl || '#'"
target="_blank"
title="有新版本可用"
:title="t('header.newVersionAvailable')"
>
<i class="fas fa-arrow-up text-[10px]" />
<span>新版本</span>
<span>{{ t('header.newVersion') }}</span>
</a>
</div>
</template>
@@ -77,7 +77,7 @@
<!-- 版本信息 -->
<div class="border-b border-gray-100 px-4 py-3 dark:border-gray-700">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-500 dark:text-gray-400">当前版本</span>
<span class="text-gray-500 dark:text-gray-400">{{ t('header.currentVersion') }}</span>
<span class="font-mono text-gray-700 dark:text-gray-300"
>v{{ versionInfo.current || '...' }}</span
>
@@ -85,7 +85,7 @@
<div v-if="versionInfo.hasUpdate" class="mt-2">
<div class="mb-2 flex items-center justify-between text-sm">
<span class="font-medium text-green-600 dark:text-green-400">
<i class="fas fa-arrow-up mr-1" />有新版本
<i class="fas fa-arrow-up mr-1" />{{ t('header.hasUpdate') }}
</span>
<span class="font-mono text-green-600 dark:text-green-400"
>v{{ versionInfo.latest }}</span
@@ -96,14 +96,14 @@
:href="versionInfo.releaseInfo?.htmlUrl || '#'"
target="_blank"
>
<i class="fas fa-external-link-alt mr-1" />查看更新
<i class="fas fa-external-link-alt mr-1" />{{ t('header.viewUpdate') }}
</a>
</div>
<div
v-else-if="versionInfo.checkingUpdate"
class="mt-2 text-center text-xs text-gray-500 dark:text-gray-400"
>
<i class="fas fa-spinner fa-spin mr-1" />检查更新中...
<i class="fas fa-spinner fa-spin mr-1" />{{ t('header.checkingUpdate') }}
</div>
<div v-else class="mt-2 text-center">
<!-- 已是最新版提醒 -->
@@ -114,7 +114,7 @@
class="inline-block rounded-lg border border-green-200 bg-green-100 px-3 py-1.5 dark:border-green-800 dark:bg-green-900/30"
>
<p class="text-xs font-medium text-green-700 dark:text-green-400">
<i class="fas fa-check-circle mr-1" />当前已是最新版本
<i class="fas fa-check-circle mr-1" />{{ t('header.alreadyLatest') }}
</p>
</div>
<button
@@ -123,7 +123,7 @@
class="text-xs text-blue-500 transition-colors hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
@click="checkForUpdates()"
>
<i class="fas fa-sync-alt mr-1" />检查更新
<i class="fas fa-sync-alt mr-1" />{{ t('header.checkUpdate') }}
</button>
</transition>
</div>
@@ -134,7 +134,7 @@
@click="openChangePasswordModal"
>
<i class="fas fa-key text-blue-500" />
<span>修改账户信息</span>
<span>{{ t('header.changeAccountInfo') }}</span>
</button>
<hr class="my-2 border-gray-200 dark:border-gray-700" />
@@ -144,7 +144,7 @@
@click="logout"
>
<i class="fas fa-sign-out-alt text-red-500" />
<span>退出登录</span>
<span>{{ t('header.logout') }}</span>
</button>
</div>
</div>
@@ -165,7 +165,7 @@
>
<i class="fas fa-key text-white" />
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">修改账户信息</h3>
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">{{ t('header.changePasswordModal.title') }}</h3>
</div>
<button
class="text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
@@ -181,7 +181,7 @@
>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>当前用户名</label
>{{ t('header.changePasswordModal.currentUsername') }}</label
>
<input
class="form-input w-full cursor-not-allowed bg-gray-100 dark:bg-gray-700 dark:text-gray-300"
@@ -190,31 +190,31 @@
:value="currentUser.username || 'Admin'"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
当前用户名输入新用户名以修改
{{ t('header.changePasswordModal.currentUsernameHint') }}
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>新用户名</label
>{{ t('header.changePasswordModal.newUsername') }}</label
>
<input
v-model="changePasswordForm.newUsername"
class="form-input w-full"
placeholder="输入新用户名(留空保持不变)"
:placeholder="t('header.changePasswordModal.newUsernamePlaceholder')"
type="text"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">留空表示不修改用户名</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{{ t('header.changePasswordModal.newUsernameHint') }}</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>当前密码</label
>{{ t('header.changePasswordModal.currentPassword') }}</label
>
<input
v-model="changePasswordForm.currentPassword"
class="form-input w-full"
placeholder="请输入当前密码"
:placeholder="t('header.changePasswordModal.currentPasswordPlaceholder')"
required
type="password"
/>
@@ -222,26 +222,26 @@
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>新密码</label
>{{ t('header.changePasswordModal.newPassword') }}</label
>
<input
v-model="changePasswordForm.newPassword"
class="form-input w-full"
placeholder="请输入新密码"
:placeholder="t('header.changePasswordModal.newPasswordPlaceholder')"
required
type="password"
/>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">密码长度至少8位</p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{{ t('header.changePasswordModal.newPasswordHint') }}</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>确认新密码</label
>{{ t('header.changePasswordModal.confirmPassword') }}</label
>
<input
v-model="changePasswordForm.confirmPassword"
class="form-input w-full"
placeholder="请再次输入新密码"
:placeholder="t('header.changePasswordModal.confirmPasswordPlaceholder')"
required
type="password"
/>
@@ -253,7 +253,7 @@
type="button"
@click="closeChangePasswordModal"
>
取消
{{ t('common.cancel') }}
</button>
<button
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
@@ -262,7 +262,7 @@
>
<div v-if="changePasswordLoading" class="loading-spinner mr-2" />
<i v-else class="fas fa-save mr-2" />
{{ changePasswordLoading ? '保存中...' : '保存修改' }}
{{ changePasswordLoading ? t('header.changePasswordModal.saving') : t('header.changePasswordModal.save') }}
</button>
</div>
</form>
@@ -273,6 +273,7 @@
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { showToast } from '@/utils/toast'
import { apiClient } from '@/config/api'
@@ -282,6 +283,7 @@ import LanguageSwitch from '@/components/common/LanguageSwitch.vue'
const router = useRouter()
const authStore = useAuthStore()
const { t } = useI18n()
// 当前用户信息
const currentUser = computed(() => authStore.user || { username: 'Admin' })
@@ -391,12 +393,12 @@ const closeChangePasswordModal = () => {
// 修改密码
const changePassword = async () => {
if (changePasswordForm.newPassword !== changePasswordForm.confirmPassword) {
showToast('两次输入的密码不一致', 'error')
showToast(t('header.changePasswordModal.passwordMismatch'), 'error')
return
}
if (changePasswordForm.newPassword.length < 8) {
showToast('新密码长度至少8位', 'error')
showToast(t('header.changePasswordModal.passwordTooShort'), 'error')
return
}
@@ -411,8 +413,8 @@ const changePassword = async () => {
if (data.success) {
const message = changePasswordForm.newUsername
? '账户信息修改成功,请重新登录'
: '密码修改成功,请重新登录'
? t('header.changePasswordModal.accountInfoChangeSuccess')
: t('header.changePasswordModal.passwordChangeSuccess')
showToast(message, 'success')
closeChangePasswordModal()
@@ -422,10 +424,10 @@ const changePassword = async () => {
router.push('/login')
}, 1500)
} else {
showToast(data.message || '修改失败', 'error')
showToast(data.message || t('header.changePasswordModal.changeFailed'), 'error')
}
} catch (error) {
showToast('修改密码失败', 'error')
showToast(t('header.changePasswordModal.changePasswordFailed'), 'error')
} finally {
changePasswordLoading.value = false
}
@@ -433,10 +435,10 @@ const changePassword = async () => {
// 退出登录
const logout = () => {
if (confirm('确定要退出登录吗?')) {
if (confirm(t('header.logoutConfirm'))) {
authStore.logout()
router.push('/login')
showToast('已安全退出', 'success')
showToast(t('header.logoutSuccess'), 'success')
}
userMenuOpen.value = false
}