From 72f7c891f364b42a046bf0932cd0ce3a97fbd6f2 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Sun, 26 Apr 2026 18:52:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E3=80=81API=E3=80=81=E9=A1=B5=E9=9D=A2=E7=AD=89=E5=A4=A7?= =?UTF-8?q?=E9=87=8F=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 4 + .env.production | 4 + design-system/ether-pms/MASTER.md | 202 ++++++ src/api/dept.ts | 6 +- src/api/inspection-template.ts | 23 +- src/api/maintenance.ts | 49 +- src/api/permission.ts | 19 +- src/api/project.ts | 28 +- src/api/role.ts | 29 +- src/api/space.ts | 25 +- src/api/sparepart.ts | 13 +- src/api/user.ts | 28 +- .../BuildingVisualization/index.vue | 648 ++++++++++++++---- src/components/Card/index.vue | 18 +- src/components/EmptyState/index.vue | 6 +- src/components/PageHeader/index.vue | 2 + src/components/ProjectSelect/index.vue | 67 -- src/components/SpaceTree/index.vue | 344 ++++++---- src/components/StatusTag/index.vue | 18 +- src/components/TableActions/index.vue | 12 - src/components/TableCard/index.vue | 59 -- src/components/index.ts | 7 +- src/config/constants.ts | 22 + src/router/index.ts | 34 +- src/stores/user.ts | 6 +- src/styles/building-colors.css | 40 ++ src/styles/common.css | 76 -- src/styles/page-common.css | 184 +++-- src/types/index.ts | 21 +- src/types/projectMember.ts | 4 +- src/types/space.ts | 1 + src/utils/error-handler.ts | 26 + src/utils/request.ts | 127 +++- src/views/Dashboard.vue | 485 ++++++++----- src/views/Layout.vue | 3 +- src/views/auth/Login.vue | 38 +- src/views/energy/ConsumptionRecord.vue | 2 +- src/views/energy/EnergyStatistics.vue | 4 +- src/views/energy/MeterList.vue | 20 +- .../equipment/components/DocumentManager.vue | 2 +- .../equipment/components/PhotoManager.vue | 2 +- src/views/inspection/TemplateList.vue | 64 +- src/views/maintenance/PlanList.vue | 24 +- src/views/maintenance/TaskList.vue | 10 +- src/views/project/List.vue | 63 +- .../project/components/ProjectSelector.vue | 8 +- src/views/sparepart/SparePartList.vue | 37 +- src/views/sparepart/StockOperation.vue | 5 +- src/views/system/Audit.vue | 28 +- src/views/system/Depts.vue | 97 ++- src/views/system/Permissions.vue | 45 +- src/views/system/Roles.vue | 125 ++-- src/views/system/Settings.vue | 4 +- src/views/system/UserDetail.vue | 8 +- src/views/system/Users.vue | 164 +++-- tests/space-batch-e2e.spec.ts | 630 +++++++++++++++++ vite.config.ts | 45 +- 57 files changed, 2835 insertions(+), 1230 deletions(-) create mode 100644 .env.development create mode 100644 .env.production create mode 100644 design-system/ether-pms/MASTER.md delete mode 100644 src/components/ProjectSelect/index.vue delete mode 100644 src/components/TableCard/index.vue create mode 100644 src/config/constants.ts create mode 100644 src/styles/building-colors.css delete mode 100644 src/styles/common.css create mode 100644 src/utils/error-handler.ts create mode 100644 tests/space-batch-e2e.spec.ts diff --git a/.env.development b/.env.development new file mode 100644 index 00000000..e81c18bf --- /dev/null +++ b/.env.development @@ -0,0 +1,4 @@ +# 开发环境配置 +VITE_API_BASE_URL=http://localhost:8080 +VITE_APP_TITLE=Ether 管理后台 - 开发 +VITE_REQUEST_TIMEOUT=10000 diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..1bc0f0e3 --- /dev/null +++ b/.env.production @@ -0,0 +1,4 @@ +# 生产环境配置 +VITE_API_BASE_URL=/ +VITE_APP_TITLE=Ether 管理后台 +VITE_REQUEST_TIMEOUT=15000 diff --git a/design-system/ether-pms/MASTER.md b/design-system/ether-pms/MASTER.md new file mode 100644 index 00000000..65a8b1d1 --- /dev/null +++ b/design-system/ether-pms/MASTER.md @@ -0,0 +1,202 @@ +# Design System Master File + +> **LOGIC:** When building a specific page, first check `design-system/pages/[page-name].md`. +> If that file exists, its rules **override** this Master file. +> If not, strictly follow the rules below. + +--- + +**Project:** Ether PMS +**Generated:** 2026-04-13 09:34:11 +**Category:** Analytics Dashboard + +--- + +## Global Rules + +### Color Palette + +| Role | Hex | CSS Variable | +|------|-----|--------------| +| Primary | `#0F766E` | `--color-primary` | +| Secondary | `#14B8A6` | `--color-secondary` | +| CTA/Accent | `#0369A1` | `--color-cta` | +| Background | `#F0FDFA` | `--color-background` | +| Text | `#134E4A` | `--color-text` | + +**Color Notes:** Trust teal + professional blue + +### Typography + +- **Heading Font:** Fira Code +- **Body Font:** Fira Sans +- **Mood:** dashboard, data, analytics, code, technical, precise +- **Google Fonts:** [Fira Code + Fira Sans](https://fonts.google.com/share?selection.family=Fira+Code:wght@400;500;600;700|Fira+Sans:wght@300;400;500;600;700) + +**CSS Import:** +```css +@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap'); +``` + +### Spacing Variables + +| Token | Value | Usage | +|-------|-------|-------| +| `--space-xs` | `4px` / `0.25rem` | Tight gaps | +| `--space-sm` | `8px` / `0.5rem` | Icon gaps, inline spacing | +| `--space-md` | `16px` / `1rem` | Standard padding | +| `--space-lg` | `24px` / `1.5rem` | Section padding | +| `--space-xl` | `32px` / `2rem` | Large gaps | +| `--space-2xl` | `48px` / `3rem` | Section margins | +| `--space-3xl` | `64px` / `4rem` | Hero padding | + +### Shadow Depths + +| Level | Value | Usage | +|-------|-------|-------| +| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift | +| `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons | +| `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns | +| `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards | + +--- + +## Component Specs + +### Buttons + +```css +/* Primary Button */ +.btn-primary { + background: #0369A1; + color: white; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; +} + +.btn-primary:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +/* Secondary Button */ +.btn-secondary { + background: transparent; + color: #0F766E; + border: 2px solid #0F766E; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + transition: all 200ms ease; + cursor: pointer; +} +``` + +### Cards + +```css +.card { + background: #F0FDFA; + border-radius: 12px; + padding: 24px; + box-shadow: var(--shadow-md); + transition: all 200ms ease; + cursor: pointer; +} + +.card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-2px); +} +``` + +### Inputs + +```css +.input { + padding: 12px 16px; + border: 1px solid #E2E8F0; + border-radius: 8px; + font-size: 16px; + transition: border-color 200ms ease; +} + +.input:focus { + border-color: #0F766E; + outline: none; + box-shadow: 0 0 0 3px #0F766E20; +} +``` + +### Modals + +```css +.modal-overlay { + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); +} + +.modal { + background: white; + border-radius: 16px; + padding: 32px; + box-shadow: var(--shadow-xl); + max-width: 500px; + width: 90%; +} +``` + +--- + +## Style Guidelines + +**Style:** Data-Dense Dashboard + +**Keywords:** Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility + +**Best For:** Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing + +**Key Effects:** Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners + +### Page Pattern + +**Pattern Name:** Data-Dense + Drill-Down + +- **CTA Placement:** Above fold +- **Section Order:** Hero > Features > CTA + +--- + +## Anti-Patterns (Do NOT Use) + +- ❌ Ornate design +- ❌ No filtering + +### Additional Forbidden Patterns + +- ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons) +- ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer +- ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout +- ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio +- ❌ **Instant state changes** — Always use transitions (150-300ms) +- ❌ **Invisible focus states** — Focus states must be visible for a11y + +--- + +## Pre-Delivery Checklist + +Before delivering any UI code, verify: + +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons from consistent icon set (Heroicons/Lucide) +- [ ] `cursor-pointer` on all clickable elements +- [ ] Hover states with smooth transitions (150-300ms) +- [ ] Light mode: text contrast 4.5:1 minimum +- [ ] Focus states visible for keyboard navigation +- [ ] `prefers-reduced-motion` respected +- [ ] Responsive: 375px, 768px, 1024px, 1440px +- [ ] No content hidden behind fixed navbars +- [ ] No horizontal scroll on mobile diff --git a/src/api/dept.ts b/src/api/dept.ts index 08c215bd..3524b371 100644 --- a/src/api/dept.ts +++ b/src/api/dept.ts @@ -5,22 +5,20 @@ export interface Dept { id?: string parentId?: string deptName: string - deptCode?: string deptType?: string deptTypeDesc?: string - defaultRoleCode?: string leaderId?: string sortOrder?: number status?: string + deptCode?: string + defaultRoleCode?: string children?: Dept[] } export interface DeptDTO { deptName: string - deptCode?: string parentId?: string deptType?: string - defaultRoleCode?: string leaderId?: string sortOrder?: number } diff --git a/src/api/inspection-template.ts b/src/api/inspection-template.ts index f0ca8748..e73a6a00 100644 --- a/src/api/inspection-template.ts +++ b/src/api/inspection-template.ts @@ -1,4 +1,5 @@ import request from '@/utils/request' +import type { ApiResponse } from '@/types' // ==================== 点检模板类型 ==================== @@ -37,39 +38,27 @@ export interface TemplateFormData { // 获取模板列表 export function getInspectionTemplates(projectId: string) { - return request.get('/api/ops/inspection-templates', { + return request.get>('/api/ops/inspection-templates', { params: { projectId } }) } // 获取模板详情 export function getInspectionTemplateDetail(id: string) { - return request.get(`/api/ops/inspection-templates/${id}`) + return request.get>(`/api/ops/inspection-templates/${id}`) } // 创建模板 export function createInspectionTemplate(data: TemplateFormData) { - return request.post('/api/ops/inspection-templates', data) + return request.post('/api/ops/inspection-templates', data) } // 更新模板 export function updateInspectionTemplate(id: string, data: TemplateFormData) { - return request.put(`/api/ops/inspection-templates/${id}`, data) -} - -// 复制模板 -export function copyInspectionTemplate(id: string, targetProjectId?: string) { - return request.post(`/api/ops/inspection-templates/${id}/copy`, { - targetProjectId - }) -} - -// 按设备类型获取模板 -export function getTemplatesByEquipmentType(equipmentType: string) { - return request.get(`/api/ops/inspection-templates/by-type/${equipmentType}`) + return request.put(`/api/ops/inspection-templates/${id}`, data) } // 删除模板 export function deleteInspectionTemplate(id: string) { return request.delete(`/api/ops/inspection-templates/${id}`) -} \ No newline at end of file +} diff --git a/src/api/maintenance.ts b/src/api/maintenance.ts index 1eab5be0..084bb062 100644 --- a/src/api/maintenance.ts +++ b/src/api/maintenance.ts @@ -1,4 +1,5 @@ import request from '@/utils/request' +import type { ApiResponse } from '@/types' // ==================== 维保计划相关类型 ==================== @@ -72,85 +73,61 @@ export interface TaskQueryParams { // 获取维保计划列表 export function getMaintenancePlans(projectId: string, triggerType?: string) { - return request.get({ - url: '/api/ops/maintenance-plans', + return request.get>('/api/ops/maintenance-plans', { params: { projectId, triggerType } }) } // 获取维保计划详情 export function getMaintenancePlan(id: string) { - return request.get({ - url: `/api/ops/maintenance-plans/${id}` - }) + return request.get>(`/api/ops/maintenance-plans/${id}`) } // 创建维保计划 export function createMaintenancePlan(data: MaintenancePlanForm) { - return request.post({ - url: '/api/ops/maintenance-plans', - data - }) + return request.post('/api/ops/maintenance-plans', data) } // 更新维保计划 export function updateMaintenancePlan(id: string, data: MaintenancePlanForm) { - return request.put({ - url: `/api/ops/maintenance-plans/${id}`, - data - }) + return request.put(`/api/ops/maintenance-plans/${id}`, data) } // 删除/停用维保计划 export function deleteMaintenancePlan(id: string) { - return request.delete({ - url: `/api/ops/maintenance-plans/${id}` - }) + return request.delete(`/api/ops/maintenance-plans/${id}`) } // ==================== 维保任务 API ==================== // 获取维保任务列表 export function getMaintenanceTasks(params: TaskQueryParams) { - return request.get({ - url: '/api/ops/maintenance-tasks', - params - }) + return request.get>('/api/ops/maintenance-tasks', { params }) } // 获取维保任务详情 export function getMaintenanceTask(id: string) { - return request.get({ - url: `/api/ops/maintenance-tasks/${id}` - }) + return request.get>(`/api/ops/maintenance-tasks/${id}`) } // 接受任务 export function acceptMaintenanceTask(id: string, userId: string) { - return request.post({ - url: `/api/ops/maintenance-tasks/${id}/accept`, + return request.post(`/api/ops/maintenance-tasks/${id}/accept`, null, { params: { userId } }) } // 开始执行任务 export function startMaintenanceTask(id: string) { - return request.post({ - url: `/api/ops/maintenance-tasks/${id}/start` - }) + return request.post(`/api/ops/maintenance-tasks/${id}/start`) } // 完成维保 export function completeMaintenanceTask(id: string, data: { completionNotes?: string }) { - return request.post({ - url: `/api/ops/maintenance-tasks/${id}/complete`, - data - }) + return request.post(`/api/ops/maintenance-tasks/${id}/complete`, data) } // 取消任务 export function cancelMaintenanceTask(id: string) { - return request.post({ - url: `/api/ops/maintenance-tasks/${id}/cancel` - }) -} \ No newline at end of file + return request.post(`/api/ops/maintenance-tasks/${id}/cancel`) +} diff --git a/src/api/permission.ts b/src/api/permission.ts index c7444df6..2375b2a7 100644 --- a/src/api/permission.ts +++ b/src/api/permission.ts @@ -1,14 +1,19 @@ import request from '@/utils/request' import type { Permission } from '@/types' import type { ApiResponse } from '@/types' +import type { AxiosResponse } from 'axios' /** - * 获取权限列表 - * @description 获取系统中所有权限的列表 - * @returns 返回包含所有权限的数组 + * 获取权限列表(支持分页) + * @description 获取系统中所有权限的列表,支持分页和关键词搜索 + * @param params - 分页和查询参数 + * @param params.page - 页码(从0开始) + * @param params.size - 每页大小 + * @param params.keyword - 搜索关键词(权限编码/名称) + * @returns 返回包含权限列表的分页响应 */ -export const getPermissions = () => { - return request.get>('/api/auth/permissions') +export const getPermissions = (params?: { page?: number; size?: number; keyword?: string }) => { + return request.get>('/api/auth/permissions', { params }) } /** @@ -48,8 +53,8 @@ export const updatePermission = (id: string, data: Partial) => { * @param id - 权限唯一标识符 * @returns 返回删除操作的结果 */ -export const deletePermission = (id: string) => { - return request.delete(`/api/auth/permissions/${id}`) +export const deletePermission = (id: string): Promise>> => { + return request.delete>(`/api/auth/permissions/${id}`) } /** diff --git a/src/api/project.ts b/src/api/project.ts index c7f20f99..7a62efaf 100644 --- a/src/api/project.ts +++ b/src/api/project.ts @@ -1,5 +1,5 @@ import request from '@/utils/request' -import type { Project } from '@/types' +import type { ApiResponse, Project } from '@/types' import type { ProjectQuery, PageResponse, @@ -16,32 +16,32 @@ import type { // PM-001 分页查询项目列表 export const queryProjects = (params: ProjectQuery) => { - return request.get>('/api/mdm/projects', { params }) + return request.get>>('/api/mdm/projects', { params }) } // 获取所有项目(兼容旧接口) export const getProjects = () => { - return request.get('/api/mdm/projects/all') + return request.get>('/api/mdm/projects/all') } // 获取项目详情 export const getProject = (id: string) => { - return request.get(`/api/mdm/projects/${id}`) + return request.get>(`/api/mdm/projects/${id}`) } // 根据编码获取项目 export const getProjectByCode = (code: string) => { - return request.get(`/api/mdm/projects/code/${code}`) + return request.get>(`/api/mdm/projects/code/${code}`) } // 创建项目 export const createProject = (data: Partial) => { - return request.post('/api/mdm/projects', data) + return request.post>('/api/mdm/projects', data) } // 更新项目 export const updateProject = (id: string, data: Partial) => { - return request.put(`/api/mdm/projects/${id}`, data) + return request.put>(`/api/mdm/projects/${id}`, data) } // 删除项目 @@ -51,21 +51,21 @@ export const deleteProject = (id: string) => { // 检查项目删除可行性 export const checkProjectDelete = (projectId: string) => { - return request.get(`/api/mdm/projects/${projectId}/delete-check`) + return request.get>(`/api/mdm/projects/${projectId}/delete-check`) } // ==================== 统计数据 ==================== // PM-002 获取项目统计数据 export const getProjectStatistics = (id: string) => { - return request.get(`/api/mdm/projects/${id}/statistics`) + return request.get>(`/api/mdm/projects/${id}/statistics`) } // ==================== 成员管理 ==================== // PM-003 获取项目成员列表 export const getProjectMembers = (projectId: string, params?: { page?: number; size?: number }) => { - return request.get>(`/api/auth/projects/${projectId}/members`, { params }) + return request.get>>(`/api/auth/projects/${projectId}/members`, { params }) } // 添加项目成员 @@ -87,7 +87,7 @@ export const updateMemberRole = (projectId: string, memberId: string, roleInProj // PM-005 生成项目编码 export const generateProjectCode = () => { - return request.get<{ code: string }>('/api/mdm/projects/generate-code') + return request.get>('/api/mdm/projects/generate-code') } // ==================== 状态管理 ==================== @@ -111,17 +111,17 @@ export const disableProject = (id: string, reason?: string) => { // PM-008 获取项目配置 export const getProjectConfig = (id: string) => { - return request.get(`/api/mdm/projects/${id}/config`) + return request.get>(`/api/mdm/projects/${id}/config`) } // 更新项目配置 export const updateProjectConfig = (id: string, data: Partial) => { - return request.put(`/api/mdm/projects/${id}/config`, data) + return request.put>(`/api/mdm/projects/${id}/config`, data) } // ==================== 选择器 ==================== // PM-010 获取项目选择器列表 export const getProjectSelectorList = (params?: { keyword?: string }) => { - return request.get('/api/mdm/projects/selector', { params }) + return request.get>('/api/mdm/projects/selector', { params }) } diff --git a/src/api/role.ts b/src/api/role.ts index 39351c01..f97777be 100644 --- a/src/api/role.ts +++ b/src/api/role.ts @@ -1,14 +1,19 @@ import request from '@/utils/request' -import type { Role, Permission } from '@/types' +import type { Role, Permission, User } from '@/types' import type { ApiResponse } from '@/types' +import type { AxiosResponse } from 'axios' /** - * 获取角色列表 - * @description 获取系统中所有角色的列表 - * @returns 返回包含所有角色的数组 + * 获取角色列表(支持分页) + * @description 获取系统中所有角色的列表,支持分页和关键词搜索 + * @param params - 分页和查询参数 + * @param params.page - 页码(从0开始) + * @param params.size - 每页大小 + * @param params.keyword - 搜索关键词(角色编码/名称) + * @returns 返回包含角色列表的分页响应 */ -export const getRoles = () => { - return request.get>('/api/auth/roles') +export const getRoles = (params?: { page?: number; size?: number; keyword?: string }) => { + return request.get>('/api/auth/roles', { params }) } /** @@ -68,8 +73,8 @@ export const updateRole = (id: string, data: Partial) => { * @param id - 角色唯一标识符 * @returns 返回删除操作的结果 */ -export const deleteRole = (id: string) => { - return request.delete(`/api/auth/roles/${id}`) +export const deleteRole = (id: string): Promise>> => { + return request.delete>(`/api/auth/roles/${id}`) } /** @@ -79,8 +84,8 @@ export const deleteRole = (id: string) => { * @param permissionIds - 权限ID数组,表示要分配给角色的权限 * @returns 返回权限分配操作的结果 */ -export const assignPermissions = (roleId: string, permissionIds: string[]) => { - return request.post(`/api/auth/roles/${roleId}/permissions`, permissionIds) +export const assignPermissions = (roleId: string, permissionIds: string[]): Promise>> => { + return request.post>(`/api/auth/roles/${roleId}/permissions`, permissionIds) } /** @@ -110,6 +115,6 @@ export const removeRoleFromUser = (userId: string, roleId: string) => { * @param roleId - 角色唯一标识符 * @returns 返回拥有该角色的用户列表 */ -export const getRoleUsers = (roleId: string) => { - return request.get(`/api/auth/roles/${roleId}/users`) +export const getRoleUsers = (roleId: string): Promise>> => { + return request.get>(`/api/auth/roles/${roleId}/users`) } diff --git a/src/api/space.ts b/src/api/space.ts index 3a608fb6..b13d0885 100644 --- a/src/api/space.ts +++ b/src/api/space.ts @@ -1,36 +1,37 @@ import request from '@/utils/request' -import type { SpaceNode, SpaceNodeTree, SpaceNodeCreateForm, SpaceNodeUpdateForm, FloorInfoVO } from '@/types/space' +import type { ApiResponse } from '@/types' +import type { SpaceNode, SpaceNodeTree, SpaceNodeCreateForm, SpaceNodeUpdateForm, FloorInfoVO, SpaceNodeDeleteCheck } from '@/types/space' export const getSpaceNodes = (projectId: string) => { - return request.get(`/api/mdm/space-nodes/project/${projectId}`) + return request.get>(`/api/mdm/space-nodes/project/${projectId}`) } export const getSpaceTree = (projectId: string) => { - return request.get(`/api/mdm/space-nodes/project/${projectId}/tree`) + return request.get>(`/api/mdm/space-nodes/project/${projectId}/tree`) } export const getSpaceRoots = (projectId: string) => { - return request.get(`/api/mdm/space-nodes/project/${projectId}/roots`) + return request.get>(`/api/mdm/space-nodes/project/${projectId}/roots`) } export const getSpaceChildren = (parentId: string) => { - return request.get(`/api/mdm/space-nodes/parent/${parentId}/children`) + return request.get>(`/api/mdm/space-nodes/parent/${parentId}/children`) } export const getSpaceNode = (id: string) => { - return request.get(`/api/mdm/space-nodes/${id}`) + return request.get>(`/api/mdm/space-nodes/${id}`) } export const getSpaceNodesByType = (projectId: string, nodeType: string) => { - return request.get(`/api/mdm/space-nodes/project/${projectId}/type/${nodeType}`) + return request.get>(`/api/mdm/space-nodes/project/${projectId}/type/${nodeType}`) } export const createSpaceNode = (data: SpaceNodeCreateForm) => { - return request.post('/api/mdm/space-nodes', data) + return request.post>('/api/mdm/space-nodes', data) } export const updateSpaceNode = (id: string, data: SpaceNodeUpdateForm) => { - return request.put(`/api/mdm/space-nodes/${id}`, data) + return request.put>(`/api/mdm/space-nodes/${id}`, data) } export const deleteSpaceNode = (id: string) => { @@ -38,7 +39,7 @@ export const deleteSpaceNode = (id: string) => { } export const checkSpaceNodeDelete = (id: string) => { - return request.get<{ code: number, message: string, data: SpaceNodeDeleteCheck }>(`/api/mdm/space-nodes/${id}/delete-check`) + return request.get>(`/api/mdm/space-nodes/${id}/delete-check`) } export const deleteSpaceNodeWithChildren = (id: string) => { @@ -46,5 +47,5 @@ export const deleteSpaceNodeWithChildren = (id: string) => { } export const getBuildingFloorInfo = (buildingId: string) => { - return request.get(`/api/mdm/space-nodes/${buildingId}/floor-info`) -} \ No newline at end of file + return request.get>(`/api/mdm/space-nodes/${buildingId}/floor-info`) +} diff --git a/src/api/sparepart.ts b/src/api/sparepart.ts index 0329f699..0edb598e 100644 --- a/src/api/sparepart.ts +++ b/src/api/sparepart.ts @@ -1,4 +1,5 @@ import request from '@/utils/request' +import type { ApiResponse } from '@/types' // ==================== 备件相关类型 ==================== @@ -88,7 +89,7 @@ export interface PageResponse { // 获取分类列表 export function getSparePartCategories() { - return request.get('/api/ops/spare-parts/categories') + return request.get>('/api/ops/spare-parts/categories') } // 创建分类 @@ -100,14 +101,14 @@ export function createSparePartCategory(data: { name: string; description?: stri // 获取备件列表 export function getSparePartList(projectId: string, categoryId?: string) { - return request.get>('/api/ops/spare-parts', { + return request.get>>('/api/ops/spare-parts', { params: { projectId, categoryId } }) } // 获取备件详情 export function getSparePartDetail(id: string) { - return request.get(`/api/ops/spare-parts/${id}`) + return request.get>(`/api/ops/spare-parts/${id}`) } // 创建备件 @@ -127,7 +128,7 @@ export function deleteSparePart(id: string) { // 获取低库存备件 export function getLowStockSpareParts(projectId: string) { - return request.get('/api/ops/spare-parts/low-stock', { + return request.get>('/api/ops/spare-parts/low-stock', { params: { projectId } }) } @@ -144,5 +145,5 @@ export function outStock(data: OutStockRequest) { // 获取备件记录 export function getSparePartRecords(id: string) { - return request.get(`/api/ops/spare-parts/${id}/records`) -} \ No newline at end of file + return request.get>(`/api/ops/spare-parts/${id}/records`) +} diff --git a/src/api/user.ts b/src/api/user.ts index e4890877..db43c59c 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,14 +1,20 @@ import request from '@/utils/request' import type { User } from '@/types' import type { ApiResponse } from '@/types' +import type { AxiosResponse } from 'axios' /** - * 获取用户列表 - * @description 获取系统中所有用户的列表 - * @returns 返回包含所有用户的数组 + * 获取用户列表(支持分页) + * @description 获取系统中所有用户的列表,支持分页和关键词搜索 + * @param params - 分页和查询参数 + * @param params.page - 页码(从0开始) + * @param params.size - 每页大小 + * @param params.keyword - 搜索关键词(用户名/姓名/手机号) + * @param params.status - 用户状态筛选 + * @returns 返回包含用户列表的分页响应 */ -export const getUsers = () => { - return request.get>('/api/auth/users') +export const getUsers = (params?: { page?: number; size?: number; keyword?: string; status?: string }) => { + return request.get>('/api/auth/users', { params }) } /** @@ -48,8 +54,8 @@ export const updateUser = (id: string, data: Partial) => { * @param id - 用户唯一标识符 * @returns 返回删除操作的结果 */ -export const deleteUser = (id: string) => { - return request.delete(`/api/auth/users/${id}`) +export const deleteUser = (id: string): Promise>> => { + return request.delete>(`/api/auth/users/${id}`) } /** @@ -60,8 +66,8 @@ export const deleteUser = (id: string) => { * @param newPassword - 用户想要设置的新密码 * @returns 返回密码修改操作的结果 */ -export const updatePassword = (id: string, oldPassword: string, newPassword: string) => { - return request.put(`/api/auth/users/${id}/password`, { oldPassword, newPassword }) +export const updatePassword = (id: string, oldPassword: string, newPassword: string): Promise>> => { + return request.put>(`/api/auth/users/${id}/password`, { oldPassword, newPassword }) } /** @@ -71,8 +77,8 @@ export const updatePassword = (id: string, oldPassword: string, newPassword: str * @param roleIds - 角色ID数组,表示要分配给用户的角色 * @returns 返回角色分配操作的结果 */ -export const assignRoles = (userId: string, roleIds: string[]) => { - return request.post(`/api/auth/users/${userId}/roles`, roleIds) +export const assignRoles = (userId: string, roleIds: string[]): Promise>> => { + return request.post>(`/api/auth/users/${userId}/roles`, roleIds) } /** diff --git a/src/components/BuildingVisualization/index.vue b/src/components/BuildingVisualization/index.vue index 092c115d..ea4cf9bf 100644 --- a/src/components/BuildingVisualization/index.vue +++ b/src/components/BuildingVisualization/index.vue @@ -1,12 +1,16 @@ diff --git a/src/components/Card/index.vue b/src/components/Card/index.vue index 3ddccc9d..cdab0606 100644 --- a/src/components/Card/index.vue +++ b/src/components/Card/index.vue @@ -1,6 +1,6 @@ - - diff --git a/src/components/SpaceTree/index.vue b/src/components/SpaceTree/index.vue index 52786943..1ff5ef13 100644 --- a/src/components/SpaceTree/index.vue +++ b/src/components/SpaceTree/index.vue @@ -1,16 +1,14 @@ - - - - diff --git a/src/components/index.ts b/src/components/index.ts index 655499f3..9abdfe19 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,8 +3,7 @@ * * 基础组件: * - PageHeader: 页面标题区 - * - Card: 通用卡片 - * - TableCard: 表格卡片 + * - Card: 通用卡片(支持表格场景) * - StatCard: 统计卡片 * - FilterBar: 筛选栏 * - StatusTag: 状态标签 @@ -22,7 +21,7 @@ * 业务组件: * - UserSelect: 用户选择器 * - RoleSelect: 角色选择器 - * - ProjectSelect: 项目选择器 + * - ProjectSelector: 项目选择器(views/project/components/) * - StatusSelect: 状态选择器 * - PhoneItem: 手机号表单项 * - EmailItem: 邮箱表单项 @@ -34,7 +33,6 @@ // 基础组件 export { default as PageHeader } from './PageHeader/index.vue' export { default as Card } from './Card/index.vue' -export { default as TableCard } from './TableCard/index.vue' export { default as StatCard } from './StatCard/index.vue' export { default as FilterBar } from './FilterBar/index.vue' export { default as StatusTag } from './StatusTag/index.vue' @@ -53,7 +51,6 @@ export { default as Pagination } from './Pagination/index.vue' export { default as UserSelect } from './UserSelect/index.vue' export { default as RoleSelect } from './RoleSelect/index.vue' export { default as RoleCardSelect } from './RoleCardSelect/index.vue' -export { default as ProjectSelect } from './ProjectSelect/index.vue' export { default as StatusSelect } from './StatusSelect/index.vue' // 业务组件 - 表单项 diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 00000000..8927dfb7 --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,22 @@ +/** + * 全局常量配置 + * 用于统一管理项目中的硬编码值,便于维护和修改 + */ + +/** + * 默认用户密码 + * 支持通过环境变量 VITE_DEFAULT_PASSWORD 配置,默认为 'Ether@123' + */ +export const DEFAULT_USER_PASSWORD = import.meta.env.VITE_DEFAULT_PASSWORD || 'Ether@123' + +/** + * 默认省份 + * TODO: 后续应从区域选择器动态获取省市名称,而非硬编码 + */ +export const DEFAULT_PROVINCE = '上海市' + +/** + * 默认城市 + * TODO: 后续应从区域选择器动态获取省市名称,而非硬编码 + */ +export const DEFAULT_CITY = '上海市' diff --git a/src/router/index.ts b/src/router/index.ts index 4e0a9843..e5301a0d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -25,37 +25,37 @@ const router = createRouter({ path: 'system/users', name: 'Users', component: () => import('@/views/system/Users.vue'), - meta: { title: '用户管理' } + meta: { title: '用户管理', requiredRoles: ['SYS_ADMIN'] } }, { path: 'system/roles', name: 'Roles', component: () => import('@/views/system/Roles.vue'), - meta: { title: '角色管理' } + meta: { title: '角色管理', requiredRoles: ['SYS_ADMIN'] } }, { path: 'system/permissions', name: 'Permissions', component: () => import('@/views/system/Permissions.vue'), - meta: { title: '权限管理' } + meta: { title: '权限管理', requiredRoles: ['SYS_ADMIN'] } }, { path: 'system/depts', name: 'Depts', component: () => import('@/views/system/Depts.vue'), - meta: { title: '组织架构' } + meta: { title: '组织架构', requiredRoles: ['SYS_ADMIN'] } }, { path: 'system/audit', name: 'Audit', component: () => import('@/views/system/Audit.vue'), - meta: { title: '审计日志' } + meta: { title: '审计日志', requiredRoles: ['SYS_ADMIN'] } }, { path: 'system/settings', name: 'Settings', component: () => import('@/views/system/Settings.vue'), - meta: { title: '系统设置' } + meta: { title: '系统设置', requiredRoles: ['SYS_ADMIN'] } }, { path: 'project/list', @@ -166,11 +166,25 @@ const router = createRouter({ router.beforeEach((to, _from, next) => { const userStore = useUserStore() - if (to.path !== '/login' && !userStore.isLoggedIn()) { - next('/login') - } else { - next() + + // 白名单路由直接放行 + if (to.path === '/login') { + return next() } + + // 未登录跳转登录页 + if (!userStore.isLoggedIn()) { + return next('/login') + } + + // 检查角色权限 + const requiredRoles = to.meta?.requiredRoles as string[] | undefined + if (requiredRoles?.length && !userStore.hasAnyRole(requiredRoles)) { + // 无权限时跳转到首页,并通过 query 参数传递来源路径 + return next({ path: '/', query: { from: to.fullPath } }) + } + + next() }) export default router diff --git a/src/stores/user.ts b/src/stores/user.ts index 7b5c5750..b024f746 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { login as loginApi, logout as logoutApi } from '@/api/auth' -import type { LoginRequest } from '@/types' +import type { LoginRequest, LoginResponse } from '@/types' export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') @@ -22,17 +22,17 @@ export const useUserStore = defineStore('user', () => { const login = async (data: LoginRequest) => { const res = await loginApi(data) - const loginData = res.data.data + const loginData = res.data.data as LoginResponse if (!loginData?.token) { throw new Error('登录失败:未获取到token') } const newToken = loginData.token token.value = newToken + roles.value = loginData.roles || [] userInfo.value = { username: loginData.username, realName: loginData.realName } - roles.value = loginData.roles || [] localStorage.setItem('token', newToken) localStorage.setItem('userInfo', JSON.stringify(userInfo.value)) localStorage.setItem('roles', JSON.stringify(roles.value)) diff --git a/src/styles/building-colors.css b/src/styles/building-colors.css new file mode 100644 index 00000000..3ffadca0 --- /dev/null +++ b/src/styles/building-colors.css @@ -0,0 +1,40 @@ +/* BuildingVisualization 组件颜色变量 */ + +:root { + /* 建筑空间 - 蓝色系 */ + --building-primary: #1890ff; + --building-primary-bg: #e6f7ff; + --building-primary-light: #bae0ff; + + /* 停车空间 - 紫色系 */ + --building-parking: #722ed1; + --building-parking-bg: #f9f0ff; + --building-parking-light: #efdbff; + + /* 配套空间 - 橙色系 */ + --building-facility: #fa8c16; + --building-facility-bg: #fff7e6; + --building-facility-light: #ffe7ba; + + /* 公共区域 - 绿色系 */ + --building-public: #52c41a; + --building-public-bg: #f6ffed; + --building-public-light: #d9f7be; + + /* 房间状态 */ + --room-normal-bg: #e6f4ff; + --room-normal-text: #0958d9; + --room-normal-hover: #bae0ff; + + --room-occupied-bg: #f6ffed; + --room-occupied-text: #389e0d; + --room-occupied-hover: #d9f7be; + + --room-vacant-bg: #f5f5f5; + --room-vacant-text: #8c8c8c; + --room-vacant-hover: #e8e8e8; + + --room-maintenance-bg: #fff7e6; + --room-maintenance-text: #d46b08; + --room-maintenance-hover: #ffe7ba; +} diff --git a/src/styles/common.css b/src/styles/common.css deleted file mode 100644 index b4042de5..00000000 --- a/src/styles/common.css +++ /dev/null @@ -1,76 +0,0 @@ -/* 全局通用样式 - Ether Admin Design System */ - -/* 页面容器 */ -.page-container { - padding: 24px; -} - -/* 页面标题区 */ -.page-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; -} - -.page-title { - font-size: 20px; - font-weight: 500; - color: #262626; - margin: 0; -} - -/* 筛选栏 */ -.filter-bar { - margin-bottom: 16px; - padding: 16px; - background: #fafafa; - border-radius: 8px; -} - -/* 卡片容器 */ -.card { - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 24px; -} - -.card-title { - font-size: 16px; - font-weight: 500; - color: #262626; - margin: 0 0 20px 0; -} - -/* 表格卡片 */ -.table-card { - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 24px; -} - -/* 两栏布局 */ -.two-column { - display: grid; - grid-template-columns: 2fr 1fr; - gap: 24px; -} - -/* 状态标签颜色 */ -.status-success { - color: #52c41a; -} - -.status-warning { - color: #faad14; -} - -.status-error { - color: #f5222d; -} - -.status-default { - color: #8c8c8c; -} diff --git a/src/styles/page-common.css b/src/styles/page-common.css index 88941ff3..94d05f1a 100644 --- a/src/styles/page-common.css +++ b/src/styles/page-common.css @@ -1,8 +1,65 @@ -/* 页面通用样式 - Ether Admin */ +/* 页面通用样式 - Ether Admin Design System */ +/* 基于 DESIGN_SPEC.md 规范实现 */ + +/* ========== CSS 变量(设计令牌) ========== */ +:root { + /* 主色 */ + --color-primary: #1890ff; + --color-primary-dark: #096dd9; + --color-primary-light: #40a9ff; + + /* 状态色 */ + --color-success: #52c41a; + --color-warning: #faad14; + --color-error: #f5222d; + --color-locked: #ff4d4f; + + /* 中性色 */ + --color-title: #1a1a1a; + --color-text: #262626; + --color-text-secondary: #595959; + --color-text-placeholder: #8c8c8c; + --color-text-disabled: #bfbfbf; + --color-border: #e8e8e8; + --color-bg-table: #f0f0f0; + --color-bg-page: #f5f7fa; + --color-bg-card: #ffffff; + --color-bg-filter: #fafafa; + + /* 状态背景色 */ + --color-bg-success: #f6ffed; + --color-bg-warning: #fffbe6; + --color-bg-error: #fff1f0; + --color-bg-default: #f5f5f5; + + /* 主题色背景(用于统计图标等) */ + --color-primary-bg: #e6f7ff; + --color-success-bg: #f6ffed; + --color-warning-bg: #fff7e6; + --color-purple-bg: #f9f0ff; + --color-purple: #722ed1; + + /* 间距 */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + --space-xxl: 48px; + + /* 圆角 */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + + /* 阴影 */ + --shadow-hover: 0 4px 12px rgba(24, 144, 255, 0.15); + --shadow-login: 0 4px 24px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04); +} /* ========== 页面容器 ========== */ .page-container { - padding: 16px; + padding: var(--space-lg); } /* ========== 页面标题区 ========== */ @@ -10,80 +67,80 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; + margin-bottom: var(--space-lg); } .page-title { - font-size: 18px; + font-size: 20px; font-weight: 500; - color: #262626; + color: var(--color-text); margin: 0; } .page-header-actions { display: flex; - gap: 8px; + gap: var(--space-sm); } /* ========== 筛选栏 ========== */ .filter-bar { - margin-bottom: 12px; - padding: 12px; - background: #fafafa; - border-radius: 6px; + margin-bottom: var(--space-md); + padding: var(--space-md); + background: var(--color-bg-filter); + border-radius: var(--radius-md); } .filter-bar :deep(.ant-space) { flex-wrap: wrap; - gap: 12px; + gap: var(--space-md); } /* ========== 卡片容器 ========== */ .card { - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 6px; - padding: 16px; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-lg); } .card-title { - font-size: 15px; + font-size: 16px; font-weight: 500; - color: #262626; - margin: 0 0 12px 0; + color: var(--color-text); + margin: 0 0 var(--space-md) 0; display: flex; align-items: center; - gap: 6px; + gap: var(--space-sm); } .card-title .anticon { - color: #1890ff; + color: var(--color-primary); } /* ========== 表格卡片 ========== */ .table-card { - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 6px; - padding: 16px; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-lg); } /* ========== 两栏布局 ========== */ .two-column { display: grid; grid-template-columns: 2fr 1fr; - gap: 16px; + gap: var(--space-lg); } .two-column-equal { display: grid; grid-template-columns: 1fr 1fr; - gap: 16px; + gap: var(--space-lg); } /* ========== 内容行间距 ========== */ .content-row { - margin-bottom: 16px; + margin-bottom: var(--space-lg); } .content-row:last-child { @@ -94,7 +151,7 @@ .list-container { display: flex; flex-direction: column; - gap: 8px; + gap: var(--space-sm); } .list-item { @@ -111,42 +168,42 @@ .list-item-title { font-size: 14px; - color: #262626; + color: var(--color-text); } .list-item-meta { font-size: 12px; - color: #8c8c8c; + color: var(--color-text-placeholder); } /* ========== 统计卡片 ========== */ .stats-row { display: grid; grid-template-columns: repeat(4, 1fr); - gap: 16px; - margin-bottom: 16px; + gap: var(--space-lg); + margin-bottom: var(--space-lg); } .stat-card { display: flex; align-items: center; - gap: 12px; - padding: 16px; - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 6px; + gap: var(--space-md); + padding: var(--space-lg); + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); } .stat-icon { width: 40px; height: 40px; background: #e6f7ff; - border-radius: 6px; + border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; font-size: 20px; - color: #1890ff; + color: var(--color-primary); } .stat-content { @@ -155,14 +212,14 @@ .stat-label { font-size: 13px; - color: #8c8c8c; + color: var(--color-text-placeholder); margin-bottom: 2px; } .stat-value { font-size: 22px; font-weight: 600; - color: #262626; + color: var(--color-text); margin-bottom: 2px; } @@ -174,11 +231,28 @@ } .stat-change.up { - color: #52c41a; + color: var(--color-success); } .stat-change.down { - color: #f5222d; + color: var(--color-error); +} + +/* ========== 状态标签颜色(供全局使用) ========== */ +.status-success { + color: var(--color-success); +} + +.status-warning { + color: var(--color-warning); +} + +.status-error { + color: var(--color-error); +} + +.status-default { + color: var(--color-text-placeholder); } /* ========== 快捷入口 ========== */ @@ -193,8 +267,8 @@ align-items: center; gap: 10px; padding: 12px; - background: #fafafa; - border-radius: 6px; + background: var(--color-bg-filter); + border-radius: var(--radius-md); cursor: pointer; transition: background 0.2s; } @@ -205,12 +279,12 @@ .action-icon { font-size: 18px; - color: #1890ff; + color: var(--color-primary); } .action-title { font-size: 14px; - color: #262626; + color: var(--color-text); } /* ========== 响应式 ========== */ @@ -218,7 +292,7 @@ .stats-row { grid-template-columns: repeat(2, 1fr); } - + .two-column, .two-column-equal { grid-template-columns: 1fr; @@ -227,23 +301,23 @@ @media (max-width: 576px) { .page-container { - padding: 12px; + padding: var(--space-md); } - + .stats-row { grid-template-columns: 1fr; } - + .action-list { grid-template-columns: 1fr; } - + .filter-bar { - padding: 10px; + padding: var(--space-sm); } - + .card, .table-card { - padding: 12px; + padding: var(--space-md); } } diff --git a/src/types/index.ts b/src/types/index.ts index 49504ca7..31262eb2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,9 +1,27 @@ -export interface ApiResponse { +/** + * API 统一响应接口 + * @description 所有 API 接口的统一返回格式,强制调用方显式指定泛型类型 + * @example + * // 正确:显式指定类型 + * Promise> + * Promise> + * // 错误:不再允许省略泛型参数 + */ +export interface ApiResponse { code: number message: string data: T } +/** + * 分页信息接口(用于 Pagination 组件 @change 事件) + */ +export interface PaginationInfo { + current: number + pageSize: number + total?: number +} + export interface LoginRequest { username: string password: string @@ -14,6 +32,7 @@ export interface LoginResponse { userId: string username: string realName: string + roles: string[] } export interface User { diff --git a/src/types/projectMember.ts b/src/types/projectMember.ts index 597a5a10..e3d2db29 100644 --- a/src/types/projectMember.ts +++ b/src/types/projectMember.ts @@ -16,12 +16,12 @@ export interface AddMemberRequest { roleCode?: string } -// 项目成员类型选项 +// 项目成员类型选项(四保一服) export const StaffTypeOptions = [ { value: 'SECURITY', label: '保安' }, { value: 'CLEANING', label: '保洁' }, { value: 'GARDEN', label: '绿化' }, { value: 'MAINTENANCE', label: '维修' }, { value: 'CUSTOMER_SERVICE', label: '客服' }, - { value: 'GENERAL', label: '普通员工' } + { value: 'PROJECT_STAFF', label: '项目人员' } ] diff --git a/src/types/space.ts b/src/types/space.ts index 341e8ae9..3d3c10e7 100644 --- a/src/types/space.ts +++ b/src/types/space.ts @@ -73,6 +73,7 @@ export interface SpaceNode { street?: string address?: string attributes?: string + remark?: string createdAt?: string updatedAt?: string createdBy?: string diff --git a/src/utils/error-handler.ts b/src/utils/error-handler.ts new file mode 100644 index 00000000..80864e32 --- /dev/null +++ b/src/utils/error-handler.ts @@ -0,0 +1,26 @@ +/** + * 安全获取错误消息 + * @param error - 未知类型的错误对象 + * @returns 可读的错误消息字符串 + */ +export function getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message + if (typeof error === 'string') return error + if (error && typeof error === 'object' && 'message' in error) { + return String((error as { message: unknown }).message) + } + return '操作失败,请稍后重试' +} + +/** + * 检查是否为表单验证错误(Ant Design Vue 表单) + * @param error - 未知类型的错误对象 + * @returns 是否为表单验证错误 + */ +export function isValidationError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'errorFields' in error + ) +} diff --git a/src/utils/request.ts b/src/utils/request.ts index e5d19831..0d496a7b 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,17 +1,60 @@ import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios' +import { message } from 'ant-design-vue' import type { ApiResponse } from '@/types' +// 扩展 Axios 配置类型以支持重试计数器 +declare module 'axios' { + interface InternalAxiosRequestConfig { + __retryCount?: number + } +} + const request: AxiosInstance = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080', - timeout: 10000 + baseURL: import.meta.env.VITE_API_BASE_URL as string, + timeout: Number(import.meta.env.VITE_REQUEST_TIMEOUT) || 10000 }) +// 重试配置 +const MAX_RETRIES = 2 +const RETRY_DELAY = 1000 + +// 延迟函数 +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +// 判断是否为可重试的请求 +const isRetryableError = (error: AxiosError): boolean => { + // 只对 GET 请求进行重试 + if (error.config?.method?.toLowerCase() !== 'get') { + return false + } + + // 只在网络错误或 5xx 错误时重试 + if (!error.response && error.code === 'ERR_NETWORK') { + return true + } + + if (error.response && error.response.status >= 500 && error.response.status < 600) { + return true + } + + // 超时也可以重试 + if (error.code === 'ECONNABORTED') { + return true + } + + return false +} + request.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const token = localStorage.getItem('token') if (token && config.headers) { config.headers.Authorization = `Bearer ${token}` } + + // 初始化重试计数器 + config.__retryCount = config.__retryCount || 0 + return config }, (error) => { @@ -21,17 +64,83 @@ request.interceptors.request.use( request.interceptors.response.use( (response) => { - const res = response.data as ApiResponse - if (res.code !== 200 && res.code !== '00000' && res.code !== 0) { - return Promise.reject(new Error(res.message || 'Error')) + const res = response.data as ApiResponse + + // 业务逻辑层面的错误码处理 + const successCodes = [200, 0] + const code = Number(res.code) + if (!successCodes.includes(code) && String(res.code) !== '00000') { + message.error(res.message || '操作失败') + return Promise.reject(new Error(res.message || '操作失败')) } + return response }, - (error: AxiosError) => { - if (error.response?.status === 401) { - localStorage.removeItem('token') - window.location.href = '/login' + async (error: AxiosError) => { + const config = error.config + + // 尝试重试逻辑 + if (config && isRetryableError(error) && (config.__retryCount || 0) < MAX_RETRIES) { + config.__retryCount = (config.__retryCount || 0) + 1 + + // 等待后重试 + await delay(RETRY_DELAY) + + try { + return await request(config) + } catch (retryError) { + // 重试失败,继续下面的错误处理 + return Promise.reject(retryError) + } } + + // HTTP 层面的错误分类处理 + if (error.response) { + const status = error.response.status + const errorMessage = (error.response.data as any)?.message + + switch (status) { + case 400: + message.error(errorMessage || '请求参数错误') + break + case 401: + // 清除 token 并跳转登录 + localStorage.removeItem('token') + window.location.href = '/login' + break + case 403: + message.error('没有权限执行此操作') + break + case 404: + message.error('请求的资源不存在') + break + case 422: + message.error(errorMessage || '数据验证失败,请检查输入') + break + case 429: + message.error('操作太频繁,请稍后再试') + break + case 500: + message.error('服务器内部错误,请稍后重试') + break + case 502: + case 503: + case 504: + message.error('服务暂时不可用,请稍后重试') + break + default: + message.error(errorMessage || `请求失败 (${status})`) + } + } else if (error.code === 'ERR_NETWORK' || !navigator.onLine) { + message.error('网络连接失败,请检查网络设置') + } else if (error.code === 'ECONNABORTED') { + message.error('请求超时,请稍后重试') + } else if (error.code === 'ERR_CANCELED') { + // 请求被取消,不显示错误提示 + } else { + message.error('未知错误,请刷新页面重试') + } + return Promise.reject(error) } ) diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index 37a1cf76..004bd287 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -1,104 +1,78 @@ diff --git a/src/views/Layout.vue b/src/views/Layout.vue index 75debf8d..71952af0 100644 --- a/src/views/Layout.vue +++ b/src/views/Layout.vue @@ -8,7 +8,6 @@ import { DashboardOutlined, UserOutlined, TeamOutlined, - AppstoreOutlined, BuildOutlined, HeatMapOutlined, LogoutOutlined, @@ -32,7 +31,7 @@ const systemAdminMenus = [ { key: '/system/settings', icon: () => h(SettingOutlined), label: '系统设置' } ] -const menuItems: MenuProps['items'] = computed(() => { +const menuItems = computed(() => { const items: MenuProps['items'] = [ { key: 'workbench', diff --git a/src/views/auth/Login.vue b/src/views/auth/Login.vue index 47a3af8e..399989ce 100644 --- a/src/views/auth/Login.vue +++ b/src/views/auth/Login.vue @@ -1,11 +1,9 @@