mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 07:28:37 +08:00
fix layout ui
This commit is contained in:
@@ -13,11 +13,7 @@ const { name: themeName, global: globalTheme } = useTheme()
|
|||||||
|
|
||||||
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
||||||
|
|
||||||
const {
|
const { state: currentThemeName, next: getNextThemeName } = useCycleList(
|
||||||
state: currentThemeName,
|
|
||||||
next: getNextThemeName,
|
|
||||||
index: currentThemeIndex,
|
|
||||||
} = useCycleList(
|
|
||||||
props.themes.map(t => t.name),
|
props.themes.map(t => t.name),
|
||||||
{ initialValue: savedTheme.value },
|
{ initialValue: savedTheme.value },
|
||||||
)
|
)
|
||||||
@@ -90,15 +86,16 @@ function updateTheme() {
|
|||||||
globalTheme.name.value = theme
|
globalTheme.name.value = theme
|
||||||
savedTheme.value = theme
|
savedTheme.value = theme
|
||||||
themeTransition()
|
themeTransition()
|
||||||
|
// 保存主题到本地
|
||||||
|
localStorage.setItem('theme', theme)
|
||||||
|
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换主题
|
// 切换主题
|
||||||
function changeTheme() {
|
function changeTheme(theme: string) {
|
||||||
const nextTheme = getNextThemeName()
|
let nextTheme = theme
|
||||||
|
if (!theme) nextTheme = getNextThemeName()
|
||||||
currentThemeName.value = nextTheme
|
currentThemeName.value = nextTheme
|
||||||
// 保存主题到本地
|
|
||||||
localStorage.setItem('theme', nextTheme)
|
|
||||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
|
||||||
// 保存主题到服务端
|
// 保存主题到服务端
|
||||||
try {
|
try {
|
||||||
api.post('/user/config/theme', nextTheme, {
|
api.post('/user/config/theme', nextTheme, {
|
||||||
@@ -126,6 +123,12 @@ try {
|
|||||||
console.error('当前设备不支持监听系统主题变化')
|
console.error('当前设备不支持监听系统主题变化')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询当前主题的图标
|
||||||
|
const getThemeIcon = computed(() => {
|
||||||
|
const theme = props.themes.find(t => t.name === currentThemeName.value)
|
||||||
|
return theme?.icon ?? 'mdi-circle'
|
||||||
|
})
|
||||||
|
|
||||||
// 监听设置主题变化
|
// 监听设置主题变化
|
||||||
watch(
|
watch(
|
||||||
() => currentThemeName.value,
|
() => currentThemeName.value,
|
||||||
@@ -134,9 +137,21 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<IconBtn @click="changeTheme">
|
<VMenu v-if="props.themes">
|
||||||
<VIcon :icon="props.themes[currentThemeIndex].icon" />
|
<template v-slot:activator="{ props }">
|
||||||
</IconBtn>
|
<IconBtn v-bind="props">
|
||||||
|
<VIcon :icon="getThemeIcon" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
<VList>
|
||||||
|
<VListItem v-for="theme in props.themes" :key="theme.name" @click="changeTheme(theme.name)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon :icon="theme.icon" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ theme.title }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
|
|||||||
Vendored
+6
-5
@@ -6,19 +6,19 @@ export interface UserConfig {
|
|||||||
app: {
|
app: {
|
||||||
title: Lowercase<string>
|
title: Lowercase<string>
|
||||||
logo: VNode
|
logo: VNode
|
||||||
contentWidth: typeof ContentWidth[keyof typeof ContentWidth]
|
contentWidth: (typeof ContentWidth)[keyof typeof ContentWidth]
|
||||||
contentLayoutNav: typeof AppContentLayoutNav[keyof typeof AppContentLayoutNav]
|
contentLayoutNav: (typeof AppContentLayoutNav)[keyof typeof AppContentLayoutNav]
|
||||||
overlayNavFromBreakpoint: number
|
overlayNavFromBreakpoint: number
|
||||||
enableI18n: boolean
|
enableI18n: boolean
|
||||||
isRtl: boolean
|
isRtl: boolean
|
||||||
iconRenderer?: Component
|
iconRenderer?: Component
|
||||||
}
|
}
|
||||||
navbar: {
|
navbar: {
|
||||||
type: typeof NavbarType[keyof typeof NavbarType]
|
type: (typeof NavbarType)[keyof typeof NavbarType]
|
||||||
navbarBlur: boolean
|
navbarBlur: boolean
|
||||||
}
|
}
|
||||||
footer: {
|
footer: {
|
||||||
type:typeof FooterType[keyof typeof FooterType]
|
type: (typeof FooterType)[keyof typeof FooterType]
|
||||||
}
|
}
|
||||||
verticalNav: {
|
verticalNav: {
|
||||||
isVerticalNavCollapsed: boolean
|
isVerticalNavCollapsed: boolean
|
||||||
@@ -143,7 +143,7 @@ interface I18nLanguage {
|
|||||||
// avatar | text | icon
|
// avatar | text | icon
|
||||||
// Thanks: https://stackoverflow.com/a/60617060/10796681
|
// Thanks: https://stackoverflow.com/a/60617060/10796681
|
||||||
type Notification = {
|
type Notification = {
|
||||||
id:number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
subtitle: string
|
subtitle: string
|
||||||
time: string
|
time: string
|
||||||
@@ -157,5 +157,6 @@ type Notification = {
|
|||||||
|
|
||||||
interface ThemeSwitcherTheme {
|
interface ThemeSwitcherTheme {
|
||||||
name: string
|
name: string
|
||||||
|
title: string
|
||||||
icon: string
|
icon: string
|
||||||
}
|
}
|
||||||
|
|||||||
-24
@@ -1,15 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import api from '@/api'
|
|
||||||
import store from './store'
|
|
||||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||||
|
|
||||||
const { global: globalTheme } = useTheme()
|
const { global: globalTheme } = useTheme()
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 生效主题
|
// 生效主题
|
||||||
async function setTheme() {
|
async function setTheme() {
|
||||||
let themeValue = localStorage.getItem('theme') || 'light'
|
let themeValue = localStorage.getItem('theme') || 'light'
|
||||||
@@ -17,27 +11,9 @@ async function setTheme() {
|
|||||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSE持续接收消息
|
|
||||||
function startSSEMessager() {
|
|
||||||
const token = store.state.auth.token
|
|
||||||
if (token) {
|
|
||||||
const eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`)
|
|
||||||
|
|
||||||
eventSource.addEventListener('message', event => {
|
|
||||||
const message = event.data
|
|
||||||
if (message) $toast.info(message)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
eventSource.close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载时,加载当前用户数据
|
// 页面加载时,加载当前用户数据
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
setTheme()
|
setTheme()
|
||||||
startSSEMessager()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -806,3 +806,15 @@ export interface Message {
|
|||||||
// JSON
|
// JSON
|
||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统通知
|
||||||
|
export interface SystemNotification {
|
||||||
|
// 通知类型 user/system/plugin
|
||||||
|
type: string
|
||||||
|
// 通知标题
|
||||||
|
title: string
|
||||||
|
// 通知内容
|
||||||
|
text: string
|
||||||
|
// 通知时间
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
|
|||||||
// Components
|
// Components
|
||||||
import Footer from '@/layouts/components/Footer.vue'
|
import Footer from '@/layouts/components/Footer.vue'
|
||||||
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
|
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
|
||||||
|
import UserNofification from '@/layouts/components/UserNotification.vue'
|
||||||
import SearchBar from '@/layouts/components/SearchBar.vue'
|
import SearchBar from '@/layouts/components/SearchBar.vue'
|
||||||
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||||
@@ -34,7 +35,10 @@ const superUser = store.state.auth.superUser
|
|||||||
<ShortcutBar v-if="superUser" />
|
<ShortcutBar v-if="superUser" />
|
||||||
|
|
||||||
<!-- 👉 Theme -->
|
<!-- 👉 Theme -->
|
||||||
<NavbarThemeSwitcher class="me-2" />
|
<NavbarThemeSwitcher />
|
||||||
|
|
||||||
|
<!-- 👉 Notification -->
|
||||||
|
<UserNofification />
|
||||||
|
|
||||||
<!-- 👉 UserProfile -->
|
<!-- 👉 UserProfile -->
|
||||||
<UserProfile />
|
<UserProfile />
|
||||||
|
|||||||
@@ -2,25 +2,29 @@
|
|||||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||||
|
|
||||||
const themes: ThemeSwitcherTheme[] = [
|
const themes: ThemeSwitcherTheme[] = [
|
||||||
|
{
|
||||||
|
name: 'auto',
|
||||||
|
title: '跟随系统',
|
||||||
|
icon: 'mdi-laptop',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'light',
|
name: 'light',
|
||||||
|
title: '明亮',
|
||||||
icon: 'mdi-weather-sunny',
|
icon: 'mdi-weather-sunny',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dark',
|
name: 'dark',
|
||||||
|
title: '暗黑',
|
||||||
icon: 'mdi-weather-night',
|
icon: 'mdi-weather-night',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'purple',
|
name: 'purple',
|
||||||
|
title: '紫韵幽兰',
|
||||||
icon: 'mdi-brightness-4',
|
icon: 'mdi-brightness-4',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'auto',
|
|
||||||
icon: 'mdi-brightness-auto',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ThemeSwitcher :themes="themes" />
|
<ThemeSwitcher class="ms-2" :themes="themes" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<!-- Menu Activator -->
|
<!-- Menu Activator -->
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn class="me-2" v-bind="props">
|
<IconBtn class="ms-2" v-bind="props">
|
||||||
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
|
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import store from '@/store'
|
||||||
|
import { formatDateDifference } from '@core/utils/formatters'
|
||||||
|
import { SystemNotification } from '@/api/types'
|
||||||
|
|
||||||
|
// 是否有新消息
|
||||||
|
const hasNewMessage = ref(false)
|
||||||
|
|
||||||
|
// 通知列表
|
||||||
|
const notificationList = ref<SystemNotification[]>([])
|
||||||
|
|
||||||
|
// 事件源
|
||||||
|
let eventSource: EventSource | null = null
|
||||||
|
|
||||||
|
// 弹窗
|
||||||
|
const appsMenu = ref(false)
|
||||||
|
|
||||||
|
// SSE持续接收消息
|
||||||
|
function startSSEMessager() {
|
||||||
|
const token = store.state.auth.token
|
||||||
|
if (token) {
|
||||||
|
eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/message?token=${token}`)
|
||||||
|
eventSource.addEventListener('message', event => {
|
||||||
|
if (event.data) {
|
||||||
|
const noti: SystemNotification = JSON.parse(event.data)
|
||||||
|
notificationList.value.unshift(noti)
|
||||||
|
hasNewMessage.value = true
|
||||||
|
// TODO 在顶部显示消息汽泡
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时,加载当前用户数据
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
startSSEMessager()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载时,关闭事件源
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (eventSource) eventSource.close()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VMenu v-model="appsMenu" width="400" transition="scale-transition" close-on-content-click>
|
||||||
|
<!-- Menu Activator -->
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<VBadge v-if="hasNewMessage" dot color="error" :offset-x="5" :offset-y="5" v-bind="props">
|
||||||
|
<IconBtn>
|
||||||
|
<VIcon icon="mdi-bell-outline" />
|
||||||
|
</IconBtn>
|
||||||
|
</VBadge>
|
||||||
|
<IconBtn v-else v-bind="props">
|
||||||
|
<VIcon icon="mdi-bell-outline" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
<!-- Menu Content -->
|
||||||
|
<VCard>
|
||||||
|
<VCardItem class="border-b">
|
||||||
|
<VCardTitle>通知</VCardTitle>
|
||||||
|
<template #append>
|
||||||
|
<VTooltip text="设为已读">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<IconBtn
|
||||||
|
v-bind="props"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
hasNewMessage = false
|
||||||
|
appsMenu = false
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-email-mark-as-unread" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
</template>
|
||||||
|
</VCardItem>
|
||||||
|
<VList lines="two" v-if="notificationList.length > 0" max-height="600">
|
||||||
|
<VListItem v-for="(item, i) in notificationList" :key="i">
|
||||||
|
<template #prepend>
|
||||||
|
<VAvatar rounded>
|
||||||
|
<VIcon v-if="item.type === 'user'" icon="mdi-account-alert" size="large"></VIcon>
|
||||||
|
<VIcon v-else-if="item.type === 'plugin'" icon="mdi-robot-happy" size="large"></VIcon>
|
||||||
|
<VIcon v-else icon="mdi-laptop" size="large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VListItemTitle class="overflow-visiable break-words whitespace-break-spaces">
|
||||||
|
{{ item.title }}
|
||||||
|
</VListItemTitle>
|
||||||
|
<VListItemSubtitle class="mt-2">{{ item.text }}</VListItemSubtitle>
|
||||||
|
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
<VList v-else>
|
||||||
|
<VListItem>
|
||||||
|
<VListItemTitle class="text-center">暂无通知</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VCard>
|
||||||
|
</VMenu>
|
||||||
|
</template>
|
||||||
@@ -64,7 +64,7 @@ const avatar = store.state.auth.avatar
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VAvatar class="cursor-pointer" color="primary" variant="tonal">
|
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
|
||||||
<VImg :src="avatar ?? avatar1" />
|
<VImg :src="avatar ?? avatar1" />
|
||||||
|
|
||||||
<!-- SECTION Menu -->
|
<!-- SECTION Menu -->
|
||||||
@@ -92,10 +92,17 @@ const avatar = store.state.auth.avatar
|
|||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
|
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<VListItemTitle>设定</VListItemTitle>
|
<VListItemTitle>设定</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
|
|
||||||
|
<!-- 👉 FAQ -->
|
||||||
|
<VListItem href="https://github.com/jxxghp/MoviePilot/blob/main/README.md" target="_blank">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>帮助</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<VDivider class="my-2" />
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
@@ -104,26 +111,18 @@ const avatar = store.state.auth.avatar
|
|||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon class="me-2" icon="mdi-restart" size="22" />
|
<VIcon class="me-2" icon="mdi-restart" size="22" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<VListItemTitle>重启</VListItemTitle>
|
<VListItemTitle>重启</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
|
|
||||||
<!-- 👉 FAQ -->
|
<!-- Divider -->
|
||||||
<VListItem href="https://github.com/jxxghp/MoviePilot/blob/main/README.md" target="_blank">
|
<VDivider class="my-2" />
|
||||||
<template #prepend>
|
|
||||||
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<VListItemTitle>帮助</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
|
|
||||||
<!-- 👉 Logout -->
|
<!-- 👉 Logout -->
|
||||||
<VListItem @click="logout">
|
<VListItem @click="logout">
|
||||||
<template #prepend>
|
<VBtn color="error" block>
|
||||||
<VIcon class="me-2" icon="mdi-logout" size="22" />
|
<template #append> <VIcon size="small" icon="mdi-logout" /> </template>
|
||||||
</template>
|
退出登录
|
||||||
|
</VBtn>
|
||||||
<VListItemTitle>注销</VListItemTitle>
|
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ onBeforeMount(async () => {
|
|||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol v-for="item in dashboardConfigs" :key="item.id" cols="12" md="4" sm="4">
|
<VCol v-for="item in dashboardConfigs" :key="item.id" cols="6" md="4" sm="4">
|
||||||
<VCheckbox v-model="enableConfig[item.id]" :label="item.name" />
|
<VCheckbox v-model="enableConfig[item.id]" :label="item.name" />
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
|||||||
@@ -135,3 +135,7 @@
|
|||||||
border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity));
|
border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity));
|
||||||
opacity:0.75;
|
opacity:0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.apexcharts-title-text {
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user