awake
This commit is contained in:
		
							
								
								
									
										6
									
								
								.env.development
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.env.development
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					ENV = 'production'
 | 
				
			||||||
 | 
					VUE_APP_MODE = 'production'
 | 
				
			||||||
 | 
					VITE_BASE_URL = 'https://wly-demo.wlyun.co'
 | 
				
			||||||
 | 
					VITE_ENVURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/hdr/'
 | 
				
			||||||
 | 
					VITE_MODELURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/model/'
 | 
				
			||||||
 | 
					VITE_PUBLIC_URL=/
 | 
				
			||||||
							
								
								
									
										6
									
								
								.env.production
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.env.production
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					ENV = 'production'
 | 
				
			||||||
 | 
					VUE_APP_MODE = 'production'
 | 
				
			||||||
 | 
					VITE_BASE_URL = 'https://wly-demo.wlyun.co'
 | 
				
			||||||
 | 
					VITE_ENVURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/hdr/'
 | 
				
			||||||
 | 
					VITE_MODELURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/model/'
 | 
				
			||||||
 | 
					VITE_PUBLIC_URL=./
 | 
				
			||||||
							
								
								
									
										24
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.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
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "recommendations": ["Vue.volar"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										156
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,156 @@
 | 
				
			|||||||
 | 
					# Vite + Vue 3 + TypeScript 脚手架
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					一个基于 Vite 构建的现代化 Vue 3 + TypeScript 前端脚手架,集成了 Pinia 状态管理、Vue Router 动态路由、持久化存储工具和环境变量配置。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## ✨ 特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- ⚡ **Vite 4** - 极速的开发体验
 | 
				
			||||||
 | 
					- 🎯 **Vue 3** - 组合式 API,性能更强
 | 
				
			||||||
 | 
					- 🔷 **TypeScript** - 类型安全
 | 
				
			||||||
 | 
					- 🍍 **Pinia** - 现代化状态管理
 | 
				
			||||||
 | 
					- 🛣️ **Vue Router 4** - 路由管理与权限控制
 | 
				
			||||||
 | 
					- 💾 **持久化存储** - localStorage/sessionStorage 封装
 | 
				
			||||||
 | 
					- 🎨 **主题切换** - 明亮/暗黑主题支持
 | 
				
			||||||
 | 
					- 📱 **响应式设计** - 移动端适配
 | 
				
			||||||
 | 
					- 🔧 **环境变量配置** - 开发/生产环境分离
 | 
				
			||||||
 | 
					- 📦 **自动代码分割** - 优化打包体积
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 🏗️ 项目结构
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					src/
 | 
				
			||||||
 | 
					├── assets/          # 静态资源
 | 
				
			||||||
 | 
					├── components/      # 公共组件
 | 
				
			||||||
 | 
					├── config/          # 配置文件
 | 
				
			||||||
 | 
					│   └── env.ts      # 环境变量配置
 | 
				
			||||||
 | 
					├── router/          # 路由配置
 | 
				
			||||||
 | 
					│   └── index.ts    # 路由定义与权限控制
 | 
				
			||||||
 | 
					├── stores/          # 状态管理
 | 
				
			||||||
 | 
					│   ├── index.ts    # Pinia 主文件
 | 
				
			||||||
 | 
					│   └── modules/    # 状态模块
 | 
				
			||||||
 | 
					│       ├── app.ts  # 应用状态
 | 
				
			||||||
 | 
					│       └── user.ts # 用户状态
 | 
				
			||||||
 | 
					├── utils/           # 工具函数
 | 
				
			||||||
 | 
					│   └── storage.ts  # 存储工具
 | 
				
			||||||
 | 
					├── views/           # 页面组件
 | 
				
			||||||
 | 
					│   ├── Home.vue    # 首页
 | 
				
			||||||
 | 
					│   ├── About.vue   # 关于页面
 | 
				
			||||||
 | 
					│   ├── Login.vue   # 登录页面
 | 
				
			||||||
 | 
					│   ├── Dashboard.vue # 仪表板
 | 
				
			||||||
 | 
					│   ├── Profile.vue # 个人中心
 | 
				
			||||||
 | 
					│   ├── Admin.vue   # 管理页面
 | 
				
			||||||
 | 
					│   └── 404.vue     # 404 页面
 | 
				
			||||||
 | 
					├── App.vue          # 根组件
 | 
				
			||||||
 | 
					└── main.ts          # 入口文件
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 🚀 快速开始
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 安装依赖
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					npm install
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 开发环境
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					npm run dev
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 生产构建
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					npm run build
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 预览构建结果
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					npm run preview
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 🔧 主要功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 状态管理 (Pinia)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **用户状态管理**: 登录、用户信息、权限控制
 | 
				
			||||||
 | 
					- **应用状态管理**: 主题、语言、应用配置
 | 
				
			||||||
 | 
					- **持久化支持**: 自动同步到本地存储
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 路由管理 (Vue Router)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **路由守卫**: 登录验证、权限检查
 | 
				
			||||||
 | 
					- **动态路由**: 根据用户权限动态加载
 | 
				
			||||||
 | 
					- **页面缓存**: 支持 keep-alive
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 持久化存储
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					import { storage } from '@/utils/storage'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 本地存储
 | 
				
			||||||
 | 
					storage.local.set('key', value, 3600) // 设置过期时间(秒)
 | 
				
			||||||
 | 
					storage.local.get('key')
 | 
				
			||||||
 | 
					storage.local.remove('key')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 会话存储
 | 
				
			||||||
 | 
					storage.session.set('key', value)
 | 
				
			||||||
 | 
					storage.session.get('key')
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 环境变量配置
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					在 `src/config/env.ts` 中配置环境变量:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					export const envConfig = {
 | 
				
			||||||
 | 
					  VITE_APP_TITLE: 'Vite应用',
 | 
				
			||||||
 | 
					  VITE_APP_PORT: 8080,
 | 
				
			||||||
 | 
					  VITE_APP_BASE_API: '/api',
 | 
				
			||||||
 | 
					  VITE_APP_ENV: 'development'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 主题切换
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					import { useAppStore } from '@/stores/modules/app'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const appStore = useAppStore()
 | 
				
			||||||
 | 
					appStore.toggleTheme() // 切换主题
 | 
				
			||||||
 | 
					appStore.setTheme('dark') // 设置暗色主题
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 📝 开发指南
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 添加新页面
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. 在 `src/views/` 下创建页面组件
 | 
				
			||||||
 | 
					2. 在 `src/router/index.ts` 中添加路由配置
 | 
				
			||||||
 | 
					3. 如需权限控制,设置 `meta.requireAuth: true`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 添加新的状态模块
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. 在 `src/stores/modules/` 下创建新模块
 | 
				
			||||||
 | 
					2. 在 `src/stores/index.ts` 中导出模块
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 自定义样式
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- 全局样式在 `src/App.vue` 中定义
 | 
				
			||||||
 | 
					- 支持 CSS 变量进行主题切换
 | 
				
			||||||
 | 
					- 响应式断点已预设
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 🛠️ 技术栈
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **前端框架**: Vue 3
 | 
				
			||||||
 | 
					- **构建工具**: Vite 4
 | 
				
			||||||
 | 
					- **编程语言**: TypeScript
 | 
				
			||||||
 | 
					- **状态管理**: Pinia
 | 
				
			||||||
 | 
					- **路由管理**: Vue Router 4
 | 
				
			||||||
 | 
					- **HTTP 客户端**: Axios
 | 
				
			||||||
 | 
					- **样式**: CSS3 + CSS Variables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 📄 许可证
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MIT License
 | 
				
			||||||
							
								
								
									
										13
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								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>
 | 
				
			||||||
							
								
								
									
										4561
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4561
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										29
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "client",
 | 
				
			||||||
 | 
					  "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",
 | 
				
			||||||
 | 
					    "axios": "^1.10.0",
 | 
				
			||||||
 | 
					    "element-plus": "^2.10.3",
 | 
				
			||||||
 | 
					    "pinia": "^3.0.3",
 | 
				
			||||||
 | 
					    "vue": "^3.5.13",
 | 
				
			||||||
 | 
					    "vue-router": "^4.5.1"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@types/node": "^24.0.12",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-legacy": "^6.1.1",
 | 
				
			||||||
 | 
					    "@vitejs/plugin-vue": "^5.2.3",
 | 
				
			||||||
 | 
					    "@vue/tsconfig": "^0.7.0",
 | 
				
			||||||
 | 
					    "rollup-plugin-visualizer": "^6.0.3",
 | 
				
			||||||
 | 
					    "typescript": "~5.8.3",
 | 
				
			||||||
 | 
					    "vite": "^6.3.5",
 | 
				
			||||||
 | 
					    "vue-tsc": "^2.2.8"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								public/vite.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								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  | 
							
								
								
									
										70
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// 应用入口组件
 | 
				
			||||||
 | 
					import NavHeader from './components/NavHeader.vue'
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div id="app">
 | 
				
			||||||
 | 
					    <NavHeader />
 | 
				
			||||||
 | 
					    <el-container>
 | 
				
			||||||
 | 
					      <!-- 导航头部 -->
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 主要内容区域 -->
 | 
				
			||||||
 | 
					      <el-main class="main-content">
 | 
				
			||||||
 | 
					        <router-view />
 | 
				
			||||||
 | 
					      </el-main>
 | 
				
			||||||
 | 
					    </el-container>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					/* 全局样式 */
 | 
				
			||||||
 | 
					* {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
 | 
				
			||||||
 | 
					  line-height: 1.6;
 | 
				
			||||||
 | 
					  color: #2d3748;
 | 
				
			||||||
 | 
					  background-color: #ffffff;
 | 
				
			||||||
 | 
					  transition: background-color 0.2s, color 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#app {
 | 
				
			||||||
 | 
					  min-height: 100vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.el-container {
 | 
				
			||||||
 | 
					  min-height: 100vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.main-content {
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  background-color: #f5f5f5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 暗色主题 */
 | 
				
			||||||
 | 
					[data-theme="dark"] body {
 | 
				
			||||||
 | 
					  color: #e2e8f0;
 | 
				
			||||||
 | 
					  background-color: #1a202c;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .main-content {
 | 
				
			||||||
 | 
					  background-color: #1a202c;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h1, h2, h3, h4, h5, h6 {
 | 
				
			||||||
 | 
					  margin-bottom: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 响应式设计 */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  body {
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										288
									
								
								src/api/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								src/api/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,288 @@
 | 
				
			|||||||
 | 
					# API 架构重构说明
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 概述
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					本次重构将原有的网络请求代码进行了全面的重新设计和组织,提供了更加模块化、类型安全和易于扩展的API架构。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 目录结构
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					src/api/
 | 
				
			||||||
 | 
					├── types/           # 类型定义
 | 
				
			||||||
 | 
					│   └── index.ts
 | 
				
			||||||
 | 
					├── config/          # 配置文件
 | 
				
			||||||
 | 
					│   └── index.ts
 | 
				
			||||||
 | 
					├── utils/           # 工具函数
 | 
				
			||||||
 | 
					│   └── index.ts
 | 
				
			||||||
 | 
					├── core/            # 核心模块
 | 
				
			||||||
 | 
					│   ├── request.ts   # HTTP请求客户端
 | 
				
			||||||
 | 
					│   ├── upload.ts    # 文件上传模块
 | 
				
			||||||
 | 
					│   └── download.ts  # 文件下载模块
 | 
				
			||||||
 | 
					├── modules/         # 业务API模块
 | 
				
			||||||
 | 
					│   ├── editor.ts    # 编辑器相关API
 | 
				
			||||||
 | 
					│   └── resource.ts  # 资源管理API
 | 
				
			||||||
 | 
					├── examples/        # 使用示例
 | 
				
			||||||
 | 
					│   └── usage.ts
 | 
				
			||||||
 | 
					├── index.ts         # 统一入口
 | 
				
			||||||
 | 
					└── README.md        # 说明文档
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 主要特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1. 类型安全
 | 
				
			||||||
 | 
					- 完整的TypeScript类型定义
 | 
				
			||||||
 | 
					- 泛型支持,确保请求和响应数据的类型安全
 | 
				
			||||||
 | 
					- 严格的类型检查
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2. 模块化设计
 | 
				
			||||||
 | 
					- 核心功能与业务逻辑分离
 | 
				
			||||||
 | 
					- 可插拔的模块设计
 | 
				
			||||||
 | 
					- 易于扩展和维护
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 3. 文件上传功能
 | 
				
			||||||
 | 
					- 普通文件上传
 | 
				
			||||||
 | 
					- 图片上传(支持压缩)
 | 
				
			||||||
 | 
					- 大文件分片上传
 | 
				
			||||||
 | 
					- 批量文件上传
 | 
				
			||||||
 | 
					- Base64上传
 | 
				
			||||||
 | 
					- 上传进度监控
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 4. 文件下载功能
 | 
				
			||||||
 | 
					- 普通文件下载
 | 
				
			||||||
 | 
					- 分片下载(大文件)
 | 
				
			||||||
 | 
					- 批量下载
 | 
				
			||||||
 | 
					- 下载为不同格式(Blob、Base64、ArrayBuffer、Text)
 | 
				
			||||||
 | 
					- 下载进度监控
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 5. 错误处理
 | 
				
			||||||
 | 
					- 统一的错误处理机制
 | 
				
			||||||
 | 
					- 自动重试功能
 | 
				
			||||||
 | 
					- 详细的错误信息
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 6. 请求管理
 | 
				
			||||||
 | 
					- 请求拦截器
 | 
				
			||||||
 | 
					- 响应拦截器
 | 
				
			||||||
 | 
					- 请求取消功能
 | 
				
			||||||
 | 
					- 认证token管理
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 快速开始
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 基本使用
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					import { editorApi, resourceApi, ApiManager } from '@/api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 设置认证token
 | 
				
			||||||
 | 
					ApiManager.setAuthToken('your-token')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建订单
 | 
				
			||||||
 | 
					const order = await editorApi.createOrder({
 | 
				
			||||||
 | 
					  projectId: 'project-123',
 | 
				
			||||||
 | 
					  type: 'render'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 上传资源
 | 
				
			||||||
 | 
					const resource = await resourceApi.uploadResource(file, {
 | 
				
			||||||
 | 
					  type: 'model',
 | 
				
			||||||
 | 
					  tags: ['建筑', '现代']
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 文件上传
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					import { fileUploader } from '@/api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 普通文件上传
 | 
				
			||||||
 | 
					const result = await fileUploader.uploadFile({
 | 
				
			||||||
 | 
					  file,
 | 
				
			||||||
 | 
					  onProgress: (progress) => {
 | 
				
			||||||
 | 
					    console.log(`上传进度: ${progress}%`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 图片上传(带压缩)
 | 
				
			||||||
 | 
					const imageResult = await fileUploader.uploadImage(file, {
 | 
				
			||||||
 | 
					  compress: true,
 | 
				
			||||||
 | 
					  quality: 0.8,
 | 
				
			||||||
 | 
					  maxWidth: 1920,
 | 
				
			||||||
 | 
					  maxHeight: 1080
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 大文件分片上传
 | 
				
			||||||
 | 
					const largeFileResult = await fileUploader.uploadLargeFile(file, {
 | 
				
			||||||
 | 
					  chunkSize: 2 * 1024 * 1024, // 2MB per chunk
 | 
				
			||||||
 | 
					  onProgress: (progress) => {
 | 
				
			||||||
 | 
					    console.log(`上传进度: ${progress}%`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 文件下载
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					import { fileDownloader } from '@/api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 下载并保存文件
 | 
				
			||||||
 | 
					await fileDownloader.downloadAndSave({
 | 
				
			||||||
 | 
					  url: 'https://example.com/file.zip',
 | 
				
			||||||
 | 
					  filename: 'download.zip'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 下载为Base64
 | 
				
			||||||
 | 
					const base64 = await fileDownloader.downloadAsBase64({
 | 
				
			||||||
 | 
					  url: 'https://example.com/image.jpg'
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 批量下载
 | 
				
			||||||
 | 
					const result = await fileDownloader.downloadBatch([
 | 
				
			||||||
 | 
					  'https://example.com/file1.zip',
 | 
				
			||||||
 | 
					  'https://example.com/file2.zip'
 | 
				
			||||||
 | 
					], {
 | 
				
			||||||
 | 
					  concurrent: 3,
 | 
				
			||||||
 | 
					  onProgress: (progress) => {
 | 
				
			||||||
 | 
					    console.log(`批量下载进度: ${progress}%`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## API 模块
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 编辑器API (EditorApi)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					提供编辑器相关的功能:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `createOrder(params)` - 创建订单
 | 
				
			||||||
 | 
					- `saveProject(projectData)` - 保存项目
 | 
				
			||||||
 | 
					- `loadProject(projectId)` - 加载项目
 | 
				
			||||||
 | 
					- `deleteProject(projectId)` - 删除项目
 | 
				
			||||||
 | 
					- `getProjectList(params)` - 获取项目列表
 | 
				
			||||||
 | 
					- `uploadThumbnail(file, projectId)` - 上传缩略图
 | 
				
			||||||
 | 
					- `exportProject(projectId, format)` - 导出项目
 | 
				
			||||||
 | 
					- `importProject(file)` - 导入项目
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 资源API (ResourceApi)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					提供资源管理功能:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `getResourceList(params)` - 获取资源列表
 | 
				
			||||||
 | 
					- `getResourceDetail(resourceId)` - 获取资源详情
 | 
				
			||||||
 | 
					- `uploadResource(file, metadata)` - 上传资源
 | 
				
			||||||
 | 
					- `uploadMultipleResources(files, metadata)` - 批量上传资源
 | 
				
			||||||
 | 
					- `deleteResource(resourceId)` - 删除资源
 | 
				
			||||||
 | 
					- `downloadResource(resource)` - 下载资源
 | 
				
			||||||
 | 
					- `searchResources(params)` - 搜索资源
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 配置
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 环境变量
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					# .env.development
 | 
				
			||||||
 | 
					VITE_BASE_URL=http://localhost:3000/api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# .env.production
 | 
				
			||||||
 | 
					VITE_BASE_URL=https://api.example.com
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### API配置
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					// src/api/config/index.ts
 | 
				
			||||||
 | 
					export const API_CONFIG = {
 | 
				
			||||||
 | 
					  BASE_URL: import.meta.env.VITE_BASE_URL,
 | 
				
			||||||
 | 
					  TIMEOUT: 15000,
 | 
				
			||||||
 | 
					  RETRY_COUNT: 3,
 | 
				
			||||||
 | 
					  RETRY_DELAY: 1000,
 | 
				
			||||||
 | 
					  // ...其他配置
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 错误处理
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					API会自动处理常见的错误情况:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- 网络连接失败
 | 
				
			||||||
 | 
					- 请求超时
 | 
				
			||||||
 | 
					- 认证失败
 | 
				
			||||||
 | 
					- 权限不足
 | 
				
			||||||
 | 
					- 服务器错误
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					所有API方法都返回统一的响应格式:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					interface ApiResponse<T> {
 | 
				
			||||||
 | 
					  code: number
 | 
				
			||||||
 | 
					  message: string
 | 
				
			||||||
 | 
					  data: T
 | 
				
			||||||
 | 
					  success: boolean
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 扩展性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 添加新的API模块
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. 在 `src/api/modules/` 目录下创建新的模块文件
 | 
				
			||||||
 | 
					2. 定义相关的类型接口
 | 
				
			||||||
 | 
					3. 实现API类
 | 
				
			||||||
 | 
					4. 在 `src/api/index.ts` 中导出
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 添加新的请求拦截器
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					import { httpClient } from '@/api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 添加请求拦截器
 | 
				
			||||||
 | 
					httpClient.instance.interceptors.request.use(
 | 
				
			||||||
 | 
					  (config) => {
 | 
				
			||||||
 | 
					    // 自定义请求处理
 | 
				
			||||||
 | 
					    return config
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  (error) => {
 | 
				
			||||||
 | 
					    return Promise.reject(error)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 迁移指南
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 从旧API迁移
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					旧的API调用:
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					// 旧方式
 | 
				
			||||||
 | 
					const request = new Request()
 | 
				
			||||||
 | 
					request.post(new ParamsReq('/api/endpoint', data))
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					新的API调用:
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					// 新方式
 | 
				
			||||||
 | 
					import { httpClient } from '@/api'
 | 
				
			||||||
 | 
					const response = await httpClient.post('/api/endpoint', data)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 兼容性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					新的API架构与旧代码完全分离,可以逐步迁移,不会影响现有功能。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 最佳实践
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **使用TypeScript类型**:充分利用类型定义,确保代码的类型安全
 | 
				
			||||||
 | 
					2. **错误处理**:始终处理API调用可能出现的错误
 | 
				
			||||||
 | 
					3. **进度监控**:对于文件上传下载,使用进度回调提升用户体验
 | 
				
			||||||
 | 
					4. **请求取消**:在组件卸载时取消未完成的请求
 | 
				
			||||||
 | 
					5. **缓存策略**:对于不经常变化的数据,考虑实现缓存机制
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 性能优化
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **并发控制**:批量操作时控制并发数量
 | 
				
			||||||
 | 
					2. **分片上传**:大文件使用分片上传
 | 
				
			||||||
 | 
					3. **压缩优化**:图片上传时自动压缩
 | 
				
			||||||
 | 
					4. **请求去重**:避免重复请求
 | 
				
			||||||
 | 
					5. **缓存机制**:合理使用缓存减少网络请求
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 示例代码
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					详细的使用示例请参考 `src/api/examples/usage.ts` 文件。 
 | 
				
			||||||
							
								
								
									
										83
									
								
								src/api/config/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/api/config/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					// API 基础配置
 | 
				
			||||||
 | 
					export const API_CONFIG = {
 | 
				
			||||||
 | 
					  // 基础URL
 | 
				
			||||||
 | 
					  BASE_URL: import.meta.env.VITE_BASE_URL || 'https://script.jiaweijia.cn/script',
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 超时时间
 | 
				
			||||||
 | 
					  TIMEOUT: 15000,
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 重试次数
 | 
				
			||||||
 | 
					  RETRY_COUNT: 3,
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 重试延迟(毫秒)
 | 
				
			||||||
 | 
					  RETRY_DELAY: 1000,
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 默认请求头
 | 
				
			||||||
 | 
					  DEFAULT_HEADERS: {
 | 
				
			||||||
 | 
					    'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					    'Accept': 'application/json',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 文件上传请求头
 | 
				
			||||||
 | 
					  UPLOAD_HEADERS: {
 | 
				
			||||||
 | 
					    'Content-Type': 'multipart/form-data',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 响应状态码
 | 
				
			||||||
 | 
					  STATUS_CODES: {
 | 
				
			||||||
 | 
					    SUCCESS: 200,
 | 
				
			||||||
 | 
					    UNAUTHORIZED: 401,
 | 
				
			||||||
 | 
					    FORBIDDEN: 403,
 | 
				
			||||||
 | 
					    NOT_FOUND: 404,
 | 
				
			||||||
 | 
					    SERVER_ERROR: 500,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 业务状态码
 | 
				
			||||||
 | 
					  BUSINESS_CODES: {
 | 
				
			||||||
 | 
					    SUCCESS: 0,
 | 
				
			||||||
 | 
					    FAIL: -1,
 | 
				
			||||||
 | 
					    TOKEN_EXPIRED: 401,
 | 
				
			||||||
 | 
					    PERMISSION_DENIED: 403,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 环境配置
 | 
				
			||||||
 | 
					export const ENV_CONFIG = {
 | 
				
			||||||
 | 
					  isDev: import.meta.env.DEV,
 | 
				
			||||||
 | 
					  isProd: import.meta.env.PROD,
 | 
				
			||||||
 | 
					  mode: import.meta.env.MODE,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// API 端点配置
 | 
				
			||||||
 | 
					export const API_ENDPOINTS = {
 | 
				
			||||||
 | 
					  // 编辑器相关
 | 
				
			||||||
 | 
					  EDITOR: {
 | 
				
			||||||
 | 
					    CREATE_ORDER: '/cpq/3d/create/order',
 | 
				
			||||||
 | 
					    SAVE_PROJECT: '/editor/project/save',
 | 
				
			||||||
 | 
					    LOAD_PROJECT: '/editor/project/load',
 | 
				
			||||||
 | 
					    DELETE_PROJECT: '/editor/project/delete',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 文件相关
 | 
				
			||||||
 | 
					  FILE: {
 | 
				
			||||||
 | 
					    UPLOAD: '/file/upload',
 | 
				
			||||||
 | 
					    DOWNLOAD: '/file/download',
 | 
				
			||||||
 | 
					    DELETE: '/file/delete',
 | 
				
			||||||
 | 
					    LIST: '/file/list',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 用户相关
 | 
				
			||||||
 | 
					  USER: {
 | 
				
			||||||
 | 
					    LOGIN: '/user/login',
 | 
				
			||||||
 | 
					    LOGOUT: '/user/logout',
 | 
				
			||||||
 | 
					    INFO: '/user/info',
 | 
				
			||||||
 | 
					    UPDATE: '/user/update',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 资源相关
 | 
				
			||||||
 | 
					  RESOURCE: {
 | 
				
			||||||
 | 
					    LIST: '/resource/list',
 | 
				
			||||||
 | 
					    UPLOAD: '/resource/upload',
 | 
				
			||||||
 | 
					    DELETE: '/resource/delete',
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
							
								
								
									
										377
									
								
								src/api/core/download.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										377
									
								
								src/api/core/download.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,377 @@
 | 
				
			|||||||
 | 
					import { httpClient } from './request'
 | 
				
			||||||
 | 
					import { DownloadConfig, ApiResponse } from '../types'
 | 
				
			||||||
 | 
					import { API_ENDPOINTS } from '../config'
 | 
				
			||||||
 | 
					import { downloadFile, generateId } from '../utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 下载响应类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface DownloadResponse {
 | 
				
			||||||
 | 
					  blob: Blob
 | 
				
			||||||
 | 
					  filename: string
 | 
				
			||||||
 | 
					  size: number
 | 
				
			||||||
 | 
					  type: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 批量下载响应类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface BatchDownloadResponse {
 | 
				
			||||||
 | 
					  success: DownloadResponse[]
 | 
				
			||||||
 | 
					  failed: Array<{
 | 
				
			||||||
 | 
					    url: string
 | 
				
			||||||
 | 
					    error: string
 | 
				
			||||||
 | 
					  }>
 | 
				
			||||||
 | 
					  total: number
 | 
				
			||||||
 | 
					  successCount: number
 | 
				
			||||||
 | 
					  failedCount: number
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 文件下载类
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class FileDownloader {
 | 
				
			||||||
 | 
					  private downloadQueue: Map<string, AbortController> = new Map()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 下载文件
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadFile(config: DownloadConfig): Promise<DownloadResponse> {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      filename,
 | 
				
			||||||
 | 
					      params,
 | 
				
			||||||
 | 
					      onProgress
 | 
				
			||||||
 | 
					    } = config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 生成下载ID
 | 
				
			||||||
 | 
					    const downloadId = generateId()
 | 
				
			||||||
 | 
					    const controller = new AbortController()
 | 
				
			||||||
 | 
					    this.downloadQueue.set(downloadId, controller)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await httpClient.request<Blob>({
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        method: 'GET',
 | 
				
			||||||
 | 
					        params,
 | 
				
			||||||
 | 
					        responseType: 'blob',
 | 
				
			||||||
 | 
					        timeout: 0, // 下载不设置超时
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!response.success) {
 | 
				
			||||||
 | 
					        throw new Error(response.message)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const blob = response.data
 | 
				
			||||||
 | 
					      const contentType = blob.type || 'application/octet-stream'
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // 从响应头或URL中获取文件名
 | 
				
			||||||
 | 
					      const finalFilename = filename || this.extractFilenameFromUrl(url) || 'download'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        blob,
 | 
				
			||||||
 | 
					        filename: finalFilename,
 | 
				
			||||||
 | 
					        size: blob.size,
 | 
				
			||||||
 | 
					        type: contentType
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      this.downloadQueue.delete(downloadId)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 下载并保存文件
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadAndSave(config: DownloadConfig): Promise<void> {
 | 
				
			||||||
 | 
					    const result = await this.downloadFile(config)
 | 
				
			||||||
 | 
					    downloadFile(result.blob, result.filename)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 下载文件为Base64
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadAsBase64(config: DownloadConfig): Promise<string> {
 | 
				
			||||||
 | 
					    const result = await this.downloadFile(config)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					      const reader = new FileReader()
 | 
				
			||||||
 | 
					      reader.onload = () => {
 | 
				
			||||||
 | 
					        const base64 = reader.result as string
 | 
				
			||||||
 | 
					        resolve(base64.split(',')[1]) // 移除data:xxx;base64,前缀
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      reader.onerror = reject
 | 
				
			||||||
 | 
					      reader.readAsDataURL(result.blob)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 下载文件为ArrayBuffer
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadAsArrayBuffer(config: DownloadConfig): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    const result = await this.downloadFile(config)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					      const reader = new FileReader()
 | 
				
			||||||
 | 
					      reader.onload = () => resolve(reader.result as ArrayBuffer)
 | 
				
			||||||
 | 
					      reader.onerror = reject
 | 
				
			||||||
 | 
					      reader.readAsArrayBuffer(result.blob)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 下载文件为文本
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadAsText(config: DownloadConfig): Promise<string> {
 | 
				
			||||||
 | 
					    const result = await this.downloadFile(config)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					      const reader = new FileReader()
 | 
				
			||||||
 | 
					      reader.onload = () => resolve(reader.result as string)
 | 
				
			||||||
 | 
					      reader.onerror = reject
 | 
				
			||||||
 | 
					      reader.readAsText(result.blob)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 分片下载(大文件)
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadLargeFile(
 | 
				
			||||||
 | 
					    config: DownloadConfig & {
 | 
				
			||||||
 | 
					      chunkSize?: number
 | 
				
			||||||
 | 
					      onChunkProgress?: (chunkIndex: number, chunkProgress: number) => void
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ): Promise<DownloadResponse> {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      filename,
 | 
				
			||||||
 | 
					      params,
 | 
				
			||||||
 | 
					      chunkSize = 2 * 1024 * 1024, // 2MB per chunk
 | 
				
			||||||
 | 
					      onProgress,
 | 
				
			||||||
 | 
					      onChunkProgress
 | 
				
			||||||
 | 
					    } = config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // 获取文件信息
 | 
				
			||||||
 | 
					      const headResponse = await httpClient.request({
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        method: 'HEAD',
 | 
				
			||||||
 | 
					        params
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!headResponse.success) {
 | 
				
			||||||
 | 
					        throw new Error('无法获取文件信息')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 这里需要根据实际的响应头来获取文件大小
 | 
				
			||||||
 | 
					      // const fileSize = parseInt(headResponse.headers['content-length'] || '0')
 | 
				
			||||||
 | 
					      const fileSize = 0 // 临时设置,实际应该从响应头获取
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (fileSize === 0) {
 | 
				
			||||||
 | 
					        // 如果无法获取文件大小,回退到普通下载
 | 
				
			||||||
 | 
					        return this.downloadFile(config)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const totalChunks = Math.ceil(fileSize / chunkSize)
 | 
				
			||||||
 | 
					      const chunks: Blob[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 下载分片
 | 
				
			||||||
 | 
					      for (let i = 0; i < totalChunks; i++) {
 | 
				
			||||||
 | 
					        const start = i * chunkSize
 | 
				
			||||||
 | 
					        const end = Math.min(start + chunkSize - 1, fileSize - 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const chunkResponse = await httpClient.request<Blob>({
 | 
				
			||||||
 | 
					          url,
 | 
				
			||||||
 | 
					          method: 'GET',
 | 
				
			||||||
 | 
					          params,
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Range': `bytes=${start}-${end}`
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          responseType: 'blob'
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (chunkResponse.success) {
 | 
				
			||||||
 | 
					          chunks.push(chunkResponse.data)
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // 分片进度回调
 | 
				
			||||||
 | 
					          onChunkProgress?.(i, 100)
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // 总进度回调
 | 
				
			||||||
 | 
					          const progress = ((i + 1) / totalChunks) * 100
 | 
				
			||||||
 | 
					          onProgress?.(progress)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          throw new Error(`分片 ${i} 下载失败: ${chunkResponse.message}`)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 合并分片
 | 
				
			||||||
 | 
					      const blob = new Blob(chunks)
 | 
				
			||||||
 | 
					      const finalFilename = filename || this.extractFilenameFromUrl(url) || 'download'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        blob,
 | 
				
			||||||
 | 
					        filename: finalFilename,
 | 
				
			||||||
 | 
					        size: blob.size,
 | 
				
			||||||
 | 
					        type: blob.type || 'application/octet-stream'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      throw new Error(`分片下载失败: ${error}`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 批量下载
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadBatch(
 | 
				
			||||||
 | 
					    urls: string[],
 | 
				
			||||||
 | 
					    options: {
 | 
				
			||||||
 | 
					      concurrent?: number
 | 
				
			||||||
 | 
					      onProgress?: (progress: number) => void
 | 
				
			||||||
 | 
					      onFileProgress?: (url: string, progress: number) => void
 | 
				
			||||||
 | 
					      onFileComplete?: (url: string, result: DownloadResponse | Error) => void
 | 
				
			||||||
 | 
					    } = {}
 | 
				
			||||||
 | 
					  ): Promise<BatchDownloadResponse> {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      concurrent = 3,
 | 
				
			||||||
 | 
					      onProgress,
 | 
				
			||||||
 | 
					      onFileProgress,
 | 
				
			||||||
 | 
					      onFileComplete
 | 
				
			||||||
 | 
					    } = options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const results: BatchDownloadResponse = {
 | 
				
			||||||
 | 
					      success: [],
 | 
				
			||||||
 | 
					      failed: [],
 | 
				
			||||||
 | 
					      total: urls.length,
 | 
				
			||||||
 | 
					      successCount: 0,
 | 
				
			||||||
 | 
					      failedCount: 0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const downloadUrl = async (url: string): Promise<void> => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await this.downloadFile({
 | 
				
			||||||
 | 
					          url,
 | 
				
			||||||
 | 
					          onProgress: (progress) => onFileProgress?.(url, progress)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        results.success.push(response)
 | 
				
			||||||
 | 
					        results.successCount++
 | 
				
			||||||
 | 
					        onFileComplete?.(url, response)
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        results.failed.push({
 | 
				
			||||||
 | 
					          url,
 | 
				
			||||||
 | 
					          error: error instanceof Error ? error.message : '下载失败'
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        results.failedCount++
 | 
				
			||||||
 | 
					        onFileComplete?.(url, error as Error)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 更新总进度
 | 
				
			||||||
 | 
					      const completed = results.successCount + results.failedCount
 | 
				
			||||||
 | 
					      const progress = (completed / results.total) * 100
 | 
				
			||||||
 | 
					      onProgress?.(progress)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 并发下载
 | 
				
			||||||
 | 
					    const chunks = []
 | 
				
			||||||
 | 
					    for (let i = 0; i < urls.length; i += concurrent) {
 | 
				
			||||||
 | 
					      chunks.push(urls.slice(i, i + concurrent))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const chunk of chunks) {
 | 
				
			||||||
 | 
					      await Promise.all(chunk.map(downloadUrl))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return results
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 批量下载并打包为ZIP
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadAsZip(
 | 
				
			||||||
 | 
					    files: Array<{
 | 
				
			||||||
 | 
					      url: string
 | 
				
			||||||
 | 
					      filename?: string
 | 
				
			||||||
 | 
					    }>,
 | 
				
			||||||
 | 
					    zipFilename: string = 'download.zip'
 | 
				
			||||||
 | 
					  ): Promise<void> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // 下载所有文件
 | 
				
			||||||
 | 
					      const downloadPromises = files.map(async (file) => {
 | 
				
			||||||
 | 
					        const result = await this.downloadFile({
 | 
				
			||||||
 | 
					          url: file.url,
 | 
				
			||||||
 | 
					          filename: file.filename
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          filename: result.filename,
 | 
				
			||||||
 | 
					          blob: result.blob
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const downloadedFiles = await Promise.all(downloadPromises)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 这里需要使用JSZip库来创建ZIP文件
 | 
				
			||||||
 | 
					      // 由于没有导入JSZip,这里只是示例代码
 | 
				
			||||||
 | 
					      console.log('需要集成JSZip库来实现ZIP打包功能')
 | 
				
			||||||
 | 
					      console.log('下载的文件:', downloadedFiles)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 示例:如果有JSZip
 | 
				
			||||||
 | 
					      // const JSZip = (await import('jszip')).default
 | 
				
			||||||
 | 
					      // const zip = new JSZip()
 | 
				
			||||||
 | 
					      // 
 | 
				
			||||||
 | 
					      // downloadedFiles.forEach(file => {
 | 
				
			||||||
 | 
					      //   zip.file(file.filename, file.blob)
 | 
				
			||||||
 | 
					      // })
 | 
				
			||||||
 | 
					      // 
 | 
				
			||||||
 | 
					      // const zipBlob = await zip.generateAsync({ type: 'blob' })
 | 
				
			||||||
 | 
					      // downloadFile(zipBlob, zipFilename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      throw new Error(`ZIP下载失败: ${error}`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 从URL中提取文件名
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private extractFilenameFromUrl(url: string): string {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const urlObj = new URL(url)
 | 
				
			||||||
 | 
					      const pathname = urlObj.pathname
 | 
				
			||||||
 | 
					      const filename = pathname.split('/').pop()
 | 
				
			||||||
 | 
					      return filename || 'download'
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      return 'download'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 取消下载
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public cancelDownload(downloadId: string): void {
 | 
				
			||||||
 | 
					    const controller = this.downloadQueue.get(downloadId)
 | 
				
			||||||
 | 
					    if (controller) {
 | 
				
			||||||
 | 
					      controller.abort()
 | 
				
			||||||
 | 
					      this.downloadQueue.delete(downloadId)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 取消所有下载
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public cancelAllDownloads(): void {
 | 
				
			||||||
 | 
					    this.downloadQueue.forEach((controller) => {
 | 
				
			||||||
 | 
					      controller.abort()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    this.downloadQueue.clear()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取下载进度
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public getDownloadProgress(downloadId: string): number {
 | 
				
			||||||
 | 
					    // 这里可以实现获取具体下载进度的逻辑
 | 
				
			||||||
 | 
					    return 0
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建默认实例
 | 
				
			||||||
 | 
					export const fileDownloader = new FileDownloader() 
 | 
				
			||||||
							
								
								
									
										344
									
								
								src/api/core/request.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								src/api/core/request.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,344 @@
 | 
				
			|||||||
 | 
					import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
 | 
				
			||||||
 | 
					import { ApiResponse, RequestConfig, ApiError } from '../types'
 | 
				
			||||||
 | 
					import { API_CONFIG, ENV_CONFIG } from '../config'
 | 
				
			||||||
 | 
					import { createApiError, retry } from '../utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * HTTP请求客户端类
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class HttpClient {
 | 
				
			||||||
 | 
					  private instance: AxiosInstance
 | 
				
			||||||
 | 
					  private requestQueue: Map<string, AbortController> = new Map()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    this.instance = this.createInstance()
 | 
				
			||||||
 | 
					    this.setupInterceptors()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 创建axios实例
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private createInstance(): AxiosInstance {
 | 
				
			||||||
 | 
					    return axios.create({
 | 
				
			||||||
 | 
					      baseURL: API_CONFIG.BASE_URL,
 | 
				
			||||||
 | 
					      timeout: API_CONFIG.TIMEOUT,
 | 
				
			||||||
 | 
					      headers: API_CONFIG.DEFAULT_HEADERS,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 设置拦截器
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private setupInterceptors(): void {
 | 
				
			||||||
 | 
					    // 请求拦截器
 | 
				
			||||||
 | 
					    this.instance.interceptors.request.use(
 | 
				
			||||||
 | 
					      (config) => {
 | 
				
			||||||
 | 
					        // 添加请求ID用于取消请求
 | 
				
			||||||
 | 
					        const requestId = this.generateRequestId(config)
 | 
				
			||||||
 | 
					        config.metadata = { requestId }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 添加认证token
 | 
				
			||||||
 | 
					        const token = this.getAuthToken()
 | 
				
			||||||
 | 
					        if (token && config.headers) {
 | 
				
			||||||
 | 
					          config.headers.Authorization = `Bearer ${token}`
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 开发环境下打印请求信息
 | 
				
			||||||
 | 
					        if (ENV_CONFIG.isDev) {
 | 
				
			||||||
 | 
					          console.log('🚀 Request:', {
 | 
				
			||||||
 | 
					            url: config.url,
 | 
				
			||||||
 | 
					            method: config.method,
 | 
				
			||||||
 | 
					            params: config.params,
 | 
				
			||||||
 | 
					            data: config.data,
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return config
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      (error) => {
 | 
				
			||||||
 | 
					        return Promise.reject(this.handleError(error))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 响应拦截器
 | 
				
			||||||
 | 
					    this.instance.interceptors.response.use(
 | 
				
			||||||
 | 
					      (response) => {
 | 
				
			||||||
 | 
					        // 清理请求队列
 | 
				
			||||||
 | 
					        const requestId = response.config.metadata?.requestId
 | 
				
			||||||
 | 
					        if (requestId) {
 | 
				
			||||||
 | 
					          this.requestQueue.delete(requestId)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 开发环境下打印响应信息
 | 
				
			||||||
 | 
					        if (ENV_CONFIG.isDev) {
 | 
				
			||||||
 | 
					          console.log('✅ Response:', {
 | 
				
			||||||
 | 
					            url: response.config.url,
 | 
				
			||||||
 | 
					            status: response.status,
 | 
				
			||||||
 | 
					            data: response.data,
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 直接返回response,在具体的请求方法中处理数据格式
 | 
				
			||||||
 | 
					        return response
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      (error) => {
 | 
				
			||||||
 | 
					        // 清理请求队列
 | 
				
			||||||
 | 
					        const requestId = error.config?.metadata?.requestId
 | 
				
			||||||
 | 
					        if (requestId) {
 | 
				
			||||||
 | 
					          this.requestQueue.delete(requestId)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Promise.reject(this.handleError(error))
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 处理响应数据
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private handleResponse<T>(response: AxiosResponse): ApiResponse<T> {
 | 
				
			||||||
 | 
					    const { data } = response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 如果后端返回的数据已经是标准格式
 | 
				
			||||||
 | 
					    if (data && typeof data.code !== 'undefined') {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        code: data.code,
 | 
				
			||||||
 | 
					        message: data.message || 'Success',
 | 
				
			||||||
 | 
					        data: data.data,
 | 
				
			||||||
 | 
					        success: data.code === API_CONFIG.BUSINESS_CODES.SUCCESS,
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 否则包装成标准格式
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      code: API_CONFIG.BUSINESS_CODES.SUCCESS,
 | 
				
			||||||
 | 
					      message: 'Success',
 | 
				
			||||||
 | 
					      data: data,
 | 
				
			||||||
 | 
					      success: true,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 处理错误
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private handleError(error: any): ApiError {
 | 
				
			||||||
 | 
					    if (ENV_CONFIG.isDev) {
 | 
				
			||||||
 | 
					      console.error('❌ Request Error:', error)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 请求被取消
 | 
				
			||||||
 | 
					    if (axios.isCancel(error)) {
 | 
				
			||||||
 | 
					      return createApiError(-1, '请求已取消')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 网络错误
 | 
				
			||||||
 | 
					    if (!error.response) {
 | 
				
			||||||
 | 
					      return createApiError(-1, '网络连接失败,请检查网络设置')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { status, data } = error.response as AxiosResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 根据HTTP状态码处理
 | 
				
			||||||
 | 
					    switch (status) {
 | 
				
			||||||
 | 
					      case API_CONFIG.STATUS_CODES.UNAUTHORIZED:
 | 
				
			||||||
 | 
					        this.handleUnauthorized()
 | 
				
			||||||
 | 
					        return createApiError(status, '登录已过期,请重新登录')
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      case API_CONFIG.STATUS_CODES.FORBIDDEN:
 | 
				
			||||||
 | 
					        return createApiError(status, '没有权限访问该资源')
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      case API_CONFIG.STATUS_CODES.NOT_FOUND:
 | 
				
			||||||
 | 
					        return createApiError(status, '请求的资源不存在')
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      case API_CONFIG.STATUS_CODES.SERVER_ERROR:
 | 
				
			||||||
 | 
					        return createApiError(status, '服务器内部错误')
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return createApiError(
 | 
				
			||||||
 | 
					          status,
 | 
				
			||||||
 | 
					          data?.message || `请求失败 (${status})`,
 | 
				
			||||||
 | 
					          data
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 处理未授权
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private handleUnauthorized(): void {
 | 
				
			||||||
 | 
					    // 清除token
 | 
				
			||||||
 | 
					    this.clearAuthToken()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 可以在这里添加跳转到登录页的逻辑
 | 
				
			||||||
 | 
					    // router.push('/login')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 生成请求ID
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private generateRequestId(config: AxiosRequestConfig): string {
 | 
				
			||||||
 | 
					    return `${config.method}_${config.url}_${Date.now()}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取认证token
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private getAuthToken(): string | null {
 | 
				
			||||||
 | 
					    return localStorage.getItem('auth_token')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 清除认证token
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private clearAuthToken(): void {
 | 
				
			||||||
 | 
					    localStorage.removeItem('auth_token')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 设置认证token
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public setAuthToken(token: string): void {
 | 
				
			||||||
 | 
					    localStorage.setItem('auth_token', token)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 通用请求方法
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
 | 
				
			||||||
 | 
					    const axiosConfig: AxiosRequestConfig = {
 | 
				
			||||||
 | 
					      url: config.url,
 | 
				
			||||||
 | 
					      method: config.method || 'GET',
 | 
				
			||||||
 | 
					      params: config.params,
 | 
				
			||||||
 | 
					      data: config.data,
 | 
				
			||||||
 | 
					      headers: config.headers,
 | 
				
			||||||
 | 
					      timeout: config.timeout,
 | 
				
			||||||
 | 
					      responseType: config.responseType || 'json',
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await this.instance.request(axiosConfig)
 | 
				
			||||||
 | 
					    return this.handleResponse<T>(response)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 带重试的请求方法
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async requestWithRetry<T = any>(
 | 
				
			||||||
 | 
					    config: RequestConfig,
 | 
				
			||||||
 | 
					    retries?: number
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<T>> {
 | 
				
			||||||
 | 
					    return retry(() => this.request<T>(config), retries)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * GET请求
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public get<T = any>(
 | 
				
			||||||
 | 
					    url: string,
 | 
				
			||||||
 | 
					    params?: Record<string, any>,
 | 
				
			||||||
 | 
					    config?: Partial<RequestConfig>
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<T>> {
 | 
				
			||||||
 | 
					    return this.request<T>({
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      method: 'GET',
 | 
				
			||||||
 | 
					      params,
 | 
				
			||||||
 | 
					      ...config,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * POST请求
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public post<T = any>(
 | 
				
			||||||
 | 
					    url: string,
 | 
				
			||||||
 | 
					    data?: any,
 | 
				
			||||||
 | 
					    config?: Partial<RequestConfig>
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<T>> {
 | 
				
			||||||
 | 
					    return this.request<T>({
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      data,
 | 
				
			||||||
 | 
					      ...config,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * PUT请求
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public put<T = any>(
 | 
				
			||||||
 | 
					    url: string,
 | 
				
			||||||
 | 
					    data?: any,
 | 
				
			||||||
 | 
					    config?: Partial<RequestConfig>
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<T>> {
 | 
				
			||||||
 | 
					    return this.request<T>({
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      method: 'PUT',
 | 
				
			||||||
 | 
					      data,
 | 
				
			||||||
 | 
					      ...config,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * DELETE请求
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public delete<T = any>(
 | 
				
			||||||
 | 
					    url: string,
 | 
				
			||||||
 | 
					    params?: Record<string, any>,
 | 
				
			||||||
 | 
					    config?: Partial<RequestConfig>
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<T>> {
 | 
				
			||||||
 | 
					    return this.request<T>({
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      method: 'DELETE',
 | 
				
			||||||
 | 
					      params,
 | 
				
			||||||
 | 
					      ...config,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * PATCH请求
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public patch<T = any>(
 | 
				
			||||||
 | 
					    url: string,
 | 
				
			||||||
 | 
					    data?: any,
 | 
				
			||||||
 | 
					    config?: Partial<RequestConfig>
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<T>> {
 | 
				
			||||||
 | 
					    return this.request<T>({
 | 
				
			||||||
 | 
					      url,
 | 
				
			||||||
 | 
					      method: 'PATCH',
 | 
				
			||||||
 | 
					      data,
 | 
				
			||||||
 | 
					      ...config,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 取消请求
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public cancelRequest(requestId: string): void {
 | 
				
			||||||
 | 
					    const controller = this.requestQueue.get(requestId)
 | 
				
			||||||
 | 
					    if (controller) {
 | 
				
			||||||
 | 
					      controller.abort()
 | 
				
			||||||
 | 
					      this.requestQueue.delete(requestId)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 取消所有请求
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public cancelAllRequests(): void {
 | 
				
			||||||
 | 
					    this.requestQueue.forEach((controller) => {
 | 
				
			||||||
 | 
					      controller.abort()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    this.requestQueue.clear()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建默认实例
 | 
				
			||||||
 | 
					export const httpClient = new HttpClient()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 扩展axios配置类型
 | 
				
			||||||
 | 
					declare module 'axios' {
 | 
				
			||||||
 | 
					  interface AxiosRequestConfig {
 | 
				
			||||||
 | 
					    metadata?: {
 | 
				
			||||||
 | 
					      requestId: string
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
							
								
								
									
										362
									
								
								src/api/core/upload.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								src/api/core/upload.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,362 @@
 | 
				
			|||||||
 | 
					import { AxiosRequestConfig } from 'axios'
 | 
				
			||||||
 | 
					import { httpClient } from './request'
 | 
				
			||||||
 | 
					import { UploadConfig, ApiResponse } from '../types'
 | 
				
			||||||
 | 
					import { API_CONFIG, API_ENDPOINTS } from '../config'
 | 
				
			||||||
 | 
					import { 
 | 
				
			||||||
 | 
					  validateFileType, 
 | 
				
			||||||
 | 
					  formatFileSize, 
 | 
				
			||||||
 | 
					  compressImage, 
 | 
				
			||||||
 | 
					  readFileAsBase64,
 | 
				
			||||||
 | 
					  generateId 
 | 
				
			||||||
 | 
					} from '../utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 文件上传响应类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface UploadResponse {
 | 
				
			||||||
 | 
					  fileId: string
 | 
				
			||||||
 | 
					  filename: string
 | 
				
			||||||
 | 
					  url: string
 | 
				
			||||||
 | 
					  size: number
 | 
				
			||||||
 | 
					  type: string
 | 
				
			||||||
 | 
					  uploadTime: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 批量上传响应类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface BatchUploadResponse {
 | 
				
			||||||
 | 
					  success: UploadResponse[]
 | 
				
			||||||
 | 
					  failed: Array<{
 | 
				
			||||||
 | 
					    file: File
 | 
				
			||||||
 | 
					    error: string
 | 
				
			||||||
 | 
					  }>
 | 
				
			||||||
 | 
					  total: number
 | 
				
			||||||
 | 
					  successCount: number
 | 
				
			||||||
 | 
					  failedCount: number
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 文件上传类
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class FileUploader {
 | 
				
			||||||
 | 
					  private uploadQueue: Map<string, AbortController> = new Map()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 单文件上传
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async uploadFile(config: UploadConfig): Promise<ApiResponse<UploadResponse>> {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      file,
 | 
				
			||||||
 | 
					      filename = file instanceof File ? file.name : 'blob',
 | 
				
			||||||
 | 
					      fieldName = 'file',
 | 
				
			||||||
 | 
					      data = {},
 | 
				
			||||||
 | 
					      onProgress
 | 
				
			||||||
 | 
					    } = config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 创建FormData
 | 
				
			||||||
 | 
					    const formData = new FormData()
 | 
				
			||||||
 | 
					    formData.append(fieldName, file, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 添加额外数据
 | 
				
			||||||
 | 
					    Object.keys(data).forEach(key => {
 | 
				
			||||||
 | 
					      formData.append(key, data[key])
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 生成上传ID
 | 
				
			||||||
 | 
					    const uploadId = generateId()
 | 
				
			||||||
 | 
					    const controller = new AbortController()
 | 
				
			||||||
 | 
					    this.uploadQueue.set(uploadId, controller)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await httpClient.request<UploadResponse>({
 | 
				
			||||||
 | 
					        url: API_ENDPOINTS.FILE.UPLOAD,
 | 
				
			||||||
 | 
					        method: 'POST',
 | 
				
			||||||
 | 
					        data: formData,
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          'Content-Type': 'multipart/form-data',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        timeout: 0, // 上传不设置超时
 | 
				
			||||||
 | 
					        responseType: 'json',
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return response
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      this.uploadQueue.delete(uploadId)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 图片上传(带压缩)
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async uploadImage(
 | 
				
			||||||
 | 
					    file: File,
 | 
				
			||||||
 | 
					    options: {
 | 
				
			||||||
 | 
					      compress?: boolean
 | 
				
			||||||
 | 
					      quality?: number
 | 
				
			||||||
 | 
					      maxWidth?: number
 | 
				
			||||||
 | 
					      maxHeight?: number
 | 
				
			||||||
 | 
					      allowedTypes?: string[]
 | 
				
			||||||
 | 
					      maxSize?: number
 | 
				
			||||||
 | 
					      onProgress?: (progress: number) => void
 | 
				
			||||||
 | 
					    } = {}
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<UploadResponse>> {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      compress = true,
 | 
				
			||||||
 | 
					      quality = 0.8,
 | 
				
			||||||
 | 
					      maxWidth = 1920,
 | 
				
			||||||
 | 
					      maxHeight = 1080,
 | 
				
			||||||
 | 
					      allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'],
 | 
				
			||||||
 | 
					      maxSize = 10 * 1024 * 1024, // 10MB
 | 
				
			||||||
 | 
					      onProgress
 | 
				
			||||||
 | 
					    } = options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 验证文件类型
 | 
				
			||||||
 | 
					    if (!validateFileType(file, allowedTypes)) {
 | 
				
			||||||
 | 
					      throw new Error(`不支持的文件类型,仅支持: ${allowedTypes.join(', ')}`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 验证文件大小
 | 
				
			||||||
 | 
					    if (file.size > maxSize) {
 | 
				
			||||||
 | 
					      throw new Error(`文件大小超出限制,最大支持: ${formatFileSize(maxSize)}`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let uploadFile = file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 压缩图片
 | 
				
			||||||
 | 
					    if (compress && file.type.startsWith('image/')) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const compressedBlob = await compressImage(file, quality, maxWidth, maxHeight)
 | 
				
			||||||
 | 
					        uploadFile = new File([compressedBlob], file.name, { type: file.type })
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.warn('图片压缩失败,使用原文件上传:', error)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.uploadFile({
 | 
				
			||||||
 | 
					      file: uploadFile,
 | 
				
			||||||
 | 
					      onProgress
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Base64上传
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async uploadBase64(
 | 
				
			||||||
 | 
					    file: File,
 | 
				
			||||||
 | 
					    options: {
 | 
				
			||||||
 | 
					      onProgress?: (progress: number) => void
 | 
				
			||||||
 | 
					    } = {}
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<UploadResponse>> {
 | 
				
			||||||
 | 
					    const { onProgress } = options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const base64 = await readFileAsBase64(file)
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      const response = await httpClient.post<UploadResponse>(
 | 
				
			||||||
 | 
					        API_ENDPOINTS.FILE.UPLOAD,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          filename: file.name,
 | 
				
			||||||
 | 
					          content: base64,
 | 
				
			||||||
 | 
					          type: file.type,
 | 
				
			||||||
 | 
					          size: file.size,
 | 
				
			||||||
 | 
					          encoding: 'base64'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return response
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      throw new Error(`Base64上传失败: ${error}`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 分片上传(大文件)
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async uploadLargeFile(
 | 
				
			||||||
 | 
					    file: File,
 | 
				
			||||||
 | 
					    options: {
 | 
				
			||||||
 | 
					      chunkSize?: number
 | 
				
			||||||
 | 
					      onProgress?: (progress: number) => void
 | 
				
			||||||
 | 
					      onChunkProgress?: (chunkIndex: number, chunkProgress: number) => void
 | 
				
			||||||
 | 
					    } = {}
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<UploadResponse>> {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      chunkSize = 2 * 1024 * 1024, // 2MB per chunk
 | 
				
			||||||
 | 
					      onProgress,
 | 
				
			||||||
 | 
					      onChunkProgress
 | 
				
			||||||
 | 
					    } = options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const totalChunks = Math.ceil(file.size / chunkSize)
 | 
				
			||||||
 | 
					    const uploadId = generateId()
 | 
				
			||||||
 | 
					    const uploadedChunks: string[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // 初始化分片上传
 | 
				
			||||||
 | 
					      const initResponse = await httpClient.post('/file/upload/init', {
 | 
				
			||||||
 | 
					        filename: file.name,
 | 
				
			||||||
 | 
					        fileSize: file.size,
 | 
				
			||||||
 | 
					        totalChunks,
 | 
				
			||||||
 | 
					        uploadId
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!initResponse.success) {
 | 
				
			||||||
 | 
					        throw new Error(initResponse.message)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 上传分片
 | 
				
			||||||
 | 
					      for (let i = 0; i < totalChunks; i++) {
 | 
				
			||||||
 | 
					        const start = i * chunkSize
 | 
				
			||||||
 | 
					        const end = Math.min(start + chunkSize, file.size)
 | 
				
			||||||
 | 
					        const chunk = file.slice(start, end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const formData = new FormData()
 | 
				
			||||||
 | 
					        formData.append('chunk', chunk)
 | 
				
			||||||
 | 
					        formData.append('chunkIndex', i.toString())
 | 
				
			||||||
 | 
					        formData.append('uploadId', uploadId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const chunkResponse = await httpClient.post('/file/upload/chunk', formData, {
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Content-Type': 'multipart/form-data',
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (chunkResponse.success) {
 | 
				
			||||||
 | 
					          uploadedChunks.push(chunkResponse.data.chunkId)
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // 分片进度回调
 | 
				
			||||||
 | 
					          onChunkProgress?.(i, 100)
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // 总进度回调
 | 
				
			||||||
 | 
					          const progress = ((i + 1) / totalChunks) * 100
 | 
				
			||||||
 | 
					          onProgress?.(progress)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          throw new Error(`分片 ${i} 上传失败: ${chunkResponse.message}`)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 合并分片
 | 
				
			||||||
 | 
					      const mergeResponse = await httpClient.post<UploadResponse>('/file/upload/merge', {
 | 
				
			||||||
 | 
					        uploadId,
 | 
				
			||||||
 | 
					        filename: file.name,
 | 
				
			||||||
 | 
					        chunks: uploadedChunks
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return mergeResponse
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      // 清理失败的上传
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await httpClient.post('/file/upload/cleanup', { uploadId })
 | 
				
			||||||
 | 
					      } catch (cleanupError) {
 | 
				
			||||||
 | 
					        console.warn('清理上传失败:', cleanupError)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      throw error
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 批量上传
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async uploadBatch(
 | 
				
			||||||
 | 
					    files: File[],
 | 
				
			||||||
 | 
					    options: {
 | 
				
			||||||
 | 
					      concurrent?: number
 | 
				
			||||||
 | 
					      onProgress?: (progress: number) => void
 | 
				
			||||||
 | 
					      onFileProgress?: (file: File, progress: number) => void
 | 
				
			||||||
 | 
					      onFileComplete?: (file: File, result: UploadResponse | Error) => void
 | 
				
			||||||
 | 
					    } = {}
 | 
				
			||||||
 | 
					  ): Promise<BatchUploadResponse> {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      concurrent = 3,
 | 
				
			||||||
 | 
					      onProgress,
 | 
				
			||||||
 | 
					      onFileProgress,
 | 
				
			||||||
 | 
					      onFileComplete
 | 
				
			||||||
 | 
					    } = options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const results: BatchUploadResponse = {
 | 
				
			||||||
 | 
					      success: [],
 | 
				
			||||||
 | 
					      failed: [],
 | 
				
			||||||
 | 
					      total: files.length,
 | 
				
			||||||
 | 
					      successCount: 0,
 | 
				
			||||||
 | 
					      failedCount: 0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const uploadFile = async (file: File): Promise<void> => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await this.uploadFile({
 | 
				
			||||||
 | 
					          file,
 | 
				
			||||||
 | 
					          onProgress: (progress) => onFileProgress?.(file, progress)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.success) {
 | 
				
			||||||
 | 
					          results.success.push(response.data)
 | 
				
			||||||
 | 
					          results.successCount++
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          results.failed.push({
 | 
				
			||||||
 | 
					            file,
 | 
				
			||||||
 | 
					            error: response.message
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          results.failedCount++
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        onFileComplete?.(file, response.data)
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        results.failed.push({
 | 
				
			||||||
 | 
					          file,
 | 
				
			||||||
 | 
					          error: error instanceof Error ? error.message : '上传失败'
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        results.failedCount++
 | 
				
			||||||
 | 
					        onFileComplete?.(file, error as Error)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 更新总进度
 | 
				
			||||||
 | 
					      const completed = results.successCount + results.failedCount
 | 
				
			||||||
 | 
					      const progress = (completed / results.total) * 100
 | 
				
			||||||
 | 
					      onProgress?.(progress)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 并发上传
 | 
				
			||||||
 | 
					    const chunks = []
 | 
				
			||||||
 | 
					    for (let i = 0; i < files.length; i += concurrent) {
 | 
				
			||||||
 | 
					      chunks.push(files.slice(i, i + concurrent))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const chunk of chunks) {
 | 
				
			||||||
 | 
					      await Promise.all(chunk.map(uploadFile))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return results
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 取消上传
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public cancelUpload(uploadId: string): void {
 | 
				
			||||||
 | 
					    const controller = this.uploadQueue.get(uploadId)
 | 
				
			||||||
 | 
					    if (controller) {
 | 
				
			||||||
 | 
					      controller.abort()
 | 
				
			||||||
 | 
					      this.uploadQueue.delete(uploadId)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 取消所有上传
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public cancelAllUploads(): void {
 | 
				
			||||||
 | 
					    this.uploadQueue.forEach((controller) => {
 | 
				
			||||||
 | 
					      controller.abort()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    this.uploadQueue.clear()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取上传进度
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public getUploadProgress(uploadId: string): number {
 | 
				
			||||||
 | 
					    // 这里可以实现获取具体上传进度的逻辑
 | 
				
			||||||
 | 
					    return 0
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建默认实例
 | 
				
			||||||
 | 
					export const fileUploader = new FileUploader() 
 | 
				
			||||||
							
								
								
									
										116
									
								
								src/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/api/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,116 @@
 | 
				
			|||||||
 | 
					// 核心模块
 | 
				
			||||||
 | 
					export { httpClient, HttpClient } from './core/request'
 | 
				
			||||||
 | 
					export { fileUploader, FileUploader } from './core/upload'
 | 
				
			||||||
 | 
					export { fileDownloader, FileDownloader } from './core/download'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 业务API模块
 | 
				
			||||||
 | 
					export { editorApi, EditorApi } from './modules/editor'
 | 
				
			||||||
 | 
					export { resourceApi, ResourceApi } from './modules/resource'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 类型定义
 | 
				
			||||||
 | 
					export type {
 | 
				
			||||||
 | 
					  ApiResponse,
 | 
				
			||||||
 | 
					  RequestConfig,
 | 
				
			||||||
 | 
					  UploadConfig,
 | 
				
			||||||
 | 
					  DownloadConfig,
 | 
				
			||||||
 | 
					  PaginationParams,
 | 
				
			||||||
 | 
					  PaginationResponse,
 | 
				
			||||||
 | 
					  ApiError
 | 
				
			||||||
 | 
					} from './types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 配置
 | 
				
			||||||
 | 
					export { API_CONFIG, API_ENDPOINTS, ENV_CONFIG } from './config'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 工具函数
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  delay,
 | 
				
			||||||
 | 
					  generateId,
 | 
				
			||||||
 | 
					  formatFileSize,
 | 
				
			||||||
 | 
					  getFileExtension,
 | 
				
			||||||
 | 
					  validateFileType,
 | 
				
			||||||
 | 
					  createApiError,
 | 
				
			||||||
 | 
					  isApiError,
 | 
				
			||||||
 | 
					  retry,
 | 
				
			||||||
 | 
					  downloadFile,
 | 
				
			||||||
 | 
					  readFileAsBase64,
 | 
				
			||||||
 | 
					  readFileAsArrayBuffer,
 | 
				
			||||||
 | 
					  compressImage
 | 
				
			||||||
 | 
					} from './utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 业务类型
 | 
				
			||||||
 | 
					export type {
 | 
				
			||||||
 | 
					  ProjectData,
 | 
				
			||||||
 | 
					  OrderData
 | 
				
			||||||
 | 
					} from './modules/editor'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type {
 | 
				
			||||||
 | 
					  ResourceData,
 | 
				
			||||||
 | 
					  ResourceCategory,
 | 
				
			||||||
 | 
					  ResourceSearchParams
 | 
				
			||||||
 | 
					} from './modules/resource'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 上传下载相关类型
 | 
				
			||||||
 | 
					export type {
 | 
				
			||||||
 | 
					  UploadResponse,
 | 
				
			||||||
 | 
					  BatchUploadResponse
 | 
				
			||||||
 | 
					} from './core/upload'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type {
 | 
				
			||||||
 | 
					  DownloadResponse,
 | 
				
			||||||
 | 
					  BatchDownloadResponse
 | 
				
			||||||
 | 
					} from './core/download'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 导入实例用于ApiManager
 | 
				
			||||||
 | 
					import { httpClient } from './core/request'
 | 
				
			||||||
 | 
					import { fileUploader } from './core/upload'
 | 
				
			||||||
 | 
					import { fileDownloader } from './core/download'
 | 
				
			||||||
 | 
					import { API_CONFIG, ENV_CONFIG } from './config'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * API管理器类
 | 
				
			||||||
 | 
					 * 提供统一的API管理和配置
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class ApiManager {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 设置认证token
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public static setAuthToken(token: string): void {
 | 
				
			||||||
 | 
					    httpClient.setAuthToken(token)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 取消所有请求
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public static cancelAllRequests(): void {
 | 
				
			||||||
 | 
					    httpClient.cancelAllRequests()
 | 
				
			||||||
 | 
					    fileUploader.cancelAllUploads()
 | 
				
			||||||
 | 
					    fileDownloader.cancelAllDownloads()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取API配置
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public static getConfig() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      baseUrl: API_CONFIG.BASE_URL,
 | 
				
			||||||
 | 
					      timeout: API_CONFIG.TIMEOUT,
 | 
				
			||||||
 | 
					      retryCount: API_CONFIG.RETRY_COUNT,
 | 
				
			||||||
 | 
					      env: ENV_CONFIG
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 检查网络连接
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public static async checkConnection(): Promise<boolean> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await httpClient.get('/health')
 | 
				
			||||||
 | 
					      return response.success
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      return false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 默认导出API管理器
 | 
				
			||||||
 | 
					export default ApiManager 
 | 
				
			||||||
							
								
								
									
										143
									
								
								src/api/modules/editor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/api/modules/editor.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,143 @@
 | 
				
			|||||||
 | 
					import { httpClient } from '../core/request'
 | 
				
			||||||
 | 
					import { fileUploader } from '../core/upload'
 | 
				
			||||||
 | 
					import { fileDownloader } from '../core/download'
 | 
				
			||||||
 | 
					import { ApiResponse, PaginationParams, PaginationResponse } from '../types'
 | 
				
			||||||
 | 
					import { API_ENDPOINTS } from '../config'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 项目数据类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface ProjectData {
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					  name: string
 | 
				
			||||||
 | 
					  description?: string
 | 
				
			||||||
 | 
					  thumbnail?: string
 | 
				
			||||||
 | 
					  sceneData: any
 | 
				
			||||||
 | 
					  createdAt: string
 | 
				
			||||||
 | 
					  updatedAt: string
 | 
				
			||||||
 | 
					  userId: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 订单数据类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface OrderData {
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					  projectId: string
 | 
				
			||||||
 | 
					  status: 'pending' | 'processing' | 'completed' | 'failed'
 | 
				
			||||||
 | 
					  result?: any
 | 
				
			||||||
 | 
					  createdAt: string
 | 
				
			||||||
 | 
					  updatedAt: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 编辑器API类
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class EditorApi {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 创建订单
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async createOrder(params: any): Promise<ApiResponse<OrderData>> {
 | 
				
			||||||
 | 
					    return httpClient.post<OrderData>(API_ENDPOINTS.EDITOR.CREATE_ORDER, params)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 保存项目
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async saveProject(projectData: Partial<ProjectData>): Promise<ApiResponse<ProjectData>> {
 | 
				
			||||||
 | 
					    return httpClient.post<ProjectData>(API_ENDPOINTS.EDITOR.SAVE_PROJECT, projectData)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 加载项目
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async loadProject(projectId: string): Promise<ApiResponse<ProjectData>> {
 | 
				
			||||||
 | 
					    return httpClient.get<ProjectData>(`${API_ENDPOINTS.EDITOR.LOAD_PROJECT}/${projectId}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 删除项目
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async deleteProject(projectId: string): Promise<ApiResponse<void>> {
 | 
				
			||||||
 | 
					    return httpClient.delete<void>(`${API_ENDPOINTS.EDITOR.DELETE_PROJECT}/${projectId}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取项目列表
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async getProjectList(params: PaginationParams): Promise<ApiResponse<PaginationResponse<ProjectData>>> {
 | 
				
			||||||
 | 
					    return httpClient.get<PaginationResponse<ProjectData>>(API_ENDPOINTS.EDITOR.LOAD_PROJECT, params)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 上传项目缩略图
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async uploadThumbnail(file: File, projectId: string): Promise<ApiResponse<{ url: string }>> {
 | 
				
			||||||
 | 
					    return fileUploader.uploadImage(file, {
 | 
				
			||||||
 | 
					      compress: true,
 | 
				
			||||||
 | 
					      quality: 0.8,
 | 
				
			||||||
 | 
					      maxWidth: 400,
 | 
				
			||||||
 | 
					      maxHeight: 300,
 | 
				
			||||||
 | 
					      onProgress: (progress) => {
 | 
				
			||||||
 | 
					        console.log(`缩略图上传进度: ${progress}%`)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 导出项目
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async exportProject(projectId: string, format: 'json' | 'glb' | 'fbx' = 'json'): Promise<void> {
 | 
				
			||||||
 | 
					    await fileDownloader.downloadAndSave({
 | 
				
			||||||
 | 
					      url: `/editor/project/export/${projectId}`,
 | 
				
			||||||
 | 
					      filename: `project_${projectId}.${format}`,
 | 
				
			||||||
 | 
					      params: { format }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 导入项目
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async importProject(file: File): Promise<ApiResponse<ProjectData>> {
 | 
				
			||||||
 | 
					    const uploadResponse = await fileUploader.uploadFile({
 | 
				
			||||||
 | 
					      file,
 | 
				
			||||||
 | 
					      fieldName: 'projectFile',
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        action: 'import'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!uploadResponse.success) {
 | 
				
			||||||
 | 
					      throw new Error(uploadResponse.message)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 上传成功后,调用导入API
 | 
				
			||||||
 | 
					    return httpClient.post<ProjectData>('/editor/project/import', {
 | 
				
			||||||
 | 
					      fileUrl: uploadResponse.data.url
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 批量导出项目
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async exportMultipleProjects(projectIds: string[], format: 'json' | 'glb' | 'fbx' = 'json'): Promise<void> {
 | 
				
			||||||
 | 
					    const downloadUrls = projectIds.map(id => `/editor/project/export/${id}?format=${format}`)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    await fileDownloader.downloadBatch(downloadUrls, {
 | 
				
			||||||
 | 
					      concurrent: 2,
 | 
				
			||||||
 | 
					      onProgress: (progress) => {
 | 
				
			||||||
 | 
					        console.log(`批量导出进度: ${progress}%`)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onFileComplete: (url, result) => {
 | 
				
			||||||
 | 
					        if (result instanceof Error) {
 | 
				
			||||||
 | 
					          console.error(`导出失败: ${url}`, result)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          console.log(`导出完成: ${url}`)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建默认实例
 | 
				
			||||||
 | 
					export const editorApi = new EditorApi() 
 | 
				
			||||||
							
								
								
									
										281
									
								
								src/api/modules/resource.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								src/api/modules/resource.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,281 @@
 | 
				
			|||||||
 | 
					import { httpClient } from '../core/request'
 | 
				
			||||||
 | 
					import { fileUploader } from '../core/upload'
 | 
				
			||||||
 | 
					import { fileDownloader } from '../core/download'
 | 
				
			||||||
 | 
					import { ApiResponse, PaginationParams, PaginationResponse } from '../types'
 | 
				
			||||||
 | 
					import { API_ENDPOINTS } from '../config'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 资源数据类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface ResourceData {
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					  name: string
 | 
				
			||||||
 | 
					  type: 'model' | 'texture' | 'material' | 'audio' | 'video' | 'other'
 | 
				
			||||||
 | 
					  url: string
 | 
				
			||||||
 | 
					  thumbnailUrl?: string
 | 
				
			||||||
 | 
					  size: number
 | 
				
			||||||
 | 
					  format: string
 | 
				
			||||||
 | 
					  tags: string[]
 | 
				
			||||||
 | 
					  description?: string
 | 
				
			||||||
 | 
					  createdAt: string
 | 
				
			||||||
 | 
					  updatedAt: string
 | 
				
			||||||
 | 
					  userId: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 资源分类类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface ResourceCategory {
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					  name: string
 | 
				
			||||||
 | 
					  description?: string
 | 
				
			||||||
 | 
					  parentId?: string
 | 
				
			||||||
 | 
					  children?: ResourceCategory[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 资源搜索参数
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface ResourceSearchParams extends PaginationParams {
 | 
				
			||||||
 | 
					  keyword?: string
 | 
				
			||||||
 | 
					  type?: ResourceData['type']
 | 
				
			||||||
 | 
					  tags?: string[]
 | 
				
			||||||
 | 
					  categoryId?: string
 | 
				
			||||||
 | 
					  format?: string
 | 
				
			||||||
 | 
					  sizeMin?: number
 | 
				
			||||||
 | 
					  sizeMax?: number
 | 
				
			||||||
 | 
					  dateFrom?: string
 | 
				
			||||||
 | 
					  dateTo?: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 资源API类
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class ResourceApi {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取资源列表
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async getResourceList(params: ResourceSearchParams): Promise<ApiResponse<PaginationResponse<ResourceData>>> {
 | 
				
			||||||
 | 
					    return httpClient.get<PaginationResponse<ResourceData>>(API_ENDPOINTS.RESOURCE.LIST, params)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取资源详情
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async getResourceDetail(resourceId: string): Promise<ApiResponse<ResourceData>> {
 | 
				
			||||||
 | 
					    return httpClient.get<ResourceData>(`${API_ENDPOINTS.RESOURCE.LIST}/${resourceId}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 上传资源
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async uploadResource(
 | 
				
			||||||
 | 
					    file: File,
 | 
				
			||||||
 | 
					    metadata: {
 | 
				
			||||||
 | 
					      name?: string
 | 
				
			||||||
 | 
					      type: ResourceData['type']
 | 
				
			||||||
 | 
					      tags?: string[]
 | 
				
			||||||
 | 
					      description?: string
 | 
				
			||||||
 | 
					      categoryId?: string
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<ResourceData>> {
 | 
				
			||||||
 | 
					    // 根据文件类型选择上传方式
 | 
				
			||||||
 | 
					    let uploadResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (metadata.type === 'texture' && file.type.startsWith('image/')) {
 | 
				
			||||||
 | 
					      // 图片资源,使用图片上传(带压缩)
 | 
				
			||||||
 | 
					      uploadResponse = await fileUploader.uploadImage(file, {
 | 
				
			||||||
 | 
					        compress: true,
 | 
				
			||||||
 | 
					        quality: 0.9,
 | 
				
			||||||
 | 
					        maxWidth: 2048,
 | 
				
			||||||
 | 
					        maxHeight: 2048,
 | 
				
			||||||
 | 
					        onProgress: (progress) => {
 | 
				
			||||||
 | 
					          console.log(`资源上传进度: ${progress}%`)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    } else if (file.size > 50 * 1024 * 1024) {
 | 
				
			||||||
 | 
					      // 大文件,使用分片上传
 | 
				
			||||||
 | 
					      uploadResponse = await fileUploader.uploadLargeFile(file, {
 | 
				
			||||||
 | 
					        onProgress: (progress) => {
 | 
				
			||||||
 | 
					          console.log(`大文件上传进度: ${progress}%`)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // 普通文件上传
 | 
				
			||||||
 | 
					      uploadResponse = await fileUploader.uploadFile({
 | 
				
			||||||
 | 
					        file,
 | 
				
			||||||
 | 
					        data: metadata,
 | 
				
			||||||
 | 
					        onProgress: (progress) => {
 | 
				
			||||||
 | 
					          console.log(`文件上传进度: ${progress}%`)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!uploadResponse.success) {
 | 
				
			||||||
 | 
					      throw new Error(uploadResponse.message)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 创建资源记录
 | 
				
			||||||
 | 
					    return httpClient.post<ResourceData>(API_ENDPOINTS.RESOURCE.UPLOAD, {
 | 
				
			||||||
 | 
					      ...metadata,
 | 
				
			||||||
 | 
					      name: metadata.name || file.name,
 | 
				
			||||||
 | 
					      url: uploadResponse.data.url,
 | 
				
			||||||
 | 
					      size: file.size,
 | 
				
			||||||
 | 
					      format: file.name.split('.').pop()?.toLowerCase() || 'unknown'
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 批量上传资源
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async uploadMultipleResources(
 | 
				
			||||||
 | 
					    files: File[],
 | 
				
			||||||
 | 
					    metadata: {
 | 
				
			||||||
 | 
					      type: ResourceData['type']
 | 
				
			||||||
 | 
					      tags?: string[]
 | 
				
			||||||
 | 
					      categoryId?: string
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<ResourceData[]>> {
 | 
				
			||||||
 | 
					    const results = await fileUploader.uploadBatch(files, {
 | 
				
			||||||
 | 
					      concurrent: 3,
 | 
				
			||||||
 | 
					      onProgress: (progress) => {
 | 
				
			||||||
 | 
					        console.log(`批量上传进度: ${progress}%`)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onFileComplete: (file, result) => {
 | 
				
			||||||
 | 
					        if (result instanceof Error) {
 | 
				
			||||||
 | 
					          console.error(`文件上传失败: ${file.name}`, result)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          console.log(`文件上传完成: ${file.name}`)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 创建资源记录
 | 
				
			||||||
 | 
					    const resourcePromises = results.success.map(uploadResult => 
 | 
				
			||||||
 | 
					      httpClient.post<ResourceData>(API_ENDPOINTS.RESOURCE.UPLOAD, {
 | 
				
			||||||
 | 
					        ...metadata,
 | 
				
			||||||
 | 
					        name: uploadResult.filename,
 | 
				
			||||||
 | 
					        url: uploadResult.url,
 | 
				
			||||||
 | 
					        size: uploadResult.size,
 | 
				
			||||||
 | 
					        format: uploadResult.filename.split('.').pop()?.toLowerCase() || 'unknown'
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const resourceResults = await Promise.all(resourcePromises)
 | 
				
			||||||
 | 
					    const successResources = resourceResults
 | 
				
			||||||
 | 
					      .filter(result => result.success)
 | 
				
			||||||
 | 
					      .map(result => result.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      code: 0,
 | 
				
			||||||
 | 
					      message: 'Success',
 | 
				
			||||||
 | 
					      data: successResources,
 | 
				
			||||||
 | 
					      success: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 删除资源
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async deleteResource(resourceId: string): Promise<ApiResponse<void>> {
 | 
				
			||||||
 | 
					    return httpClient.delete<void>(`${API_ENDPOINTS.RESOURCE.DELETE}/${resourceId}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 批量删除资源
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async deleteMultipleResources(resourceIds: string[]): Promise<ApiResponse<void>> {
 | 
				
			||||||
 | 
					    return httpClient.post<void>('/resource/batch-delete', { resourceIds })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 更新资源信息
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async updateResource(
 | 
				
			||||||
 | 
					    resourceId: string,
 | 
				
			||||||
 | 
					    data: Partial<Pick<ResourceData, 'name' | 'tags' | 'description'>>
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<ResourceData>> {
 | 
				
			||||||
 | 
					    return httpClient.put<ResourceData>(`/resource/${resourceId}`, data)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 下载资源
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadResource(resource: ResourceData): Promise<void> {
 | 
				
			||||||
 | 
					    await fileDownloader.downloadAndSave({
 | 
				
			||||||
 | 
					      url: resource.url,
 | 
				
			||||||
 | 
					      filename: resource.name
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 批量下载资源
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async downloadMultipleResources(resources: ResourceData[]): Promise<void> {
 | 
				
			||||||
 | 
					    const downloadFiles = resources.map(resource => ({
 | 
				
			||||||
 | 
					      url: resource.url,
 | 
				
			||||||
 | 
					      filename: resource.name
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await fileDownloader.downloadAsZip(downloadFiles, 'resources.zip')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取资源分类
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async getResourceCategories(): Promise<ApiResponse<ResourceCategory[]>> {
 | 
				
			||||||
 | 
					    return httpClient.get<ResourceCategory[]>('/resource/categories')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 创建资源分类
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async createResourceCategory(
 | 
				
			||||||
 | 
					    data: Pick<ResourceCategory, 'name' | 'description' | 'parentId'>
 | 
				
			||||||
 | 
					  ): Promise<ApiResponse<ResourceCategory>> {
 | 
				
			||||||
 | 
					    return httpClient.post<ResourceCategory>('/resource/categories', data)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 搜索资源
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async searchResources(params: ResourceSearchParams): Promise<ApiResponse<PaginationResponse<ResourceData>>> {
 | 
				
			||||||
 | 
					    return httpClient.get<PaginationResponse<ResourceData>>('/resource/search', params)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取热门标签
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async getPopularTags(limit: number = 20): Promise<ApiResponse<string[]>> {
 | 
				
			||||||
 | 
					    return httpClient.get<string[]>('/resource/tags/popular', { limit })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取资源统计信息
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async getResourceStats(): Promise<ApiResponse<{
 | 
				
			||||||
 | 
					    total: number
 | 
				
			||||||
 | 
					    byType: Record<ResourceData['type'], number>
 | 
				
			||||||
 | 
					    totalSize: number
 | 
				
			||||||
 | 
					    recentUploads: number
 | 
				
			||||||
 | 
					  }>> {
 | 
				
			||||||
 | 
					    return httpClient.get('/resource/stats')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 预览资源(获取预览URL)
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async getResourcePreview(resourceId: string): Promise<ApiResponse<{ previewUrl: string }>> {
 | 
				
			||||||
 | 
					    return httpClient.get<{ previewUrl: string }>(`/resource/${resourceId}/preview`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 生成资源缩略图
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public async generateThumbnail(resourceId: string): Promise<ApiResponse<{ thumbnailUrl: string }>> {
 | 
				
			||||||
 | 
					    return httpClient.post<{ thumbnailUrl: string }>(`/resource/${resourceId}/thumbnail`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建默认实例
 | 
				
			||||||
 | 
					export const resourceApi = new ResourceApi() 
 | 
				
			||||||
							
								
								
									
										58
									
								
								src/api/types/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/api/types/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					// 通用响应类型
 | 
				
			||||||
 | 
					export interface ApiResponse<T = any> {
 | 
				
			||||||
 | 
					  code: number
 | 
				
			||||||
 | 
					  message: string
 | 
				
			||||||
 | 
					  data: T
 | 
				
			||||||
 | 
					  success: boolean
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 请求配置类型
 | 
				
			||||||
 | 
					export interface RequestConfig {
 | 
				
			||||||
 | 
					  url: string
 | 
				
			||||||
 | 
					  method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'
 | 
				
			||||||
 | 
					  params?: Record<string, any>
 | 
				
			||||||
 | 
					  data?: any
 | 
				
			||||||
 | 
					  headers?: Record<string, string>
 | 
				
			||||||
 | 
					  timeout?: number
 | 
				
			||||||
 | 
					  responseType?: 'json' | 'blob' | 'text' | 'arraybuffer'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 文件上传配置
 | 
				
			||||||
 | 
					export interface UploadConfig {
 | 
				
			||||||
 | 
					  file: File | Blob
 | 
				
			||||||
 | 
					  filename?: string
 | 
				
			||||||
 | 
					  fieldName?: string
 | 
				
			||||||
 | 
					  data?: Record<string, any>
 | 
				
			||||||
 | 
					  onProgress?: (progress: number) => void
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 文件下载配置
 | 
				
			||||||
 | 
					export interface DownloadConfig {
 | 
				
			||||||
 | 
					  url: string
 | 
				
			||||||
 | 
					  filename?: string
 | 
				
			||||||
 | 
					  params?: Record<string, any>
 | 
				
			||||||
 | 
					  onProgress?: (progress: number) => void
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 分页请求参数
 | 
				
			||||||
 | 
					export interface PaginationParams {
 | 
				
			||||||
 | 
					  page: number
 | 
				
			||||||
 | 
					  pageSize: number
 | 
				
			||||||
 | 
					  [key: string]: any
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 分页响应数据
 | 
				
			||||||
 | 
					export interface PaginationResponse<T> {
 | 
				
			||||||
 | 
					  list: T[]
 | 
				
			||||||
 | 
					  total: number
 | 
				
			||||||
 | 
					  page: number
 | 
				
			||||||
 | 
					  pageSize: number
 | 
				
			||||||
 | 
					  totalPages: number
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 错误类型
 | 
				
			||||||
 | 
					export interface ApiError {
 | 
				
			||||||
 | 
					  code: number
 | 
				
			||||||
 | 
					  message: string
 | 
				
			||||||
 | 
					  details?: any
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
							
								
								
									
										184
									
								
								src/api/utils/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								src/api/utils/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,184 @@
 | 
				
			|||||||
 | 
					import { ApiError } from '../types'
 | 
				
			||||||
 | 
					import { API_CONFIG } from '../config'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 延迟函数
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const delay = (ms: number): Promise<void> => {
 | 
				
			||||||
 | 
					  return new Promise(resolve => setTimeout(resolve, ms))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 生成唯一ID
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const generateId = (): string => {
 | 
				
			||||||
 | 
					  return Date.now().toString(36) + Math.random().toString(36).substr(2)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 格式化文件大小
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const formatFileSize = (bytes: number): string => {
 | 
				
			||||||
 | 
					  if (bytes === 0) return '0 Bytes'
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  const k = 1024
 | 
				
			||||||
 | 
					  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
 | 
				
			||||||
 | 
					  const i = Math.floor(Math.log(bytes) / Math.log(k))
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 获取文件扩展名
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const getFileExtension = (filename: string): string => {
 | 
				
			||||||
 | 
					  return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 验证文件类型
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const validateFileType = (file: File, allowedTypes: string[]): boolean => {
 | 
				
			||||||
 | 
					  const fileType = file.type
 | 
				
			||||||
 | 
					  const fileExtension = getFileExtension(file.name).toLowerCase()
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  return allowedTypes.some(type => {
 | 
				
			||||||
 | 
					    if (type.includes('/')) {
 | 
				
			||||||
 | 
					      return fileType === type
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return fileExtension === type.toLowerCase()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 创建API错误对象
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const createApiError = (
 | 
				
			||||||
 | 
					  code: number,
 | 
				
			||||||
 | 
					  message: string,
 | 
				
			||||||
 | 
					  details?: any
 | 
				
			||||||
 | 
					): ApiError => {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    code,
 | 
				
			||||||
 | 
					    message,
 | 
				
			||||||
 | 
					    details
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 判断是否为API错误
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const isApiError = (error: any): error is ApiError => {
 | 
				
			||||||
 | 
					  return error && typeof error.code === 'number' && typeof error.message === 'string'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 重试函数
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const retry = async <T>(
 | 
				
			||||||
 | 
					  fn: () => Promise<T>,
 | 
				
			||||||
 | 
					  retries: number = API_CONFIG.RETRY_COUNT,
 | 
				
			||||||
 | 
					  delayMs: number = API_CONFIG.RETRY_DELAY
 | 
				
			||||||
 | 
					): Promise<T> => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    return await fn()
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    if (retries > 0) {
 | 
				
			||||||
 | 
					      await delay(delayMs)
 | 
				
			||||||
 | 
					      return retry(fn, retries - 1, delayMs * 2) // 指数退避
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    throw error
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 下载文件
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const downloadFile = (blob: Blob, filename: string): void => {
 | 
				
			||||||
 | 
					  const url = window.URL.createObjectURL(blob)
 | 
				
			||||||
 | 
					  const link = document.createElement('a')
 | 
				
			||||||
 | 
					  link.href = url
 | 
				
			||||||
 | 
					  link.download = filename
 | 
				
			||||||
 | 
					  document.body.appendChild(link)
 | 
				
			||||||
 | 
					  link.click()
 | 
				
			||||||
 | 
					  document.body.removeChild(link)
 | 
				
			||||||
 | 
					  window.URL.revokeObjectURL(url)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 读取文件为Base64
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const readFileAsBase64 = (file: File): Promise<string> => {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    const reader = new FileReader()
 | 
				
			||||||
 | 
					    reader.onload = () => {
 | 
				
			||||||
 | 
					      const result = reader.result as string
 | 
				
			||||||
 | 
					      resolve(result.split(',')[1]) // 移除data:xxx;base64,前缀
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    reader.onerror = reject
 | 
				
			||||||
 | 
					    reader.readAsDataURL(file)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 读取文件为ArrayBuffer
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const readFileAsArrayBuffer = (file: File): Promise<ArrayBuffer> => {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    const reader = new FileReader()
 | 
				
			||||||
 | 
					    reader.onload = () => resolve(reader.result as ArrayBuffer)
 | 
				
			||||||
 | 
					    reader.onerror = reject
 | 
				
			||||||
 | 
					    reader.readAsArrayBuffer(file)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 压缩图片
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const compressImage = (
 | 
				
			||||||
 | 
					  file: File,
 | 
				
			||||||
 | 
					  quality: number = 0.8,
 | 
				
			||||||
 | 
					  maxWidth: number = 1920,
 | 
				
			||||||
 | 
					  maxHeight: number = 1080
 | 
				
			||||||
 | 
					): Promise<Blob> => {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    const canvas = document.createElement('canvas')
 | 
				
			||||||
 | 
					    const ctx = canvas.getContext('2d')
 | 
				
			||||||
 | 
					    const img = new Image()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    img.onload = () => {
 | 
				
			||||||
 | 
					      // 计算压缩后的尺寸
 | 
				
			||||||
 | 
					      let { width, height } = img
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (width > maxWidth) {
 | 
				
			||||||
 | 
					        height = (height * maxWidth) / width
 | 
				
			||||||
 | 
					        width = maxWidth
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (height > maxHeight) {
 | 
				
			||||||
 | 
					        width = (width * maxHeight) / height
 | 
				
			||||||
 | 
					        height = maxHeight
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      canvas.width = width
 | 
				
			||||||
 | 
					      canvas.height = height
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // 绘制并压缩
 | 
				
			||||||
 | 
					      ctx?.drawImage(img, 0, 0, width, height)
 | 
				
			||||||
 | 
					      canvas.toBlob(
 | 
				
			||||||
 | 
					        (blob) => {
 | 
				
			||||||
 | 
					          if (blob) {
 | 
				
			||||||
 | 
					            resolve(blob)
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            reject(new Error('图片压缩失败'))
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        file.type,
 | 
				
			||||||
 | 
					        quality
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    img.onerror = reject
 | 
				
			||||||
 | 
					    img.src = URL.createObjectURL(file)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/assets/vue.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/vue.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="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 496 B  | 
							
								
								
									
										41
									
								
								src/components/HelloWorld.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/HelloWorld.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineProps<{ msg: string }>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const count = ref(0)
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <h1>{{ msg }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="card">
 | 
				
			||||||
 | 
					    <button type="button" @click="count++">count is {{ count }}</button>
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Edit
 | 
				
			||||||
 | 
					      <code>components/HelloWorld.vue</code> to test HMR
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <p>
 | 
				
			||||||
 | 
					    Check out
 | 
				
			||||||
 | 
					    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
 | 
				
			||||||
 | 
					      >create-vue</a
 | 
				
			||||||
 | 
					    >, the official Vue + Vite starter
 | 
				
			||||||
 | 
					  </p>
 | 
				
			||||||
 | 
					  <p>
 | 
				
			||||||
 | 
					    Learn more about IDE Support for Vue in the
 | 
				
			||||||
 | 
					    <a
 | 
				
			||||||
 | 
					      href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
 | 
				
			||||||
 | 
					      target="_blank"
 | 
				
			||||||
 | 
					      >Vue Docs Scaling up Guide</a
 | 
				
			||||||
 | 
					    >.
 | 
				
			||||||
 | 
					  </p>
 | 
				
			||||||
 | 
					  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.read-the-docs {
 | 
				
			||||||
 | 
					  color: #888;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										188
									
								
								src/components/NavHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/components/NavHeader.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,188 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <el-header class="nav-header">
 | 
				
			||||||
 | 
					    <div class="nav-container">
 | 
				
			||||||
 | 
					      <!-- 左侧Logo和菜单 -->
 | 
				
			||||||
 | 
					      <div class="nav-left">
 | 
				
			||||||
 | 
					        <div class="logo">
 | 
				
			||||||
 | 
					          <router-link to="/" class="logo-link">
 | 
				
			||||||
 | 
					            {{ appStore.title }}
 | 
				
			||||||
 | 
					          </router-link>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- 主菜单 -->
 | 
				
			||||||
 | 
					        <el-menu
 | 
				
			||||||
 | 
					          :default-active="activeIndex"
 | 
				
			||||||
 | 
					          mode="horizontal"
 | 
				
			||||||
 | 
					          @select="handleSelect"
 | 
				
			||||||
 | 
					          class="nav-menu"
 | 
				
			||||||
 | 
					          :ellipsis="false"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <el-menu-item index="/">
 | 
				
			||||||
 | 
					            <el-icon><HomeFilled /></el-icon>
 | 
				
			||||||
 | 
					            <span>首页</span>
 | 
				
			||||||
 | 
					          </el-menu-item>
 | 
				
			||||||
 | 
					          <el-menu-item index="/about">
 | 
				
			||||||
 | 
					            <el-icon><InfoFilled /></el-icon>
 | 
				
			||||||
 | 
					            <span>关于</span>
 | 
				
			||||||
 | 
					          </el-menu-item>
 | 
				
			||||||
 | 
					          <el-menu-item v-if="userStore.isLoggedIn" index="/dashboard">
 | 
				
			||||||
 | 
					            <el-icon><DataBoard /></el-icon>
 | 
				
			||||||
 | 
					            <span>仪表板</span>
 | 
				
			||||||
 | 
					          </el-menu-item>
 | 
				
			||||||
 | 
					          <el-menu-item v-if="userStore.isLoggedIn" index="/profile">
 | 
				
			||||||
 | 
					            <el-icon><User /></el-icon>
 | 
				
			||||||
 | 
					            <span>个人资料</span>
 | 
				
			||||||
 | 
					          </el-menu-item>
 | 
				
			||||||
 | 
					          <el-menu-item v-if="userStore.isLoggedIn && userStore.hasRole('admin')" index="/admin">
 | 
				
			||||||
 | 
					            <el-icon><Setting /></el-icon>
 | 
				
			||||||
 | 
					            <span>管理</span>
 | 
				
			||||||
 | 
					          </el-menu-item>
 | 
				
			||||||
 | 
					        </el-menu>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- 右侧操作区 -->
 | 
				
			||||||
 | 
					      <div class="nav-right">
 | 
				
			||||||
 | 
					        <!-- 主题切换 -->
 | 
				
			||||||
 | 
					        <el-button 
 | 
				
			||||||
 | 
					          @click="appStore.toggleTheme" 
 | 
				
			||||||
 | 
					          :icon="appStore.theme === 'dark' ? 'Sunny' : 'Moon'"
 | 
				
			||||||
 | 
					          circle
 | 
				
			||||||
 | 
					          class="theme-btn"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- 用户区域 -->
 | 
				
			||||||
 | 
					        <div v-if="userStore.isLoggedIn" class="user-area">
 | 
				
			||||||
 | 
					          <el-dropdown>
 | 
				
			||||||
 | 
					                         <span class="user-dropdown">
 | 
				
			||||||
 | 
					               <el-avatar :size="32" :src="userStore.userInfo?.avatar">
 | 
				
			||||||
 | 
					                 <el-icon><User /></el-icon>
 | 
				
			||||||
 | 
					               </el-avatar>
 | 
				
			||||||
 | 
					               <span class="username">{{ userStore.userInfo?.nickname }}</span>
 | 
				
			||||||
 | 
					               <el-icon class="el-icon--right"><arrow-down /></el-icon>
 | 
				
			||||||
 | 
					             </span>
 | 
				
			||||||
 | 
					            <template #dropdown>
 | 
				
			||||||
 | 
					              <el-dropdown-menu>
 | 
				
			||||||
 | 
					                <el-dropdown-item @click="$router.push('/profile')">
 | 
				
			||||||
 | 
					                  <el-icon><User /></el-icon>
 | 
				
			||||||
 | 
					                  个人资料
 | 
				
			||||||
 | 
					                </el-dropdown-item>
 | 
				
			||||||
 | 
					                <el-dropdown-item @click="userStore.logout" divided>
 | 
				
			||||||
 | 
					                  <el-icon><SwitchButton /></el-icon>
 | 
				
			||||||
 | 
					                  退出登录
 | 
				
			||||||
 | 
					                </el-dropdown-item>
 | 
				
			||||||
 | 
					              </el-dropdown-menu>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </el-dropdown>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- 登录按钮 -->
 | 
				
			||||||
 | 
					        <el-button 
 | 
				
			||||||
 | 
					          v-else
 | 
				
			||||||
 | 
					          type="primary"
 | 
				
			||||||
 | 
					          @click="$router.push('/login')"
 | 
				
			||||||
 | 
					          class="login-btn"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <el-icon><User /></el-icon>
 | 
				
			||||||
 | 
					          登录
 | 
				
			||||||
 | 
					        </el-button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </el-header>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { computed } from 'vue'
 | 
				
			||||||
 | 
					import { useRoute, useRouter } from 'vue-router'
 | 
				
			||||||
 | 
					import { useAppStore } from '../stores/modules/app'
 | 
				
			||||||
 | 
					import { useUserStore } from '../stores/modules/user'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const route = useRoute()
 | 
				
			||||||
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					const appStore = useAppStore()
 | 
				
			||||||
 | 
					const userStore = useUserStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 当前激活的菜单项
 | 
				
			||||||
 | 
					const activeIndex = computed(() => route.path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 菜单选择处理
 | 
				
			||||||
 | 
					const handleSelect = (key: string) => {
 | 
				
			||||||
 | 
					  router.push(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.nav-header {
 | 
				
			||||||
 | 
					  border-bottom: 1px solid var(--el-border-color);
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  height: 60px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-container {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  max-width: 1200px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					  padding: 0 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-left {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.logo {
 | 
				
			||||||
 | 
					  font-size: 20px;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.logo-link {
 | 
				
			||||||
 | 
					  color: var(--el-color-primary);
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-menu {
 | 
				
			||||||
 | 
					  border-bottom: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-right {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.theme-btn {
 | 
				
			||||||
 | 
					  margin-right: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user-area {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user-dropdown {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-regular);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.username {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.login-btn {
 | 
				
			||||||
 | 
					  height: 36px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 暗黑主题适配 */
 | 
				
			||||||
 | 
					[data-theme="dark"] .nav-header {
 | 
				
			||||||
 | 
					  background-color: #1f2937;
 | 
				
			||||||
 | 
					  border-bottom-color: #374151;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .logo-link {
 | 
				
			||||||
 | 
					  color: #60a5fa;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style> 
 | 
				
			||||||
							
								
								
									
										50
									
								
								src/config/env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/config/env.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 环境变量配置
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 获取环境变量
 | 
				
			||||||
 | 
					export const getEnv = (): string => {
 | 
				
			||||||
 | 
					  return import.meta.env.MODE
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 获取环境变量前缀
 | 
				
			||||||
 | 
					export const getEnvConfig = (): Partial<ViteEnv> => {
 | 
				
			||||||
 | 
					  const envConfig = import.meta.env as Record<string, any>
 | 
				
			||||||
 | 
					  const ret: any = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const envName of Object.keys(envConfig)) {
 | 
				
			||||||
 | 
					    if (envName.startsWith('VITE_')) {
 | 
				
			||||||
 | 
					      let realName = envConfig[envName]
 | 
				
			||||||
 | 
					      if (typeof realName === 'string') {
 | 
				
			||||||
 | 
					        realName = realName.replace(/\\n/g, '\n')
 | 
				
			||||||
 | 
					        realName = realName === 'true' ? true : realName === 'false' ? false : realName
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      ret[envName] = realName
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 环境变量类型定义
 | 
				
			||||||
 | 
					export interface ViteEnv {
 | 
				
			||||||
 | 
					  VITE_APP_TITLE: string
 | 
				
			||||||
 | 
					  VITE_APP_PORT: number
 | 
				
			||||||
 | 
					  VITE_APP_BASE_API: string
 | 
				
			||||||
 | 
					  VITE_APP_ENV: string
 | 
				
			||||||
 | 
					  VITE_APP_BASE_URL: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 默认环境配置
 | 
				
			||||||
 | 
					export const defaultEnvConfig: ViteEnv = {
 | 
				
			||||||
 | 
					  VITE_APP_TITLE: 'Vite应用',
 | 
				
			||||||
 | 
					  VITE_APP_PORT: 8080,
 | 
				
			||||||
 | 
					  VITE_APP_BASE_API: '/api',
 | 
				
			||||||
 | 
					  VITE_APP_ENV: 'development',
 | 
				
			||||||
 | 
					  VITE_APP_BASE_URL: '/'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 获取当前环境配置
 | 
				
			||||||
 | 
					export const envConfig = {
 | 
				
			||||||
 | 
					  ...defaultEnvConfig,
 | 
				
			||||||
 | 
					  ...getEnvConfig()
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					import { createApp } from 'vue'
 | 
				
			||||||
 | 
					import './style.css'
 | 
				
			||||||
 | 
					import App from './App.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 导入Element Plus
 | 
				
			||||||
 | 
					import ElementPlus from 'element-plus'
 | 
				
			||||||
 | 
					import 'element-plus/dist/index.css'
 | 
				
			||||||
 | 
					import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 导入路由
 | 
				
			||||||
 | 
					import router from './router'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 导入状态管理
 | 
				
			||||||
 | 
					import pinia from './stores'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 导入应用store进行初始化
 | 
				
			||||||
 | 
					import { useAppStore } from './stores/modules/app'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const app = createApp(App)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 安装插件
 | 
				
			||||||
 | 
					app.use(router)
 | 
				
			||||||
 | 
					app.use(pinia)
 | 
				
			||||||
 | 
					app.use(ElementPlus)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 注册Element Plus图标
 | 
				
			||||||
 | 
					for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
 | 
				
			||||||
 | 
					  app.component(key, component)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 挂载应用
 | 
				
			||||||
 | 
					app.mount('#app')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 初始化应用
 | 
				
			||||||
 | 
					const appStore = useAppStore()
 | 
				
			||||||
 | 
					appStore.initApp()
 | 
				
			||||||
							
								
								
									
										160
									
								
								src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,160 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Vue Router路由配置
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
 | 
				
			||||||
 | 
					import { useUserStore } from '../stores/modules/user'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 路由元信息接口
 | 
				
			||||||
 | 
					export interface RouteMeta {
 | 
				
			||||||
 | 
					  title?: string
 | 
				
			||||||
 | 
					  icon?: string
 | 
				
			||||||
 | 
					  hidden?: boolean
 | 
				
			||||||
 | 
					  roles?: string[]
 | 
				
			||||||
 | 
					  requireAuth?: boolean
 | 
				
			||||||
 | 
					  keepAlive?: boolean
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 扩展RouteRecordRaw类型
 | 
				
			||||||
 | 
					declare module 'vue-router' {
 | 
				
			||||||
 | 
					  interface RouteMeta {
 | 
				
			||||||
 | 
					    title?: string
 | 
				
			||||||
 | 
					    icon?: string
 | 
				
			||||||
 | 
					    hidden?: boolean
 | 
				
			||||||
 | 
					    roles?: string[]
 | 
				
			||||||
 | 
					    requireAuth?: boolean
 | 
				
			||||||
 | 
					    keepAlive?: boolean
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 静态路由
 | 
				
			||||||
 | 
					export const staticRoutes: RouteRecordRaw[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/',
 | 
				
			||||||
 | 
					    name: 'Home',
 | 
				
			||||||
 | 
					    component: () => import('../views/Home.vue'),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      title: '首页',
 | 
				
			||||||
 | 
					      icon: 'home'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/about',
 | 
				
			||||||
 | 
					    name: 'About',
 | 
				
			||||||
 | 
					    component: () => import('../views/About.vue'),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      title: '关于',
 | 
				
			||||||
 | 
					      icon: 'info'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/login',
 | 
				
			||||||
 | 
					    name: 'Login',
 | 
				
			||||||
 | 
					    component: () => import('../views/Login.vue'),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      title: '登录',
 | 
				
			||||||
 | 
					      hidden: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/404',
 | 
				
			||||||
 | 
					    name: 'NotFound',
 | 
				
			||||||
 | 
					    component: () => import('../views/404.vue'),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      title: '页面未找到',
 | 
				
			||||||
 | 
					      hidden: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 需要登录的路由
 | 
				
			||||||
 | 
					export const authRoutes: RouteRecordRaw[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/dashboard',
 | 
				
			||||||
 | 
					    name: 'Dashboard',
 | 
				
			||||||
 | 
					    component: () => import('../views/Dashboard.vue'),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      title: '仪表板',
 | 
				
			||||||
 | 
					      icon: 'dashboard',
 | 
				
			||||||
 | 
					      requireAuth: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/admin',
 | 
				
			||||||
 | 
					    name: 'Admin',
 | 
				
			||||||
 | 
					    component: () => import('../views/Admin.vue'),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      title: '管理中心',
 | 
				
			||||||
 | 
					      icon: 'admin',
 | 
				
			||||||
 | 
					      requireAuth: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/profile',
 | 
				
			||||||
 | 
					    name: 'Profile',
 | 
				
			||||||
 | 
					    component: () => import('../views/Profile.vue'),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      title: '个人中心',
 | 
				
			||||||
 | 
					      icon: 'user',
 | 
				
			||||||
 | 
					      requireAuth: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建路由实例
 | 
				
			||||||
 | 
					const router = createRouter({
 | 
				
			||||||
 | 
					  history: createWebHistory(),
 | 
				
			||||||
 | 
					  routes: [
 | 
				
			||||||
 | 
					    ...staticRoutes,
 | 
				
			||||||
 | 
					    ...authRoutes,
 | 
				
			||||||
 | 
					    // 404通配符路由
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      path: '/:pathMatch(.*)*',
 | 
				
			||||||
 | 
					      redirect: '/404'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  scrollBehavior(_, __, savedPosition) {
 | 
				
			||||||
 | 
					    if (savedPosition) {
 | 
				
			||||||
 | 
					      return savedPosition
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return { top: 0 }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 路由守卫
 | 
				
			||||||
 | 
					router.beforeEach((to, _, next) => {
 | 
				
			||||||
 | 
					  const userStore = useUserStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查是否需要认证
 | 
				
			||||||
 | 
					  if (to.meta?.requireAuth) {
 | 
				
			||||||
 | 
					    if (!userStore.isLoggedIn) {
 | 
				
			||||||
 | 
					      // 未登录,跳转到登录页
 | 
				
			||||||
 | 
					      next({
 | 
				
			||||||
 | 
					        path: '/login',
 | 
				
			||||||
 | 
					        query: { redirect: to.fullPath }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 如果是登录页且已登录,跳转到首页
 | 
				
			||||||
 | 
					  if (to.path === '/login' && userStore.isLoggedIn) {
 | 
				
			||||||
 | 
					    next('/')
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  next()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 路由错误处理
 | 
				
			||||||
 | 
					router.onError((error) => {
 | 
				
			||||||
 | 
					  console.error('路由错误:', error)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 工具函数 - 获取所有路由(用于菜单生成)
 | 
				
			||||||
 | 
					export const getAllRoutes = (): RouteRecordRaw[] => {
 | 
				
			||||||
 | 
					  const allRoutes = [...staticRoutes, ...authRoutes]
 | 
				
			||||||
 | 
					  return allRoutes.filter(route => !route.meta?.hidden)
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/stores/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/stores/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Pinia状态管理
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { createPinia } from 'pinia'
 | 
				
			||||||
 | 
					import { storage } from '../utils/storage'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 创建pinia实例
 | 
				
			||||||
 | 
					const pinia = createPinia()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 添加持久化插件
 | 
				
			||||||
 | 
					pinia.use(({ store }) => {
 | 
				
			||||||
 | 
					  // 从存储中恢复状态
 | 
				
			||||||
 | 
					  const storageKey = `store_${store.$id}`
 | 
				
			||||||
 | 
					  const savedState = storage.local.get(storageKey)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  if (savedState) {
 | 
				
			||||||
 | 
					    store.$patch(savedState)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 监听状态变化并持久化
 | 
				
			||||||
 | 
					  store.$subscribe((_, state) => {
 | 
				
			||||||
 | 
					    storage.local.set(storageKey, state)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default pinia
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 导出所有store
 | 
				
			||||||
 | 
					export * from './modules/user'
 | 
				
			||||||
 | 
					export * from './modules/app' 
 | 
				
			||||||
							
								
								
									
										129
									
								
								src/stores/modules/app.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/stores/modules/app.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 应用状态管理
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { defineStore } from 'pinia'
 | 
				
			||||||
 | 
					import { ref, computed } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Theme = 'light' | 'dark' | 'auto'
 | 
				
			||||||
 | 
					export type Language = 'zh-CN' | 'en-US'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AppConfig {
 | 
				
			||||||
 | 
					  theme: Theme
 | 
				
			||||||
 | 
					  language: Language
 | 
				
			||||||
 | 
					  collapsed: boolean
 | 
				
			||||||
 | 
					  loading: boolean
 | 
				
			||||||
 | 
					  title: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useAppStore = defineStore('app', () => {
 | 
				
			||||||
 | 
					  // 应用配置
 | 
				
			||||||
 | 
					  const config = ref<AppConfig>({
 | 
				
			||||||
 | 
					    theme: 'light',
 | 
				
			||||||
 | 
					    language: 'zh-CN',
 | 
				
			||||||
 | 
					    collapsed: false,
 | 
				
			||||||
 | 
					    loading: false,
 | 
				
			||||||
 | 
					    title: 'Vite应用'
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 侧边栏折叠状态
 | 
				
			||||||
 | 
					  const collapsed = computed(() => config.value.collapsed)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 主题
 | 
				
			||||||
 | 
					  const theme = computed(() => config.value.theme)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 语言
 | 
				
			||||||
 | 
					  const language = computed(() => config.value.language)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 加载状态
 | 
				
			||||||
 | 
					  const loading = computed(() => config.value.loading)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 应用标题
 | 
				
			||||||
 | 
					  const title = computed(() => config.value.title)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 设置主题
 | 
				
			||||||
 | 
					  const setTheme = (newTheme: Theme) => {
 | 
				
			||||||
 | 
					    config.value.theme = newTheme
 | 
				
			||||||
 | 
					    // 应用主题到文档
 | 
				
			||||||
 | 
					    document.documentElement.setAttribute('data-theme', newTheme)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 切换主题
 | 
				
			||||||
 | 
					  const toggleTheme = () => {
 | 
				
			||||||
 | 
					    const newTheme = theme.value === 'light' ? 'dark' : 'light'
 | 
				
			||||||
 | 
					    setTheme(newTheme)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 设置语言
 | 
				
			||||||
 | 
					  const setLanguage = (newLanguage: Language) => {
 | 
				
			||||||
 | 
					    config.value.language = newLanguage
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 切换侧边栏折叠状态
 | 
				
			||||||
 | 
					  const toggleCollapsed = () => {
 | 
				
			||||||
 | 
					    config.value.collapsed = !config.value.collapsed
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 设置侧边栏折叠状态
 | 
				
			||||||
 | 
					  const setCollapsed = (newCollapsed: boolean) => {
 | 
				
			||||||
 | 
					    config.value.collapsed = newCollapsed
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 设置加载状态
 | 
				
			||||||
 | 
					  const setLoading = (newLoading: boolean) => {
 | 
				
			||||||
 | 
					    config.value.loading = newLoading
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 设置应用标题
 | 
				
			||||||
 | 
					  const setTitle = (newTitle: string) => {
 | 
				
			||||||
 | 
					    config.value.title = newTitle
 | 
				
			||||||
 | 
					    document.title = newTitle
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 更新配置
 | 
				
			||||||
 | 
					  const updateConfig = (updates: Partial<AppConfig>) => {
 | 
				
			||||||
 | 
					    config.value = { ...config.value, ...updates }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 重置配置
 | 
				
			||||||
 | 
					  const resetConfig = () => {
 | 
				
			||||||
 | 
					    config.value = {
 | 
				
			||||||
 | 
					      theme: 'light',
 | 
				
			||||||
 | 
					      language: 'zh-CN',
 | 
				
			||||||
 | 
					      collapsed: false,
 | 
				
			||||||
 | 
					      loading: false,
 | 
				
			||||||
 | 
					      title: 'Vite应用'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 初始化应用
 | 
				
			||||||
 | 
					  const initApp = () => {
 | 
				
			||||||
 | 
					    // 应用主题
 | 
				
			||||||
 | 
					    setTheme(theme.value)
 | 
				
			||||||
 | 
					    // 设置标题
 | 
				
			||||||
 | 
					    setTitle(title.value)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    // 状态
 | 
				
			||||||
 | 
					    config,
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 计算属性
 | 
				
			||||||
 | 
					    collapsed,
 | 
				
			||||||
 | 
					    theme,
 | 
				
			||||||
 | 
					    language,
 | 
				
			||||||
 | 
					    loading,
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 方法
 | 
				
			||||||
 | 
					    setTheme,
 | 
				
			||||||
 | 
					    toggleTheme,
 | 
				
			||||||
 | 
					    setLanguage,
 | 
				
			||||||
 | 
					    toggleCollapsed,
 | 
				
			||||||
 | 
					    setCollapsed,
 | 
				
			||||||
 | 
					    setLoading,
 | 
				
			||||||
 | 
					    setTitle,
 | 
				
			||||||
 | 
					    updateConfig,
 | 
				
			||||||
 | 
					    resetConfig,
 | 
				
			||||||
 | 
					    initApp
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}) 
 | 
				
			||||||
							
								
								
									
										119
									
								
								src/stores/modules/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/stores/modules/user.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 用户状态管理
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { defineStore } from 'pinia'
 | 
				
			||||||
 | 
					import { ref, computed } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface UserInfo {
 | 
				
			||||||
 | 
					  id: number | string
 | 
				
			||||||
 | 
					  username: string
 | 
				
			||||||
 | 
					  nickname: string
 | 
				
			||||||
 | 
					  avatar?: string
 | 
				
			||||||
 | 
					  email?: string
 | 
				
			||||||
 | 
					  phone?: string
 | 
				
			||||||
 | 
					  roles: string[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useUserStore = defineStore('user', () => {
 | 
				
			||||||
 | 
					  // 用户信息
 | 
				
			||||||
 | 
					  const userInfo = ref<UserInfo | null>(null)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 访问令牌
 | 
				
			||||||
 | 
					  const accessToken = ref<string>('')
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 刷新令牌
 | 
				
			||||||
 | 
					  const refreshToken = ref<string>('')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 计算属性
 | 
				
			||||||
 | 
					  const isLoggedIn = computed(() => !!accessToken.value && !!userInfo.value)
 | 
				
			||||||
 | 
					  const username = computed(() => userInfo.value?.username || '')
 | 
				
			||||||
 | 
					  const nickname = computed(() => userInfo.value?.nickname || '')
 | 
				
			||||||
 | 
					  const roles = computed(() => userInfo.value?.roles || [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 设置用户信息
 | 
				
			||||||
 | 
					  const setUserInfo = (info: UserInfo) => {
 | 
				
			||||||
 | 
					    userInfo.value = info
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 设置访问令牌
 | 
				
			||||||
 | 
					  const setAccessToken = (token: string) => {
 | 
				
			||||||
 | 
					    accessToken.value = token
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 设置刷新令牌
 | 
				
			||||||
 | 
					  const setRefreshToken = (token: string) => {
 | 
				
			||||||
 | 
					    refreshToken.value = token
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 登录
 | 
				
			||||||
 | 
					  const login = async (loginData: { username: string; password: string }) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // 这里调用登录API
 | 
				
			||||||
 | 
					      // const response = await loginApi(loginData)
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // 模拟登录成功
 | 
				
			||||||
 | 
					      const mockUserInfo: UserInfo = {
 | 
				
			||||||
 | 
					        id: 1,
 | 
				
			||||||
 | 
					        username: loginData.username,
 | 
				
			||||||
 | 
					        nickname: loginData.username,
 | 
				
			||||||
 | 
					        roles: ['user']
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      setUserInfo(mockUserInfo)
 | 
				
			||||||
 | 
					      setAccessToken('mock-access-token')
 | 
				
			||||||
 | 
					      setRefreshToken('mock-refresh-token')
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      return { success: true, data: mockUserInfo }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('登录失败:', error)
 | 
				
			||||||
 | 
					      return { success: false, error: '登录失败' }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 登出
 | 
				
			||||||
 | 
					  const logout = () => {
 | 
				
			||||||
 | 
					    userInfo.value = null
 | 
				
			||||||
 | 
					    accessToken.value = ''
 | 
				
			||||||
 | 
					    refreshToken.value = ''
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 更新用户信息
 | 
				
			||||||
 | 
					  const updateUserInfo = (updates: Partial<UserInfo>) => {
 | 
				
			||||||
 | 
					    if (userInfo.value) {
 | 
				
			||||||
 | 
					      userInfo.value = { ...userInfo.value, ...updates }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查权限
 | 
				
			||||||
 | 
					  const hasRole = (role: string) => {
 | 
				
			||||||
 | 
					    return roles.value.includes(role)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查多个权限
 | 
				
			||||||
 | 
					  const hasAnyRole = (roleList: string[]) => {
 | 
				
			||||||
 | 
					    return roleList.some(role => hasRole(role))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    // 状态
 | 
				
			||||||
 | 
					    userInfo,
 | 
				
			||||||
 | 
					    accessToken,
 | 
				
			||||||
 | 
					    refreshToken,
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 计算属性
 | 
				
			||||||
 | 
					    isLoggedIn,
 | 
				
			||||||
 | 
					    username,
 | 
				
			||||||
 | 
					    nickname,
 | 
				
			||||||
 | 
					    roles,
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 方法
 | 
				
			||||||
 | 
					    setUserInfo,
 | 
				
			||||||
 | 
					    setAccessToken,
 | 
				
			||||||
 | 
					    setRefreshToken,
 | 
				
			||||||
 | 
					    login,
 | 
				
			||||||
 | 
					    logout,
 | 
				
			||||||
 | 
					    updateUserInfo,
 | 
				
			||||||
 | 
					    hasRole,
 | 
				
			||||||
 | 
					    hasAnyRole
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}) 
 | 
				
			||||||
							
								
								
									
										79
									
								
								src/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/style.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					:root {
 | 
				
			||||||
 | 
					  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  color-scheme: light dark;
 | 
				
			||||||
 | 
					  color: rgba(255, 255, 255, 0.87);
 | 
				
			||||||
 | 
					  background-color: #242424;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  font-synthesis: none;
 | 
				
			||||||
 | 
					  text-rendering: optimizeLegibility;
 | 
				
			||||||
 | 
					  -webkit-font-smoothing: antialiased;
 | 
				
			||||||
 | 
					  -moz-osx-font-smoothing: grayscale;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 {
 | 
				
			||||||
 | 
					  max-width: 1280px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					  padding: 2rem;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (prefers-color-scheme: light) {
 | 
				
			||||||
 | 
					  :root {
 | 
				
			||||||
 | 
					    color: #213547;
 | 
				
			||||||
 | 
					    background-color: #ffffff;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  a:hover {
 | 
				
			||||||
 | 
					    color: #747bff;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  button {
 | 
				
			||||||
 | 
					    background-color: #f9f9f9;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										116
									
								
								src/utils/storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/utils/storage.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,116 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 持久化存储工具
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface StorageOptions {
 | 
				
			||||||
 | 
					  prefix?: string
 | 
				
			||||||
 | 
					  storage?: Storage
 | 
				
			||||||
 | 
					  expire?: number
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class StorageUtil {
 | 
				
			||||||
 | 
					  private prefix: string
 | 
				
			||||||
 | 
					  private storage: Storage
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  constructor(options: StorageOptions = {}) {
 | 
				
			||||||
 | 
					    this.prefix = options.prefix || 'app_'
 | 
				
			||||||
 | 
					    this.storage = options.storage || window.localStorage
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 生成存储key
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private getKey(key: string): string {
 | 
				
			||||||
 | 
					    return `${this.prefix}${key}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 设置存储
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  set<T = any>(key: string, value: T, expire?: number): void {
 | 
				
			||||||
 | 
					    const storageData = {
 | 
				
			||||||
 | 
					      value,
 | 
				
			||||||
 | 
					      expire: expire ? Date.now() + expire * 1000 : null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.storage.setItem(this.getKey(key), JSON.stringify(storageData))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取存储
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  get<T = any>(key: string): T | null {
 | 
				
			||||||
 | 
					    const item = this.storage.getItem(this.getKey(key))
 | 
				
			||||||
 | 
					    if (!item) return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const storageData = JSON.parse(item)
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // 检查是否过期
 | 
				
			||||||
 | 
					      if (storageData.expire && Date.now() > storageData.expire) {
 | 
				
			||||||
 | 
					        this.remove(key)
 | 
				
			||||||
 | 
					        return null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      return storageData.value
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('Storage parse error:', error)
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 删除存储
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  remove(key: string): void {
 | 
				
			||||||
 | 
					    this.storage.removeItem(this.getKey(key))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 清空所有存储
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  clear(): void {
 | 
				
			||||||
 | 
					    const keys = Object.keys(this.storage)
 | 
				
			||||||
 | 
					    keys.forEach(key => {
 | 
				
			||||||
 | 
					      if (key.startsWith(this.prefix)) {
 | 
				
			||||||
 | 
					        this.storage.removeItem(key)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 获取所有存储的keys
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  keys(): string[] {
 | 
				
			||||||
 | 
					    const keys = Object.keys(this.storage)
 | 
				
			||||||
 | 
					    return keys
 | 
				
			||||||
 | 
					      .filter(key => key.startsWith(this.prefix))
 | 
				
			||||||
 | 
					      .map(key => key.replace(this.prefix, ''))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 检查是否存在
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  has(key: string): boolean {
 | 
				
			||||||
 | 
					    return this.get(key) !== null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 默认实例
 | 
				
			||||||
 | 
					export const localStorage = new StorageUtil({
 | 
				
			||||||
 | 
					  prefix: 'local_',
 | 
				
			||||||
 | 
					  storage: window.localStorage
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sessionStorage = new StorageUtil({
 | 
				
			||||||
 | 
					  prefix: 'session_',
 | 
				
			||||||
 | 
					  storage: window.sessionStorage
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 便捷方法
 | 
				
			||||||
 | 
					export const storage = {
 | 
				
			||||||
 | 
					  local: localStorage,
 | 
				
			||||||
 | 
					  session: sessionStorage,
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 创建自定义存储实例
 | 
				
			||||||
 | 
					  create: (options: StorageOptions) => new StorageUtil(options)
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
							
								
								
									
										109
									
								
								src/views/404.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/views/404.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="not-found">
 | 
				
			||||||
 | 
					    <div class="content">
 | 
				
			||||||
 | 
					      <h1>404</h1>
 | 
				
			||||||
 | 
					      <h2>页面未找到</h2>
 | 
				
			||||||
 | 
					      <p>抱歉,您访问的页面不存在。</p>
 | 
				
			||||||
 | 
					      <div class="actions">
 | 
				
			||||||
 | 
					        <router-link to="/" class="btn">返回首页</router-link>
 | 
				
			||||||
 | 
					        <button @click="goBack" class="btn btn-secondary">返回上页</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useRouter } from 'vue-router'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const goBack = () => {
 | 
				
			||||||
 | 
					  if (window.history.length > 1) {
 | 
				
			||||||
 | 
					    router.go(-1)
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    router.push('/')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.not-found {
 | 
				
			||||||
 | 
					  min-height: 100vh;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.content h1 {
 | 
				
			||||||
 | 
					  font-size: 6rem;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: #3182ce;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.content h2 {
 | 
				
			||||||
 | 
					  font-size: 2rem;
 | 
				
			||||||
 | 
					  margin: 1rem 0;
 | 
				
			||||||
 | 
					  color: #2d3748;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.content p {
 | 
				
			||||||
 | 
					  font-size: 1.2rem;
 | 
				
			||||||
 | 
					  color: #4a5568;
 | 
				
			||||||
 | 
					  margin-bottom: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.actions {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn {
 | 
				
			||||||
 | 
					  padding: 0.75rem 1.5rem;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  transition: all 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn {
 | 
				
			||||||
 | 
					  background: #3182ce;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn:hover {
 | 
				
			||||||
 | 
					  background: #2c5aa0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-secondary {
 | 
				
			||||||
 | 
					  background: #e2e8f0;
 | 
				
			||||||
 | 
					  color: #2d3748;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-secondary:hover {
 | 
				
			||||||
 | 
					  background: #cbd5e0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .content h2 {
 | 
				
			||||||
 | 
					  color: #e2e8f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .content p {
 | 
				
			||||||
 | 
					  color: #a0aec0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .btn-secondary {
 | 
				
			||||||
 | 
					  background: #4a5568;
 | 
				
			||||||
 | 
					  color: #e2e8f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .btn-secondary:hover {
 | 
				
			||||||
 | 
					  background: #2d3748;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style> 
 | 
				
			||||||
							
								
								
									
										241
									
								
								src/views/About.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								src/views/About.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,241 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="about">
 | 
				
			||||||
 | 
					    <el-row justify="center">
 | 
				
			||||||
 | 
					      <el-col :span="20" :md="16" :lg="14">
 | 
				
			||||||
 | 
					        <!-- 页面标题 -->
 | 
				
			||||||
 | 
					        <div class="page-header">
 | 
				
			||||||
 | 
					          <h1 class="page-title">关于我们</h1>
 | 
				
			||||||
 | 
					          <p class="page-desc">了解项目技术架构和功能特性</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- 技术栈部分 -->
 | 
				
			||||||
 | 
					        <el-card class="section-card" shadow="hover">
 | 
				
			||||||
 | 
					          <template #header>
 | 
				
			||||||
 | 
					            <div class="card-header">
 | 
				
			||||||
 | 
					              <el-icon class="header-icon"><Monitor /></el-icon>
 | 
				
			||||||
 | 
					              <span class="header-title">技术栈</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <el-space direction="vertical" size="large" style="width: 100%">
 | 
				
			||||||
 | 
					            <div v-for="tech in techStack" :key="tech.name" class="tech-item">
 | 
				
			||||||
 | 
					              <div class="tech-header">
 | 
				
			||||||
 | 
					                <el-tag :type="tech.type" size="large">{{ tech.name }}</el-tag>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <p class="tech-desc">{{ tech.description }}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </el-space>
 | 
				
			||||||
 | 
					        </el-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- 功能特性部分 -->
 | 
				
			||||||
 | 
					        <el-card class="section-card" shadow="hover">
 | 
				
			||||||
 | 
					          <template #header>
 | 
				
			||||||
 | 
					            <div class="card-header">
 | 
				
			||||||
 | 
					              <el-icon class="header-icon"><Star /></el-icon>
 | 
				
			||||||
 | 
					              <span class="header-title">功能特性</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <el-row :gutter="20">
 | 
				
			||||||
 | 
					            <el-col :span="12" :sm="8" :md="6" v-for="feature in features" :key="feature.title">
 | 
				
			||||||
 | 
					              <div class="feature-item">
 | 
				
			||||||
 | 
					                <div class="feature-icon">{{ feature.icon }}</div>
 | 
				
			||||||
 | 
					                <div class="feature-text">{{ feature.title }}</div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					          </el-row>
 | 
				
			||||||
 | 
					        </el-card>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <!-- 操作按钮 -->
 | 
				
			||||||
 | 
					        <div class="actions">
 | 
				
			||||||
 | 
					          <el-button 
 | 
				
			||||||
 | 
					            type="primary" 
 | 
				
			||||||
 | 
					            size="large"
 | 
				
			||||||
 | 
					            @click="$router.push('/')"
 | 
				
			||||||
 | 
					            icon="HomeFilled"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            返回首页
 | 
				
			||||||
 | 
					          </el-button>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <el-button 
 | 
				
			||||||
 | 
					            type="success" 
 | 
				
			||||||
 | 
					            size="large"
 | 
				
			||||||
 | 
					            @click="$router.push('/dashboard')"
 | 
				
			||||||
 | 
					            icon="DataBoard"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            前往控制台
 | 
				
			||||||
 | 
					          </el-button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </el-col>
 | 
				
			||||||
 | 
					    </el-row>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					// 技术栈数据
 | 
				
			||||||
 | 
					const techStack = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Vite',
 | 
				
			||||||
 | 
					    type: 'primary',
 | 
				
			||||||
 | 
					    description: '极速的前端构建工具'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Vue 3',
 | 
				
			||||||
 | 
					    type: 'success',
 | 
				
			||||||
 | 
					    description: '渐进式JavaScript框架'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'TypeScript',
 | 
				
			||||||
 | 
					    type: 'info',
 | 
				
			||||||
 | 
					    description: 'JavaScript的超集'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Axios',
 | 
				
			||||||
 | 
					    type: 'warning',
 | 
				
			||||||
 | 
					    description: '基于Promise的HTTP客户端,用于处理API请求和响应'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Pinia',
 | 
				
			||||||
 | 
					    type: 'danger',
 | 
				
			||||||
 | 
					    description: 'Vue.js的官方状态管理库'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Vue Router',
 | 
				
			||||||
 | 
					    type: 'primary',
 | 
				
			||||||
 | 
					    description: 'Vue.js的官方路由管理器'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 功能特性数据
 | 
				
			||||||
 | 
					const features = [
 | 
				
			||||||
 | 
					  { icon: '🎨', title: '主题切换' },
 | 
				
			||||||
 | 
					  { icon: '🔐', title: '用户认证' },
 | 
				
			||||||
 | 
					  { icon: '📱', title: '响应式设计' },
 | 
				
			||||||
 | 
					  { icon: '💾', title: '数据存储' },
 | 
				
			||||||
 | 
					  { icon: '🛣️', title: '动态路由' },
 | 
				
			||||||
 | 
					  { icon: '⚡', title: '热模块替换' }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.about {
 | 
				
			||||||
 | 
					  max-width: 1200px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 页面头部 */
 | 
				
			||||||
 | 
					.page-header {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  margin-bottom: 3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-title {
 | 
				
			||||||
 | 
					  font-size: 2.5rem;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  color: var(--el-color-primary);
 | 
				
			||||||
 | 
					  margin-bottom: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-desc {
 | 
				
			||||||
 | 
					  font-size: 1.2rem;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-regular);
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 卡片样式 */
 | 
				
			||||||
 | 
					.section-card {
 | 
				
			||||||
 | 
					  margin-bottom: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header-icon {
 | 
				
			||||||
 | 
					  font-size: 1.2rem;
 | 
				
			||||||
 | 
					  color: var(--el-color-primary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header-title {
 | 
				
			||||||
 | 
					  font-size: 1.1rem;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 技术栈项目 */
 | 
				
			||||||
 | 
					.tech-item {
 | 
				
			||||||
 | 
					  padding: 1rem 0;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid var(--el-border-color-lighter);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tech-item:last-child {
 | 
				
			||||||
 | 
					  border-bottom: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tech-header {
 | 
				
			||||||
 | 
					  margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tech-desc {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-regular);
 | 
				
			||||||
 | 
					  line-height: 1.6;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 功能特性项目 */
 | 
				
			||||||
 | 
					.feature-item {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					  margin-bottom: 1rem;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  background: var(--el-bg-color-page);
 | 
				
			||||||
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feature-item:hover {
 | 
				
			||||||
 | 
					  background: var(--el-color-primary-light-9);
 | 
				
			||||||
 | 
					  transform: translateY(-2px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feature-icon {
 | 
				
			||||||
 | 
					  font-size: 2rem;
 | 
				
			||||||
 | 
					  margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feature-text {
 | 
				
			||||||
 | 
					  font-size: 0.9rem;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-primary);
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 操作按钮 */
 | 
				
			||||||
 | 
					.actions {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  margin-top: 3rem;
 | 
				
			||||||
 | 
					  padding: 2rem 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 响应式设计 */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .page-title {
 | 
				
			||||||
 | 
					    font-size: 2rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .page-desc {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .feature-item {
 | 
				
			||||||
 | 
					    padding: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .feature-icon {
 | 
				
			||||||
 | 
					    font-size: 1.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .feature-text {
 | 
				
			||||||
 | 
					    font-size: 0.8rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style> 
 | 
				
			||||||
							
								
								
									
										34
									
								
								src/views/Admin.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/views/Admin.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="admin">
 | 
				
			||||||
 | 
					    <h1>管理中心</h1>
 | 
				
			||||||
 | 
					    <p>这是一个简单的管理页面示例</p>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <div class="actions">
 | 
				
			||||||
 | 
					      <router-link to="/" class="btn">返回首页</router-link>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.admin {
 | 
				
			||||||
 | 
					  padding: 2rem;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.actions {
 | 
				
			||||||
 | 
					  margin-top: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn {
 | 
				
			||||||
 | 
					  padding: 0.75rem 1.5rem;
 | 
				
			||||||
 | 
					  background: #3182ce;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  transition: background-color 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn:hover {
 | 
				
			||||||
 | 
					  background: #2c5aa0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style> 
 | 
				
			||||||
							
								
								
									
										360
									
								
								src/views/Dashboard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								src/views/Dashboard.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,360 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="dashboard">
 | 
				
			||||||
 | 
					    <!-- 欢迎区域 -->
 | 
				
			||||||
 | 
					    <el-row class="welcome-section">
 | 
				
			||||||
 | 
					      <el-col :span="24">
 | 
				
			||||||
 | 
					        <el-card class="welcome-card" shadow="never">
 | 
				
			||||||
 | 
					          <div class="welcome-content">
 | 
				
			||||||
 | 
					            <h1 class="welcome-title">
 | 
				
			||||||
 | 
					              <el-icon class="welcome-icon"><DataBoard /></el-icon>
 | 
				
			||||||
 | 
					              仪表板
 | 
				
			||||||
 | 
					            </h1>
 | 
				
			||||||
 | 
					            <p class="welcome-text">欢迎回来,{{ userStore.userInfo?.nickname || '用户' }}!</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </el-card>
 | 
				
			||||||
 | 
					      </el-col>
 | 
				
			||||||
 | 
					    </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 统计卡片 -->
 | 
				
			||||||
 | 
					    <el-row :gutter="24" class="stats-section">
 | 
				
			||||||
 | 
					      <el-col :xs="12" :sm="12" :md="6" v-for="stat in statsData" :key="stat.title">
 | 
				
			||||||
 | 
					        <el-card class="stat-card" shadow="hover">
 | 
				
			||||||
 | 
					          <div class="stat-header">
 | 
				
			||||||
 | 
					            <el-icon :class="['stat-icon', stat.iconClass]" :size="28">
 | 
				
			||||||
 | 
					              <component :is="stat.icon" />
 | 
				
			||||||
 | 
					            </el-icon>
 | 
				
			||||||
 | 
					            <span class="stat-title">{{ stat.title }}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="stat-content">
 | 
				
			||||||
 | 
					            <div class="stat-value">{{ stat.value }}</div>
 | 
				
			||||||
 | 
					            <div class="stat-desc">{{ stat.description }}</div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </el-card>
 | 
				
			||||||
 | 
					      </el-col>
 | 
				
			||||||
 | 
					    </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 功能区域 -->
 | 
				
			||||||
 | 
					    <el-row :gutter="24" class="bottom-section">
 | 
				
			||||||
 | 
					      <!-- 快速操作 -->
 | 
				
			||||||
 | 
					      <el-col :xs="24" :lg="12">
 | 
				
			||||||
 | 
					        <el-card class="action-card" shadow="hover">
 | 
				
			||||||
 | 
					          <template #header>
 | 
				
			||||||
 | 
					            <div class="card-header">
 | 
				
			||||||
 | 
					              <el-icon class="header-icon"><Operation /></el-icon>
 | 
				
			||||||
 | 
					              <span class="header-title">快速操作</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div class="action-buttons">
 | 
				
			||||||
 | 
					            <el-button 
 | 
				
			||||||
 | 
					              type="primary" 
 | 
				
			||||||
 | 
					              size="large"
 | 
				
			||||||
 | 
					              @click="appStore.toggleTheme"
 | 
				
			||||||
 | 
					              :icon="appStore.theme === 'dark' ? 'Sunny' : 'Moon'"
 | 
				
			||||||
 | 
					              class="action-button"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              切换主题 ({{ appStore.theme === 'dark' ? '暗黑' : '明亮' }})
 | 
				
			||||||
 | 
					            </el-button>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <el-button 
 | 
				
			||||||
 | 
					              type="success" 
 | 
				
			||||||
 | 
					              size="large"
 | 
				
			||||||
 | 
					              @click="$router.push('/profile')"
 | 
				
			||||||
 | 
					              icon="User"
 | 
				
			||||||
 | 
					              class="action-button"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              个人中心
 | 
				
			||||||
 | 
					            </el-button>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <el-button 
 | 
				
			||||||
 | 
					              v-if="userStore.hasRole('admin')"
 | 
				
			||||||
 | 
					              type="warning" 
 | 
				
			||||||
 | 
					              size="large"
 | 
				
			||||||
 | 
					              @click="$router.push('/admin')"
 | 
				
			||||||
 | 
					              icon="Setting"
 | 
				
			||||||
 | 
					              class="action-button"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              管理中心
 | 
				
			||||||
 | 
					            </el-button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </el-card>
 | 
				
			||||||
 | 
					      </el-col>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- 系统信息 -->
 | 
				
			||||||
 | 
					      <el-col :xs="24" :lg="12">
 | 
				
			||||||
 | 
					        <el-card class="info-card" shadow="hover">
 | 
				
			||||||
 | 
					          <template #header>
 | 
				
			||||||
 | 
					            <div class="card-header">
 | 
				
			||||||
 | 
					              <el-icon class="header-icon"><Monitor /></el-icon>
 | 
				
			||||||
 | 
					              <span class="header-title">系统信息</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <el-descriptions :column="1" size="default" border>
 | 
				
			||||||
 | 
					            <el-descriptions-item label="应用标题">
 | 
				
			||||||
 | 
					              {{ appStore.title }}
 | 
				
			||||||
 | 
					            </el-descriptions-item>
 | 
				
			||||||
 | 
					            <el-descriptions-item label="当前主题">
 | 
				
			||||||
 | 
					              <el-tag :type="appStore.theme === 'dark' ? 'info' : 'success'" size="small">
 | 
				
			||||||
 | 
					                {{ appStore.theme === 'dark' ? '暗黑模式' : '明亮模式' }}
 | 
				
			||||||
 | 
					              </el-tag>
 | 
				
			||||||
 | 
					            </el-descriptions-item>
 | 
				
			||||||
 | 
					            <el-descriptions-item label="语言设置">
 | 
				
			||||||
 | 
					              {{ appStore.language }}
 | 
				
			||||||
 | 
					            </el-descriptions-item>
 | 
				
			||||||
 | 
					            <el-descriptions-item label="本地存储">
 | 
				
			||||||
 | 
					              {{ getStorageInfo().localStorage }} 项
 | 
				
			||||||
 | 
					            </el-descriptions-item>
 | 
				
			||||||
 | 
					            <el-descriptions-item label="会话存储">
 | 
				
			||||||
 | 
					              {{ getStorageInfo().sessionStorage }} 项
 | 
				
			||||||
 | 
					            </el-descriptions-item>
 | 
				
			||||||
 | 
					          </el-descriptions>
 | 
				
			||||||
 | 
					        </el-card>
 | 
				
			||||||
 | 
					      </el-col>
 | 
				
			||||||
 | 
					    </el-row>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { computed } from 'vue'
 | 
				
			||||||
 | 
					import { useRouter } from 'vue-router'
 | 
				
			||||||
 | 
					import { useUserStore } from '../stores/modules/user'
 | 
				
			||||||
 | 
					import { useAppStore } from '../stores/modules/app'
 | 
				
			||||||
 | 
					import { storage } from '../utils/storage'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					const userStore = useUserStore()
 | 
				
			||||||
 | 
					const appStore = useAppStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getStorageInfo = () => {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    localStorage: storage.local.keys().length,
 | 
				
			||||||
 | 
					    sessionStorage: storage.session.keys().length
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 统计数据
 | 
				
			||||||
 | 
					const statsData = computed(() => [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    title: '用户信息',
 | 
				
			||||||
 | 
					    icon: 'User',
 | 
				
			||||||
 | 
					    iconClass: 'user-icon',
 | 
				
			||||||
 | 
					    value: userStore.userInfo?.username || '未登录',
 | 
				
			||||||
 | 
					    description: `角色: ${userStore.roles.join(', ') || '无'}`
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    title: '在线状态',
 | 
				
			||||||
 | 
					    icon: 'Connection',
 | 
				
			||||||
 | 
					    iconClass: 'status-icon',
 | 
				
			||||||
 | 
					    value: '在线',
 | 
				
			||||||
 | 
					    description: '连接正常'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    title: '存储使用',
 | 
				
			||||||
 | 
					    icon: 'FolderOpened',
 | 
				
			||||||
 | 
					    iconClass: 'storage-icon',
 | 
				
			||||||
 | 
					    value: `${getStorageInfo().localStorage + getStorageInfo().sessionStorage}`,
 | 
				
			||||||
 | 
					    description: '总存储项数'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    title: '应用版本',
 | 
				
			||||||
 | 
					    icon: 'Trophy',
 | 
				
			||||||
 | 
					    iconClass: 'version-icon',
 | 
				
			||||||
 | 
					    value: 'v1.0.0',
 | 
				
			||||||
 | 
					    description: '最新版本'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.dashboard {
 | 
				
			||||||
 | 
					  max-width: 1200px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 欢迎区域 */
 | 
				
			||||||
 | 
					.welcome-section {
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.welcome-card {
 | 
				
			||||||
 | 
					  background: linear-gradient(135deg, var(--el-color-primary-light-7) 0%, var(--el-color-primary-light-9) 100%);
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.welcome-content {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding: 20px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.welcome-title {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  gap: 12px;
 | 
				
			||||||
 | 
					  font-size: 2rem;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  color: var(--el-color-primary);
 | 
				
			||||||
 | 
					  margin: 0 0 10px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.welcome-icon {
 | 
				
			||||||
 | 
					  font-size: 2.2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.welcome-text {
 | 
				
			||||||
 | 
					  font-size: 1.1rem;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-regular);
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 统计卡片区域 */
 | 
				
			||||||
 | 
					.stats-section {
 | 
				
			||||||
 | 
					  margin-bottom: 24px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-card {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  transition: transform 0.3s ease, box-shadow 0.3s ease;
 | 
				
			||||||
 | 
					  height: 150px;
 | 
				
			||||||
 | 
					  margin-bottom: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-card:hover {
 | 
				
			||||||
 | 
					  transform: translateY(-5px);
 | 
				
			||||||
 | 
					  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  margin-bottom: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-icon {
 | 
				
			||||||
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-icon.user-icon {
 | 
				
			||||||
 | 
					  color: var(--el-color-primary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-icon.status-icon {
 | 
				
			||||||
 | 
					  color: var(--el-color-success);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-icon.storage-icon {
 | 
				
			||||||
 | 
					  color: var(--el-color-warning);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-icon.version-icon {
 | 
				
			||||||
 | 
					  color: var(--el-color-danger);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-title {
 | 
				
			||||||
 | 
					  font-size: 0.95rem;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-regular);
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-content {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-value {
 | 
				
			||||||
 | 
					  font-size: 1.6rem;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-primary);
 | 
				
			||||||
 | 
					  margin-bottom: 6px;
 | 
				
			||||||
 | 
					  line-height: 1.2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-desc {
 | 
				
			||||||
 | 
					  font-size: 0.85rem;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-secondary);
 | 
				
			||||||
 | 
					  line-height: 1.3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 功能区域 */
 | 
				
			||||||
 | 
					.bottom-section {
 | 
				
			||||||
 | 
					  margin-top: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.action-card,
 | 
				
			||||||
 | 
					.info-card {
 | 
				
			||||||
 | 
					  margin-bottom: 24px;
 | 
				
			||||||
 | 
					  min-height: 280px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 快速操作按钮 */
 | 
				
			||||||
 | 
					.action-buttons {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.action-button {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 44px;
 | 
				
			||||||
 | 
					  font-size: 0.95rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header-icon {
 | 
				
			||||||
 | 
					  font-size: 1.2rem;
 | 
				
			||||||
 | 
					  color: var(--el-color-primary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header-title {
 | 
				
			||||||
 | 
					  font-size: 1.1rem;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 响应式设计 */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .welcome-title {
 | 
				
			||||||
 | 
					    font-size: 1.6rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .welcome-text {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .stat-card {
 | 
				
			||||||
 | 
					    height: 120px;
 | 
				
			||||||
 | 
					    margin-bottom: 16px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .stat-value {
 | 
				
			||||||
 | 
					    font-size: 1.3rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .stat-title {
 | 
				
			||||||
 | 
					    font-size: 0.8rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .stat-desc {
 | 
				
			||||||
 | 
					    font-size: 0.7rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 暗黑主题适配 */
 | 
				
			||||||
 | 
					[data-theme="dark"] .welcome-card {
 | 
				
			||||||
 | 
					  background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .welcome-title {
 | 
				
			||||||
 | 
					  color: #60a5fa;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .welcome-text {
 | 
				
			||||||
 | 
					  color: #d1d5db;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style> 
 | 
				
			||||||
							
								
								
									
										195
									
								
								src/views/Home.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								src/views/Home.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,195 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="home">
 | 
				
			||||||
 | 
					    <!-- 主标题区域 -->
 | 
				
			||||||
 | 
					    <el-row justify="center" class="hero-section">
 | 
				
			||||||
 | 
					      <el-col :span="20">
 | 
				
			||||||
 | 
					        <div class="hero-content">
 | 
				
			||||||
 | 
					          <h1 class="hero-title">{{ appStore.title }}</h1>
 | 
				
			||||||
 | 
					          <p class="hero-subtitle">欢迎使用Vite + Vue 3 + Pinia + Vue Router框架</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </el-col>
 | 
				
			||||||
 | 
					    </el-row>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <!-- 技术特性卡片 -->
 | 
				
			||||||
 | 
					    <el-row justify="center">
 | 
				
			||||||
 | 
					      <el-col :span="20">
 | 
				
			||||||
 | 
					        <h2 class="section-title">技术特性</h2>
 | 
				
			||||||
 | 
					        <el-row :gutter="20" class="features-grid">
 | 
				
			||||||
 | 
					          <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="feature in features" :key="feature.name">
 | 
				
			||||||
 | 
					            <el-card class="feature-card" shadow="hover">
 | 
				
			||||||
 | 
					              <div class="feature-icon">{{ feature.icon }}</div>
 | 
				
			||||||
 | 
					              <h3 class="feature-name">{{ feature.name }}</h3>
 | 
				
			||||||
 | 
					              <p class="feature-desc">{{ feature.description }}</p>
 | 
				
			||||||
 | 
					            </el-card>
 | 
				
			||||||
 | 
					          </el-col>
 | 
				
			||||||
 | 
					        </el-row>
 | 
				
			||||||
 | 
					      </el-col>
 | 
				
			||||||
 | 
					    </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 快速操作 -->
 | 
				
			||||||
 | 
					    <el-row justify="center" class="actions-section">
 | 
				
			||||||
 | 
					      <el-col :span="20">
 | 
				
			||||||
 | 
					        <el-space size="large" wrap>
 | 
				
			||||||
 | 
					          <el-button 
 | 
				
			||||||
 | 
					            type="primary" 
 | 
				
			||||||
 | 
					            size="large"
 | 
				
			||||||
 | 
					            @click="appStore.toggleTheme"
 | 
				
			||||||
 | 
					            :icon="appStore.theme === 'dark' ? 'Sunny' : 'Moon'"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            切换主题 ({{ appStore.theme === 'dark' ? '暗黑' : '明亮' }})
 | 
				
			||||||
 | 
					          </el-button>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <el-button 
 | 
				
			||||||
 | 
					            type="success" 
 | 
				
			||||||
 | 
					            size="large"
 | 
				
			||||||
 | 
					            @click="$router.push('/about')"
 | 
				
			||||||
 | 
					            icon="InfoFilled"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            了解更多
 | 
				
			||||||
 | 
					          </el-button>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <el-button 
 | 
				
			||||||
 | 
					            v-if="userStore.isLoggedIn"
 | 
				
			||||||
 | 
					            type="primary" 
 | 
				
			||||||
 | 
					            size="large"
 | 
				
			||||||
 | 
					            @click="$router.push('/dashboard')"
 | 
				
			||||||
 | 
					            icon="DataBoard"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            控制台
 | 
				
			||||||
 | 
					          </el-button>
 | 
				
			||||||
 | 
					        </el-space>
 | 
				
			||||||
 | 
					      </el-col>
 | 
				
			||||||
 | 
					    </el-row>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useAppStore } from '../stores/modules/app'
 | 
				
			||||||
 | 
					import { useUserStore } from '../stores/modules/user'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const appStore = useAppStore()
 | 
				
			||||||
 | 
					const userStore = useUserStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 技术特性数据
 | 
				
			||||||
 | 
					const features = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Vite',
 | 
				
			||||||
 | 
					    icon: '✨',
 | 
				
			||||||
 | 
					    description: '极速的开发体验'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Vue 3',
 | 
				
			||||||
 | 
					    icon: '🚀',
 | 
				
			||||||
 | 
					    description: '组合式API,性能更强'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Axios',
 | 
				
			||||||
 | 
					    icon: '📡',
 | 
				
			||||||
 | 
					    description: 'HTTP客户端,API请求处理'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Pinia',
 | 
				
			||||||
 | 
					    icon: '🍍',
 | 
				
			||||||
 | 
					    description: '现代化状态管理'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Vue Router',
 | 
				
			||||||
 | 
					    icon: '🛣️',
 | 
				
			||||||
 | 
					    description: '动态路由配置'
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.home {
 | 
				
			||||||
 | 
					  max-width: 1200px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 主标题区域 */
 | 
				
			||||||
 | 
					.hero-section {
 | 
				
			||||||
 | 
					  padding: 40px 0;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hero-content {
 | 
				
			||||||
 | 
					  max-width: 600px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hero-title {
 | 
				
			||||||
 | 
					  font-size: 2.5rem;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  color: var(--el-color-primary);
 | 
				
			||||||
 | 
					  margin-bottom: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hero-subtitle {
 | 
				
			||||||
 | 
					  font-size: 1.2rem;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-regular);
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 技术特性区域 */
 | 
				
			||||||
 | 
					.section-title {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  font-size: 1.8rem;
 | 
				
			||||||
 | 
					  margin-bottom: 2rem;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-primary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.features-grid {
 | 
				
			||||||
 | 
					  margin-bottom: 3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feature-card {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  height: 180px;
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					  transition: transform 0.3s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feature-card:hover {
 | 
				
			||||||
 | 
					  transform: translateY(-5px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feature-icon {
 | 
				
			||||||
 | 
					  font-size: 3rem;
 | 
				
			||||||
 | 
					  margin-bottom: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feature-name {
 | 
				
			||||||
 | 
					  font-size: 1.2rem;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					  color: var(--el-text-color-primary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feature-desc {
 | 
				
			||||||
 | 
					  color: var(--el-text-color-regular);
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 操作按钮区域 */
 | 
				
			||||||
 | 
					.actions-section {
 | 
				
			||||||
 | 
					  padding: 40px 0;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 响应式设计 */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .hero-title {
 | 
				
			||||||
 | 
					    font-size: 2rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .hero-subtitle {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .feature-card {
 | 
				
			||||||
 | 
					    height: auto;
 | 
				
			||||||
 | 
					    min-height: 150px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style> 
 | 
				
			||||||
							
								
								
									
										186
									
								
								src/views/Login.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								src/views/Login.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,186 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="login">
 | 
				
			||||||
 | 
					    <div class="login-card">
 | 
				
			||||||
 | 
					      <h2>用户登录</h2>
 | 
				
			||||||
 | 
					      <form @submit.prevent="handleLogin">
 | 
				
			||||||
 | 
					        <div class="form-group">
 | 
				
			||||||
 | 
					          <label for="username">用户名</label>
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            id="username"
 | 
				
			||||||
 | 
					            v-model="loginForm.username"
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            placeholder="请输入用户名"
 | 
				
			||||||
 | 
					            required
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="form-group">
 | 
				
			||||||
 | 
					          <label for="password">密码</label>
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            id="password"
 | 
				
			||||||
 | 
					            v-model="loginForm.password"
 | 
				
			||||||
 | 
					            type="password"
 | 
				
			||||||
 | 
					            placeholder="请输入密码"
 | 
				
			||||||
 | 
					            required
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <button type="submit" class="login-btn" :disabled="loading">
 | 
				
			||||||
 | 
					          {{ loading ? '登录中...' : '登录' }}
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="tips">
 | 
				
			||||||
 | 
					        <p>提示:输入任意用户名和密码即可登录</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					import { useRouter, useRoute } from 'vue-router'
 | 
				
			||||||
 | 
					import { useUserStore } from '../stores/modules/user'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					const route = useRoute()
 | 
				
			||||||
 | 
					const userStore = useUserStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loading = ref(false)
 | 
				
			||||||
 | 
					const loginForm = ref({
 | 
				
			||||||
 | 
					  username: '',
 | 
				
			||||||
 | 
					  password: ''
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleLogin = async () => {
 | 
				
			||||||
 | 
					  if (!loginForm.value.username || !loginForm.value.password) {
 | 
				
			||||||
 | 
					    alert('请输入用户名和密码')
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  loading.value = true
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const result = await userStore.login(loginForm.value)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (result.success) {
 | 
				
			||||||
 | 
					      // 登录成功,跳转到目标页面或首页
 | 
				
			||||||
 | 
					      const redirect = route.query.redirect as string || '/'
 | 
				
			||||||
 | 
					      await router.push(redirect)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      alert(result.error || '登录失败')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error('登录错误:', error)
 | 
				
			||||||
 | 
					    alert('登录失败,请稍后重试')
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    loading.value = false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.login {
 | 
				
			||||||
 | 
					  min-height: 100vh;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.login-card {
 | 
				
			||||||
 | 
					  background: white;
 | 
				
			||||||
 | 
					  padding: 2rem;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  max-width: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.login-card h2 {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  margin-bottom: 2rem;
 | 
				
			||||||
 | 
					  color: #2d3748;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group {
 | 
				
			||||||
 | 
					  margin-bottom: 1.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group label {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					  color: #4a5568;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group input {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding: 0.75rem;
 | 
				
			||||||
 | 
					  border: 1px solid #e2e8f0;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  transition: border-color 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group input:focus {
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  border-color: #3182ce;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.login-btn {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding: 0.75rem;
 | 
				
			||||||
 | 
					  background: #3182ce;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: background-color 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.login-btn:hover:not(:disabled) {
 | 
				
			||||||
 | 
					  background: #2c5aa0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.login-btn:disabled {
 | 
				
			||||||
 | 
					  opacity: 0.6;
 | 
				
			||||||
 | 
					  cursor: not-allowed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tips {
 | 
				
			||||||
 | 
					  margin-top: 1.5rem;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tips p {
 | 
				
			||||||
 | 
					  color: #718096;
 | 
				
			||||||
 | 
					  font-size: 0.875rem;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .login-card {
 | 
				
			||||||
 | 
					  background: #2d3748;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .login-card h2 {
 | 
				
			||||||
 | 
					  color: #e2e8f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .form-group label {
 | 
				
			||||||
 | 
					  color: #a0aec0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .form-group input {
 | 
				
			||||||
 | 
					  background: #4a5568;
 | 
				
			||||||
 | 
					  border-color: #4a5568;
 | 
				
			||||||
 | 
					  color: #e2e8f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .tips p {
 | 
				
			||||||
 | 
					  color: #a0aec0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style> 
 | 
				
			||||||
							
								
								
									
										135
									
								
								src/views/Profile.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/views/Profile.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="profile">
 | 
				
			||||||
 | 
					    <h1>个人中心</h1>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <div class="profile-card">
 | 
				
			||||||
 | 
					      <h2>用户信息</h2>
 | 
				
			||||||
 | 
					      <div class="info-grid">
 | 
				
			||||||
 | 
					        <div class="info-item">
 | 
				
			||||||
 | 
					          <label>用户名:</label>
 | 
				
			||||||
 | 
					          <span>{{ userStore.username }}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="info-item">
 | 
				
			||||||
 | 
					          <label>昵称:</label>
 | 
				
			||||||
 | 
					          <span>{{ userStore.nickname }}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="info-item">
 | 
				
			||||||
 | 
					          <label>角色:</label>
 | 
				
			||||||
 | 
					          <span>{{ userStore.roles.join(', ') }}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <div class="actions">
 | 
				
			||||||
 | 
					      <button @click="handleLogout" class="btn btn-danger">退出登录</button>
 | 
				
			||||||
 | 
					      <router-link to="/" class="btn">返回首页</router-link>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useRouter } from 'vue-router'
 | 
				
			||||||
 | 
					import { useUserStore } from '../stores/modules/user'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					const userStore = useUserStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleLogout = () => {
 | 
				
			||||||
 | 
					  if (confirm('确定要退出登录吗?')) {
 | 
				
			||||||
 | 
					    userStore.logout()
 | 
				
			||||||
 | 
					    router.push('/login')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.profile {
 | 
				
			||||||
 | 
					  padding: 2rem;
 | 
				
			||||||
 | 
					  max-width: 600px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-card {
 | 
				
			||||||
 | 
					  background: #f8fafc;
 | 
				
			||||||
 | 
					  border: 1px solid #e2e8f0;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 2rem;
 | 
				
			||||||
 | 
					  margin: 2rem 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-card h2 {
 | 
				
			||||||
 | 
					  margin: 0 0 1.5rem 0;
 | 
				
			||||||
 | 
					  color: #2d3748;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.info-grid {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.info-item {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: 100px 1fr;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.info-item label {
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					  color: #4a5568;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.info-item span {
 | 
				
			||||||
 | 
					  color: #2d3748;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.actions {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn {
 | 
				
			||||||
 | 
					  padding: 0.75rem 1.5rem;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: all 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn {
 | 
				
			||||||
 | 
					  background: #3182ce;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn:hover {
 | 
				
			||||||
 | 
					  background: #2c5aa0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-danger {
 | 
				
			||||||
 | 
					  background: #e53e3e;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-danger:hover {
 | 
				
			||||||
 | 
					  background: #c53030;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .profile-card {
 | 
				
			||||||
 | 
					  background: #2d3748;
 | 
				
			||||||
 | 
					  border-color: #4a5568;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .profile-card h2 {
 | 
				
			||||||
 | 
					  color: #e2e8f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .info-item label {
 | 
				
			||||||
 | 
					  color: #a0aec0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[data-theme="dark"] .info-item span {
 | 
				
			||||||
 | 
					  color: #e2e8f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style> 
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					/// <reference types="vite/client" />
 | 
				
			||||||
							
								
								
									
										15
									
								
								tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "@vue/tsconfig/tsconfig.dom.json",
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Linting */
 | 
				
			||||||
 | 
					    "strict": true,
 | 
				
			||||||
 | 
					    "noUnusedLocals": true,
 | 
				
			||||||
 | 
					    "noUnusedParameters": true,
 | 
				
			||||||
 | 
					    "erasableSyntaxOnly": true,
 | 
				
			||||||
 | 
					    "noFallthroughCasesInSwitch": true,
 | 
				
			||||||
 | 
					    "noUncheckedSideEffectImports": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "target": "esnext",
 | 
				
			||||||
 | 
					    "useDefineForClassFields": true,
 | 
				
			||||||
 | 
					    "module": "esnext",
 | 
				
			||||||
 | 
					    "moduleResolution": "node",
 | 
				
			||||||
 | 
					    "strict": true,
 | 
				
			||||||
 | 
					    "jsx": "preserve",
 | 
				
			||||||
 | 
					    "noEmit": true,
 | 
				
			||||||
 | 
					    "resolveJsonModule": true,
 | 
				
			||||||
 | 
					    "isolatedModules": false,
 | 
				
			||||||
 | 
					    "esModuleInterop": true,
 | 
				
			||||||
 | 
					    "lib": ["esnext", "dom"],
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					    "baseUrl": "./",
 | 
				
			||||||
 | 
					    "paths": {
 | 
				
			||||||
 | 
					      "@/*": ["./src/*"],
 | 
				
			||||||
 | 
					      "script/*": ["src/script/*"]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "types": [
 | 
				
			||||||
 | 
					      "node"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
 | 
				
			||||||
 | 
					    "target": "ES2022",
 | 
				
			||||||
 | 
					    "lib": ["ES2023"],
 | 
				
			||||||
 | 
					    "module": "ESNext",
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Bundler mode */
 | 
				
			||||||
 | 
					    "moduleResolution": "bundler",
 | 
				
			||||||
 | 
					    "allowImportingTsExtensions": true,
 | 
				
			||||||
 | 
					    "verbatimModuleSyntax": true,
 | 
				
			||||||
 | 
					    "moduleDetection": "force",
 | 
				
			||||||
 | 
					    "noEmit": true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Linting */
 | 
				
			||||||
 | 
					    "strict": true,
 | 
				
			||||||
 | 
					    "noUnusedLocals": true,
 | 
				
			||||||
 | 
					    "noUnusedParameters": true,
 | 
				
			||||||
 | 
					    "erasableSyntaxOnly": true,
 | 
				
			||||||
 | 
					    "noFallthroughCasesInSwitch": true,
 | 
				
			||||||
 | 
					    "noUncheckedSideEffectImports": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["vite.config.ts"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					import { defineConfig } from 'vite'
 | 
				
			||||||
 | 
					import vue from '@vitejs/plugin-vue'
 | 
				
			||||||
 | 
					import { resolve } from 'path'
 | 
				
			||||||
 | 
					import legacy from '@vitejs/plugin-legacy'
 | 
				
			||||||
 | 
					import { visualizer } from 'rollup-plugin-visualizer'
 | 
				
			||||||
 | 
					// https://vitejs.dev/config/
 | 
				
			||||||
 | 
					export default defineConfig({
 | 
				
			||||||
 | 
					  base: './', //打包后的相对路径
 | 
				
			||||||
 | 
					  define: {
 | 
				
			||||||
 | 
					    'process.env': process.env //public环境
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  server: {
 | 
				
			||||||
 | 
					    host: true,
 | 
				
			||||||
 | 
					    port: 8080, //vite项目启动时自定义端口
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    open: true,
 | 
				
			||||||
 | 
					    proxy: {
 | 
				
			||||||
 | 
					      // 正则表达式写法
 | 
				
			||||||
 | 
					      '^/api': {
 | 
				
			||||||
 | 
					        target: 'http://192.168.3.151:3000/api', // 后端服务实际地址
 | 
				
			||||||
 | 
					        changeOrigin: true, //开启代理
 | 
				
			||||||
 | 
					        rewrite: (path) => path.replace(/^\/api/, '')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  plugins: [
 | 
				
			||||||
 | 
					    vue(),
 | 
				
			||||||
 | 
					    legacy({
 | 
				
			||||||
 | 
					      targets: ['defaults', 'not IE 11']
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    visualizer({
 | 
				
			||||||
 | 
					      open: true, // 构建完成后自动打开浏览器
 | 
				
			||||||
 | 
					      gzipSize: true,
 | 
				
			||||||
 | 
					      brotliSize: true
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  resolve: {
 | 
				
			||||||
 | 
					    //别名
 | 
				
			||||||
 | 
					    alias: {
 | 
				
			||||||
 | 
					      '@': resolve(__dirname, './src'),
 | 
				
			||||||
 | 
					      components: resolve(__dirname, './src/components'),
 | 
				
			||||||
 | 
					      script: resolve(__dirname, './src/script'),
 | 
				
			||||||
 | 
					      utils: resolve(__dirname, './src/utils')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  build: {
 | 
				
			||||||
 | 
					    assetsDir: 'static', //打包后的公共文件夹名
 | 
				
			||||||
 | 
					    target: 'es2015',
 | 
				
			||||||
 | 
					    cssTarget: ['chrome61'],
 | 
				
			||||||
 | 
					    chunkSizeWarningLimit: 5000
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
		Reference in New Issue
	
	Block a user