删除语言和主题切换组件,整合相关功能至用户个人资料组件中

This commit is contained in:
jxxghp
2025-04-28 15:43:07 +08:00
parent 6be4694327
commit 43460d4198
8 changed files with 315 additions and 306 deletions
+285 -1
View File
@@ -8,6 +8,12 @@ import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
import { useAuthStore, useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
import { useDisplay, useTheme } from 'vuetify'
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
import { saveLocalTheme } from '@/@core/utils/theme'
import type { ThemeSwitcherTheme } from '@layouts/types'
// 认证 Store
const authStore = useAuthStore()
@@ -15,6 +21,8 @@ const authStore = useAuthStore()
const userStore = useUserStore()
// 国际化
const { t } = useI18n()
// 显示器
const display = useDisplay()
// 确认框
const createConfirm = useConfirm()
@@ -31,6 +39,18 @@ const siteAuthDialog = ref(false)
// 重启确认对话框
const restartDialog = ref(false)
// 自定义CSS弹窗
const cssDialog = ref(false)
// 主题菜单是否显示
const showThemeMenu = ref(false)
// 语言菜单是否显示
const showLanguageMenu = ref(false)
// 自定义CSS
const customCSS = ref('')
// 执行注销操作
function logout() {
// 清除登录状态信息
@@ -82,13 +102,172 @@ const superUser = computed(() => userStore.superUser)
const userName = computed(() => userStore.userName)
const avatar = computed(() => userStore.avatar || avatar1)
const userLevel = computed(() => userStore.level)
// 主题相关功能
const { name: themeName, global: globalTheme } = useTheme()
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
const currentThemeName = ref(savedTheme.value)
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',
},
]
// 编辑器主题
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
// 更新主题
function updateTheme() {
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
globalTheme.name.value = theme
// 保存原始主题设置,而不是计算后的值
savedTheme.value = currentThemeName.value
// 保存主题到本地
saveLocalTheme(currentThemeName.value, globalTheme)
// 刷新页面
location.reload()
}
// 切换主题
function changeTheme(theme: string) {
currentThemeName.value = theme
showThemeMenu.value = false
// 保存主题到服务端
try {
api.post('/user/config/Layout', {
theme,
})
} catch (e) {
console.error(e)
}
}
// 获取自定义 CSS
async function getCustomCSS() {
try {
const result: { [key: string]: any } = await api.get('system/setting/UserCustomCSS')
if (result && result.success && result.data?.value) {
customCSS.value = result.data?.value ?? ''
if (customCSS.value) {
const style = document.createElement('style')
style.innerHTML = result.data?.value ?? ''
document.head.appendChild(style)
}
}
} catch (error) {
console.error(error)
}
}
// 保存自定义 CSS
async function saveCustomCSS() {
cssDialog.value = false
try {
const result: { [key: string]: any } = await api.post('system/setting/UserCustomCSS', customCSS.value, {
headers: {
'Content-Type': 'text/plain',
},
})
if (result.success) $toast.success(t('theme.customCssSaveSuccess'))
} catch (e) {
console.error(t('theme.customCssSaveFailed'))
}
}
// 监听主题变化
watch(
() => currentThemeName.value,
() => updateTheme(),
)
// 监听系统主题变化
try {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
} catch (e) {
console.error(t('theme.deviceNotSupport'))
}
// 语言相关功能
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) {
showLanguageMenu.value = false
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 || '🌐'
})
// 获取当前主题图标
const getThemeIcon = computed(() => {
const theme = themes.find(t => t.name === currentThemeName.value)
return theme?.icon || 'mdi-laptop'
})
onMounted(() => {
getCustomCSS()
})
</script>
<template>
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
<VImg :src="avatar" />
<VMenu activator="parent" width="230" location="bottom end" offset="14px" class="user-menu" scrim>
<VMenu
activator="parent"
width="15rem"
location="bottom end"
offset="14px"
class="user-menu"
:close-on-content-click="true"
scrim
>
<VList class="pt-0">
<!-- 👉 User Avatar & Name -->
<VListItem class="py-4" bg-color="primary" bg-opacity="0.05">
@@ -131,6 +310,80 @@ const userLevel = computed(() => userStore.level)
<VListItemTitle>{{ t('user.siteAuth') }}</VListItemTitle>
</VListItem>
<!-- 👉 主题设置 - 使用嵌套菜单 -->
<VMenu location="end" offset-x min-width="200" v-model="showThemeMenu" :close-on-content-click="true">
<template v-slot:activator="{ props: menuProps }">
<VListItem v-bind="menuProps" class="mb-1 rounded-lg" hover>
<template #prepend>
<VIcon :icon="getThemeIcon" />
</template>
<VListItemTitle>
{{ themes.find(t => t.name === currentThemeName)?.title || t('common.theme') }}
</VListItemTitle>
<template #append>
<VIcon icon="mdi-chevron-right" size="small" />
</template>
</VListItem>
</template>
<VList>
<VListItem
v-for="theme in themes"
:key="theme.name"
@click="changeTheme(theme.name)"
:active="currentThemeName === theme.name"
class="mb-1"
>
<template #prepend>
<VIcon :icon="theme.icon" />
</template>
<VListItemTitle>{{ theme.title }}</VListItemTitle>
<template #append v-if="currentThemeName === theme.name">
<VIcon icon="mdi-check" color="primary" size="small" />
</template>
</VListItem>
<VListItem @click="cssDialog = true">
<template #prepend>
<VIcon icon="mdi-palette" />
</template>
<VListItemTitle>{{ t('theme.custom') }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
<!-- 👉 语言设置 - 使用嵌套菜单 -->
<VMenu location="end" offset-x min-width="200" v-model="showLanguageMenu" :close-on-content-click="true">
<template v-slot:activator="{ props: menuProps }">
<VListItem v-bind="menuProps" class="mb-1 rounded-lg" hover>
<template #prepend>
<span class="me-4">{{ getCurrentIcon }}</span>
</template>
<VListItemTitle>
{{ locales.find(l => l.value === currentLocale)?.title || t('common.language') }}
</VListItemTitle>
<template #append>
<VIcon icon="mdi-chevron-right" size="small" />
</template>
</VListItem>
</template>
<VList>
<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>
</VList>
</VMenu>
<!-- 👉 FAQ -->
<VListItem href="https://wiki.movie-pilot.org" target="_blank" class="mb-1 rounded-lg" hover>
<template #prepend>
@@ -163,6 +416,7 @@ const userLevel = computed(() => userStore.level)
</VMenu>
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" />
<!-- 用户认证对话框 -->
@@ -192,4 +446,34 @@ const userLevel = computed(() => userStore.level)
<VDialogCloseBtn @click="restartDialog = false" />
</VCard>
</VDialog>
<!-- 自定义 CSS -->
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-palette" class="me-2" />
{{ t('theme.custom') }}
</VCardTitle>
<VDialogCloseBtn @click="cssDialog = false" />
</VCardItem>
<VDivider />
<VAceEditor v-model:value="customCSS" lang="css" :theme="editorTheme" class="w-full min-h-[30rem]" />
<VDivider />
<VCardText class="text-center">
<VBtn @click="saveCustomCSS" class="w-1/2">
<template #prepend>
<VIcon icon="mdi-content-save" />
</template>
{{ t('common.save') }}
</VBtn>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>
.v-list-item__prepend {
min-inline-size: 24px !important;
}
</style>