Merge pull request #320 from madrays/v2

This commit is contained in:
jxxghp
2025-04-08 06:44:08 +08:00
committed by GitHub
14 changed files with 644 additions and 193 deletions

View File

@@ -18,10 +18,12 @@ const { name: themeName, global: globalTheme } = useTheme()
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
const { state: currentThemeName, next: getNextThemeName } = useCycleList(
props.themes.map(t => t.name),
{ initialValue: savedTheme.value },
)
const currentThemeName = ref(savedTheme.value)
const getNextThemeName = () => {
const currentIndex = props.themes.findIndex(t => t.name === currentThemeName.value)
const nextIndex = (currentIndex + 1) % props.themes.length
return props.themes[nextIndex].name
}
const $toast = useToast()
@@ -187,25 +189,47 @@ onMounted(() => {
</script>
<template>
<VMenu v-if="props.themes">
<VMenu v-if="props.themes" class="theme-menu">
<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>
<VListItem @click="cssDialog = true">
<template #prepend>
<VIcon icon="mdi-palette" />
</template>
<VListItemTitle>自定义</VListItemTitle>
</VListItem>
<VList elevation="0" class="theme-switcher-list">
<div class="theme-switcher-header px-3 py-3 mb-2">
<div class="text-primary text-h6 font-weight-medium">主题选择</div>
</div>
<div class="theme-switcher-options px-2">
<VListItem
v-for="theme in props.themes"
:key="theme.name"
@click="changeTheme(theme.name)"
class="theme-option"
:class="{ 'theme-option-active': currentThemeName === theme.name }"
>
<template #prepend>
<div class="theme-icon-wrapper">
<VIcon :icon="theme.icon" />
</div>
</template>
<VListItemTitle>{{ theme.title }}</VListItemTitle>
<template #append v-if="currentThemeName === theme.name">
<VIcon icon="mdi-check" color="primary" size="small" />
</template>
</VListItem>
<VDivider class="my-2" />
<VListItem @click="cssDialog = true" class="theme-option custom-theme-option">
<template #prepend>
<div class="theme-icon-wrapper custom-theme-icon">
<VIcon icon="mdi-palette" />
</div>
</template>
<VListItemTitle>自定义主题</VListItemTitle>
</VListItem>
</div>
</VList>
</VMenu>
<!-- 自定义 CSS -- -->
@@ -232,18 +256,68 @@ onMounted(() => {
</VDialog>
</template>
<style lang="sass">
// Theme transition
.app-copy
position: fixed !important
z-index: -1 !important
pointer-events: none !important
contain: size style !important
overflow: clip !important
<style lang="scss">
.theme-switcher-header {
border-bottom: 1px solid rgba(var(--v-theme-on-surface), 0.05);
}
.app-transition
--clip-size: 0
--clip-pos: 0 0
clip-path: circle(var(--clip-size) at var(--clip-pos))
transition: clip-path .35s ease-out
.theme-switcher-options {
max-height: 300px;
overflow-y: auto;
}
.theme-option {
border-radius: 8px;
margin: 4px 0;
transition: all 0.2s ease;
&:hover {
background-color: rgba(var(--v-theme-primary), 0.04);
transform: translateX(4px);
}
&.theme-option-active {
background-color: rgba(var(--v-theme-primary), 0.08);
}
}
.theme-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background-color: rgba(var(--v-theme-primary), 0.08);
margin-right: 12px;
transition: all 0.2s ease;
.v-icon {
color: rgba(var(--v-theme-primary), 0.9);
}
}
.custom-theme-icon {
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.15), rgba(var(--v-theme-info), 0.15));
.v-icon {
color: rgba(var(--v-theme-primary), 0.9);
}
}
// Theme transition
.app-copy {
position: fixed !important;
z-index: -1 !important;
pointer-events: none !important;
contain: size style !important;
overflow: clip !important;
}
.app-transition {
--clip-size: 0;
--clip-pos: 0 0;
clip-path: circle(var(--clip-size) at var(--clip-pos));
transition: clip-path .35s ease-out;
}
</style>

View File

@@ -18,6 +18,9 @@ $header: ".layout-navbar";
// Add transition
#{$header} {
transition: padding 0.2s ease, background-color 0.18s ease;
@extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav;
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
margin-bottom: 16px;
}
// If navbar is contained => Add border radius to header
@@ -27,21 +30,20 @@ $header: ".layout-navbar";
}
}
// Scrolled styles for sticky navbar
.navbar-blur#{$header} {
@extend %blurry-bg;
}
// Scrolled styles for sticky navbar (保留原有逻辑,但现在默认就会显示)
@at-root {
/* This html selector with not selector is required when:
dialog is opened and window don't have any scroll. This removes window-scrolled class from layout and out style broke
*/
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-fixed,
&.window-scrolled.layout-navbar-fixed {
#{$header} {
@extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav;
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
// 原有样式已在上面默认应用
}
.navbar-blur#{$header} {
@extend %blurry-bg;
// 原有样式已在上面默认应用
}
}
}
@@ -56,19 +58,12 @@ $header: ".layout-navbar";
}
}
&:not(.layout-navbar-fixed) {
#{$header} {
margin-block-start: variables.$vertical-nav-floating-navbar-top;
}
}
#{$header} {
@if variables.$layout-vertical-nav-navbar-is-contained {
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
}
background-color: rgb(var(--v-theme-surface));
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
}

