Merge branch 'main' of github.com:cedar-v/license-manager
This commit is contained in:
commit
183f5e7896
18 files changed
+1405
-70
No files matched your search
@@ -1,8 +1,16 @@
|
||||
###
|
||||
# @Author: 13895237362 2205451508@qq.com
|
||||
# @Date: 2025-07-31 14:30:20
|
||||
# @LastEditors: 13895237362 2205451508@qq.com
|
||||
# @LastEditTime: 2025-08-08 09:18:54
|
||||
# @FilePath: /frontend/.env.development
|
||||
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
###
|
||||
# 开发环境
|
||||
NODE_ENV = "development"
|
||||
|
||||
# 开发环境接口地址
|
||||
VITE_API_BASE_URL = '/devApi'
|
||||
VITE_API_BASE_URL = ''
|
||||
|
||||
# # 开发环境打包名称
|
||||
# VITE_OUTDIR='dev'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
NODE_ENV = "production"
|
||||
|
||||
# 正式环境接口地址
|
||||
VITE_API_BASE_URL = '/proApi'
|
||||
VITE_API_BASE_URL = ''
|
||||
|
||||
# 正式环境打包名称
|
||||
VITE_OUTDIR='production'
|
||||
|
||||
+9
-1
@@ -1,8 +1,16 @@
|
||||
###
|
||||
# @Author: 13895237362 2205451508@qq.com
|
||||
# @Date: 2025-07-31 14:30:20
|
||||
# @LastEditors: 13895237362 2205451508@qq.com
|
||||
# @LastEditTime: 2025-08-08 10:07:33
|
||||
# @FilePath: /frontend/.env.test
|
||||
# @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
###
|
||||
# 测试环境
|
||||
NODE_ENV = "test"
|
||||
|
||||
# 测试环境接口地址
|
||||
VITE_API_BASE_URL = '/testApi'
|
||||
VITE_API_BASE_URL = ''
|
||||
|
||||
# 测试环境打包名称
|
||||
VITE_OUTDIR='test'
|
||||
|
||||
Generated
+4
-3
@@ -8,6 +8,7 @@
|
||||
"name": "vue3_vite",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"amfe-flexible": "^2.2.1",
|
||||
"axios": "^1.3.6",
|
||||
"element-plus": "^2.10.5",
|
||||
@@ -85,9 +86,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@element-plus/icons-vue": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
|
||||
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
|
||||
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"amfe-flexible": "^2.2.1",
|
||||
"axios": "^1.3.6",
|
||||
"element-plus": "^2.10.5",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 1.1 MiB |
@@ -2,7 +2,7 @@
|
||||
"login": {
|
||||
"title": "Authorization Management Platform",
|
||||
"subtitle": "Sign in to your account",
|
||||
"usernamePlaceholder": "Username",
|
||||
"usernamePlaceholder": "Please enter Username",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"remember": "Remember password",
|
||||
"forgotPassword": "Forgot password?",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"login": {
|
||||
"title": "権限管理プラットフォーム",
|
||||
"subtitle": "アカウントにログイン",
|
||||
"usernamePlaceholder": "ユーザー名",
|
||||
"usernamePlaceholder": "ユーザー名を入力してください",
|
||||
"passwordPlaceholder": "パスワードを入力してください",
|
||||
"remember": "パスワードを記憶する",
|
||||
"forgotPassword": "パスワードをお忘れですか?",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"login": {
|
||||
"title": "授权管理平台",
|
||||
"subtitle": "登录您的账户",
|
||||
"usernamePlaceholder": "用户名",
|
||||
"usernamePlaceholder": "请输入用户名",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"remember": "记住密码",
|
||||
"forgotPassword": "忘记密码?",
|
||||
|
||||
@@ -15,11 +15,17 @@ import './assets/styles/global.css'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import i18n, { getCurrentElementLocale } from './i18n'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 全局注册Element Plus图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
// 正确配置Element Plus语言包
|
||||
app.use(ElementPlus, {
|
||||
locale: getCurrentElementLocale()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-01 09:32:42
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-08-06 16:40:56
|
||||
* @LastEditTime: 2025-08-08 11:03:44
|
||||
* @FilePath: /frontend/src/router/index.ts
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
*/
|
||||
@@ -22,7 +22,8 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: "/dashboard",
|
||||
component: () => import("@/views/Dashboard.vue"),
|
||||
meta: { title: "仪表板", requiresAuth: true }
|
||||
// meta: { title: "仪表板", requiresAuth: true }
|
||||
meta: { title: "仪表板"}
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,53 +1,42 @@
|
||||
<!--
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-01 09:32:42
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-08-08 14:08:31
|
||||
* @FilePath: /frontend/src/views/Dashboard.vue
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
-->
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>欢迎来到授权管理平台</h1>
|
||||
<p>您已成功登录!</p>
|
||||
<button @click="logout" class="logout-btn">退出登录</button>
|
||||
</div>
|
||||
<Layout app-name="License Manager" :nav-items="navigationItems" :breadcrumb-items="breadcrumbs" page-title="仪表盘"
|
||||
@nav-click="handleNavigation">
|
||||
<!-- 页面内容 -->
|
||||
<div>
|
||||
<h1>欢迎来到授权管理平台</h1>
|
||||
<p>您已成功登录!</p>
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import Layout from '@/views/components/layout/Layout.vue';
|
||||
import type { NavItem, BreadcrumbItem } from '@/views/components/layout/index';
|
||||
const router = useRouter();
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem("token");
|
||||
router.push("/login");
|
||||
}
|
||||
const navigationItems = ref<NavItem[]>([
|
||||
{ id: "dashboard", label: "仪表盘", href: "/dashboard", icon: "Dashboard", active: true },
|
||||
{ id: "customers", label: "客户管理", href: "/customers", icon: "User" },
|
||||
{ id: "licenses", label: "授权管理", href: "/licenses", icon: "Key" },
|
||||
{ id: "roles", label: "角色权限", href: "/roles", icon: "UserFilled" },
|
||||
{ id: "users", label: "系统用户", href: "/users", icon: "Avatar" }
|
||||
]);
|
||||
const breadcrumbs = ref<BreadcrumbItem[]>([
|
||||
{ title: "仪表盘", path: "/dashboard" }
|
||||
]);
|
||||
const handleNavigation = (item: NavItem) => {
|
||||
router.push(item.href);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 10px 20px;
|
||||
background: #2E69F7;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #1557E5;
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
@@ -29,10 +29,9 @@
|
||||
<!-- 登录区域 -->
|
||||
<div class="login-section">
|
||||
<div class="login-container">
|
||||
<el-card class="login-card" :body-style="{ padding: '2rem' }">
|
||||
<div class="login-card" >
|
||||
<h1 class="title">{{ t('login.title') }}</h1>
|
||||
<p class="subtitle">{{ t('login.subtitle') }}</p>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
@@ -94,7 +93,7 @@
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,11 +305,7 @@ async function handleLogin() {
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
|
||||
backdrop-filter: blur(4px);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: '2rem'
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -341,7 +336,7 @@ async function handleLogin() {
|
||||
.login-input :deep(.el-input__wrapper) {
|
||||
height: clamp(45px, 8vh, 55px);
|
||||
border-radius: 10px;
|
||||
border: 1px solid #019C7C;
|
||||
/* border: 1px solid #019C7C; */
|
||||
background-color: #ffffff;
|
||||
box-shadow: none;
|
||||
transition: all 0.3s ease;
|
||||
@@ -425,7 +420,7 @@ async function handleLogin() {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.language-switcher :deep(.el-input__inner):focus {
|
||||
.language-switcher :deep(.el-input__inner):hover {
|
||||
border-color: #019C7C;
|
||||
box-shadow: 0 0 0 2px rgba(1, 156, 124, 0.2);
|
||||
}
|
||||
@@ -483,7 +478,7 @@ async function handleLogin() {
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.login-card :deep(.el-card__body) {
|
||||
.login-card {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
@@ -507,7 +502,7 @@ async function handleLogin() {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.login-card :deep(.el-card__body) {
|
||||
.login-card {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
@@ -538,7 +533,7 @@ async function handleLogin() {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.login-card :deep(.el-card__body) {
|
||||
.login-card {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -548,7 +543,7 @@ async function handleLogin() {
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
.login-card :deep(.el-card__body) {
|
||||
.login-card {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
<!--
|
||||
/**
|
||||
* 主布局组件
|
||||
* 提供完整的后台管理系统布局结构,包括侧边栏、顶部导航和主内容区域
|
||||
* 支持响应式设计和侧边栏收起/展开功能
|
||||
*/
|
||||
-->
|
||||
<template>
|
||||
<!-- 主布局容器 -->
|
||||
<div class="app-layout" :class="layoutClasses">
|
||||
<!-- 遮罩层 - 移动端使用 -->
|
||||
<div
|
||||
v-if="isMobile && showSidebar"
|
||||
class="layout-overlay"
|
||||
@click="closeSidebar"
|
||||
></div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<Sidebar
|
||||
:app-name="appName"
|
||||
:nav-items="navItems"
|
||||
:collapsible="sidebarCollapsible"
|
||||
:default-collapsed="sidebarCollapsed"
|
||||
:class="{
|
||||
'sidebar--mobile-open': isMobile && showSidebar,
|
||||
'sidebar--collapsed': sidebarCollapsed
|
||||
}"
|
||||
@nav-click="handleNavClick"
|
||||
@toggle="handleSidebarToggle"
|
||||
>
|
||||
<template #logo>
|
||||
<slot name="sidebar-logo">
|
||||
<span class="logo-text">{{ appName }}</span>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template #nav-items>
|
||||
<slot name="sidebar-nav"></slot>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<slot name="sidebar-footer"></slot>
|
||||
</template>
|
||||
</Sidebar>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="layout-main" :class="mainContentClasses">
|
||||
<!-- 顶部导航 -->
|
||||
<NavContent
|
||||
:breadcrumb-items="breadcrumbItems"
|
||||
:page-title="pageTitle"
|
||||
:show-search="showSearch"
|
||||
:search-placeholder="searchPlaceholder"
|
||||
:show-notifications="showNotifications"
|
||||
:notification-count="notificationCount"
|
||||
:show-settings="showSettings"
|
||||
:show-user-avatar="showUserAvatar"
|
||||
:user-avatar="userAvatar"
|
||||
:user-name="userName"
|
||||
:show-menu-button="isMobile"
|
||||
:class="{ 'nav-content--sidebar-collapsed': sidebarCollapsed }"
|
||||
@search="handleSearch"
|
||||
@breadcrumb-click="handleBreadcrumbClick"
|
||||
@notification-click="handleNotificationClick"
|
||||
@settings-click="handleSettingsClick"
|
||||
@user-click="handleUserClick"
|
||||
@menu-toggle="toggleSidebar"
|
||||
>
|
||||
<template #breadcrumb>
|
||||
<slot name="nav-breadcrumb"></slot>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<slot name="nav-actions"></slot>
|
||||
</template>
|
||||
</NavContent>
|
||||
<!-- 页面内容 -->
|
||||
<main class="layout-content">
|
||||
<div class="content-container">
|
||||
<slot name="default">
|
||||
<router-view />
|
||||
</slot>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import NavContent from './NavContent.vue'
|
||||
|
||||
// 导航项接口定义
|
||||
interface NavItem {
|
||||
id: string // 导航项唯一标识
|
||||
label: string // 显示文本
|
||||
href: string // 链接地址
|
||||
icon?: string // 图标类名(可选)
|
||||
active?: boolean // 是否为当前活跃项(可选)
|
||||
children?: NavItem[] // 子菜单项(可选)
|
||||
}
|
||||
|
||||
// 面包屑导航项接口定义
|
||||
interface BreadcrumbItem {
|
||||
title: string // 显示标题
|
||||
path?: string // 链接路径(可选,最后一项通常不需要链接)
|
||||
}
|
||||
|
||||
// 组件属性接口定义
|
||||
interface Props {
|
||||
appName?: string // 应用名称
|
||||
navItems?: NavItem[] // 导航菜单项列表
|
||||
sidebarCollapsible?: boolean // 侧边栏是否可折叠
|
||||
defaultSidebarCollapsed?: boolean // 侧边栏默认是否折叠
|
||||
breadcrumbItems?: BreadcrumbItem[] // 面包屑导航项列表
|
||||
pageTitle?: string // 当前页面标题
|
||||
showSearch?: boolean // 是否显示搜索框
|
||||
searchPlaceholder?: string // 搜索框占位符文本
|
||||
showNotifications?: boolean // 是否显示通知按钮
|
||||
notificationCount?: number // 未读通知数量
|
||||
showSettings?: boolean // 是否显示设置按钮
|
||||
showUserAvatar?: boolean // 是否显示用户头像
|
||||
userAvatar?: string // 用户头像URL
|
||||
userName?: string // 用户名称
|
||||
}
|
||||
|
||||
// 定义组件属性和默认值
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
appName: 'Cedar',
|
||||
navItems: () => [],
|
||||
sidebarCollapsible: true,
|
||||
defaultSidebarCollapsed: false,
|
||||
breadcrumbItems: () => [],
|
||||
pageTitle: '',
|
||||
showSearch: true,
|
||||
searchPlaceholder: '搜索...',
|
||||
showNotifications: true,
|
||||
notificationCount: 0,
|
||||
showSettings: true,
|
||||
showUserAvatar: true,
|
||||
userName: ''
|
||||
})
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
navClick: [item: NavItem, event: Event]
|
||||
search: [query: string]
|
||||
breadcrumbClick: [item: BreadcrumbItem, event: Event]
|
||||
notificationClick: []
|
||||
settingsClick: []
|
||||
userClick: []
|
||||
}>()
|
||||
|
||||
// 响应式状态管理
|
||||
const isMobile = ref(false)
|
||||
const showSidebar = ref(false)
|
||||
const sidebarCollapsed = ref(props.defaultSidebarCollapsed)
|
||||
|
||||
// 计算布局样式类
|
||||
const layoutClasses = computed(() => ({
|
||||
'app-layout--mobile': isMobile.value,
|
||||
'app-layout--sidebar-collapsed': sidebarCollapsed.value
|
||||
}))
|
||||
|
||||
// 计算主内容区样式类
|
||||
const mainContentClasses = computed(() => ({
|
||||
'layout-main--sidebar-collapsed': sidebarCollapsed.value,
|
||||
'layout-main--mobile': isMobile.value
|
||||
}))
|
||||
|
||||
// 处理导航点击事件
|
||||
const handleNavClick = (item: NavItem, event: Event) => {
|
||||
if (isMobile.value) {
|
||||
showSidebar.value = false
|
||||
}
|
||||
emit('navClick', item, event)
|
||||
}
|
||||
|
||||
// 处理侧边栏收起/展开
|
||||
const handleSidebarToggle = (collapsed: boolean) => {
|
||||
sidebarCollapsed.value = collapsed
|
||||
}
|
||||
|
||||
// 切换侧边栏显示状态
|
||||
const toggleSidebar = () => {
|
||||
if (isMobile.value) {
|
||||
showSidebar.value = !showSidebar.value
|
||||
} else {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭侧边栏(移动端)
|
||||
const closeSidebar = () => {
|
||||
showSidebar.value = false
|
||||
}
|
||||
|
||||
// 处理搜索事件
|
||||
const handleSearch = (query: string) => {
|
||||
emit('search', query)
|
||||
}
|
||||
|
||||
// 处理面包屑导航点击
|
||||
const handleBreadcrumbClick = (item: BreadcrumbItem, event: Event) => {
|
||||
emit('breadcrumbClick', item, event)
|
||||
}
|
||||
|
||||
// 处理通知按钮点击
|
||||
const handleNotificationClick = () => {
|
||||
emit('notificationClick')
|
||||
}
|
||||
|
||||
// 处理设置按钮点击
|
||||
const handleSettingsClick = () => {
|
||||
emit('settingsClick')
|
||||
}
|
||||
|
||||
// 处理用户头像点击
|
||||
const handleUserClick = () => {
|
||||
emit('userClick')
|
||||
}
|
||||
|
||||
// 检测是否为移动设备
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 1024
|
||||
if (!isMobile.value) {
|
||||
showSidebar.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-layout {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #F7F8FA;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 遮罩层 */
|
||||
.layout-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.layout-main {
|
||||
margin-left: 280px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.layout-main--sidebar-collapsed {
|
||||
margin-left: 64px;
|
||||
}
|
||||
|
||||
.layout-main--mobile {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 页面内容 */
|
||||
.layout-content {
|
||||
flex: 1;
|
||||
padding-top: 80px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
min-height: calc(100vh - 80px);
|
||||
padding: 24px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 1024px) {
|
||||
.layout-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-container {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.content-container {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.layout-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.layout-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.layout-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.layout-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.app-layout * {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 打印样式 */
|
||||
@media print {
|
||||
.app-layout {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高对比度模式支持 */
|
||||
@media (prefers-contrast: high) {
|
||||
.app-layout {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画模式 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.app-layout *,
|
||||
.layout-main,
|
||||
.layout-content {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,530 @@
|
||||
<!--
|
||||
/**
|
||||
* 顶部导航栏组件
|
||||
* 提供面包屑导航、页面标题、搜索框、通知、设置和用户信息
|
||||
* 支持响应式设计和移动端适配
|
||||
*/
|
||||
-->
|
||||
<template>
|
||||
<!-- 顶部导航栏容器 -->
|
||||
<header class="nav-content" :class="{ 'nav-content--mobile': isMobile }">
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<button
|
||||
v-if="showMenuButton"
|
||||
class="nav-content__menu-btn"
|
||||
@click="handleMenuToggle"
|
||||
:aria-label="'打开菜单'"
|
||||
>
|
||||
<i class="el-icon-s-unfold"></i>
|
||||
</button>
|
||||
|
||||
<!-- 面包屑导航区域 -->
|
||||
<div class="nav-content__breadcrumb">
|
||||
<div class="breadcrumb-container">
|
||||
<slot name="breadcrumb">
|
||||
<nav class="breadcrumb" v-if="breadcrumbItems.length">
|
||||
<el-icon><SetUp /></el-icon>
|
||||
<span
|
||||
v-for="(item, index) in breadcrumbItems"
|
||||
:key="item.path || index"
|
||||
class="breadcrumb-item"
|
||||
>
|
||||
<a
|
||||
v-if="item.path && index < breadcrumbItems.length - 1"
|
||||
:href="item.path"
|
||||
class="breadcrumb-link"
|
||||
@click="handleBreadcrumbClick(item, $event)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
<span
|
||||
v-else
|
||||
class="breadcrumb-current"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<i
|
||||
v-if="index < breadcrumbItems.length - 1"
|
||||
class="breadcrumb-separator"
|
||||
>
|
||||
/
|
||||
</i>
|
||||
</span>
|
||||
</nav>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="nav-content__actions">
|
||||
<slot name="actions">
|
||||
<div class="actions-container">
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="showSearch" class="search-box">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="searchPlaceholder"
|
||||
class="search-input"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
<i class="search-icon el-icon-search"></i>
|
||||
</div>
|
||||
|
||||
<!-- 通知按钮 -->
|
||||
<button
|
||||
v-if="showNotifications"
|
||||
class="action-btn notification-btn"
|
||||
@click="handleNotificationClick"
|
||||
:aria-label="'通知'"
|
||||
>
|
||||
<i class="el-icon-bell"></i>
|
||||
<span v-if="notificationCount" class="notification-badge">
|
||||
{{ notificationCount > 99 ? '99+' : notificationCount }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- 设置按钮 -->
|
||||
<button
|
||||
v-if="showSettings"
|
||||
class="action-btn settings-btn"
|
||||
@click="handleSettingsClick"
|
||||
:aria-label="'设置'"
|
||||
>
|
||||
<i class="el-icon-setting"></i>
|
||||
</button>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<div v-if="showUserAvatar" class="user-avatar" @click="handleUserClick">
|
||||
<img
|
||||
v-if="userAvatar"
|
||||
:src="userAvatar"
|
||||
:alt="userName || '用户头像'"
|
||||
class="avatar-img"
|
||||
/>
|
||||
<div v-else class="avatar-placeholder">
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 面包屑导航项接口定义
|
||||
interface BreadcrumbItem {
|
||||
title: string // 显示标题
|
||||
path?: string // 链接路径(可选,最后一项通常不需要链接)
|
||||
}
|
||||
|
||||
// 组件属性接口定义
|
||||
interface Props {
|
||||
breadcrumbItems?: BreadcrumbItem[] // 面包屑导航项列表
|
||||
pageTitle?: string // 当前页面标题
|
||||
showSearch?: boolean // 是否显示搜索框
|
||||
searchPlaceholder?: string // 搜索框占位符文本
|
||||
showNotifications?: boolean // 是否显示通知按钮
|
||||
notificationCount?: number // 未读通知数量
|
||||
showSettings?: boolean // 是否显示设置按钮
|
||||
showUserAvatar?: boolean // 是否显示用户头像
|
||||
userAvatar?: string // 用户头像URL
|
||||
userName?: string // 用户名称
|
||||
showMenuButton?: boolean // 是否显示菜单按钮(移动端使用)
|
||||
}
|
||||
|
||||
// 定义组件属性和默认值
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
breadcrumbItems: () => [],
|
||||
pageTitle: '',
|
||||
showSearch: true,
|
||||
searchPlaceholder: '搜索...',
|
||||
showNotifications: true,
|
||||
notificationCount: 0,
|
||||
showSettings: true,
|
||||
showUserAvatar: true,
|
||||
userName: '',
|
||||
showMenuButton: false
|
||||
})
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
search: [query: string]
|
||||
breadcrumbClick: [item: BreadcrumbItem, event: Event]
|
||||
notificationClick: []
|
||||
settingsClick: []
|
||||
userClick: []
|
||||
menuToggle: []
|
||||
}>()
|
||||
|
||||
// 响应式状态管理
|
||||
const searchQuery = ref('')
|
||||
const isMobile = ref(false)
|
||||
|
||||
// 计算用户姓名缩写
|
||||
const userInitials = computed(() => {
|
||||
if (!props.userName) return 'U'
|
||||
return props.userName
|
||||
.split(' ')
|
||||
.map(name => name.charAt(0))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
})
|
||||
|
||||
// 处理搜索事件
|
||||
const handleSearch = () => {
|
||||
emit('search', searchQuery.value)
|
||||
}
|
||||
|
||||
// 处理面包屑导航点击
|
||||
const handleBreadcrumbClick = (item: BreadcrumbItem, event: Event) => {
|
||||
emit('breadcrumbClick', item, event)
|
||||
}
|
||||
|
||||
// 处理通知按钮点击
|
||||
const handleNotificationClick = () => {
|
||||
emit('notificationClick')
|
||||
}
|
||||
|
||||
// 处理设置按钮点击
|
||||
const handleSettingsClick = () => {
|
||||
emit('settingsClick')
|
||||
}
|
||||
|
||||
// 处理用户头像点击
|
||||
const handleUserClick = () => {
|
||||
emit('userClick')
|
||||
}
|
||||
|
||||
// 处理菜单按钮点击(移动端)
|
||||
const handleMenuToggle = () => {
|
||||
emit('menuToggle')
|
||||
}
|
||||
|
||||
// 检测是否为移动设备
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth <= 1024
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav-content {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 280px;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
background: #FFFFFF;
|
||||
border-bottom: 1px solid rgba(29, 29, 29, 0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
z-index: 999;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-content--mobile {
|
||||
left: 0;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
/* 移动端菜单按钮 */
|
||||
.nav-content__menu-btn {
|
||||
display: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-content__menu-btn:hover {
|
||||
background: rgba(1, 156, 124, 0.1);
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
/* 面包屑区域 */
|
||||
.nav-content__breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
color: #019C7C;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.breadcrumb-link:hover {
|
||||
color: #017A63;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: #ccc;
|
||||
font-style: normal;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 操作按钮区域 */
|
||||
.nav-content__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 搜索框 */
|
||||
.search-box {
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 40px 0 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: #fafafa;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #019C7C;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2px rgba(1, 156, 124, 0.1);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #888;
|
||||
font-size: 16px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-btn {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(1, 156, 124, 0.1);
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
background: #ff4757;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 用户头像 */
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
border-color: #019C7C;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #019C7C, #017A63);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.nav-content {
|
||||
left: 0;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.nav-content__menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-content {
|
||||
padding: 12px 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
height: 32px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.nav-content {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 与侧边栏收起状态的配合 */
|
||||
.nav-content--sidebar-collapsed {
|
||||
left: 64px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,394 @@
|
||||
<!--
|
||||
/**
|
||||
* 侧边栏组件
|
||||
* 提供应用导航菜单,支持折叠/展开功能和响应式设计
|
||||
* 包含Logo区域、导航菜单和底部区域
|
||||
*/
|
||||
-->
|
||||
<template>
|
||||
<!-- 侧边栏容器 -->
|
||||
<aside
|
||||
class="sidebar"
|
||||
:class="{ 'sidebar--collapsed': isCollapsed }"
|
||||
>
|
||||
<!-- Logo区域 -->
|
||||
<div class="sidebar__header">
|
||||
<div class="sidebar__logo">
|
||||
<div class="logo-container">
|
||||
<div class="logo-icon">
|
||||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="logo-icon-group">
|
||||
<path d="M0 0H29.74V40H0V0Z" fill="#019C7C"/>
|
||||
<path d="M24.59 20.16H41V40H24.59V20.16Z" fill="#146B59"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<span v-show="!isCollapsed" class="logo-text">Cedar-V</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="collapsible"
|
||||
class="sidebar__toggle"
|
||||
@click="toggleSidebar"
|
||||
:aria-label="isCollapsed ? '展开侧边栏' : '收起侧边栏'"
|
||||
>
|
||||
<i :class="isCollapsed ? 'el-icon-s-unfold' : 'el-icon-s-fold'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="sidebar__nav">
|
||||
<div class="nav-section">
|
||||
<slot name="nav-items">
|
||||
<div class="nav-item" v-for="item in navItems" :key="item.id">
|
||||
<a
|
||||
:href="item.href"
|
||||
class="nav-link"
|
||||
:class="{ 'nav-link--active': item.active }"
|
||||
@click="handleNavClick(item, $event)"
|
||||
>
|
||||
<div class="nav-icon-wrapper">
|
||||
<el-icon v-if="item.icon" class="nav-icon">
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<span v-show="!isCollapsed" class="nav-text">{{ item.label }}</span>
|
||||
</a>
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<div v-if="item.children && !isCollapsed" class="nav-submenu">
|
||||
<a
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:href="child.href"
|
||||
class="nav-sublink"
|
||||
:class="{ 'nav-sublink--active': child.active }"
|
||||
@click="handleNavClick(child, $event)"
|
||||
>
|
||||
<span class="nav-subtext">{{ child.label }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 底部区域 -->
|
||||
<div class="sidebar__footer">
|
||||
<slot name="footer">
|
||||
<!-- 用户信息等 -->
|
||||
</slot>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 导航项接口定义
|
||||
interface NavItem {
|
||||
id: string // 导航项唯一标识
|
||||
label: string // 显示文本
|
||||
href: string // 链接地址
|
||||
icon?: string // 图标类名(可选)
|
||||
active?: boolean // 是否为当前活跃项(可选)
|
||||
children?: NavItem[] // 子菜单项(可选)
|
||||
}
|
||||
|
||||
// 组件属性接口定义
|
||||
interface Props {
|
||||
appName?: string // 应用名称,显示在Logo区域
|
||||
navItems?: NavItem[] // 导航菜单项列表
|
||||
collapsible?: boolean // 是否允许折叠侧边栏
|
||||
defaultCollapsed?: boolean // 默认是否为折叠状态
|
||||
}
|
||||
|
||||
// 定义组件属性和默认值
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
appName: 'Cedar',
|
||||
navItems: () => [],
|
||||
collapsible: true,
|
||||
defaultCollapsed: false
|
||||
})
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
navClick: [item: NavItem, event: Event]
|
||||
toggle: [collapsed: boolean]
|
||||
}>()
|
||||
|
||||
// 侧边栏折叠状态管理
|
||||
const isCollapsed = ref(props.defaultCollapsed)
|
||||
|
||||
// 切换侧边栏折叠状态
|
||||
const toggleSidebar = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
emit('toggle', isCollapsed.value)
|
||||
}
|
||||
|
||||
// 处理导航项点击事件
|
||||
const handleNavClick = (item: NavItem, event: Event) => {
|
||||
emit('navClick', item, event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #F5F7FA 0%, #FFFFFF 100%);
|
||||
border-right: 1px solid rgba(29, 29, 29, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.sidebar--collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
/* Header 区域 */
|
||||
.sidebar__header {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid rgba(29, 29, 29, 0.06);
|
||||
}
|
||||
|
||||
.sidebar__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 41px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Swis721 BlkCn BT', sans-serif;
|
||||
font-size: 30px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sidebar__toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #B2B8C2;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sidebar__toggle:hover {
|
||||
background: rgba(1, 156, 124, 0.1);
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
/* 导航区域 */
|
||||
.sidebar__nav {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
color: #1D1D1D;
|
||||
text-decoration: none;
|
||||
border-radius: 28px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 16px;
|
||||
font-family: 'OPPOSans', sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: rgba(1, 156, 124, 0.08);
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
.nav-link--active {
|
||||
background: linear-gradient(90deg, #39CC68 0%, #A6E852 100%);
|
||||
color: #019C7C;
|
||||
font-weight: 700;
|
||||
box-shadow: 0px 2px 32px 0px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.nav-link--active .nav-text {
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
.nav-icon-wrapper {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #FFFFFF;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 16px;
|
||||
color: #B2B8C2;
|
||||
}
|
||||
|
||||
.nav-link--active .nav-icon {
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 子菜单 */
|
||||
.nav-submenu {
|
||||
margin-left: 32px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.nav-sublink {
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.nav-sublink:hover {
|
||||
background: rgba(1, 156, 124, 0.06);
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
.nav-sublink--active {
|
||||
background: rgba(1, 156, 124, 0.08);
|
||||
color: #019C7C;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-subtext {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 底部区域 */
|
||||
.sidebar__footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid rgba(29, 29, 29, 0.06);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar--mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidebar--collapsed {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100vw;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.sidebar--collapsed {
|
||||
width: 100vw;
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 收起状态下的样式调整 */
|
||||
.sidebar--collapsed .sidebar__logo {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .logo-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .logo-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .nav-link {
|
||||
justify-content: center;
|
||||
padding: 24px 12px;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .nav-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .nav-submenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.sidebar__nav::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.sidebar__nav::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar__nav::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sidebar__nav::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Layout布局组件导出文件
|
||||
* 统一管理布局相关组件和类型定义的导出
|
||||
*/
|
||||
|
||||
// 布局组件导出
|
||||
export { default as Layout } from './Layout.vue'
|
||||
export { default as Sidebar } from './Sidebar.vue'
|
||||
export { default as NavContent } from './NavContent.vue'
|
||||
|
||||
// 类型定义导出
|
||||
|
||||
// 导航菜单项类型定义
|
||||
export interface NavItem {
|
||||
id: string // 导航项唯一标识
|
||||
label: string // 显示文本
|
||||
href: string // 链接地址
|
||||
icon?: string // 图标类名(可选)
|
||||
active?: boolean // 是否为当前活跃项(可选)
|
||||
children?: NavItem[] // 子菜单项(可选)
|
||||
}
|
||||
|
||||
// 面包屑导航项类型定义
|
||||
export interface BreadcrumbItem {
|
||||
title: string // 显示标题
|
||||
path?: string // 链接路径(可选,最后一项通常不需要链接)
|
||||
}
|
||||
Reference in new issue
Block a user