mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-22 08:49:47 +08:00
添加国际化支持:引入 vue-i18n,更新多个组件以支持语言切换和文本翻译
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { SystemNavMenus } from '@/router/menu'
|
||||
import { getNavMenus } from '@/router/i18n-menu'
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
// 从 Store 中获取superuser信息
|
||||
@@ -12,7 +12,7 @@ const appGroups = ref<Record<string, NavMenu[]>>({})
|
||||
// 根据header属性对应用进行分类
|
||||
function categorizeApps() {
|
||||
// 获取可见的菜单项
|
||||
const menus = SystemNavMenus.filter((item: NavMenu) => (!item.admin || superUser) && !item.footer)
|
||||
const menus = getNavMenus().filter((item: NavMenu) => (!item.admin || superUser) && !item.footer)
|
||||
|
||||
// 按header属性分组
|
||||
const groupedMenus: Record<string, NavMenu[]> = {}
|
||||
@@ -80,16 +80,16 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-settings-container {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
margin-block: 0;
|
||||
margin-inline: auto;
|
||||
max-inline-size: 960px;
|
||||
}
|
||||
|
||||
.settings-section-card {
|
||||
overflow: hidden;
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 8%);
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
@@ -97,11 +97,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.settings-list-item {
|
||||
padding: 8px 12px;
|
||||
padding-block: 8px;
|
||||
padding-inline: 12px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(var(--v-border-color), 0.12);
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), 0.12);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { DiscoverTabs } from '@/router/menu'
|
||||
import { getDiscoverTabs } from '@/router/i18n-menu'
|
||||
import draggable from 'vuedraggable'
|
||||
import TheMovieDbView from '@/views/discover/TheMovieDbView.vue'
|
||||
import DoubanView from '@/views/discover/DoubanView.vue'
|
||||
@@ -7,7 +7,6 @@ import BangumiView from '@/views/discover/BangumiView.vue'
|
||||
import ExtraSourceView from '@/views/discover/ExtraSourceView.vue'
|
||||
import { DiscoverSource } from '@/api/types'
|
||||
import api from '@/api'
|
||||
import { or } from '@vueuse/math'
|
||||
|
||||
const activeTab = ref('')
|
||||
|
||||
@@ -24,6 +23,7 @@ const discoverTabs = ref<DiscoverSource[]>([])
|
||||
const discoverTabItems = computed(() => {
|
||||
return discoverTabs.value.map(item => ({
|
||||
title: item.name,
|
||||
tab: item.mediaid_prefix,
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -35,7 +35,8 @@ const orderConfigDialog = ref(false)
|
||||
|
||||
// 初始化发现标签
|
||||
function initDiscoverTabs() {
|
||||
for (const tab of DiscoverTabs) {
|
||||
const tabs = getDiscoverTabs()
|
||||
for (const tab of tabs) {
|
||||
discoverTabs.value.push({
|
||||
name: tab.name,
|
||||
mediaid_prefix: tab.tab,
|
||||
|
||||
@@ -14,6 +14,7 @@ const downloaders = ref<DownloaderConf[]>([])
|
||||
const downloaderItems = computed(() => {
|
||||
return downloaders.value.map(item => ({
|
||||
title: item.name,
|
||||
tab: item.name,
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ import api from '@/api'
|
||||
import router from '@/router'
|
||||
import logo from '@images/logo.png'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
|
||||
import { saveLocalTheme } from '@/@core/utils/theme'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/locales/types'
|
||||
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
|
||||
|
||||
// 主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
@@ -18,6 +19,8 @@ const { global: globalTheme } = useTheme()
|
||||
const authStore = useAuthStore()
|
||||
//用户 Store
|
||||
const userStore = useUserStore()
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 表单
|
||||
const form = ref({
|
||||
@@ -41,6 +44,21 @@ const isOTP = ref(false)
|
||||
// 用户名称输入框
|
||||
const usernameInput = ref()
|
||||
|
||||
// 语言选择菜单
|
||||
const langMenu = ref(false)
|
||||
// 当前语言
|
||||
const currentLocale = ref(getCurrentLocale())
|
||||
|
||||
// 可用的语言列表
|
||||
const locales = Object.values(SUPPORTED_LOCALES)
|
||||
|
||||
// 切换语言
|
||||
async function switchLanguage(locale: SupportedLocale) {
|
||||
await setI18nLanguage(locale)
|
||||
currentLocale.value = locale
|
||||
langMenu.value = false
|
||||
}
|
||||
|
||||
// 查询是否开启双重验证
|
||||
const fetchOTP = debounce(async () => {
|
||||
const userid = usernameInput.value?.value
|
||||
@@ -58,24 +76,6 @@ const fetchOTP = debounce(async () => {
|
||||
})
|
||||
}, 500)
|
||||
|
||||
// 获取用户主题配置
|
||||
async function fetchThemeConfig() {
|
||||
const response = await api.get('/user/config/Layout')
|
||||
if (response && response.data && response.data.value) {
|
||||
return response.data.value?.theme
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 生效主题
|
||||
async function setTheme() {
|
||||
let themeValue = (await fetchThemeConfig()) || localStorage.getItem('theme') || 'light'
|
||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
// 存储主题到本地
|
||||
saveLocalTheme(themeValue, globalTheme)
|
||||
}
|
||||
|
||||
// 订阅推送通知
|
||||
async function subscribeForPushNotifications() {
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
@@ -103,8 +103,6 @@ async function subscribeForPushNotifications() {
|
||||
|
||||
// 登录后处理
|
||||
async function afterLogin(superuser: boolean) {
|
||||
// 生效主题配置
|
||||
await setTheme()
|
||||
// 跳转到首页或回原始页面
|
||||
router.push(authStore.originalPath ?? '/')
|
||||
// 订阅推送通知
|
||||
@@ -156,11 +154,11 @@ function login() {
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// 登录失败,显示错误提示
|
||||
if (!error.response) errorMessage.value = '登录失败,请检查网络连接!'
|
||||
else if (error.response.status === 401) errorMessage.value = '登录失败,请检查用户名、密码或双重验证是否正确!'
|
||||
else if (error.response.status === 403) errorMessage.value = '登录失败,您没有权限访问!'
|
||||
else if (error.response.status === 500) errorMessage.value = '登录失败,服务器错误!'
|
||||
else errorMessage.value = `登录失败 ${error.response.status},请检查用户名、密码或双重验证码是否正确!`
|
||||
if (!error.response) errorMessage.value = t('login.networkError')
|
||||
else if (error.response.status === 401) errorMessage.value = t('login.authFailure')
|
||||
else if (error.response.status === 403) errorMessage.value = t('login.permissionDenied')
|
||||
else if (error.response.status === 500) errorMessage.value = t('login.serverError')
|
||||
else errorMessage.value = `${t('login.loginFailed')} ${error.response.status},${t('login.checkCredentials')}`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,6 +193,35 @@ onMounted(async () => {
|
||||
</div>
|
||||
</template>
|
||||
<VCardTitle class="font-weight-bold text-2xl text-uppercase"> MoviePilot </VCardTitle>
|
||||
|
||||
<!-- 语言切换按钮 -->
|
||||
<template #append>
|
||||
<VMenu v-model="langMenu" :close-on-content-click="false">
|
||||
<template #activator="{ props }">
|
||||
<VBtn variant="text" size="small" v-bind="props" class="lang-switch-btn">
|
||||
<span v-if="SUPPORTED_LOCALES[currentLocale].flag">{{ SUPPORTED_LOCALES[currentLocale].flag }}</span>
|
||||
<VIcon v-else icon="mdi-translate" />
|
||||
<span class="ms-1">{{ SUPPORTED_LOCALES[currentLocale].title }}</span>
|
||||
</VBtn>
|
||||
</template>
|
||||
<VCard min-width="180">
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="locale in locales"
|
||||
:key="locale.name"
|
||||
:value="locale.name"
|
||||
@click="switchLanguage(locale.name as SupportedLocale)"
|
||||
>
|
||||
<template #prepend>
|
||||
<span v-if="locale.flag" class="mr-2">{{ locale.flag }}</span>
|
||||
<VIcon v-else icon="mdi-translate" size="small" />
|
||||
</template>
|
||||
<VListItemTitle>{{ locale.title }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm ref="refForm" autocomplete="on" @submit.prevent="() => {}">
|
||||
@@ -204,7 +231,7 @@ onMounted(async () => {
|
||||
<VTextField
|
||||
ref="usernameInput"
|
||||
v-model="form.username"
|
||||
label="用户名"
|
||||
:label="t('login.username')"
|
||||
type="text"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
@@ -216,7 +243,7 @@ onMounted(async () => {
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="form.password"
|
||||
label="密码"
|
||||
:label="t('login.password')"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
name="current-password"
|
||||
autocomplete="current-password"
|
||||
@@ -226,15 +253,15 @@ onMounted(async () => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField v-if="isOTP" v-model="form.otp_password" label="双重验证码" type="input" />
|
||||
<VTextField v-if="isOTP" v-model="form.otp_password" :label="t('login.otpCode')" type="input" />
|
||||
<!-- remember me checkbox -->
|
||||
<div class="d-flex align-center justify-space-between flex-wrap">
|
||||
<VCheckbox v-model="form.remember" label="保持登录" required />
|
||||
<VCheckbox v-model="form.remember" :label="t('login.stayLoggedIn')" required />
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<!-- login button -->
|
||||
<VBtn block type="submit" @click="login" prepend-icon="mdi-login"> 登录 </VBtn>
|
||||
<VBtn block type="submit" @click="login" prepend-icon="mdi-login"> {{ t('login.login') }} </VBtn>
|
||||
<VAlert v-if="errorMessage" type="error" variant="tonal" class="mt-3">
|
||||
{{ errorMessage }}
|
||||
</VAlert>
|
||||
@@ -258,4 +285,10 @@ onMounted(async () => {
|
||||
overflow: hidden;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.lang-switch-btn {
|
||||
position: absolute;
|
||||
inset-block-start: 8px;
|
||||
inset-inline-end: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -160,27 +160,27 @@ const categoryItems: Record<string, string>[] = [
|
||||
{
|
||||
title: '全部',
|
||||
icon: 'mdi-filmstrip-box-multiple',
|
||||
key: 'all',
|
||||
tab: '全部',
|
||||
},
|
||||
{
|
||||
title: '电影',
|
||||
icon: 'mdi-movie',
|
||||
key: 'movie',
|
||||
tab: '电影',
|
||||
},
|
||||
{
|
||||
title: '电视剧',
|
||||
icon: 'mdi-television-classic',
|
||||
key: 'tv',
|
||||
tab: '电视剧',
|
||||
},
|
||||
{
|
||||
title: '动漫',
|
||||
icon: 'mdi-animation',
|
||||
key: 'anime',
|
||||
tab: '动漫',
|
||||
},
|
||||
{
|
||||
title: '榜单',
|
||||
icon: 'mdi-trophy',
|
||||
key: 'rank',
|
||||
tab: '榜单',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -11,23 +11,20 @@ import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
import AccountSettingScheduler from '@/views/setting/AccountSettingScheduler.vue'
|
||||
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
|
||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||
import { SettingTabs } from '@/router/menu'
|
||||
import { getSettingTabs } from '@/router/i18n-menu'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const activeTab = ref(route.query.tab)
|
||||
|
||||
function jumpTab(tab: string) {
|
||||
router.push('/setting?tab=' + tab)
|
||||
}
|
||||
const settingTabs = computed(() => getSettingTabs())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VHeaderTab :items="SettingTabs" v-model="activeTab" />
|
||||
<VHeaderTab :items="settingTabs" v-model="activeTab" />
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<!-- 系统 -->
|
||||
<VWindowItem value="系统">
|
||||
<VWindowItem value="system">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSystem />
|
||||
@@ -36,7 +33,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 目录 -->
|
||||
<VWindowItem value="存储 & 目录">
|
||||
<VWindowItem value="directory">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingDirectory />
|
||||
@@ -45,7 +42,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 站点 -->
|
||||
<VWindowItem value="站点">
|
||||
<VWindowItem value="site">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSite />
|
||||
@@ -54,7 +51,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 规则 -->
|
||||
<VWindowItem value="规则">
|
||||
<VWindowItem value="rule">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingRule />
|
||||
@@ -63,7 +60,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<VWindowItem value="搜索 & 下载">
|
||||
<VWindowItem value="search">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSearch />
|
||||
@@ -72,7 +69,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 订阅 -->
|
||||
<VWindowItem value="订阅">
|
||||
<VWindowItem value="subscribe">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSubscribe />
|
||||
@@ -81,7 +78,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 服务 -->
|
||||
<VWindowItem value="服务">
|
||||
<VWindowItem value="scheduler">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingScheduler />
|
||||
@@ -90,7 +87,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 通知 -->
|
||||
<VWindowItem value="通知">
|
||||
<VWindowItem value="notification">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingNotification />
|
||||
@@ -99,7 +96,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 词表 -->
|
||||
<VWindowItem value="词表">
|
||||
<VWindowItem value="words">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingWords />
|
||||
@@ -108,7 +105,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 关于 -->
|
||||
<VWindowItem value="关于">
|
||||
<VWindowItem value="about">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingAbout />
|
||||
|
||||
@@ -4,7 +4,7 @@ import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
|
||||
import SubscribeShareView from '@/views/subscribe/SubscribeShareView.vue'
|
||||
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
|
||||
|
||||
import { SubscribeMovieTabs, SubscribeTvTabs } from '@/router/menu'
|
||||
import { getSubscribeMovieTabs, getSubscribeTvTabs } from '@/router/i18n-menu'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -13,6 +13,15 @@ const subId = ref(route.query.id as string)
|
||||
const activeTab = ref(route.query.tab)
|
||||
const shareViewKey = ref(0)
|
||||
|
||||
// 获取标签页
|
||||
const subscribeTabs = computed(() => {
|
||||
if (subType === '电影') {
|
||||
return getSubscribeMovieTabs()
|
||||
} else {
|
||||
return getSubscribeTvTabs()
|
||||
}
|
||||
})
|
||||
|
||||
// 默认订阅设置弹窗
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
@@ -37,10 +46,10 @@ const searchShares = () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VHeaderTab :items="subType == '电影' ? SubscribeMovieTabs : SubscribeTvTabs" v-model="activeTab">
|
||||
<VHeaderTab :items="subscribeTabs" v-model="activeTab">
|
||||
<template #append>
|
||||
<VMenu
|
||||
v-if="activeTab === '我的订阅'"
|
||||
v-if="activeTab === 'mysub'"
|
||||
v-model="filterSubscribeDialog"
|
||||
width="20rem"
|
||||
:close-on-content-click="false"
|
||||
@@ -70,7 +79,7 @@ const searchShares = () => {
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<VMenu
|
||||
v-if="activeTab === '订阅分享'"
|
||||
v-if="activeTab === 'share'"
|
||||
v-model="searchShareDialog"
|
||||
width="25rem"
|
||||
:close-on-content-click="false"
|
||||
@@ -104,7 +113,7 @@ const searchShares = () => {
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<VBtn
|
||||
v-if="activeTab === '我的订阅'"
|
||||
v-if="activeTab === 'mysub'"
|
||||
icon="mdi-clipboard-edit-outline"
|
||||
variant="text"
|
||||
color="gray"
|
||||
@@ -116,21 +125,21 @@ const searchShares = () => {
|
||||
</VHeaderTab>
|
||||
|
||||
<VWindow v-model="activeTab" class="disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="我的订阅">
|
||||
<VWindowItem value="mysub">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<SubscribeListView :type="subType" :subid="subId" :keyword="subscribeFilter" />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="热门订阅">
|
||||
<VWindowItem value="popular">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<SubscribePopularView :type="subType" />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="订阅分享">
|
||||
<VWindowItem value="share">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<SubscribeShareView :keyword="shareKeyword" :key="shareViewKey" />
|
||||
|
||||
Reference in New Issue
Block a user