View File

@@ -40,7 +40,7 @@ $theme-colors-name: (
$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
// 👉 Vertical nav
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
$vertical-nav-background-color-rgb: var(--v-theme-surface) !default;
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
// This is used to keep consistency between nav items and nav header left & right margin

View File

@@ -19,7 +19,7 @@
}
}
background-color: variables.$vertical-nav-background-color;
background-color: rgb(var(--v-theme-surface)) !important;
// 👉 Nav header
.nav-header {
@@ -60,9 +60,9 @@
z-index: 1;
background:
linear-gradient(
rgb(#{variables.$vertical-nav-background-color-rgb}) 5%,
rgba(#{variables.$vertical-nav-background-color-rgb}, 75%) 45%,
rgba(#{variables.$vertical-nav-background-color-rgb}, 20%) 80%,
rgb(var(--v-theme-surface)) 5%,
rgba(var(--v-theme-surface), 75%) 45%,
rgba(var(--v-theme-surface), 20%) 80%,
transparent
);
block-size: 55px;

View File

@@ -4,10 +4,13 @@
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
background-color: rgb(var(--v-theme-surface));
border-radius: 12px;
margin: 8px 8px 8px 0;
}
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
@include mixins.elevation(variables.$vertical-nav-navbar-elevation);
box-shadow: 0 4px 16px rgba(var(--v-theme-on-surface), 0.08);
// If navbar is contained => Squeeze navbar content on scroll
@if variables.$layout-vertical-nav-navbar-is-contained {

View File

@@ -0,0 +1,17 @@
// 更新顶栏浮动风格的样式,增强豆腐块效果
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
padding-block: 0.7rem;
background-color: rgb(var(--v-theme-surface));
box-shadow: 0 4px 16px rgba(var(--v-theme-on-surface), 0.06) !important;
border-radius: 12px;
margin: 8px 8px 0 0;
width: 100%;
z-index: 10;
position: relative;
}
// 更新毛玻璃背景效果
%blurry-bg {
background-color: rgba(var(--v-theme-surface), 0.9) !important;
backdrop-filter: blur(6px);
}

View File

@@ -98,6 +98,11 @@ function handleNavScroll(evt: Event) {
transition: transform 0.25s ease-in-out, inline-size 0.25s ease-in-out, box-shadow 0.25s ease-in-out;
visibility: hidden;
will-change: transform, inline-size;
background-color: rgb(var(--v-theme-surface));
box-shadow: 0 2px 8px rgba(var(--v-theme-on-surface), 0.08);
border-radius: 0 8px 8px 0;
margin: 8px 0 8px 8px;
max-height: calc(100% - 16px);
&:not(.overlay-nav) {
visibility: visible;

View File

@@ -116,7 +116,7 @@ export default defineComponent({
.layout-navbar {
position: fixed;
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
inset-block-start: 0;
@@ -171,7 +171,7 @@ export default defineComponent({
}
&:not(.layout-overlay-nav) .layout-content-wrapper {
padding-inline-start: variables.$layout-vertical-nav-width;
padding-inline-start: calc(variables.$layout-vertical-nav-width + 8px);
}
// Adjust right column pl when vertical nav is collapsed
@@ -203,7 +203,16 @@ export default defineComponent({
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
.layout-navbar {
width: 100%;
width: calc(100% - 0px);
margin-left: 8px;
padding-left: 0;
}
}
.layout-page-content {
margin-top: calc(variables.$layout-vertical-nav-navbar-height - 42px);
padding-top: 0px;
position: relative;
z-index: 1;
}
</style>

View File

@@ -9,7 +9,7 @@ defineProps<{
<template>
<li class="nav-link" :class="{ disabled: item.disable }">
<Component :is="item.to ? 'RouterLink' : 'a'" :to="item.to" :href="item.href">
<VIcon :icon="item.icon" class="nav-item-icon" />
<VIcon :icon="item.icon as string" class="nav-item-icon" />
<!-- 👉 Title -->
<span class="nav-item-title">
{{ item.title }}

View File

@@ -40,16 +40,18 @@ body,
padding-block: 1.5rem;
padding-top: calc(env(safe-area-inset-top) + 4.25rem);
// display: flex;
background-color: rgb(var(--v-theme-background));
.page-content-container {
// flex: 1;
display: flex;
background-color: rgb(var(--v-theme-background));
& > div:first-child {
flex: auto;
position: relative;
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
background-color: rgb(var(--v-theme-background));
}
}
}
@@ -57,6 +59,7 @@ body,
@media screen and (max-width: 1280px){
.page-content-container > div:first-child {
width: calc(100vw - 1rem) !important;
background-color: rgb(var(--v-theme-background));
}
}

View File

@@ -15,6 +15,9 @@ const display = useDisplay()
// App捷径
const appsMenu = ref(false)
// 菜单最大宽度
const menuMaxWidth = ref(480)
// 名称测试弹窗
const nameTestDialog = ref(false)
@@ -40,14 +43,13 @@ const user_message = ref('')
const sendButtonDisabled = ref(false)
// 聊天容器
const chatContainer = ref<HTMLDivElement>()
const chatContainer = ref<HTMLElement>()
// 滚动到底部
function scrollMessageToEnd() {
nextTick(() => {
if (chatContainer.value) {
const scrollDiv = chatContainer.value.$el
scrollDiv.scrollTop = scrollDiv.scrollHeight
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
}
})
}
@@ -103,91 +105,99 @@ onMounted(() => {
<template>
<VMenu
v-model="appsMenu"
max-width="600"
width="340"
:max-width="menuMaxWidth"
width="100%"
max-height="560"
location="top end"
origin="top end"
transition="scale-transition"
close-on-content-click
:close-on-content-click="false"
:close-on-back="true"
>
<!-- Menu Activator -->
<template #activator="{ props }">
<IconBtn class="ms-2" v-bind="props">
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
<VIcon icon="mdi-apps" />
</IconBtn>
</template>
<!-- Menu Content -->
<VCard elevation="1">
<VCardItem class="border-b">
<VCardTitle>捷径</VCardTitle>
<VCard elevation="1" class="shortcut-menu-card">
<VCardItem class="shortcut-header border-b">
<VCardTitle class="font-weight-medium text-primary">捷径</VCardTitle>
<template #append>
<IconBtn @click="() => {}">
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
<IconBtn @click="appsMenu = false" class="shortcut-close-btn">
<VIcon icon="mdi-close" />
</IconBtn>
</template>
</VCardItem>
<div class="ps ps--active-y">
<VRow class="ma-0 mt-n1">
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e">
<VListItem class="pa-4" @click="nameTestDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-text-recognition" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">识别</h6>
<span class="text-sm">名称识别测试</span>
</VListItem>
</VCol>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon" @click="() => {}">
<VListItem class="pa-4" @click="ruleTestDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-filter-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">规则</h6>
<span class="text-sm">规则测试</span>
</VListItem>
</VCol>
</VRow>
<VRow class="ma-0 mt-n1 border-t">
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e" @click="() => {}">
<VListItem class="pa-4" @click="loggingDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-file-document-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">日志</h6>
<span class="text-sm">实时日志</span>
</VListItem>
</VCol>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon" @click="() => {}">
<VListItem class="pa-4" @click="netTestDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-network-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">网络</h6>
<span class="text-sm">网速连通性测试</span>
</VListItem>
</VCol>
</VRow>
<VRow class="ma-0 mt-n1 border-t">
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon border-e" @click="() => {}">
<VListItem class="pa-4" @click="systemTestDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">系统</h6>
<span class="text-sm">健康检查</span>
</VListItem>
</VCol>
<VCol cols="6" class="text-center cursor-pointer pa-0 shortcut-icon" @click="() => {}">
<VListItem class="pa-4" @click="messageDialog = true">
<VAvatar size="48" variant="tonal">
<VIcon icon="mdi-message-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">消息</h6>
<span class="text-sm">消息中心</span>
</VListItem>
</VCol>
</VRow>
<div class="ps ps--active-y shortcut-menu-container">
<div class="shortcut-grid">
<!-- 识别 -->
<div class="shortcut-item" @click="nameTestDialog = true">
<div class="shortcut-icon-wrapper">
<VIcon icon="mdi-text-recognition" size="24" />
</div>
<div class="shortcut-text">
<div class="shortcut-title">识别</div>
<div class="shortcut-subtitle">名称识别测试</div>
</div>
</div>
<!-- 规则 -->
<div class="shortcut-item" @click="ruleTestDialog = true">
<div class="shortcut-icon-wrapper">
<VIcon icon="mdi-filter-cog" size="24" />
</div>
<div class="shortcut-text">
<div class="shortcut-title">规则</div>
<div class="shortcut-subtitle">规则测试</div>
</div>
</div>
<!-- 日志 -->
<div class="shortcut-item" @click="loggingDialog = true">
<div class="shortcut-icon-wrapper">
<VIcon icon="mdi-file-document" size="24" />
</div>
<div class="shortcut-text">
<div class="shortcut-title">日志</div>
<div class="shortcut-subtitle">实时日志</div>
</div>
</div>
<!-- 网络 -->
<div class="shortcut-item" @click="netTestDialog = true">
<div class="shortcut-icon-wrapper">
<VIcon icon="mdi-network" size="24" />
</div>
<div class="shortcut-text">
<div class="shortcut-title">网络</div>
<div class="shortcut-subtitle">网速连通性测试</div>
</div>
</div>
<!-- 系统 -->
<div class="shortcut-item" @click="systemTestDialog = true">
<div class="shortcut-icon-wrapper">
<VIcon icon="mdi-cog" size="24" />
</div>
<div class="shortcut-text">
<div class="shortcut-title">系统</div>
<div class="shortcut-subtitle">健康检查</div>
</div>
</div>
<!-- 消息 -->
<div class="shortcut-item" @click="messageDialog = true">
<div class="shortcut-icon-wrapper">
<VIcon icon="mdi-message" size="24" />
</div>
<div class="shortcut-text">
<div class="shortcut-title">消息</div>
<div class="shortcut-subtitle">消息中心</div>
</div>
</div>
</div>
</div>
</VCard>
</VMenu>
@@ -289,3 +299,119 @@ onMounted(() => {
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>
.shortcut-menu-card {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(var(--v-theme-on-surface), 0.12), 0 4px 12px rgba(var(--v-theme-on-surface), 0.08) !important;
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
}
.shortcut-header {
background: linear-gradient(to right, rgba(var(--v-theme-primary), 0.04), rgba(var(--v-theme-primary), 0.01));
border-bottom: 1px solid rgba(var(--v-theme-on-surface), 0.08);
padding: 12px 16px;
}
.shortcut-close-btn {
transition: transform 0.3s ease;
&:hover {
transform: rotate(90deg);
}
}
.shortcut-menu-container {
padding: 16px;
}
.shortcut-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.shortcut-item {
display: flex;
align-items: center;
padding: 16px;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
background-color: rgba(var(--v-theme-surface));
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
position: relative;
z-index: 1;
overflow: hidden;
&::before {
content: '';
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.08) 0%, rgba(var(--v-theme-primary), 0) 60%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(var(--v-theme-on-surface), 0.12);
border-color: rgba(var(--v-theme-primary), 0.15);
&::before {
opacity: 1;
}
.shortcut-icon-wrapper {
transform: scale(1.1);
background-color: rgba(var(--v-theme-primary), 0.12);
.v-icon {
transform: scale(1.2);
}
}
}
&:active {
transform: translateY(0);
box-shadow: 0 3px 10px rgba(var(--v-theme-on-surface), 0.08);
}
}
.shortcut-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 12px;
background-color: rgba(var(--v-theme-primary), 0.08);
margin-right: 16px;
transition: all 0.3s ease;
.v-icon {
transition: transform 0.3s ease;
color: rgba(var(--v-theme-primary), 1);
}
}
.shortcut-text {
flex: 1;
}
.shortcut-title {
font-weight: 600;
font-size: 1rem;
margin-bottom: 4px;
color: rgba(var(--v-theme-on-surface), 0.95);
}
.shortcut-subtitle {
font-size: 0.8rem;
color: rgba(var(--v-theme-on-surface), 0.7);
}
</style>

View File

@@ -39,8 +39,79 @@ onBeforeUnmount(() => {
if (eventSource) eventSource.close()
})
</script>
<style lang="scss" scoped>
.notification-header {
border-bottom: 1px solid rgba(var(--v-theme-on-surface), 0.08);
padding: 16px;
.v-card-title {
font-size: 1.1rem;
font-weight: 600;
color: rgba(var(--v-theme-primary), 0.9);
}
}
.notification-list {
max-height: 500px;
overflow-y: auto;
padding: 8px;
}
.notification-item {
border-radius: 12px;
margin-bottom: 8px;
transition: all 0.2s ease;
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
&:hover {
background-color: rgba(var(--v-theme-primary), 0.03);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(var(--v-theme-on-surface), 0.06);
}
.notification-avatar {
background-color: rgba(var(--v-theme-primary), 0.1);
box-shadow: 0 4px 8px rgba(var(--v-theme-primary), 0.15);
}
.notification-title {
font-weight: 600;
font-size: 0.95rem;
}
.notification-text {
font-size: 0.85rem;
color: rgba(var(--v-theme-on-surface), 0.75);
margin-top: 6px;
}
.notification-time {
font-size: 0.8rem;
color: rgba(var(--v-theme-primary), 0.8);
margin-top: 6px;
}
}
.no-notification {
padding: 30px 0;
color: rgba(var(--v-theme-on-surface), 0.6);
font-size: 0.95rem;
}
.mark-read-btn {
transition: all 0.2s ease;
border-radius: 8px;
&:hover {
background-color: rgba(var(--v-theme-primary), 0.1);
transform: scale(1.05);
}
}
</style>
<template>
<VMenu v-model="appsMenu" width="400" transition="scale-transition" close-on-content-click>
<VMenu v-model="appsMenu" width="400" transition="scale-transition" close-on-content-click class="notification-menu">
<!-- Menu Activator -->
<template #activator="{ props }">
<VBadge v-if="hasNewMessage" dot color="error" :offset-x="5" :offset-y="5" v-bind="props">
@@ -53,13 +124,14 @@ onBeforeUnmount(() => {
</IconBtn>
</template>
<!-- Menu Content -->
<VCard elevation="1">
<VCardItem class="border-b">
<VCardTitle>通知</VCardTitle>
<VCard elevation="0">
<VCardItem class="notification-header">
<VCardTitle>通知中心</VCardTitle>
<template #append>
<VTooltip text="设为已读">
<template #activator="{ props }">
<IconBtn
class="mark-read-btn"
v-bind="props"
@click="
() => {
@@ -68,33 +140,36 @@ onBeforeUnmount(() => {
}
"
>
<VIcon icon="mdi-email-mark-as-unread" />
<VIcon icon="mdi-email-check-outline" size="20" />
</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">
<div v-if="notificationList.length > 0" class="notification-list">
<VListItem v-for="(item, i) in notificationList" :key="i" lines="two" class="notification-item">
<template #prepend>
<VAvatar rounded>
<VAvatar rounded class="notification-avatar">
<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-if="item.type === 'plugin'" icon="mdi-robot" 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>
<div class="notification-content">
<div class="notification-title overflow-visiable break-words whitespace-break-spaces">
{{ item.title }}
</div>
<div class="notification-text">{{ item.text }}</div>
<div class="notification-time">{{ formatDateDifference(item.date) }}</div>
</div>
</VListItem>
</VList>
<VList v-else>
<VListItem>
<VListItemTitle class="text-center">暂无通知</VListItemTitle>
</VListItem>
</VList>
</div>
<div v-else class="no-notification">
<div class="text-center">
<VIcon icon="mdi-bell-sleep-outline" size="40" class="mb-3 text-primary" />
<div>暂无通知</div>
</div>
</div>
</VCard>
</VMenu>
</template>

View File

@@ -85,75 +85,84 @@ const userLevel = computed(() => userStore.level)
<VAvatar class="cursor-pointer ms-3" color="primary" variant="tonal">
<VImg :src="avatar" />
<VMenu activator="parent" width="230" location="bottom end" offset="14px">
<VList elevation="1">
<VMenu activator="parent" width="230" location="bottom end" offset="14px" class="user-menu">
<VList elevation="0" class="user-profile-list px-2">
<!-- 👉 User Avatar & Name -->
<VListItem>
<template #prepend>
<VListItemAction start>
<VAvatar color="primary" variant="tonal">
<VImg :src="avatar" />
</VAvatar>
</VListItemAction>
</template>
<VListItemTitle class="font-weight-semibold">
{{ superUser ? '管理员' : '普通用户' }}
</VListItemTitle>
<VListItemSubtitle>{{ userName }}</VListItemSubtitle>
</VListItem>
<VDivider class="my-2" />
<div class="user-profile-header px-2 py-4 mb-2">
<div class="d-flex align-center">
<VAvatar size="60" class="user-avatar" color="primary" rounded="sm">
<VImg :src="avatar" />
</VAvatar>
<div class="ms-4">
<div class="user-role">
{{ superUser ? '管理员' : '普通用户' }}
</div>
<div class="user-name">
{{ userName }}
</div>
</div>
</div>
</div>
<!-- 👉 Profile -->
<VListItem link @click="router.push('/profile')">
<VListItem link @click="router.push('/profile')" class="user-menu-item mb-1">
<template #prepend>
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
<div class="user-menu-icon">
<VIcon icon="mdi-account-outline" />
</div>
</template>
<VListItemTitle>个人信息</VListItemTitle>
</VListItem>
<VListItem link @click="router.push('/apps')">
<VListItem link @click="router.push('/apps')" class="user-menu-item mb-1">
<template #prepend>
<VIcon class="me-2" icon="mdi-view-grid-outline" size="22" />
<div class="user-menu-icon">
<VIcon icon="mdi-view-grid-outline" />
</div>
</template>
<VListItemTitle>功能视图</VListItemTitle>
</VListItem>
<!-- 👉 Site Auth -->
<VListItem v-if="userLevel < 2 && superUser" link @click="showSiteAuthDialog">
<VListItem v-if="userLevel < 2 && superUser" link @click="showSiteAuthDialog" class="user-menu-item mb-1">
<template #prepend>
<VIcon class="me-2" icon="mdi-lock-check-outline" size="22" />
<div class="user-menu-icon">
<VIcon icon="mdi-lock-check-outline" />
</div>
</template>
<VListItemTitle>用户认证</VListItemTitle>
</VListItem>
<!-- 👉 FAQ -->
<VListItem href="https://wiki.movie-pilot.org" target="_blank">
<VListItem href="https://wiki.movie-pilot.org" target="_blank" class="user-menu-item mb-1">
<template #prepend>
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
<div class="user-menu-icon">
<VIcon icon="mdi-help-circle-outline" />
</div>
</template>
<VListItemTitle>帮助文档</VListItemTitle>
</VListItem>
<!-- Divider -->
<VDivider v-if="superUser" class="my-2" />
<VDivider v-if="superUser" class="my-3" />
<!-- 👉 restart -->
<VListItem v-if="superUser" @click="showRestartDialog">
<VListItem v-if="superUser" @click="showRestartDialog" class="user-menu-item mb-1">
<template #prepend>
<VIcon class="me-2" icon="mdi-restart" size="22" />
<div class="user-menu-icon restart-icon">
<VIcon icon="mdi-restart" />
</div>
</template>
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- 👉 Logout -->
<VListItem @click="logout">
<VBtn color="error" block>
<template #append> <VIcon size="small" icon="mdi-logout" /> </template>
<div class="px-2 mt-3 mb-2">
<VBtn color="error" block class="logout-btn" @click="logout">
<template #prepend> <VIcon icon="mdi-logout" /> </template>
退出登录
</VBtn>
</VListItem>
</div>
</VList>
</VMenu>
<!-- !SECTION -->
@@ -184,3 +193,71 @@ const userLevel = computed(() => userStore.level)
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>
.user-profile-header {
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.05), rgba(var(--v-theme-primary), 0.02));
border-radius: 12px;
}
.user-role {
font-size: 0.875rem;
color: rgba(var(--v-theme-primary), 0.9);
font-weight: 500;
margin-bottom: 4px;
}
.user-name {
font-size: 1.125rem;
font-weight: 600;
color: rgba(var(--v-theme-on-surface), 0.9);
}
.user-avatar {
box-shadow: 0 4px 12px rgba(var(--v-theme-primary), 0.2);
border: 2px solid rgba(var(--v-theme-on-surface), 0.1);
}
.user-menu-item {
border-radius: 8px;
margin: 4px 0;
transition: all 0.2s ease;
&:hover {
background-color: rgba(var(--v-theme-primary), 0.06);
transform: translateX(4px);
}
}
.user-menu-icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background-color: rgba(var(--v-theme-primary), 0.08);
margin-right: 12px;
transition: all 0.2s ease;
.v-icon {
color: rgba(var(--v-theme-primary), 0.9);
}
}
.restart-icon {
background-color: rgba(var(--v-theme-error), 0.1);
.v-icon {
color: rgba(var(--v-theme-error), 0.9);
}
}
.logout-btn {
border-radius: 10px;
padding: 12px;
font-weight: 500;
letter-spacing: 0.5px;
box-shadow: 0 4px 12px rgba(var(--v-theme-error), 0.2);
}
</style>

View File

@@ -261,22 +261,19 @@ html.v-overlay-scroll-blocked body {
}
.v-overlay__content .v-list{
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), 0.9) !important;
background-color: rgb(var(--v-theme-surface)) !important;
border-radius: 8px !important;
box-shadow: none !important;
padding: 4px !important;
margin: 0 !important;
}
.v-overlay__content .v-card:not(.bg-primary){
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background-color: rgb(var(--v-theme-surface), 0.95) !important;
background-color: rgb(var(--v-theme-surface)) !important;
border-radius: 8px !important;
box-shadow: none !important;
.v-list, .v-table {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: none;
backdrop-filter: none;
background-color: transparent !important;
}
}
@@ -294,3 +291,73 @@ html.v-overlay-scroll-blocked body {
content: '';
inset: 0;
}
/* 菜单项样式 */
.v-list-item {
border-radius: 4px !important;
margin: 2px 0 !important;
transition: background-color 0.15s ease;
}
.v-list-item:hover {
background-color: rgba(var(--v-theme-on-surface), 0.04) !important;
}
/* 下拉菜单整体样式 */
.v-menu > .v-overlay__content {
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(var(--v-theme-on-surface), 0.08) !important;
overflow: hidden;
}
/* 通知弹窗样式 */
.notification-menu .v-overlay__content {
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(var(--v-theme-on-surface), 0.08) !important;
overflow: hidden;
}
/* 主题切换菜单样式 */
.theme-menu .v-overlay__content {
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(var(--v-theme-on-surface), 0.08) !important;
}
/* 用户菜单样式 */
.user-menu .v-overlay__content {
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(var(--v-theme-on-surface), 0.08) !important;
}
/* 菜单按钮交互效果 */
.v-btn.v-btn--icon {
transition: opacity 0.15s ease;
}
.v-btn.v-btn--icon:hover {
opacity: 0.85;
}
/* 菜单弹出动画优化 */
.v-overlay__content {
transition: opacity 0.2s ease !important;
}
/* 菜单卡片和列表 */
.v-overlay__content .v-list{
background-color: rgb(var(--v-theme-surface)) !important;
border-radius: 8px !important;
box-shadow: none !important;
padding: 4px !important;
margin: 0 !important;
}
.v-overlay__content .v-card:not(.bg-primary){
background-color: rgb(var(--v-theme-surface)) !important;
border-radius: 8px !important;
box-shadow: none !important;
.v-list, .v-table {
background-color: transparent !important;
}
}