build:根据ai开发文档修改项目结构

This commit is contained in:
13895237362 committed 2025-08-11 11:55:29 +08:00
1 parent 81761798ea
commit 0279083c59
19 files changed
+3246 -880

No files matched your search

+48
View File
@@ -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'
}
}
+14
View File
@@ -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
}
+2223 -393
View File
File diff suppressed because it is too large. Load diff
+35 -17
View File
@@ -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;
}
}
-28
View File
@@ -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;
}
+173
View File
@@ -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);
}
}
+70
View File
@@ -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>
@@ -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" />
@@ -54,17 +48,11 @@
</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)"
>
<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;
}
@@ -326,11 +357,11 @@ const handleNavClick = (item: NavItem, event: Event) => {
.sidebar {
transform: translateX(-100%);
}
.sidebar--mobile-open {
transform: translateX(0);
}
.sidebar--collapsed {
width: 280px;
}
@@ -341,7 +372,7 @@ const handleNavClick = (item: NavItem, event: Event) => {
width: 100vw;
max-width: 320px;
}
.sidebar--collapsed {
width: 100vw;
max-width: 320px;
@@ -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 {
+7 -2
View File
@@ -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')
+9
View File
@@ -0,0 +1,9 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
// 导出所有store
export * from './modules/app'
export * from './modules/user'
+63
View File
@@ -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
}
})
+95
View File
@@ -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
}
})
+2 -2
View File
@@ -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
View File
@@ -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,24 +98,52 @@ 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)
chunkSizeWarningLimit: 1000, // 降低块大小警告阈值至1MB
cssCodeSplit: true, // CSS代码分割
terserOptions: {
compress: {
drop_console: isProd, // 生产环境移除console
drop_debugger: isProd, // 生产环境移除debugger
pure_funcs: isProd ? ['console.log'] : [] // 移除纯函数调用
}
},
rollupOptions: { // Rollup打包配置
output: {
manualChunks(id) { // 手动分块策略
if (id.includes('node_modules')) {
// 将node_modules中的依赖单独分块
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
// 优化的手动分块策略
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]${Timestamp}.js`,
entryFileNames: `static/js/[name]-[hash]${Timestamp}.js`,
assetFileNames: `static/[ext]/[name]-[hash]${Timestamp}.[ext]`,
}
// 输出文件命名规则(优化缓存策略)
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 ? [] : []
}
},