更新代码
This commit is contained in:
		
							parent
							
								
									81d7bd6fd9
								
							
						
					
					
						commit
						6857d2c3de
					
				
							
								
								
									
										22
									
								
								.htaccess
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								.htaccess
									
									
									
									
									
								
							| @ -1 +1,21 @@ | |||||||
|   | <IfModule mod_rewrite.c> | ||||||
|  |   Options +FollowSymlinks -Multiviews | ||||||
|  |   RewriteEngine On | ||||||
|  | 
 | ||||||
|  |   RewriteCond %{REQUEST_FILENAME} !-d | ||||||
|  |   RewriteCond %{REQUEST_FILENAME} !-f | ||||||
|  |   RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L] | ||||||
|  | </IfModule> | ||||||
|  | 
 | ||||||
|  | # 允许跨域请求 | ||||||
|  | <IfModule mod_headers.c> | ||||||
|  |     Header always set Access-Control-Allow-Origin "*" | ||||||
|  |     Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" | ||||||
|  |     Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" | ||||||
|  |     Header always set Access-Control-Max-Age "86400" | ||||||
|  | </IfModule> | ||||||
|  | 
 | ||||||
|  | # 处理OPTIONS预检请求 | ||||||
|  | RewriteEngine On | ||||||
|  | RewriteCond %{REQUEST_METHOD} OPTIONS | ||||||
|  | RewriteRule ^(.*)$ $1 [R=200,L]  | ||||||
							
								
								
									
										
											BIN
										
									
								
								frontend/.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/.env
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										4
									
								
								frontend/.env.development
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/.env.development
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | VITE_APP_ENV=development | ||||||
|  | VITE_APP_DEBUG_MODE=true | ||||||
|  | VITE_APP_TITLE=项目管理系统 | ||||||
|  | VITE_APP_API_BASE_URL=https://www.yunzer.cn/api | ||||||
							
								
								
									
										4
									
								
								frontend/.env.production
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/.env.production
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | VITE_APP_ENV=production | ||||||
|  | VITE_APP_DEBUG_MODE=false | ||||||
|  | VITE_APP_TITLE=项目管理系统 | ||||||
|  | VITE_APP_API_BASE_URL=https://www.yunzer.cn/api | ||||||
							
								
								
									
										24
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | # Logs | ||||||
|  | logs | ||||||
|  | *.log | ||||||
|  | npm-debug.log* | ||||||
|  | yarn-debug.log* | ||||||
|  | yarn-error.log* | ||||||
|  | pnpm-debug.log* | ||||||
|  | lerna-debug.log* | ||||||
|  | 
 | ||||||
|  | node_modules | ||||||
|  | dist | ||||||
|  | dist-ssr | ||||||
|  | *.local | ||||||
|  | 
 | ||||||
|  | # Editor directories and files | ||||||
|  | .vscode/* | ||||||
|  | !.vscode/extensions.json | ||||||
|  | .idea | ||||||
|  | .DS_Store | ||||||
|  | *.suo | ||||||
|  | *.ntvs* | ||||||
|  | *.njsproj | ||||||
|  | *.sln | ||||||
|  | *.sw? | ||||||
							
								
								
									
										3
									
								
								frontend/.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | { | ||||||
|  |   "recommendations": ["Vue.volar"] | ||||||
|  | } | ||||||
							
								
								
									
										162
									
								
								frontend/API_SETUP.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								frontend/API_SETUP.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,162 @@ | |||||||
|  | # API 设置说明 | ||||||
|  | 
 | ||||||
|  | ## 概述 | ||||||
|  | 
 | ||||||
|  | 本项目已从模拟登录改为真实的API调用,支持通过数据库进行用户认证。 | ||||||
|  | 
 | ||||||
|  | ## 后端API要求 | ||||||
|  | 
 | ||||||
|  | ### 1. 基础配置 | ||||||
|  | 
 | ||||||
|  | - **API基础URL**: `http://localhost:8000/api` (开发环境) | ||||||
|  | - **请求格式**: JSON | ||||||
|  | - **认证方式**: Bearer Token | ||||||
|  | 
 | ||||||
|  | ### 2. 必需接口 | ||||||
|  | 
 | ||||||
|  | #### 用户登录 | ||||||
|  | ``` | ||||||
|  | POST /api/user/login | ||||||
|  | Content-Type: application/json | ||||||
|  | 
 | ||||||
|  | 请求体: | ||||||
|  | { | ||||||
|  |   "username": "用户名", | ||||||
|  |   "password": "密码" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 响应格式: | ||||||
|  | { | ||||||
|  |   "code": 200, | ||||||
|  |   "message": "登录成功", | ||||||
|  |   "data": { | ||||||
|  |     "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | ||||||
|  |     "userInfo": { | ||||||
|  |       "id": 1, | ||||||
|  |       "username": "admin", | ||||||
|  |       "name": "管理员", | ||||||
|  |       "avatar": "/static/images/avatar.png", | ||||||
|  |       "role": "admin" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### 获取用户信息 | ||||||
|  | ``` | ||||||
|  | GET /api/user/info | ||||||
|  | Authorization: Bearer {token} | ||||||
|  | 
 | ||||||
|  | 响应格式: | ||||||
|  | { | ||||||
|  |   "code": 200, | ||||||
|  |   "message": "获取成功", | ||||||
|  |   "data": { | ||||||
|  |     "id": 1, | ||||||
|  |     "username": "admin", | ||||||
|  |     "name": "管理员", | ||||||
|  |     "avatar": "/static/images/avatar.png", | ||||||
|  |     "role": "admin" | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### 用户登出 | ||||||
|  | ``` | ||||||
|  | POST /api/user/logout | ||||||
|  | Authorization: Bearer {token} | ||||||
|  | 
 | ||||||
|  | 响应格式: | ||||||
|  | { | ||||||
|  |   "code": 200, | ||||||
|  |   "message": "登出成功", | ||||||
|  |   "data": null | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### 修改密码 | ||||||
|  | ``` | ||||||
|  | POST /api/user/change-password | ||||||
|  | Authorization: Bearer {token} | ||||||
|  | Content-Type: application/json | ||||||
|  | 
 | ||||||
|  | 请求体: | ||||||
|  | { | ||||||
|  |   "oldPassword": "旧密码", | ||||||
|  |   "newPassword": "新密码" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 响应格式: | ||||||
|  | { | ||||||
|  |   "code": 200, | ||||||
|  |   "message": "密码修改成功", | ||||||
|  |   "data": null | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 3. 错误处理 | ||||||
|  | 
 | ||||||
|  | 所有接口都应该返回统一的错误格式: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "code": 400, | ||||||
|  |   "message": "错误描述", | ||||||
|  |   "data": null | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 常见HTTP状态码: | ||||||
|  | - `200`: 成功 | ||||||
|  | - `400`: 请求参数错误 | ||||||
|  | - `401`: 未授权(token无效或过期) | ||||||
|  | - `500`: 服务器内部错误 | ||||||
|  | 
 | ||||||
|  | ## 环境配置 | ||||||
|  | 
 | ||||||
|  | ### 开发环境 | ||||||
|  | - 后端API运行在 `http://localhost:8000` | ||||||
|  | - 前端开发服务器运行在 `http://localhost:5173` | ||||||
|  | 
 | ||||||
|  | ### 生产环境 | ||||||
|  | - 使用相对路径 `/api` | ||||||
|  | - 需要配置反向代理 | ||||||
|  | 
 | ||||||
|  | ## 数据库表结构建议 | ||||||
|  | 
 | ||||||
|  | ### 用户表 (users) | ||||||
|  | ```sql | ||||||
|  | CREATE TABLE users ( | ||||||
|  |   id INT PRIMARY KEY AUTO_INCREMENT, | ||||||
|  |   username VARCHAR(50) UNIQUE NOT NULL, | ||||||
|  |   password VARCHAR(255) NOT NULL, -- 加密后的密码 | ||||||
|  |   name VARCHAR(100) NOT NULL, | ||||||
|  |   avatar VARCHAR(255), | ||||||
|  |   role VARCHAR(20) DEFAULT 'user', | ||||||
|  |   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|  |   updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||||
|  | ); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 示例数据 | ||||||
|  | ```sql | ||||||
|  | INSERT INTO users (username, password, name, role) VALUES  | ||||||
|  | ('admin', '$2y$10$...', '管理员', 'admin'), | ||||||
|  | ('user1', '$2y$10$...', '普通用户', 'user'); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 注意事项 | ||||||
|  | 
 | ||||||
|  | 1. **密码安全**: 使用bcrypt等加密算法存储密码 | ||||||
|  | 2. **Token安全**: 使用JWT等安全的token机制 | ||||||
|  | 3. **CORS配置**: 确保前端可以访问后端API | ||||||
|  | 4. **错误处理**: 提供清晰的错误信息 | ||||||
|  | 5. **日志记录**: 记录登录、登出等重要操作 | ||||||
|  | 
 | ||||||
|  | ## 测试 | ||||||
|  | 
 | ||||||
|  | 1. 启动后端服务 | ||||||
|  | 2. 确保API接口正常工作 | ||||||
|  | 3. 启动前端开发服务器 | ||||||
|  | 4. 尝试使用真实用户名密码登录 | ||||||
|  | 5. 检查token是否正确保存和使用  | ||||||
							
								
								
									
										102
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,102 @@ | |||||||
|  | # 项目管理系统 - 前端 | ||||||
|  | 
 | ||||||
|  | ## 开发环境 | ||||||
|  | 
 | ||||||
|  | ### 安装依赖 | ||||||
|  | ```bash | ||||||
|  | npm install | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 启动开发服务器 | ||||||
|  | ```bash | ||||||
|  | npm run dev | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 构建生产版本 | ||||||
|  | ```bash | ||||||
|  | npm run build | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 类型检查 | ||||||
|  | ```bash | ||||||
|  | npm run type-check | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 部署说明 | ||||||
|  | 
 | ||||||
|  | ### 1. 构建项目 | ||||||
|  | ```bash | ||||||
|  | npm run build | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 2. 部署方式 | ||||||
|  | 
 | ||||||
|  | #### 方式一:直接部署 dist 目录 | ||||||
|  | 将 `dist` 目录中的所有文件复制到 Web 服务器的根目录或子目录中。 | ||||||
|  | 
 | ||||||
|  | #### 方式二:使用静态文件服务器 | ||||||
|  | ```bash | ||||||
|  | # 使用 serve 包 | ||||||
|  | npx serve dist | ||||||
|  | 
 | ||||||
|  | # 或使用 http-server | ||||||
|  | npx http-server dist | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 3. 环境配置 | ||||||
|  | 
 | ||||||
|  | 项目已配置为支持子目录部署,主要配置: | ||||||
|  | 
 | ||||||
|  | - **基础路径**: `./` (相对路径) | ||||||
|  | - **路由模式**: Hash 模式 (`#`) | ||||||
|  | - **API 地址**: `https://api.yunzer.com.cn` | ||||||
|  | 
 | ||||||
|  | ### 4. 注意事项 | ||||||
|  | 
 | ||||||
|  | 1. **资源路径**: 所有静态资源都使用相对路径,支持任意目录部署 | ||||||
|  | 2. **路由**: 使用 Hash 路由,无需服务器端配置 | ||||||
|  | 3. **API**: 确保 API 服务器 `api.yunzer.com.cn` 可访问 | ||||||
|  | 4. **HTTPS**: 生产环境建议使用 HTTPS | ||||||
|  | 
 | ||||||
|  | ### 5. 环境变量 | ||||||
|  | 
 | ||||||
|  | 可以通过 `.env` 文件配置环境变量: | ||||||
|  | 
 | ||||||
|  | ```env | ||||||
|  | VITE_APP_TITLE=项目管理系统 | ||||||
|  | VITE_APP_API_BASE_URL=https://api.yunzer.com.cn | ||||||
|  | VITE_APP_API_TIMEOUT=10000 | ||||||
|  | VITE_APP_ENV=production | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 技术栈 | ||||||
|  | 
 | ||||||
|  | - **框架**: Vue 3 + TypeScript | ||||||
|  | - **构建工具**: Vite | ||||||
|  | - **UI 组件**: Element Plus | ||||||
|  | - **状态管理**: Pinia | ||||||
|  | - **路由**: Vue Router | ||||||
|  | - **HTTP 客户端**: Axios | ||||||
|  | 
 | ||||||
|  | ## 项目结构 | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | src/ | ||||||
|  | ├── api/          # API 接口 | ||||||
|  | ├── assets/       # 静态资源 | ||||||
|  | ├── components/   # 公共组件 | ||||||
|  | ├── config/       # 配置文件 | ||||||
|  | ├── router/       # 路由配置 | ||||||
|  | ├── store/        # 状态管理 | ||||||
|  | ├── types/        # 类型定义 | ||||||
|  | ├── utils/        # 工具函数 | ||||||
|  | └── views/        # 页面组件 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 开发规范 | ||||||
|  | 
 | ||||||
|  | 1. **代码风格**: 使用 ESLint + Prettier | ||||||
|  | 2. **类型检查**: 使用 TypeScript 严格模式 | ||||||
|  | 3. **组件命名**: 使用 PascalCase | ||||||
|  | 4. **文件命名**: 使用 kebab-case | ||||||
|  | 5. **API 调用**: 统一使用 `@/api` 中的方法 | ||||||
							
								
								
									
										86
									
								
								frontend/auto-imports.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								frontend/auto-imports.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,86 @@ | |||||||
|  | /* eslint-disable */ | ||||||
|  | /* prettier-ignore */ | ||||||
|  | // @ts-nocheck
 | ||||||
