build:根据ai开发文档修改项目结构
This commit is contained in:
1 parent
81761798ea
commit
0279083c59
19 files changed
+3242
-876
No files matched your search
@@ -0,0 +1,48 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
},
|
||||
rules: {
|
||||
// Vue相关规则
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-unused-vars': 'error',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/require-explicit-emits': 'error',
|
||||
'vue/prefer-import-from-vue': 'error',
|
||||
|
||||
// TypeScript相关规则
|
||||
'@typescript-eslint/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_'
|
||||
}],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
|
||||
// 通用规则
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-unused-vars': 'off', // 关闭JS的unused-vars,使用TS版本
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error'
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true
|
||||
},
|
||||
globals: {
|
||||
defineEmits: 'readonly',
|
||||
defineProps: 'readonly',
|
||||
defineExpose: 'readonly',
|
||||
withDefaults: 'readonly'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf",
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
Generated
+2223
-393
File diff suppressed because it is too large.
Load diff
+35
-17
@@ -1,34 +1,52 @@
|
||||
{
|
||||
"name": "vue3_vite",
|
||||
"name": "license-manager-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "License Manager Frontend built with Vue 3 + TypeScript + Element Plus",
|
||||
"author": "License Manager Team",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"serve": "vite",
|
||||
"test": "vue-tsc && vite build --mode test",
|
||||
"prod": "vue-tsc && vite build --mode production",
|
||||
"preview": "vite preview"
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build:test": "vue-tsc --noEmit && vite build --mode test",
|
||||
"build:prod": "vue-tsc --noEmit && vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"amfe-flexible": "^2.2.1",
|
||||
"axios": "^1.3.6",
|
||||
"axios": "^1.7.0",
|
||||
"element-plus": "^2.10.5",
|
||||
"postcss-pxtorem": "^6.0.0",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"sass": "^1.89.2",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vue": "^3.2.47",
|
||||
"pinia": "^2.2.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-i18n": "^9.14.5",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.16",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.89.2",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^4.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue-tsc": "^3.0.4"
|
||||
"vitest": "^2.0.0",
|
||||
"vue-tsc": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
@use './variables.scss' as *;
|
||||
|
||||
// Element Plus 主题定制
|
||||
.el-button {
|
||||
border-radius: $border-radius-base;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&--primary {
|
||||
background-color: $primary-color;
|
||||
border-color: $primary-color;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: lighten($primary-color, 10%);
|
||||
border-color: lighten($primary-color, 10%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: darken($primary-color, 10%);
|
||||
border-color: darken($primary-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
.el-input {
|
||||
.el-input__wrapper {
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: $primary-color;
|
||||
box-shadow: 0 0 0 2px rgba(1, 156, 124, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表格样式
|
||||
.el-table {
|
||||
border-radius: $border-radius-base;
|
||||
overflow: hidden;
|
||||
|
||||
.el-table__header {
|
||||
background-color: $background-color-base;
|
||||
}
|
||||
|
||||
.el-table__row:hover > td {
|
||||
background-color: lighten($primary-color, 45%);
|
||||
}
|
||||
|
||||
th.el-table__cell {
|
||||
background-color: $background-color-base;
|
||||
color: $text-color-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
.el-card {
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: $box-shadow-base;
|
||||
|
||||
.el-card__header {
|
||||
font-weight: 500;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单样式
|
||||
.el-menu {
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
background-color: lighten($primary-color, 45%);
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: lighten($primary-color, 40%);
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框样式
|
||||
.el-dialog {
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: $spacing-medium $spacing-large;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: $spacing-large;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Element Plus 主题色自定义 */
|
||||
:root {
|
||||
--el-color-primary: #019C7C !important;
|
||||
--el-color-primary-light-3: rgba(1, 156, 124, 0.7) !important;
|
||||
--el-color-primary-light-5: rgba(1, 156, 124, 0.5) !important;
|
||||
--el-color-primary-light-7: rgba(1, 156, 124, 0.3) !important;
|
||||
--el-color-primary-light-8: rgba(1, 156, 124, 0.2) !important;
|
||||
--el-color-primary-light-9: rgba(1, 156, 124, 0.1) !important;
|
||||
--el-color-primary-dark-2: #0e8a71 !important;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
@use './variables.scss' as *;
|
||||
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
background-color: #F5F7FA;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Element Plus 主题色自定义 */
|
||||
:root {
|
||||
--el-color-primary: #019C7C !important;
|
||||
--el-color-primary-light-3: rgba(1, 156, 124, 0.7) !important;
|
||||
--el-color-primary-light-5: rgba(1, 156, 124, 0.5) !important;
|
||||
--el-color-primary-light-7: rgba(1, 156, 124, 0.3) !important;
|
||||
--el-color-primary-light-8: rgba(1, 156, 124, 0.2) !important;
|
||||
--el-color-primary-light-9: rgba(1, 156, 124, 0.1) !important;
|
||||
--el-color-primary-dark-2: #0e8a71 !important;
|
||||
--el-border-radius-base: 4px;
|
||||
--el-font-size-base: 14px;
|
||||
}
|
||||
|
||||
/* 工具类样式 */
|
||||
// 间距工具类
|
||||
.m-0 { margin: 0; }
|
||||
.m-1 { margin: 8px; }
|
||||
.m-2 { margin: 12px; }
|
||||
.m-3 { margin: 16px; }
|
||||
.m-4 { margin: 24px; }
|
||||
.m-5 { margin: 32px; }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-1 { padding: 8px; }
|
||||
.p-2 { padding: 12px; }
|
||||
.p-3 { padding: 16px; }
|
||||
.p-4 { padding: 24px; }
|
||||
.p-5 { padding: 32px; }
|
||||
|
||||
// 文本工具类
|
||||
.text-left { text-align: left; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.text-primary { color: #019C7C; }
|
||||
.text-success { color: #67C23A; }
|
||||
.text-warning { color: #E6A23C; }
|
||||
.text-danger { color: #F56C6C; }
|
||||
.text-info { color: #909399; }
|
||||
|
||||
// 显示工具类
|
||||
.d-none { display: none; }
|
||||
.d-block { display: block; }
|
||||
.d-flex { display: flex; }
|
||||
.d-inline { display: inline; }
|
||||
.d-inline-block { display: inline-block; }
|
||||
|
||||
// Flex 布局工具类
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// 文本省略
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ellipsis-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
.card {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #EBEEF5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
.scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #F5F7FA;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式工具类
|
||||
@media (max-width: 768px) {
|
||||
.hide-mobile { display: none !important; }
|
||||
.show-mobile { display: block !important; }
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.hide-desktop { display: none !important; }
|
||||
.show-desktop { display: block !important; }
|
||||
}
|
||||
|
||||
// 动画过渡
|
||||
.transition {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// 悬停阴影
|
||||
.hover-shadow {
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 主色调
|
||||
$primary-color: #019C7C; // 主色
|
||||
$success-color: #67C23A; // 成功色
|
||||
$warning-color: #E6A23C; // 警告色
|
||||
$danger-color: #F56C6C; // 危险色
|
||||
$info-color: #909399; // 信息色
|
||||
|
||||
// 中性色
|
||||
$text-color-primary: #303133; // 主要文字
|
||||
$text-color-regular: #606266; // 常规文字
|
||||
$text-color-secondary: #909399; // 次要文字
|
||||
$text-color-placeholder: #C0C4CC; // 占位符文字
|
||||
|
||||
// 边框色
|
||||
$border-color-base: #DCDFE6;
|
||||
$border-color-light: #E4E7ED;
|
||||
$border-color-lighter: #EBEEF5;
|
||||
$border-color-extra-light: #F2F6FC;
|
||||
|
||||
// 背景色
|
||||
$background-color-base: #F5F7FA;
|
||||
$background-color-white: #FFFFFF;
|
||||
|
||||
// 字体大小
|
||||
$font-size-extra-large: 20px;
|
||||
$font-size-large: 18px;
|
||||
$font-size-medium: 16px;
|
||||
$font-size-base: 14px;
|
||||
$font-size-small: 13px;
|
||||
$font-size-extra-small: 12px;
|
||||
|
||||
// 字体粗细
|
||||
$font-weight-primary: 500;
|
||||
$font-weight-secondary: 400;
|
||||
|
||||
// 行高
|
||||
$line-height-primary: 24px;
|
||||
$line-height-secondary: 16px;
|
||||
|
||||
// 间距变量
|
||||
$spacing-extra-large: 32px;
|
||||
$spacing-large: 24px;
|
||||
$spacing-medium: 16px;
|
||||
$spacing-base: 12px;
|
||||
$spacing-small: 8px;
|
||||
$spacing-extra-small: 4px;
|
||||
|
||||
// 圆角
|
||||
$border-radius-base: 4px;
|
||||
$border-radius-small: 2px;
|
||||
$border-radius-round: 20px;
|
||||
$border-radius-circle: 100%;
|
||||
|
||||
// 阴影
|
||||
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 响应式断点
|
||||
$breakpoint-mobile: 768px;
|
||||
$breakpoint-tablet: 992px;
|
||||
$breakpoint-desktop: 1200px;
|
||||
$breakpoint-wide: 1920px;
|
||||
|
||||
// Z-index 层级
|
||||
$z-index-popper: 2000;
|
||||
$z-index-dialog: 2001;
|
||||
$z-index-drawer: 2002;
|
||||
$z-index-message: 3000;
|
||||
$z-index-notification: 4000;
|
||||
$z-index-tooltip: 2003;
|
||||
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div class="app-layout" :class="layoutClasses">
|
||||
<!-- 移动端遮罩层 -->
|
||||
<div
|
||||
v-if="appStore.isMobile && !appStore.sidebarCollapsed"
|
||||
class="layout-overlay"
|
||||
@click="appStore.setSidebarCollapsed(true)"
|
||||
></div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<Sidebar
|
||||
:app-name="props.appName"
|
||||
:nav-items="props.navItems"
|
||||
@nav-click="handleNavClick"
|
||||
/>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="layout-main" :class="mainClasses">
|
||||
<!-- 顶部导航 -->
|
||||
<NavContent />
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<div class="layout-content">
|
||||
<div class="content-container">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
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
|
||||
}
|
||||
|
||||
// 组件 Props
|
||||
interface Props {
|
||||
appName?: string
|
||||
navItems?: NavItem[]
|
||||
breadcrumbItems?: BreadcrumbItem[]
|
||||
pageTitle?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
appName: 'Cedar-V',
|
||||
navItems: () => [],
|
||||
breadcrumbItems: () => [],
|
||||
pageTitle: ''
|
||||
})
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
navClick: [item: NavItem, event: Event]
|
||||
}>()
|
||||
|
||||
// 使用 store
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 处理导航点击
|
||||
const handleNavClick = (item: NavItem, event: Event) => {
|
||||
emit('navClick', item, event)
|
||||
}
|
||||
|
||||
// 计算类名
|
||||
const layoutClasses = computed(() => ({
|
||||
'app-layout--mobile': appStore.isMobile,
|
||||
'app-layout--sidebar-collapsed': appStore.sidebarCollapsed
|
||||
}))
|
||||
|
||||
const mainClasses = computed(() => ({
|
||||
'layout-main--mobile': appStore.isMobile,
|
||||
'layout-main--sidebar-collapsed': appStore.sidebarCollapsed
|
||||
}))
|
||||
|
||||
// 响应式设备检测 - 统一断点系统
|
||||
const checkResponsive = () => {
|
||||
const width = window.innerWidth
|
||||
const isMobile = width <= 768 // 768px 及以下为移动端
|
||||
const isTablet = width > 768 && width <= 1024 // 769-1024px 为平板
|
||||
|
||||
appStore.setMobile(isMobile)
|
||||
|
||||
// 移动端和小平板自动折叠侧边栏
|
||||
if (isMobile || isTablet) {
|
||||
appStore.setSidebarCollapsed(true)
|
||||
} else if (width > 1024) {
|
||||
// 桌面端默认展开侧边栏
|
||||
appStore.setSidebarCollapsed(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
checkResponsive()
|
||||
window.addEventListener('resize', checkResponsive)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkResponsive)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-layout {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #F5F7FA;
|
||||
overflow: hidden;
|
||||
|
||||
&--mobile {
|
||||
// 移动端特定样式
|
||||
}
|
||||
|
||||
&--sidebar-collapsed {
|
||||
// 侧边栏折叠状态样式
|
||||
}
|
||||
}
|
||||
|
||||
// 遮罩层
|
||||
.layout-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1999;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
// 主内容区域
|
||||
.layout-main {
|
||||
margin-left: 280px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&--sidebar-collapsed {
|
||||
margin-left: 64px;
|
||||
}
|
||||
|
||||
&--mobile {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面内容
|
||||
.layout-content {
|
||||
flex: 1;
|
||||
padding-top: 80px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
|
||||
/* 滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
min-height: calc(100vh - 80px);
|
||||
padding: 24px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// 响应式设计 - 统一断点系统
|
||||
// 平板以下:768px 及以下为移动端
|
||||
@media (max-width: 768px) {
|
||||
.layout-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏手机:480px 及以下
|
||||
@media (max-width: 480px) {
|
||||
.content-container {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 平板:769px - 1024px 之间
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.layout-main {
|
||||
margin-left: 64px; // 平板显示折叠侧边栏
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.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;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
// 高对比度模式支持
|
||||
@media (prefers-contrast: high) {
|
||||
.app-layout {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
// 减少动画模式
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.app-layout *,
|
||||
.layout-main,
|
||||
.layout-overlay {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File renamed without changes.
+76
-45
@@ -7,32 +7,30 @@
|
||||
-->
|
||||
<template>
|
||||
<!-- 侧边栏容器 -->
|
||||
<aside
|
||||
class="sidebar"
|
||||
:class="{ 'sidebar--collapsed': isCollapsed }"
|
||||
>
|
||||
<aside class="sidebar" :class="{
|
||||
'sidebar--collapsed': isCollapsed,
|
||||
'sidebar--mobile-open': appStore.isMobile && !isCollapsed
|
||||
}">
|
||||
<!-- Logo区域 -->
|
||||
<div class="sidebar__header">
|
||||
<div class="sidebar__logo">
|
||||
<div class="sidebar__logo" v-show="!isCollapsed">
|
||||
<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"/>
|
||||
<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>
|
||||
<span 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 v-if="collapsible" class="sidebar__toggle" :class="{ 'sidebar__toggle--collapsed': isCollapsed }"
|
||||
@click="toggleSidebar" :aria-label="isCollapsed ? '展开侧边栏' : '收起侧边栏'">
|
||||
<el-icon class="toggle-icon">
|
||||
<component :is="isCollapsed ? 'ArrowRight' : 'ArrowLeft'" />
|
||||
</el-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -41,12 +39,8 @@
|
||||
<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)"
|
||||
>
|
||||
<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" />
|
||||
@@ -57,14 +51,8 @@
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<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)"
|
||||
>
|
||||
<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>
|
||||
@@ -83,7 +71,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
|
||||
// 导航项接口定义
|
||||
interface NavItem {
|
||||
@@ -117,13 +106,16 @@ const emit = defineEmits<{
|
||||
toggle: [collapsed: boolean]
|
||||
}>()
|
||||
|
||||
// 侧边栏折叠状态管理
|
||||
const isCollapsed = ref(props.defaultCollapsed)
|
||||
// 使用全局状态管理
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 从 store 获取折叠状态
|
||||
const isCollapsed = computed(() => appStore.sidebarCollapsed)
|
||||
|
||||
// 切换侧边栏折叠状态
|
||||
const toggleSidebar = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
emit('toggle', isCollapsed.value)
|
||||
appStore.toggleSidebar()
|
||||
emit('toggle', !appStore.sidebarCollapsed)
|
||||
}
|
||||
|
||||
// 处理导航项点击事件
|
||||
@@ -190,22 +182,57 @@ const handleNavClick = (item: NavItem, event: Event) => {
|
||||
}
|
||||
|
||||
.sidebar__toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #B2B8C2;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(29, 29, 29, 0.08);
|
||||
}
|
||||
|
||||
/* 未选中状态的hover */
|
||||
.sidebar__toggle:hover {
|
||||
background: rgba(1, 156, 124, 0.1);
|
||||
background: #F8F9FA;
|
||||
color: #019C7C;
|
||||
border-color: rgba(1, 156, 124, 0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 选中状态(折叠时的样式) */
|
||||
.sidebar__toggle--collapsed {
|
||||
background: linear-gradient(135deg, #019C7C 0%, #0BB68A 100%);
|
||||
color: #FFFFFF;
|
||||
border-color: #019C7C;
|
||||
box-shadow: 0px 2px 8px 0px rgba(1, 156, 124, 0.24);
|
||||
}
|
||||
|
||||
/* 选中状态的hover */
|
||||
.sidebar__toggle--collapsed:hover {
|
||||
background: linear-gradient(135deg, #028970 0%, #0AA67D 100%);
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0px 4px 12px 0px rgba(1, 156, 124, 0.32);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 14px;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.sidebar__toggle:hover .toggle-icon {
|
||||
transform: translateX(1px);
|
||||
}
|
||||
|
||||
.sidebar__toggle--collapsed:hover .toggle-icon {
|
||||
transform: translateX(-1px);
|
||||
}
|
||||
|
||||
/* 导航区域 */
|
||||
@@ -220,14 +247,15 @@ const handleNavClick = (item: NavItem, event: Event) => {
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
padding: 12px 20px;
|
||||
color: #1D1D1D;
|
||||
text-decoration: none;
|
||||
border-radius: 28px;
|
||||
@@ -236,7 +264,6 @@ const handleNavClick = (item: NavItem, event: Event) => {
|
||||
font-family: 'OPPOSans', sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
@@ -245,7 +272,7 @@ const handleNavClick = (item: NavItem, event: Event) => {
|
||||
}
|
||||
|
||||
.nav-link--active {
|
||||
background: linear-gradient(90deg, #39CC68 0%, #A6E852 100%);
|
||||
background: rgba(0, 194, 124, 0.12);
|
||||
color: #019C7C;
|
||||
font-weight: 700;
|
||||
box-shadow: 0px 2px 32px 0px rgba(0, 0, 0, 0.02);
|
||||
@@ -273,6 +300,10 @@ const handleNavClick = (item: NavItem, event: Event) => {
|
||||
color: #B2B8C2;
|
||||
}
|
||||
|
||||
.nav-link:hover .nav-icon {
|
||||
color: #019C7C;
|
||||
}
|
||||
|
||||
.nav-link--active .nav-icon {
|
||||
color: #019C7C;
|
||||
}
|
||||
@@ -363,7 +394,7 @@ const handleNavClick = (item: NavItem, event: Event) => {
|
||||
|
||||
.sidebar--collapsed .nav-link {
|
||||
justify-content: center;
|
||||
padding: 24px 12px;
|
||||
padding: 3px 12px;
|
||||
}
|
||||
|
||||
.sidebar--collapsed .nav-text {
|
||||
File renamed without changes.
@@ -10,8 +10,12 @@ import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
// 全局样式重置
|
||||
import './assets/styles/global.css'
|
||||
// 全局样式系统
|
||||
import './assets/styles/global.scss'
|
||||
import './assets/styles/element-theme.scss'
|
||||
|
||||
// Pinia状态管理
|
||||
import pinia from './store'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
@@ -30,6 +34,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.use(ElementPlus, {
|
||||
locale: getCurrentElementLocale()
|
||||
})
|
||||
app.use(pinia)
|
||||
app.use(i18n)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
export default pinia
|
||||
|
||||
// 导出所有store
|
||||
export * from './modules/app'
|
||||
export * from './modules/user'
|
||||
@@ -0,0 +1,63 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// 状态
|
||||
const sidebarCollapsed = ref(false)
|
||||
const isMobile = ref(false)
|
||||
const theme = ref<'light' | 'dark' | 'auto'>('light')
|
||||
const language = ref<'zh' | 'en'>('zh')
|
||||
const loading = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const isDark = computed(() => {
|
||||
if (theme.value === 'auto') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
return theme.value === 'dark'
|
||||
})
|
||||
|
||||
// 操作
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
const setSidebarCollapsed = (collapsed: boolean) => {
|
||||
sidebarCollapsed.value = collapsed
|
||||
}
|
||||
|
||||
const setMobile = (mobile: boolean) => {
|
||||
isMobile.value = mobile
|
||||
}
|
||||
|
||||
const setTheme = (newTheme: 'light' | 'dark' | 'auto') => {
|
||||
theme.value = newTheme
|
||||
document.documentElement.setAttribute('data-theme', newTheme)
|
||||
}
|
||||
|
||||
const setLanguage = (lang: 'zh' | 'en') => {
|
||||
language.value = lang
|
||||
}
|
||||
|
||||
const setLoading = (isLoading: boolean) => {
|
||||
loading.value = isLoading
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
sidebarCollapsed,
|
||||
isMobile,
|
||||
theme,
|
||||
language,
|
||||
loading,
|
||||
// 计算属性
|
||||
isDark,
|
||||
// 操作
|
||||
toggleSidebar,
|
||||
setSidebarCollapsed,
|
||||
setMobile,
|
||||
setTheme,
|
||||
setLanguage,
|
||||
setLoading
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface UserInfo {
|
||||
id: string
|
||||
username: string
|
||||
name: string
|
||||
email: string
|
||||
avatar?: string
|
||||
role: 'admin' | 'user' | 'guest'
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 状态
|
||||
const token = ref<string | null>(localStorage.getItem('token'))
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const isLoggedIn = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const isAdmin = computed(() => userInfo.value?.role === 'admin')
|
||||
const hasPermission = computed(() => (permission: string) => {
|
||||
return userInfo.value?.permissions?.includes(permission) || false
|
||||
})
|
||||
|
||||
// 操作
|
||||
const setToken = (newToken: string | null) => {
|
||||
token.value = newToken
|
||||
if (newToken) {
|
||||
localStorage.setItem('token', newToken)
|
||||
} else {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
}
|
||||
|
||||
const setUserInfo = (info: UserInfo | null) => {
|
||||
userInfo.value = info
|
||||
isLoggedIn.value = !!info
|
||||
}
|
||||
|
||||
const login = async (credentials: { username: string; password: string }) => {
|
||||
// 这里应该调用登录API
|
||||
// const response = await loginApi(credentials)
|
||||
// setToken(response.data.token)
|
||||
// setUserInfo(response.data.user)
|
||||
|
||||
// 模拟登录
|
||||
setToken('mock-token-123')
|
||||
setUserInfo({
|
||||
id: '1',
|
||||
username: credentials.username,
|
||||
name: '管理员',
|
||||
email: 'admin@example.com',
|
||||
role: 'admin',
|
||||
permissions: ['*']
|
||||
})
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
setToken(null)
|
||||
setUserInfo(null)
|
||||
}
|
||||
|
||||
const updateProfile = (profile: Partial<UserInfo>) => {
|
||||
if (userInfo.value) {
|
||||
userInfo.value = { ...userInfo.value, ...profile }
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时检查登录状态
|
||||
const initAuth = () => {
|
||||
if (token.value) {
|
||||
// 这里应该调用API验证token有效性
|
||||
// 暂时模拟已登录状态
|
||||
isLoggedIn.value = true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
token,
|
||||
userInfo,
|
||||
isLoggedIn,
|
||||
// 计算属性
|
||||
isAdmin,
|
||||
hasPermission,
|
||||
// 操作
|
||||
setToken,
|
||||
setUserInfo,
|
||||
login,
|
||||
logout,
|
||||
updateProfile,
|
||||
initAuth
|
||||
}
|
||||
})
|
||||
@@ -20,8 +20,8 @@
|
||||
<script setup lang="ts">
|
||||
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';
|
||||
import Layout from '@/components/common/layout/Layout.vue';
|
||||
import type { NavItem, BreadcrumbItem } from '@/components/common/layout/index';
|
||||
const router = useRouter();
|
||||
|
||||
const navigationItems = ref<NavItem[]>([
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
<!--
|
||||
/**
|
||||
* 主布局组件
|
||||
* 提供完整的后台管理系统布局结构,包括侧边栏、顶部导航和主内容区域
|
||||
* 支持响应式设计和侧边栏收起/展开功能
|
||||
*/
|
||||
-->
|
||||
<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>
|
||||
+60
-14
@@ -50,6 +50,10 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
open: true, // 自动打开浏览器
|
||||
https: false, // 不启用HTTPS
|
||||
cors: true, // 启用CORS
|
||||
// 开发环境热更新优化
|
||||
hmr: {
|
||||
overlay: true
|
||||
},
|
||||
proxy: { // 代理配置
|
||||
"/devApi": {
|
||||
target: "http://104.156.140.42:18888", // 目标服务器
|
||||
@@ -59,6 +63,20 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
}
|
||||
},
|
||||
|
||||
// 依赖优化配置
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'pinia',
|
||||
'element-plus',
|
||||
'@element-plus/icons-vue',
|
||||
'axios',
|
||||
'vue-i18n'
|
||||
],
|
||||
exclude: ['@vueuse/core'] // 排除某些包的预构建
|
||||
},
|
||||
|
||||
// 模块解析配置
|
||||
resolve: {
|
||||
alias: { // 路径别名
|
||||
@@ -80,26 +98,54 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
// 构建配置
|
||||
build: {
|
||||
minify: 'terser', // 使用terser进行代码压缩
|
||||
assetsInlineLimit: 8 * 1024, // 小于8KB的资源转为base64
|
||||
sourcemap: isProd, // 生产环境生成sourcemap
|
||||
outDir: env.VITE_OUTDIR, // 输出目录(从环境变量获取)
|
||||
assetsInlineLimit: 4 * 1024, // 小于4KB的资源转为base64(优化)
|
||||
sourcemap: !isProd, // 开发环境生成sourcemap,生产环境不生成
|
||||
outDir: env.VITE_OUTDIR || 'dist', // 输出目录
|
||||
emptyOutDir: true, // 构建前清空输出目录
|
||||
chunkSizeWarningLimit: 1500, // 块大小警告阈值(KB)
|
||||
rollupOptions: { // Rollup打包配置
|
||||
output: {
|
||||
manualChunks(id) { // 手动分块策略
|
||||
if (id.includes('node_modules')) {
|
||||
// 将node_modules中的依赖单独分块
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||
chunkSizeWarningLimit: 1000, // 降低块大小警告阈值至1MB
|
||||
cssCodeSplit: true, // CSS代码分割
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: isProd, // 生产环境移除console
|
||||
drop_debugger: isProd, // 生产环境移除debugger
|
||||
pure_funcs: isProd ? ['console.log'] : [] // 移除纯函数调用
|
||||
}
|
||||
},
|
||||
// 输出文件命名规则(添加时间戳避免缓存)
|
||||
chunkFileNames: `static/js/[name]-[hash]${Timestamp}.js`,
|
||||
entryFileNames: `static/js/[name]-[hash]${Timestamp}.js`,
|
||||
assetFileNames: `static/[ext]/[name]-[hash]${Timestamp}.[ext]`,
|
||||
rollupOptions: { // Rollup打包配置
|
||||
output: {
|
||||
// 优化的手动分块策略
|
||||
manualChunks: {
|
||||
// Vue核心
|
||||
vue: ['vue', 'vue-router', 'pinia'],
|
||||
// Element Plus相关
|
||||
elementPlus: ['element-plus'],
|
||||
// Element Plus图标
|
||||
elementIcons: ['@element-plus/icons-vue'],
|
||||
// 工具库
|
||||
utils: ['axios'],
|
||||
// 其他第三方库单独打包
|
||||
vendor: ['vue-i18n']
|
||||
},
|
||||
// 输出文件命名规则(优化缓存策略)
|
||||
chunkFileNames: `static/js/[name].[hash].js`,
|
||||
entryFileNames: `static/js/[name].[hash].js`,
|
||||
assetFileNames: (assetInfo) => {
|
||||
const name = assetInfo.name || ''
|
||||
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(name)) {
|
||||
return `static/media/[name].[hash][extname]`
|
||||
} else if (/\.(png|jpe?g|gif|svg|ico|webp)(\?.*)?$/i.test(name)) {
|
||||
return `static/images/[name].[hash][extname]`
|
||||
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(name)) {
|
||||
return `static/fonts/[name].[hash][extname]`
|
||||
} else {
|
||||
return `static/assets/[name].[hash][extname]`
|
||||
}
|
||||
}
|
||||
},
|
||||
// 外部化依赖(CDN优化,可选)
|
||||
external: isProd ? [] : []
|
||||
}
|
||||
},
|
||||
|
||||
// JSON文件处理配置
|
||||
json: {
|
||||
|
||||
Reference in new issue
Block a user