fix layout ui

This commit is contained in:
jxxghp
2024-05-11 12:53:42 +08:00
parent f227ae89ec
commit 2c35d0f897
11 changed files with 183 additions and 66 deletions

View File

@@ -13,11 +13,7 @@ const { name: themeName, global: globalTheme } = useTheme()
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
const {
state: currentThemeName,
next: getNextThemeName,
index: currentThemeIndex,
} = useCycleList(
const { state: currentThemeName, next: getNextThemeName } = useCycleList(
props.themes.map(t => t.name),
{ initialValue: savedTheme.value },
)
@@ -90,15 +86,16 @@ function updateTheme() {
globalTheme.name.value = theme
savedTheme.value = theme
themeTransition()
// 保存主题到本地
localStorage.setItem('theme', theme)
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
}
// 切换主题
function changeTheme() {
const nextTheme = getNextThemeName()
function changeTheme(theme: string) {
let nextTheme = theme
if (!theme) nextTheme = getNextThemeName()
currentThemeName.value = nextTheme
// 保存主题到本地
localStorage.setItem('theme', nextTheme)
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
// 保存主题到服务端
try {
api.post('/user/config/theme', nextTheme, {
@@ -126,6 +123,12 @@ try {
console.error('当前设备不支持监听系统主题变化')
}
// 查询当前主题的图标
const getThemeIcon = computed(() => {
const theme = props.themes.find(t => t.name === currentThemeName.value)
return theme?.icon ?? 'mdi-circle'
})
// 监听设置主题变化
watch(
() => currentThemeName.value,
@@ -134,9 +137,21 @@ watch(
</script>
<template>
<IconBtn @click="changeTheme">
<VIcon :icon="props.themes[currentThemeIndex].icon" />
</IconBtn>
<VMenu v-if="props.themes">
<template v-slot:activator="{ props }">
<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>
<style lang="sass">

View File

@@ -6,19 +6,19 @@ export interface UserConfig {
app: {
title: Lowercase<string>
logo: VNode
contentWidth: typeof ContentWidth[keyof typeof ContentWidth]
contentLayoutNav: typeof AppContentLayoutNav[keyof typeof AppContentLayoutNav]
contentWidth: (typeof ContentWidth)[keyof typeof ContentWidth]
contentLayoutNav: (typeof AppContentLayoutNav)[keyof typeof AppContentLayoutNav]
overlayNavFromBreakpoint: number
enableI18n: boolean
isRtl: boolean
iconRenderer?: Component
}
navbar: {
type: typeof NavbarType[keyof typeof NavbarType]
type: (typeof NavbarType)[keyof typeof NavbarType]
navbarBlur: boolean
}
footer: {
type:typeof FooterType[keyof typeof FooterType]
type: (typeof FooterType)[keyof typeof FooterType]
}
verticalNav: {
isVerticalNavCollapsed: boolean
@@ -143,7 +143,7 @@ interface I18nLanguage {
// avatar | text | icon
// Thanks: https://stackoverflow.com/a/60617060/10796681
type Notification = {
id:number
id: number
title: string
subtitle: string
time: string
@@ -157,5 +157,6 @@ type Notification = {
interface ThemeSwitcherTheme {
name: string
title: string
icon: string
}

View File

@@ -1,15 +1,9 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify'
import api from '@/api'
import store from './store'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
const { global: globalTheme } = useTheme()
// 提示框
const $toast = useToast()
// 生效主题
async function setTheme() {
let themeValue = localStorage.getItem('theme') || 'light'
@@ -17,27 +11,9 @@ async function setTheme() {
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 () => {
setTheme()
startSSEMessager()
})
</script>

View File

@@ -806,3 +806,15 @@ export interface Message {
// JSON
note?: string
}
// 系统通知
export interface SystemNotification {
// 通知类型 user/system/plugin
type: string
// 通知标题
title: string
// 通知内容
text: string
// 通知时间
date: string
}

View File

@@ -6,6 +6,7 @@ import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.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'
@@ -34,7 +35,10 @@ const superUser = store.state.auth.superUser
<ShortcutBar v-if="superUser" />
<!-- 👉 Theme -->
<NavbarThemeSwitcher class="me-2" />
<NavbarThemeSwitcher />
<!-- 👉 Notification -->
<UserNofification />
<!-- 👉 UserProfile -->
<UserProfile />

View File

@@ -2,25 +2,29 @@
import type { ThemeSwitcherTheme } from '@layouts/types'
const themes: ThemeSwitcherTheme[] = [
{
name: 'auto',
title: '跟随系统',
icon: 'mdi-laptop',
},
{
name: 'light',
title: '明亮',
icon: 'mdi-weather-sunny',
},
{
name: 'dark',
title: '暗黑',
icon: 'mdi-weather-night',
},
{
name: 'purple',
title: '紫韵幽兰',
icon: 'mdi-brightness-4',
},
{
name: 'auto',
icon: 'mdi-brightness-auto',
},
]
</script>
<template>
<ThemeSwitcher :themes="themes" />
<ThemeSwitcher class="ms-2" :themes="themes" />
</template>

View File

@@ -91,7 +91,7 @@ onMounted(() => {
>
<!-- Menu Activator -->
<template #activator="{ props }">
<IconBtn class="me-2" v-bind="props">
<IconBtn class="ms-2" v-bind="props">
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
</IconBtn>
</template>

View File

@@ -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>

View File

@@ -64,7 +64,7 @@ const avatar = store.state.auth.avatar
</script>
<template>
<VAvatar class="cursor-pointer" color="primary" variant="tonal">
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
<VImg :src="avatar ?? avatar1" />
<!-- SECTION Menu -->
@@ -92,10 +92,17 @@ const avatar = store.state.auth.avatar
<template #prepend>
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
</template>
<VListItemTitle>设定</VListItemTitle>
</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 -->
<VDivider class="my-2" />
@@ -104,26 +111,18 @@ const avatar = store.state.auth.avatar
<template #prepend>
<VIcon class="me-2" icon="mdi-restart" size="22" />
</template>
<VListItemTitle>重启</VListItemTitle>
</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 -->
<VDivider class="my-2" />
<!-- 👉 Logout -->
<VListItem @click="logout">
<template #prepend>
<VIcon class="me-2" icon="mdi-logout" size="22" />
</template>
<VListItemTitle>注销</VListItemTitle>
<VBtn color="error" block>
<template #append> <VIcon size="small" icon="mdi-logout" /> </template>
退出登录
</VBtn>
</VListItem>
</VList>
</VMenu>

View File

@@ -255,7 +255,7 @@ onBeforeMount(async () => {
<VDivider />
<VCardText>
<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" />
</VCol>
</VRow>

View File

@@ -135,3 +135,7 @@
border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity));
opacity:0.75;
}
.apexcharts-title-text {
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
}