|  | // noinspection JSUnusedGlobalSymbols
 | ||||||
|  | // Generated by unplugin-auto-import
 | ||||||
|  | // biome-ignore lint: disable
 | ||||||
|  | export {} | ||||||
|  | declare global { | ||||||
|  |   const EffectScope: typeof import('vue')['EffectScope'] | ||||||
|  |   const ElNotification: typeof import('element-plus/es')['ElNotification'] | ||||||
|  |   const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] | ||||||
|  |   const computed: typeof import('vue')['computed'] | ||||||
|  |   const createApp: typeof import('vue')['createApp'] | ||||||
|  |   const createPinia: typeof import('pinia')['createPinia'] | ||||||
|  |   const customRef: typeof import('vue')['customRef'] | ||||||
|  |   const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] | ||||||
|  |   const defineComponent: typeof import('vue')['defineComponent'] | ||||||
|  |   const defineStore: typeof import('pinia')['defineStore'] | ||||||
|  |   const effectScope: typeof import('vue')['effectScope'] | ||||||
|  |   const getActivePinia: typeof import('pinia')['getActivePinia'] | ||||||
|  |   const getCurrentInstance: typeof import('vue')['getCurrentInstance'] | ||||||
|  |   const getCurrentScope: typeof import('vue')['getCurrentScope'] | ||||||
|  |   const getCurrentWatcher: typeof import('vue')['getCurrentWatcher'] | ||||||
|  |   const h: typeof import('vue')['h'] | ||||||
|  |   const inject: typeof import('vue')['inject'] | ||||||
|  |   const isProxy: typeof import('vue')['isProxy'] | ||||||
|  |   const isReactive: typeof import('vue')['isReactive'] | ||||||
|  |   const isReadonly: typeof import('vue')['isReadonly'] | ||||||
|  |   const isRef: typeof import('vue')['isRef'] | ||||||
|  |   const isShallow: typeof import('vue')['isShallow'] | ||||||
|  |   const mapActions: typeof import('pinia')['mapActions'] | ||||||
|  |   const mapGetters: typeof import('pinia')['mapGetters'] | ||||||
|  |   const mapState: typeof import('pinia')['mapState'] | ||||||
|  |   const mapStores: typeof import('pinia')['mapStores'] | ||||||
|  |   const mapWritableState: typeof import('pinia')['mapWritableState'] | ||||||
|  |   const markRaw: typeof import('vue')['markRaw'] | ||||||
|  |   const nextTick: typeof import('vue')['nextTick'] | ||||||
|  |   const onActivated: typeof import('vue')['onActivated'] | ||||||
|  |   const onBeforeMount: typeof import('vue')['onBeforeMount'] | ||||||
|  |   const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] | ||||||
|  |   const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] | ||||||
|  |   const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] | ||||||
|  |   const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] | ||||||
|  |   const onDeactivated: typeof import('vue')['onDeactivated'] | ||||||
|  |   const onErrorCaptured: typeof import('vue')['onErrorCaptured'] | ||||||
|  |   const onMounted: typeof import('vue')['onMounted'] | ||||||
|  |   const onRenderTracked: typeof import('vue')['onRenderTracked'] | ||||||
|  |   const onRenderTriggered: typeof import('vue')['onRenderTriggered'] | ||||||
|  |   const onScopeDispose: typeof import('vue')['onScopeDispose'] | ||||||
|  |   const onServerPrefetch: typeof import('vue')['onServerPrefetch'] | ||||||
|  |   const onUnmounted: typeof import('vue')['onUnmounted'] | ||||||
|  |   const onUpdated: typeof import('vue')['onUpdated'] | ||||||
|  |   const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] | ||||||
|  |   const provide: typeof import('vue')['provide'] | ||||||
|  |   const reactive: typeof import('vue')['reactive'] | ||||||
|  |   const readonly: typeof import('vue')['readonly'] | ||||||
|  |   const ref: typeof import('vue')['ref'] | ||||||
|  |   const resolveComponent: typeof import('vue')['resolveComponent'] | ||||||
|  |   const setActivePinia: typeof import('pinia')['setActivePinia'] | ||||||
|  |   const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] | ||||||
|  |   const shallowReactive: typeof import('vue')['shallowReactive'] | ||||||
|  |   const shallowReadonly: typeof import('vue')['shallowReadonly'] | ||||||
|  |   const shallowRef: typeof import('vue')['shallowRef'] | ||||||
|  |   const storeToRefs: typeof import('pinia')['storeToRefs'] | ||||||
|  |   const toRaw: typeof import('vue')['toRaw'] | ||||||
|  |   const toRef: typeof import('vue')['toRef'] | ||||||
|  |   const toRefs: typeof import('vue')['toRefs'] | ||||||
|  |   const toValue: typeof import('vue')['toValue'] | ||||||
|  |   const triggerRef: typeof import('vue')['triggerRef'] | ||||||
|  |   const unref: typeof import('vue')['unref'] | ||||||
|  |   const useAttrs: typeof import('vue')['useAttrs'] | ||||||
|  |   const useCssModule: typeof import('vue')['useCssModule'] | ||||||
|  |   const useCssVars: typeof import('vue')['useCssVars'] | ||||||
|  |   const useI18n: typeof import('vue-i18n')['useI18n'] | ||||||
|  |   const useId: typeof import('vue')['useId'] | ||||||
|  |   const useLink: typeof import('vue-router')['useLink'] | ||||||
|  |   const useModel: typeof import('vue')['useModel'] | ||||||
|  |   const useRoute: typeof import('vue-router')['useRoute'] | ||||||
|  |   const useRouter: typeof import('vue-router')['useRouter'] | ||||||
|  |   const useSlots: typeof import('vue')['useSlots'] | ||||||
|  |   const useTemplateRef: typeof import('vue')['useTemplateRef'] | ||||||
|  |   const watch: typeof import('vue')['watch'] | ||||||
|  |   const watchEffect: typeof import('vue')['watchEffect'] | ||||||
|  |   const watchPostEffect: typeof import('vue')['watchPostEffect'] | ||||||
|  |   const watchSyncEffect: typeof import('vue')['watchSyncEffect'] | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								frontend/components.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								frontend/components.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | /* eslint-disable */ | ||||||
|  | // @ts-nocheck
 | ||||||
|  | // Generated by unplugin-vue-components
 | ||||||
|  | // Read more: https://github.com/vuejs/core/pull/3399
 | ||||||
|  | // biome-ignore lint: disable
 | ||||||
|  | export {} | ||||||
|  | 
 | ||||||
