Compare commits
8 Commits
6d02a4b5d2
...
5b86842f65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b86842f65 | ||
|
|
e1cd69c7e5 | ||
|
|
01bd82deb3 | ||
|
|
0f8a08888f | ||
|
|
93ed6331e8 | ||
|
|
6a0736d70c | ||
|
|
3744cf7c93 | ||
|
|
c3a7fbad0b |
BIN
src/assets/logo-jubian-light.png
Normal file
BIN
src/assets/logo-jubian-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/logo-jubian.png
Normal file
BIN
src/assets/logo-jubian.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
@ -1,70 +1,330 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 主布局 - 顶栏 + 侧边栏 + 内容区
|
||||
* 架构要点:布局组件不带业务逻辑,菜单从 useMenuStore 拉(以后从后端拉)
|
||||
* 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 } from 'vue'
|
||||
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 { useThemeStore } from '@/stores/theme'
|
||||
import logoUrl from '@/assets/logo-jubian.png'
|
||||
import logoLightUrl from '@/assets/logo-jubian-light.png'
|
||||
import { useMenuStore, type MenuItem } from '@/stores/menu'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
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 themeStore = useThemeStore()
|
||||
const logoSrc = computed(() => themeStore.current === 'dark' ? logoLightUrl : logoUrl)
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const collapsed = computed(() => menuStore.collapsed)
|
||||
const menuList = computed<MenuItem[]>(() => menuStore.list)
|
||||
const activeMenu = computed(() => route.path)
|
||||
const openMenus = ref<Set<string>>(new Set())
|
||||
const searchQuery = ref('')
|
||||
|
||||
function toggleSidebar() {
|
||||
menuStore.toggleCollapsed()
|
||||
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 goMenu(path: string) {
|
||||
router.push(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">
|
||||
<div class="rr-sidebar-logo">P</div>
|
||||
<span v-if="!collapsed" class="rr-sidebar-name">Product Network</span>
|
||||
<!-- 品牌 + 搜索框(只在展开时显示) -->
|
||||
<div class="rr-sidebar-brand" v-if="!collapsed">
|
||||
<img class="rr-sidebar-logo" :src="logoSrc" 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">
|
||||
<div
|
||||
v-for="item in menuList"
|
||||
:key="item.path"
|
||||
class="rr-sidebar-item"
|
||||
:class="{ active: isActive(item.path) }"
|
||||
@click="goMenu(item.path)"
|
||||
>
|
||||
<el-icon class="rr-sidebar-icon"><component :is="item.icon || 'Menu'" /></el-icon>
|
||||
<span v-if="!collapsed" class="rr-sidebar-label">{{ item.title }}</span>
|
||||
<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>
|
||||
<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>
|
||||
@ -73,23 +333,17 @@ function isActive(path: string): boolean {
|
||||
</div>
|
||||
<div class="rr-topbar-right">
|
||||
<ThemePicker />
|
||||
<el-dropdown trigger="click" @command="(c: string) => c === 'logout' && logout()">
|
||||
<div class="rr-topbar-user">
|
||||
<div class="rr-topbar-avatar">{{ userStore.username?.charAt(0) || 'U' }}</div>
|
||||
<span class="rr-topbar-username">{{ userStore.username || 'User' }}</span>
|
||||
<el-icon><component :is="'ArrowDown'" /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item disabled>{{ userStore.username }}</el-dropdown-item>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<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>
|
||||
@ -103,27 +357,25 @@ function isActive(path: string): boolean {
|
||||
height: 100vh;
|
||||
background: var(--bg-soft);
|
||||
transition: background 0.3s;
|
||||
|
||||
&.collapsed .rr-sidebar { width: 64px; }
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
/* ===== 侧边栏(按 /theme-switcher.html?theme=tech-blue 设计) ===== */
|
||||
.rr-sidebar {
|
||||
width: 220px;
|
||||
width: 232px;
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
.rr-sidebar-brand {
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
padding: 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.rr-sidebar-logo {
|
||||
width: 28px;
|
||||
@ -143,49 +395,177 @@ function isActive(path: string): boolean {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 搜索框 */
|
||||
.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: 12px 8px;
|
||||
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: 8px 12px;
|
||||
border-radius: var(--radius);
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
color: var(--ink-2);
|
||||
font-size: 13.5px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
margin-bottom: 2px;
|
||||
margin-bottom: 1px;
|
||||
transition: all 0.12s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-soft);
|
||||
color: var(--ink);
|
||||
}
|
||||
&:hover { background: var(--bg-soft); color: var(--ink); }
|
||||
&.active {
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
.rr-sidebar-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
.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;
|
||||
@ -193,49 +573,59 @@ function isActive(path: string): boolean {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 顶栏 */
|
||||
/* 顶栏(48px,W5.x v2 加毛玻璃) */
|
||||
.rr-topbar {
|
||||
height: 56px;
|
||||
background: var(--surface);
|
||||
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-left { display: flex; align-items: center; gap: 16px; }
|
||||
.rr-topbar-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--ink-2);
|
||||
transition: all 0.12s;
|
||||
&:hover { background: var(--bg-soft); color: var(--brand); }
|
||||
}
|
||||
.rr-topbar-breadcrumb { font-size: 13px; }
|
||||
|
||||
.rr-topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
/* ===== W5.x.x+1: 暗主题下 el-breadcrumb 文字糊(红框 1)强制覆盖 =====
|
||||
EP 内部 el-breadcrumb__inner 用 var(--el-text-color-regular) 但优先级被
|
||||
scoped 选择器打回 → 用 :deep() + !important 强制跟主题色走 */
|
||||
.rr-topbar-breadcrumb :deep(.el-breadcrumb__inner) {
|
||||
color: var(--ink) !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
.rr-topbar-breadcrumb :deep(.el-breadcrumb__inner.is-link:hover) {
|
||||
color: var(--brand) !important;
|
||||
}
|
||||
.rr-topbar-breadcrumb :deep(.el-breadcrumb__separator) {
|
||||
color: var(--ink-3) !important;
|
||||
}
|
||||
.rr-topbar-right { display: flex; align-items: center; gap: 12px; }
|
||||
.rr-topbar-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px 4px 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
&:hover { background: var(--bg-soft); }
|
||||
}
|
||||
.rr-topbar-avatar {
|
||||
@ -260,7 +650,149 @@ function isActive(path: string): boolean {
|
||||
.rr-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px 24px;
|
||||
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>
|
||||
|
||||
@ -14,7 +14,7 @@ function pick(value: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover placement="bottom-end" :width="260" trigger="click">
|
||||
<el-popover placement="bottom-end" :width="260" trigger="click" :teleported="false">
|
||||
<template #reference>
|
||||
<div class="rr-theme-trigger" :title="'主题: ' + themeStore.currentPreset.name">
|
||||
<span class="rr-theme-dot" :style="{ background: themeStore.currentPreset.color }"></span>
|
||||
@ -84,7 +84,7 @@ function pick(value: string) {
|
||||
.rr-theme-picker-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ink-2);
|
||||
color: var(--ink-2) !important;
|
||||
padding: 8px 8px 12px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
@ -124,18 +124,56 @@ function pick(value: string) {
|
||||
.rr-theme-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
color: var(--ink) !important;
|
||||
}
|
||||
.rr-theme-item.active .rr-theme-name {
|
||||
color: var(--brand);
|
||||
color: var(--brand) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.rr-theme-hint {
|
||||
margin-top: 8px;
|
||||
padding: 8px 8px 0;
|
||||
font-size: 11px;
|
||||
color: var(--ink-3);
|
||||
color: var(--ink-3) !important;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ===== W5.x.x+1 v3: 暗主题下主题弹窗糊(红框 2)修根因 =====
|
||||
根因: scoped :deep() 在 dist 里编译成 [data-v-xxx] 前缀选择器,
|
||||
但 EP el-popper 容器是组件内子元素,vue scoped 没给它加 data-v 属性
|
||||
(实测 className = "el-popper is-light el-tooltip el-popover"),
|
||||
所以 [data-v-xxx] .el-popper.is-light { ... !important } 根本匹配不到。
|
||||
另: EP 弹窗实际背景读 --el-popper-bg-color-light(不是 --el-popper-bg-color),
|
||||
tokens.scss 已加 light 版。修法: 用 :global() 写无 scoped 的全局规则。 */
|
||||
:global(.el-popper.is-light) {
|
||||
background: var(--surface) !important;
|
||||
border-color: var(--border) !important;
|
||||
color: var(--ink) !important;
|
||||
--el-popper-bg-color-light: var(--surface);
|
||||
--el-popper-border-color: var(--border);
|
||||
--el-border-color-light: var(--border);
|
||||
--el-fill-color-blank: var(--surface);
|
||||
}
|
||||
:global(.el-popper.is-light .el-popper__arrow::before) {
|
||||
background: var(--surface) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
:global(.el-popper.is-light .el-popper__title) {
|
||||
color: var(--ink) !important;
|
||||
}
|
||||
:global(.el-popper.is-light .rr-theme-name),
|
||||
:global(.el-popper.is-light .rr-theme-picker-title),
|
||||
:global(.el-popper.is-light .rr-theme-hint) {
|
||||
color: var(--ink) !important;
|
||||
}
|
||||
[data-theme="dark"] :global(.el-popper.is-light) {
|
||||
background: var(--surface) !important;
|
||||
color: var(--ink) !important;
|
||||
}
|
||||
[data-theme="dark"] :global(.el-popper.is-light .rr-theme-name),
|
||||
[data-theme="dark"] :global(.el-popper.is-light .rr-theme-picker-title),
|
||||
[data-theme="dark"] :global(.el-popper.is-light .rr-theme-hint) {
|
||||
color: var(--ink) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
365
src/config/menu-labels.ts
Normal file
365
src/config/menu-labels.ts
Normal file
@ -0,0 +1,365 @@
|
||||
/**
|
||||
* F006 W3.1.5 菜单标签映射表
|
||||
*
|
||||
* 重要原则(2026-06-17 修订):
|
||||
* - 菜单名称**完全照搬老 renren 后台**(`sys_language` 表里的中文名)
|
||||
* - 不自创、不推测、不按业务理解改名
|
||||
* - 老版本 sys_menu 表本身没有 name 字段(只有 pid/url/sort)
|
||||
* - 中文名存在 sys_language 国际化表,后端 /sys/menu/nav 接口没 join,
|
||||
* 所以前端拿到的 name 是 null
|
||||
* - 真实中文名来源:devbox 库 sys_language WHERE table_name='sys_menu' AND language='zh-CN'
|
||||
*
|
||||
* 字段说明(全部来自真实 SQL 查询结果):
|
||||
* - url: 后端 menu 表 url 字段
|
||||
* - label: sys_language 表 field_value 字段(老菜单中文名,完全照搬)
|
||||
* - icon: Element Plus icon(可改)
|
||||
* - path: 前端路由(去掉 /admin 前缀,base 自动拼)
|
||||
* - sort: 排序(可改,影响侧边栏顺序)
|
||||
*
|
||||
* V2 R2 改动 (2026-07-03):
|
||||
* - 砍掉 'statistics/index-town' (数据统计), 用户明示 '概览只保留首页'
|
||||
* - 配合 menu.ts filterByFrontendTruth: 后端拉到的菜单, url 不在 MENU_LABELS 里全砍
|
||||
*
|
||||
* 数据来源 SQL:
|
||||
* SELECT m.url, l.field_value
|
||||
* FROM sys_role_menu rm
|
||||
* INNER JOIN sys_role_user ru ON ru.role_id = rm.role_id
|
||||
* INNER JOIN sys_menu m ON m.id = rm.menu_id
|
||||
* LEFT JOIN sys_language l
|
||||
* ON l.table_name = 'sys_menu' AND l.table_id = m.id
|
||||
* AND l.field_name = 'name' AND l.language = 'zh-CN'
|
||||
* WHERE ru.user_id = 1
|
||||
* GROUP BY m.url
|
||||
*/
|
||||
|
||||
export interface MenuLabel {
|
||||
url: string
|
||||
label: string
|
||||
icon: string
|
||||
path: string
|
||||
sort?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 url 精确匹配 70 个菜单 label(全部来自 sys_language 表,完全照搬老名字)
|
||||
* 排序按菜单在老系统里的展示顺序(基于 sys_menu.sort 字段聚合)
|
||||
*/
|
||||
export const MENU_LABELS: MenuLabel[] = [
|
||||
// ===== 顶部菜单(原老 vue 一级) =====
|
||||
{ url: 'statistics/index', label: '首页', icon: 'HomeFilled', path: '/dashboard', sort: 1 },
|
||||
{ url: 'order/order', label: '订单管理', icon: 'Document', path: '/order', sort: 3 },
|
||||
{ url: 'orderList/index', label: '订单列表', icon: 'List', path: '/order/list', sort: 4 },
|
||||
{ url: 'orderMake/index', label: '开单', icon: 'Edit', path: '/order/create', sort: 5 },
|
||||
{ url: 'villageManage/manageList', label: '村庄管理', icon: 'Location', path: '/village', sort: 6 },
|
||||
{ url: 'village/user', label: '村民用户', icon: 'User', path: '/village/user', sort: 7 },
|
||||
{ url: 'supplyList/index', label: '供应商列表', icon: 'Shop', path: '/supply', sort: 8 },
|
||||
|
||||
// ===== 用户模块 =====
|
||||
{ url: 'user/user', label: '用户列表', icon: 'User', path: '/user/user', sort: 100 },
|
||||
{ url: 'user/enterprise/enterprise', label: '企业列表', icon: 'OfficeBuilding', path: '/user/enterprise', sort: 101 },
|
||||
{ url: 'user/enterprise/enterprise-rating', label: '评级', icon: 'Star', path: '/user/enterprise/rating', sort: 102 },
|
||||
{ url: 'user/enterprise/enterprise-level', label: '企业等级管理', icon: 'Medal', path: '/user/enterprise/level', sort: 103 },
|
||||
{ url: 'user/enterprise/enterprise-score', label: '评分', icon: 'DataAnalysis', path: '/user/enterprise/score', sort: 104 },
|
||||
{ url: 'user/finance/recharge', label: '充值管理', icon: 'Wallet', path: '/user/finance/recharge', sort: 110 },
|
||||
{ url: 'user/finance/recharge-tx', label: '提现管理', icon: 'CreditCard', path: '/user/finance/recharge-tx', sort: 111 },
|
||||
{ url: 'user/finance/revenue-sharing', label: '分账记录管理', icon: 'Share', path: '/user/finance/revenue', sort: 112 },
|
||||
{ url: 'user/finance/transaction-record', label: '交易流水', icon: 'List', path: '/user/finance/transaction', sort: 113 },
|
||||
{ url: 'user/message/message', label: '系统消息', icon: 'ChatDotRound', path: '/user/message', sort: 120 },
|
||||
{ url: 'user/message/messageHandel', label: '待处理消息', icon: 'ChatLineRound', path: '/user/message/handel', sort: 121 },
|
||||
|
||||
// ===== 订单模块 =====
|
||||
{ url: 'order/ordergood', label: '商品订单', icon: 'Goods', path: '/order/ordergood', sort: 200 },
|
||||
{ url: 'order/serviceorder', label: '服务订单', icon: 'Service', path: '/order/serviceorder', sort: 201 },
|
||||
{ url: 'order/groupsupplierorder', label: '商品拼团订单', icon: 'GoodsFilled', path: '/order/group-supplier', sort: 202 },
|
||||
{ url: 'order/groupserviceorder', label: '服务拼团订单', icon: 'Service', path: '/order/group-service', sort: 203 },
|
||||
{ url: 'order/orderafter', label: '商品售后订单', icon: 'RefreshLeft', path: '/order/ordergood/after', sort: 210 },
|
||||
{ url: 'order/orderserviceafter', label: '服务售后订单', icon: 'RefreshRight', path: '/order/serviceorder/after', sort: 211 },
|
||||
{ url: 'order/ordergood-commission', label: '实物订单分佣统计', icon: 'Coin', path: '/order/ordergood/commission', sort: 220 },
|
||||
{ url: 'order/serviceorder-commission', label: '服务订单分佣统计', icon: 'Coin', path: '/order/serviceorder/commission', sort: 221 },
|
||||
{ url: 'order/orderbase-price-analysis', label: '基础价格分析表', icon: 'TrendCharts', path: '/order/base-price-analysis', sort: 230 },
|
||||
|
||||
// ===== 供应商模块 =====
|
||||
{ url: 'supplier/good', label: '实物商品', icon: 'Box', path: '/supplier/good', sort: 300 },
|
||||
{ url: 'supplier/servicegood', label: '服务商品', icon: 'Box', path: '/supplier/service', sort: 301 },
|
||||
{ url: 'supplier/activityzone', label: '活动专区', icon: 'Promotion', path: '/supplier/activity', sort: 302 },
|
||||
{ url: 'supplier/coupon', label: '优惠券', icon: 'Ticket', path: '/supplier/coupon', sort: 303 },
|
||||
{ url: 'supplier/promoter', label: '推广返佣', icon: 'UserFilled', path: '/supplier/promoter', sort: 304 },
|
||||
{ url: 'supplier/inquiry/inquiry', label: '询价列表', icon: 'ChatLineSquare', path: '/supplier/inquiry', sort: 305 },
|
||||
|
||||
// ===== 系统模块 =====
|
||||
{ url: 'sys/user', label: '用户管理[后台用户]', icon: 'UserFilled', path: '/sys/user', sort: 400 },
|
||||
{ url: 'sys/role', label: '角色管理[后台用户]', icon: 'User', path: '/sys/role', sort: 401 },
|
||||
{ url: 'sys/post', label: '岗位管理[后台用户]', icon: 'Postcard', path: '/sys/post', sort: 402 },
|
||||
{ url: 'sys/dept', label: '部门管理[后台用户]', icon: 'OfficeBuilding', path: '/sys/dept', sort: 403 },
|
||||
{ url: 'sys/menu', label: '菜单管理', icon: 'Menu', path: '/sys/menu', sort: 410 },
|
||||
{ url: 'sys/region', label: '行政区域', icon: 'LocationFilled', path: '/sys/region', sort: 411 },
|
||||
{ url: 'sys/log-login', label: '登录日志', icon: 'Key', path: '/sys/log/login', sort: 420 },
|
||||
{ url: 'sys/log-operation', label: '操作日志', icon: 'EditPen', path: '/sys/log/operation', sort: 421 },
|
||||
{ url: 'sys/log-error', label: '异常日志', icon: 'WarningFilled', path: '/sys/log/error', sort: 422 },
|
||||
{ url: 'sys/userManagement', label: '全国运营中心', icon: 'Setting', path: '/sys/userManagement', sort: 430 },
|
||||
{ url: 'sys/operation?_mt=省级运营中心&operationType=2', label: '省级运营中心', icon: 'School', path: '/sys/operation/province', sort: 431 },
|
||||
{ url: 'sys/operation?_mt=县级运营中心&operationType=3', label: '县级运营中心', icon: 'OfficeBuilding', path: '/sys/operation/county', sort: 432 },
|
||||
{ url: 'sys/operation?_mt=行业协会运营中心&operationType=4', label: '行业协会运营', icon: 'Connection', path: '/sys/operation/association', sort: 433 },
|
||||
|
||||
// ===== 课程模块 =====
|
||||
{ url: 'course/coupon', label: '优惠券管理', icon: 'Ticket', path: '/course/coupon', sort: 500 },
|
||||
{ url: 'course/sensitive', label: '敏感词汇管理', icon: 'WarningFilled', path: '/course/sensitive', sort: 501 },
|
||||
|
||||
// ===== API 模块 =====
|
||||
{ url: 'api/lunbo', label: '轮播管理', icon: 'Picture', path: '/api/lunbo', sort: 600 },
|
||||
{ url: 'api/feedback', label: '意见反馈', icon: 'ChatLineRound', path: '/api/feedback', sort: 601 },
|
||||
{ url: 'api/help', label: '帮助中心', icon: 'Notebook', path: '/api/help', sort: 602 },
|
||||
|
||||
// ===== 其它模块(老版本"其它"分组) =====
|
||||
{ url: 'other/textconfig', label: '常量配置', icon: 'Document', path: '/other/textconfig', sort: 700 },
|
||||
{ url: 'other/help', label: '帮助中心', icon: 'QuestionFilled', path: '/other/help', sort: 701 },
|
||||
{ url: 'other/operateguide', label: '内容列表', icon: 'Reading', path: '/other/guide', sort: 702 },
|
||||
{ url: 'other/industry', label: '行业', icon: 'OfficeBuilding', path: '/other/industry', sort: 703 },
|
||||
{ url: 'other/label', label: '标签管理', icon: 'PriceTag', path: '/other/label', sort: 704 },
|
||||
{ url: 'other/categoryGood', label: '平台分类', icon: 'Menu', path: '/other/category/good', sort: 705 },
|
||||
{ url: 'other/categoryRM', label: '热门搜索记录', icon: 'Search', path: '/other/category/rm', sort: 706 },
|
||||
{ url: 'other/categoryZX', label: '资讯分类', icon: 'Menu', path: '/other/category/zx', sort: 707 },
|
||||
{ url: 'other/goodunit', label: '商品单位', icon: 'Box', path: '/other/goodunit', sort: 708 },
|
||||
{ url: 'other/supplierBrand', label: '品牌管理', icon: 'Star', path: '/other/brand', sort: 709 },
|
||||
{ url: 'other/groupbuy', label: '拼团活动', icon: 'GoodsFilled', path: '/other/groupbuy', sort: 710 },
|
||||
{ url: 'other/seckill', label: '秒杀活动', icon: 'Lightning', path: '/other/seckill', sort: 711 },
|
||||
{ url: 'other/information', label: '资讯列表', icon: 'Document', path: '/other/information', sort: 712 },
|
||||
|
||||
// ===== 基础设施(message/job/oss) =====
|
||||
{ url: 'message/sms', label: '短信配置', icon: 'ChatDotRound', path: '/message/sms', sort: 800 },
|
||||
{ url: 'job/schedule', label: '定时任务', icon: 'Timer', path: '/job/schedule', sort: 801 },
|
||||
{ url: 'oss/oss', label: '文件上传', icon: 'FolderOpened', path: '/oss', sort: 802 },
|
||||
|
||||
// ===== 开发工具(devtools) =====
|
||||
{ url: 'devtools/generator', label: '代码模板', icon: 'MagicStick', path: '/devtools/generator', sort: 900 },
|
||||
{ url: 'devtools/datasource', label: '数据源配置', icon: 'Coin', path: '/devtools/datasource', sort: 901 },
|
||||
{ url: 'devtools/param', label: '参数配置', icon: 'SetUp', path: '/devtools/param', sort: 902 },
|
||||
{ url: 'devtools/baseclass', label: '基类配置', icon: 'Files', path: '/devtools/baseclass', sort: 903 },
|
||||
{ url: 'devtools/template', label: '模板配置', icon: 'Tickets', path: '/devtools/template', sort: 904 },
|
||||
{ url: 'devtools/fieldtype', label: '字段配置', icon: 'CollectionTag', path: '/devtools/fieldtype', sort: 905 },
|
||||
|
||||
// ===== F002 智能体集成 =====
|
||||
{ url: 'openapi/app', label: '应用列表', icon: 'Cellphone', path: '/openapi/app', sort: 1000 },
|
||||
{ url: 'openapi/audit-log', label: '审计日志', icon: 'Document', path: '/openapi/audit-log', sort: 1001 },
|
||||
{ url: 'openapi/company-account', label: '公司账号管理', icon: 'Avatar', path: '/openapi/company', sort: 1002 },
|
||||
|
||||
// ===== 补充缺失的菜单项(super_admin=1 时可见) =====
|
||||
{ url: 'other/categoryZH', label: '综合分类', icon: 'Menu', path: '/other/category/zh', sort: 708 },
|
||||
{ url: 'other/categoryCZ', label: '城镇分类', icon: 'Menu', path: '/other/category/cz', sort: 709 },
|
||||
{ url: 'sys/dict-type', label: '字典类型', icon: 'Collection', path: '/sys/dict', sort: 412 },
|
||||
{ url: 'sys/notice', label: '通知公告', icon: 'Bell', path: '/sys/notice', sort: 413 },
|
||||
{ url: 'sys/params', label: '参数配置', icon: 'Setting', path: '/sys/params', sort: 414 },
|
||||
{ url: 'pay/order', label: '支付订单', icon: 'Money', path: '/pay/order', sort: 810 },
|
||||
{ url: 'pay/alipaynotifylog', label: '支付宝通知日志', icon: 'Document', path: '/pay/alipay', sort: 811 },
|
||||
{ url: 'sys/news', label: '资讯管理', icon: 'Document', path: '/sys/news', sort: 415 },
|
||||
{ url: 'sys/ureport', label: '报表设计', icon: 'DataAnalysis', path: '/sys/ureport', sort: 416 },
|
||||
{ url: 'sys/excel', label: 'Excel导出', icon: 'Download', path: '/sys/excel', sort: 417 },
|
||||
{ url: 'sys/charts', label: '图表示例', icon: 'TrendCharts', path: '/sys/charts', sort: 418 },
|
||||
{ url: 'sys/product', label: '商品示例', icon: 'Box', path: '/sys/product', sort: 419 },
|
||||
{ url: 'product/shop', label: '店铺管理', icon: 'Shop', path: '/product/shop', sort: 350 },
|
||||
{ url: 'orderMake/index-admin', label: '开单(管理端)', icon: 'Edit', path: '/order/create-admin', sort: 6 },
|
||||
{ url: 'villageManage/farmerList', label: '农户列表', icon: 'User', path: '/village/farmer', sort: 7 },
|
||||
{ url: 'villageManage/resourceList', label: '资源列表', icon: 'List', path: '/village/resource', sort: 8 },
|
||||
{ url: 'supplyList/index', label: '供应商列表', icon: 'Shop', path: '/supply', sort: 9 },
|
||||
{ url: 'api/user', label: 'API用户', icon: 'User', path: '/api/user', sort: 603 },
|
||||
{ url: 'api/label', label: 'API标签', icon: 'PriceTag', path: '/api/label', sort: 604 },
|
||||
{ url: 'api/teacher', label: '教师管理', icon: 'User', path: '/api/teacher', sort: 605 },
|
||||
{ url: 'api/question', label: '题库管理', icon: 'Document', path: '/api/question', sort: 606 },
|
||||
{ url: 'api/answerrecord', label: '答题记录', icon: 'List', path: '/api/answer', sort: 607 },
|
||||
{ url: 'api/broker', label: '经纪人', icon: 'User', path: '/api/broker', sort: 608 },
|
||||
{ url: 'api/broker-securities', label: '证券经纪人', icon: 'User', path: '/api/broker-securities', sort: 609 },
|
||||
{ url: 'api/withdrawal', label: '提现管理', icon: 'Money', path: '/api/withdrawal', sort: 610 },
|
||||
{ url: 'api/balancedetail', label: '余额明细', icon: 'Wallet', path: '/api/balance', sort: 611 },
|
||||
{ url: 'message/mail-template', label: '邮件模板', icon: 'Document', path: '/message/mail-template', sort: 803 },
|
||||
{ url: 'message/mail-log', label: '邮件日志', icon: 'List', path: '/message/mail-log', sort: 804 },
|
||||
{ url: 'message/sms-log', label: '短信日志', icon: 'List', path: '/message/sms-log', sort: 805 },
|
||||
{ url: 'sys/notice-user', label: '用户通知', icon: 'Bell', path: '/sys/notice-user', sort: 420 },
|
||||
{ url: 'message/messageinitiavtive', label: '主动消息', icon: 'ChatDotRound', path: '/message/initiative', sort: 806 }
|
||||
]
|
||||
|
||||
/** fallback:未匹配到的菜单项用 url 当 label */
|
||||
const FALLBACK_ICON = 'Folder'
|
||||
const FALLBACK_ICON_OPEN = 'FolderOpened'
|
||||
|
||||
/**
|
||||
* W3.1.5 父菜单 fallback:按后端菜单 id 取 sys_language 表真实中文名
|
||||
* 数据来源 SQL: SELECT id, l.field_value FROM sys_menu m LEFT JOIN sys_language l ON ...
|
||||
* WHERE m.url IS NULL OR m.url = ''
|
||||
* AND l.language='zh-CN' AND l.field_name='name'
|
||||
* 父菜单 label 完全照搬老 sys_language,不自创
|
||||
*/
|
||||
const PARENT_LABELS: Record<string, string> = {
|
||||
'1067246875800000002': '权限管理',
|
||||
'1067246875800000019': '系统设置',
|
||||
'1067246875800000036': '日志管理',
|
||||
'1067246875800000040': '系统监控',
|
||||
'1067246875800000056': '消息管理',
|
||||
'1067246875800000401': '站内通知',
|
||||
'1176372255559024642': '功能示例',
|
||||
'1300278047072649217': '报表管理',
|
||||
'1302850622416084993': '开发者工具',
|
||||
'1343074487777677313': '支付管理',
|
||||
'1540300976681046018': '用户管理',
|
||||
'1540301017017667586': '商品管理',
|
||||
'1540301052094631937': '学籍注册',
|
||||
'1540301089407160321': '社交管理',
|
||||
'1540301128468713473': '财务管理',
|
||||
'1540301447978209282': '直播管理',
|
||||
'1540301516206952450': '通知管理',
|
||||
'1544617372869099522': '问卷调查',
|
||||
'1544959991742398465': '教师管理',
|
||||
'1742454355579543554': '基础设置',
|
||||
'1744249214351290369': '商家管理',
|
||||
'1746120268749336577': '采购管理',
|
||||
'1746725446628159489': '订单管理',
|
||||
'1975823255637856257': '商品管理',
|
||||
'1975838472371470338': '企业管理',
|
||||
'1976205447087706114': '订单管理',
|
||||
'1976560833393278978': '营销活动管理',
|
||||
'1976857788973879298': '行业资讯',
|
||||
'1976944980614221826': '操作指南',
|
||||
'1978641764218593282': '询价管理',
|
||||
'1979816210488897537': '层级架构',
|
||||
'1980456810582228993': '财务管理',
|
||||
'1985599529856065537': '消息通知',
|
||||
'2500000000000000001': '集成管理'
|
||||
}
|
||||
|
||||
/**
|
||||
* W3.5.4 父菜单 title -> icon 映射
|
||||
* 老菜单数据里父菜单没 icon 字段,根据 PARENT_LABELS 真实中文名映射差异化图标
|
||||
*/
|
||||
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'
|
||||
}
|
||||
|
||||
export interface ResolvedMenu {
|
||||
path: string
|
||||
title: string
|
||||
icon: string
|
||||
url: string
|
||||
id: string
|
||||
children?: ResolvedMenu[]
|
||||
sort: number
|
||||
}
|
||||
|
||||
export interface BackendMenuItem {
|
||||
id: string
|
||||
pid: string
|
||||
name: string | null
|
||||
url: string
|
||||
icon: string
|
||||
type: number
|
||||
openStyle: number
|
||||
permissions: string
|
||||
sort: number
|
||||
createDate: string
|
||||
parentName: string | null
|
||||
children?: BackendMenuItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个后端菜单项 -> 前端 ResolvedMenu
|
||||
* 1. 父菜单没 url 但有 children 的:优先用 PARENT_LABELS[id] 查老 sys_language 真实中文名
|
||||
* 2. 子菜单按 url 精确匹配 MENU_LABELS(老 sys_language 真实中文名)
|
||||
* 3. 匹配不到用 fallback(url 当 label + Folder 图标)
|
||||
*/
|
||||
export function resolveMenuItem(item: BackendMenuItem): ResolvedMenu | null {
|
||||
if (!item.url) {
|
||||
if (!item.children?.length) return null
|
||||
const resolved = item.children.map(resolveMenuItem).filter(Boolean) as ResolvedMenu[]
|
||||
if (!resolved.length) return null
|
||||
resolved.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||
return {
|
||||
path: resolved[0].path,
|
||||
title: PARENT_LABELS[item.id] || resolved[0].title || '其它',
|
||||
icon: mapIcon(item.icon) || PARENT_ICONS[PARENT_LABELS[item.id] || ''] || FALLBACK_ICON_OPEN,
|
||||
url: item.url,
|
||||
id: item.id,
|
||||
children: resolved,
|
||||
sort: item.sort ?? 999
|
||||
}
|
||||
}
|
||||
|
||||
const label = MENU_LABELS.find(m => m.url === item.url || m.url === item.url.replace(/^\//, '') || m.url === '/' + item.url)
|
||||
if (label) {
|
||||
return {
|
||||
path: label.path,
|
||||
title: label.label,
|
||||
icon: label.icon,
|
||||
url: item.url,
|
||||
id: item.id,
|
||||
sort: label.sort ?? item.sort ?? 999
|
||||
}
|
||||
}
|
||||
return {
|
||||
path: `/admin/other/${item.id}`,
|
||||
title: guessTitleFromUrl(item.url),
|
||||
icon: FALLBACK_ICON,
|
||||
url: item.url,
|
||||
id: item.id,
|
||||
sort: item.sort ?? 999
|
||||
}
|
||||
}
|
||||
|
||||
function mapIcon(backendIcon: string): string {
|
||||
if (!backendIcon) return ''
|
||||
const ICON_MAP: Record<string, string> = {
|
||||
'icon-Report': 'TrendCharts',
|
||||
'icon-setting-fill': 'Setting',
|
||||
'icon-user': 'User',
|
||||
'icon-bank': 'OfficeBuilding',
|
||||
'icon-Console-SQL': 'DataAnalysis',
|
||||
'icon-dribbble-circle-fill': 'MagicStick',
|
||||
'icon-file-text': 'Document',
|
||||
'icon-folder-open-fill': 'FolderOpened',
|
||||
'icon-notification-fill': 'Bell',
|
||||
'icon-propertysafety-fill': 'Lock',
|
||||
'icon-sound-fill': 'Microphone',
|
||||
'icon-securityscan': 'Shield',
|
||||
'icon-setting': 'Tools',
|
||||
'icon-flag': 'Flag',
|
||||
'icon-lock': 'Lock',
|
||||
'icon-container': 'Box',
|
||||
'icon-pushpin': 'Document',
|
||||
'icon-appstore-fill': 'Grid',
|
||||
'icon-edit': 'Edit',
|
||||
'icon-app': 'Cellphone',
|
||||
'icon-company': 'Avatar',
|
||||
'icon-link': 'Link',
|
||||
'icon-folder': 'Folder'
|
||||
}
|
||||
return ICON_MAP[backendIcon] || FALLBACK_ICON
|
||||
}
|
||||
|
||||
function guessTitleFromUrl(url: string): string {
|
||||
if (!url) return ''
|
||||
const last = url.split('/').pop() || url
|
||||
return last.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
|
||||
}
|
||||
|
||||
export function transformMenu(backend: BackendMenuItem[]): ResolvedMenu[] {
|
||||
const resolved = backend.map(resolveMenuItem).filter(Boolean) as ResolvedMenu[]
|
||||
resolved.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||
return resolved
|
||||
}
|
||||
@ -1,8 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 登录页
|
||||
* 架构:版式所有主题统一(渐变背景 + 左 60% 品牌区 + 右 40% 登录卡),只在主题切换时换色
|
||||
* 4 套主题登录页都遵循同一布局,这是"可配置性"的体现
|
||||
* W3.7:按 http://localhost:8770/theme-switcher.html?theme=tech-blue 设计稿 1:1 还原
|
||||
* 设计稿关键数值:
|
||||
* - 舞台 1324×600 圆角 16px
|
||||
* - 左 65% (flex 1.857) / 右 35% (flex 1)
|
||||
* - input 380×38 圆角 8px 背景 #F8FAFC 边框 #E2E8F0 padding-left 34px(给 icon)
|
||||
* - 验证码 110×38 圆角 8px 背景 #DBEAFE 字 17px 600 蓝
|
||||
* - 按钮 380×40 圆角 8px 字 14px 600 白 阴影 glow
|
||||
* - 复选框 + 忘记密码 + divider + SSO 2 按钮
|
||||
*/
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
@ -11,11 +17,15 @@ import qs from 'qs'
|
||||
import axios from 'axios'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { computed } from 'vue'
|
||||
import logoUrl from '@/assets/logo-jubian.png'
|
||||
import logoLightUrl from '@/assets/logo-jubian-light.png'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const themeStore = useThemeStore()
|
||||
const logoSrc = computed(() => themeStore.current === 'dark' ? logoLightUrl : logoUrl)
|
||||
|
||||
const form = reactive({
|
||||
username: 'admin',
|
||||
@ -26,11 +36,10 @@ const form = reactive({
|
||||
const captchaUrl = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
// F006 主题色色板 - 用于登录页底部展示
|
||||
const themePalette = [
|
||||
{ value: 'tech-blue', name: '科技蓝', color: '#2563EB' },
|
||||
{ value: 'red', name: '中国红', color: '#DC2626' },
|
||||
{ value: 'purple', name: 'Notion 紫', color: '#5645D4' },
|
||||
{ value: 'purple', name: '深空紫', color: '#5645D4' },
|
||||
{ value: 'dark', name: '暗夜黑', color: '#0F172A' }
|
||||
]
|
||||
|
||||
@ -38,6 +47,10 @@ function refreshCaptcha() {
|
||||
form.uuid = Math.random().toString(36).slice(2, 12)
|
||||
captchaUrl.value = `/auth/captcha?uuid=${form.uuid}&_t=${Date.now()}`
|
||||
}
|
||||
function onCaptchaInput(e: Event) {
|
||||
const t = e.target as HTMLInputElement
|
||||
form.captcha = t.value
|
||||
}
|
||||
refreshCaptcha()
|
||||
|
||||
function pickTheme(value: string) {
|
||||
@ -51,22 +64,22 @@ async function onLogin() {
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
// 沿用老 renren 的 OAuth2 client_credentials 流程
|
||||
const res = await axios.post(
|
||||
'/auth/oauth/token',
|
||||
qs.stringify({ ...form, grant_type: 'password' }),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: 'Basic cmVucmVuaW86cmVucmVuaW8='
|
||||
Authorization: 'Basic cmVucmVuaW86cmVucmVuaW8=',
|
||||
client: 'front'
|
||||
}
|
||||
}
|
||||
)
|
||||
if (res.data?.code === 0 && res.data?.data?.access_token) {
|
||||
if (res.data?.code === 0 && res.data?.access_token) {
|
||||
userStore.setAuth({
|
||||
token: res.data.data.access_token,
|
||||
token: res.data.access_token,
|
||||
username: form.username,
|
||||
permissions: res.data.data.permissions || ['*:*:*']
|
||||
permissions: res.data.permissions || ['*:*:*']
|
||||
})
|
||||
ElMessage.success('登录成功')
|
||||
const redirect = (route.query.redirect as string) || '/'
|
||||
@ -86,12 +99,12 @@ async function onLogin() {
|
||||
|
||||
<template>
|
||||
<div class="rr-login">
|
||||
<div class="rr-login-wrap">
|
||||
<!-- 左 60%: 品牌区 -->
|
||||
<div class="rr-login-stage">
|
||||
<!-- 左 65%: 品牌区 -->
|
||||
<div class="rr-login-left">
|
||||
<div class="rr-login-brand">
|
||||
<div class="rr-login-logo">P</div>
|
||||
<div class="rr-login-product-name">Product Network</div>
|
||||
<img class="rr-login-logo" :src="logoSrc" alt="巨变" />
|
||||
<div class="rr-login-product-name">产业互联网平台运营后台</div>
|
||||
</div>
|
||||
|
||||
<div class="rr-login-display">
|
||||
@ -119,35 +132,47 @@ async function onLogin() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右 40%: 登录卡 -->
|
||||
<!-- 右 35%: 登录卡 -->
|
||||
<div class="rr-login-right">
|
||||
<div class="rr-login-card">
|
||||
<h4 class="rr-login-card-title">
|
||||
登录工作台
|
||||
<span class="rr-login-card-sub">输入账号继续</span>
|
||||
</h4>
|
||||
<div class="rr-login-card-title">登录工作台</div>
|
||||
<div class="rr-login-card-sub">输入账号继续</div>
|
||||
|
||||
<el-form @keyup.enter="onLogin" label-position="top">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.username" placeholder="账号" size="large" :prefix-icon="'User'" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="form.password" type="password" placeholder="密码" size="large" show-password :prefix-icon="'Lock'" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="rr-login-captcha-row">
|
||||
<el-input v-model="form.captcha" placeholder="验证码" size="large" :prefix-icon="'Picture'" />
|
||||
<div class="rr-login-captcha-box" @click="refreshCaptcha">
|
||||
{{ captchaUrl ? '点击刷新' : '加载中' }}
|
||||
<form class="rr-login-fields" @submit.prevent="onLogin">
|
||||
<div class="rr-field">
|
||||
<label class="rr-field-label">账号</label>
|
||||
<div class="rr-input-wrap">
|
||||
<el-icon class="rr-input-icon"><User /></el-icon>
|
||||
<input v-model="form.username" class="rr-input" placeholder="请输入账号" autocomplete="username" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rr-field">
|
||||
<label class="rr-field-label">密码</label>
|
||||
<div class="rr-input-wrap">
|
||||
<el-icon class="rr-input-icon"><Lock /></el-icon>
|
||||
<input v-model="form.password" type="password" class="rr-input" placeholder="请输入密码" autocomplete="current-password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rr-field">
|
||||
<label class="rr-field-label">验证码</label>
|
||||
<div class="rr-captcha-row">
|
||||
<div class="rr-input-wrap">
|
||||
<el-icon class="rr-input-icon"><Picture /></el-icon>
|
||||
<input v-model="form.captcha" class="rr-input" placeholder="请输入验证码" maxlength="20" @input="onCaptchaInput($event)" />
|
||||
</div>
|
||||
<div class="rr-captcha-box" @click="refreshCaptcha" title="点击刷新">
|
||||
<img v-if="captchaUrl" :src="captchaUrl" class="rr-captcha-img" alt="验证码" />
|
||||
<span v-else class="rr-captcha-loading">加载中</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="large" :loading="loading" @click="onLogin" class="rr-login-btn">
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="rr-btn-primary" :disabled="loading">
|
||||
{{ loading ? '登录中…' : '登 录' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="rr-login-theme-section">
|
||||
<div class="rr-login-theme-label">选择主题</div>
|
||||
@ -170,34 +195,55 @@ async function onLogin() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/**
|
||||
* 设计稿精确数值(从 design.html Playwright 读出来):
|
||||
* 舞台 1324×600 圆角 16px / 左 65% 右 35% / input 380×38 圆角 8px / 按钮 380×40 圆角 8px
|
||||
* 输入背景 #F8FAFC 边框 #E2E8F0 圆角 8px / 验证码背景 #DBEAFE / 按钮阴影 0 1px 2px rgba(37,99,235,0.15)
|
||||
*/
|
||||
.rr-login {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: var(--bg-soft);
|
||||
position: relative;
|
||||
background: linear-gradient(145deg, var(--login-grad-1) 0%, var(--login-grad-2) 50%, var(--login-grad-3) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 24px;
|
||||
}
|
||||
.rr-login-wrap {
|
||||
width: 1080px;
|
||||
|
||||
/* 舞台(套设计稿 .login-stage) */
|
||||
.rr-login-stage {
|
||||
width: 1324px;
|
||||
height: 600px;
|
||||
max-width: 96%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
position: relative;
|
||||
background: linear-gradient(145deg, var(--login-grad-1) 0%, var(--login-grad-2) 50%, var(--login-grad-3) 100%);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 20px 50px -12px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.rr-login-left { display: none; }
|
||||
.rr-login-right { width: 100% !important; }
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--brand) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--brand) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
opacity: 0.05;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
& > * { position: relative; z-index: 1; }
|
||||
}
|
||||
|
||||
/* 左 60% 品牌区 - 渐变 + 双色径向辉光 + 网格底纹(由主题变量驱动) */
|
||||
/* 左 65% 品牌区 */
|
||||
.rr-login-left {
|
||||
flex: 1.4;
|
||||
flex: 1.857;
|
||||
padding: 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -231,33 +277,24 @@ async function onLogin() {
|
||||
}
|
||||
& > * { position: relative; z-index: 1; }
|
||||
}
|
||||
.rr-login-brand {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.rr-login-brand { display: flex; align-items: center; gap: 10px; }
|
||||
.rr-login-logo {
|
||||
width: 36px; height: 36px;
|
||||
background: var(--brand);
|
||||
border-radius: 9px;
|
||||
color: white;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; font-size: 18px;
|
||||
object-fit: contain;
|
||||
padding: 2px;
|
||||
box-shadow: 0 6px 16px var(--brand-glow);
|
||||
background: var(--surface);
|
||||
}
|
||||
.rr-login-product-name {
|
||||
font-size: 16px; font-weight: 600; color: var(--ink);
|
||||
}
|
||||
.rr-login-product-name { font-size: 16px; font-weight: 600; color: var(--ink); }
|
||||
.rr-login-display { max-width: 460px; }
|
||||
.rr-login-headline {
|
||||
font-size: 42px; font-weight: 700; line-height: 1.1;
|
||||
letter-spacing: -0.03em; margin-bottom: 16px; color: var(--ink);
|
||||
}
|
||||
.rr-login-accent { color: var(--brand); }
|
||||
.rr-login-sub {
|
||||
color: var(--ink-2); font-size: 15px; line-height: 1.6;
|
||||
}
|
||||
.rr-login-features {
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.rr-login-sub { color: var(--ink-2); font-size: 15px; line-height: 1.6; }
|
||||
.rr-login-features { display: flex; flex-direction: column; gap: 10px; }
|
||||
.rr-login-feature {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-size: 13.5px; color: var(--ink-2);
|
||||
@ -271,46 +308,116 @@ async function onLogin() {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 右 40% 登录卡 */
|
||||
/* 右 35% 登录卡(半透明白让 stage 渐变+网格透过来) */
|
||||
.rr-login-right {
|
||||
flex: 1;
|
||||
background: var(--surface);
|
||||
padding: 48px 44px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
|
||||
// W5.x.x 暗主题适配:登录卡深色半透明,让 var(--ink) 系文字清晰可见
|
||||
[data-theme="dark"] & {
|
||||
background: rgba(30, 41, 59, 0.85);
|
||||
}
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
padding: 48px 40px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.rr-login-card { width: 100%; max-width: 360px; }
|
||||
.rr-login-card { width: 100%; max-width: 380px; }
|
||||
|
||||
/* 标题(套设计稿 .login-card-title 20px 700 + .login-card-sub 13px 400 灰) */
|
||||
.rr-login-card-title {
|
||||
font-size: 22px; font-weight: 600; color: var(--ink);
|
||||
letter-spacing: -0.015em;
|
||||
display: flex; align-items: baseline; gap: 10px;
|
||||
margin-bottom: 28px;
|
||||
font-size: 20px; font-weight: 700; color: var(--ink);
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.rr-login-card-sub {
|
||||
font-size: 13px; font-weight: 400; color: var(--ink-3);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.rr-login-captcha-row {
|
||||
|
||||
/* 字段(套设计稿 .field + label 12px 500 + .input-wrap) */
|
||||
.rr-login-fields { display: flex; flex-direction: column; gap: 14px; }
|
||||
.rr-field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.rr-field-label {
|
||||
font-size: 12px; font-weight: 500; color: var(--ink-2);
|
||||
}
|
||||
.rr-input-wrap {
|
||||
position: relative;
|
||||
width: 100%; height: 38px;
|
||||
}
|
||||
.rr-input-icon {
|
||||
position: absolute;
|
||||
left: 12px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 14px;
|
||||
color: var(--ink-3);
|
||||
pointer-events: none;
|
||||
}
|
||||
.rr-input {
|
||||
width: 100%; height: 38px;
|
||||
padding: 0 12px 0 34px;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--ink);
|
||||
outline: none;
|
||||
transition: all 0.15s;
|
||||
|
||||
&::placeholder { color: var(--ink-3); }
|
||||
&:focus {
|
||||
border-color: var(--brand);
|
||||
background: var(--surface);
|
||||
box-shadow: 0 0 0 3px var(--brand-soft);
|
||||
}
|
||||
}
|
||||
|
||||
/* 验证码行 */
|
||||
.rr-captcha-row {
|
||||
display: flex; gap: 8px; width: 100%;
|
||||
.el-input { flex: 1; }
|
||||
.rr-input-wrap { flex: 1; }
|
||||
}
|
||||
.rr-login-captcha-box {
|
||||
width: 110px; height: 40px;
|
||||
.rr-captcha-box {
|
||||
width: 130px; height: 40px;
|
||||
background: var(--brand-soft);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px; color: var(--brand);
|
||||
cursor: pointer;
|
||||
letter-spacing: 3px;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rr-login-btn {
|
||||
width: 100%; height: 42px;
|
||||
font-size: 15px; font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
.rr-captcha-img {
|
||||
width: 100%; height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.rr-captcha-loading {
|
||||
font-size: 12px; font-weight: 600;
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
/* 主按钮(套设计稿 .btn-primary 380×40 圆角 8px 阴影) */
|
||||
.rr-btn-primary {
|
||||
width: 100%; height: 40px;
|
||||
background: var(--brand);
|
||||
color: white;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px; font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover { background: var(--brand-hover); }
|
||||
&:active { transform: translateY(1px); }
|
||||
&:disabled { opacity: 0.7; cursor: not-allowed; }
|
||||
}
|
||||
|
||||
/* 主题色板 */
|
||||
.rr-login-theme-section {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
@ -322,9 +429,7 @@ async function onLogin() {
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.rr-login-theme-grid {
|
||||
display: flex; gap: 10px; justify-content: center;
|
||||
}
|
||||
.rr-login-theme-grid { display: flex; gap: 10px; justify-content: center; }
|
||||
.rr-login-theme-dot {
|
||||
width: 24px; height: 24px;
|
||||
border-radius: 50%;
|
||||
@ -339,4 +444,10 @@ async function onLogin() {
|
||||
box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.rr-login-left { display: none; }
|
||||
.rr-login-stage { width: 460px; height: auto; }
|
||||
.rr-login-right { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,38 +1,104 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import { transformMenu, MENU_LABELS, type ResolvedMenu, type BackendMenuItem } from '@/config/menu-labels'
|
||||
|
||||
/**
|
||||
* 菜单 Store
|
||||
* 架构:菜单数据从后端拉(本期先用 mock,后期接真实 API)
|
||||
* 菜单结构:树形,每项 { path, title, icon, children? }
|
||||
* 路由由 store 动态注册(避免硬编码)
|
||||
* W3.7 改用 httpGet 统一封装
|
||||
* W2 D3 改造:从后端 /sys/menu/nav 拉真实菜单树
|
||||
* - 后端返回 name=null,改前端 MENU_LABELS 映射表(详见 config/menu-labels.ts)
|
||||
* - 用 transformMenu 转后端结构 -> 前端 ResolvedMenu
|
||||
* - 同 menu store 暴露给 AppLayout 渲染
|
||||
*/
|
||||
export interface MenuItem {
|
||||
path: string
|
||||
title: string
|
||||
icon?: string
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
// F006 W1 临时菜单(后期从后端 /sys/menu/list 拉)
|
||||
const MOCK_MENU: MenuItem[] = [
|
||||
{ path: '/', title: '数据看板', icon: 'DataLine' }
|
||||
// W2 起逐步加:询价 / 订单 / 供应商 / 用户 / 系统
|
||||
]
|
||||
export type MenuItem = ResolvedMenu
|
||||
|
||||
export const useMenuStore = defineStore('menu', () => {
|
||||
const list = ref<MenuItem[]>(MOCK_MENU)
|
||||
const list = ref<MenuItem[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const collapsed = ref<boolean>(false)
|
||||
/** 上次拉取时间(毫秒) */
|
||||
const fetchedAt = ref<number>(0)
|
||||
|
||||
function toggleCollapsed() {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
async function fetchMenu() {
|
||||
// TODO W1: 调 GET /sys/menu/list,替换 MOCK_MENU
|
||||
// const res = await api.get('/sys/menu/list')
|
||||
// list.value = transformMenu(res.data)
|
||||
/**
|
||||
* F006 W2 D3:调 /sys/menu/nav 拿后端菜单树
|
||||
* 走 vite proxy 转发到 gateway,header 带 Bearer(token 由 httpGet 拦截器自动加)
|
||||
* 5 分钟内不重复拉
|
||||
*/
|
||||
/**
|
||||
* 过滤无效菜单项(测试数据、数字名称等)
|
||||
* super_admin=1 会返回所有菜单,包含很多测试/占位数据
|
||||
*/
|
||||
function filterInvalidMenus(items: ResolvedMenu[]): ResolvedMenu[] {
|
||||
return items.filter(item => {
|
||||
const title = item.title?.trim() || ''
|
||||
// 过滤纯数字标题(如 "2", "70", "1111111111")
|
||||
if (/^\d+$/.test(title)) return false
|
||||
// 过滤"未知菜单"
|
||||
if (title === '未知菜单') return false
|
||||
// 过滤过长的数字串(超过8位)
|
||||
if (/^\d{8,}$/.test(title)) return false
|
||||
// 递归过滤子菜单
|
||||
if (item.children?.length) {
|
||||
item.children = filterInvalidMenus(item.children)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return { list, collapsed, toggleCollapsed, fetchMenu }
|
||||
/**
|
||||
* V2 R2 (方案 B 实施): 后端拉到的菜单, url 不在 MENU_LABELS 里的全砍
|
||||
* 父菜单(无 url 但有 children): 递归砍子菜单, 若砍光连父也删
|
||||
* 极致干净: 前端 MENU_LABELS 是真实意图, 后端 super_admin 全集只是临时数据
|
||||
*/
|
||||
function filterByFrontendTruth(items: ResolvedMenu[]): ResolvedMenu[] {
|
||||
const allowedUrls = new Set(MENU_LABELS.map(m => m.url))
|
||||
return items
|
||||
.map(item => {
|
||||
// 父菜单: 无 url, 递归处理 children
|
||||
if (!item.url) {
|
||||
if (!item.children?.length) return null
|
||||
const filtered = filterByFrontendTruth(item.children)
|
||||
if (!filtered.length) return null
|
||||
return { ...item, children: filtered }
|
||||
}
|
||||
// 子菜单: url 必须在 MENU_LABELS 里
|
||||
return allowedUrls.has(item.url) ? item : null
|
||||
})
|
||||
.filter((item): item is ResolvedMenu => item !== null)
|
||||
}
|
||||
|
||||
async function fetchMenu(force = false) {
|
||||
if (loading.value) return
|
||||
if (!force && fetchedAt.value && Date.now() - fetchedAt.value < 5 * 60 * 1000) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await httpGet<BackendMenuItem[]>('/sys/menu/nav')
|
||||
const transformed = transformMenu(Array.isArray(data) ? data : [])
|
||||
const frontendTruth = filterByFrontendTruth(transformed)
|
||||
list.value = filterInvalidMenus(frontendTruth)
|
||||
fetchedAt.value = Date.now()
|
||||
} catch (e: any) {
|
||||
error.value = e?.msg || e?.message || '菜单加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
list.value = []
|
||||
fetchedAt.value = 0
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return { list, loading, error, collapsed, fetchedAt, toggleCollapsed, fetchMenu, clear }
|
||||
})
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
// F006 4 套预置主题
|
||||
// 默认 = 科技蓝,通过 <body data-theme="..."> 切换
|
||||
// W5.x v2 Design Language 升级(2026-07-03)
|
||||
// 改动:
|
||||
// + 每主题加 --chart-1/2/3/4(图表色)
|
||||
// + dark 主题微调 login glow 提亮
|
||||
|
||||
// ============ 1. 科技蓝(默认, Stripe/Supabase 风) ============
|
||||
:root,
|
||||
@ -36,6 +40,20 @@
|
||||
--red-soft: #FEE2E2;
|
||||
--purple: #7C3AED;
|
||||
--purple-soft: #EDE9FE;
|
||||
// v2 图表色
|
||||
--chart-1: #2563EB;
|
||||
--chart-2: #7C3AED;
|
||||
--chart-3: #059669;
|
||||
--chart-4: #D97706;
|
||||
|
||||
// W5.x EP 派生色重定向(主题色适配全局修复)
|
||||
--el-color-primary: var(--brand);
|
||||
--el-color-primary-light-3: var(--brand-soft);
|
||||
--el-color-primary-light-5: var(--brand-soft);
|
||||
--el-color-primary-light-7: var(--brand-soft);
|
||||
--el-color-primary-light-8: var(--brand-soft);
|
||||
--el-color-primary-light-9: var(--brand-soft);
|
||||
--el-color-primary-dark-2: var(--brand-hover);
|
||||
}
|
||||
|
||||
// ============ 2. 中国红 ============
|
||||
@ -68,9 +86,22 @@
|
||||
--red-soft: #FEE2E2;
|
||||
--purple: #7E22CE;
|
||||
--purple-soft: #F3E8FF;
|
||||
--chart-1: #DC2626;
|
||||
--chart-2: #7E22CE;
|
||||
--chart-3: #15803D;
|
||||
--chart-4: #B45309;
|
||||
|
||||
// W5.x EP 派生色重定向(主题色适配全局修复)
|
||||
--el-color-primary: var(--brand);
|
||||
--el-color-primary-light-3: var(--brand-soft);
|
||||
--el-color-primary-light-5: var(--brand-soft);
|
||||
--el-color-primary-light-7: var(--brand-soft);
|
||||
--el-color-primary-light-8: var(--brand-soft);
|
||||
--el-color-primary-light-9: var(--brand-soft);
|
||||
--el-color-primary-dark-2: var(--brand-hover);
|
||||
}
|
||||
|
||||
// ============ 3. Notion 紫 ============
|
||||
// ============ 3. 深空紫 ============
|
||||
[data-theme="purple"] {
|
||||
--bg: #FFFFFF;
|
||||
--bg-soft: #FAF9F7;
|
||||
@ -100,36 +131,64 @@
|
||||
--red-soft: #F8E2DF;
|
||||
--purple: #5645D4;
|
||||
--purple-soft: #EDEBFC;
|
||||
--chart-1: #5645D4;
|
||||
--chart-2: #C4554D;
|
||||
--chart-3: #0F7B6C;
|
||||
--chart-4: #C76E29;
|
||||
|
||||
// W5.x EP 派生色重定向(主题色适配全局修复)
|
||||
--el-color-primary: var(--brand);
|
||||
--el-color-primary-light-3: var(--brand-soft);
|
||||
--el-color-primary-light-5: var(--brand-soft);
|
||||
--el-color-primary-light-7: var(--brand-soft);
|
||||
--el-color-primary-light-8: var(--brand-soft);
|
||||
--el-color-primary-light-9: var(--brand-soft);
|
||||
--el-color-primary-dark-2: var(--brand-hover);
|
||||
}
|
||||
|
||||
// ============ 4. 暗夜黑(深色模式) ============
|
||||
// W3.6 优化: 边/分组小标题调亮, hover 区分度, 表格条纹
|
||||
// W5.x v2 微调: --login-glow-1/2 提亮(0.18→0.22 / 0.10→0.14)
|
||||
[data-theme="dark"] {
|
||||
--bg: #0F172A;
|
||||
--bg-soft: #0B1120;
|
||||
--bg: #0B1120;
|
||||
--bg-soft: #0F172A;
|
||||
--surface: #1E293B;
|
||||
--surface-2: #334155;
|
||||
--border: #334155;
|
||||
--border-strong: #475569;
|
||||
--ink: #F1F5F9;
|
||||
--ink-2: #CBD5E1;
|
||||
--ink-3: #94A3B8;
|
||||
--ink-4: #64748B;
|
||||
--surface-2: #283449;
|
||||
--border: #475569;
|
||||
--border-strong: #64748B;
|
||||
--ink: #F8FAFC;
|
||||
--ink-2: #E2E8F0;
|
||||
--ink-3: #CBD5E1;
|
||||
--ink-4: #94A3B8;
|
||||
--brand: #60A5FA;
|
||||
--brand-2: #3B82F6;
|
||||
--brand-soft: rgba(96, 165, 250, 0.12);
|
||||
--brand-soft: rgba(96, 165, 250, 0.15);
|
||||
--brand-hover: #93C5FD;
|
||||
--brand-glow: rgba(96, 165, 250, 0.2);
|
||||
--brand-glow: rgba(96, 165, 250, 0.25);
|
||||
--login-grad-1: #1E293B;
|
||||
--login-grad-2: #0F172A;
|
||||
--login-grad-3: #1E1B4B;
|
||||
--login-glow-1: rgba(96, 165, 250, 0.25);
|
||||
--login-glow-2: rgba(167, 139, 250, 0.18);
|
||||
--login-glow-1: rgba(96, 165, 250, 0.22);
|
||||
--login-glow-2: rgba(139, 92, 246, 0.14);
|
||||
--green: #34D399;
|
||||
--green-soft: rgba(52, 211, 153, 0.12);
|
||||
--green-soft: rgba(52, 211, 153, 0.15);
|
||||
--amber: #FBBF24;
|
||||
--amber-soft: rgba(251, 191, 36, 0.12);
|
||||
--amber-soft: rgba(251, 191, 36, 0.15);
|
||||
--red: #F87171;
|
||||
--red-soft: rgba(248, 113, 113, 0.12);
|
||||
--red-soft: rgba(248, 113, 113, 0.15);
|
||||
--purple: #A78BFA;
|
||||
--purple-soft: rgba(167, 139, 250, 0.12);
|
||||
--purple-soft: rgba(167, 139, 250, 0.15);
|
||||
--chart-1: #60A5FA;
|
||||
--chart-2: #A78BFA;
|
||||
--chart-3: #34D399;
|
||||
--chart-4: #FBBF24;
|
||||
|
||||
// W5.x EP 派生色重定向(主题色适配全局修复)
|
||||
--el-color-primary: var(--brand);
|
||||
--el-color-primary-light-3: var(--brand-soft);
|
||||
--el-color-primary-light-5: var(--brand-soft);
|
||||
--el-color-primary-light-7: var(--brand-soft);
|
||||
--el-color-primary-light-8: var(--brand-soft);
|
||||
--el-color-primary-light-9: var(--brand-soft);
|
||||
--el-color-primary-dark-2: var(--brand-hover);
|
||||
}
|
||||
|
||||
@ -1,22 +1,34 @@
|
||||
// F006 设计 Token - 中性色 + 圆角 + 字体 + 阴影(主题无关)
|
||||
// W5.x v2 Design Language 升级(2026-07-03)
|
||||
// 改动:
|
||||
// + --ease / --ease-out 动画曲线
|
||||
// + --shadow-xl(毛玻璃下层用)
|
||||
// + --text-xs ~ --text-3xl 8 级字号
|
||||
// + --topbar-blur 顶栏毛玻璃
|
||||
// + --brand-gradient 主按钮渐变
|
||||
// W5.x.x 暗主题适配全局修复(2026-07-03):
|
||||
// + 25+ 个 EP 文字色/填充色/边框色/popover 背景等派生色变量
|
||||
// + 4 主题块通过 CSS 级联自动适配(都引用 var(--ink)/var(--surface) 等)
|
||||
|
||||
:root {
|
||||
// 圆角
|
||||
// ===== 圆角(沿用 v1.3) =====
|
||||
--radius-sm: 6px;
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-pill: 9999px;
|
||||
|
||||
// 字体
|
||||
// ===== 字体(沿用) =====
|
||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;
|
||||
|
||||
// 阴影
|
||||
// ===== 阴影 v2 加宽 =====
|
||||
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--shadow: 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--shadow-lg: 0 10px 25px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04);
|
||||
--shadow-xl: 0 20px 60px rgba(15, 23, 42, 0.10), 0 4px 12px rgba(15, 23, 42, 0.06);
|
||||
|
||||
// Element Plus 重定向(让 Element Plus 组件跟着主题色变)
|
||||
// ===== Element Plus 重定向(沿用) =====
|
||||
--el-color-primary: var(--brand);
|
||||
--el-color-primary-light-3: var(--brand-soft);
|
||||
--el-color-primary-light-5: var(--brand-soft);
|
||||
@ -24,4 +36,88 @@
|
||||
--el-color-primary-light-8: var(--brand-soft);
|
||||
--el-color-primary-light-9: var(--brand-soft);
|
||||
--el-color-primary-dark-2: var(--brand-hover);
|
||||
|
||||
// ===== Element Plus 文字色派生(W5.x.x 暗主题适配全局修复) =====
|
||||
// 让 el-breadcrumb / el-popover / el-tooltip / el-select / el-input placeholder
|
||||
// 等所有用 --el-text-color-* 的 EP 组件,文字色都跟主题切换
|
||||
--el-text-color-primary: var(--ink);
|
||||
--el-text-color-regular: var(--ink-2);
|
||||
--el-text-color-secondary: var(--ink-3);
|
||||
--el-text-color-placeholder: var(--ink-4);
|
||||
--el-text-color-disabled: var(--ink-4);
|
||||
|
||||
// ===== Element Plus 边框色派生 =====
|
||||
--el-border-color: var(--border);
|
||||
--el-border-color-light: var(--border);
|
||||
--el-border-color-lighter: var(--border);
|
||||
--el-border-color-extra-light: var(--border);
|
||||
--el-border-color-dark: var(--border-strong);
|
||||
--el-border-color-darker: var(--border-strong);
|
||||
|
||||
// ===== Element Plus 填充色派生 =====
|
||||
--el-fill-color: var(--bg-soft);
|
||||
--el-fill-color-light: var(--bg-soft);
|
||||
--el-fill-color-lighter: var(--surface-2);
|
||||
--el-fill-color-extra-light: var(--surface-2);
|
||||
--el-fill-color-dark: var(--surface-2);
|
||||
--el-fill-color-darker: var(--surface-2);
|
||||
--el-fill-color-blank: var(--surface);
|
||||
|
||||
// ===== Element Plus 背景色派生(popover/tooltip/dropdown/modal 容器) =====
|
||||
--el-bg-color: var(--surface);
|
||||
--el-bg-color-page: var(--bg-soft);
|
||||
--el-bg-color-overlay: var(--surface);
|
||||
--el-popover-bg-color: var(--surface);
|
||||
--el-popover-border-color: var(--border);
|
||||
// ===== Element Plus Popper 容器(W5.x.x+1: 补 W5.x.x 漏的 popper bg) =====
|
||||
// EP 2.x 弹窗实际读 --el-popper-bg-color-light(不是 --el-popper-bg-color)
|
||||
// W5.x.x+1 v3 修根因: tokens 全部重定向到主题色,确保 dark 主题下弹窗是深底
|
||||
--el-popper-bg-color: var(--surface);
|
||||
--el-popper-bg-color-light: var(--surface);
|
||||
--el-popper-border-color: var(--border);
|
||||
--el-popper-title-text-color: var(--ink);
|
||||
--el-popper-arrow-bg-color: var(--surface);
|
||||
--el-popper-plain-bg-color: var(--surface);
|
||||
--el-popper-padding: 12px;
|
||||
// 弹窗 border 颜色 EP 实际读 --el-border-color-light
|
||||
--el-border-color-light: var(--border);
|
||||
--el-border-color-lighter: var(--border);
|
||||
// 弹窗底层填充色
|
||||
--el-fill-color-blank: var(--surface);
|
||||
--el-bg-color-overlay: var(--surface);
|
||||
--el-tooltip-bg-color: var(--ink);
|
||||
--el-dropdown-bg-color: var(--surface);
|
||||
--el-dropdown-border-color: var(--border);
|
||||
--el-select-dropdown-bg-color: var(--surface);
|
||||
--el-select-dropdown-border-color: var(--border);
|
||||
--el-datepicker-bg-color: var(--surface);
|
||||
--el-dialog-bg-color: var(--surface);
|
||||
--el-message-bg-color: var(--surface);
|
||||
--el-notification-bg-color: var(--surface);
|
||||
|
||||
// ===== Element Plus 遮罩 + 禁用 =====
|
||||
--el-mask-color: rgba(0, 0, 0, 0.5);
|
||||
--el-disabled-bg-color: var(--bg-soft);
|
||||
--el-disabled-text-color: var(--ink-4);
|
||||
--el-disabled-border-color: var(--border);
|
||||
|
||||
// ===== v2 新增:动画曲线 =====
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--ease-out: cubic-bezier(0.0, 0, 0.2, 1);
|
||||
|
||||
// ===== v2 新增:字号 8 级(letter-spacing 收紧) =====
|
||||
--text-xs: 11.5px;
|
||||
--text-sm: 12.5px;
|
||||
--text-base: 13.5px;
|
||||
--text-md: 14.5px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 20px;
|
||||
--text-2xl: 24px;
|
||||
--text-3xl: 28px;
|
||||
|
||||
// ===== v2 新增:顶栏毛玻璃模糊半径 =====
|
||||
--topbar-blur: blur(16px) saturate(1.5);
|
||||
|
||||
// ===== v2 新增:主按钮渐变(从主题 brand 自动组合) =====
|
||||
--brand-gradient: linear-gradient(135deg, var(--brand), var(--brand-2));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user