admin_new/src/components/AppLayout/AppLayout.vue
Sisyphus c3a7fbad0b feat(F006): apply v2 design language patch
- AppLayout.vue + LoginView.vue: 顶栏毛玻璃/通知红点,Login 渐变 + 透明 logo
- theme/tokens.scss + themes.scss: v2 设计 token + 4 主题(科技蓝/中国红/Notion 紫/暗夜黑)
- stores/menu.ts: V2 R2 + filterByFrontendTruth(后端全菜单→前端子集)
- config/menu-labels.ts: V2 R2 菜单 label 映射(砍数据统计)
- assets/logo-jubian.png: 长方形 logo 抠图透明版

source: ~/projects/_meta/f006-v2-patch-20260703-143500/
2026-07-03 15:10:40 +08:00

780 lines
22 KiB
Vue

<script setup lang="ts">
/**
* F006 主布局 - 完全按 /theme-switcher.html?theme=tech-blue 设计
* W5.x: v2 Design Language - 毛玻璃顶栏 + 透明 logo + 通知红点
* W5.x: v2 Design Language - 毛玻璃顶栏 + 透明 logo + 通知红点
* W3.5.1: 修分组错位(整组画在一块,不是每菜单画分组)
* W3.5.2: 修图标(从 @element-plus/icons-vue import 实际组件,不是字符串)
* W3.5.3: 修一级菜单字重 600 → 400(设计稿是细字体)
*/
import { computed, onMounted, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import ThemePicker from '@/components/ThemePicker/ThemePicker.vue'
import { useUserStore } from '@/stores/user'
import { useMenuStore, type MenuItem } from '@/stores/menu'
import {
Odometer, Box, Document, ChatLineSquare, ChatDotRound, OfficeBuilding,
User, Lock, Setting, Menu, Memo, Timer, MagicStick, FolderOpened,
Ticket, Wallet, QuestionFilled, Files, UserFilled, Cellphone, Promotion,
Search, ArrowDown, ArrowRight, Expand, Fold, SwitchButton, Bell, DataAnalysis,
Folder, Postcard, PriceTag, DataLine, RefreshRight, Message, Link, Grid,
Star, ShoppingCart, Notification
} from '@element-plus/icons-vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const menuStore = useMenuStore()
const collapsed = computed(() => menuStore.collapsed)
const menuList = computed<MenuItem[]>(() => menuStore.list)
const openMenus = ref<Set<string>>(new Set())
const searchQuery = ref('')
onMounted(async () => {
if (userStore.isLoggedIn && !menuStore.list.length) {
await Promise.all([menuStore.fetchMenu(), userStore.fetchUserInfo()])
}
})
function toggleSidebar() { menuStore.toggleCollapsed() }
function goMenu(item: MenuItem) {
if (item.children?.length) {
if (openMenus.value.has(item.path)) openMenus.value.delete(item.path)
else openMenus.value.add(item.path)
return
}
router.push(item.path)
}
function logout() {
userStore.logout()
menuStore.clear()
router.push('/login')
}
function isActive(path: string): boolean {
return path === '/' ? route.path === '/' : route.path.startsWith(path)
}
const PARENT_GROUP: Record<string, string> = {
'概览': '概览',
'业务': '业务',
'系统': '系统',
'数据统计': '概览',
'基础设置': '其它',
'用户管理': '业务',
'企业管理': '业务',
'操作指南': '其它',
'商品管理': '业务',
'订单管理': '业务',
'询价管理': '业务',
'营销活动管理': '业务',
'财务管理': '业务',
'行业资讯': '其它',
'系统设置': '系统',
'层级架构': '系统',
'权限管理': '系统',
'消息通知': '系统',
'日志管理': '系统',
'集成管理': '系统',
'帮助中心': '其它'
}
function groupOf(item: MenuItem): string {
if (PARENT_GROUP[item.title]) return PARENT_GROUP[item.title]
const url = item.url || ''
if (url.includes('statistics')) return '概览'
return '其它'
}
const PARENT_ICONS: Record<string, string> = {
'权限管理': 'Lock',
'系统设置': 'Setting',
'日志管理': 'Memo',
'系统监控': 'Monitor',
'消息管理': 'ChatDotRound',
'站内通知': 'Bell',
'报表管理': 'DataAnalysis',
'用户管理': 'UserFilled',
'商品管理': 'Box',
'订单管理': 'Document',
'询价管理': 'ChatLineSquare',
'财务管理': 'Wallet',
'营销活动管理': 'Promotion',
'企业管理': 'OfficeBuilding',
'行业资讯': 'Reading',
'操作指南': 'Reading',
'层级架构': 'Connection',
'消息通知': 'Bell',
'集成管理': 'Connection',
'基础设置': 'Tools',
'帮助中心': 'QuestionFilled'
}
const ICON_MAP: Record<string, any> = {
Odometer, Box, Document, ChatLineSquare, ChatDotRound, OfficeBuilding,
User, Lock, Setting, Menu, Memo, Timer, MagicStick, FolderOpened,
Ticket, Wallet, QuestionFilled, Files, UserFilled, Cellphone, Promotion,
Search, ArrowDown, ArrowRight, Expand, Fold, SwitchButton, Bell, DataAnalysis,
Folder, Postcard, PriceTag, DataLine, RefreshRight, Message, Link, Grid,
Star, ShoppingCart, Notification
}
function iconOf(item: MenuItem): any {
if (item.title && PARENT_ICONS[item.title] && ICON_MAP[PARENT_ICONS[item.title]]) {
return ICON_MAP[PARENT_ICONS[item.title]]
}
if (item.icon && ICON_MAP[item.icon]) return ICON_MAP[item.icon]
const url = (item.url || '').replace(/^\//, '')
if (!url) return ICON_MAP.Folder
const seg = url.split('?')[0].split('/').filter(Boolean)
const last = seg[seg.length - 1] || ''
if (url.includes('statistics') || last === 'statistics') return ICON_MAP.Odometer
if (last === 'user' || last === 'sys/user' || last === 'users') return ICON_MAP.User
if (last === 'role' || last === 'sys/role') return ICON_MAP.Lock
if (last === 'post' || last === 'sys/post') return ICON_MAP.Postcard
if (last === 'dept' || last === 'sys/dept') return ICON_MAP.OfficeBuilding
if (last === 'menu' || last === 'sys/menu') return ICON_MAP.Menu
if (last.startsWith('log')) return ICON_MAP.Memo
if (last === 'region' || last === 'operation' || last === 'userManagement') return ICON_MAP.OfficeBuilding
if (last === 'message') return ICON_MAP.ChatDotRound
if (last === 'schedule' || last === 'job') return ICON_MAP.Timer
if (last === 'oss') return ICON_MAP.FolderOpened
if (last === 'coupon') return ICON_MAP.Ticket
if (last === 'recharge' || last === 'revenue-sharing' || last === 'transaction-record') return ICON_MAP.Wallet
if (last === 'finance') return ICON_MAP.Wallet
if (last === 'sensitive' || last === 'course') return ICON_MAP.Ticket
if (last === 'sms') return ICON_MAP.Message
if (url.startsWith('devtools/')) {
if (last === 'generator') return ICON_MAP.MagicStick
if (last === 'datasource') return ICON_MAP.DataLine
if (last === 'param') return ICON_MAP.Setting
if (last === 'baseclass') return ICON_MAP.Files
if (last === 'template') return ICON_MAP.Document
if (last === 'fieldtype') return ICON_MAP.PriceTag
return ICON_MAP.MagicStick
}
if (url.startsWith('openapi/')) {
if (last === 'app') return ICON_MAP.Cellphone
if (last === 'company-account') return ICON_MAP.OfficeBuilding
if (last === 'audit-log') return ICON_MAP.Memo
return ICON_MAP.Cellphone
}
if (url.startsWith('supplier/') || url.includes('inquiry')) {
if (last.includes('inquiry')) return ICON_MAP.ChatLineSquare
if (last === 'good' || last === 'servicegood') return ICON_MAP.Box
if (last === 'coupon') return ICON_MAP.Ticket
if (last === 'activityzone') return ICON_MAP.Promotion
if (last === 'promoter') return ICON_MAP.UserFilled
return ICON_MAP.OfficeBuilding
}
if (url.startsWith('order/')) {
if (last.includes('commission')) return ICON_MAP.Money
if (last.includes('after')) return ICON_MAP.RefreshRight
if (last.includes('analysis')) return ICON_MAP.DataAnalysis
if (last === 'ordergood' || last === 'serviceorder' || last === 'order') return ICON_MAP.Document
if (last.includes('order')) return ICON_MAP.Document
return ICON_MAP.Document
}
if (url.startsWith('user/')) {
if (last === 'enterprise' || last.includes('enterprise')) return ICON_MAP.OfficeBuilding
if (last === 'finance' || url.includes('finance')) return ICON_MAP.Wallet
if (last === 'message' || url.includes('message')) return ICON_MAP.ChatDotRound
return ICON_MAP.User
}
if (url.startsWith('other/')) {
if (last === 'operateguide' || last === 'help') return ICON_MAP.QuestionFilled
if (last === 'categoryGood' || last === 'categoryRM' || last === 'categoryZX') return ICON_MAP.Grid
if (last === 'goodunit') return ICON_MAP.PriceTag
if (last === 'supplierBrand') return ICON_MAP.Star
if (last === 'groupbuy') return ICON_MAP.ShoppingCart
if (last === 'seckill') return ICON_MAP.Timer
if (last === 'information') return ICON_MAP.Notification
return ICON_MAP.Files
}
if (url.startsWith('api/')) return ICON_MAP.Link
return ICON_MAP.Folder
}
const GROUP_ORDER = ['概览', '业务', '系统', '其它'] as const
const groupedMenus = computed(() => {
const buckets: Record<string, MenuItem[]> = { 概览: [], 业务: [], 系统: [], 其它: [] }
for (const m of menuList.value) {
const g = groupOf(m)
if (!buckets[g]) buckets[g] = []
buckets[g].push(m)
}
return GROUP_ORDER.map(g => ({ name: g, items: buckets[g] || [] }))
})
const badges = ref<Record<string, string>>({})
async function loadBadges() {
try {
const data = await fetch('/sys/openapi/inquiry/list?page=1&limit=1', {
headers: { Authorization: `Bearer ${userStore.token}` }
}).then(r => r.json()).catch(() => null)
if (data?.code === 0) {
const total = data.total ?? data.data?.length ?? 0
badges.value['询价管理'] = String(total)
}
} catch {}
}
onMounted(loadBadges)
</script>
<template>
<div class="rr-layout" :class="{ collapsed }">
<!-- 侧边栏(按设计稿) -->
<aside class="rr-sidebar">
<!-- 品牌 + 搜索框(只在展开时显示) -->
<div class="rr-sidebar-brand" v-if="!collapsed">
<img class="rr-sidebar-logo" src="@/assets/logo-jubian.png" alt="巨变" />
<span class="rr-sidebar-name">产业互联网平台运营后台</span>
</div>
<div v-if="!collapsed" class="rr-sidebar-search">
<el-icon><component :is="Search" /></el-icon>
<input
v-model="searchQuery"
class="rr-sidebar-search-input"
placeholder="搜索 ⌘K"
type="text"
/>
</div>
<nav class="rr-sidebar-nav">
<template v-for="group in groupedMenus" :key="group.name">
<template v-if="group.items.length">
<div v-if="!collapsed" class="rr-sidebar-group-label">{{ group.name }}</div>
<template v-for="item in group.items" :key="item.path">
<div v-if="item.children?.length" class="rr-sidebar-group">
<div
class="rr-sidebar-item rr-sidebar-parent"
:class="{ active: isActive(item.path) }"
@click="goMenu(item)"
>
<el-icon class="rr-sidebar-icon"><component :is="iconOf(item)" /></el-icon>
<span v-if="!collapsed" class="rr-sidebar-label">{{ item.title }}</span>
<span v-if="badges[item.title] && !collapsed" class="rr-sidebar-badge">{{ badges[item.title] }}</span>
<el-icon v-if="!collapsed" class="rr-sidebar-arrow">
<component :is="openMenus.has(item.path) ? ArrowDown : ArrowRight" />
</el-icon>
</div>
<div v-if="!collapsed && openMenus.has(item.path)" class="rr-sidebar-subnav">
<div
v-for="sub in item.children"
:key="sub.path"
class="rr-sidebar-subitem"
:class="{ active: isActive(sub.path) }"
@click="goMenu(sub)"
>
<span class="rr-sidebar-label">{{ sub.title }}</span>
<span v-if="badges[sub.title]" class="rr-sidebar-badge">{{ badges[sub.title] }}</span>
</div>
</div>
</div>
<div
v-else
class="rr-sidebar-item"
:class="{ active: isActive(item.path) }"
@click="goMenu(item)"
>
<el-icon class="rr-sidebar-icon"><component :is="iconOf(item)" /></el-icon>
<span v-if="!collapsed" class="rr-sidebar-label">{{ item.title }}</span>
<span v-if="badges[item.title] && !collapsed" class="rr-sidebar-badge">{{ badges[item.title] }}</span>
</div>
</template>
</template>
</template>
<div v-if="!menuList.length && !collapsed" class="rr-sidebar-empty">
<span v-if="menuStore.loading">菜单加载中…</span>
<span v-else-if="menuStore.error">{{ menuStore.error }}</span>
<span v-else>暂无菜单</span>
</div>
</nav>
<!-- 底栏用户卡片(只在展开时显示) -->
<div v-if="!collapsed" class="rr-sidebar-footer">
<div class="rr-sidebar-user-avatar">{{ userStore.initial }}</div>
<div class="rr-sidebar-user-info">
<div class="rr-sidebar-user-name">{{ userStore.displayName }}</div>
<div class="rr-sidebar-user-email">{{ userStore.username }}@direv.io</div>
</div>
<el-icon class="rr-sidebar-user-arrow" @click.stop="logout"><component :is="SwitchButton" /></el-icon>
</div>
</aside>
<!-- 主区域 -->
<div class="rr-main">
<!-- 顶栏(极薄 48px) -->
<header class="rr-topbar">
<div class="rr-topbar-left">
<div class="rr-topbar-toggle" @click="toggleSidebar">
<el-icon><component :is="collapsed ? Expand : Fold" /></el-icon>
</div>
<el-breadcrumb separator=">" class="rr-topbar-breadcrumb">
<el-breadcrumb-item :to="{ path: '/' }">工作台</el-breadcrumb-item>
<el-breadcrumb-item>{{ route.meta?.title || '' }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="rr-topbar-right">
<ThemePicker />
<div class="rr-topbar-notification" title="通知">
<el-icon class="rr-topbar-notification-icon"><component :is="Bell" /></el-icon>
<span class="rr-topbar-notification-dot">5</span>
</div>
<div class="rr-topbar-user">
<div class="rr-topbar-avatar">{{ userStore.initial }}</div>
<span class="rr-topbar-username">{{ userStore.displayName }}</span>
</div>
</div>
</header>
<main class="rr-content">
<slot />
</main>
</div>
</div>
</template>
<style scoped lang="scss">
.rr-layout {
display: flex;
height: 100vh;
background: var(--bg-soft);
transition: background 0.3s;
&.collapsed .rr-sidebar { width: 64px; }
}
/* ===== 侧边栏(按 /theme-switcher.html?theme=tech-blue 设计) ===== */
.rr-sidebar {
width: 232px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
transition: width 0.2s;
}
.rr-sidebar-brand {
height: 56px;
padding: 0 18px;
display: flex;
align-items: center;
gap: 10px;
}
.rr-sidebar-logo {
width: 28px;
height: 28px;
background: var(--brand);
border-radius: 6px;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
flex-shrink: 0;
box-shadow: 0 2px 6px var(--brand-glow);
}
.rr-sidebar-name {
font-size: 14px;
font-weight: 600;
color: var(--ink);
}
/* 搜索框 */
.rr-sidebar-search {
margin: 0 12px 12px;
padding: 7px 10px;
border: 1px solid var(--border);
border-radius: 6px;
display: flex;
align-items: center;
gap: 6px;
color: var(--ink-3);
font-size: 12.5px;
transition: all 0.15s;
&:focus-within { border-color: var(--brand); background: var(--brand-soft); }
.el-icon { font-size: 13px; }
}
.rr-sidebar-search-input {
flex: 1;
border: 0;
outline: 0;
background: transparent;
color: var(--ink);
font-size: 12.5px;
&::placeholder { color: var(--ink-3); }
}
/* 导航区 */
.rr-sidebar-nav {
flex: 1;
padding: 4px 12px 8px;
overflow-y: auto;
}
/* 分组小标题(概览/业务/系统) */
.rr-sidebar-group-label {
font-size: 11px;
font-weight: 600;
color: var(--ink-3);
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 14px 10px 6px;
}
/* 菜单项 */
.rr-sidebar-item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
border-radius: 6px;
color: var(--ink-2);
font-size: 13px;
font-weight: 400;
cursor: pointer;
margin-bottom: 1px;
transition: all 0.12s;
white-space: nowrap;
&:hover { background: var(--bg-soft); color: var(--ink); }
&.active {
background: var(--brand-soft);
color: var(--brand);
font-weight: 400;
}
}
.rr-sidebar-item.rr-sidebar-parent { font-weight: 400; }
.rr-sidebar-icon { font-size: 16px; flex-shrink: 0; }
.rr-sidebar-arrow { font-size: 12px; flex-shrink: 0; color: var(--ink-3); margin-left: auto; }
/* 徽章数字(2.4K / 12 / 5) */
.rr-sidebar-badge {
margin-left: auto;
font-size: 11px;
font-weight: 600;
padding: 1px 7px;
border-radius: 10px;
background: var(--bg-soft);
color: var(--ink-3);
.rr-sidebar-item.active & { background: rgba(255,255,255,0.18); color: var(--brand); }
}
/* 子菜单 */
.rr-sidebar-subnav {
margin: 1px 0 4px;
padding-left: 8px;
border-left: 1px solid var(--border);
margin-left: 18px;
}
.rr-sidebar-subitem {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px 5px 12px;
border-radius: 6px;
color: var(--ink-2);
font-size: 12.5px;
font-weight: 400;
cursor: pointer;
margin-bottom: 1px;
white-space: nowrap;
&:hover { background: var(--bg-soft); color: var(--ink); }
&.active {
background: var(--brand-soft);
color: var(--brand);
font-weight: 500;
}
}
.rr-sidebar-subitem .rr-sidebar-icon { font-size: 14px; }
.rr-sidebar-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.rr-sidebar-empty {
text-align: center;
color: var(--ink-3);
font-size: 12px;
padding: 16px 8px;
}
/* 底栏用户卡片 */
.rr-sidebar-footer {
margin: 8px 12px 12px;
padding: 10px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.rr-sidebar-user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
flex-shrink: 0;
}
.rr-sidebar-user-info { flex: 1; min-width: 0; }
.rr-sidebar-user-name {
font-size: 13px;
font-weight: 600;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rr-sidebar-user-email {
font-size: 11px;
color: var(--ink-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rr-sidebar-user-arrow {
font-size: 16px;
color: var(--ink-3);
cursor: pointer;
&:hover { color: var(--brand); }
}
/* ===== 主区域 ===== */
.rr-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
/* 顶栏(48px,W5.x v2 加毛玻璃) */
.rr-topbar {
position: sticky;
top: 0;
z-index: 10;
height: 48px;
background: color-mix(in srgb, var(--surface) 75%, transparent);
backdrop-filter: var(--topbar-blur);
-webkit-backdrop-filter: var(--topbar-blur);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow-sm);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
flex-shrink: 0;
transition: background 0.2s var(--ease);
}
.rr-topbar-left { display: flex; align-items: center; gap: 16px; }
.rr-topbar-toggle {
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--ink-2);
&:hover { background: var(--bg-soft); color: var(--brand); }
}
.rr-topbar-breadcrumb { font-size: 13px; }
.rr-topbar-right { display: flex; align-items: center; gap: 12px; }
.rr-topbar-user {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 16px;
cursor: pointer;
&:hover { background: var(--bg-soft); }
}
.rr-topbar-avatar {
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.rr-topbar-username {
font-size: 13px;
font-weight: 500;
color: var(--ink);
}
/* 内容区 */
.rr-content {
flex: 1;
overflow: auto;
padding: 24px 28px;
background: var(--bg-soft);
}
/* ===== W5.x v2 Design Language 新增样式 ===== */
/* 侧边栏 logo 改为图片(W5.x) */
.rr-sidebar-logo {
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--bg-soft);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.rr-sidebar-logo[src] {
background: transparent;
}
img.rr-sidebar-logo {
object-fit: contain;
padding: 2px;
}
/* 顶栏通知红点(W5.x) */
.rr-topbar-notification {
position: relative;
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--ink-2);
transition: all 0.2s var(--ease);
}
.rr-topbar-notification:hover {
background: var(--bg-soft);
color: var(--brand);
}
.rr-topbar-notification-icon {
font-size: 18px;
}
.rr-topbar-notification-dot {
position: absolute;
top: 4px;
right: 4px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: var(--red);
color: white;
font-size: 10px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
box-shadow: 0 0 0 2px var(--surface);
}
[data-theme="dark"] .rr-topbar-notification-dot {
box-shadow: 0 0 0 2px var(--bg-soft);
}
/* 顶栏头像(W5.x 加渐变) */
.rr-topbar-avatar {
background: linear-gradient(135deg, var(--brand), var(--brand-2));
box-shadow: 0 2px 4px var(--brand-glow);
}
/* ===== W5.x v2 Design Language 新增样式 ===== */
/* 侧边栏 logo 改为图片(W5.x) */
.rr-sidebar-logo {
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--bg-soft);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.rr-sidebar-logo[src] {
background: transparent;
}
img.rr-sidebar-logo {
object-fit: contain;
padding: 2px;
}
/* 顶栏通知红点(W5.x) */
.rr-topbar-notification {
position: relative;
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--ink-2);
transition: all 0.2s var(--ease);
}
.rr-topbar-notification:hover {
background: var(--bg-soft);
color: var(--brand);
}
.rr-topbar-notification-icon {
font-size: 18px;
}
.rr-topbar-notification-dot {
position: absolute;
top: 4px;
right: 4px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: var(--red);
color: white;
font-size: 10px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
box-shadow: 0 0 0 2px var(--surface);
}
[data-theme="dark"] .rr-topbar-notification-dot {
box-shadow: 0 0 0 2px var(--bg-soft);
}
/* 顶栏头像(W5.x 加渐变) */
.rr-topbar-avatar {
background: linear-gradient(135deg, var(--brand), var(--brand-2));
box-shadow: 0 2px 4px var(--brand-glow);
}
</style>