|  | /* prettier-ignore */ | ||||||
|  | declare module 'vue' { | ||||||
|  |   export interface GlobalComponents { | ||||||
|  |     ElAside: typeof import('element-plus/es')['ElAside'] | ||||||
|  |     ElButton: typeof import('element-plus/es')['ElButton'] | ||||||
|  |     ElCard: typeof import('element-plus/es')['ElCard'] | ||||||
|  |     ElCol: typeof import('element-plus/es')['ElCol'] | ||||||
|  |     ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] | ||||||
|  |     ElContainer: typeof import('element-plus/es')['ElContainer'] | ||||||
|  |     ElDivider: typeof import('element-plus/es')['ElDivider'] | ||||||
|  |     ElDropdown: typeof import('element-plus/es')['ElDropdown'] | ||||||
|  |     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] | ||||||
|  |     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] | ||||||
|  |     ElForm: typeof import('element-plus/es')['ElForm'] | ||||||
|  |     ElFormItem: typeof import('element-plus/es')['ElFormItem'] | ||||||
|  |     ElHeader: typeof import('element-plus/es')['ElHeader'] | ||||||
|  |     ElIcon: typeof import('element-plus/es')['ElIcon'] | ||||||
|  |     ElInput: typeof import('element-plus/es')['ElInput'] | ||||||
|  |     ElMain: typeof import('element-plus/es')['ElMain'] | ||||||
|  |     ElMenu: typeof import('element-plus/es')['ElMenu'] | ||||||
|  |     ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] | ||||||
|  |     ElRow: typeof import('element-plus/es')['ElRow'] | ||||||
|  |     ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] | ||||||
|  |     ElStatistic: typeof import('element-plus/es')['ElStatistic'] | ||||||
|  |     ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] | ||||||
|  |     ElSwitch: typeof import('element-plus/es')['ElSwitch'] | ||||||
|  |     ElTable: typeof import('element-plus/es')['ElTable'] | ||||||
|  |     ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] | ||||||
|  |     ElTag: typeof import('element-plus/es')['ElTag'] | ||||||
|  |     RouterLink: typeof import('vue-router')['RouterLink'] | ||||||
|  |     RouterView: typeof import('vue-router')['RouterView'] | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | <!doctype html> | ||||||
|  | <html lang="en"> | ||||||
|  |   <head> | ||||||
|  |     <meta charset="UTF-8" /> | ||||||
|  |     <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|  |     <title>Vite + Vue + TS</title> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <div id="app"></div> | ||||||
|  |     <script type="module" src="/src/main.ts"></script> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										3344
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3344
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										35
									
								
								frontend/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								frontend/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | { | ||||||
|  |   "name": "frontend", | ||||||
|  |   "private": true, | ||||||
|  |   "version": "0.0.0", | ||||||
|  |   "type": "module", | ||||||
|  |   "scripts": { | ||||||
|  |     "dev": "vite", | ||||||
|  |     "build": "vue-tsc -b && vite build", | ||||||
|  |     "preview": "vite preview" | ||||||
|  |   }, | ||||||
|  |   "dependencies": { | ||||||
|  |     "@element-plus/icons-vue": "^2.3.1", | ||||||
|  |     "@vueuse/core": "^10.11.0", | ||||||
|  |     "axios": "^1.11.0", | ||||||
|  |     "mitt": "^3.0.1", | ||||||
|  |     "pinia": "^2.1.7", | ||||||
|  |     "pinia-plugin-persistedstate": "^3.2.1", | ||||||
|  |     "vue": "^3.5.18", | ||||||
|  |     "vue-router": "^4.5.1" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@types/node": "^24.3.0", | ||||||
|  |     "@vitejs/plugin-vue": "^6.0.1", | ||||||
|  |     "@vue/tsconfig": "^0.7.0", | ||||||
|  |     "element-plus": "^2.10.7", | ||||||
|  |     "sass": "^1.90.0", | ||||||
|  |     "sass-loader": "^16.0.5", | ||||||
|  |     "typescript": "~5.8.3", | ||||||
|  |     "unplugin-auto-import": "^20.0.0", | ||||||
|  |     "unplugin-icons": "^22.2.0", | ||||||
|  |     "unplugin-vue-components": "^29.0.0", | ||||||
|  |     "vite": "^7.1.2", | ||||||
|  |     "vue-tsc": "^3.0.5" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								frontend/public/vite.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/public/vite.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> | ||||||
| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										32
									
								
								frontend/src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								frontend/src/App.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useDark } from '@vueuse/core' | ||||||
|  | import { onMounted } from 'vue' | ||||||
|  | import useColorStore from '@/store/color' | ||||||
|  | import useUserStore from '@/store/modules/user' | ||||||
|  | import ENV_CONFIG from '@/config/env' | ||||||
|  | 
 | ||||||
|  | useDark() | ||||||
|  | 
 | ||||||
|  | const colorStore = useColorStore() | ||||||
|  | const userStore = useUserStore() | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  |   // 设置页面标题 | ||||||
|  |   document.title = ENV_CONFIG.APP_TITLE | ||||||
|  |   // 初始化用户状态 | ||||||
|  |   userStore.initUserState() | ||||||
|  |   // 初始化主题色 | ||||||
|  |   colorStore.primaryChange(colorStore.primary) | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <RouterView /> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | * { | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										51
									
								
								frontend/src/api/menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/src/api/menu.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | import api from './user' | ||||||
|  | 
 | ||||||
|  | // 菜单相关接口类型定义
 | ||||||
|  | export interface MenuItem { | ||||||
|  |   smid: number | ||||||
|  |   label: string | ||||||
|  |   icon_class: string | ||||||
|  |   type: number | ||||||
|  |   src: string | ||||||
|  |   sort: number | ||||||
|  |   status: number | ||||||
|  |   parent_id: number | ||||||
|  |   children?: MenuItem[] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface RoleItem { | ||||||
|  |   group_id: number | ||||||
|  |   group_name: string | ||||||
|  |   status: number | ||||||
|  |   create_time: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 获取所有菜单列表
 | ||||||
|  | export const getMenuList = () => { | ||||||
|  |   return api.get('/menu/list') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 根据用户角色获取菜单
 | ||||||
|  | export const getUserMenus = () => { | ||||||
|  |   return api.get('/menu/userMenus') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 获取角色列表
 | ||||||
|  | export const getRoleList = () => { | ||||||
|  |   return api.get('/menu/roles') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 获取菜单详情
 | ||||||
|  | export const getMenuDetail = (id: number) => { | ||||||
|  |   return api.get('/menu/detail', { params: { id } }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 临时菜单接口(使用用户控制器)
 | ||||||
|  | export const getTempUserMenus = () => { | ||||||
|  |   return api.get('/user/menus') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 或者直接使用完整路径(如果baseURL有问题)
 | ||||||
|  | export const getTempUserMenusDirect = () => { | ||||||
|  |   return api.get('https://www.yunzer.cn/api/user/menus') | ||||||
|  | }  | ||||||
							
								
								
									
										79
									
								
								frontend/src/api/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								frontend/src/api/user.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | |||||||
|  | import axios from 'axios' | ||||||
|  | import ENV_CONFIG from '@/config/env' | ||||||
|  | 
 | ||||||
|  | // 创建axios实例
 | ||||||
|  | const api = axios.create({ | ||||||
|  |   baseURL: ENV_CONFIG.API_BASE_URL, | ||||||
|  |   timeout: ENV_CONFIG.REQUEST_TIMEOUT, | ||||||
|  |   headers: { | ||||||
|  |     'Content-Type': 'application/json' | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 调试信息
 | ||||||
|  | console.log('API配置:', { | ||||||
|  |   baseURL: ENV_CONFIG.API_BASE_URL, | ||||||
|  |   timeout: ENV_CONFIG.REQUEST_TIMEOUT, | ||||||
|  |   '环境变量VITE_APP_API_BASE_URL': import.meta.env.VITE_APP_API_BASE_URL | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 请求拦截器 - 添加token
 | ||||||
|  | api.interceptors.request.use( | ||||||
|  |   (config) => { | ||||||
|  |     // 调试信息
 | ||||||
|  |     console.log('API请求:', { | ||||||
|  |       method: config.method, | ||||||
|  |       url: config.url, | ||||||
|  |       fullURL: (config.baseURL || '') + (config.url || ''), | ||||||
|  |       baseURL: config.baseURL | ||||||
|  |     }) | ||||||
|  |      | ||||||
|  |     const token = localStorage.getItem(ENV_CONFIG.TOKEN_KEY) | ||||||
|  |     if (token) { | ||||||
|  |       config.headers.Authorization = `Bearer ${token}` | ||||||
|  |     } | ||||||
|  |     return config | ||||||
|  |   }, | ||||||
|  |   (error) => { | ||||||
|  |     return Promise.reject(error) | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 响应拦截器 - 处理错误
 | ||||||
|  | api.interceptors.response.use( | ||||||
|  |   (response) => { | ||||||
|  |     // 直接返回响应数据,而不是整个response对象
 | ||||||
|  |     return response.data | ||||||
|  |   }, | ||||||
|  |   (error) => { | ||||||
|  |     if (error.response?.status === 401) { | ||||||
|  |       // token过期,清除本地存储
 | ||||||
|  |       localStorage.removeItem(ENV_CONFIG.TOKEN_KEY) | ||||||
|  |       localStorage.removeItem(ENV_CONFIG.USER_INFO_KEY) | ||||||
|  |       window.location.href = '/#/login' | ||||||
|  |     } | ||||||
|  |     return Promise.reject(error) | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 用户登录接口
 | ||||||
|  | export const login = (data: { username: string; password: string }) => { | ||||||
|  |   return api.post('/user/login', data) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 获取用户信息接口
 | ||||||
|  | export const getUserInfo = () => { | ||||||
|  |   return api.get('/user/info') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 用户登出接口
 | ||||||
|  | export const logout = () => { | ||||||
|  |   return api.post('/user/logout') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 修改密码接口
 | ||||||
|  | export const changePassword = (data: { oldPassword: string; newPassword: string }) => { | ||||||
|  |   return api.post('/user/change-password', data) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default api  | ||||||
							
								
								
									
										114
									
								
								frontend/src/assets/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								frontend/src/assets/css/style.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,114 @@ | |||||||
|  | :root { | ||||||
|  |   font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; | ||||||
|  |   line-height: 1.5; | ||||||
|  |   font-weight: 400; | ||||||
|  | 
 | ||||||
|  |   color-scheme: light dark; | ||||||
|  |   color: #213547; | ||||||
|  |   background-color: #ffffff; | ||||||
|  | 
 | ||||||
|  |   font-synthesis: none; | ||||||
|  |   text-rendering: optimizeLegibility; | ||||||
|  |   -webkit-font-smoothing: antialiased; | ||||||
|  |   -moz-osx-font-smoothing: grayscale; | ||||||
|  | 
 | ||||||
|  |   --el-color-primary: green; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | html.dark { | ||||||
|  |   color-scheme: dark; | ||||||
|  |   color: rgba(255, 255, 255, 0.87); | ||||||
|  |   background-color: #242424; | ||||||
|  |    | ||||||
|  |   --el-color-primary: green; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | a { | ||||||
|  |   font-weight: 500; | ||||||
|  |   color: #646cff; | ||||||
|  |   text-decoration: inherit; | ||||||
|  | } | ||||||
|  | a:hover { | ||||||
|  |   color: #535bf2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | body { | ||||||
|  |   margin: 0; | ||||||
|  |   display: flex; | ||||||
|  |   place-items: center; | ||||||
|  |   min-width: 320px; | ||||||
|  |   min-height: 100vh; | ||||||
|  |   background-color: #ffffff; | ||||||
|  |   color: #213547; | ||||||
|  |   transition: background-color 0.3s, color 0.3s; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | html.dark body { | ||||||
|  |   background-color: #242424; | ||||||
|  |   color: rgba(255, 255, 255, 0.87); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | h1 { | ||||||
|  |   font-size: 3.2em; | ||||||
|  |   line-height: 1.1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | button { | ||||||
|  |   border-radius: 8px; | ||||||
|  |   border: 1px solid transparent; | ||||||
|  |   padding: 0.6em 1.2em; | ||||||
|  |   font-size: 1em; | ||||||
|  |   font-weight: 500; | ||||||
|  |   font-family: inherit; | ||||||
|  |   background-color: #1a1a1a; | ||||||
|  |   cursor: pointer; | ||||||
|  |   transition: border-color 0.25s; | ||||||
|  | } | ||||||
|  | button:hover { | ||||||
|  |   border-color: #646cff; | ||||||
|  | } | ||||||
|  | button:focus, | ||||||
|  | button:focus-visible { | ||||||
|  |   outline: 4px auto -webkit-focus-ring-color; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .card { | ||||||
|  |   padding: 2em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #app { | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
|  |   width: 100vw; | ||||||
|  |   height: 100vh; | ||||||
|  |   background-color: #ffffff; | ||||||
|  |   color: #213547; | ||||||
|  |   transition: background-color 0.3s, color 0.3s; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | html.dark #app { | ||||||
|  |   background-color: #242424; | ||||||
|  |   color: rgba(255, 255, 255, 0.87); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media (prefers-color-scheme: light) { | ||||||
|  |   :root { | ||||||
|  |     color: #213547; | ||||||
|  |     background-color: #ffffff; | ||||||
|  |   } | ||||||
|  |   a:hover { | ||||||
|  |     color: #747bff; | ||||||
|  |   } | ||||||
|  |   button { | ||||||
|  |     background-color: #f9f9f9; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | html, | ||||||
|  | body, | ||||||
|  | body > #app, | ||||||
|  | #app > .el-container { | ||||||
|  |   padding: 0px; | ||||||
|  |   margin: 0px; | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								frontend/src/assets/images/background.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/assets/images/background.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 251 KiB | 
							
								
								
									
										
											BIN
										
									
								
								frontend/src/assets/images/background.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/assets/images/background.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.9 MiB | 
							
								
								
									
										
											BIN
										
									
								
								frontend/src/assets/images/login_form.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/assets/images/login_form.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 359 KiB | 
							
								
								
									
										22
									
								
								frontend/src/config/env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/src/config/env.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | // src/config/env.ts
 | ||||||
|  | 
 | ||||||
|  | const ENV_CONFIG = { | ||||||
|  |   // API配置
 | ||||||
|  |   API_BASE_URL: import.meta.env.VITE_APP_API_BASE_URL || 'https://www.yunzer.cn/api', | ||||||
|  |   REQUEST_TIMEOUT: 10000, | ||||||
|  | 
 | ||||||
|  |   // 应用配置
 | ||||||
|  |   APP_TITLE: import.meta.env.VITE_APP_TITLE || '项目管理系统', | ||||||
|  |    | ||||||
|  |   // 本地存储key
 | ||||||
|  |   TOKEN_KEY: 'token', | ||||||
|  |   USER_INFO_KEY: 'userInfo', | ||||||
|  | 
 | ||||||
|  |   // 是否为开发环境
 | ||||||
|  |   IS_DEV: import.meta.env.DEV, | ||||||
|  | 
 | ||||||
|  |   // 是否为生产环境
 | ||||||
|  |   IS_PROD: import.meta.env.PROD | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default ENV_CONFIG; | ||||||
							
								
								
									
										39
									
								
								frontend/src/data/menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								frontend/src/data/menu.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | const menus: IMenu[] = [ | ||||||
|  |     { | ||||||
|  |         name: 'Home', | ||||||
|  |         desc: '首页', | ||||||
|  |         icon: 'House', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         name: 'sys', | ||||||
|  |         desc: '系统', | ||||||
|  |         children: [ | ||||||
|  |             { | ||||||
|  |                 name: 'AdmUserPassword', | ||||||
|  |                 desc: '密码更新', | ||||||
|  |                 key: [1, 11] | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 name: 'AdmUser', | ||||||
|  |                 desc: '管理员', | ||||||
|  |                 key: [1] | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |         icon: 'User', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         name: 'log', | ||||||
|  |         desc: '日志', | ||||||
|  |         children: [ | ||||||
|  |             { | ||||||
|  |                 name: 'AdmUserLogin', | ||||||
|  |                 desc: '管理员登录', | ||||||
|  |                 key: [1], | ||||||
|  |             }, | ||||||
|  |         ], | ||||||
|  |         icon: 'Notification', | ||||||
|  |     }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export default menus | ||||||
							
								
								
									
										28
									
								
								frontend/src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/src/main.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | import { createApp } from 'vue' | ||||||
|  | import '@/assets/css/style.css' | ||||||
|  | import App from './App.vue' | ||||||
|  | import router from "./router"; | ||||||
|  | import ElementPlus from 'element-plus' | ||||||
|  | import 'element-plus/dist/index.css' | ||||||
|  | import 'element-plus/theme-chalk/dark/css-vars.css' | ||||||
|  | import { createPinia } from 'pinia' | ||||||
|  | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' | ||||||
|  | import * as ElementPlusIconsVue from '@element-plus/icons-vue' | ||||||
|  | import useUserStore from '@/store/modules/user' | ||||||
|  | 
 | ||||||
|  | const app = createApp(App) | ||||||
|  | 
 | ||||||
|  | const pinia = createPinia() | ||||||
|  | pinia.use(piniaPluginPersistedstate) | ||||||
|  | 
 | ||||||
|  | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { | ||||||
|  |     app.component(key, component) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | app.use(pinia).use(ElementPlus).use(router) | ||||||
|  | 
 | ||||||
|  | // 初始化用户状态
 | ||||||
|  | const userStore = useUserStore() | ||||||
|  | userStore.initUserState() | ||||||
|  | 
 | ||||||
|  | app.mount('#app') | ||||||
							
								
								
									
										95
									
								
								frontend/src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								frontend/src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | |||||||
|  | import { RouteRecordRaw, createRouter, createWebHashHistory } from 'vue-router' | ||||||
|  | 
 | ||||||
|  | import * as MenuUtil from '@/util/menu' | ||||||
|  | import useFastnavStore from '@/store/fastnav' | ||||||
|  | import useUserStore from '@/store/modules/user' | ||||||
|  | 
 | ||||||
|  | // 只保留非菜单相关的静态路由
 | ||||||
|  | const routes: RouteRecordRaw[] = [ | ||||||
|  |     { | ||||||
|  |         path: '/login', | ||||||
|  |         name: 'Login', | ||||||
|  |         component: () => import('@/views/login/index.vue'), | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         path: '/', | ||||||
|  |         name: 'Main', | ||||||
|  |         component: () => import('@/views/main/index.vue'), | ||||||
|  |         children: [], // 预定义空的子路由数组
 | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         path: '/:catchAll(.*)', | ||||||
|  |         component: () => import('@/views/error/404.vue'), | ||||||
|  |     }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | const router = createRouter({ | ||||||
|  |     routes, | ||||||
|  |     history: createWebHashHistory() | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | let isAddRoute = false | ||||||
|  | let isUserStateInitialized = false | ||||||
|  | 
 | ||||||
|  | router.beforeEach(async (to, _from, next) => { | ||||||
|  |     const userStore = useUserStore() | ||||||
|  | 
 | ||||||
|  |     // 等待用户状态初始化完成
 | ||||||
|  |     if (!isUserStateInitialized) { | ||||||
|  |         await userStore.initUserState() | ||||||
|  |         isUserStateInitialized = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 如果已登录且访问登录页,跳转到首页
 | ||||||
|  |     if (to.path == '/login' && userStore.isLogin) { | ||||||
|  |         next({ path: '/' }) | ||||||
|  |         return | ||||||
|  |     }  | ||||||
|  |     // 如果未登录且访问非登录页,跳转到登录页
 | ||||||
|  |     else if (to.path != '/login' && !userStore.isLogin) { | ||||||
|  |         next({ path: '/login' }) | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 未登录直接放行(此时只可能是去登录页)
 | ||||||
|  |     if (!userStore.isLogin) { | ||||||
|  |         next() | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 已经添加过动态路由
 | ||||||
|  |     if (isAddRoute) { | ||||||
|  |         if (to.meta.desc) { | ||||||
|  |             const fastnavStore = useFastnavStore() | ||||||
|  |             fastnavStore.addData(to.meta.desc as string, to.path) | ||||||
|  |         } | ||||||
|  |         next() | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 添加主页子路由
 | ||||||
|  |     isAddRoute = true | ||||||
|  | 
 | ||||||
|  |     // 找到主路由
 | ||||||
|  |     const mainRoute = router.options.routes.find((v) => v.path == '/')! | ||||||
|  | 
 | ||||||
|  |     // 根据用户类型生成菜单和路由(默认使用管理员类型)
 | ||||||
|  |     const [_genMenus, genRoutes] = MenuUtil.gen(1) | ||||||
|  | 
 | ||||||
|  |     // 设置主路由的重定向和子路由
 | ||||||
|  |     if (genRoutes.length > 0) { | ||||||
|  |         mainRoute.redirect = genRoutes[0].path | ||||||
|  |         mainRoute.children = genRoutes | ||||||
|  |          | ||||||
|  |         // 添加主路由(包含子路由)
 | ||||||
|  |         router.addRoute(mainRoute) | ||||||
|  |          | ||||||
|  |         // 重新导航到目标路由
 | ||||||
|  |         next({ ...to, replace: true }) | ||||||
|  |     } else { | ||||||
|  |         // 如果没有生成路由,至少重定向到登录页
 | ||||||
|  |         next({ path: '/login' }) | ||||||
|  |     } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | export default router | ||||||
							
								
								
									
										29
									
								
								frontend/src/store/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/store/auth.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | import { defineStore } from 'pinia' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | const useAuthStore = defineStore('appauth', { | ||||||
|  |     state: () => { | ||||||
|  |         return { | ||||||
|  |             isLogin: false, | ||||||
|  |             _userInfo: null, | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     persist: true, | ||||||
|  | 
 | ||||||
|  |     getters: { | ||||||
|  |         userInfo(): IUser | null { | ||||||
|  |             return this._userInfo | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     actions: { | ||||||
|  |         login(data: any) { | ||||||
|  |             this.isLogin = true | ||||||
|  |             this._userInfo = data | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export default useAuthStore | ||||||
							
								
								
									
										115
									
								
								frontend/src/store/color.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								frontend/src/store/color.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | |||||||
|  | import { defineStore } from 'pinia' | ||||||
|  | 
 | ||||||
|  | const color2rgb = (color: string) => { | ||||||
|  |     return color.startsWith('#') ? hex2rgb(color) : rgb2rgb(color) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // rgb(255, 0, 0) | rgba(255, 0, 0) => [255, 0, 0]
 | ||||||
|  | const rgb2rgb = (color: string) => { | ||||||
|  |     const colors = color.split('(')[1].split(')')[0].split(',') | ||||||
|  |     return colors.slice(0, 3).map(item => parseInt(item.trim())) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // #FF0000 => [255, 0, 0]
 | ||||||
|  | const hex2rgb = (color: string) => { | ||||||
|  |     color = color.replace('#', '') | ||||||
|  |     const matchs = color.match(/../g) | ||||||
|  | 
 | ||||||
|  |     const rgbs: number[] = [] | ||||||
|  |     for (let i = 0; i < matchs!.length; i++) { | ||||||
|  |         rgbs[i] = parseInt(matchs![i], 16) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return rgbs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const rgb2hex = (r: number, g: number, b: number) => { | ||||||
|  |     const hexs = [r.toString(16), g.toString(16), b.toString(16)] | ||||||
|  | 
 | ||||||
|  |     for (let i = 0; i < hexs.length; i++) { | ||||||
|  |         if (hexs[i].length === 1) { | ||||||
|  |             hexs[i] = '0' + hexs[i] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return '#' + hexs.join('') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 颜色变亮
 | ||||||
|  | const lighten = (color: string, level: number) => { | ||||||
|  |     const rgbs = color2rgb(color) | ||||||
|  | 
 | ||||||
|  |     for (let i = 0; i < rgbs.length; i++) { | ||||||
|  |         rgbs[i] = Math.floor((255 - rgbs[i]) * level + rgbs[i]) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return rgb2hex(rgbs[0], rgbs[1], rgbs[2]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 颜色变暗
 | ||||||
|  | const darken = (color: string, level: number) => { | ||||||
|  |     const rgbs = color2rgb(color) | ||||||
|  | 
 | ||||||
|  |     for (let i = 0; i < rgbs.length; i++) { | ||||||
|  |         rgbs[i] = Math.floor(rgbs[i] * (1 - level)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return rgb2hex(rgbs[0], rgbs[1], rgbs[2]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const useColorStore = defineStore('appcolor', { | ||||||
|  |     state: () => { | ||||||
|  |         return { | ||||||
|  |             primary: '#409EFF', | ||||||
|  |             primaryPredefines: [ | ||||||
|  |                 '#409EFF', | ||||||
|  |                 '#67C23A', | ||||||
|  |                 '#E6A23C', | ||||||
|  |                 '#F56C6C', | ||||||
|  |                 '#909399', | ||||||
|  |                 '#FF6B6B', | ||||||
|  |                 '#4ECDC4', | ||||||
|  |                 '#45B7D1', | ||||||
|  |                 '#96CEB4', | ||||||
|  |                 '#FFEAA7', | ||||||
|  |                 '#DDA0DD', | ||||||
|  |                 '#98D8C8' | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     actions: { | ||||||
|  |         primaryChange(color: string | null) { | ||||||
|  |             if (!color) return | ||||||
|  |              | ||||||
|  |             // 设置 CSS 变量
 | ||||||
|  |             document.documentElement.style.setProperty('--el-color-primary', color) | ||||||
|  |              | ||||||
|  |             // 生成不同深浅的主题色
 | ||||||
|  |             const colors = { | ||||||
|  |                 'primary': color, | ||||||
|  |                 'primary-light-3': lighten(color, 0.3), | ||||||
|  |                 'primary-light-5': lighten(color, 0.5), | ||||||
|  |                 'primary-light-7': lighten(color, 0.7), | ||||||
|  |                 'primary-light-8': lighten(color, 0.8), | ||||||
|  |                 'primary-light-9': lighten(color, 0.9), | ||||||
|  |                 'primary-dark-2': darken(color, 0.2) | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 设置所有主题色变量
 | ||||||
|  |             Object.entries(colors).forEach(([key, value]) => { | ||||||
|  |                 document.documentElement.style.setProperty(`--el-color-${key}`, value) | ||||||
|  |             }) | ||||||
|  |         }, | ||||||
|  | 
 | ||||||
|  |         primarySave(color: string | null) { | ||||||
|  |             if (!color) return | ||||||
|  |             this.primary = color | ||||||
|  |             this.primaryChange(color) | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     persist: true, | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | export default useColorStore | ||||||
							
								
								
									
										108
									
								
								frontend/src/store/fastnav.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								frontend/src/store/fastnav.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | |||||||
|  | import { defineStore } from 'pinia' | ||||||
|  | 
 | ||||||
|  | const idMap: Map<string, number> = new Map() | ||||||
|  | 
 | ||||||
|  | const useFastnavStore = defineStore('appfastnav', { | ||||||
|  |     state: () => { | ||||||
|  |         const datas: IFastnavItem[] = [] | ||||||
|  |         const currPath: string = '' | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             datas, | ||||||
|  |             currPath, | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |      | ||||||
|  |     persist: true, | ||||||
|  | 
 | ||||||
|  |     actions: { | ||||||
|  |         addData(desc: string, path: string) { | ||||||
|  |             const data = this.datas.find(item => item.path == path) | ||||||
|  |             if (data) { | ||||||
|  |                 this.currPath = path | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             this.datas.push({ desc, path }) | ||||||
|  | 
 | ||||||
|  |             this.currPath = path | ||||||
|  |         }, | ||||||
|  |         removeData(path: string): string { | ||||||
|  |             if (this.datas.length <= 1) return '' | ||||||
|  | 
 | ||||||
|  |             for (let i = 0; i < this.datas.length; i++) { | ||||||
|  |                 const item = this.datas[i] | ||||||
|  |                 if (item.path != path) continue | ||||||
|  | 
 | ||||||
|  |                 // 修改:删除数据
 | ||||||
|  |                 this.datas.splice(i, 1) | ||||||
|  | 
 | ||||||
|  |                 if (item.path != this.currPath) return '' | ||||||
|  | 
 | ||||||
|  |                 return i == 0 ? this.datas[0].path : this.datas[i - 1].path | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return '' | ||||||
|  |         }, | ||||||
|  |         removeOther(path: string): string { | ||||||
|  |             const data = this.datas.find(item => item.path == path) | ||||||
|  |             if (!data) return '' | ||||||
|  | 
 | ||||||
|  |             this.idAddAll(this.datas, path) | ||||||
|  |             this.datas = [data] | ||||||
|  | 
 | ||||||
|  |             return path == this.currPath ? '' : path | ||||||
|  |         }, | ||||||
|  |         removeLeft(path: string): string { | ||||||
|  |             for (let i = 0; i < this.datas.length; i++) { | ||||||
|  |                 const data = this.datas[i] | ||||||
|  |                 if (data.path != path) continue | ||||||
|  | 
 | ||||||
|  |                 const removes = this.datas.splice(0, i) | ||||||
|  |                 this.idAddAll(removes) | ||||||
|  | 
 | ||||||
|  |                 return path == this.currPath ? '' : path | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return '' | ||||||
|  |         }, | ||||||
|  |         removeRight(path: string): string { | ||||||
|  |             for (let i = 0; i < this.datas.length; i++) { | ||||||
|  |                 const data = this.datas[i] | ||||||
|  |                 if (data.path != path) continue | ||||||
|  | 
 | ||||||
|  |                 const removes = this.datas.splice(i + 1) | ||||||
|  |                 this.idAddAll(removes) | ||||||
|  | 
 | ||||||
|  |                 return path == this.currPath ? '' : path | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return '' | ||||||
|  |         }, | ||||||
|  |         idGet(path: string): string { | ||||||
|  |             const id = idMap.get(path) ?? 1 | ||||||
|  |             return path + id | ||||||
|  |         }, | ||||||
|  |         idAdd(path: string): number { | ||||||
|  |             // 自增id并返回
 | ||||||
|  |             const id = (idMap.get(path) ?? 1) + 1 | ||||||
|  |             idMap.set(path, id) | ||||||
|  |             return id | ||||||
|  |         }, | ||||||
|  |         idAddAll(datas: IFastnavItem[], excludePath?: string) { | ||||||
|  |             datas.forEach(item => { | ||||||
|  |                 if (item.path != excludePath) { | ||||||
|  |                     this.idAdd(item.path) | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         }, | ||||||
|  |         isFirst(path: string): boolean { | ||||||
|  |             return this.datas.length > 0 && this.datas[0].path == path | ||||||
|  |         }, | ||||||
|  |         isLast(path: string): boolean { | ||||||
|  |             return this.datas.length > 0 && this.datas[this.datas.length - 1].path == path | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | export default useFastnavStore | ||||||
							
								
								
									
										6
									
								
								frontend/src/store/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/store/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | //仓库大仓库
 | ||||||
|  | import { createPinia } from 'pinia' | ||||||
|  | //创建大仓库
 | ||||||
|  | const pinia = createPinia() | ||||||
|  | //对外暴露:入口文件需要安装仓库
 | ||||||
|  | export default pinia | ||||||
							
								
								
									
										19
									
								
								frontend/src/store/menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/store/menu.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | import { defineStore } from 'pinia' | ||||||
|  | 
 | ||||||
|  | const useMenuStore = defineStore('appmenu', { | ||||||
|  |     state: () => { | ||||||
|  |         return { | ||||||
|  |             collapse: false, | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     getters: { | ||||||
|  |         width(): string { | ||||||
|  |             return this.collapse ? '64px' : '200px' | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     persist: true, | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | export default useMenuStore | ||||||
							
								
								
									
										91
									
								
								frontend/src/store/modules/menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								frontend/src/store/modules/menu.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | |||||||
|  | import { defineStore } from 'pinia' | ||||||
|  | import { getUserMenus, getMenuList, getTempUserMenus, type MenuItem } from '@/api/menu' | ||||||
|  | import ENV_CONFIG from '@/config/env' | ||||||
|  | 
 | ||||||
|  | interface MenuState { | ||||||
|  |   menus: MenuItem[] | ||||||
|  |   loading: boolean | ||||||
|  |   error: string | null | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface ApiResponse<T> { | ||||||
|  |   code: number | ||||||
|  |   message: string | ||||||
|  |   data: T | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const useMenuStore = defineStore('menu', { | ||||||
|  |   state: (): MenuState => ({ | ||||||
|  |     menus: [], | ||||||
|  |     loading: false, | ||||||
|  |     error: null | ||||||
|  |   }), | ||||||
|  | 
 | ||||||
|  |   getters: { | ||||||
|  |     // 获取菜单列表
 | ||||||
|  |     getMenus: (state) => state.menus, | ||||||
|  |      | ||||||
|  |     // 获取加载状态
 | ||||||
|  |     getLoading: (state) => state.loading, | ||||||
|  |      | ||||||
|  |     // 获取错误信息
 | ||||||
|  |     getError: (state) => state.error | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   actions: { | ||||||
|  |     // 获取用户菜单(使用临时接口)
 | ||||||
|  |     async fetchUserMenus() { | ||||||
|  |       this.loading = true | ||||||
|  |       this.error = null | ||||||
|  |        | ||||||
|  |       try { | ||||||
|  |         const response = await getTempUserMenus() as unknown as ApiResponse<MenuItem[]> | ||||||
|  |          | ||||||
|  |         if (response.code === 200 && response.data) { | ||||||
|  |           this.menus = response.data | ||||||
|  |         } else { | ||||||
|  |           throw new Error(response.message || '获取菜单失败') | ||||||
|  |         } | ||||||
|  |       } catch (error: any) { | ||||||
|  |         this.error = error.message || '获取菜单失败' | ||||||
|  |         console.error('获取用户菜单失败:', error) | ||||||
|  |       } finally { | ||||||
|  |         this.loading = false | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     // 获取所有菜单(管理员用)
 | ||||||
|  |     async fetchAllMenus() { | ||||||
|  |       this.loading = true | ||||||
|  |       this.error = null | ||||||
|  |        | ||||||
|  |       try { | ||||||
|  |         const response = await getMenuList() as unknown as ApiResponse<MenuItem[]> | ||||||
|  |          | ||||||
|  |         if (response.code === 200 && response.data) { | ||||||
|  |           this.menus = response.data | ||||||
|  |         } else { | ||||||
|  |           throw new Error(response.message || '获取菜单失败') | ||||||
|  |         } | ||||||
|  |       } catch (error: any) { | ||||||
|  |         this.error = error.message || '获取菜单失败' | ||||||
|  |         console.error('获取所有菜单失败:', error) | ||||||
|  |       } finally { | ||||||
|  |         this.loading = false | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     // 清除菜单数据
 | ||||||
|  |     clearMenus() { | ||||||
|  |       this.menus = [] | ||||||
|  |       this.error = null | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     // 设置错误信息
 | ||||||
|  |     setError(error: string) { | ||||||
|  |       this.error = error | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | export default useMenuStore  | ||||||
							
								
								
									
										146
									
								
								frontend/src/store/modules/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								frontend/src/store/modules/user.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,146 @@ | |||||||
|  | import { defineStore } from 'pinia' | ||||||
|  | import { login as loginApi, getUserInfo as getUserInfoApi, logout as logoutApi } from '@/api/user' | ||||||
|  | import ENV_CONFIG from '@/config/env' | ||||||
|  | 
 | ||||||
|  | interface LoginForm { | ||||||
|  |   username: string | ||||||
|  |   password: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface UserInfo { | ||||||
|  |   id: number | ||||||
|  |   username: string | ||||||
|  |   name: string | ||||||
|  |   avatar?: string | ||||||
|  |   role: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface ApiResponse<T> { | ||||||
|  |   code: number | ||||||
|  |   message: string | ||||||
|  |   data: T | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface LoginResponse { | ||||||
|  |   token: string | ||||||
|  |   userInfo: UserInfo | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const useUserStore = defineStore('user', { | ||||||
|  |   state: () => { | ||||||
|  |     return { | ||||||
|  |       token: '', | ||||||
|  |       userInfo: null as UserInfo | null, | ||||||
|  |       isLogin: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   getters: { | ||||||
|  |     // 获取用户信息
 | ||||||
|  |     getUserInfo: (state) => state.userInfo, | ||||||
|  |     // 获取token
 | ||||||
|  |     getToken: (state) => state.token, | ||||||
|  |     // 是否已登录
 | ||||||
|  |     getIsLogin: (state) => state.isLogin | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   actions: { | ||||||
|  |     // 用户登录
 | ||||||
|  |     async userLogin(loginForm: LoginForm) { | ||||||
|  |       try { | ||||||
|  |         const response = await loginApi(loginForm) as unknown as ApiResponse<LoginResponse> | ||||||
|  |          | ||||||
|  |         // 假设API返回格式为 { code: 200, data: { token: string, userInfo: UserInfo } }
 | ||||||
|  |         if (response.code === 200 && response.data) { | ||||||
|  |           const { token, userInfo } = response.data | ||||||
|  |            | ||||||
|  |           // 保存登录状态
 | ||||||
|  |           this.token = token | ||||||
|  |           this.userInfo = userInfo | ||||||
|  |           this.isLogin = true | ||||||
|  |            | ||||||
|  |           // 保存到 localStorage
 | ||||||
|  |           localStorage.setItem(ENV_CONFIG.TOKEN_KEY, token) | ||||||
|  |           localStorage.setItem(ENV_CONFIG.USER_INFO_KEY, JSON.stringify(userInfo)) | ||||||
|  |            | ||||||
|  |           return userInfo | ||||||
|  |         } else { | ||||||
|  |           throw new Error(response.message || '登录失败') | ||||||
|  |         } | ||||||
|  |       } catch (error: any) { | ||||||
|  |         // 处理不同类型的错误
 | ||||||
|  |         if (error.response?.data?.message) { | ||||||
|  |           throw new Error(error.response.data.message) | ||||||
|  |         } else if (error.message) { | ||||||
|  |           throw new Error(error.message) | ||||||
|  |         } else { | ||||||
|  |           throw new Error('登录失败,请稍后重试') | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     // 用户登出
 | ||||||
|  |     async userLogout() { | ||||||
|  |       try { | ||||||
|  |         await logoutApi() | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('登出API调用失败:', error) | ||||||
|  |       } finally { | ||||||
|  |         this.token = '' | ||||||
|  |         this.userInfo = null | ||||||
|  |         this.isLogin = false | ||||||
|  |          | ||||||
|  |         // 清除 localStorage
 | ||||||
|  |         localStorage.removeItem(ENV_CONFIG.TOKEN_KEY) | ||||||
|  |         localStorage.removeItem(ENV_CONFIG.USER_INFO_KEY) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     // 初始化用户状态(从 localStorage 恢复)
 | ||||||
|  |     async initUserState() { | ||||||
|  |       const token = localStorage.getItem(ENV_CONFIG.TOKEN_KEY) | ||||||
|  |       const userInfoStr = localStorage.getItem(ENV_CONFIG.USER_INFO_KEY) | ||||||
|  |        | ||||||
|  |       if (token && userInfoStr) { | ||||||
|  |         try { | ||||||
|  |           // 验证token是否有效
 | ||||||
|  |           const response = await getUserInfoApi() as unknown as ApiResponse<UserInfo> | ||||||
|  |           if (response.code === 200 && response.data) { | ||||||
|  |             this.token = token | ||||||
|  |             this.userInfo = response.data | ||||||
|  |             this.isLogin = true | ||||||
|  |           } else { | ||||||
|  |             // token无效,清除本地存储但不调用logout API
 | ||||||
|  |             this.clearUserState() | ||||||
|  |           } | ||||||
|  |         } catch (error: any) { | ||||||
|  |           console.error('获取用户信息失败:', error) | ||||||
|  |           // 网络错误时,不清除本地状态,保持用户登录状态
 | ||||||
|  |           // 只有在明确知道token无效时才清除
 | ||||||
|  |           if (error.response?.status === 401) { | ||||||
|  |             this.clearUserState() | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     // 清除用户状态(不调用API)
 | ||||||
|  |     clearUserState() { | ||||||
|  |       this.token = '' | ||||||
|  |       this.userInfo = null | ||||||
|  |       this.isLogin = false | ||||||
|  |        | ||||||
|  |       // 清除 localStorage
 | ||||||
|  |       localStorage.removeItem(ENV_CONFIG.TOKEN_KEY) | ||||||
|  |       localStorage.removeItem(ENV_CONFIG.USER_INFO_KEY) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   persist: { | ||||||
|  |     key: 'user-store', | ||||||
|  |     storage: localStorage, | ||||||
|  |     paths: ['token', 'userInfo', 'isLogin'] | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | export default useUserStore  | ||||||
							
								
								
									
										20
									
								
								frontend/src/types/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/types/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | interface IMenu { | ||||||
|  |     name: string | ||||||
|  |     desc: string | ||||||
|  | 
 | ||||||
|  |     key?: number[] //菜单权限(1:管理员|11:游客),没有配置或者为空数组则所有角色都有权限
 | ||||||
|  |     route?: string //路由
 | ||||||
|  |     children?: IMenu[] //子菜单
 | ||||||
|  |     redirect?: string //如果有子菜单,要重定向到第一个子菜单
 | ||||||
|  |     icon?: string //菜单图标(一级菜单才有)
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface IFastnavItem { | ||||||
|  |     desc: string | ||||||
|  |     path: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface IUser { | ||||||
|  |     account: string | ||||||
|  |     type: number | ||||||
|  | } | ||||||
							
								
								
									
										123
									
								
								frontend/src/util/menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								frontend/src/util/menu.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | |||||||
|  | import menus from "@/data/menu" | ||||||
|  | import { RouteRecordRaw } from "vue-router" | ||||||
|  | 
 | ||||||
|  | // 使用相对路径,vite 的 import.meta.glob 会返回 /src/views/... 格式的key
 | ||||||
|  | const views = import.meta.glob('@/views/**/*.vue') | ||||||
|  | 
 | ||||||
|  | // 创建一个映射,将 @/views/... 格式的路径映射到实际的组件
 | ||||||
|  | const viewsMap = new Map() | ||||||
|  | for (const [key, component] of Object.entries(views)) { | ||||||
|  |     // 将 /src/views/... 转换为 @/views/... 格式作为key
 | ||||||
|  |     const mappedKey = key.replace('/src/views/', '@/views/') | ||||||
|  |     viewsMap.set(mappedKey, component) | ||||||
|  |     // 同时保留原始key
 | ||||||
|  |     viewsMap.set(key, component) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 生成菜单和路由
 | ||||||
|  | export const gen = (userType: number): [IMenu[], RouteRecordRaw[]] => { | ||||||
|  |     return gen2(userType, menus, '', [], []) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const gen2 = ( | ||||||
|  |     userType: number, | ||||||
|  |     menus: IMenu[], | ||||||
|  |     parentPath: string, | ||||||
|  |     genMenus: IMenu[], | ||||||
|  |     genRoutes: RouteRecordRaw[] | ||||||
|  | ): [IMenu[], RouteRecordRaw[]] => { | ||||||
|  |     for (let i = 0; i < menus.length; i++) { | ||||||
|  |         const menuTmp = menus[i] | ||||||
|  | 
 | ||||||
|  |         // 权限校验
 | ||||||
|  |         if (menuTmp.key && menuTmp.key.length > 0 && menuTmp.key.indexOf(userType) < 0) continue | ||||||
|  | 
 | ||||||
|  |         const menu = { ...menuTmp } | ||||||
|  |         // 处理路由拼接,避免多余斜杠
 | ||||||
|  |         if (parentPath) { | ||||||
|  |             // 如果父路径以斜杠结尾,直接拼接
 | ||||||
|  |             if (parentPath.endsWith('/')) { | ||||||
|  |                 menu.route = `${parentPath}${menu.name}` | ||||||
|  |             } else { | ||||||
|  |                 menu.route = `${parentPath}/${menu.name}` | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             menu.route = `/${menu.name}` | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (menu.children) { | ||||||
|  |             const [genMenusChild, genRoutesChild] = gen2(userType, menu.children, menu.route, [], []) | ||||||
|  |             if (genMenusChild.length > 0) { | ||||||
|  |                 menu.children = genMenusChild | ||||||
|  | 
 | ||||||
|  |                 genMenus.push(menu) | ||||||
|  |                 genRoutes.push({ | ||||||
|  |                     path: menu.route, | ||||||
|  |                     name: menu.name, | ||||||
|  |                     redirect: genMenusChild[0].route, | ||||||
|  |                     children: genRoutesChild, | ||||||
|  |                     meta: { | ||||||
|  |                         desc: menu.desc | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             genMenus.push(menu) | ||||||
|  | 
 | ||||||
|  |             // 智能组件路径匹配
 | ||||||
|  |             const component = findComponent(menu.name, menu.route) | ||||||
|  |             if (component) { | ||||||
|  |                 genRoutes.push({ | ||||||
|  |                     path: menu.route, | ||||||
|  |                     name: menu.name, | ||||||
|  |                     component: component, | ||||||
|  |                     meta: { | ||||||
|  |                         desc: menu.desc | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return [genMenus, genRoutes] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 智能查找组件文件
 | ||||||
|  | function findComponent(componentName: string, routePath: string): any { | ||||||
|  |     // 处理 routePath,去除开头的斜杠
 | ||||||
|  |     let cleanPath = routePath.startsWith('/') ? routePath.slice(1) : routePath | ||||||
|  | 
 | ||||||
|  |     // 只保留第一个斜杠后的路径(去掉多余的斜杠)
 | ||||||
|  |     cleanPath = cleanPath.replace(/\/+/g, '/') | ||||||
|  | 
 | ||||||
|  |     // 可能的路径组合
 | ||||||
|  |     const possiblePaths = [ | ||||||
|  |         // 1. 直接匹配:/src/views/路径.vue
 | ||||||
|  |         `/src/views/${cleanPath}.vue`, | ||||||
|  |         // 2. index.vue 结尾:/src/views/路径/index.vue
 | ||||||
|  |         `/src/views/${cleanPath}/index.vue`, | ||||||
|  |         // 3. 小写路径匹配
 | ||||||
|  |         `/src/views/${cleanPath.toLowerCase()}.vue`, | ||||||
|  |         `/src/views/${cleanPath.toLowerCase()}/index.vue`, | ||||||
|  |     ].filter(Boolean) | ||||||
|  | 
 | ||||||
|  |     // 精确匹配 - 使用映射查找
 | ||||||
|  |     for (const path of possiblePaths) { | ||||||
|  |         if (viewsMap.get(path)) { | ||||||
|  |             return viewsMap.get(path) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 模糊匹配 - 改进逻辑
 | ||||||
|  |     const searchTerms = [componentName.toLowerCase()] | ||||||
|  |      | ||||||
|  |     // 优先匹配更精确的路径
 | ||||||
|  |     for (const [path, component] of Object.entries(views)) { | ||||||
|  |         // 检查路径是否包含组件名(不区分大小写)
 | ||||||
|  |         if (path.toLowerCase().includes(componentName.toLowerCase())) { | ||||||
|  |             return component | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return null | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								frontend/src/views/error/404.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/views/error/404.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <template> | ||||||
|  |     404 page not found | ||||||
|  | </template> | ||||||
							
								
								
									
										54
									
								
								frontend/src/views/home/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								frontend/src/views/home/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | <template> | ||||||
|  |   <el-row :gutter="20"> | ||||||
|  |     <el-col :span="24"> | ||||||
|  |       <el-card shadow="hover"> | ||||||
|  |         <template #header> | ||||||
|  |           <div class="card-header"> | ||||||
|  |             <span>工作台</span> | ||||||
|  |           </div> | ||||||
|  |         </template> | ||||||
|  |         <el-row :gutter="20"> | ||||||
|  |           <el-col :span="6"> | ||||||
|  |             <el-statistic title="待办事项" :value="12" /> | ||||||
|  |           </el-col> | ||||||
|  |           <el-col :span="6"> | ||||||
|  |             <el-statistic title="进行中项目" :value="5" /> | ||||||
|  |           </el-col> | ||||||
|  |           <el-col :span="6"> | ||||||
|  |             <el-statistic title="消息通知" :value="3" /> | ||||||
|  |           </el-col> | ||||||
|  |           <el-col :span="6"> | ||||||
|  |             <el-statistic title="已完成任务" :value="27" /> | ||||||
|  |           </el-col> | ||||||
|  |         </el-row> | ||||||
|  |         <el-divider /> | ||||||
|  |         <el-table :data="tableData" style="width: 100%"> | ||||||
|  |           <el-table-column prop="name" label="任务名称" /> | ||||||
|  |           <el-table-column prop="status" label="状态" /> | ||||||
|  |           <el-table-column prop="date" label="截止日期" /> | ||||||
|  |         </el-table> | ||||||
|  |       </el-card> | ||||||
|  |     </el-col> | ||||||
|  |   </el-row> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup> | ||||||
|  | import { ref, onMounted } from 'vue' | ||||||
|  | 
 | ||||||
|  | const tableData = ref([ | ||||||
|  |   { name: '设计首页', status: '进行中', date: '2024-06-10' }, | ||||||
|  |   { name: '接口联调', status: '待办', date: '2024-06-12' }, | ||||||
|  |   { name: '测试用例', status: '已完成', date: '2024-06-08' }, | ||||||
|  | ]) | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  |   // console.log('工作台挂载完成') | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .card-header { | ||||||
|  |   font-size: 20px; | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										3
									
								
								frontend/src/views/log/AdmUserLogin.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/views/log/AdmUserLogin.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <template> | ||||||
|  |     管理员登录日志 | ||||||
|  | </template> | ||||||
							
								
								
									
										105
									
								
								frontend/src/views/login/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								frontend/src/views/login/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="login_container"> | ||||||
|  |     <el-row> | ||||||
|  |       <!-- <el-col :span="12" :xs="0"></el-col> --> | ||||||
|  |       <el-col :span="12" :xs="24"> | ||||||
|  |         <el-form class="login_form"> | ||||||
|  |           <h1>Hello</h1> | ||||||
|  |           <h2>欢迎来到项目管理系统</h2> | ||||||
|  |           <el-form-item> | ||||||
|  |             <el-input | ||||||
|  |               :prefix-icon="User" | ||||||
|  |               v-model="loginForm.username" | ||||||
|  |             ></el-input> | ||||||
|  |           </el-form-item> | ||||||
|  |           <el-form-item> | ||||||
|  |             <el-input | ||||||
|  |               type="password" | ||||||
|  |               :prefix-icon="Lock" | ||||||
|  |               v-model="loginForm.password" | ||||||
|  |               show-password | ||||||
|  |             ></el-input> | ||||||
|  |           </el-form-item> | ||||||
|  |           <el-form-item> | ||||||
|  |             <el-button | ||||||
|  |               class="login_btn" | ||||||
|  |               type="primary" | ||||||
|  |               size="default" | ||||||
|  |               :loading="loading" | ||||||
|  |               @click="login" | ||||||
|  |             > | ||||||
|  |               登录 | ||||||
|  |             </el-button> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-form> | ||||||
|  |       </el-col> | ||||||
|  |     </el-row> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { User, Lock } from '@element-plus/icons-vue' | ||||||
|  | import { reactive, ref } from 'vue' | ||||||
|  | import { useRouter } from 'vue-router' | ||||||
|  | import { ElNotification } from 'element-plus' | ||||||
|  | import useUserStore from '@/store/modules/user' | ||||||
|  | 
 | ||||||
|  | // 收集账号与密码数据 | ||||||
|  | const loginForm = reactive({ username: '', password: '' }) | ||||||
|  | // 按钮加载状态 | ||||||
|  | const loading = ref(false) | ||||||
|  | // 获取路由实例 | ||||||
|  | const router = useRouter() | ||||||
|  | // 获取用户仓库 | ||||||
|  | const userStore = useUserStore() | ||||||
|  | 
 | ||||||
|  | // 登录按钮的回调 | ||||||
|  | const login = async () => { | ||||||
|  |   loading.value = true | ||||||
|  |   try { | ||||||
|  |     await userStore.userLogin(loginForm) | ||||||
|  |     router.push('/') | ||||||
|  |     ElNotification({ | ||||||
|  |       type: 'success', | ||||||
|  |       message: '登录成功!', | ||||||
|  |     }) | ||||||
|  |     loading.value = false | ||||||
|  |   } catch (error: any) { | ||||||
|  |     loading.value = false | ||||||
|  |     ElNotification({ | ||||||
|  |       type: 'error', | ||||||
|  |       message: error?.message || '登录失败', | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .login_container { | ||||||
|  |   width: 100vw; | ||||||
|  |   height: 100vh; | ||||||
|  |   background: url('@/assets/images/background.jpg') no-repeat; | ||||||
|  |   // background: url('@/assets/images/background.png') no-repeat; | ||||||
|  |   background-size: cover; | ||||||
|  |   .login_form { | ||||||
|  |     position: relative; | ||||||
|  |     width: 80%; | ||||||
|  |     top: 30vh; | ||||||
|  |     // background: url('@/assets/images/login_form.png') no-repeat; | ||||||
|  |     background-size: cover; | ||||||
|  |     padding: 40px; | ||||||
|  |     h1 { | ||||||
|  |       color: white; | ||||||
|  |       font-size: 40px; | ||||||
|  |     } | ||||||
|  |     h2 { | ||||||
|  |       color: white; | ||||||
|  |       font-size: 20px; | ||||||
|  |       margin: 20px 0px; | ||||||
|  |     } | ||||||
|  |     .login_btn { | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										381
									
								
								frontend/src/views/main/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										381
									
								
								frontend/src/views/main/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,381 @@ | |||||||
|  | <template> | ||||||
|  |   <el-container> | ||||||
|  |     <el-header> | ||||||
|  |       <div class="header-left" :style="{ width: menuStore.width }"> | ||||||
|  |         <router-link to="/"> | ||||||
|  |           <div class="logo">VE</div> | ||||||
|  |           <div v-if="!menuStore.collapse" class="title"> | ||||||
|  |             <span>Vue Element</span> | ||||||
|  |           </div> | ||||||
|  |         </router-link> | ||||||
|  |       </div> | ||||||
|  |       <div class="header-center"> | ||||||
|  |         <el-icon size="22" style="margin-left: 15px;" @click="onCollapseSwitch"> | ||||||
|  |           <expand v-if="menuStore.collapse" /> | ||||||
|  |           <fold v-else /> | ||||||
|  |         </el-icon> | ||||||
|  |       </div> | ||||||
|  |       <div class="header-right"> | ||||||
|  |         <el-dropdown trigger="click" @command="handleUserCommand"> | ||||||
|  |           <span class="user-info"> | ||||||
|  |             <el-icon class="el-icon--left"> | ||||||
|  |               <user /> | ||||||
|  |             </el-icon> | ||||||
|  |             {{ userStore.userInfo?.name || 'admin' }} | ||||||
|  |             <el-icon class="el-icon--right"> | ||||||
|  |               <arrow-down /> | ||||||
|  |             </el-icon> | ||||||
|  |           </span> | ||||||
|  |           <template #dropdown> | ||||||
|  |             <el-dropdown-menu> | ||||||
|  |               <el-dropdown-item command="info">个人信息</el-dropdown-item> | ||||||
|  |               <el-dropdown-item command="password">修改密码</el-dropdown-item> | ||||||
|  |               <el-dropdown-item divided command="logout">退出登录</el-dropdown-item> | ||||||
|  |             </el-dropdown-menu> | ||||||
|  |           </template> | ||||||
|  |         </el-dropdown> | ||||||
|  |         <el-switch | ||||||
|  |           v-model="isDark" | ||||||
|  |           style="margin-right: 15px;" | ||||||
|  |           active-action-icon="moon-night" | ||||||
|  |           inactive-action-icon="sunny" | ||||||
|  |         /> | ||||||
|  |         <el-color-picker | ||||||
|  |           v-model="colorStore.primary" | ||||||
|  |           show-alpha | ||||||
|  |           :predefine="colorStore.primaryPredefines" | ||||||
|  |           @active-change="colorStore.primaryChange" | ||||||
|  |           @change="colorStore.primarySave" | ||||||
|  |           style="margin-right: 15px;" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |       <el-icon style="margin-right: 15px;" @click="toggle"> | ||||||
|  |         <rank v-if="isFullscreen" /> | ||||||
|  |         <full-screen v-else /> | ||||||
|  |       </el-icon> | ||||||
|  |     </el-header> | ||||||
|  | 
 | ||||||
|  |     <el-container> | ||||||
|  |       <el-aside :width="menuStore.width"> | ||||||
|  |         <el-menu | ||||||
|  |           router | ||||||
|  |           :default-active="route.path" | ||||||
|  |           :collapse="menuStore.collapse" | ||||||
|  |           style="height: 100%;" | ||||||
|  |         > | ||||||
|  |           <template v-for="menu in menus" :key="menu.smid"> | ||||||
|  |             <el-sub-menu | ||||||
|  |               v-if="menu.children && menu.children.length > 0" | ||||||
|  |               :index="menu.smid.toString()" | ||||||
|  |             > | ||||||
|  |               <template #title> | ||||||
|  |                 <el-icon> | ||||||
|  |                   <component :is="menu.icon_class"></component> | ||||||
|  |                 </el-icon> | ||||||
|  |                 <span>{{ menu.label }}</span> | ||||||
|  |               </template> | ||||||
|  |               <el-menu-item | ||||||
|  |                 v-for="child in menu.children" | ||||||
|  |                 :key="child.smid" | ||||||
|  |                 :index="child.smid.toString()" | ||||||
|  |                 @click="handleMenuClick(child)" | ||||||
|  |               > | ||||||
|  |                 <el-icon> | ||||||
|  |                   <component :is="child.icon_class"></component> | ||||||
|  |                 </el-icon> | ||||||
|  |                 <span>{{ child.label }}</span> | ||||||
|  |               </el-menu-item> | ||||||
|  |             </el-sub-menu> | ||||||
|  | 
 | ||||||
|  |             <el-menu-item | ||||||
|  |               v-else | ||||||
|  |               :key="menu.smid" | ||||||
|  |               :index="menu.smid.toString()" | ||||||
|  |               @click="handleMenuClick(menu)" | ||||||
|  |             > | ||||||
|  |               <el-icon> | ||||||
|  |                 <component :is="menu.icon_class"></component> | ||||||
|  |               </el-icon> | ||||||
|  |               <template #title>{{ menu.label }}</template> | ||||||
|  |             </el-menu-item> | ||||||
|  |           </template> | ||||||
|  |         </el-menu> | ||||||
|  |       </el-aside> | ||||||
|  | 
 | ||||||
|  |       <el-main> | ||||||
|  |         <!-- 导航标签栏 --> | ||||||
|  |         <div class="navbar"> | ||||||
|  |           <el-tag | ||||||
|  |             v-for="item in fastnavStore.datas" | ||||||
|  |             :key="item.path" | ||||||
|  |             :type="item.path == fastnavStore.currPath ? 'primary' : 'info'" | ||||||
|  |             size="large" | ||||||
|  |             :closable="fastnavStore.datas.length != 1" | ||||||
|  |             style="margin-right: 10px;" | ||||||
|  |             @click="onTagClick(item.path)" | ||||||
|  |             @close="onTagClose(item.path)" | ||||||
|  |             @contextmenu.native.prevent="openTagContextMenu($event, item)" | ||||||
|  |           > | ||||||
|  |             {{ item.desc }} | ||||||
|  |           </el-tag> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- 主内容区域 --> | ||||||
|  |         <el-scrollbar class="main-scrollbar"> | ||||||
|  |           <div class="main-content"> | ||||||
|  |             <RouterView /> | ||||||
|  |           </div> | ||||||
|  |         </el-scrollbar> | ||||||
|  | 
 | ||||||
|  |         <el-dropdown | ||||||
|  |           ref="navbarDropdown" | ||||||
|  |           trigger="contextmenu" | ||||||
|  |           style="position: absolute;" | ||||||
|  |           @command="handleTagCommand" | ||||||
|  |         > | ||||||
|  |           <span></span> | ||||||
|  |           <template #dropdown> | ||||||
|  |             <el-dropdown-menu> | ||||||
|  |               <el-dropdown-item command="reload" icon="Refresh" | ||||||
|  |                 >重新加载</el-dropdown-item | ||||||
|  |               > | ||||||
|  |               <el-dropdown-item command="close" icon="Close" | ||||||
|  |                 >关闭</el-dropdown-item | ||||||
|  |               > | ||||||
|  |               <el-dropdown-item command="closeOther" icon="Delete" | ||||||
|  |                 >关闭其他</el-dropdown-item | ||||||
|  |               > | ||||||
|  |               <el-dropdown-item command="closeLeft" icon="Back" | ||||||
|  |                 >关闭左侧</el-dropdown-item | ||||||
|  |               > | ||||||
|  |               <el-dropdown-item command="closeRight" icon="Right" | ||||||
|  |                 >关闭右侧</el-dropdown-item | ||||||
|  |               > | ||||||
|  |             </el-dropdown-menu> | ||||||
|  |           </template> | ||||||
|  |         </el-dropdown> | ||||||
|  |       </el-main> | ||||||
|  |     </el-container> | ||||||
|  |   </el-container> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import useUserStore from '@/store/modules/user' | ||||||
|  | import useMenuStore from '@/store/modules/menu' | ||||||
|  | import useFastnavStore from '@/store/fastnav' | ||||||
|  | import useMenuCollapseStore from '@/store/menu' | ||||||
|  | import useColorStore from '@/store/color' | ||||||
|  | import { onMounted, ref } from 'vue' | ||||||
|  | import { useRoute, useRouter, RouterView } from 'vue-router' | ||||||
|  | import { useFullscreen, useDark } from '@vueuse/core' | ||||||
|  | 
 | ||||||
|  | const route = useRoute() | ||||||
|  | const router = useRouter() | ||||||
|  | const { isFullscreen, toggle } = useFullscreen() | ||||||
|  | const isDark = useDark() | ||||||
|  | 
 | ||||||
|  | const userStore = useUserStore() | ||||||
|  | const menuStore = useMenuCollapseStore() | ||||||
|  | const fastnavStore = useFastnavStore() | ||||||
|  | const menuDataStore = useMenuStore() | ||||||
|  | const colorStore = useColorStore() | ||||||
|  | 
 | ||||||
|  | const menus = ref<any[]>([]) | ||||||
|  | const collapse = ref(true) | ||||||
|  | 
 | ||||||
|  | // 新增:控制右键菜单各项的禁用状态 | ||||||
|  | const tagMenuReloadDisabled = ref(false) | ||||||
|  | const tagMenuCloseDisabled = ref(false) | ||||||
|  | const tagMenuOtherDisabled = ref(false) | ||||||
|  | const tagMenuLeftDisabled = ref(false) | ||||||
|  | const tagMenuRightDisabled = ref(false) | ||||||
|  | 
 | ||||||
|  | onMounted(async () => { | ||||||
|  |     // 获取动态菜单数据 | ||||||
|  |     await menuDataStore.fetchUserMenus() | ||||||
|  |     menus.value = menuDataStore.getMenus | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 折叠侧边栏 | ||||||
|  | const onCollapseSwitch = () => { | ||||||
|  |     menuStore.collapse = !menuStore.collapse | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const onTagClick = (path: string) => { | ||||||
|  |     if (path == fastnavStore.currPath) return | ||||||
|  |     router.push(path) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const onTagClose = (path: string) => { | ||||||
|  |     console.log(`关闭页签:${path}`) | ||||||
|  |     const pathNew = fastnavStore.removeData(path) | ||||||
|  |     if (!pathNew) return | ||||||
|  |     router.push(pathNew) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 右键菜单相关 | ||||||
|  | let navbarItem: any | ||||||
|  | const navbarDropdown = ref() | ||||||
|  | const openTagContextMenu = (e: PointerEvent, item: any) => { | ||||||
|  |     navbarItem = item | ||||||
|  |     navbarDropdown.value.handleClose() | ||||||
|  | 
 | ||||||
|  |     tagMenuReloadDisabled.value = item.path != fastnavStore.currPath | ||||||
|  |     tagMenuCloseDisabled.value = fastnavStore.datas.length == 1 | ||||||
|  |     tagMenuOtherDisabled.value = fastnavStore.datas.length == 1 | ||||||
|  |     tagMenuLeftDisabled.value = fastnavStore.isFirst(item.path) | ||||||
|  |     tagMenuRightDisabled.value = fastnavStore.isLast(item.path) | ||||||
|  | 
 | ||||||
|  |     setTimeout(() => { | ||||||
|  |         navbarDropdown.value.$el.style.left = e.x + 'px' | ||||||
|  |         navbarDropdown.value.$el.style.top = e.y + 'px' | ||||||
|  |         navbarDropdown.value.handleOpen() | ||||||
|  |     }, 50) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleTagCommand = (command: string) => { | ||||||
|  |     switch (command) { | ||||||
|  |         case 'reload': | ||||||
|  |             // 重新加载当前页面 | ||||||
|  |             const currentPath = fastnavStore.currPath | ||||||
|  |             router.replace('/redirect' + currentPath) | ||||||
|  |             break | ||||||
|  |         case 'close': | ||||||
|  |             onTagClose(navbarItem.path) | ||||||
|  |             break | ||||||
|  |         case 'closeOther': | ||||||
|  |             const pathNew1 = fastnavStore.removeOther(navbarItem.path) | ||||||
|  |             if (!pathNew1) return | ||||||
|  |             router.push(pathNew1) | ||||||
|  |             break | ||||||
|  |         case 'closeLeft': | ||||||
|  |             const pathNew2 = fastnavStore.removeLeft(navbarItem.path) | ||||||
|  |             if (!pathNew2) return | ||||||
|  |             router.push(pathNew2) | ||||||
|  |             break | ||||||
|  |         case 'closeRight': | ||||||
|  |             const pathNew3 = fastnavStore.removeRight(navbarItem.path) | ||||||
|  |             if (!pathNew3) return | ||||||
|  |             router.push(pathNew3) | ||||||
|  |             break | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 菜单点击处理 | ||||||
|  | const handleMenuClick = (menu: any) => { | ||||||
|  |     if (menu.type === 1 && menu.src) { | ||||||
|  |         // 内部跳转 | ||||||
|  |         const title = menu.label | ||||||
|  |         const path = '/' + menu.src.replace('/', '_') | ||||||
|  |         // 这里需要根据实际情况处理页面跳转 | ||||||
|  |         console.log('内部跳转:', title, path) | ||||||
|  |     } else if (menu.type === 2 && menu.src) { | ||||||
|  |         // 外部链接 | ||||||
|  |         window.open(menu.src, '_blank') | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 用户下拉菜单处理 | ||||||
|  | const handleUserCommand = (command: string) => { | ||||||
|  |     if (command === 'logout') { | ||||||
|  |         userStore.userLogout() | ||||||
|  |         router.push('/login') | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | .el-header { | ||||||
|  |     padding: 0px; | ||||||
|  |     border-bottom: 1px solid var(--el-border-color); | ||||||
|  | 
 | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | .el-main { | ||||||
|  |     height: calc(100vh - 60px); | ||||||
|  |     padding: 12px; | ||||||
|  |     text-align: left; | ||||||
|  |      | ||||||
|  |     .navbar { | ||||||
|  |         margin-bottom: 20px; | ||||||
|  |         padding: 10px 0; | ||||||
|  |         border-bottom: 1px solid var(--el-border-color-light); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .main-scrollbar { | ||||||
|  |         height: calc(100vh - 200px); | ||||||
|  |          | ||||||
|  |         .main-content { | ||||||
|  |             padding: 20px; | ||||||
|  |             min-height: 100%; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .header-center { | ||||||
|  |     flex-grow: 1; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  | 
 | ||||||
|  |     width: 1px; | ||||||
|  | 
 | ||||||
|  |     .el-scrollbar { | ||||||
|  |         margin: 0 12px; | ||||||
|  | 
 | ||||||
|  |         .navbar { | ||||||
|  |             display: flex; | ||||||
|  | 
 | ||||||
|  |             .el-tag { | ||||||
|  |                 margin-right: 10px; | ||||||
|  |                 cursor: pointer; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | .header-left { | ||||||
|  |     & a { | ||||||
|  |         height: 60px; | ||||||
|  |         color: inherit; | ||||||
|  |         text-decoration: none; | ||||||
|  | 
 | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  | 
 | ||||||
|  |         .logo { | ||||||
|  |             margin-left: 14px; | ||||||
|  |             width: 36px; | ||||||
|  |             height: 36px; | ||||||
|  |             background-color: var(--el-color-primary); | ||||||
|  |             border-radius: 4px; | ||||||
|  | 
 | ||||||
|  |             display: flex; | ||||||
|  |             justify-content: center; | ||||||
|  |             align-items: center; | ||||||
|  | 
 | ||||||
|  |             color: white; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .title { | ||||||
|  |             margin-left: 14px; | ||||||
|  |             font-weight: 700; | ||||||
|  | 
 | ||||||
|  |             & span { | ||||||
|  |                 color: var(--el-color-primary); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | .header-right { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  | 
 | ||||||
|  |     .user-info { | ||||||
|  |         margin-right: 15px; | ||||||
|  |         cursor: pointer; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										3
									
								
								frontend/src/views/sys/AdmUser.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/views/sys/AdmUser.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <template> | ||||||
|  |     管理员 | ||||||
|  | </template> | ||||||
							
								
								
									
										3
									
								
								frontend/src/views/sys/AdmUserLogin.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/views/sys/AdmUserLogin.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <template> | ||||||
|  |     管理员登录日志 | ||||||
|  | </template> | ||||||
							
								
								
									
										3
									
								
								frontend/src/views/sys/AdmUserPassword.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/views/sys/AdmUserPassword.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <template> | ||||||
|  |     密码更新 | ||||||
|  | </template> | ||||||
							
								
								
									
										7
									
								
								frontend/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | /// <reference types="vite/client" />
 | ||||||
|  | 
 | ||||||
|  | declare module '*.vue' { | ||||||
|  |   import type { DefineComponent } from 'vue' | ||||||
|  |   const component: DefineComponent<{}, {}, any> | ||||||
|  |   export default component | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								frontend/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | { | ||||||
|  |   "extends": "@vue/tsconfig/tsconfig.dom.json", | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||||||
|  | 
 | ||||||
|  |     /* Linting */ | ||||||
|  |     "strict": true, | ||||||
|  |     "noUnusedLocals": true, | ||||||
|  |     "noUnusedParameters": true, | ||||||
|  |     "noFallthroughCasesInSwitch": true, | ||||||
|  |     "noUncheckedSideEffectImports": true | ||||||
|  |   }, | ||||||
|  |   "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] | ||||||
|  | } | ||||||
							
								
								
									
										28
									
								
								frontend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | { | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "target": "ESNext", | ||||||
|  |     "useDefineForClassFields": true, | ||||||
|  |     "module": "ESNext", | ||||||
|  |     "moduleResolution": "Node", | ||||||
|  |     "strict": true, | ||||||
|  |     "jsx": "preserve", | ||||||
|  |     "jsxImportSource": "vue", | ||||||
|  |     "resolveJsonModule": true, | ||||||
|  |     "isolatedModules": true, | ||||||
|  |     "types": ["element-plus/global"], | ||||||
|  |     "esModuleInterop": true, | ||||||
|  |     "lib": ["ESNext", "DOM"], | ||||||
|  |     "skipLibCheck": true, | ||||||
|  |     "noEmit": true, | ||||||
|  |     "baseUrl": ".", | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": [ | ||||||
|  |         "src/*" | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], | ||||||
|  |   "references": [ | ||||||
|  |     { "path": "./tsconfig.node.json" } | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								frontend/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | { | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "composite": true, | ||||||
|  |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", | ||||||
|  |     "target": "ES2023", | ||||||
|  |     "lib": ["ES2023"], | ||||||
|  |     "module": "ESNext", | ||||||
|  |     "skipLibCheck": true, | ||||||
|  | 
 | ||||||
|  |     /* Bundler mode */ | ||||||
|  |     "moduleResolution": "bundler", | ||||||
|  |     "allowImportingTsExtensions": true, | ||||||
|  |     "verbatimModuleSyntax": true, | ||||||
|  |     "moduleDetection": "force", | ||||||
|  |     "emitDeclarationOnly": true, | ||||||
|  | 
 | ||||||
|  |     /* Linting */ | ||||||
|  |     "strict": true, | ||||||
|  |     "noUnusedLocals": true, | ||||||
|  |     "noUnusedParameters": true, | ||||||
|  |     "noFallthroughCasesInSwitch": true, | ||||||
|  |     "noUncheckedSideEffectImports": true | ||||||
|  |   }, | ||||||
|  |   "include": ["vite.config.ts"] | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								frontend/tsconfig.tsbuildinfo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/tsconfig.tsbuildinfo
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | {"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/user.ts","./src/config/env.ts","./src/data/menu.ts","./src/router/index.ts","./src/store/auth.ts","./src/store/color.ts","./src/store/fastnav.ts","./src/store/index.ts","./src/store/menu.ts","./src/store/modules/user.ts","./src/types/index.d.ts","./src/util/menu.ts","./src/utils/request.ts","./src/app.vue","./src/views/error/404.vue","./src/views/home/index.vue","./src/views/log/admuserlogin.vue","./src/views/login/index.vue","./src/views/main/index.vue","./src/views/sys/admuser.vue","./src/views/sys/admuserlogin.vue","./src/views/sys/admuserpassword.vue"],"version":"5.8.3"} | ||||||
							
								
								
									
										2
									
								
								frontend/vite.config.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								frontend/vite.config.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | declare const _default: import("vite").UserConfig; | ||||||
|  | export default _default; | ||||||
							
								
								
									
										61
									
								
								frontend/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								frontend/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | |||||||
|  | // vite.config.ts
 | ||||||
|  | import { defineConfig } from 'vite' | ||||||
|  | import vue from '@vitejs/plugin-vue' | ||||||
|  | import AutoImport from 'unplugin-auto-import/vite' | ||||||
|  | import Components from 'unplugin-vue-components/vite' | ||||||
|  | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' | ||||||
|  | import path from 'path' | ||||||
|  | 
 | ||||||
|  | export default defineConfig({ | ||||||
|  |   // 设置基础路径,支持子目录部署
 | ||||||
|  |   base: './', | ||||||
|  |    | ||||||
|  |   resolve: { | ||||||
|  |     alias: { | ||||||
|  |       '@': path.resolve(__dirname, './src') | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |    | ||||||
|  |   plugins: [ | ||||||
|  |     vue(), | ||||||
|  |     AutoImport({ | ||||||
|  |       resolvers: [ElementPlusResolver()], | ||||||
|  |     }), | ||||||
|  |     Components({ | ||||||
|  |       resolvers: [ElementPlusResolver()], | ||||||
|  |     }), | ||||||
|  |   ], | ||||||
|  |    | ||||||
|  |   // 构建配置
 | ||||||
|  |   build: { | ||||||
|  |     // 输出目录
 | ||||||
|  |     outDir: 'dist', | ||||||
|  |     // 静态资源目录
 | ||||||
|  |     assetsDir: 'assets', | ||||||
|  |     // 生成 sourcemap
 | ||||||
|  |     sourcemap: false, | ||||||
|  |     // 压缩配置
 | ||||||
|  |     minify: 'esbuild', | ||||||
|  |     // 代码分割配置
 | ||||||
|  |     rollupOptions: { | ||||||
|  |       output: { | ||||||
|  |         // 手动分割代码块
 | ||||||
|  |         manualChunks: { | ||||||
|  |           // 将 Vue 相关库单独打包
 | ||||||
|  |           'vue-vendor': ['vue', 'vue-router', 'pinia'], | ||||||
|  |           // 将 Element Plus 单独打包
 | ||||||
|  |           'element-plus': ['element-plus'], | ||||||
|  |           // 将图标库单独打包
 | ||||||
|  |           'icons': ['@element-plus/icons-vue'] | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |    | ||||||
|  |   // 服务器配置
 | ||||||
|  |   // server: {
 | ||||||
|  |   //   port: 3000,
 | ||||||
|  |   //   open: true,
 | ||||||
|  |   //   cors: true
 | ||||||
|  |   // }
 | ||||||
|  | }) | ||||||
| @ -1,4 +1,4 @@ | |||||||
| <?php /*a:4:{s:59:"E:\Demos\DemoOwns\PHP\yunzer\app\index\view\index\index.php";i:1750323451;s:64:"E:\Demos\DemoOwns\PHP\yunzer\app\index\view\component\header.php";i:1750323451;s:62:"E:\Demos\DemoOwns\PHP\yunzer\app\index\view\component\main.php";i:1751594649;s:64:"E:\Demos\DemoOwns\PHP\yunzer\app\index\view\component\footer.php";i:1750323451;}*/ ?>
 | <?php /*a:4:{s:59:"E:\Demos\DemoOwns\PHP\yunzer\app\index\view\index\index.php";i:1754756464;s:64:"E:\Demos\DemoOwns\PHP\yunzer\app\index\view\component\header.php";i:1750323451;s:62:"E:\Demos\DemoOwns\PHP\yunzer\app\index\view\component\main.php";i:1751594649;s:64:"E:\Demos\DemoOwns\PHP\yunzer\app\index\view\component\footer.php";i:1750323451;}*/ ?>
 | ||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html> | <html> | ||||||
| 
 | 
 | ||||||
| @ -10,9 +10,11 @@ | |||||||
|     <link rel="stylesheet" href="/static/css/style.css"> |     <link rel="stylesheet" href="/static/css/style.css"> | ||||||
|     <link rel="stylesheet" href="/static/css/bootstrap.min.css"> |     <link rel="stylesheet" href="/static/css/bootstrap.min.css"> | ||||||
|     <link rel="stylesheet" href="/static/css/fontawesome.css"> |     <link rel="stylesheet" href="/static/css/fontawesome.css"> | ||||||
|  |     <link rel="stylesheet" href="/static/css/all.min.css"> | ||||||
| 
 | 
 | ||||||
|     <script src="/static/layui/layui.js" charset="utf-8"></script> |     <script src="/static/layui/layui.js" charset="utf-8"></script> | ||||||
|     <script src="/static/js/bootstrap.bundle.js"></script> |     <script src="/static/js/bootstrap.bundle.js"></script> | ||||||
|  |     <script src="/static/js/all.min.js"></script> | ||||||
| </head> | </head> | ||||||
| 
 | 
 | ||||||
| <body> | <body> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user