# EMS 前端设计文档 ## 1. 技术栈 - **前端框架**: Vue 3 - **构建工具**: Vite - **状态管理**: Pinia - **路由管理**: Vue Router - **UI 组件库**: Element Plus - **HTTP 客户端**: Axios - **CSS 预处理器**: SCSS - **类型检查**: TypeScript - **地图组件**: Leaflet.js - **图表库**: ECharts - **表单验证**: Vee-Validate - **国际化**: Vue I18n - **测试框架**: Vitest ## 2. 架构设计 ### 2.1 目录结构 ``` ems-frontend/ ├── public/ # 静态资源 ├── src/ │ ├── api/ # API 请求模块 │ │ ├── auth.ts # 认证相关 API │ │ ├── dashboard.ts # 仪表盘相关 API │ │ └── ... │ ├── assets/ # 静态资源 │ │ ├── styles/ # 全局样式 │ │ └── images/ # 图片资源 │ ├── components/ # 通用组件 │ │ ├── common/ # 基础组件 │ │ ├── layout/ # 布局组件 │ │ └── business/ # 业务组件 │ ├── composables/ # 组合式函数 │ ├── config/ # 配置文件 │ ├── constants/ # 常量定义 │ ├── directives/ # 自定义指令 │ ├── hooks/ # 自定义 Hooks │ ├── locales/ # 国际化资源 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia 状态管理 │ ├── types/ # TypeScript 类型定义 │ ├── utils/ # 工具函数 │ ├── views/ # 页面组件 │ │ ├── auth/ # 认证相关页面 │ │ ├── dashboard/ # 仪表盘页面 │ │ └── ... │ ├── App.vue # 根组件 │ ├── main.ts # 入口文件 │ └── env.d.ts # 环境变量类型声明 ├── .env # 环境变量 ├── .env.development # 开发环境变量 ├── .env.production # 生产环境变量 ├── index.html # HTML 模板 ├── package.json # 依赖配置 ├── tsconfig.json # TypeScript 配置 └── vite.config.ts # Vite 配置 ``` ### 2.2 核心模块 1. **认证模块**: 处理用户登录、注册、密码重置等功能 2. **仪表盘模块**: 展示系统概览、数据统计和图表 3. **地图模块**: 展示地理信息、网格分布和污染热图 4. **任务管理模块**: 处理任务分配、查看和审核 5. **反馈管理模块**: 处理用户反馈的提交和管理 6. **人员管理模块**: 管理系统用户、角色和权限 7. **个人中心模块**: 查看和修改个人信息 ## 3. 页面结构 ### 3.1 基础布局 系统将采用以下几种基础布局: 1. **主布局**: 包含侧边导航栏、顶部导航栏和内容区域 2. **认证布局**: 用于登录、注册等不需要导航的页面 3. **空白布局**: 用于全屏展示的页面,如地图全屏模式 ### 3.2 响应式设计 - 采用移动优先的响应式设计 - 断点设置: - 移动端: < 768px - 平板: 768px - 1024px - 桌面: > 1024px - 关键组件的响应式行为: - 侧边栏在移动端变为可折叠抽屉 - 表格在移动端优化为卡片式布局 - 表单在移动端采用单列布局 ## 4. 路由设计 ### 4.1 路由结构 ```javascript const routes = [ // 认证相关路由 { path: '/auth', component: AuthLayout, children: [ { path: 'login', component: LoginView }, { path: 'register', component: RegisterView }, { path: 'forgot-password', component: ForgotPasswordView }, { path: 'reset-password', component: ResetPasswordView }, ], meta: { requiresAuth: false } }, // 主应用路由 { path: '/', component: MainLayout, children: [ { path: '', component: DashboardView, meta: { requiresAuth: true, roles: ['ADMIN', 'DECISION_MAKER'] } }, { path: 'map', component: MapView, meta: { requiresAuth: false } }, { path: 'tasks', component: TasksView, meta: { requiresAuth: true } }, { path: 'feedback', component: FeedbackView, meta: { requiresAuth: true } }, { path: 'personnel', component: PersonnelView, meta: { requiresAuth: true, roles: ['ADMIN'] } }, { path: 'profile', component: ProfileView, meta: { requiresAuth: true } }, ] }, // 错误页面路由 { path: '/404', component: NotFoundView }, { path: '/403', component: ForbiddenView }, { path: '/:pathMatch(.*)*', redirect: '/404' } ] ``` ### 4.2 路由守卫 ```javascript router.beforeEach((to, from, next) => { const authStore = useAuthStore(); // 检查路由是否需要认证 const requiresAuth = to.matched.some(record => record.meta.requiresAuth !== false); // 检查用户角色权限 const hasRequiredRole = to.matched.every(record => { if (!record.meta.roles) return true; return authStore.user && record.meta.roles.includes(authStore.user.role); }); if (requiresAuth && !authStore.isAuthenticated) { // 未登录,重定向到登录页 next({ path: '/auth/login', query: { redirect: to.fullPath } }); } else if (requiresAuth && !hasRequiredRole) { // 无权限访问 next({ path: '/403' }); } else { // 正常导航 next(); } }); ``` ## 5. 状态管理 ### 5.1 Pinia Store 设计 ```typescript // 认证状态管理 export const useAuthStore = defineStore('auth', { state: () => ({ user: null, token: localStorage.getItem('token'), error: null, }), getters: { isAuthenticated: state => !!state.token, userRole: state => state.user?.role || null, }, actions: { async login(credentials) { /* 实现登录逻辑 */ }, async logout() { /* 实现登出逻辑 */ }, async fetchUserProfile() { /* 获取用户信息 */ }, } }); // 任务状态管理 export const useTaskStore = defineStore('task', { state: () => ({ tasks: [], currentTask: null, loading: false, error: null, }), getters: { tasksByStatus: state => status => state.tasks.filter(task => task.status === status), }, actions: { async fetchTasks(filters) { /* 获取任务列表 */ }, async fetchTaskById(id) { /* 获取任务详情 */ }, async assignTask(taskId, userId) { /* 分配任务 */ }, async submitTask(taskId, data) { /* 提交任务 */ }, } }); // 其他模块状态管理... ``` ### 5.2 持久化策略 - 使用 localStorage 存储认证 token - 关键用户设置使用 localStorage 持久化 - 敏感数据不进行持久化,每次会话重新获取 ## 6. API 请求封装 ### 6.1 基础请求客户端 ```typescript // api/client.ts import axios from 'axios'; import { useAuthStore } from '@/stores/auth'; const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, headers: { 'Content-Type': 'application/json', } }); // 请求拦截器 apiClient.interceptors.request.use( config => { const authStore = useAuthStore(); if (authStore.token) { config.headers.Authorization = `Bearer ${authStore.token}`; } return config; }, error => Promise.reject(error) ); // 响应拦截器 apiClient.interceptors.response.use( response => response, error => { const authStore = useAuthStore(); // 处理 401 未授权错误 if (error.response && error.response.status === 401) { authStore.logout(); router.push('/auth/login'); } return Promise.reject(error); } ); export default apiClient; ``` ### 6.2 模块化 API 封装 ```typescript // api/auth.ts import apiClient from './client'; export const authApi = { login: (credentials) => apiClient.post('/auth/login', credentials), register: (userData) => apiClient.post('/auth/signup', userData), requestPasswordReset: (email) => apiClient.post('/auth/request-password-reset', { email }), resetPassword: (data) => apiClient.post('/auth/reset-password', data), }; // api/tasks.ts import apiClient from './client'; export const taskApi = { getTasks: (params) => apiClient.get('/management/tasks', { params }), getTaskById: (id) => apiClient.get(`/management/tasks/${id}`), assignTask: (taskId, data) => apiClient.post(`/management/tasks/${taskId}/assign`, data), reviewTask: (taskId, data) => apiClient.post(`/management/tasks/${taskId}/review`, data), }; // 其他模块 API... ``` ## 7. 组件设计 ### 7.1 基础组件 - **AppButton**: 统一按钮样式和行为 - **AppCard**: 卡片容器组件 - **AppForm**: 表单容器组件 - **AppInput**: 输入框组件 - **AppSelect**: 下拉选择组件 - **AppTable**: 表格组件 - **AppPagination**: 分页组件 - **AppModal**: 模态框组件 - **AppAlert**: 提示框组件 ### 7.2 业务组件 - **TaskCard**: 任务卡片组件 - **FeedbackForm**: 反馈提交表单 - **UserSelector**: 用户选择器 - **StatusBadge**: 状态标签组件 - **MapGrid**: 地图网格组件 - **PollutionTypeIcon**: 污染类型图标 - **TaskFilterPanel**: 任务筛选面板 - **StatisticCard**: 统计数据卡片 ## 8. 主题设计 ### 8.1 色彩系统 - **主色**: #1890ff (蓝色) - **成功色**: #52c41a (绿色) - **警告色**: #faad14 (黄色) - **错误色**: #f5222d (红色) - **中性色**: - 标题: #262626 - 正文: #595959 - 辅助文字: #8c8c8c - 禁用: #bfbfbf - 边框: #d9d9d9 - 分割线: #f0f0f0 - 背景: #f5f5f5 - 表格头部: #fafafa ### 8.2 字体系统 - **主字体**: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif - **代码字体**: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace - **字体大小**: - 小号: 12px - 正文: 14px - 标题: 16px, 18px, 20px, 24px ### 8.3 间距系统 - **基础单位**: 4px - **间距尺寸**: - xs: 4px - sm: 8px - md: 16px - lg: 24px - xl: 32px - xxl: 48px ### 8.4 阴影系统 - **轻微阴影**: 0 2px 8px rgba(0, 0, 0, 0.15) - **中等阴影**: 0 4px 12px rgba(0, 0, 0, 0.15) - **深度阴影**: 0 8px 16px rgba(0, 0, 0, 0.15) ## 9. 国际化 支持中文和英文两种语言,使用 Vue I18n 实现国际化。 ```typescript // locales/zh-CN.json { "auth": { "login": "登录", "register": "注册", "forgotPassword": "忘记密码", "email": "邮箱", "password": "密码", "confirmPassword": "确认密码" }, "dashboard": { "title": "仪表盘", "stats": "统计数据", "tasks": "任务概览", "feedback": "反馈概览" } } // locales/en-US.json { "auth": { "login": "Login", "register": "Register", "forgotPassword": "Forgot Password", "email": "Email", "password": "Password", "confirmPassword": "Confirm Password" }, "dashboard": { "title": "Dashboard", "stats": "Statistics", "tasks": "Task Overview", "feedback": "Feedback Overview" } } ``` ## 10. 权限控制 ### 10.1 角色定义 - **ADMIN**: 系统管理员,拥有所有权限 - **SUPERVISOR**: 主管,负责任务分配和审核 - **GRID_WORKER**: 网格员,负责执行任务 - **DECISION_MAKER**: 决策者,查看数据和报表 ### 10.2 权限指令 ```typescript // 创建自定义指令控制元素的显示/隐藏 app.directive('permission', { mounted(el, binding) { const { value } = binding; const authStore = useAuthStore(); if (value && !authStore.hasPermission(value)) { el.parentNode && el.parentNode.removeChild(el); } } }); // 使用示例
管理内容
``` ## 11. 错误处理 ### 11.1 全局错误处理 ```typescript // 全局错误处理 app.config.errorHandler = (err, vm, info) => { console.error('全局错误:', err); // 上报错误到监控系统 errorReportingService.report(err, { component: vm?.$options?.name, info, user: useAuthStore().user?.id }); // 显示用户友好的错误提示 ElMessage.error('操作失败,请稍后重试'); }; ``` ### 11.2 API 错误处理 ```typescript // 统一处理 API 错误 export const handleApiError = (error) => { if (error.response) { // 服务器响应错误 const { status, data } = error.response; switch (status) { case 400: ElMessage.error(data.message || '请求参数错误'); break; case 401: ElMessage.error('会话已过期,请重新登录'); break; case 403: ElMessage.error('没有权限执行此操作'); break; case 404: ElMessage.error('请求的资源不存在'); break; case 500: ElMessage.error('服务器内部错误,请稍后重试'); break; default: ElMessage.error('请求失败,请稍后重试'); } } else if (error.request) { // 请求发出但没有收到响应 ElMessage.error('网络连接失败,请检查网络设置'); } else { // 请求配置错误 ElMessage.error('请求配置错误'); } return Promise.reject(error); }; ``` ## 12. 性能优化 ### 12.1 代码分割 - 使用动态导入实现路由级别的代码分割 - 将大型第三方库单独打包 ### 12.2 资源优化 - 图片资源使用 WebP 格式并进行压缩 - 使用 SVG 图标代替图片图标 - 使用字体图标减少 HTTP 请求 ### 12.3 渲染优化 - 使用虚拟滚动处理长列表 - 合理使用 `v-show` 和 `v-if` - 大型表格使用分页加载 - 使用 `keep-alive` 缓存组件状态 ## 13. 测试策略 ### 13.1 单元测试 - 使用 Vitest 进行单元测试 - 重点测试工具函数、Hooks 和小型组件 - 使用 Vue Test Utils 测试组件 ### 13.2 端到端测试 - 使用 Cypress 进行端到端测试 - 覆盖关键用户流程: - 用户注册和登录 - 任务创建和分配 - 反馈提交和处理 ## 14. 部署策略 ### 14.1 构建配置 - 开发环境: `vite build --mode development` - 测试环境: `vite build --mode staging` - 生产环境: `vite build --mode production` ### 14.2 环境变量 ``` # .env.development VITE_API_BASE_URL=http://localhost:8080/api VITE_APP_TITLE=EMS开发环境 # .env.production VITE_API_BASE_URL=/api VITE_APP_TITLE=环境监测系统 ``` ### 14.3 CI/CD 流程 - 使用 GitHub Actions 自动化构建和部署 - 提交到 `develop` 分支自动部署到测试环境 - 提交到 `main` 分支自动部署到生产环境