mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-06 20:43:03 +08:00
添加国际化支持:引入 vue-i18n,更新多个组件以支持语言切换和文本翻译
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -11,6 +11,7 @@ declare module 'vue' {
|
||||
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
|
||||
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
|
||||
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
|
||||
LocaleSwitcher: typeof import('./src/@core/components/LocaleSwitcher.vue')['default']
|
||||
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
|
||||
PageContentTitle: typeof import('./src/@core/components/PageContentTitle.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
64
src/@core/components/LocaleSwitcher.vue
Normal file
64
src/@core/components/LocaleSwitcher.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/locales/types'
|
||||
import { setI18nLanguage, getCurrentLocale } from '@/plugins/i18n'
|
||||
|
||||
// 当前语言
|
||||
const currentLocale = ref<SupportedLocale>(getCurrentLocale())
|
||||
|
||||
// 支持的语言列表
|
||||
const locales = computed(() => {
|
||||
return Object.entries(SUPPORTED_LOCALES).map(([key, locale]) => ({
|
||||
value: key as SupportedLocale,
|
||||
title: locale.title,
|
||||
flag: locale.flag,
|
||||
icon: `flag-${key.split('-')[0]}`,
|
||||
}))
|
||||
})
|
||||
|
||||
// 切换语言
|
||||
async function changeLocale(locale: SupportedLocale) {
|
||||
try {
|
||||
await setI18nLanguage(locale)
|
||||
currentLocale.value = locale
|
||||
// 刷新页面
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前语言图标
|
||||
const getCurrentIcon = computed(() => {
|
||||
const locale = locales.value.find(l => l.value === currentLocale.value)
|
||||
return locale?.flag || '🌐'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VMenu class="locale-menu" scrim>
|
||||
<template v-slot:activator="{ props }">
|
||||
<IconBtn v-bind="props">
|
||||
<span class="text-xl">{{ getCurrentIcon }}</span>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VList>
|
||||
<div class="px-2">
|
||||
<VListItem
|
||||
v-for="locale in locales"
|
||||
:key="locale.value"
|
||||
@click="changeLocale(locale.value)"
|
||||
:active="currentLocale === locale.value"
|
||||
class="mb-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="text-xl me-2">{{ locale.flag }}</span>
|
||||
</template>
|
||||
<VListItemTitle>{{ locale.title }}</VListItemTitle>
|
||||
<template #append v-if="currentLocale === locale.value">
|
||||
<VIcon icon="mdi-check" color="primary" size="small" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</div>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</template>
|
||||
@@ -6,6 +6,7 @@ import api from '@/api'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { saveLocalTheme } from '../utils/theme'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -27,6 +28,8 @@ const getNextThemeName = () => {
|
||||
|
||||
const $toast = useToast()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 自定义CSS弹窗
|
||||
const cssDialog = ref(false)
|
||||
|
||||
@@ -64,14 +67,6 @@ function changeTheme(theme: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 是否有滚动条
|
||||
function hasScrollbar(el?: Element | null) {
|
||||
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false
|
||||
|
||||
const style = window.getComputedStyle(el)
|
||||
return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight)
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
try {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
|
||||
@@ -158,7 +153,7 @@ onMounted(() => {
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-palette" />
|
||||
</template>
|
||||
<VListItemTitle>自定义主题</VListItemTitle>
|
||||
<VListItemTitle>{{ t('theme.custom') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</div>
|
||||
</VList>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// ————————————————————————————————————
|
||||
//* ——— Perfect Scrollbar
|
||||
// Perfect Scrollbar
|
||||
// ————————————————————————————————————
|
||||
|
||||
.v-application.v-theme--dark {
|
||||
|
||||
12
src/App.vue
12
src/App.vue
@@ -4,6 +4,12 @@ import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
|
||||
import api from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
||||
import { SupportedLocale } from './locales/types'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
@@ -11,6 +17,10 @@ let themeValue = localStorage.getItem('theme') || 'light'
|
||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
|
||||
// 生效语言
|
||||
const localeValue = getBrowserLocale()
|
||||
setI18nLanguage(localeValue as SupportedLocale)
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
@@ -67,7 +77,7 @@ function updateHtmlThemeAttribute(themeName: string) {
|
||||
// 获取背景图片
|
||||
async function fetchBackgroundImages() {
|
||||
try {
|
||||
backgroundImages.value = await api.get('/login/wallpapers')
|
||||
backgroundImages.value = await api.get(`/login/${t('login.wallpapers')}`)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { Site, Plugin, Subscribe } from '@/api/types'
|
||||
import { SystemNavMenus, SettingTabs } from '@/router/menu'
|
||||
import { getNavMenus, getSettingTabs } from '@/router/i18n-menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useUserStore } from '@/stores'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
@@ -73,7 +73,7 @@ function loadRecentSearches() {
|
||||
function getMenus(): NavMenu[] {
|
||||
let menus: NavMenu[] = []
|
||||
// 导航菜单
|
||||
SystemNavMenus.forEach(
|
||||
getNavMenus().forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
@@ -85,7 +85,7 @@ function getMenus(): NavMenu[] {
|
||||
}),
|
||||
)
|
||||
// 设置标签页
|
||||
SettingTabs.forEach(
|
||||
getSettingTabs().forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
|
||||
@@ -3,18 +3,20 @@ import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTit
|
||||
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
|
||||
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
|
||||
import Footer from '@/layouts/components/Footer.vue'
|
||||
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
|
||||
import NavbarActions from '@/layouts/components/NavbarActions.vue'
|
||||
import UserNofification from '@/layouts/components/UserNotification.vue'
|
||||
import SearchBar from '@/layouts/components/SearchBar.vue'
|
||||
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { SystemNavMenus } from '@/router/menu'
|
||||
import { getNavMenus } from '@/router/i18n-menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const display = useDisplay()
|
||||
const appMode = inject('pwaMode')
|
||||
const { t } = useI18n()
|
||||
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
@@ -39,7 +41,9 @@ const systemMenus = ref<NavMenu[]>([])
|
||||
|
||||
// 根据分类获取菜单列表
|
||||
const getMenuList = (header: string) => {
|
||||
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (superUser || !item.admin))
|
||||
// 使用国际化菜单
|
||||
const menus = getNavMenus()
|
||||
return menus.filter((item: NavMenu) => item.header === header && (superUser || !item.admin))
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
@@ -49,11 +53,11 @@ function goBack() {
|
||||
|
||||
onMounted(() => {
|
||||
// 获取菜单列表
|
||||
startMenus.value = getMenuList('开始')
|
||||
discoveryMenus.value = getMenuList('发现')
|
||||
subscribeMenus.value = getMenuList('订阅')
|
||||
organizeMenus.value = getMenuList('整理')
|
||||
systemMenus.value = getMenuList('系统')
|
||||
startMenus.value = getMenuList(t('menu.start'))
|
||||
discoveryMenus.value = getMenuList(t('menu.discovery'))
|
||||
subscribeMenus.value = getMenuList(t('menu.subscribe'))
|
||||
organizeMenus.value = getMenuList(t('menu.organize'))
|
||||
systemMenus.value = getMenuList(t('menu.system'))
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -74,10 +78,10 @@ onMounted(() => {
|
||||
<SearchBar />
|
||||
<!-- 👉 Spacer -->
|
||||
<VSpacer />
|
||||
<!-- 👉 Theme & Language -->
|
||||
<NavbarActions />
|
||||
<!-- 👉 Shortcuts -->
|
||||
<ShortcutBar v-if="superUser" />
|
||||
<!-- 👉 Theme -->
|
||||
<NavbarThemeSwitcher />
|
||||
<!-- 👉 Notification -->
|
||||
<UserNofification />
|
||||
<!-- 👉 UserProfile -->
|
||||
@@ -91,7 +95,7 @@ onMounted(() => {
|
||||
<VerticalNavSectionTitle
|
||||
v-if="discoveryMenus.length > 0"
|
||||
:item="{
|
||||
heading: '发现',
|
||||
heading: t('menu.discovery'),
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink v-for="item in discoveryMenus" :item="item" />
|
||||
@@ -99,7 +103,7 @@ onMounted(() => {
|
||||
<VerticalNavSectionTitle
|
||||
v-if="subscribeMenus.length > 0"
|
||||
:item="{
|
||||
heading: '订阅',
|
||||
heading: t('menu.subscribe'),
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink v-for="item in subscribeMenus" :item="item" />
|
||||
@@ -107,7 +111,7 @@ onMounted(() => {
|
||||
<VerticalNavSectionTitle
|
||||
v-if="organizeMenus.length > 0"
|
||||
:item="{
|
||||
heading: '整理',
|
||||
heading: t('menu.organize'),
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink v-for="item in organizeMenus" :item="item" />
|
||||
@@ -115,7 +119,7 @@ onMounted(() => {
|
||||
<VerticalNavSectionTitle
|
||||
v-if="systemMenus.length > 0"
|
||||
:item="{
|
||||
heading: '系统',
|
||||
heading: t('menu.system'),
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink v-for="item in systemMenus" :item="item" />
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { SystemNavMenus } from '@/router/menu'
|
||||
import { getNavMenus } from '@/router/i18n-menu'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
|
||||
const display = useDisplay()
|
||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 获取导航菜单
|
||||
const navMenus = computed(() => getNavMenus())
|
||||
|
||||
// 根据当前路径获取匹配的菜单路径
|
||||
function getMenuPathFromRoute(path: string): string {
|
||||
const matchedMenu = SystemNavMenus.find(menu => menu.footer === true && path.startsWith(menu.to))
|
||||
return matchedMenu ? matchedMenu.to : '/apps'
|
||||
const matchedMenu = navMenus.value.find((menu: NavMenu) => menu.footer === true && path.startsWith(menu.to as string))
|
||||
return matchedMenu ? (matchedMenu.to as string) : '/apps'
|
||||
}
|
||||
|
||||
// 当前选中的菜单,初始值基于当前路由
|
||||
@@ -18,7 +22,7 @@ const currentMenu = ref<string>(getMenuPathFromRoute(route.path))
|
||||
|
||||
// 过滤出底部菜单项
|
||||
const footerMenus = computed(() => {
|
||||
return SystemNavMenus.filter(menu => menu.footer === true)
|
||||
return navMenus.value.filter((menu: NavMenu) => menu.footer === true)
|
||||
})
|
||||
|
||||
// 监听路由变化来更新currentMenu
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '* * * * *',
|
||||
default: '',
|
||||
},
|
||||
items: {
|
||||
type: Array as PropType<{ title: string; icon: string }[]>,
|
||||
type: Array as PropType<{ title: string; icon: string; tab: string }[]>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
@@ -76,8 +76,8 @@ onUnmounted(() => {
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="header-tab"
|
||||
:class="{ 'active': currentValue === item.title }"
|
||||
@click="currentValue = item.title"
|
||||
:class="{ 'active': currentValue === item.tab }"
|
||||
@click="currentValue = item.tab"
|
||||
>
|
||||
<VIcon v-if="item.icon" :icon="item.icon" size="small" class="header-tab-icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
|
||||
45
src/layouts/components/NavbarActions.vue
Normal file
45
src/layouts/components/NavbarActions.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import LocaleSwitcher from '@/@core/components/LocaleSwitcher.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const themes: ThemeSwitcherTheme[] = [
|
||||
{
|
||||
name: 'auto',
|
||||
title: t('theme.auto'),
|
||||
icon: 'mdi-laptop',
|
||||
},
|
||||
{
|
||||
name: 'light',
|
||||
title: t('theme.light'),
|
||||
icon: 'mdi-weather-sunny',
|
||||
},
|
||||
{
|
||||
name: 'dark',
|
||||
title: t('theme.dark'),
|
||||
icon: 'mdi-weather-night',
|
||||
},
|
||||
{
|
||||
name: 'purple',
|
||||
title: t('theme.purple'),
|
||||
icon: 'mdi-brightness-4',
|
||||
},
|
||||
{
|
||||
name: 'transparent',
|
||||
title: t('theme.transparent'),
|
||||
icon: 'mdi-gradient-horizontal',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex align-center">
|
||||
<!-- 语言切换 -->
|
||||
<LocaleSwitcher class="me-2" />
|
||||
|
||||
<!-- 主题切换 -->
|
||||
<ThemeSwitcher :themes="themes" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,11 +7,14 @@ import api from '@/api'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
|
||||
import { useAuthStore, useUserStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 认证 Store
|
||||
const authStore = useAuthStore()
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
@@ -153,7 +156,7 @@ const userLevel = computed(() => userStore.level)
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-logout" />
|
||||
</template>
|
||||
退出登录
|
||||
{{ t('app.logout') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VList>
|
||||
@@ -161,7 +164,7 @@ const userLevel = computed(() => userStore.level)
|
||||
<!-- !SECTION -->
|
||||
</VAvatar>
|
||||
<!-- 重启进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在重启 ..." />
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" />
|
||||
<!-- 用户认证对话框 -->
|
||||
<UserAuthDialog v-if="siteAuthDialog" v-model="siteAuthDialog" @done="siteAuthDone" @close="siteAuthDialog = false" />
|
||||
<!-- 重启确认对话框 -->
|
||||
@@ -173,14 +176,18 @@ const userLevel = computed(() => userStore.level)
|
||||
<VIcon size="x-large" icon="mdi-alert" />
|
||||
</VAvatar>
|
||||
<div class="ms-3">
|
||||
<p class="font-weight-bold text-xl text-high-emphasis">确认重启系统吗?</p>
|
||||
<p>重启后,您将被注销并需要重新登录。</p>
|
||||
<p class="font-weight-bold text-xl text-high-emphasis">{{ t('app.confirmRestart') }}</p>
|
||||
<p>{{ t('app.restartTip') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
<VCardActions class="mx-auto">
|
||||
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">取消</VBtn>
|
||||
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5"> 确定 </VBtn>
|
||||
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">{{
|
||||
t('common.cancel')
|
||||
}}</VBtn>
|
||||
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5">{{
|
||||
t('common.confirm')
|
||||
}}</VBtn>
|
||||
</VCardActions>
|
||||
<VDialogCloseBtn @click="restartDialog = false" />
|
||||
</VCard>
|
||||
|
||||
149
src/locales/en-US.ts
Normal file
149
src/locales/en-US.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
export default {
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
add: 'Add',
|
||||
search: 'Search',
|
||||
loading: 'Loading',
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
},
|
||||
theme: {
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
auto: 'Auto',
|
||||
transparent: 'Transparent',
|
||||
purple: 'Purple',
|
||||
custom: 'Custom',
|
||||
},
|
||||
app: {
|
||||
moviepilot: 'MoviePilot',
|
||||
recommend: 'Recommend',
|
||||
subscribeMovie: 'Movie Subscription',
|
||||
subscribeTv: 'TV Subscription',
|
||||
settings: 'Settings',
|
||||
language: 'Language',
|
||||
selectLanguage: 'Select Language',
|
||||
logout: 'Logout',
|
||||
restarting: 'Restarting...',
|
||||
confirmRestart: 'Confirm Restart?',
|
||||
restartTip: 'After restarting, you will be logged out and need to log in again.',
|
||||
},
|
||||
login: {
|
||||
wallpapers: 'wallpapers',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
otpCode: 'OTP Code',
|
||||
stayLoggedIn: 'Stay Logged In',
|
||||
login: 'Login',
|
||||
networkError: 'Login failed, please check your network connection!',
|
||||
authFailure: 'Login failed, please check your username, password or OTP code!',
|
||||
permissionDenied: 'Login failed, you do not have permission to access!',
|
||||
serverError: 'Login failed, server error!',
|
||||
loginFailed: 'Login Failed',
|
||||
checkCredentials: 'Please check your username, password or OTP code!',
|
||||
},
|
||||
menu: {
|
||||
start: 'Start',
|
||||
discovery: 'Discovery',
|
||||
subscribe: 'Subscribe',
|
||||
organize: 'Organize',
|
||||
system: 'System',
|
||||
},
|
||||
navItems: {
|
||||
dashboard: 'Dashboard',
|
||||
mediaInfo: 'Media Library',
|
||||
recommend: 'Recommend',
|
||||
site: 'Sites',
|
||||
search: 'Search',
|
||||
searchResult: 'Search Results',
|
||||
download: 'Download',
|
||||
movieSubscribe: 'Movie Subscription',
|
||||
tvSubscribe: 'TV Subscription',
|
||||
history: 'History',
|
||||
transfer: 'Transfer',
|
||||
rename: 'Rename',
|
||||
statistic: 'Statistics',
|
||||
setting: 'Settings',
|
||||
plugin: 'Plugins',
|
||||
user: 'Users',
|
||||
about: 'About',
|
||||
explore: 'Explore',
|
||||
movie: 'Movies',
|
||||
tv: 'TV Shows',
|
||||
workflow: 'Workflow',
|
||||
calendar: 'Calendar',
|
||||
downloadManager: 'Download Manager',
|
||||
mediaOrganize: 'Media Organizer',
|
||||
fileManager: 'File Manager',
|
||||
pluginManager: 'Plugins',
|
||||
siteManager: 'Site Manager',
|
||||
userManager: 'User Manager',
|
||||
settings: 'Settings',
|
||||
},
|
||||
settingTabs: {
|
||||
system: {
|
||||
title: 'System',
|
||||
description: 'Basic settings, downloaders (Qbittorrent, Transmission), media servers (Emby, Jellyfin, Plex)',
|
||||
},
|
||||
directory: {
|
||||
title: 'Storage & Directories',
|
||||
description: 'Download directories, media library directories, organization, metadata scraping',
|
||||
},
|
||||
site: {
|
||||
title: 'Sites',
|
||||
description: 'Site synchronization, site data refresh, site reset',
|
||||
},
|
||||
rule: {
|
||||
title: 'Rules',
|
||||
description: 'Custom rules, priority rule groups, download rules',
|
||||
},
|
||||
search: {
|
||||
title: 'Search & Download',
|
||||
description: 'Search data sources (TheMovieDb, Douban, Bangumi), download task labels, search sites',
|
||||
},
|
||||
subscribe: {
|
||||
title: 'Subscription',
|
||||
description: 'Subscription sites, subscription modes, subscription rules, upgrade rules',
|
||||
},
|
||||
scheduler: {
|
||||
title: 'Services',
|
||||
description: 'Scheduled tasks',
|
||||
},
|
||||
notification: {
|
||||
title: 'Notifications',
|
||||
description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope',
|
||||
},
|
||||
words: {
|
||||
title: 'Word Lists',
|
||||
description: 'Custom recognition words, custom groups, custom placeholders, file organization filter words',
|
||||
},
|
||||
about: {
|
||||
title: 'About',
|
||||
description: 'Software version',
|
||||
},
|
||||
},
|
||||
subscribeTabs: {
|
||||
movie: {
|
||||
mysub: 'My Subscriptions',
|
||||
popular: 'Popular',
|
||||
},
|
||||
tv: {
|
||||
mysub: 'My Subscriptions',
|
||||
popular: 'Popular',
|
||||
share: 'Shared',
|
||||
},
|
||||
},
|
||||
pluginTabs: {
|
||||
installed: 'My Plugins',
|
||||
market: 'Plugin Market',
|
||||
},
|
||||
discoverTabs: {
|
||||
themoviedb: 'TheMovieDb',
|
||||
douban: 'Douban',
|
||||
bangumi: 'Bangumi',
|
||||
},
|
||||
}
|
||||
25
src/locales/types.ts
Normal file
25
src/locales/types.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import zhCN from './zh-CN'
|
||||
|
||||
export type MessageSchema = typeof zhCN
|
||||
export type LocaleKey = keyof typeof zhCN
|
||||
|
||||
export interface LocaleInfo {
|
||||
name: string
|
||||
title: string
|
||||
flag?: string
|
||||
}
|
||||
|
||||
export const SUPPORTED_LOCALES: Record<string, LocaleInfo> = {
|
||||
'zh-CN': {
|
||||
name: 'zh-CN',
|
||||
title: '简体中文',
|
||||
flag: '🇨🇳',
|
||||
},
|
||||
'en-US': {
|
||||
name: 'en-US',
|
||||
title: 'English',
|
||||
flag: '🇺🇸',
|
||||
},
|
||||
}
|
||||
|
||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
||||
149
src/locales/zh-CN.ts
Normal file
149
src/locales/zh-CN.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
export default {
|
||||
common: {
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
add: '添加',
|
||||
search: '搜索',
|
||||
loading: '加载中',
|
||||
success: '成功',
|
||||
error: '错误',
|
||||
},
|
||||
theme: {
|
||||
light: '浅色',
|
||||
dark: '深色',
|
||||
auto: '跟随系统',
|
||||
transparent: '透明',
|
||||
purple: '幻紫',
|
||||
custom: '自定义主题',
|
||||
},
|
||||
app: {
|
||||
moviepilot: 'MoviePilot',
|
||||
recommend: '推荐',
|
||||
subscribeMovie: '电影订阅',
|
||||
subscribeTv: '电视剧订阅',
|
||||
settings: '设置',
|
||||
language: '语言设置',
|
||||
selectLanguage: '选择语言',
|
||||
logout: '退出登录',
|
||||
restarting: '正在重启...',
|
||||
confirmRestart: '确认重启系统吗?',
|
||||
restartTip: '重启后,您将被注销并需要重新登录。',
|
||||
},
|
||||
login: {
|
||||
wallpapers: '壁纸',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
otpCode: '双重验证码',
|
||||
stayLoggedIn: '保持登录',
|
||||
login: '登录',
|
||||
networkError: '登录失败,请检查网络连接!',
|
||||
authFailure: '登录失败,请检查用户名、密码或双重验证是否正确!',
|
||||
permissionDenied: '登录失败,您没有权限访问!',
|
||||
serverError: '登录失败,服务器错误!',
|
||||
loginFailed: '登录失败',
|
||||
checkCredentials: '请检查用户名、密码或双重验证码是否正确!',
|
||||
},
|
||||
menu: {
|
||||
start: '开始',
|
||||
discovery: '发现',
|
||||
subscribe: '订阅',
|
||||
organize: '整理',
|
||||
system: '系统',
|
||||
},
|
||||
navItems: {
|
||||
dashboard: '仪表盘',
|
||||
mediaInfo: '媒体库',
|
||||
recommend: '推荐',
|
||||
site: '站点',
|
||||
search: '搜索',
|
||||
searchResult: '搜索结果',
|
||||
download: '下载',
|
||||
movieSubscribe: '电影订阅',
|
||||
tvSubscribe: '电视剧订阅',
|
||||
history: '历史记录',
|
||||
transfer: '整理',
|
||||
rename: '重命名',
|
||||
statistic: '统计',
|
||||
setting: '设置',
|
||||
plugin: '插件',
|
||||
user: '用户',
|
||||
about: '关于',
|
||||
explore: '探索',
|
||||
movie: '电影',
|
||||
tv: '电视剧',
|
||||
workflow: '工作流',
|
||||
calendar: '日历',
|
||||
downloadManager: '下载管理',
|
||||
mediaOrganize: '媒体整理',
|
||||
fileManager: '文件管理',
|
||||
pluginManager: '插件',
|
||||
siteManager: '站点管理',
|
||||
userManager: '用户管理',
|
||||
settings: '设定',
|
||||
},
|
||||
settingTabs: {
|
||||
system: {
|
||||
title: '系统',
|
||||
description: '基础设置、下载器(Qbittorrent、Transmission)、媒体服务器(Emby、Jellyfin、Plex)',
|
||||
},
|
||||
directory: {
|
||||
title: '存储 & 目录',
|
||||
description: '下载目录、媒体库目录、整理、刮削',
|
||||
},
|
||||
site: {
|
||||
title: '站点',
|
||||
description: '站点同步、站点数据刷新、站点重置',
|
||||
},
|
||||
rule: {
|
||||
title: '规则',
|
||||
description: '自定义规则、优先级规则组、下载规则',
|
||||
},
|
||||
search: {
|
||||
title: '搜索 & 下载',
|
||||
description: '搜索数据源(TheMovieDb、豆瓣、Bangumi)、下载任务标签、搜索站点',
|
||||
},
|
||||
subscribe: {
|
||||
title: '订阅',
|
||||
description: '订阅站点、订阅模式、订阅规则、洗版规则',
|
||||
},
|
||||
scheduler: {
|
||||
title: '服务',
|
||||
description: '定时作业',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息发送范围',
|
||||
},
|
||||
words: {
|
||||
title: '词表',
|
||||
description: '自定义识别词、自定义制作组/字幕组、自定义占位符、文件整理屏蔽词',
|
||||
},
|
||||
about: {
|
||||
title: '关于',
|
||||
description: '软件版本',
|
||||
},
|
||||
},
|
||||
subscribeTabs: {
|
||||
movie: {
|
||||
mysub: '我的订阅',
|
||||
popular: '热门订阅',
|
||||
},
|
||||
tv: {
|
||||
mysub: '我的订阅',
|
||||
popular: '热门订阅',
|
||||
share: '订阅分享',
|
||||
},
|
||||
},
|
||||
pluginTabs: {
|
||||
installed: '我的插件',
|
||||
market: '插件市场',
|
||||
},
|
||||
discoverTabs: {
|
||||
themoviedb: 'TheMovieDb',
|
||||
douban: '豆瓣',
|
||||
bangumi: 'Bangumi',
|
||||
},
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { createApp } from 'vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
import router from '@/router'
|
||||
import pinia from '@/stores/index'
|
||||
import i18n from '@/plugins/i18n'
|
||||
|
||||
// 3. 全局组件
|
||||
import App from '@/App.vue'
|
||||
@@ -117,5 +118,6 @@ initializeApp().then(() => {
|
||||
cancellationText: '取消',
|
||||
},
|
||||
})
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
})
|
||||
|
||||
@@ -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" />
|
||||
|
||||
69
src/plugins/i18n.ts
Normal file
69
src/plugins/i18n.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { nextTick } from 'vue'
|
||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/locales/types'
|
||||
|
||||
// 导入语言文件
|
||||
import zhCN from '@/locales/zh-CN'
|
||||
import enUS from '@/locales/en-US'
|
||||
|
||||
// 创建 i18n 实例
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用组合式API
|
||||
locale: getBrowserLocale() || 'zh-CN', // 默认语言
|
||||
fallbackLocale: 'zh-CN', // 回退语言
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
},
|
||||
silentTranslationWarn: true,
|
||||
silentFallbackWarn: true,
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取浏览器语言设置
|
||||
*/
|
||||
export function getBrowserLocale(): SupportedLocale | null {
|
||||
// 从本地存储获取
|
||||
const storedLocale = localStorage.getItem('MP_LOCALE')
|
||||
if (storedLocale && Object.keys(SUPPORTED_LOCALES).includes(storedLocale)) {
|
||||
return storedLocale as SupportedLocale
|
||||
}
|
||||
|
||||
// 从浏览器获取
|
||||
const navigatorLocale = navigator.languages?.[0] || navigator.language || 'zh-CN'
|
||||
|
||||
// 检查是否为支持的语言
|
||||
const locale = Object.keys(SUPPORTED_LOCALES).find(locale => {
|
||||
return navigatorLocale.includes(locale.split('-')[0])
|
||||
})
|
||||
|
||||
return (locale as SupportedLocale) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置i18n语言环境
|
||||
*/
|
||||
export async function setI18nLanguage(locale: SupportedLocale) {
|
||||
// 加载语言文件(如果使用动态导入)
|
||||
// await loadLocaleMessages(i18n, locale)
|
||||
|
||||
// 更新 i18n 实例语言
|
||||
i18n.global.locale.value = locale as any as any
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('MP_LOCALE', locale)
|
||||
|
||||
// 更新 HTML 标签 lang 属性
|
||||
document.querySelector('html')?.setAttribute('lang', locale)
|
||||
|
||||
return nextTick()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
*/
|
||||
export function getCurrentLocale(): SupportedLocale {
|
||||
return i18n.global.locale.value as SupportedLocale
|
||||
}
|
||||
|
||||
export default i18n
|
||||
274
src/router/i18n-menu.ts
Normal file
274
src/router/i18n-menu.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 构建路由菜单,每次调用时使用当前的语言环境
|
||||
export function getNavMenus() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('navItems.dashboard'),
|
||||
icon: 'mdi-home-outline',
|
||||
to: '/dashboard',
|
||||
header: t('menu.start'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
},
|
||||
{
|
||||
title: t('navItems.searchResult'),
|
||||
icon: 'mdi-magnify',
|
||||
to: '/resource',
|
||||
header: t('menu.start'),
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: t('navItems.recommend'),
|
||||
icon: 'mdi-star-outline',
|
||||
to: '/recommend',
|
||||
header: t('menu.discovery'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
},
|
||||
{
|
||||
title: t('navItems.explore'),
|
||||
icon: 'mdi-apple-safari',
|
||||
to: '/discover',
|
||||
header: t('menu.discovery'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
},
|
||||
{
|
||||
title: t('navItems.movie'),
|
||||
full_title: t('navItems.movieSubscribe'),
|
||||
icon: 'mdi-movie-open-outline',
|
||||
to: '/subscribe/movie',
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
footer: false,
|
||||
},
|
||||
{
|
||||
title: t('navItems.tv'),
|
||||
full_title: t('navItems.tvSubscribe'),
|
||||
icon: 'mdi-television',
|
||||
to: '/subscribe/tv',
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
footer: false,
|
||||
},
|
||||
{
|
||||
title: t('navItems.workflow'),
|
||||
full_title: t('navItems.workflow'),
|
||||
icon: 'mdi-state-machine',
|
||||
to: '/workflow',
|
||||
header: t('menu.subscribe'),
|
||||
admin: true,
|
||||
footer: false,
|
||||
},
|
||||
{
|
||||
title: t('navItems.calendar'),
|
||||
full_title: t('navItems.calendar'),
|
||||
icon: 'mdi-calendar',
|
||||
to: '/calendar',
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: t('navItems.downloadManager'),
|
||||
icon: 'mdi-download-outline',
|
||||
to: '/downloading',
|
||||
header: t('menu.organize'),
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: t('navItems.mediaOrganize'),
|
||||
icon: 'mdi-folder-play-outline',
|
||||
to: '/history',
|
||||
header: t('menu.organize'),
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: t('navItems.fileManager'),
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
to: '/filemanager',
|
||||
header: t('menu.organize'),
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: t('navItems.pluginManager'),
|
||||
icon: 'mdi-puzzle-outline',
|
||||
to: '/plugins',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: t('navItems.siteManager'),
|
||||
icon: 'mdi-web',
|
||||
to: '/site',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: t('navItems.userManager'),
|
||||
icon: 'mdi-account-group-outline',
|
||||
to: '/user',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: t('navItems.settings'),
|
||||
icon: 'mdi-cog-outline',
|
||||
to: '/setting',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 获取设置标签页
|
||||
export function getSettingTabs() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('settingTabs.system.title'),
|
||||
icon: 'mdi-server-network',
|
||||
tab: 'system',
|
||||
description: t('settingTabs.system.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.directory.title'),
|
||||
icon: 'mdi-folder',
|
||||
tab: 'directory',
|
||||
description: t('settingTabs.directory.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.site.title'),
|
||||
icon: 'mdi-web',
|
||||
tab: 'site',
|
||||
description: t('settingTabs.site.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.rule.title'),
|
||||
icon: 'mdi-filter',
|
||||
tab: 'rule',
|
||||
description: t('settingTabs.rule.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.search.title'),
|
||||
icon: 'mdi-magnify',
|
||||
tab: 'search',
|
||||
description: t('settingTabs.search.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.subscribe.title'),
|
||||
icon: 'mdi-rss',
|
||||
tab: 'subscribe',
|
||||
description: t('settingTabs.subscribe.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.scheduler.title'),
|
||||
icon: 'mdi-list-box',
|
||||
tab: 'scheduler',
|
||||
description: t('settingTabs.scheduler.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.notification.title'),
|
||||
icon: 'mdi-bell',
|
||||
tab: 'notification',
|
||||
description: t('settingTabs.notification.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.words.title'),
|
||||
icon: 'mdi-file-word-box',
|
||||
tab: 'words',
|
||||
description: t('settingTabs.words.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.about.title'),
|
||||
icon: 'mdi-information',
|
||||
tab: 'about',
|
||||
description: t('settingTabs.about.description'),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 获取电影订阅标签页
|
||||
export function getSubscribeMovieTabs() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('subscribeTabs.movie.mysub'),
|
||||
tab: 'mysub',
|
||||
icon: 'mdi-bell-check',
|
||||
},
|
||||
{
|
||||
title: t('subscribeTabs.movie.popular'),
|
||||
tab: 'popular',
|
||||
icon: 'mdi-fire',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 获取电视剧订阅标签页
|
||||
export function getSubscribeTvTabs() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('subscribeTabs.tv.mysub'),
|
||||
tab: 'mysub',
|
||||
icon: 'mdi-bell-check',
|
||||
},
|
||||
{
|
||||
title: t('subscribeTabs.tv.popular'),
|
||||
tab: 'popular',
|
||||
icon: 'mdi-fire',
|
||||
},
|
||||
{
|
||||
title: t('subscribeTabs.tv.share'),
|
||||
tab: 'share',
|
||||
icon: 'mdi-share-variant',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 获取插件标签页
|
||||
export function getPluginTabs() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('pluginTabs.installed'),
|
||||
tab: 'installed',
|
||||
icon: 'mdi-puzzle',
|
||||
},
|
||||
{
|
||||
title: t('pluginTabs.market'),
|
||||
tab: 'market',
|
||||
icon: 'mdi-shopping',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 获取发现标签页
|
||||
export function getDiscoverTabs() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return [
|
||||
{
|
||||
name: t('discoverTabs.themoviedb'),
|
||||
tab: 'themoviedb',
|
||||
icon: 'themoviedb',
|
||||
},
|
||||
{
|
||||
name: t('discoverTabs.douban'),
|
||||
tab: 'douban',
|
||||
icon: 'douban',
|
||||
},
|
||||
{
|
||||
name: t('discoverTabs.bangumi'),
|
||||
tab: 'bangumi',
|
||||
icon: 'bangumi',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
// 导般菜单
|
||||
export const SystemNavMenus = [
|
||||
{
|
||||
title: '仪表板',
|
||||
icon: 'mdi-home-outline',
|
||||
to: '/dashboard',
|
||||
header: '开始',
|
||||
admin: false,
|
||||
footer: true,
|
||||
},
|
||||
{
|
||||
title: '搜索结果',
|
||||
icon: 'mdi-magnify',
|
||||
to: '/resource',
|
||||
header: '开始',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '推荐',
|
||||
icon: 'mdi-star-outline',
|
||||
to: '/recommend',
|
||||
header: '发现',
|
||||
admin: false,
|
||||
footer: true,
|
||||
},
|
||||
{
|
||||
title: '探索',
|
||||
icon: 'mdi-apple-safari',
|
||||
to: '/discover',
|
||||
header: '发现',
|
||||
admin: false,
|
||||
footer: true,
|
||||
},
|
||||
{
|
||||
title: '电影',
|
||||
full_title: '电影订阅',
|
||||
icon: 'mdi-movie-open-outline',
|
||||
to: '/subscribe/movie',
|
||||
header: '订阅',
|
||||
admin: false,
|
||||
footer: false,
|
||||
},
|
||||
{
|
||||
title: '电视剧',
|
||||
full_title: '电视剧订阅',
|
||||
icon: 'mdi-television',
|
||||
to: '/subscribe/tv',
|
||||
header: '订阅',
|
||||
admin: false,
|
||||
footer: false,
|
||||
},
|
||||
|
||||
{
|
||||
title: '工作流',
|
||||
full_title: '工作流',
|
||||
icon: 'mdi-state-machine',
|
||||
to: '/workflow',
|
||||
header: '订阅',
|
||||
admin: true,
|
||||
footer: false,
|
||||
},
|
||||
{
|
||||
title: '日历',
|
||||
full_title: '订阅日历',
|
||||
icon: 'mdi-calendar',
|
||||
to: '/calendar',
|
||||
header: '订阅',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '下载管理',
|
||||
icon: 'mdi-download-outline',
|
||||
to: '/downloading',
|
||||
header: '整理',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '媒体整理',
|
||||
icon: 'mdi-folder-play-outline',
|
||||
to: '/history',
|
||||
header: '整理',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '文件管理',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
to: '/filemanager',
|
||||
header: '整理',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '插件',
|
||||
icon: 'mdi-puzzle-outline',
|
||||
to: '/plugins',
|
||||
header: '系统',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '站点管理',
|
||||
icon: 'mdi-web',
|
||||
to: '/site',
|
||||
header: '系统',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '用户管理',
|
||||
icon: 'mdi-account-group-outline',
|
||||
to: '/user',
|
||||
header: '系统',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '设定',
|
||||
icon: 'mdi-cog-outline',
|
||||
to: '/setting',
|
||||
header: '系统',
|
||||
admin: true,
|
||||
},
|
||||
]
|
||||
|
||||
// 设定标签页
|
||||
export const SettingTabs = [
|
||||
{
|
||||
title: '系统',
|
||||
icon: 'mdi-server-network',
|
||||
tab: 'system',
|
||||
description: '基础设置、下载器(Qbittorrent、Transmission)、媒体服务器(Emby、Jellyfin、Plex)',
|
||||
},
|
||||
{
|
||||
title: '存储 & 目录',
|
||||
icon: 'mdi-folder',
|
||||
tab: 'directory',
|
||||
description: '下载目录、媒体库目录、整理、刮削',
|
||||
},
|
||||
{
|
||||
title: '站点',
|
||||
icon: 'mdi-web',
|
||||
tab: 'site',
|
||||
description: '站点同步、站点数据刷新、站点重置',
|
||||
},
|
||||
{
|
||||
title: '规则',
|
||||
icon: 'mdi-filter',
|
||||
tab: 'rule',
|
||||
description: '自定义规则、优先级规则组、下载规则',
|
||||
},
|
||||
{
|
||||
title: '搜索 & 下载',
|
||||
icon: 'mdi-magnify',
|
||||
tab: 'search',
|
||||
description: '搜索数据源(TheMovieDb、豆瓣、Bangumi)、下载任务标签、搜索站点',
|
||||
},
|
||||
{
|
||||
title: '订阅',
|
||||
icon: 'mdi-rss',
|
||||
tab: 'subscribe',
|
||||
description: '订阅站点、订阅模式、订阅规则、洗版规则',
|
||||
},
|
||||
{
|
||||
title: '服务',
|
||||
icon: 'mdi-list-box',
|
||||
tab: 'scheduler',
|
||||
description: '定时作业',
|
||||
},
|
||||
{
|
||||
title: '通知',
|
||||
icon: 'mdi-bell',
|
||||
tab: 'notification',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息发送范围',
|
||||
},
|
||||
{
|
||||
title: '词表',
|
||||
icon: 'mdi-file-word-box',
|
||||
tab: 'words',
|
||||
description: '自定义识别词、自定义制作组/字幕组、自定义占位符、文件整理屏蔽词',
|
||||
},
|
||||
{
|
||||
title: '关于',
|
||||
icon: 'mdi-information',
|
||||
tab: 'about',
|
||||
description: '软件版本',
|
||||
},
|
||||
]
|
||||
|
||||
// 电影订阅标签页
|
||||
export const SubscribeMovieTabs = [
|
||||
{
|
||||
title: '我的订阅',
|
||||
tab: 'mysub',
|
||||
icon: 'mdi-bell-check',
|
||||
},
|
||||
{
|
||||
title: '热门订阅',
|
||||
tab: 'popular',
|
||||
icon: 'mdi-fire',
|
||||
},
|
||||
]
|
||||
|
||||
// 电视剧订阅标签页
|
||||
export const SubscribeTvTabs = [
|
||||
{
|
||||
title: '我的订阅',
|
||||
tab: 'mysub',
|
||||
icon: 'mdi-bell-check',
|
||||
},
|
||||
{
|
||||
title: '热门订阅',
|
||||
tab: 'popular',
|
||||
icon: 'mdi-fire',
|
||||
},
|
||||
{
|
||||
title: '订阅分享',
|
||||
tab: 'share',
|
||||
icon: 'mdi-share-variant',
|
||||
},
|
||||
]
|
||||
|
||||
// 插件标签页
|
||||
export const PluginTabs = [
|
||||
{
|
||||
title: '我的插件',
|
||||
tab: 'installed',
|
||||
icon: 'mdi-puzzle',
|
||||
},
|
||||
{
|
||||
title: '插件市场',
|
||||
tab: 'market',
|
||||
icon: 'mdi-shopping',
|
||||
},
|
||||
]
|
||||
|
||||
// 发现标签页
|
||||
export const DiscoverTabs = [
|
||||
{
|
||||
name: 'TheMovieDb',
|
||||
tab: 'themoviedb',
|
||||
icon: 'themoviedb',
|
||||
},
|
||||
{
|
||||
name: '豆瓣',
|
||||
tab: 'douban',
|
||||
icon: 'douban',
|
||||
},
|
||||
{
|
||||
name: 'Bangumi',
|
||||
tab: 'bangumi',
|
||||
icon: 'bangumi',
|
||||
},
|
||||
]
|
||||
@@ -9,7 +9,7 @@ import PluginCard from '@/components/cards/PluginCard.vue'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { PluginTabs } from '@/router/menu'
|
||||
import { getPluginTabs } from '@/router/i18n-menu'
|
||||
import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDialog.vue'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
|
||||
@@ -22,7 +22,10 @@ const display = useDisplay()
|
||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref('我的插件')
|
||||
const activeTab = ref('installed')
|
||||
|
||||
// 获取插件标签页
|
||||
const pluginTabs = computed(() => getPluginTabs())
|
||||
|
||||
// 插件ID参数
|
||||
const pluginId = ref(route.query.id)
|
||||
@@ -326,7 +329,7 @@ async function fetchUninstalledPlugins() {
|
||||
loading.value = false
|
||||
isRefreshed.value = true
|
||||
// 更新插件市场列表
|
||||
// 排除已安装且有更新的,上面的问题在于“本地存在未安装的旧版本插件且云端有更新时”不会在插件市场展示
|
||||
// 排除已安装且有更新的,上面的问题在于"本地存在未安装的旧版本插件且云端有更新时"不会在插件市场展示
|
||||
marketList.value = uninstalledList.value.filter(item => !(item.has_update && item.installed))
|
||||
// 初始化过滤选项
|
||||
marketList.value.forEach(initOptions)
|
||||
@@ -467,10 +470,10 @@ useDynamicButton({
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VHeaderTab :items="PluginTabs" v-model="activeTab">
|
||||
<VHeaderTab :items="pluginTabs" v-model="activeTab">
|
||||
<template #append>
|
||||
<VMenu
|
||||
v-if="activeTab === '我的插件'"
|
||||
v-if="activeTab === 'installed'"
|
||||
v-model="filterInstalledPluginDialog"
|
||||
width="20rem"
|
||||
:close-on-content-click="false"
|
||||
@@ -513,7 +516,7 @@ useDynamicButton({
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<VMenu
|
||||
v-if="activeTab === '插件市场'"
|
||||
v-if="activeTab === 'market'"
|
||||
v-model="filterMarketPluginDialog"
|
||||
width="25rem"
|
||||
:close-on-content-click="false"
|
||||
@@ -586,7 +589,7 @@ useDynamicButton({
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<VBtn
|
||||
v-if="activeTab === '插件市场'"
|
||||
v-if="activeTab === 'market'"
|
||||
icon="mdi-store-cog"
|
||||
variant="text"
|
||||
color="gray"
|
||||
@@ -599,7 +602,7 @@ useDynamicButton({
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<!-- 我的插件 -->
|
||||
<VWindowItem value="我的插件">
|
||||
<VWindowItem value="installed">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<VPageContentTitle v-if="installedFilter" :title="`筛选:${installedFilter}`" />
|
||||
@@ -638,7 +641,7 @@ useDynamicButton({
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- 插件市场 -->
|
||||
<VWindowItem value="插件市场">
|
||||
<VWindowItem value="market">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
|
||||
|
||||
@@ -6,6 +6,8 @@ import Components from 'unplugin-vue-components/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import vuetify from 'vite-plugin-vuetify'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -23,9 +25,12 @@ export default defineConfig({
|
||||
dts: true,
|
||||
}),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router', '@vueuse/core', '@vueuse/math', 'pinia'],
|
||||
imports: ['vue', 'vue-router', '@vueuse/core', '@vueuse/math', 'pinia', 'vue-i18n'],
|
||||
vueTemplate: true,
|
||||
}),
|
||||
VueI18n({
|
||||
include: [resolve(__dirname, 'src/locales/**')],
|
||||
}),
|
||||
VitePWA({
|
||||
injectRegister: 'script',
|
||||
registerType: 'autoUpdate',
|
||||
|
||||
Reference in New Issue
Block a user