Showing
35 changed files
with
10471 additions
and
1658 deletions
API接口文档.md
0 → 100644
| 1 | +# Apple ERP 系统 API 接口文档 | ||
| 2 | + | ||
| 3 | +## 概述 | ||
| 4 | + | ||
| 5 | +本文档描述了 Apple ERP 系统的后端 API 接口,包括认证管理、用户管理、角色管理、菜单管理、字典管理、日志管理等模块的接口规范。 | ||
| 6 | + | ||
| 7 | +**基础信息:** | ||
| 8 | +- 基础URL: `http://localhost:8083` | ||
| 9 | +- 认证方式: JWT Bearer Token | ||
| 10 | +- 响应格式: JSON | ||
| 11 | +- 字符编码: UTF-8 | ||
| 12 | + | ||
| 13 | +## 通用响应格式 | ||
| 14 | + | ||
| 15 | +所有接口都遵循统一的响应格式: | ||
| 16 | + | ||
| 17 | +```json | ||
| 18 | +{ | ||
| 19 | + "code": 200, | ||
| 20 | + "message": "操作成功", | ||
| 21 | + "data": {} | ||
| 22 | +} | ||
| 23 | +``` | ||
| 24 | + | ||
| 25 | +**响应字段说明:** | ||
| 26 | +- `code`: 响应状态码,200表示成功,其他表示失败 | ||
| 27 | +- `message`: 响应消息 | ||
| 28 | +- `data`: 响应数据,具体内容根据接口而定 | ||
| 29 | + | ||
| 30 | +## 1. 认证管理 (AuthController) | ||
| 31 | + | ||
| 32 | +### 1.1 用户登录 | ||
| 33 | + | ||
| 34 | +**接口路径:** `POST /api/auth/login` | ||
| 35 | + | ||
| 36 | +**功能描述:** 用户登录获取JWT令牌 | ||
| 37 | + | ||
| 38 | +**请求参数:** | ||
| 39 | +```json | ||
| 40 | +{ | ||
| 41 | + "username": "admin", | ||
| 42 | + "password": "123456" | ||
| 43 | +} | ||
| 44 | +``` | ||
| 45 | + | ||
| 46 | +**响应示例:** | ||
| 47 | +```json | ||
| 48 | +{ | ||
| 49 | + "code": 200, | ||
| 50 | + "message": "登录成功", | ||
| 51 | + "data": { | ||
| 52 | + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | ||
| 53 | + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | ||
| 54 | + "username": "admin" | ||
| 55 | + } | ||
| 56 | +} | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +### 1.2 刷新令牌 | ||
| 60 | + | ||
| 61 | +**接口路径:** `POST /api/auth/refresh` | ||
| 62 | + | ||
| 63 | +**功能描述:** 使用刷新令牌获取新的访问令牌 | ||
| 64 | + | ||
| 65 | +**请求参数:** | ||
| 66 | +```json | ||
| 67 | +{ | ||
| 68 | + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | ||
| 69 | +} | ||
| 70 | +``` | ||
| 71 | + | ||
| 72 | +**响应示例:** | ||
| 73 | +```json | ||
| 74 | +{ | ||
| 75 | + "code": 200, | ||
| 76 | + "message": "令牌刷新成功", | ||
| 77 | + "data": { | ||
| 78 | + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | ||
| 79 | + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | ||
| 80 | + "username": "admin" | ||
| 81 | + } | ||
| 82 | +} | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +### 1.3 用户登出 | ||
| 86 | + | ||
| 87 | +**接口路径:** `POST /api/auth/logout` | ||
| 88 | + | ||
| 89 | +**功能描述:** 用户登出清除认证信息和缓存 | ||
| 90 | + | ||
| 91 | +**请求头:** `Authorization: Bearer {token}` | ||
| 92 | + | ||
| 93 | +**响应示例:** | ||
| 94 | +```json | ||
| 95 | +{ | ||
| 96 | + "code": 200, | ||
| 97 | + "message": "登出成功", | ||
| 98 | + "data": null | ||
| 99 | +} | ||
| 100 | +``` | ||
| 101 | + | ||
| 102 | +### 1.4 获取用户信息 | ||
| 103 | + | ||
| 104 | +**接口路径:** `GET /api/auth/userinfo` | ||
| 105 | + | ||
| 106 | +**功能描述:** 获取当前登录用户的详细信息 | ||
| 107 | + | ||
| 108 | +**请求头:** `Authorization: Bearer {token}` | ||
| 109 | + | ||
| 110 | +**响应示例:** | ||
| 111 | +```json | ||
| 112 | +{ | ||
| 113 | + "code": 200, | ||
| 114 | + "message": "获取用户信息成功", | ||
| 115 | + "data": { | ||
| 116 | + "username": "admin", | ||
| 117 | + "authorities": ["ROLE_ADMIN"] | ||
| 118 | + } | ||
| 119 | +} | ||
| 120 | +``` | ||
| 121 | + | ||
| 122 | +## 2. 用户管理 (SysUserController) | ||
| 123 | + | ||
| 124 | +### 2.1 获取用户列表 | ||
| 125 | + | ||
| 126 | +**接口路径:** `GET /api/system/user/list` | ||
| 127 | + | ||
| 128 | +**功能描述:** 支持分页查询和条件筛选,包括用户名、真实姓名、手机号、邮箱、状态、创建时间范围等条件 | ||
| 129 | + | ||
| 130 | +**权限要求:** `sys:user:list` | ||
| 131 | + | ||
| 132 | +**请求参数:** | ||
| 133 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 134 | +|--------|------|------|------| | ||
| 135 | +| pageNum | Integer | 否 | 页码,默认1 | | ||
| 136 | +| pageSize | Integer | 否 | 每页大小,默认10 | | ||
| 137 | +| username | String | 否 | 用户名,支持模糊查询 | | ||
| 138 | +| realName | String | 否 | 真实姓名,支持模糊查询 | | ||
| 139 | +| phone | String | 否 | 手机号,支持模糊查询 | | ||
| 140 | +| email | String | 否 | 邮箱,支持模糊查询 | | ||
| 141 | +| status | Integer | 否 | 用户状态,0-停用,1-启用 | | ||
| 142 | +| startTime | String | 否 | 开始时间,创建时间范围查询的起始时间,格式:yyyy-MM-dd,自动转换为当天00:00:00 | | ||
| 143 | +| endTime | String | 否 | 结束时间,创建时间范围查询的结束时间,格式:yyyy-MM-dd,自动转换为当天23:59:59 | | ||
| 144 | + | ||
| 145 | +**响应示例:** | ||
| 146 | +```json | ||
| 147 | +{ | ||
| 148 | + "code": 200, | ||
| 149 | + "message": "操作成功", | ||
| 150 | + "data": { | ||
| 151 | + "records": [ | ||
| 152 | + { | ||
| 153 | + "userId": 1, | ||
| 154 | + "username": "admin", | ||
| 155 | + "realName": "管理员", | ||
| 156 | + "phone": "13800138000", | ||
| 157 | + "email": "admin@example.com", | ||
| 158 | + "status": 1, | ||
| 159 | + "statusText": "正常", | ||
| 160 | + "roles": [ | ||
| 161 | + { | ||
| 162 | + "roleId": 1, | ||
| 163 | + "roleName": "超级管理员" | ||
| 164 | + } | ||
| 165 | + ], | ||
| 166 | + "createTime": "2024-01-01T00:00:00", | ||
| 167 | + "updateTime": "2024-01-01T00:00:00" | ||
| 168 | + } | ||
| 169 | + ], | ||
| 170 | + "total": 1, | ||
| 171 | + "current": 1, | ||
| 172 | + "size": 10 | ||
| 173 | + } | ||
| 174 | +} | ||
| 175 | +``` | ||
| 176 | + | ||
| 177 | +### 2.2 获取用户详情 | ||
| 178 | + | ||
| 179 | +**接口路径:** `GET /api/system/user/{userId}` | ||
| 180 | + | ||
| 181 | +**功能描述:** 根据用户ID获取用户的详细信息,包括用户基本资料和分配的角色信息 | ||
| 182 | + | ||
| 183 | +**权限要求:** `sys:user:query` | ||
| 184 | + | ||
| 185 | +**路径参数:** | ||
| 186 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 187 | +|--------|------|------|------| | ||
| 188 | +| userId | Long | 是 | 用户ID | | ||
| 189 | + | ||
| 190 | +**响应示例:** | ||
| 191 | +```json | ||
| 192 | +{ | ||
| 193 | + "code": 200, | ||
| 194 | + "message": "操作成功", | ||
| 195 | + "data": { | ||
| 196 | + "userId": 1, | ||
| 197 | + "username": "admin", | ||
| 198 | + "realName": "管理员", | ||
| 199 | + "phone": "13800138000", | ||
| 200 | + "email": "admin@example.com", | ||
| 201 | + "status": 1, | ||
| 202 | + "statusText": "正常", | ||
| 203 | + "roles": [ | ||
| 204 | + { | ||
| 205 | + "roleId": 1, | ||
| 206 | + "roleName": "超级管理员" | ||
| 207 | + } | ||
| 208 | + ], | ||
| 209 | + "createTime": "2024-01-01T00:00:00", | ||
| 210 | + "updateTime": "2024-01-01T00:00:00" | ||
| 211 | + } | ||
| 212 | +} | ||
| 213 | +``` | ||
| 214 | + | ||
| 215 | +### 2.3 新增用户 | ||
| 216 | + | ||
| 217 | +**接口路径:** `POST /api/system/user/add` | ||
| 218 | + | ||
| 219 | +**功能描述:** 创建新用户,包括用户基本信息和角色分配 | ||
| 220 | + | ||
| 221 | +**权限要求:** `sys:user:add` | ||
| 222 | + | ||
| 223 | +**请求参数:** | ||
| 224 | +```json | ||
| 225 | +{ | ||
| 226 | + "username": "testuser", | ||
| 227 | + "password": "123456", | ||
| 228 | + "realName": "测试用户", | ||
| 229 | + "phone": "13800138001", | ||
| 230 | + "email": "test@example.com", | ||
| 231 | + "status": 1, | ||
| 232 | + "roleIds": [2, 3] | ||
| 233 | +} | ||
| 234 | +``` | ||
| 235 | + | ||
| 236 | +**响应示例:** | ||
| 237 | +```json | ||
| 238 | +{ | ||
| 239 | + "code": 200, | ||
| 240 | + "message": "操作成功", | ||
| 241 | + "data": null | ||
| 242 | +} | ||
| 243 | +``` | ||
| 244 | + | ||
| 245 | +### 2.4 修改用户 | ||
| 246 | + | ||
| 247 | +**接口路径:** `POST /api/system/user/edit` | ||
| 248 | + | ||
| 249 | +**功能描述:** 更新用户基本信息,包括用户资料和角色分配 | ||
| 250 | + | ||
| 251 | +**权限要求:** `sys:user:edit` | ||
| 252 | + | ||
| 253 | +**请求参数:** | ||
| 254 | +```json | ||
| 255 | +{ | ||
| 256 | + "userId": 2, | ||
| 257 | + "username": "testuser", | ||
| 258 | + "realName": "测试用户", | ||
| 259 | + "phone": "13800138001", | ||
| 260 | + "email": "test@example.com", | ||
| 261 | + "status": 1, | ||
| 262 | + "roleIds": [2, 3] | ||
| 263 | +} | ||
| 264 | +``` | ||
| 265 | + | ||
| 266 | +**响应示例:** | ||
| 267 | +```json | ||
| 268 | +{ | ||
| 269 | + "code": 200, | ||
| 270 | + "message": "操作成功", | ||
| 271 | + "data": null | ||
| 272 | +} | ||
| 273 | +``` | ||
| 274 | + | ||
| 275 | +### 2.5 删除用户 | ||
| 276 | + | ||
| 277 | +**接口路径:** `DELETE /api/system/user/{userIds}` | ||
| 278 | + | ||
| 279 | +**功能描述:** 批量删除用户,会同时清理用户角色关联关系 | ||
| 280 | + | ||
| 281 | +**权限要求:** `sys:user:remove` | ||
| 282 | + | ||
| 283 | +**路径参数:** | ||
| 284 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 285 | +|--------|------|------|------| | ||
| 286 | +| userIds | Long[] | 是 | 需要删除的用户ID数组 | | ||
| 287 | + | ||
| 288 | +**响应示例:** | ||
| 289 | +```json | ||
| 290 | +{ | ||
| 291 | + "code": 200, | ||
| 292 | + "message": "操作成功", | ||
| 293 | + "data": null | ||
| 294 | +} | ||
| 295 | +``` | ||
| 296 | + | ||
| 297 | +### 2.6 重置密码 | ||
| 298 | + | ||
| 299 | +**接口路径:** `PUT /api/system/user/resetPwd` | ||
| 300 | + | ||
| 301 | +**功能描述:** 重置指定用户的登录密码 | ||
| 302 | + | ||
| 303 | +**权限要求:** `sys:user:resetPwd` | ||
| 304 | + | ||
| 305 | +**请求参数:** | ||
| 306 | +```json | ||
| 307 | +{ | ||
| 308 | + "userId": 2, | ||
| 309 | + "newPassword": "newpassword123" | ||
| 310 | +} | ||
| 311 | +``` | ||
| 312 | + | ||
| 313 | +**响应示例:** | ||
| 314 | +```json | ||
| 315 | +{ | ||
| 316 | + "code": 200, | ||
| 317 | + "message": "操作成功", | ||
| 318 | + "data": null | ||
| 319 | +} | ||
| 320 | +``` | ||
| 321 | + | ||
| 322 | +### 2.7 修改用户状态 | ||
| 323 | + | ||
| 324 | +**接口路径:** `POST /api/system/user/changeStatus` | ||
| 325 | + | ||
| 326 | +**功能描述:** 启用或停用用户账户 | ||
| 327 | + | ||
| 328 | +**权限要求:** `sys:user:edit` | ||
| 329 | + | ||
| 330 | +**请求参数:** | ||
| 331 | +```json | ||
| 332 | +{ | ||
| 333 | + "userId": 2, | ||
| 334 | + "status": 0 | ||
| 335 | +} | ||
| 336 | +``` | ||
| 337 | + | ||
| 338 | +**响应示例:** | ||
| 339 | +```json | ||
| 340 | +{ | ||
| 341 | + "code": 200, | ||
| 342 | + "message": "操作成功", | ||
| 343 | + "data": null | ||
| 344 | +} | ||
| 345 | +``` | ||
| 346 | + | ||
| 347 | +## 3. 角色管理 (SysRoleController) | ||
| 348 | + | ||
| 349 | +### 3.1 获取角色列表 | ||
| 350 | + | ||
| 351 | +**接口路径:** `GET /api/system/role/list` | ||
| 352 | + | ||
| 353 | +**功能描述:** 支持分页查询和条件筛选,包括角色名称、角色编码、状态等条件 | ||
| 354 | + | ||
| 355 | +**权限要求:** `sys:role:list` | ||
| 356 | + | ||
| 357 | +**请求参数:** | ||
| 358 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 359 | +|--------|------|------|------| | ||
| 360 | +| pageNum | Integer | 否 | 页码,默认1 | | ||
| 361 | +| pageSize | Integer | 否 | 每页大小,默认10 | | ||
| 362 | +| roleName | String | 否 | 角色名称,支持模糊查询 | | ||
| 363 | +| roleCode | String | 否 | 角色编码,支持模糊查询 | | ||
| 364 | +| status | Integer | 否 | 角色状态,0-停用,1-启用 | | ||
| 365 | + | ||
| 366 | +**响应示例:** | ||
| 367 | +```json | ||
| 368 | +{ | ||
| 369 | + "code": 200, | ||
| 370 | + "message": "操作成功", | ||
| 371 | + "data": { | ||
| 372 | + "records": [ | ||
| 373 | + { | ||
| 374 | + "roleId": 1, | ||
| 375 | + "roleCode": "admin", | ||
| 376 | + "roleName": "超级管理员", | ||
| 377 | + "status": 1, | ||
| 378 | + "statusText": "正常", | ||
| 379 | + "remark": "系统超级管理员", | ||
| 380 | + "createTime": "2024-01-01T00:00:00", | ||
| 381 | + "updateTime": "2024-01-01T00:00:00" | ||
| 382 | + } | ||
| 383 | + ], | ||
| 384 | + "total": 1, | ||
| 385 | + "current": 1, | ||
| 386 | + "size": 10 | ||
| 387 | + } | ||
| 388 | +} | ||
| 389 | +``` | ||
| 390 | + | ||
| 391 | +### 3.2 获取角色详情 | ||
| 392 | + | ||
| 393 | +**接口路径:** `GET /api/system/role/{roleId}` | ||
| 394 | + | ||
| 395 | +**功能描述:** 根据角色ID获取角色的详细信息,包括角色基本资料和分配的菜单权限 | ||
| 396 | + | ||
| 397 | +**权限要求:** `sys:role:query` | ||
| 398 | + | ||
| 399 | +**路径参数:** | ||
| 400 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 401 | +|--------|------|------|------| | ||
| 402 | +| roleId | Long | 是 | 角色ID | | ||
| 403 | + | ||
| 404 | +**响应示例:** | ||
| 405 | +```json | ||
| 406 | +{ | ||
| 407 | + "code": 200, | ||
| 408 | + "message": "操作成功", | ||
| 409 | + "data": { | ||
| 410 | + "roleId": 1, | ||
| 411 | + "roleCode": "admin", | ||
| 412 | + "roleName": "超级管理员", | ||
| 413 | + "status": 1, | ||
| 414 | + "statusText": "正常", | ||
| 415 | + "menuIds": [1, 2, 3, 4, 5], | ||
| 416 | + "remark": "系统超级管理员", | ||
| 417 | + "createTime": "2024-01-01T00:00:00", | ||
| 418 | + "updateTime": "2024-01-01T00:00:00" | ||
| 419 | + } | ||
| 420 | +} | ||
| 421 | +``` | ||
| 422 | + | ||
| 423 | +### 3.3 新增角色 | ||
| 424 | + | ||
| 425 | +**接口路径:** `POST /api/system/role/add` | ||
| 426 | + | ||
| 427 | +**功能描述:** 创建新角色,包括角色基本信息和菜单权限分配 | ||
| 428 | + | ||
| 429 | +**权限要求:** `sys:role:add` | ||
| 430 | + | ||
| 431 | +**请求参数:** | ||
| 432 | +```json | ||
| 433 | +{ | ||
| 434 | + "roleCode": "testrole", | ||
| 435 | + "roleName": "测试角色", | ||
| 436 | + "status": 1, | ||
| 437 | + "remark": "测试角色描述", | ||
| 438 | + "menuIds": [1, 2, 3] | ||
| 439 | +} | ||
| 440 | +``` | ||
| 441 | + | ||
| 442 | +**响应示例:** | ||
| 443 | +```json | ||
| 444 | +{ | ||
| 445 | + "code": 200, | ||
| 446 | + "message": "操作成功", | ||
| 447 | + "data": null | ||
| 448 | +} | ||
| 449 | +``` | ||
| 450 | + | ||
| 451 | +### 3.4 修改角色 | ||
| 452 | + | ||
| 453 | +**接口路径:** `POST /api/system/role/edit` | ||
| 454 | + | ||
| 455 | +**功能描述:** 更新角色基本信息,包括角色资料和菜单权限分配 | ||
| 456 | + | ||
| 457 | +**权限要求:** `sys:role:edit` | ||
| 458 | + | ||
| 459 | +**请求参数:** | ||
| 460 | +```json | ||
| 461 | +{ | ||
| 462 | + "roleId": 2, | ||
| 463 | + "roleCode": "testrole", | ||
| 464 | + "roleName": "测试角色", | ||
| 465 | + "status": 1, | ||
| 466 | + "remark": "测试角色描述", | ||
| 467 | + "menuIds": [1, 2, 3] | ||
| 468 | +} | ||
| 469 | +``` | ||
| 470 | + | ||
| 471 | +**响应示例:** | ||
| 472 | +```json | ||
| 473 | +{ | ||
| 474 | + "code": 200, | ||
| 475 | + "message": "操作成功", | ||
| 476 | + "data": null | ||
| 477 | +} | ||
| 478 | +``` | ||
| 479 | + | ||
| 480 | +### 3.5 删除角色 | ||
| 481 | + | ||
| 482 | +**接口路径:** `DELETE /api/system/role/{roleIds}` | ||
| 483 | + | ||
| 484 | +**功能描述:** 批量删除角色,会同时清理角色与用户、菜单的关联关系 | ||
| 485 | + | ||
| 486 | +**权限要求:** `sys:role:remove` | ||
| 487 | + | ||
| 488 | +**路径参数:** | ||
| 489 | +| 参数名 | 类型 | 必填 | 说明 | | ||
| 490 | +|--------|------|------|------| | ||
| 491 | +| roleIds | Long[] | 是 | 需要删除的角色ID数组 | | ||
| 492 | + | ||
| 493 | +**响应示例:** | ||
| 494 | +```json | ||
| 495 | +{ | ||
| 496 | + "code": 200, | ||
| 497 | + "message": "操作成功", | ||
| 498 | + "data": null | ||
| 499 | +} | ||
| 500 | +``` | ||
| 501 | + | ||
| 502 | +### 3.6 修改角色状态 | ||
| 503 | + | ||
| 504 | +**接口路径:** `POST /api/system/role/changeStatus` | ||
| 505 | + | ||
| 506 | +**功能描述:** 启用或停用角色 | ||
| 507 | + | ||
| 508 | +**权限要求:** `sys:role:edit` | ||
| 509 | + | ||
| 510 | +**请求参数:** | ||
| 511 | +```json | ||
| 512 | +{ | ||
| 513 | + "roleId": 2, | ||
| 514 | + "status": 0 | ||
| 515 | +} | ||
| 516 | +``` | ||
| 517 | + | ||
| 518 | +**响应示例:** | ||
| 519 | +```json | ||
| 520 | +{ | ||
| 521 | + "code": 200, | ||
| 522 | + "message": "操作成功", | ||
| 523 | + "data": null | ||
| 524 | +} | ||
| 525 | +``` | ||
| 526 | + | ||
| 527 | +## 4. 测试接口 (TestController) | ||
| 528 | + | ||
| 529 | +### 4.1 公开接口测试 | ||
| 530 | + | ||
| 531 | +**接口路径:** `GET /api/test/public` | ||
| 532 | + | ||
| 533 | +**功能描述:** 无需认证的公开接口 | ||
| 534 | + | ||
| 535 | +**响应示例:** | ||
| 536 | +```json | ||
| 537 | +{ | ||
| 538 | + "code": 200, | ||
| 539 | + "message": "公开接口测试成功", | ||
| 540 | + "data": { | ||
| 541 | + "message": "这是一个公开接口", | ||
| 542 | + "timestamp": 1640995200000 | ||
| 543 | + } | ||
| 544 | +} | ||
| 545 | +``` | ||
| 546 | + | ||
| 547 | +### 4.2 认证接口测试 | ||
| 548 | + | ||
| 549 | +**接口路径:** `GET /api/test/auth` | ||
| 550 | + | ||
| 551 | +**功能描述:** 需要JWT令牌认证的接口 | ||
| 552 | + | ||
| 553 | +**请求头:** `Authorization: Bearer {token}` | ||
| 554 | + | ||
| 555 | +**响应示例:** | ||
| 556 | +```json | ||
| 557 | +{ | ||
| 558 | + "code": 200, | ||
| 559 | + "message": "认证接口测试成功", | ||
| 560 | + "data": { | ||
| 561 | + "message": "这是一个需要认证的接口", | ||
| 562 | + "timestamp": 1640995200000 | ||
| 563 | + } | ||
| 564 | +} | ||
| 565 | +``` | ||
| 566 | + | ||
| 567 | +### 4.3 管理员接口测试 | ||
| 568 | + | ||
| 569 | +**接口路径:** `GET /api/test/admin` | ||
| 570 | + | ||
| 571 | +**功能描述:** 需要ADMIN角色权限的接口 | ||
| 572 | + | ||
| 573 | +**请求头:** `Authorization: Bearer {token}` | ||
| 574 | + | ||
| 575 | +**权限要求:** `ROLE_ADMIN` | ||
| 576 | + | ||
| 577 | +**响应示例:** | ||
| 578 | +```json | ||
| 579 | +{ | ||
| 580 | + "code": 200, | ||
| 581 | + "message": "管理员接口测试成功", | ||
| 582 | + "data": { | ||
| 583 | + "message": "这是一个需要管理员权限的接口", | ||
| 584 | + "timestamp": 1640995200000 | ||
| 585 | + } | ||
| 586 | +} | ||
| 587 | +``` | ||
| 588 | + | ||
| 589 | +### 4.4 权限接口测试 | ||
| 590 | + | ||
| 591 | +**接口路径:** `GET /api/test/permission` | ||
| 592 | + | ||
| 593 | +**功能描述:** 需要特定权限的接口 | ||
| 594 | + | ||
| 595 | +**请求头:** `Authorization: Bearer {token}` | ||
| 596 | + | ||
| 597 | +**权限要求:** `sys:user:list` | ||
| 598 | + | ||
| 599 | +**响应示例:** | ||
| 600 | +```json | ||
| 601 | +{ | ||
| 602 | + "code": 200, | ||
| 603 | + "message": "权限接口测试成功", | ||
| 604 | + "data": { | ||
| 605 | + "message": "这是一个需要特定权限的接口", | ||
| 606 | + "timestamp": 1640995200000 | ||
| 607 | + } | ||
| 608 | +} | ||
| 609 | +``` | ||
| 610 | + | ||
| 611 | +## 5. 错误码说明 | ||
| 612 | + | ||
| 613 | +| 错误码 | 说明 | | ||
| 614 | +|--------|------| | ||
| 615 | +| 200 | 操作成功 | | ||
| 616 | +| 400 | 请求参数错误 | | ||
| 617 | +| 401 | 未认证或认证失败 | | ||
| 618 | +| 403 | 权限不足 | | ||
| 619 | +| 404 | 资源不存在 | | ||
| 620 | +| 409 | 数据冲突(如用户名已存在) | | ||
| 621 | +| 500 | 服务器内部错误 | | ||
| 622 | + | ||
| 623 | +## 6. 权限说明 | ||
| 624 | + | ||
| 625 | +### 6.1 用户管理权限 | ||
| 626 | +- `sys:user:list` - 查看用户列表 | ||
| 627 | +- `sys:user:query` - 查看用户详情 | ||
| 628 | +- `sys:user:add` - 新增用户 | ||
| 629 | +- `sys:user:edit` - 修改用户 | ||
| 630 | +- `sys:user:remove` - 删除用户 | ||
| 631 | +- `sys:user:resetPwd` - 重置密码 | ||
| 632 | + | ||
| 633 | +### 6.2 角色管理权限 | ||
| 634 | +- `sys:role:list` - 查看角色列表 | ||
| 635 | +- `sys:role:query` - 查看角色详情 | ||
| 636 | +- `sys:role:add` - 新增角色 | ||
| 637 | +- `sys:role:edit` - 修改角色 | ||
| 638 | +- `sys:role:remove` - 删除角色 | ||
| 639 | + | ||
| 640 | +### 6.3 字典管理权限 | ||
| 641 | +- `sys:dict:list` - 查看字典列表 | ||
| 642 | +- `sys:dict:query` - 查看字典详情 | ||
| 643 | +- `sys:dict:add` - 新增字典 | ||
| 644 | +- `sys:dict:edit` - 修改字典 | ||
| 645 | +- `sys:dict:remove` - 删除字典 | ||
| 646 | + | ||
| 647 | +## 7. 使用说明 | ||
| 648 | + | ||
| 649 | +### 7.1 认证流程 | ||
| 650 | +1. 调用登录接口获取JWT令牌 | ||
| 651 | +2. 在后续请求的Header中携带令牌:`Authorization: Bearer {token}` | ||
| 652 | +3. 令牌过期时使用刷新令牌获取新令牌 | ||
| 653 | +4. 登出时调用登出接口清除认证信息 | ||
| 654 | + | ||
| 655 | +### 7.2 分页查询 | ||
| 656 | +所有列表接口都支持分页查询,使用以下参数: | ||
| 657 | +- `pageNum`: 页码,从1开始 | ||
| 658 | +- `pageSize`: 每页大小,建议10-50之间 | ||
| 659 | + | ||
| 660 | +### 7.3 数据验证 | ||
| 661 | +- 所有必填字段都会进行验证 | ||
| 662 | +- 字符串长度、邮箱格式等都有相应验证规则 | ||
| 663 | +- 唯一性字段(如用户名、角色编码)会进行重复性检查 | ||
| 664 | + | ||
| 665 | +### 7.4 安全限制 | ||
| 666 | +- 超级管理员用户(ID=1)和角色(ID=1)不允许删除 | ||
| 667 | +- 超级管理员状态不允许修改 | ||
| 668 | +- 所有操作都会记录操作日志 | ||
| 669 | + | ||
| 670 | +--- | ||
| 671 | + | ||
| 672 | +**文档版本:** 1.0.0 | ||
| 673 | +**最后更新:** 2024-01-01 | ||
| 674 | +**维护人员:** Apple ERP Team |
| ... | @@ -5,8 +5,11 @@ import com.apple.erp.dto.request.RoleQueryReq; | ... | @@ -5,8 +5,11 @@ import com.apple.erp.dto.request.RoleQueryReq; |
| 5 | import com.apple.erp.dto.request.RoleUpdateReq; | 5 | import com.apple.erp.dto.request.RoleUpdateReq; |
| 6 | import com.apple.erp.dto.response.ApiRes; | 6 | import com.apple.erp.dto.response.ApiRes; |
| 7 | import com.apple.erp.dto.response.RoleRes; | 7 | import com.apple.erp.dto.response.RoleRes; |
| 8 | +import com.apple.erp.dto.response.MenuRes; | ||
| 8 | import com.apple.erp.entity.SysRole; | 9 | import com.apple.erp.entity.SysRole; |
| 10 | +import com.apple.erp.entity.SysMenu; | ||
| 9 | import com.apple.erp.service.SysRoleService; | 11 | import com.apple.erp.service.SysRoleService; |
| 12 | +import com.apple.erp.mapper.SysMenuMapper; | ||
| 10 | import com.baomidou.mybatisplus.core.metadata.IPage; | 13 | import com.baomidou.mybatisplus.core.metadata.IPage; |
| 11 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | 14 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| 12 | import io.swagger.v3.oas.annotations.Operation; | 15 | import io.swagger.v3.oas.annotations.Operation; |
| ... | @@ -21,7 +24,6 @@ import org.springframework.web.bind.annotation.*; | ... | @@ -21,7 +24,6 @@ import org.springframework.web.bind.annotation.*; |
| 21 | import javax.validation.Valid; | 24 | import javax.validation.Valid; |
| 22 | import javax.validation.constraints.NotNull; | 25 | import javax.validation.constraints.NotNull; |
| 23 | import java.time.LocalDateTime; | 26 | import java.time.LocalDateTime; |
| 24 | -import java.util.Arrays; | ||
| 25 | import java.util.List; | 27 | import java.util.List; |
| 26 | import java.util.stream.Collectors; | 28 | import java.util.stream.Collectors; |
| 27 | 29 | ||
| ... | @@ -41,6 +43,9 @@ public class SysRoleController { | ... | @@ -41,6 +43,9 @@ public class SysRoleController { |
| 41 | @Autowired | 43 | @Autowired |
| 42 | private SysRoleService sysRoleService; | 44 | private SysRoleService sysRoleService; |
| 43 | 45 | ||
| 46 | + @Autowired | ||
| 47 | + private SysMenuMapper sysMenuMapper; | ||
| 48 | + | ||
| 44 | /** | 49 | /** |
| 45 | * 获取角色列表 | 50 | * 获取角色列表 |
| 46 | * 支持分页查询和条件筛选,包括角色名称、角色编码、状态等条件 | 51 | * 支持分页查询和条件筛选,包括角色名称、角色编码、状态等条件 |
| ... | @@ -156,7 +161,7 @@ public class SysRoleController { | ... | @@ -156,7 +161,7 @@ public class SysRoleController { |
| 156 | * @throws RuntimeException 当角色编码或名称已存在时 | 161 | * @throws RuntimeException 当角色编码或名称已存在时 |
| 157 | */ | 162 | */ |
| 158 | @Operation(summary = "新增角色", description = "创建新角色,包括角色基本信息和菜单权限分配") | 163 | @Operation(summary = "新增角色", description = "创建新角色,包括角色基本信息和菜单权限分配") |
| 159 | - @PostMapping | 164 | + @PostMapping("/add") |
| 160 | @PreAuthorize("hasAuthority('sys:role:add')") | 165 | @PreAuthorize("hasAuthority('sys:role:add')") |
| 161 | public ApiRes<Void> add( | 166 | public ApiRes<Void> add( |
| 162 | @Parameter(description = "角色新增请求,包含角色基本信息和菜单ID列表", required = true) | 167 | @Parameter(description = "角色新增请求,包含角色基本信息和菜单ID列表", required = true) |
| ... | @@ -222,7 +227,7 @@ public class SysRoleController { | ... | @@ -222,7 +227,7 @@ public class SysRoleController { |
| 222 | * @throws RuntimeException 当角色不存在或角色编码/名称已存在时 | 227 | * @throws RuntimeException 当角色不存在或角色编码/名称已存在时 |
| 223 | */ | 228 | */ |
| 224 | @Operation(summary = "修改角色", description = "更新角色基本信息,包括角色资料和菜单权限分配") | 229 | @Operation(summary = "修改角色", description = "更新角色基本信息,包括角色资料和菜单权限分配") |
| 225 | - @PutMapping | 230 | + @PostMapping("/edit") |
| 226 | @PreAuthorize("hasAuthority('sys:role:edit')") | 231 | @PreAuthorize("hasAuthority('sys:role:edit')") |
| 227 | public ApiRes<Void> edit( | 232 | public ApiRes<Void> edit( |
| 228 | @Parameter(description = "角色更新请求,包含角色ID、基本信息和菜单ID列表", required = true) | 233 | @Parameter(description = "角色更新请求,包含角色ID、基本信息和菜单ID列表", required = true) |
| ... | @@ -329,7 +334,7 @@ public class SysRoleController { | ... | @@ -329,7 +334,7 @@ public class SysRoleController { |
| 329 | * @throws RuntimeException 当尝试修改超级管理员角色状态时 | 334 | * @throws RuntimeException 当尝试修改超级管理员角色状态时 |
| 330 | */ | 335 | */ |
| 331 | @Operation(summary = "修改角色状态", description = "启用或停用角色") | 336 | @Operation(summary = "修改角色状态", description = "启用或停用角色") |
| 332 | - @PutMapping("/changeStatus") | 337 | + @PostMapping("/changeStatus") |
| 333 | @PreAuthorize("hasAuthority('sys:role:edit')") | 338 | @PreAuthorize("hasAuthority('sys:role:edit')") |
| 334 | public ApiRes<Void> changeStatus( | 339 | public ApiRes<Void> changeStatus( |
| 335 | @Parameter(description = "角色信息,主要使用角色ID和状态字段", required = true) @RequestBody SysRole role) { | 340 | @Parameter(description = "角色信息,主要使用角色ID和状态字段", required = true) @RequestBody SysRole role) { |
| ... | @@ -358,6 +363,53 @@ public class SysRoleController { | ... | @@ -358,6 +363,53 @@ public class SysRoleController { |
| 358 | roleRes.setStatusText(role.getStatus() == 1 ? "正常" : "停用"); | 363 | roleRes.setStatusText(role.getStatus() == 1 ? "正常" : "停用"); |
| 359 | } | 364 | } |
| 360 | 365 | ||
| 366 | + // 获取角色菜单权限 | ||
| 367 | + if (role.getRoleId() != null) { | ||
| 368 | + List<SysMenu> menuList = sysMenuMapper.selectMenuTreeByRoleId(role.getRoleId()); | ||
| 369 | + if (menuList != null && !menuList.isEmpty()) { | ||
| 370 | + List<MenuRes> menuResList = menuList.stream() | ||
| 371 | + .map(this::convertToMenuRes) | ||
| 372 | + .collect(Collectors.toList()); | ||
| 373 | + roleRes.setMenus(menuResList); | ||
| 374 | + } | ||
| 375 | + } | ||
| 376 | + | ||
| 361 | return roleRes; | 377 | return roleRes; |
| 362 | } | 378 | } |
| 379 | + | ||
| 380 | + /** | ||
| 381 | + * 转换菜单实体为菜单响应DTO | ||
| 382 | + * | ||
| 383 | + * @param menu 菜单实体 | ||
| 384 | + * @return 菜单响应DTO | ||
| 385 | + */ | ||
| 386 | + private MenuRes convertToMenuRes(SysMenu menu) { | ||
| 387 | + MenuRes menuRes = new MenuRes(); | ||
| 388 | + BeanUtils.copyProperties(menu, menuRes); | ||
| 389 | + | ||
| 390 | + // 设置菜单类型描述 | ||
| 391 | + if (menu.getMenuType() != null) { | ||
| 392 | + switch (menu.getMenuType()) { | ||
| 393 | + case "0": | ||
| 394 | + menuRes.setMenuTypeText("目录"); | ||
| 395 | + break; | ||
| 396 | + case "1": | ||
| 397 | + menuRes.setMenuTypeText("菜单"); | ||
| 398 | + break; | ||
| 399 | + case "2": | ||
| 400 | + menuRes.setMenuTypeText("按钮"); | ||
| 401 | + break; | ||
| 402 | + default: | ||
| 403 | + menuRes.setMenuTypeText("未知"); | ||
| 404 | + break; | ||
| 405 | + } | ||
| 406 | + } | ||
| 407 | + | ||
| 408 | + // 设置状态描述 | ||
| 409 | + if (menu.getStatus() != null) { | ||
| 410 | + menuRes.setStatusText(menu.getStatus().equals(1) ? "显示" : "隐藏"); | ||
| 411 | + } | ||
| 412 | + | ||
| 413 | + return menuRes; | ||
| 414 | + } | ||
| 363 | } | 415 | } | ... | ... |
| ... | @@ -8,6 +8,7 @@ import com.apple.erp.dto.response.ApiRes; | ... | @@ -8,6 +8,7 @@ import com.apple.erp.dto.response.ApiRes; |
| 8 | import com.apple.erp.dto.response.UserRes; | 8 | import com.apple.erp.dto.response.UserRes; |
| 9 | import com.apple.erp.entity.SysUser; | 9 | import com.apple.erp.entity.SysUser; |
| 10 | import com.apple.erp.service.SysUserService; | 10 | import com.apple.erp.service.SysUserService; |
| 11 | +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||
| 11 | import com.baomidou.mybatisplus.core.metadata.IPage; | 12 | import com.baomidou.mybatisplus.core.metadata.IPage; |
| 12 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | 13 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| 13 | import io.swagger.v3.oas.annotations.Operation; | 14 | import io.swagger.v3.oas.annotations.Operation; |
| ... | @@ -17,6 +18,7 @@ import org.springframework.beans.BeanUtils; | ... | @@ -17,6 +18,7 @@ import org.springframework.beans.BeanUtils; |
| 17 | import org.springframework.beans.factory.annotation.Autowired; | 18 | import org.springframework.beans.factory.annotation.Autowired; |
| 18 | import org.springframework.security.access.prepost.PreAuthorize; | 19 | import org.springframework.security.access.prepost.PreAuthorize; |
| 19 | import org.springframework.security.crypto.password.PasswordEncoder; | 20 | import org.springframework.security.crypto.password.PasswordEncoder; |
| 21 | +import org.springframework.transaction.annotation.Transactional; | ||
| 20 | import org.springframework.validation.annotation.Validated; | 22 | import org.springframework.validation.annotation.Validated; |
| 21 | import org.springframework.web.bind.annotation.*; | 23 | import org.springframework.web.bind.annotation.*; |
| 22 | 24 | ||
| ... | @@ -53,7 +55,7 @@ public class SysUserController { | ... | @@ -53,7 +55,7 @@ public class SysUserController { |
| 53 | * @apiNote 接口路径: GET /api/system/user/list | 55 | * @apiNote 接口路径: GET /api/system/user/list |
| 54 | * @apiNote 权限要求: sys:user:list | 56 | * @apiNote 权限要求: sys:user:list |
| 55 | * @apiNote 支持分页参数: pageNum(页码), pageSize(每页大小) | 57 | * @apiNote 支持分页参数: pageNum(页码), pageSize(每页大小) |
| 56 | - * @apiNote 支持筛选条件: username(用户名), realName(真实姓名), phone(手机号), email(邮箱), status(状态) | 58 | + * @apiNote 支持筛选条件: username(用户名), realName(真实姓名), phone(手机号), email(邮箱), status(状态), startTime(开始时间), endTime(结束时间) |
| 57 | * | 59 | * |
| 58 | * @param queryReq 用户查询条件,包含分页参数和筛选条件 | 60 | * @param queryReq 用户查询条件,包含分页参数和筛选条件 |
| 59 | * - pageNum: 页码,默认1 | 61 | * - pageNum: 页码,默认1 |
| ... | @@ -63,6 +65,8 @@ public class SysUserController { | ... | @@ -63,6 +65,8 @@ public class SysUserController { |
| 63 | * - phone: 手机号,支持模糊查询 | 65 | * - phone: 手机号,支持模糊查询 |
| 64 | * - email: 邮箱,支持模糊查询 | 66 | * - email: 邮箱,支持模糊查询 |
| 65 | * - status: 用户状态,0-停用,1-启用 | 67 | * - status: 用户状态,0-停用,1-启用 |
| 68 | + * - startTime: 开始时间,创建时间范围查询的起始时间 | ||
| 69 | + * - endTime: 结束时间,创建时间范围查询的结束时间 | ||
| 66 | * @return 分页的用户列表数据 | 70 | * @return 分页的用户列表数据 |
| 67 | * - code: 响应码,200表示成功 | 71 | * - code: 响应码,200表示成功 |
| 68 | * - message: 响应消息 | 72 | * - message: 响应消息 |
| ... | @@ -73,18 +77,17 @@ public class SysUserController { | ... | @@ -73,18 +77,17 @@ public class SysUserController { |
| 73 | * - size: 每页大小 | 77 | * - size: 每页大小 |
| 74 | * @throws SecurityException 当用户没有sys:user:list权限时 | 78 | * @throws SecurityException 当用户没有sys:user:list权限时 |
| 75 | */ | 79 | */ |
| 76 | - @Operation(summary = "获取用户列表", description = "支持分页查询和条件筛选,包括用户名、真实姓名、手机号、邮箱、状态等条件") | 80 | + @Operation(summary = "获取用户列表", description = "支持分页查询和条件筛选,包括用户名、真实姓名、手机号、邮箱、状态、创建时间范围等条件") |
| 77 | @GetMapping("/list") | 81 | @GetMapping("/list") |
| 78 | @PreAuthorize("hasAuthority('sys:user:list')") | 82 | @PreAuthorize("hasAuthority('sys:user:list')") |
| 79 | public ApiRes<IPage<UserRes>> list( | 83 | public ApiRes<IPage<UserRes>> list( |
| 80 | @Parameter(description = "用户查询条件,包含分页参数和筛选条件") UserQueryReq queryReq) { | 84 | @Parameter(description = "用户查询条件,包含分页参数和筛选条件") UserQueryReq queryReq) { |
| 81 | Page<SysUser> page = new Page<>(queryReq.getPageNum(), queryReq.getPageSize()); | 85 | Page<SysUser> page = new Page<>(queryReq.getPageNum(), queryReq.getPageSize()); |
| 82 | 86 | ||
| 83 | - // 构建查询条件 | 87 | + // 获取查询条件包装器(包含所有查询条件) |
| 84 | - SysUser queryUser = new SysUser(); | 88 | + LambdaQueryWrapper<SysUser> wrapper = sysUserService.getQueryWrapper(queryReq); |
| 85 | - BeanUtils.copyProperties(queryReq, queryUser); | ||
| 86 | 89 | ||
| 87 | - IPage<SysUser> userPage = sysUserService.page(page, sysUserService.getQueryWrapper(queryUser)); | 90 | + IPage<SysUser> userPage = sysUserService.page(page, wrapper); |
| 88 | 91 | ||
| 89 | // 转换为响应DTO | 92 | // 转换为响应DTO |
| 90 | List<UserRes> userResList = userPage.getRecords().stream() | 93 | List<UserRes> userResList = userPage.getRecords().stream() |
| ... | @@ -143,7 +146,7 @@ public class SysUserController { | ... | @@ -143,7 +146,7 @@ public class SysUserController { |
| 143 | * 新增用户 | 146 | * 新增用户 |
| 144 | * 创建新用户,包括用户基本信息和角色分配 | 147 | * 创建新用户,包括用户基本信息和角色分配 |
| 145 | * | 148 | * |
| 146 | - * @apiNote 接口路径: POST /api/system/user | 149 | + * @apiNote 接口路径: POST /api/system/user/add |
| 147 | * @apiNote 权限要求: sys:user:add | 150 | * @apiNote 权限要求: sys:user:add |
| 148 | * @apiNote 功能说明: 创建新用户账户,支持同时分配角色权限 | 151 | * @apiNote 功能说明: 创建新用户账户,支持同时分配角色权限 |
| 149 | * @apiNote 数据验证: 用户名唯一性检查,密码强度验证,邮箱格式验证 | 152 | * @apiNote 数据验证: 用户名唯一性检查,密码强度验证,邮箱格式验证 |
| ... | @@ -165,8 +168,9 @@ public class SysUserController { | ... | @@ -165,8 +168,9 @@ public class SysUserController { |
| 165 | * @throws RuntimeException 当用户名已存在时 | 168 | * @throws RuntimeException 当用户名已存在时 |
| 166 | */ | 169 | */ |
| 167 | @Operation(summary = "新增用户", description = "创建新用户,包括用户基本信息和角色分配") | 170 | @Operation(summary = "新增用户", description = "创建新用户,包括用户基本信息和角色分配") |
| 168 | - @PostMapping | 171 | + @PostMapping("/add") |
| 169 | @PreAuthorize("hasAuthority('sys:user:add')") | 172 | @PreAuthorize("hasAuthority('sys:user:add')") |
| 173 | + @Transactional(rollbackFor = Exception.class) | ||
| 170 | public ApiRes<Void> add( | 174 | public ApiRes<Void> add( |
| 171 | @Parameter(description = "用户新增请求,包含用户基本信息和角色ID列表", required = true) | 175 | @Parameter(description = "用户新增请求,包含用户基本信息和角色ID列表", required = true) |
| 172 | @Valid @RequestBody UserAddReq addReq) { | 176 | @Valid @RequestBody UserAddReq addReq) { |
| ... | @@ -198,7 +202,7 @@ public class SysUserController { | ... | @@ -198,7 +202,7 @@ public class SysUserController { |
| 198 | * 修改用户 | 202 | * 修改用户 |
| 199 | * 更新用户基本信息,包括用户资料和角色分配 | 203 | * 更新用户基本信息,包括用户资料和角色分配 |
| 200 | * | 204 | * |
| 201 | - * @apiNote 接口路径: PUT /api/system/user | 205 | + * @apiNote 接口路径: POST /api/system/user/edit |
| 202 | * @apiNote 权限要求: sys:user:edit | 206 | * @apiNote 权限要求: sys:user:edit |
| 203 | * @apiNote 功能说明: 更新现有用户的基本信息和角色分配 | 207 | * @apiNote 功能说明: 更新现有用户的基本信息和角色分配 |
| 204 | * @apiNote 数据验证: 用户存在性检查,邮箱格式验证,角色有效性验证 | 208 | * @apiNote 数据验证: 用户存在性检查,邮箱格式验证,角色有效性验证 |
| ... | @@ -219,8 +223,9 @@ public class SysUserController { | ... | @@ -219,8 +223,9 @@ public class SysUserController { |
| 219 | * @throws RuntimeException 当用户不存在时 | 223 | * @throws RuntimeException 当用户不存在时 |
| 220 | */ | 224 | */ |
| 221 | @Operation(summary = "修改用户", description = "更新用户基本信息,包括用户资料和角色分配") | 225 | @Operation(summary = "修改用户", description = "更新用户基本信息,包括用户资料和角色分配") |
| 222 | - @PutMapping | 226 | + @PostMapping("/edit") |
| 223 | @PreAuthorize("hasAuthority('sys:user:edit')") | 227 | @PreAuthorize("hasAuthority('sys:user:edit')") |
| 228 | + @Transactional(rollbackFor = Exception.class) | ||
| 224 | public ApiRes<Void> edit( | 229 | public ApiRes<Void> edit( |
| 225 | @Parameter(description = "用户更新请求,包含用户ID、基本信息和角色ID列表", required = true) | 230 | @Parameter(description = "用户更新请求,包含用户ID、基本信息和角色ID列表", required = true) |
| 226 | @Valid @RequestBody UserUpdateReq updateReq) { | 231 | @Valid @RequestBody UserUpdateReq updateReq) { |
| ... | @@ -274,6 +279,7 @@ public class SysUserController { | ... | @@ -274,6 +279,7 @@ public class SysUserController { |
| 274 | @Operation(summary = "删除用户", description = "批量删除用户,会同时清理用户角色关联关系") | 279 | @Operation(summary = "删除用户", description = "批量删除用户,会同时清理用户角色关联关系") |
| 275 | @DeleteMapping("/{userIds}") | 280 | @DeleteMapping("/{userIds}") |
| 276 | @PreAuthorize("hasAuthority('sys:user:remove')") | 281 | @PreAuthorize("hasAuthority('sys:user:remove')") |
| 282 | + @Transactional(rollbackFor = Exception.class) | ||
| 277 | public ApiRes<Void> remove( | 283 | public ApiRes<Void> remove( |
| 278 | @Parameter(description = "需要删除的用户ID数组", required = true) @PathVariable Long[] userIds) { | 284 | @Parameter(description = "需要删除的用户ID数组", required = true) @PathVariable Long[] userIds) { |
| 279 | // 检查是否包含超级管理员 | 285 | // 检查是否包含超级管理员 |
| ... | @@ -341,7 +347,7 @@ public class SysUserController { | ... | @@ -341,7 +347,7 @@ public class SysUserController { |
| 341 | * 状态修改 | 347 | * 状态修改 |
| 342 | * 启用或停用用户账户 | 348 | * 启用或停用用户账户 |
| 343 | * | 349 | * |
| 344 | - * @apiNote 接口路径: PUT /api/system/user/changeStatus | 350 | + * @apiNote 接口路径: POST /api/system/user/changeStatus |
| 345 | * @apiNote 权限要求: sys:user:edit | 351 | * @apiNote 权限要求: sys:user:edit |
| 346 | * @apiNote 功能说明: 修改用户账户状态,支持启用或停用用户 | 352 | * @apiNote 功能说明: 修改用户账户状态,支持启用或停用用户 |
| 347 | * @apiNote 安全限制: 不允许修改超级管理员用户状态 | 353 | * @apiNote 安全限制: 不允许修改超级管理员用户状态 |
| ... | @@ -358,7 +364,7 @@ public class SysUserController { | ... | @@ -358,7 +364,7 @@ public class SysUserController { |
| 358 | * @throws RuntimeException 当尝试修改超级管理员状态时 | 364 | * @throws RuntimeException 当尝试修改超级管理员状态时 |
| 359 | */ | 365 | */ |
| 360 | @Operation(summary = "修改用户状态", description = "启用或停用用户账户") | 366 | @Operation(summary = "修改用户状态", description = "启用或停用用户账户") |
| 361 | - @PutMapping("/changeStatus") | 367 | + @PostMapping("/changeStatus") |
| 362 | @PreAuthorize("hasAuthority('sys:user:edit')") | 368 | @PreAuthorize("hasAuthority('sys:user:edit')") |
| 363 | public ApiRes<Void> changeStatus( | 369 | public ApiRes<Void> changeStatus( |
| 364 | @Parameter(description = "用户信息,主要使用用户ID和状态字段", required = true) @RequestBody SysUser user) { | 370 | @Parameter(description = "用户信息,主要使用用户ID和状态字段", required = true) @RequestBody SysUser user) { | ... | ... |
| ... | @@ -2,6 +2,7 @@ package com.apple.erp.dto.request; | ... | @@ -2,6 +2,7 @@ package com.apple.erp.dto.request; |
| 2 | 2 | ||
| 3 | import lombok.Data; | 3 | import lombok.Data; |
| 4 | 4 | ||
| 5 | + | ||
| 5 | /** | 6 | /** |
| 6 | * 用户查询请求DTO | 7 | * 用户查询请求DTO |
| 7 | * | 8 | * |
| ... | @@ -38,6 +39,16 @@ public class UserQueryReq { | ... | @@ -38,6 +39,16 @@ public class UserQueryReq { |
| 38 | private Integer status; | 39 | private Integer status; |
| 39 | 40 | ||
| 40 | /** | 41 | /** |
| 42 | + * 开始时间(创建时间范围查询) | ||
| 43 | + */ | ||
| 44 | + private String startTime; | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 结束时间(创建时间范围查询) | ||
| 48 | + */ | ||
| 49 | + private String endTime; | ||
| 50 | + | ||
| 51 | + /** | ||
| 41 | * 页码 | 52 | * 页码 |
| 42 | */ | 53 | */ |
| 43 | private Integer pageNum = 1; | 54 | private Integer pageNum = 1; | ... | ... |
| ... | @@ -33,8 +33,9 @@ public interface SysUserMapper extends BaseMapper<SysUser> { | ... | @@ -33,8 +33,9 @@ public interface SysUserMapper extends BaseMapper<SysUser> { |
| 33 | 33 | ||
| 34 | /** | 34 | /** |
| 35 | * 根据用户名查询用户基本信息 | 35 | * 根据用户名查询用户基本信息 |
| 36 | + * 用于用户名唯一性检查,需要查询所有状态的用户(包括停用用户) | ||
| 36 | */ | 37 | */ |
| 37 | - @Select("SELECT * FROM t_sys_user WHERE username = #{username} AND del_flag = '0' AND status = 1") | 38 | + @Select("SELECT * FROM t_sys_user WHERE username = #{username} AND del_flag = '0'") |
| 38 | SysUser findByUsername(@Param("username") String username); | 39 | SysUser findByUsername(@Param("username") String username); |
| 39 | 40 | ||
| 40 | /** | 41 | /** | ... | ... |
| 1 | package com.apple.erp.service; | 1 | package com.apple.erp.service; |
| 2 | 2 | ||
| 3 | +import com.apple.erp.dto.request.UserQueryReq; | ||
| 3 | import com.apple.erp.dto.response.RoleRes; | 4 | import com.apple.erp.dto.response.RoleRes; |
| 4 | import com.apple.erp.entity.SysUser; | 5 | import com.apple.erp.entity.SysUser; |
| 5 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | 6 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| ... | @@ -38,12 +39,12 @@ public interface SysUserService extends IService<SysUser> { | ... | @@ -38,12 +39,12 @@ public interface SysUserService extends IService<SysUser> { |
| 38 | 39 | ||
| 39 | /** | 40 | /** |
| 40 | * 获取用户查询条件包装器 | 41 | * 获取用户查询条件包装器 |
| 41 | - * 根据用户信息构建MyBatis Plus查询条件,支持用户名、真实姓名、手机号、邮箱、状态的模糊查询 | 42 | + * 根据查询请求构建MyBatis Plus查询条件,支持用户名、真实姓名、手机号、邮箱、状态、创建时间范围等条件 |
| 42 | * | 43 | * |
| 43 | - * @param user 用户查询条件对象 | 44 | + * @param queryReq 用户查询请求对象 |
| 44 | * @return MyBatis Plus查询条件包装器 | 45 | * @return MyBatis Plus查询条件包装器 |
| 45 | */ | 46 | */ |
| 46 | - LambdaQueryWrapper<SysUser> getQueryWrapper(SysUser user); | 47 | + LambdaQueryWrapper<SysUser> getQueryWrapper(UserQueryReq queryReq); |
| 47 | 48 | ||
| 48 | /** | 49 | /** |
| 49 | * 获取用户角色列表 | 50 | * 获取用户角色列表 | ... | ... |
| 1 | package com.apple.erp.service.impl; | 1 | package com.apple.erp.service.impl; |
| 2 | 2 | ||
| 3 | +import com.apple.erp.dto.request.UserQueryReq; | ||
| 3 | import com.apple.erp.dto.response.RoleRes; | 4 | import com.apple.erp.dto.response.RoleRes; |
| 4 | import com.apple.erp.entity.SysRole; | 5 | import com.apple.erp.entity.SysRole; |
| 5 | import com.apple.erp.entity.SysUser; | 6 | import com.apple.erp.entity.SysUser; |
| ... | @@ -17,6 +18,8 @@ import org.springframework.stereotype.Service; | ... | @@ -17,6 +18,8 @@ import org.springframework.stereotype.Service; |
| 17 | import org.springframework.transaction.annotation.Transactional; | 18 | import org.springframework.transaction.annotation.Transactional; |
| 18 | 19 | ||
| 19 | import java.time.LocalDateTime; | 20 | import java.time.LocalDateTime; |
| 21 | +import java.time.format.DateTimeFormatter; | ||
| 22 | +import java.time.format.DateTimeParseException; | ||
| 20 | import java.util.ArrayList; | 23 | import java.util.ArrayList; |
| 21 | import java.util.List; | 24 | import java.util.List; |
| 22 | import java.util.concurrent.TimeUnit; | 25 | import java.util.concurrent.TimeUnit; |
| ... | @@ -93,39 +96,55 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl | ... | @@ -93,39 +96,55 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl |
| 93 | 96 | ||
| 94 | /** | 97 | /** |
| 95 | * 获取用户查询条件包装器 | 98 | * 获取用户查询条件包装器 |
| 96 | - * 根据用户信息构建MyBatis Plus查询条件,支持用户名、真实姓名、手机号、邮箱、状态的模糊查询 | 99 | + * 根据查询请求构建MyBatis Plus查询条件,支持用户名、真实姓名、手机号、邮箱、状态、创建时间范围等条件 |
| 97 | * 优化判空规则,兼容空字符串和null的情况 | 100 | * 优化判空规则,兼容空字符串和null的情况 |
| 98 | * | 101 | * |
| 99 | - * @param user 用户查询条件对象 | 102 | + * @param queryReq 用户查询请求对象 |
| 100 | * @return MyBatis Plus查询条件包装器 | 103 | * @return MyBatis Plus查询条件包装器 |
| 101 | */ | 104 | */ |
| 102 | @Override | 105 | @Override |
| 103 | - public LambdaQueryWrapper<SysUser> getQueryWrapper(SysUser user) { | 106 | + public LambdaQueryWrapper<SysUser> getQueryWrapper(UserQueryReq queryReq) { |
| 104 | LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); | 107 | LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); |
| 105 | 108 | ||
| 106 | // 用户名:支持模糊查询,过滤空字符串和null | 109 | // 用户名:支持模糊查询,过滤空字符串和null |
| 107 | - if (StringUtils.isNotEmpty(user.getUsername())) { | 110 | + if (StringUtils.isNotEmpty(queryReq.getUsername())) { |
| 108 | - wrapper.like(SysUser::getUsername, user.getUsername()); | 111 | + wrapper.like(SysUser::getUsername, queryReq.getUsername()); |
| 109 | } | 112 | } |
| 110 | 113 | ||
| 111 | // 真实姓名:支持模糊查询,过滤空字符串和null | 114 | // 真实姓名:支持模糊查询,过滤空字符串和null |
| 112 | - if (StringUtils.isNotEmpty(user.getRealName())) { | 115 | + if (StringUtils.isNotEmpty(queryReq.getRealName())) { |
| 113 | - wrapper.like(SysUser::getRealName, user.getRealName()); | 116 | + wrapper.like(SysUser::getRealName, queryReq.getRealName()); |
| 114 | } | 117 | } |
| 115 | 118 | ||
| 116 | // 手机号:支持模糊查询,过滤空字符串和null | 119 | // 手机号:支持模糊查询,过滤空字符串和null |
| 117 | - if (StringUtils.isNotEmpty(user.getPhone())) { | 120 | + if (StringUtils.isNotEmpty(queryReq.getPhone())) { |
| 118 | - wrapper.like(SysUser::getPhone, user.getPhone()); | 121 | + wrapper.like(SysUser::getPhone, queryReq.getPhone()); |
| 119 | } | 122 | } |
| 120 | 123 | ||
| 121 | // 邮箱:支持模糊查询,过滤空字符串和null | 124 | // 邮箱:支持模糊查询,过滤空字符串和null |
| 122 | - if (StringUtils.isNotEmpty(user.getEmail())) { | 125 | + if (StringUtils.isNotEmpty(queryReq.getEmail())) { |
| 123 | - wrapper.like(SysUser::getEmail, user.getEmail()); | 126 | + wrapper.like(SysUser::getEmail, queryReq.getEmail()); |
| 124 | } | 127 | } |
| 125 | 128 | ||
| 126 | // 状态:精确查询 | 129 | // 状态:精确查询 |
| 127 | - if (user.getStatus() != null) { | 130 | + if (queryReq.getStatus() != null) { |
| 128 | - wrapper.eq(SysUser::getStatus, user.getStatus()); | 131 | + wrapper.eq(SysUser::getStatus, queryReq.getStatus()); |
| 132 | + } | ||
| 133 | + | ||
| 134 | + // 开始时间:创建时间 >= 开始时间 | ||
| 135 | + if (StringUtils.isNotEmpty(queryReq.getStartTime())) { | ||
| 136 | + LocalDateTime startDateTime = parseDateTime(queryReq.getStartTime()); | ||
| 137 | + if (startDateTime != null) { | ||
| 138 | + wrapper.ge(SysUser::getCreateTime, startDateTime); | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + // 结束时间:创建时间 <= 结束时间 | ||
| 143 | + if (StringUtils.isNotEmpty(queryReq.getEndTime())) { | ||
| 144 | + LocalDateTime endDateTime = parseDateTime(queryReq.getEndTime()); | ||
| 145 | + if (endDateTime != null) { | ||
| 146 | + wrapper.le(SysUser::getCreateTime, endDateTime); | ||
| 147 | + } | ||
| 129 | } | 148 | } |
| 130 | 149 | ||
| 131 | // 固定条件:未删除 | 150 | // 固定条件:未删除 |
| ... | @@ -138,6 +157,44 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl | ... | @@ -138,6 +157,44 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl |
| 138 | } | 157 | } |
| 139 | 158 | ||
| 140 | /** | 159 | /** |
| 160 | + * 解析时间字符串为LocalDateTime | ||
| 161 | + * 支持格式:yyyy-MM-dd, yyyy-MM-dd HH:mm:ss | ||
| 162 | + * | ||
| 163 | + * @param dateTimeStr 时间字符串 | ||
| 164 | + * @return LocalDateTime对象,解析失败返回null | ||
| 165 | + */ | ||
| 166 | + private LocalDateTime parseDateTime(String dateTimeStr) { | ||
| 167 | + if (StringUtils.isEmpty(dateTimeStr)) { | ||
| 168 | + return null; | ||
| 169 | + } | ||
| 170 | + | ||
| 171 | + // 支持的时间格式 | ||
| 172 | + DateTimeFormatter[] formatters = { | ||
| 173 | + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"), | ||
| 174 | + DateTimeFormatter.ofPattern("yyyy-MM-dd") | ||
| 175 | + }; | ||
| 176 | + | ||
| 177 | + // 尝试各种格式解析 | ||
| 178 | + for (DateTimeFormatter formatter : formatters) { | ||
| 179 | + try { | ||
| 180 | + if (formatter.toString().contains("HH:mm:ss")) { | ||
| 181 | + // 完整时间格式 | ||
| 182 | + return LocalDateTime.parse(dateTimeStr, formatter); | ||
| 183 | + } else { | ||
| 184 | + // 仅日期格式,转换为当天的00:00:00 | ||
| 185 | + return LocalDateTime.parse(dateTimeStr + " 00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); | ||
| 186 | + } | ||
| 187 | + } catch (DateTimeParseException e) { | ||
| 188 | + continue; | ||
| 189 | + } | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + // 解析失败,记录日志 | ||
| 193 | + System.err.println("时间格式解析失败: " + dateTimeStr); | ||
| 194 | + return null; | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + /** | ||
| 141 | * 获取用户角色列表 | 198 | * 获取用户角色列表 |
| 142 | * 查询指定用户拥有的所有角色信息,包含角色基本信息和状态描述 | 199 | * 查询指定用户拥有的所有角色信息,包含角色基本信息和状态描述 |
| 143 | * | 200 | * | ... | ... |
| ... | @@ -54,7 +54,7 @@ | ... | @@ -54,7 +54,7 @@ |
| 54 | select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.menu_type, m.status, m.perms, m.icon, m.create_by, m.create_time, m.update_by, m.update_time, m.del_flag | 54 | select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.menu_type, m.status, m.perms, m.icon, m.create_by, m.create_time, m.update_by, m.update_time, m.del_flag |
| 55 | from t_sys_menu m | 55 | from t_sys_menu m |
| 56 | left join t_sys_role_menu rm on m.menu_id = rm.menu_id | 56 | left join t_sys_role_menu rm on m.menu_id = rm.menu_id |
| 57 | - where rm.role_id = #{roleId} and m.menu_type in ('0','1') and m.status = 1 and m.del_flag = '0' | 57 | + where rm.role_id = #{roleId} and m.status = 1 and m.del_flag = '0' |
| 58 | order by m.parent_id, m.sort | 58 | order by m.parent_id, m.sort |
| 59 | </select> | 59 | </select> |
| 60 | 60 | ... | ... |
| ... | @@ -34,7 +34,7 @@ | ... | @@ -34,7 +34,7 @@ |
| 34 | <!-- 根据用户名查询用户基本信息 --> | 34 | <!-- 根据用户名查询用户基本信息 --> |
| 35 | <select id="findByUsername" resultType="com.apple.erp.entity.SysUser"> | 35 | <select id="findByUsername" resultType="com.apple.erp.entity.SysUser"> |
| 36 | SELECT * FROM t_sys_user | 36 | SELECT * FROM t_sys_user |
| 37 | - WHERE username = #{username} AND del_flag = '0' AND status = 1 | 37 | + WHERE username = #{username} |
| 38 | </select> | 38 | </select> |
| 39 | 39 | ||
| 40 | <!-- 根据用户ID查询菜单权限标识列表 --> | 40 | <!-- 根据用户ID查询菜单权限标识列表 --> | ... | ... |
| ... | @@ -79,9 +79,3 @@ declare global { | ... | @@ -79,9 +79,3 @@ declare global { |
| 79 | const watchPostEffect: typeof import('vue')['watchPostEffect'] | 79 | const watchPostEffect: typeof import('vue')['watchPostEffect'] |
| 80 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] | 80 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] |
| 81 | } | 81 | } |
| 82 | -// for type re-export | ||
| 83 | -declare global { | ||
| 84 | - // @ts-ignore | ||
| 85 | - export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' | ||
| 86 | - import('vue') | ||
| 87 | -} | ... | ... |
| ... | @@ -7,10 +7,21 @@ export {} | ... | @@ -7,10 +7,21 @@ export {} |
| 7 | 7 | ||
| 8 | declare module 'vue' { | 8 | declare module 'vue' { |
| 9 | export interface GlobalComponents { | 9 | export interface GlobalComponents { |
| 10 | + ActionBar: typeof import('./src/components/common/ActionBar.vue')['default'] | ||
| 11 | + DataTable: typeof import('./src/components/common/DataTable.vue')['default'] | ||
| 12 | + ElButton: typeof import('element-plus/es')['ElButton'] | ||
| 13 | + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] | ||
| 14 | + ElForm: typeof import('element-plus/es')['ElForm'] | ||
| 15 | + ElFormItem: typeof import('element-plus/es')['ElFormItem'] | ||
| 16 | + ElInput: typeof import('element-plus/es')['ElInput'] | ||
| 17 | + ElLink: typeof import('element-plus/es')['ElLink'] | ||
| 10 | Header: typeof import('./src/components/layout/Header.vue')['default'] | 18 | Header: typeof import('./src/components/layout/Header.vue')['default'] |
| 19 | + Pagination: typeof import('./src/components/common/Pagination.vue')['default'] | ||
| 11 | RouterLink: typeof import('vue-router')['RouterLink'] | 20 | RouterLink: typeof import('vue-router')['RouterLink'] |
| 12 | RouterView: typeof import('vue-router')['RouterView'] | 21 | RouterView: typeof import('vue-router')['RouterView'] |
| 22 | + SearchBar: typeof import('./src/components/common/SearchBar.vue')['default'] | ||
| 13 | Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default'] | 23 | Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default'] |
| 14 | SidebarItem: typeof import('./src/components/layout/SidebarItem.vue')['default'] | 24 | SidebarItem: typeof import('./src/components/layout/SidebarItem.vue')['default'] |
| 25 | + TableToolbar: typeof import('./src/components/common/TableToolbar.vue')['default'] | ||
| 15 | } | 26 | } |
| 16 | } | 27 | } | ... | ... |
frontend/index.html
0 → 100644
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html lang="zh-CN"> | ||
| 3 | + <head> | ||
| 4 | + <meta charset="UTF-8" /> | ||
| 5 | + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| 6 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| 7 | + <title>Apple经销商ERP系统</title> | ||
| 8 | + <meta name="description" content="Apple经销商ERP系统前端应用" /> | ||
| 9 | + <meta name="keywords" content="Apple,ERP,经销商,管理系统" /> | ||
| 10 | + </head> | ||
| 11 | + <body> | ||
| 12 | + <div id="app"></div> | ||
| 13 | + <script type="module" src="/src/main.ts"></script> | ||
| 14 | + </body> | ||
| 15 | +</html> |
| 1 | <template> | 1 | <template> |
| 2 | - <div id="app"> | 2 | + <!-- <div id="app"> |
| 3 | <h1>Vue应用已启动</h1> | 3 | <h1>Vue应用已启动</h1> |
| 4 | <p>如果您能看到这个页面,说明Vue应用正常运行</p> | 4 | <p>如果您能看到这个页面,说明Vue应用正常运行</p> |
| 5 | <p>当前时间: {{ currentTime }}</p> | 5 | <p>当前时间: {{ currentTime }}</p> |
| 6 | - </div> | 6 | + </div> --> |
| 7 | + <router-view/> | ||
| 7 | </template> | 8 | </template> |
| 8 | 9 | ||
| 9 | <script setup lang="ts"> | 10 | <script setup lang="ts"> |
| ... | @@ -18,9 +19,23 @@ onMounted(() => { | ... | @@ -18,9 +19,23 @@ onMounted(() => { |
| 18 | </script> | 19 | </script> |
| 19 | 20 | ||
| 20 | <style> | 21 | <style> |
| 22 | +* { | ||
| 23 | + margin: 0; | ||
| 24 | + padding: 0; | ||
| 25 | + box-sizing: border-box; | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +html, body { | ||
| 29 | + margin: 0; | ||
| 30 | + padding: 0; | ||
| 31 | + height: 100%; | ||
| 32 | + overflow-x: hidden; | ||
| 33 | +} | ||
| 34 | + | ||
| 21 | #app { | 35 | #app { |
| 22 | height: 100vh; | 36 | height: 100vh; |
| 23 | font-family: Arial, sans-serif; | 37 | font-family: Arial, sans-serif; |
| 24 | - padding: 20px; | 38 | + margin: 0; |
| 39 | + padding: 0; | ||
| 25 | } | 40 | } |
| 26 | </style> | 41 | </style> | ... | ... |
| 1 | -import { request } from '@/utils/request' | 1 | +import axios from 'axios' |
| 2 | -import type { DictType, DictItem, PageRequest, PageResponse } from '@/types' | 2 | + |
| 3 | - | 3 | +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8083' |
| 4 | -// 字典类型相关API | 4 | + |
| 5 | - | 5 | +const api = axios.create({ |
| 6 | -// 获取字典类型列表 | 6 | + baseURL: API_BASE_URL, |
| 7 | -export function getDictTypeListApi(): Promise<DictType[]> { | 7 | + timeout: 10000, |
| 8 | - return request.get('/api/sys/dict/type/list') | 8 | + headers: { |
| 9 | -} | 9 | + 'Content-Type': 'application/json' |
| 10 | - | 10 | + } |
| 11 | -// 获取字典类型分页列表 | 11 | +}) |
| 12 | -export function getDictTypePageApi(params: PageRequest & { | 12 | + |
| 13 | - dictName?: string | 13 | +api.interceptors.request.use( |
| 14 | + (config) => { | ||
| 15 | + const token = localStorage.getItem('token') | ||
| 16 | + if (token) { | ||
| 17 | + config.headers.Authorization = `Bearer ${token}` | ||
| 18 | + } | ||
| 19 | + return config | ||
| 20 | + }, | ||
| 21 | + (error) => { | ||
| 22 | + return Promise.reject(error) | ||
| 23 | + } | ||
| 24 | +) | ||
| 25 | + | ||
| 26 | +api.interceptors.response.use( | ||
| 27 | + (response) => { | ||
| 28 | + return response.data | ||
| 29 | + }, | ||
| 30 | + (error) => { | ||
| 31 | + console.error('API请求错误:', error) | ||
| 32 | + return Promise.reject(error) | ||
| 33 | + } | ||
| 34 | +) | ||
| 35 | + | ||
| 36 | +export interface DictType { | ||
| 37 | + dictTypeId: number | ||
| 38 | + dictType: string | ||
| 39 | + dictName: string | ||
| 40 | + status: number | ||
| 41 | + statusText: string | ||
| 42 | + remark?: string | ||
| 43 | + createBy?: string | ||
| 44 | + createTime: string | ||
| 45 | + updateBy?: string | ||
| 46 | + updateTime: string | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +export interface DictItem { | ||
| 50 | + dictItemId: number | ||
| 51 | + dictTypeId: number | ||
| 52 | + dictType: string | ||
| 53 | + dictLabel: string | ||
| 54 | + dictValue: string | ||
| 55 | + sort: number | ||
| 56 | + remark?: string | ||
| 57 | + createBy?: string | ||
| 58 | + createTime: string | ||
| 59 | + updateBy?: string | ||
| 60 | + updateTime: string | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +export interface DictTypeSearchParams { | ||
| 14 | dictType?: string | 64 | dictType?: string |
| 65 | + dictName?: string | ||
| 15 | status?: number | 66 | status?: number |
| 16 | -}): Promise<PageResponse<DictType>> { | 67 | + pageNum?: number |
| 17 | - return request.get('/api/sys/dict/type/page', params) | 68 | + pageSize?: number |
| 18 | -} | ||
| 19 | - | ||
| 20 | -// 获取字典类型详情 | ||
| 21 | -export function getDictTypeDetailApi(dictId: number): Promise<DictType> { | ||
| 22 | - return request.get(`/api/sys/dict/type/${dictId}`) | ||
| 23 | -} | ||
| 24 | - | ||
| 25 | -// 新增字典类型 | ||
| 26 | -export function addDictTypeApi(data: Partial<DictType>): Promise<void> { | ||
| 27 | - return request.post('/api/sys/dict/type', data) | ||
| 28 | -} | ||
| 29 | - | ||
| 30 | -// 更新字典类型 | ||
| 31 | -export function updateDictTypeApi(data: Partial<DictType>): Promise<void> { | ||
| 32 | - return request.put('/api/sys/dict/type', data) | ||
| 33 | -} | ||
| 34 | - | ||
| 35 | -// 删除字典类型 | ||
| 36 | -export function deleteDictTypeApi(dictId: number): Promise<void> { | ||
| 37 | - return request.delete(`/api/sys/dict/type/${dictId}`) | ||
| 38 | -} | ||
| 39 | - | ||
| 40 | -// 批量删除字典类型 | ||
| 41 | -export function deleteBatchDictTypeApi(dictIds: number[]): Promise<void> { | ||
| 42 | - return request.delete('/api/sys/dict/type/batch', { dictIds }) | ||
| 43 | -} | ||
| 44 | - | ||
| 45 | -// 刷新字典缓存 | ||
| 46 | -export function refreshDictCacheApi(): Promise<void> { | ||
| 47 | - return request.post('/api/sys/dict/type/refreshCache') | ||
| 48 | } | 69 | } |
| 49 | 70 | ||
| 50 | -// 字典项相关API | 71 | +export interface DictItemSearchParams { |
| 51 | - | ||
| 52 | -// 获取字典项列表 | ||
| 53 | -export function getDictItemListApi(dictTypeId: number): Promise<DictItem[]> { | ||
| 54 | - return request.get('/api/sys/dict/item/list', { dictTypeId }) | ||
| 55 | -} | ||
| 56 | - | ||
| 57 | -// 获取字典项分页列表 | ||
| 58 | -export function getDictItemPageApi(params: PageRequest & { | ||
| 59 | dictTypeId?: number | 72 | dictTypeId?: number |
| 60 | dictLabel?: string | 73 | dictLabel?: string |
| 61 | dictValue?: string | 74 | dictValue?: string |
| 62 | - status?: number | 75 | + pageNum?: number |
| 63 | -}): Promise<PageResponse<DictItem>> { | 76 | + pageSize?: number |
| 64 | - return request.get('/api/sys/dict/item/page', params) | 77 | +} |
| 65 | -} | 78 | + |
| 66 | - | 79 | +export interface DictTypeAddReq { |
| 67 | -// 获取字典项详情 | 80 | + dictType: string |
| 68 | -export function getDictItemDetailApi(dictItemId: number): Promise<DictItem> { | 81 | + dictName: string |
| 69 | - return request.get(`/api/sys/dict/item/${dictItemId}`) | 82 | + status: number |
| 70 | -} | 83 | + remark?: string |
| 71 | - | 84 | +} |
| 72 | -// 新增字典项 | 85 | + |
| 73 | -export function addDictItemApi(data: Partial<DictItem>): Promise<void> { | 86 | +export interface DictTypeUpdateReq extends DictTypeAddReq { |
| 74 | - return request.post('/api/sys/dict/item', data) | 87 | + dictTypeId: number |
| 75 | -} | 88 | +} |
| 76 | - | 89 | + |
| 77 | -// 更新字典项 | 90 | +export interface DictItemAddReq { |
| 78 | -export function updateDictItemApi(data: Partial<DictItem>): Promise<void> { | 91 | + dictTypeId: number |
| 79 | - return request.put('/api/sys/dict/item', data) | 92 | + dictLabel: string |
| 80 | -} | 93 | + dictValue: string |
| 81 | - | 94 | + sort?: number |
| 82 | -// 删除字典项 | 95 | + remark?: string |
| 83 | -export function deleteDictItemApi(dictItemId: number): Promise<void> { | 96 | +} |
| 84 | - return request.delete(`/api/sys/dict/item/${dictItemId}`) | 97 | + |
| 85 | -} | 98 | +export interface DictItemUpdateReq extends DictItemAddReq { |
| 86 | - | 99 | + dictItemId: number |
| 87 | -// 批量删除字典项 | 100 | +} |
| 88 | -export function deleteBatchDictItemApi(dictItemIds: number[]): Promise<void> { | 101 | + |
| 89 | - return request.delete('/api/sys/dict/item/batch', { dictItemIds }) | 102 | +export interface ApiResponse<T = any> { |
| 90 | -} | 103 | + code: number |
| 91 | - | 104 | + message: string |
| 92 | -// 根据字典类型获取字典项 | 105 | + data: T |
| 93 | -export function getDictItemsByTypeApi(dictType: string): Promise<DictItem[]> { | 106 | +} |
| 94 | - return request.get(`/api/sys/dict/item/type/${dictType}`) | 107 | + |
| 108 | +export interface PageResponse<T> { | ||
| 109 | + records: T[] | ||
| 110 | + total: number | ||
| 111 | + size: number | ||
| 112 | + current: number | ||
| 113 | + orders: any[] | ||
| 114 | + optimizeCountSql: boolean | ||
| 115 | + searchCount: boolean | ||
| 116 | + maxLimit: any | ||
| 117 | + countId: any | ||
| 118 | + pages: number | ||
| 119 | +} | ||
| 120 | + | ||
| 121 | +export const dictApi = { | ||
| 122 | + // 字典类型管理 | ||
| 123 | + // 获取字典类型列表 | ||
| 124 | + getDictTypes: (params: DictTypeSearchParams): Promise<ApiResponse<PageResponse<DictType>>> => { | ||
| 125 | + return api.get('/api/system/dict/type/list', { params }) | ||
| 126 | + }, | ||
| 127 | + | ||
| 128 | + // 获取字典类型详情 | ||
| 129 | + getDictTypeById: (dictTypeId: number): Promise<ApiResponse<DictType>> => { | ||
| 130 | + return api.get(`/api/system/dict/type/${dictTypeId}`) | ||
| 131 | + }, | ||
| 132 | + | ||
| 133 | + // 新增字典类型 | ||
| 134 | + createDictType: (dictTypeData: DictTypeAddReq): Promise<ApiResponse<any>> => { | ||
| 135 | + return api.post('/api/system/dict/type', dictTypeData) | ||
| 136 | + }, | ||
| 137 | + | ||
| 138 | + // 修改字典类型 | ||
| 139 | + updateDictType: (dictTypeData: DictTypeUpdateReq): Promise<ApiResponse<any>> => { | ||
| 140 | + return api.put('/api/system/dict/type', dictTypeData) | ||
| 141 | + }, | ||
| 142 | + | ||
| 143 | + // 删除字典类型 | ||
| 144 | + deleteDictTypes: (dictTypeIds: number[]): Promise<ApiResponse<any>> => { | ||
| 145 | + return api.delete(`/api/system/dict/type/${dictTypeIds.join(',')}`) | ||
| 146 | + }, | ||
| 147 | + | ||
| 148 | + // 获取字典类型选择框列表 | ||
| 149 | + getDictTypeOptions: (): Promise<ApiResponse<DictType[]>> => { | ||
| 150 | + return api.get('/api/system/dict/type/optionselect') | ||
| 151 | + }, | ||
| 152 | + | ||
| 153 | + // 刷新字典缓存 | ||
| 154 | + refreshDictCache: (): Promise<ApiResponse<any>> => { | ||
| 155 | + return api.delete('/api/system/dict/type/refreshCache') | ||
| 156 | + }, | ||
| 157 | + | ||
| 158 | + // 字典项管理 | ||
| 159 | + // 获取字典项列表 | ||
| 160 | + getDictItems: (params: DictItemSearchParams): Promise<ApiResponse<PageResponse<DictItem>>> => { | ||
| 161 | + return api.get('/api/system/dict/item/list', { params }) | ||
| 162 | + }, | ||
| 163 | + | ||
| 164 | + // 根据字典类型获取字典项列表 | ||
| 165 | + getDictItemsByType: (dictType: string): Promise<ApiResponse<DictItem[]>> => { | ||
| 166 | + return api.get(`/api/system/dict/item/type/${dictType}`) | ||
| 167 | + }, | ||
| 168 | + | ||
| 169 | + // 获取字典项详情 | ||
| 170 | + getDictItemById: (dictItemId: number): Promise<ApiResponse<DictItem>> => { | ||
| 171 | + return api.get(`/api/system/dict/item/${dictItemId}`) | ||
| 172 | + }, | ||
| 173 | + | ||
| 174 | + // 新增字典项 | ||
| 175 | + createDictItem: (dictItemData: DictItemAddReq): Promise<ApiResponse<any>> => { | ||
| 176 | + return api.post('/api/system/dict/item', dictItemData) | ||
| 177 | + }, | ||
| 178 | + | ||
| 179 | + // 修改字典项 | ||
| 180 | + updateDictItem: (dictItemData: DictItemUpdateReq): Promise<ApiResponse<any>> => { | ||
| 181 | + return api.put('/api/system/dict/item', dictItemData) | ||
| 182 | + }, | ||
| 183 | + | ||
| 184 | + // 删除字典项 | ||
| 185 | + deleteDictItems: (dictItemIds: number[]): Promise<ApiResponse<any>> => { | ||
| 186 | + return api.delete(`/api/system/dict/item/${dictItemIds.join(',')}`) | ||
| 187 | + }, | ||
| 95 | } | 188 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
frontend/src/api/dictItem.ts
0 → 100644
| 1 | +import axios from 'axios' | ||
| 2 | + | ||
| 3 | +// 创建axios实例 | ||
| 4 | +const api = axios.create({ | ||
| 5 | + baseURL: 'http://localhost:8083/api/system/dict/item', | ||
| 6 | + timeout: 10000, | ||
| 7 | + headers: { | ||
| 8 | + 'Content-Type': 'application/json' | ||
| 9 | + } | ||
| 10 | +}) | ||
| 11 | + | ||
| 12 | +// 请求拦截器 | ||
| 13 | +api.interceptors.request.use( | ||
| 14 | + (config) => { | ||
| 15 | + const token = localStorage.getItem('token') | ||
| 16 | + if (token) { | ||
| 17 | + config.headers.Authorization = `Bearer ${token}` | ||
| 18 | + } | ||
| 19 | + return config | ||
| 20 | + }, | ||
| 21 | + (error) => { | ||
| 22 | + return Promise.reject(error) | ||
| 23 | + } | ||
| 24 | +) | ||
| 25 | + | ||
| 26 | +// 响应拦截器 | ||
| 27 | +api.interceptors.response.use( | ||
| 28 | + (response) => { | ||
| 29 | + return response.data | ||
| 30 | + }, | ||
| 31 | + (error) => { | ||
| 32 | + console.error('API请求错误:', error) | ||
| 33 | + if (error.response?.status === 401) { | ||
| 34 | + localStorage.removeItem('token') | ||
| 35 | + localStorage.removeItem('userInfo') | ||
| 36 | + window.location.href = '/login' | ||
| 37 | + } | ||
| 38 | + return Promise.reject(error) | ||
| 39 | + } | ||
| 40 | +) | ||
| 41 | + | ||
| 42 | +// 字典项接口类型定义 | ||
| 43 | +export interface DictItem { | ||
| 44 | + dictItemId: number | ||
| 45 | + dictTypeId: number | ||
| 46 | + dictType: string | ||
| 47 | + dictLabel: string | ||
| 48 | + dictValue: string | ||
| 49 | + sort: number | ||
| 50 | + remark: string | ||
| 51 | + createBy: string | ||
| 52 | + createTime: string | ||
| 53 | + updateBy: string | ||
| 54 | + updateTime: string | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +export interface DictItemSearchParams { | ||
| 58 | + pageNum: number | ||
| 59 | + pageSize: number | ||
| 60 | + dictTypeId?: number | ||
| 61 | + dictLabel?: string | ||
| 62 | + dictValue?: string | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | +export interface DictItemAddReq { | ||
| 66 | + dictTypeId: number | ||
| 67 | + dictLabel: string | ||
| 68 | + dictValue: string | ||
| 69 | + sort?: number | ||
| 70 | + remark?: string | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +export interface DictItemUpdateReq { | ||
| 74 | + dictItemId: number | ||
| 75 | + dictTypeId: number | ||
| 76 | + dictLabel: string | ||
| 77 | + dictValue: string | ||
| 78 | + sort?: number | ||
| 79 | + remark?: string | ||
| 80 | +} | ||
| 81 | + | ||
| 82 | +export interface ApiResponse<T> { | ||
| 83 | + code: number | ||
| 84 | + message: string | ||
| 85 | + data: T | ||
| 86 | +} | ||
| 87 | + | ||
| 88 | +export interface PageData<T> { | ||
| 89 | + records: T[] | ||
| 90 | + total: number | ||
| 91 | + current: number | ||
| 92 | + size: number | ||
| 93 | + pages: number | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +// API调用方法 | ||
| 97 | +export const dictItemApi = { | ||
| 98 | + // 获取字典项列表 | ||
| 99 | + getDictItemList: (params: DictItemSearchParams): Promise<ApiResponse<PageData<DictItem>>> => { | ||
| 100 | + return api.get('/list', { params }) | ||
| 101 | + }, | ||
| 102 | + | ||
| 103 | + // 根据字典类型获取字典项列表 | ||
| 104 | + getDictItemsByType: (dictType: string): Promise<ApiResponse<DictItem[]>> => { | ||
| 105 | + return api.get(`/type/${dictType}`) | ||
| 106 | + }, | ||
| 107 | + | ||
| 108 | + // 获取字典项详情 | ||
| 109 | + getDictItemById: (dictItemId: number): Promise<ApiResponse<DictItem>> => { | ||
| 110 | + return api.get(`/${dictItemId}`) | ||
| 111 | + }, | ||
| 112 | + | ||
| 113 | + // 新增字典项 | ||
| 114 | + createDictItem: (dictItemData: DictItemAddReq): Promise<ApiResponse<any>> => { | ||
| 115 | + return api.post('/', dictItemData) | ||
| 116 | + }, | ||
| 117 | + | ||
| 118 | + // 修改字典项 | ||
| 119 | + updateDictItem: (dictItemData: DictItemUpdateReq): Promise<ApiResponse<any>> => { | ||
| 120 | + return api.put('/', dictItemData) | ||
| 121 | + }, | ||
| 122 | + | ||
| 123 | + // 删除字典项 | ||
| 124 | + deleteDictItem: (dictItemIds: number[]): Promise<ApiResponse<any>> => { | ||
| 125 | + const ids = dictItemIds.join(',') | ||
| 126 | + return api.delete(`/${ids}`) | ||
| 127 | + } | ||
| 128 | +} | ||
| 129 | + | ||
| 130 | +export default dictItemApi |
| 1 | -import { request } from '@/utils/request' | 1 | +import axios from 'axios' |
| 2 | -import type { Log, PageRequest, PageResponse } from '@/types' | ||
| 3 | 2 | ||
| 4 | -// 获取日志分页列表 | 3 | +// 创建axios实例 |
| 5 | -export function getLogPageApi(params: PageRequest & { | 4 | +const api = axios.create({ |
| 6 | - title?: string | 5 | + baseURL: 'http://localhost:8083/api/system/log', |
| 6 | + timeout: 10000, | ||
| 7 | + headers: { | ||
| 8 | + 'Content-Type': 'application/json' | ||
| 9 | + } | ||
| 10 | +}) | ||
| 11 | + | ||
| 12 | +// 请求拦截器 | ||
| 13 | +api.interceptors.request.use( | ||
| 14 | + (config) => { | ||
| 15 | + const token = localStorage.getItem('token') | ||
| 16 | + if (token) { | ||
| 17 | + config.headers.Authorization = `Bearer ${token}` | ||
| 18 | + } | ||
| 19 | + return config | ||
| 20 | + }, | ||
| 21 | + (error) => { | ||
| 22 | + return Promise.reject(error) | ||
| 23 | + } | ||
| 24 | +) | ||
| 25 | + | ||
| 26 | +// 响应拦截器 | ||
| 27 | +api.interceptors.response.use( | ||
| 28 | + (response) => { | ||
| 29 | + return response.data | ||
| 30 | + }, | ||
| 31 | + (error) => { | ||
| 32 | + console.error('API请求错误:', error) | ||
| 33 | + if (error.response?.status === 401) { | ||
| 34 | + localStorage.removeItem('token') | ||
| 35 | + localStorage.removeItem('userInfo') | ||
| 36 | + window.location.href = '/login' | ||
| 37 | + } | ||
| 38 | + return Promise.reject(error) | ||
| 39 | + } | ||
| 40 | +) | ||
| 41 | + | ||
| 42 | +// 日志接口类型定义 | ||
| 43 | +export interface Log { | ||
| 44 | + logId: number | ||
| 45 | + logType: string | ||
| 46 | + logTypeText: string | ||
| 47 | + userId: number | ||
| 48 | + username: string | ||
| 49 | + module: string | ||
| 50 | + operation: string | ||
| 51 | + ip: string | ||
| 52 | + logTime: string | ||
| 53 | + status: number | ||
| 54 | + statusText: string | ||
| 55 | + errorMsg: string | ||
| 56 | + requestParam: string | ||
| 57 | + createBy: string | ||
| 58 | + createTime: string | ||
| 59 | + updateBy: string | ||
| 60 | + updateTime: string | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +export interface LogDetail { | ||
| 64 | + logId: number | ||
| 65 | + logType: string | ||
| 66 | + logTypeText: string | ||
| 67 | + userId: number | ||
| 68 | + username: string | ||
| 69 | + module: string | ||
| 70 | + operation: string | ||
| 71 | + ip: string | ||
| 72 | + logTime: string | ||
| 73 | + status: number | ||
| 74 | + statusText: string | ||
| 75 | + errorMsg: string | ||
| 76 | + requestParam: string | ||
| 77 | + createBy: string | ||
| 78 | + createTime: string | ||
| 79 | + updateBy: string | ||
| 80 | + updateTime: string | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +export interface LogSearchParams { | ||
| 84 | + pageNum: number | ||
| 85 | + pageSize: number | ||
| 7 | logType?: string | 86 | logType?: string |
| 87 | + userId?: number | ||
| 88 | + username?: string | ||
| 89 | + module?: string | ||
| 90 | + operation?: string | ||
| 91 | + ip?: string | ||
| 8 | status?: number | 92 | status?: number |
| 9 | startTime?: string | 93 | startTime?: string |
| 10 | endTime?: string | 94 | endTime?: string |
| 11 | -}): Promise<PageResponse<Log>> { | ||
| 12 | - return request.get('/api/sys/log/page', params) | ||
| 13 | } | 95 | } |
| 14 | 96 | ||
| 15 | -// 获取日志详情 | 97 | +export interface ApiResponse<T> { |
| 16 | -export function getLogDetailApi(logId: number): Promise<Log> { | 98 | + code: number |
| 17 | - return request.get(`/api/sys/log/${logId}`) | 99 | + message: string |
| 100 | + data: T | ||
| 18 | } | 101 | } |
| 19 | 102 | ||
| 20 | -// 删除日志 | 103 | +export interface PageData<T> { |
| 21 | -export function deleteLogApi(logId: number): Promise<void> { | 104 | + records: T[] |
| 22 | - return request.delete(`/api/sys/log/${logId}`) | 105 | + total: number |
| 106 | + current: number | ||
| 107 | + size: number | ||
| 108 | + pages: number | ||
| 23 | } | 109 | } |
| 24 | 110 | ||
| 25 | -// 批量删除日志 | 111 | +// API调用方法 |
| 26 | -export function deleteBatchLogApi(logIds: number[]): Promise<void> { | 112 | +export const logApi = { |
| 27 | - return request.delete('/api/sys/log/batch', { logIds }) | 113 | + // 获取日志列表 |
| 28 | -} | 114 | + getLogList: (params: LogSearchParams): Promise<ApiResponse<PageData<Log>>> => { |
| 115 | + return api.get('/page', { params }) | ||
| 116 | + }, | ||
| 29 | 117 | ||
| 30 | -// 清理过期日志 | 118 | + // 获取日志详情 |
| 31 | -export function cleanExpiredLogsApi(days: number): Promise<void> { | 119 | + getLogDetail: (logId: number): Promise<ApiResponse<LogDetail>> => { |
| 32 | - return request.post('/api/sys/log/clean', { days }) | 120 | + return api.get(`/${logId}`) |
| 33 | -} | 121 | + }, |
| 34 | 122 | ||
| 35 | -// 获取日志统计 | 123 | + // 获取用户登录日志 |
| 36 | -export function getLogCountApi(params: { | 124 | + getLoginLogsByUserId: (userId: number, startTime?: string, endTime?: string): Promise<ApiResponse<Log[]>> => { |
| 37 | - logType?: string | 125 | + return api.get(`/login/${userId}`, { |
| 38 | - startTime?: string | 126 | + params: { |
| 39 | - endTime?: string | 127 | + startTime, |
| 40 | -}): Promise<{ total: number; success: number; error: number }> { | 128 | + endTime |
| 41 | - return request.get('/api/sys/log/count', params) | 129 | + } |
| 42 | -} | 130 | + }) |
| 131 | + }, | ||
| 43 | 132 | ||
| 44 | -// 获取用户登录日志 | 133 | + // 清理过期日志 |
| 45 | -export function getUserLoginLogsApi(params: PageRequest & { | 134 | + cleanExpiredLogs: (expireDays: number): Promise<ApiResponse<string>> => { |
| 46 | - userId: number | 135 | + return api.delete('/clean', { |
| 47 | - startTime?: string | 136 | + params: { |
| 48 | - endTime?: string | 137 | + expireDays |
| 49 | -}): Promise<PageResponse<Log>> { | 138 | + } |
| 50 | - return request.get('/api/sys/log/login', params) | 139 | + }) |
| 140 | + }, | ||
| 141 | + | ||
| 142 | + // 统计日志数量 | ||
| 143 | + countLogs: (logType?: string, startTime?: string, endTime?: string): Promise<ApiResponse<number>> => { | ||
| 144 | + return api.get('/count', { | ||
| 145 | + params: { | ||
| 146 | + logType, | ||
| 147 | + startTime, | ||
| 148 | + endTime | ||
| 149 | + } | ||
| 150 | + }) | ||
| 151 | + }, | ||
| 152 | + | ||
| 153 | + // 删除日志 | ||
| 154 | + deleteLog: (logId: number): Promise<ApiResponse<string>> => { | ||
| 155 | + return api.delete(`/${logId}`) | ||
| 156 | + }, | ||
| 157 | + | ||
| 158 | + // 批量删除日志 | ||
| 159 | + deleteLogs: (logIds: number[]): Promise<ApiResponse<string>> => { | ||
| 160 | + return api.delete('/batch', { data: logIds }) | ||
| 161 | + } | ||
| 51 | } | 162 | } |
| 163 | + | ||
| 164 | +export default logApi | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | -import { request } from '@/utils/request' | 1 | +import axios from 'axios' |
| 2 | -import type { Menu, PageRequest, PageResponse } from '@/types' | ||
| 3 | 2 | ||
| 4 | -// 获取菜单列表 | 3 | +// API基础配置 |
| 5 | -export function getMenuListApi(): Promise<Menu[]> { | 4 | +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8083' |
| 6 | - return request.get('/api/sys/menu/list') | ||
| 7 | -} | ||
| 8 | 5 | ||
| 9 | -// 获取菜单树 | 6 | +// 创建axios实例 |
| 10 | -export function getMenuTreeApi(userId?: number): Promise<Menu[]> { | 7 | +const api = axios.create({ |
| 11 | - return request.get('/api/sys/menu/tree', { userId }) | 8 | + baseURL: API_BASE_URL, |
| 12 | -} | 9 | + timeout: 10000, |
| 10 | + headers: { | ||
| 11 | + 'Content-Type': 'application/json' | ||
| 12 | + } | ||
| 13 | +}) | ||
| 13 | 14 | ||
| 14 | -// 获取菜单分页列表 | 15 | +console.log('菜单API实例创建,baseURL:', API_BASE_URL) |
| 15 | -export function getMenuPageApi(params: PageRequest & { | 16 | + |
| 16 | - menuName?: string | 17 | +// 请求拦截器 - 添加token |
| 18 | +api.interceptors.request.use( | ||
| 19 | + (config) => { | ||
| 20 | + console.log('菜单API请求:', { | ||
| 21 | + url: config.url, | ||
| 22 | + baseURL: config.baseURL, | ||
| 23 | + method: config.method, | ||
| 24 | + params: config.params, | ||
| 25 | + fullURL: `${config.baseURL}${config.url}` | ||
| 26 | + }) | ||
| 27 | + | ||
| 28 | + const token = localStorage.getItem('token') | ||
| 29 | + if (token) { | ||
| 30 | + config.headers.Authorization = `Bearer ${token}` | ||
| 31 | + } | ||
| 32 | + return config | ||
| 33 | + }, | ||
| 34 | + (error) => { | ||
| 35 | + return Promise.reject(error) | ||
| 36 | + } | ||
| 37 | +) | ||
| 38 | + | ||
| 39 | +// 响应拦截器 | ||
| 40 | +api.interceptors.response.use( | ||
| 41 | + (response) => { | ||
| 42 | + return response.data | ||
| 43 | + }, | ||
| 44 | + (error) => { | ||
| 45 | + console.error('API请求错误:', error) | ||
| 46 | + return Promise.reject(error) | ||
| 47 | + } | ||
| 48 | +) | ||
| 49 | + | ||
| 50 | +// 菜单接口类型定义 | ||
| 51 | +export interface Menu { | ||
| 52 | + menuId: number | ||
| 53 | + parentId?: number | ||
| 54 | + menuName: string | ||
| 55 | + menuType: string | ||
| 56 | + menuTypeText: string | ||
| 17 | path?: string | 57 | path?: string |
| 18 | - menuType?: string | 58 | + icon?: string |
| 59 | + sort: number | ||
| 60 | + status: number | ||
| 61 | + statusText: string | ||
| 19 | perms?: string | 62 | perms?: string |
| 20 | -}): Promise<PageResponse<Menu>> { | 63 | + component?: string |
| 21 | - return request.get('/api/sys/menu/page', params) | 64 | + isFrame?: number |
| 65 | + remark?: string | ||
| 66 | + level?: number | ||
| 67 | + createTime: string | ||
| 68 | + updateTime: string | ||
| 69 | + children?: Menu[] | ||
| 22 | } | 70 | } |
| 23 | 71 | ||
| 24 | -// 获取菜单详情 | 72 | +export interface MenuSearchParams { |
| 25 | -export function getMenuDetailApi(menuId: number): Promise<Menu> { | 73 | + menuName?: string |
| 26 | - return request.get(`/api/sys/menu/${menuId}`) | 74 | + menuType?: string |
| 75 | + status?: number | ||
| 76 | + pageNum?: number | ||
| 77 | + pageSize?: number | ||
| 27 | } | 78 | } |
| 28 | 79 | ||
| 29 | -// 新增菜单 | 80 | +export interface ApiResponse<T = any> { |
| 30 | -export function addMenuApi(data: Partial<Menu>): Promise<void> { | 81 | + code: number |
| 31 | - return request.post('/api/sys/menu', data) | 82 | + message: string |
| 83 | + data: T | ||
| 32 | } | 84 | } |
| 33 | 85 | ||
| 34 | -// 更新菜单 | 86 | +export interface PageResponse<T> { |
| 35 | -export function updateMenuApi(data: Partial<Menu>): Promise<void> { | 87 | + records: T[] |
| 36 | - return request.put('/api/sys/menu', data) | 88 | + total: number |
| 89 | + size: number | ||
| 90 | + current: number | ||
| 91 | + orders: any[] | ||
| 92 | + optimizeCountSql: boolean | ||
| 93 | + searchCount: boolean | ||
| 94 | + maxLimit: any | ||
| 95 | + countId: any | ||
| 96 | + pages: number | ||
| 37 | } | 97 | } |
| 38 | 98 | ||
| 39 | -// 删除菜单 | 99 | +// 菜单API接口 |
| 40 | -export function deleteMenuApi(menuId: number): Promise<void> { | 100 | +export const menuApi = { |
| 41 | - return request.delete(`/api/sys/menu/${menuId}`) | 101 | + // 获取菜单列表 |
| 42 | -} | 102 | + getMenuList: (params: MenuSearchParams): Promise<ApiResponse<PageResponse<Menu>>> => { |
| 103 | + return api.get('/api/system/menu/list', { params }) | ||
| 104 | + }, | ||
| 43 | 105 | ||
| 44 | -// 批量删除菜单 | 106 | + // 获取菜单树列表 |
| 45 | -export function deleteBatchMenuApi(menuIds: number[]): Promise<void> { | 107 | + getMenuTree: (userId?: number): Promise<ApiResponse<Menu[]>> => { |
| 46 | - return request.delete('/api/sys/menu/batch', { menuIds }) | 108 | + return api.get('/api/system/menu/tree', { params: { userId } }) |
| 47 | -} | 109 | + }, |
| 48 | 110 | ||
| 49 | -// 获取角色菜单树 | 111 | + // 获取菜单详情 |
| 50 | -export function getRoleMenuTreeApi(roleId: number): Promise<Menu[]> { | 112 | + getMenuById: (menuId: number): Promise<ApiResponse<Menu>> => { |
| 51 | - return request.get(`/api/sys/menu/roleMenuTree/${roleId}`) | 113 | + return api.get(`/api/system/menu/${menuId}`) |
| 52 | -} | 114 | + }, |
| 115 | + | ||
| 116 | + // 新增菜单 | ||
| 117 | + createMenu: (menuData: any): Promise<ApiResponse<any>> => { | ||
| 118 | + return api.post('/api/system/menu', menuData) | ||
| 119 | + }, | ||
| 53 | 120 | ||
| 54 | -// 获取菜单选择树 | 121 | + // 修改菜单 |
| 55 | -export function getMenuSelectTreeApi(): Promise<Menu[]> { | 122 | + updateMenu: (menuData: any): Promise<ApiResponse<any>> => { |
| 56 | - return request.get('/api/sys/menu/treeselect') | 123 | + return api.put('/api/system/menu', menuData) |
| 124 | + }, | ||
| 125 | + | ||
| 126 | + // 删除菜单 | ||
| 127 | + deleteMenu: (menuIds: number[]): Promise<ApiResponse<any>> => { | ||
| 128 | + return api.delete(`/api/system/menu/${menuIds.join(',')}`) | ||
| 129 | + }, | ||
| 130 | + | ||
| 131 | + // 获取菜单下拉树选择 | ||
| 132 | + getMenuTreeSelect: (): Promise<ApiResponse<Menu[]>> => { | ||
| 133 | + return api.get('/api/system/menu/treeselect') | ||
| 134 | + }, | ||
| 135 | + | ||
| 136 | + // 根据角色ID获取菜单下拉树选择 | ||
| 137 | + getMenuTreeSelectByRole: (roleId: number): Promise<ApiResponse<Menu[]>> => { | ||
| 138 | + return api.get(`/api/system/menu/roleMenuTreeselect/${roleId}`) | ||
| 139 | + } | ||
| 57 | } | 140 | } |
| 141 | + | ||
| 142 | +export default menuApi | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | -import { request } from '@/utils/request' | 1 | +import axios from 'axios' |
| 2 | -import type { Role, PageRequest, PageResponse } from '@/types' | ||
| 3 | 2 | ||
| 4 | -// 获取角色列表 | 3 | +// API基础配置 |
| 5 | -export function getRoleListApi(): Promise<Role[]> { | 4 | +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8083' |
| 6 | - return request.get('/api/sys/role/list') | 5 | + |
| 6 | +// 创建axios实例 | ||
| 7 | +const api = axios.create({ | ||
| 8 | + baseURL: API_BASE_URL, | ||
| 9 | + timeout: 10000, | ||
| 10 | + headers: { | ||
| 11 | + 'Content-Type': 'application/json' | ||
| 12 | + } | ||
| 13 | +}) | ||
| 14 | + | ||
| 15 | +console.log('角色API实例创建,baseURL:', API_BASE_URL) | ||
| 16 | + | ||
| 17 | +// 请求拦截器 - 添加token | ||
| 18 | +api.interceptors.request.use( | ||
| 19 | + (config) => { | ||
| 20 | + console.log('角色API请求:', { | ||
| 21 | + url: config.url, | ||
| 22 | + baseURL: config.baseURL, | ||
| 23 | + method: config.method, | ||
| 24 | + params: config.params, | ||
| 25 | + fullURL: `${config.baseURL}${config.url}` | ||
| 26 | + }) | ||
| 27 | + | ||
| 28 | + const token = localStorage.getItem('token') | ||
| 29 | + if (token) { | ||
| 30 | + config.headers.Authorization = `Bearer ${token}` | ||
| 31 | + } | ||
| 32 | + return config | ||
| 33 | + }, | ||
| 34 | + (error) => { | ||
| 35 | + return Promise.reject(error) | ||
| 36 | + } | ||
| 37 | +) | ||
| 38 | + | ||
| 39 | +// 响应拦截器 | ||
| 40 | +api.interceptors.response.use( | ||
| 41 | + (response) => { | ||
| 42 | + return response.data | ||
| 43 | + }, | ||
| 44 | + (error) => { | ||
| 45 | + console.error('API请求错误:', error) | ||
| 46 | + return Promise.reject(error) | ||
| 47 | + } | ||
| 48 | +) | ||
| 49 | + | ||
| 50 | +// 菜单接口类型定义 | ||
| 51 | +export interface Menu { | ||
| 52 | + menuId: number | ||
| 53 | + parentId?: number | ||
| 54 | + menuName: string | ||
| 55 | + menuType: string | ||
| 56 | + menuTypeText: string | ||
| 57 | + path?: string | ||
| 58 | + icon?: string | ||
| 59 | + sort: number | ||
| 60 | + status: number | ||
| 61 | + statusText: string | ||
| 62 | + perms?: string | ||
| 63 | + createTime: string | ||
| 64 | + updateTime: string | ||
| 65 | + children?: Menu[] | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +// 角色接口类型定义 | ||
| 69 | +export interface Role { | ||
| 70 | + roleId: number | ||
| 71 | + roleCode: string | ||
| 72 | + roleName: string | ||
| 73 | + status: number | ||
| 74 | + statusText: string | ||
| 75 | + remark: string | ||
| 76 | + createTime: string | ||
| 77 | + updateTime: string | ||
| 78 | + menuIds?: number[] | ||
| 79 | + menus?: Menu[] | ||
| 7 | } | 80 | } |
| 8 | 81 | ||
| 9 | -// 获取角色分页列表 | 82 | +export interface RoleSearchParams { |
| 10 | -export function getRolePageApi(params: PageRequest & { | 83 | + roleCode?: string |
| 11 | roleName?: string | 84 | roleName?: string |
| 12 | - roleKey?: string | ||
| 13 | status?: number | 85 | status?: number |
| 14 | -}): Promise<PageResponse<Role>> { | 86 | + pageNum?: number |
| 15 | - return request.get('/api/sys/role/page', params) | 87 | + pageSize?: number |
| 16 | } | 88 | } |
| 17 | 89 | ||
| 18 | -// 获取角色详情 | 90 | +export interface ApiResponse<T = any> { |
| 19 | -export function getRoleDetailApi(roleId: number): Promise<Role> { | 91 | + code: number |
| 20 | - return request.get(`/api/sys/role/${roleId}`) | 92 | + message: string |
| 93 | + data: T | ||
| 21 | } | 94 | } |
| 22 | 95 | ||
| 23 | -// 新增角色 | 96 | +export interface PageResponse<T> { |
| 24 | -export function addRoleApi(data: Partial<Role>): Promise<void> { | 97 | + records: T[] |
| 25 | - return request.post('/api/sys/role', data) | 98 | + total: number |
| 99 | + size: number | ||
| 100 | + current: number | ||
| 101 | + orders: any[] | ||
| 102 | + optimizeCountSql: boolean | ||
| 103 | + searchCount: boolean | ||
| 104 | + maxLimit: any | ||
| 105 | + countId: any | ||
| 106 | + pages: number | ||
| 26 | } | 107 | } |
| 27 | 108 | ||
| 28 | -// 更新角色 | 109 | +// 角色API接口 |
| 29 | -export function updateRoleApi(data: Partial<Role>): Promise<void> { | 110 | +export const roleApi = { |
| 30 | - return request.put('/api/sys/role', data) | 111 | + // 获取角色列表 |
| 31 | -} | 112 | + getRoleList: (params?: RoleSearchParams): Promise<ApiResponse<PageResponse<Role>>> => { |
| 113 | + return api.get('/api/system/role/list', { params }) | ||
| 114 | + }, | ||
| 32 | 115 | ||
| 33 | -// 删除角色 | 116 | + // 获取所有角色(不分页,用于下拉选择) |
| 34 | -export function deleteRoleApi(roleId: number): Promise<void> { | 117 | + getAllRoles: (): Promise<ApiResponse<Role[]>> => { |
| 35 | - return request.delete(`/api/sys/role/${roleId}`) | 118 | + return api.get('/api/system/role/list', { |
| 36 | -} | 119 | + params: { status: 1, pageSize: 9999 } |
| 120 | + }) | ||
| 121 | + }, | ||
| 37 | 122 | ||
| 38 | -// 批量删除角色 | 123 | + // 根据ID获取角色详情 |
| 39 | -export function deleteBatchRoleApi(roleIds: number[]): Promise<void> { | 124 | + getRoleById: (roleId: number): Promise<ApiResponse<Role>> => { |
| 40 | - return request.delete('/api/sys/role/batch', { roleIds }) | 125 | + return api.get(`/api/system/role/${roleId}`) |
| 41 | -} | 126 | + }, |
| 42 | 127 | ||
| 43 | -// 修改角色状态 | 128 | + // 创建角色 |
| 44 | -export function changeRoleStatusApi(data: { | 129 | + createRole: (roleData: any): Promise<ApiResponse<any>> => { |
| 45 | - roleId: number | 130 | + return api.post('/api/system/role/add', roleData) |
| 46 | - status: number | 131 | + }, |
| 47 | -}): Promise<void> { | ||
| 48 | - return request.post('/api/sys/role/changeStatus', data) | ||
| 49 | -} | ||
| 50 | 132 | ||
| 51 | -// 分配权限 | 133 | + // 更新角色 |
| 52 | -export function assignPermissionsApi(data: { | 134 | + updateRole: (roleData: any): Promise<ApiResponse<any>> => { |
| 53 | - roleId: number | 135 | + return api.post('/api/system/role/edit', roleData) |
| 54 | - menuIds: number[] | 136 | + }, |
| 55 | -}): Promise<void> { | 137 | + |
| 56 | - return request.post('/api/sys/role/assignPermissions', data) | 138 | + // 删除角色(单个或批量) |
| 139 | + deleteRole: (roleIds: number[]): Promise<ApiResponse<any>> => { | ||
| 140 | + return api.delete(`/api/system/role/${roleIds.join(',')}`) | ||
| 141 | + }, | ||
| 142 | + | ||
| 143 | + // 更新角色状态 | ||
| 144 | + updateRoleStatus: (roleId: number, status: number): Promise<ApiResponse<any>> => { | ||
| 145 | + return api.post('/api/system/role/changeStatus', { | ||
| 146 | + roleId: roleId, | ||
| 147 | + status: status | ||
| 148 | + }) | ||
| 149 | + }, | ||
| 150 | + | ||
| 151 | + // 获取菜单树列表 | ||
| 152 | + getMenuTree: (): Promise<ApiResponse<Menu[]>> => { | ||
| 153 | + return api.get('/api/system/menu/tree') | ||
| 154 | + }, | ||
| 155 | + | ||
| 156 | + // 测试角色列表接口参数格式 | ||
| 157 | + testRoleListParams: (): RoleSearchParams => { | ||
| 158 | + return { | ||
| 159 | + roleCode: "", | ||
| 160 | + roleName: "", | ||
| 161 | + status: 1, // 默认获取有效角色 | ||
| 162 | + pageNum: 1, | ||
| 163 | + pageSize: 9999 // 获取全部有效角色 | ||
| 164 | + } | ||
| 165 | + } | ||
| 57 | } | 166 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | -import { request } from '@/utils/request' | 1 | +import axios from 'axios' |
| 2 | -import type { User, PageRequest, PageResponse } from '@/types' | ||
| 3 | 2 | ||
| 4 | -// 获取用户列表 | 3 | +// API基础配置 |
| 5 | -export function getUserListApi(): Promise<User[]> { | 4 | +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8083' |
| 6 | - return request.get('/api/sys/user/list') | 5 | + |
| 6 | +// 创建axios实例 | ||
| 7 | +const api = axios.create({ | ||
| 8 | + baseURL: API_BASE_URL, | ||
| 9 | + timeout: 10000, | ||
| 10 | + headers: { | ||
| 11 | + 'Content-Type': 'application/json' | ||
| 12 | + } | ||
| 13 | +}) | ||
| 14 | + | ||
| 15 | +// 请求拦截器 - 添加token | ||
| 16 | +api.interceptors.request.use( | ||
| 17 | + (config) => { | ||
| 18 | + const token = localStorage.getItem('token') | ||
| 19 | + if (token) { | ||
| 20 | + config.headers.Authorization = `Bearer ${token}` | ||
| 21 | + } | ||
| 22 | + return config | ||
| 23 | + }, | ||
| 24 | + (error) => { | ||
| 25 | + return Promise.reject(error) | ||
| 26 | + } | ||
| 27 | +) | ||
| 28 | + | ||
| 29 | +// 响应拦截器 - 处理错误 | ||
| 30 | +api.interceptors.response.use( | ||
| 31 | + (response) => { | ||
| 32 | + return response | ||
| 33 | + }, | ||
| 34 | + (error) => { | ||
| 35 | + if (error.response?.status === 401) { | ||
| 36 | + // token过期,跳转到登录页 | ||
| 37 | + localStorage.removeItem('token') | ||
| 38 | + localStorage.removeItem('userInfo') | ||
| 39 | + window.location.href = '/' | ||
| 40 | + } | ||
| 41 | + return Promise.reject(error) | ||
| 42 | + } | ||
| 43 | +) | ||
| 44 | + | ||
| 45 | +// 用户数据类型 | ||
| 46 | +export interface User { | ||
| 47 | + userId: number | ||
| 48 | + username: string | ||
| 49 | + realName: string | ||
| 50 | + phone: string | ||
| 51 | + email: string | ||
| 52 | + status: number | ||
| 53 | + statusText: string | ||
| 54 | + remark: string | ||
| 55 | + createBy: string | ||
| 56 | + createTime: string | ||
| 57 | + updateBy: string | ||
| 58 | + updateTime: string | ||
| 59 | + lastLoginTime: string | null | ||
| 60 | + lastLoginIp: string | null | ||
| 61 | + roles: Role[] | ||
| 7 | } | 62 | } |
| 8 | 63 | ||
| 9 | -// 获取用户分页列表 | 64 | +// 角色类型 |
| 10 | -export function getUserPageApi(params: PageRequest & { | 65 | +export interface Role { |
| 66 | + roleId: number | ||
| 67 | + roleCode: string | ||
| 68 | + roleName: string | ||
| 69 | + status: number | ||
| 70 | + statusText: string | ||
| 71 | + remark: string | ||
| 72 | + createBy: string | ||
| 73 | + createTime: string | ||
| 74 | + updateBy: string | ||
| 75 | + updateTime: string | ||
| 76 | + menus: any | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +// 搜索参数类型 | ||
| 80 | +export interface UserSearchParams { | ||
| 11 | username?: string | 81 | username?: string |
| 12 | - realName?: string | ||
| 13 | phone?: string | 82 | phone?: string |
| 14 | - email?: string | ||
| 15 | status?: number | 83 | status?: number |
| 16 | -}): Promise<PageResponse<User>> { | 84 | + startTime?: string |
| 17 | - return request.get('/api/sys/user/page', params) | 85 | + endTime?: string |
| 86 | + page?: number | ||
| 87 | + pageSize?: number | ||
| 18 | } | 88 | } |
| 19 | 89 | ||
| 20 | -// 获取用户详情 | 90 | +// 用户表单类型 |
| 21 | -export function getUserDetailApi(userId: number): Promise<User> { | 91 | +export interface UserForm { |
| 22 | - return request.get(`/api/sys/user/${userId}`) | 92 | + userId?: number |
| 93 | + username: string | ||
| 94 | + realName: string | ||
| 95 | + phone: string | ||
| 96 | + email: string | ||
| 97 | + status: number | ||
| 98 | + remark: string | ||
| 99 | + roleIds: number[] | ||
| 100 | + password?: string | ||
| 23 | } | 101 | } |
| 24 | 102 | ||
| 25 | -// 新增用户 | 103 | +// API响应类型 |
| 26 | -export function addUserApi(data: Partial<User>): Promise<void> { | 104 | +export interface ApiResponse<T> { |
| 27 | - return request.post('/api/sys/user', data) | 105 | + code: number |
| 106 | + message: string | ||
| 107 | + data: T | ||
| 28 | } | 108 | } |
| 29 | 109 | ||
| 30 | -// 更新用户 | 110 | +// 分页响应类型 |
| 31 | -export function updateUserApi(data: Partial<User>): Promise<void> { | 111 | +export interface PageResponse<T> { |
| 32 | - return request.put('/api/sys/user', data) | 112 | + records: T[] |
| 113 | + total: number | ||
| 114 | + size: number | ||
| 115 | + current: number | ||
| 116 | + orders: any[] | ||
| 117 | + optimizeCountSql: boolean | ||
| 118 | + searchCount: boolean | ||
| 119 | + maxLimit: any | ||
| 120 | + countId: any | ||
| 121 | + pages: number | ||
| 33 | } | 122 | } |
| 34 | 123 | ||
| 35 | -// 删除用户 | 124 | +// 用户API接口 |
| 36 | -export function deleteUserApi(userId: number): Promise<void> { | 125 | +export const userApi = { |
| 37 | - return request.delete(`/api/sys/user/${userId}`) | 126 | + // 获取用户列表 |
| 38 | -} | 127 | + getUserList: async (params: UserSearchParams = {}) => { |
| 128 | + try { | ||
| 129 | + const response = await api.get<ApiResponse<PageResponse<User>>>('/api/system/user/list', { params }) | ||
| 130 | + return response.data | ||
| 131 | + } catch (error) { | ||
| 132 | + console.error('获取用户列表失败:', error) | ||
| 133 | + throw error | ||
| 134 | + } | ||
| 135 | + }, | ||
| 39 | 136 | ||
| 40 | -// 批量删除用户 | 137 | + // 获取用户详情 |
| 41 | -export function deleteBatchUserApi(userIds: number[]): Promise<void> { | 138 | + getUserById: async (id: number) => { |
| 42 | - return request.delete('/api/sys/user/batch', { userIds }) | 139 | + try { |
| 43 | -} | 140 | + const response = await api.get<ApiResponse<User>>(`/api/system/user/${id}`) |
| 141 | + return response.data | ||
| 142 | + } catch (error) { | ||
| 143 | + console.error('获取用户详情失败:', error) | ||
| 144 | + throw error | ||
| 145 | + } | ||
| 146 | + }, | ||
| 44 | 147 | ||
| 45 | -// 重置密码 | 148 | + // 创建用户 |
| 46 | -export function resetPasswordApi(data: { | 149 | + createUser: async (userData: UserForm) => { |
| 47 | - userId: number | 150 | + try { |
| 48 | - newPassword: string | 151 | + const response = await api.post<ApiResponse<User>>('/api/system/user/add', userData) |
| 49 | -}): Promise<void> { | 152 | + return response.data |
| 50 | - return request.post('/api/sys/user/resetPassword', data) | 153 | + } catch (error) { |
| 51 | -} | 154 | + console.error('创建用户失败:', error) |
| 155 | + throw error | ||
| 156 | + } | ||
| 157 | + }, | ||
| 52 | 158 | ||
| 53 | -// 修改用户状态 | 159 | + // 更新用户 |
| 54 | -export function changeUserStatusApi(data: { | 160 | + updateUser: async (id: number, userData: UserForm) => { |
| 55 | - userId: number | 161 | + try { |
| 56 | - status: number | 162 | + const response = await api.post<ApiResponse<User>>('/api/system/user/edit', userData) |
| 57 | -}): Promise<void> { | 163 | + return response.data |
| 58 | - return request.post('/api/sys/user/changeStatus', data) | 164 | + } catch (error) { |
| 59 | -} | 165 | + console.error('更新用户失败:', error) |
| 166 | + throw error | ||
| 167 | + } | ||
| 168 | + }, | ||
| 169 | + | ||
| 170 | + // 删除用户(单个或批量) | ||
| 171 | + deleteUser: async (id: number) => { | ||
| 172 | + try { | ||
| 173 | + const response = await api.delete<ApiResponse<void>>(`/api/system/user/${id}`) | ||
| 174 | + return response.data | ||
| 175 | + } catch (error) { | ||
| 176 | + console.error('删除用户失败:', error) | ||
| 177 | + throw error | ||
| 178 | + } | ||
| 179 | + }, | ||
| 180 | + | ||
| 181 | + // 批量删除用户 | ||
| 182 | + batchDeleteUsers: async (ids: number[]) => { | ||
| 183 | + try { | ||
| 184 | + const response = await api.delete<ApiResponse<void>>(`/api/system/user/${ids.join(',')}`) | ||
| 185 | + return response.data | ||
| 186 | + } catch (error) { | ||
| 187 | + console.error('批量删除用户失败:', error) | ||
| 188 | + throw error | ||
| 189 | + } | ||
| 190 | + }, | ||
| 191 | + | ||
| 192 | + // 更新用户状态 | ||
| 193 | + updateUserStatus: async (id: number, status: number) => { | ||
| 194 | + try { | ||
| 195 | + const response = await api.post<ApiResponse<void>>('/api/system/user/changeStatus', { | ||
| 196 | + userId: id, | ||
| 197 | + status: status | ||
| 198 | + }) | ||
| 199 | + return response.data | ||
| 200 | + } catch (error) { | ||
| 201 | + console.error('更新用户状态失败:', error) | ||
| 202 | + throw error | ||
| 203 | + } | ||
| 204 | + }, | ||
| 60 | 205 | ||
| 61 | -// 分配角色 | ||
| 62 | -export function assignRolesApi(data: { | ||
| 63 | - userId: number | ||
| 64 | - roleIds: number[] | ||
| 65 | -}): Promise<void> { | ||
| 66 | - return request.post('/api/sys/user/assignRoles', data) | ||
| 67 | } | 206 | } |
| 207 | + | ||
| 208 | +export default userApi | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
frontend/src/layouts/MainLayout.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="main-layout"> | ||
| 3 | + <!-- 左侧菜单栏 --> | ||
| 4 | + <aside class="sidebar" :class="{ collapsed: sidebarCollapsed }"> | ||
| 5 | + <div class="sidebar-header"> | ||
| 6 | + <h2 class="logo">Apple ERP</h2> | ||
| 7 | + <button | ||
| 8 | + class="collapse-btn" | ||
| 9 | + @click="toggleSidebar" | ||
| 10 | + :title="sidebarCollapsed ? '展开菜单' : '收起菜单'" | ||
| 11 | + > | ||
| 12 | + {{ sidebarCollapsed ? '→' : '←' }} | ||
| 13 | + </button> | ||
| 14 | + </div> | ||
| 15 | + | ||
| 16 | + <nav class="sidebar-nav"> | ||
| 17 | + <ul class="nav-list"> | ||
| 18 | + <li v-for="item in menuItems" :key="item.path || item.name" class="nav-item"> | ||
| 19 | + <!-- 一级菜单 --> | ||
| 20 | + <div v-if="!item.children" class="nav-item-single"> | ||
| 21 | + <a | ||
| 22 | + @click="handleMenuClick(item.path)" | ||
| 23 | + class="nav-link" | ||
| 24 | + :class="{ active: $route.path === item.path }" | ||
| 25 | + > | ||
| 26 | + <span class="nav-icon">{{ item.icon }}</span> | ||
| 27 | + <span class="nav-text" v-show="!sidebarCollapsed">{{ item.name }}</span> | ||
| 28 | + </a> | ||
| 29 | + </div> | ||
| 30 | + | ||
| 31 | + <!-- 有子菜单的一级菜单 --> | ||
| 32 | + <div v-else class="nav-item-group"> | ||
| 33 | + <div | ||
| 34 | + class="nav-link nav-link-parent" | ||
| 35 | + :class="{ active: isParentActive(item) }" | ||
| 36 | + @click="toggleSubmenu(item)" | ||
| 37 | + > | ||
| 38 | + <span class="nav-icon">{{ item.icon }}</span> | ||
| 39 | + <span class="nav-text" v-show="!sidebarCollapsed">{{ item.name }}</span> | ||
| 40 | + <span class="nav-arrow" v-show="!sidebarCollapsed">{{ item.expanded ? '▼' : '▶' }}</span> | ||
| 41 | + </div> | ||
| 42 | + | ||
| 43 | + <!-- 二级菜单 --> | ||
| 44 | + <ul v-if="item.expanded && !sidebarCollapsed" class="submenu"> | ||
| 45 | + <li v-for="child in item.children" :key="child.path" class="submenu-item"> | ||
| 46 | + <a | ||
| 47 | + @click="child.path ? handleMenuClick(child.path) : null" | ||
| 48 | + class="nav-link nav-link-child" | ||
| 49 | + :class="{ active: $route.path === child.path }" | ||
| 50 | + > | ||
| 51 | + <span class="nav-icon">{{ child.icon }}</span> | ||
| 52 | + <span class="nav-text">{{ child.name }}</span> | ||
| 53 | + </a> | ||
| 54 | + </li> | ||
| 55 | + </ul> | ||
| 56 | + </div> | ||
| 57 | + </li> | ||
| 58 | + </ul> | ||
| 59 | + </nav> | ||
| 60 | + </aside> | ||
| 61 | + | ||
| 62 | + <!-- 主内容区域 --> | ||
| 63 | + <div class="main-content"> | ||
| 64 | + <!-- 顶部导航栏 --> | ||
| 65 | + <header class="top-header"> | ||
| 66 | + <div class="header-left"> | ||
| 67 | + <button class="menu-toggle" @click="toggleSidebar"> | ||
| 68 | + ☰ | ||
| 69 | + </button> | ||
| 70 | + <h1 class="page-title">{{ currentPageTitle }}</h1> | ||
| 71 | + </div> | ||
| 72 | + | ||
| 73 | + <div class="header-right"> | ||
| 74 | + <div class="user-info"> | ||
| 75 | + <span class="welcome-text">欢迎,{{ userInfo?.username || '用户' }}</span> | ||
| 76 | + <div class="user-actions"> | ||
| 77 | + <button class="logout-btn" @click="handleLogout">退出登录</button> | ||
| 78 | + </div> | ||
| 79 | + </div> | ||
| 80 | + </div> | ||
| 81 | + </header> | ||
| 82 | + | ||
| 83 | + <!-- 标签页导航栏 --> | ||
| 84 | + <div class="tab-navigation"> | ||
| 85 | + <div class="tab-scroll-left" @click="scrollTabsLeft"> | ||
| 86 | + <span class="scroll-arrow">‹‹</span> | ||
| 87 | + </div> | ||
| 88 | + <div class="tab-container" ref="tabContainer"> | ||
| 89 | + <div | ||
| 90 | + v-for="tab in openTabs" | ||
| 91 | + :key="tab.id" | ||
| 92 | + class="tab-item" | ||
| 93 | + :class="{ active: tab.active }" | ||
| 94 | + @click="switchToTab(tab)" | ||
| 95 | + > | ||
| 96 | + <span class="tab-name">{{ tab.name }}</span> | ||
| 97 | + <span | ||
| 98 | + v-if="tab.closable" | ||
| 99 | + @click.stop="closeTab(tab)" | ||
| 100 | + class="tab-close" | ||
| 101 | + >×</span> | ||
| 102 | + </div> | ||
| 103 | + </div> | ||
| 104 | + <div class="tab-scroll-right" @click="scrollTabsRight"> | ||
| 105 | + <span class="scroll-arrow">››</span> | ||
| 106 | + </div> | ||
| 107 | + </div> | ||
| 108 | + | ||
| 109 | + <!-- 页面内容 --> | ||
| 110 | + <main class="page-content"> | ||
| 111 | + <router-view /> | ||
| 112 | + </main> | ||
| 113 | + | ||
| 114 | + <!-- 底部栏 --> | ||
| 115 | + <footer class="main-footer"> | ||
| 116 | + <div class="footer-content"> | ||
| 117 | + <p class="copyright">© 2025 Erry Copyright</p> | ||
| 118 | + </div> | ||
| 119 | + </footer> | ||
| 120 | + </div> | ||
| 121 | + </div> | ||
| 122 | +</template> | ||
| 123 | + | ||
| 124 | +<script setup lang="ts"> | ||
| 125 | +import { ref, computed, onMounted, watch, nextTick } from 'vue' | ||
| 126 | +import { useRouter, useRoute } from 'vue-router' | ||
| 127 | + | ||
| 128 | +const router = useRouter() | ||
| 129 | +const route = useRoute() | ||
| 130 | + | ||
| 131 | +// 侧边栏状态 | ||
| 132 | +const sidebarCollapsed = ref(false) | ||
| 133 | + | ||
| 134 | +// 用户信息 | ||
| 135 | +const userInfo = ref<any>(null) | ||
| 136 | + | ||
| 137 | +// 标签页容器引用 | ||
| 138 | +const tabContainer = ref<HTMLElement>() | ||
| 139 | + | ||
| 140 | +// 菜单项配置 | ||
| 141 | +const menuItems = ref([ | ||
| 142 | + { name: '首页', path: '/main/dashboard', icon: '🏠' }, | ||
| 143 | + { | ||
| 144 | + name: '系统设置', | ||
| 145 | + icon: '⚙️', | ||
| 146 | + expanded: false, | ||
| 147 | + children: [ | ||
| 148 | + { name: '用户管理', path: '/main/users', icon: '👤' }, | ||
| 149 | + { name: '角色管理', path: '/main/sys/role', icon: '🛡️' }, | ||
| 150 | + { name: '菜单管理', path: '/main/sys/menu', icon: '📝' }, | ||
| 151 | + { | ||
| 152 | + name: '字典管理', | ||
| 153 | + path: '/main/sys/dict', | ||
| 154 | + icon: '📖', | ||
| 155 | + expanded: false, | ||
| 156 | + children: [ | ||
| 157 | + { name: '字典类型', path: '/main/sys/dict', icon: '📋' }, | ||
| 158 | + { name: '字典项', path: '/main/sys/dict-item', icon: '📝' } | ||
| 159 | + ] | ||
| 160 | + }, | ||
| 161 | + { name: '日志管理', path: '/main/sys/log', icon: '📊' } | ||
| 162 | + ] | ||
| 163 | + } | ||
| 164 | +]) | ||
| 165 | + | ||
| 166 | +// 打开的标签页 | ||
| 167 | +const openTabs = ref([ | ||
| 168 | + { id: 1, name: '首页', path: '/main/dashboard', active: true, closable: false } | ||
| 169 | +]) | ||
| 170 | + | ||
| 171 | +// 当前页面标题 | ||
| 172 | +const currentPageTitle = computed(() => { | ||
| 173 | + const activeTab = openTabs.value.find(tab => tab.active) | ||
| 174 | + return activeTab ? activeTab.name : 'Apple经销商ERP系统' | ||
| 175 | +}) | ||
| 176 | + | ||
| 177 | +// 处理菜单点击 | ||
| 178 | +const handleMenuClick = (path: string) => { | ||
| 179 | + if (path && path !== route.path) { | ||
| 180 | + console.log('菜单导航:', path) | ||
| 181 | + | ||
| 182 | + // 检查是否已经存在该标签页 | ||
| 183 | + const existingTab = openTabs.value.find(tab => tab.path === path) | ||
| 184 | + if (existingTab) { | ||
| 185 | + // 切换到已存在的标签页 | ||
| 186 | + switchToTab(existingTab) | ||
| 187 | + } else { | ||
| 188 | + // 创建新标签页 | ||
| 189 | + const menuItem = findMenuItemByPath(path) | ||
| 190 | + if (menuItem) { | ||
| 191 | + addNewTab(menuItem.name, path) | ||
| 192 | + } | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + router.push(path).catch(err => { | ||
| 196 | + console.error('路由跳转失败:', err) | ||
| 197 | + }) | ||
| 198 | + } | ||
| 199 | +} | ||
| 200 | + | ||
| 201 | +// 根据路径查找菜单项 | ||
| 202 | +const findMenuItemByPath = (path: string) => { | ||
| 203 | + for (const item of menuItems.value) { | ||
| 204 | + if (item.path === path) { | ||
| 205 | + return item | ||
| 206 | + } | ||
| 207 | + if (item.children && Array.isArray(item.children)) { | ||
| 208 | + const child = item.children.find((child: any) => child.path === path) | ||
| 209 | + if (child) return child | ||
| 210 | + } | ||
| 211 | + } | ||
| 212 | + return null | ||
| 213 | +} | ||
| 214 | + | ||
| 215 | +// 添加新标签页 | ||
| 216 | +const addNewTab = (name: string, path: string) => { | ||
| 217 | + // 先取消所有标签页的激活状态 | ||
| 218 | + openTabs.value.forEach(tab => tab.active = false) | ||
| 219 | + | ||
| 220 | + // 添加新标签页 | ||
| 221 | + const newTab = { | ||
| 222 | + id: Date.now(), | ||
| 223 | + name: name, | ||
| 224 | + path: path, | ||
| 225 | + active: true, | ||
| 226 | + closable: true | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + openTabs.value.push(newTab) | ||
| 230 | +} | ||
| 231 | + | ||
| 232 | +// 切换到指定标签页 | ||
| 233 | +const switchToTab = (tab: any) => { | ||
| 234 | + // 取消所有标签页的激活状态 | ||
| 235 | + openTabs.value.forEach(t => t.active = false) | ||
| 236 | + | ||
| 237 | + // 激活当前标签页 | ||
| 238 | + tab.active = true | ||
| 239 | + | ||
| 240 | + // 跳转到对应路由 | ||
| 241 | + if (tab.path !== route.path) { | ||
| 242 | + router.push(tab.path) | ||
| 243 | + } | ||
| 244 | +} | ||
| 245 | + | ||
| 246 | +// 关闭标签页 | ||
| 247 | +const closeTab = (tab: any) => { | ||
| 248 | + if (!tab.closable) return | ||
| 249 | + | ||
| 250 | + const tabIndex = openTabs.value.findIndex(t => t.id === tab.id) | ||
| 251 | + if (tabIndex === -1) return | ||
| 252 | + | ||
| 253 | + // 如果关闭的是当前激活的标签页 | ||
| 254 | + if (tab.active) { | ||
| 255 | + // 找到下一个要激活的标签页 | ||
| 256 | + let nextActiveTab = null | ||
| 257 | + | ||
| 258 | + // 优先选择右侧的标签页 | ||
| 259 | + if (tabIndex < openTabs.value.length - 1) { | ||
| 260 | + nextActiveTab = openTabs.value[tabIndex + 1] | ||
| 261 | + } else if (tabIndex > 0) { | ||
| 262 | + nextActiveTab = openTabs.value[tabIndex - 1] | ||
| 263 | + } | ||
| 264 | + | ||
| 265 | + // 移除当前标签页 | ||
| 266 | + openTabs.value.splice(tabIndex, 1) | ||
| 267 | + | ||
| 268 | + // 激活下一个标签页 | ||
| 269 | + if (nextActiveTab) { | ||
| 270 | + nextActiveTab.active = true | ||
| 271 | + router.push(nextActiveTab.path) | ||
| 272 | + } else { | ||
| 273 | + // 如果没有其他标签页,跳转到首页 | ||
| 274 | + router.push('/main/dashboard') | ||
| 275 | + } | ||
| 276 | + } else { | ||
| 277 | + // 如果关闭的不是当前激活的标签页,直接移除 | ||
| 278 | + openTabs.value.splice(tabIndex, 1) | ||
| 279 | + } | ||
| 280 | +} | ||
| 281 | + | ||
| 282 | +// 标签页滚动功能 | ||
| 283 | +const scrollTabsLeft = () => { | ||
| 284 | + if (tabContainer.value) { | ||
| 285 | + tabContainer.value.scrollBy({ left: -200, behavior: 'smooth' }) | ||
| 286 | + } | ||
| 287 | +} | ||
| 288 | + | ||
| 289 | +const scrollTabsRight = () => { | ||
| 290 | + if (tabContainer.value) { | ||
| 291 | + tabContainer.value.scrollBy({ left: 200, behavior: 'smooth' }) | ||
| 292 | + } | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +// 切换子菜单展开/收起 | ||
| 296 | +const toggleSubmenu = (item: any) => { | ||
| 297 | + item.expanded = !item.expanded | ||
| 298 | + | ||
| 299 | + // 如果父级菜单有路径,则同时导航到该路径 | ||
| 300 | + if (item.path && item.path !== route.path) { | ||
| 301 | + handleMenuClick(item.path) | ||
| 302 | + } | ||
| 303 | +} | ||
| 304 | + | ||
| 305 | +// 检查父菜单是否激活 | ||
| 306 | +const isParentActive = (item: any) => { | ||
| 307 | + if (!item.children) return false | ||
| 308 | + return item.children.some((child: any) => child.path === route.path) | ||
| 309 | +} | ||
| 310 | + | ||
| 311 | +// 切换侧边栏 | ||
| 312 | +const toggleSidebar = () => { | ||
| 313 | + sidebarCollapsed.value = !sidebarCollapsed.value | ||
| 314 | +} | ||
| 315 | + | ||
| 316 | +// 退出登录 | ||
| 317 | +const handleLogout = () => { | ||
| 318 | + if (confirm('确定要退出登录吗?')) { | ||
| 319 | + // 清除本地存储 | ||
| 320 | + localStorage.removeItem('token') | ||
| 321 | + localStorage.removeItem('userInfo') | ||
| 322 | + | ||
| 323 | + // 跳转到登录页 | ||
| 324 | + router.push('/') | ||
| 325 | + } | ||
| 326 | +} | ||
| 327 | + | ||
| 328 | +// 监听路由变化,自动管理标签页 | ||
| 329 | +watch(() => route.path, (newPath) => { | ||
| 330 | + // 检查是否已经存在该路径的标签页 | ||
| 331 | + const existingTab = openTabs.value.find(tab => tab.path === newPath) | ||
| 332 | + | ||
| 333 | + if (existingTab) { | ||
| 334 | + // 如果存在,激活该标签页 | ||
| 335 | + openTabs.value.forEach(tab => tab.active = false) | ||
| 336 | + existingTab.active = true | ||
| 337 | + } else { | ||
| 338 | + // 如果不存在,创建新标签页 | ||
| 339 | + const menuItem = findMenuItemByPath(newPath) | ||
| 340 | + if (menuItem) { | ||
| 341 | + addNewTab(menuItem.name, newPath) | ||
| 342 | + } | ||
| 343 | + } | ||
| 344 | + | ||
| 345 | + // 自动展开对应的父菜单 | ||
| 346 | + for (const item of menuItems.value) { | ||
| 347 | + if (item.children && Array.isArray(item.children)) { | ||
| 348 | + const hasActiveChild = item.children.some((child: any) => child.path === newPath) | ||
| 349 | + if (hasActiveChild) { | ||
| 350 | + item.expanded = true | ||
| 351 | + break | ||
| 352 | + } | ||
| 353 | + } | ||
| 354 | + } | ||
| 355 | +}, { immediate: true }) | ||
| 356 | + | ||
| 357 | +// 组件挂载时获取用户信息 | ||
| 358 | +onMounted(() => { | ||
| 359 | + const storedUserInfo = localStorage.getItem('userInfo') | ||
| 360 | + if (storedUserInfo) { | ||
| 361 | + userInfo.value = JSON.parse(storedUserInfo) | ||
| 362 | + } | ||
| 363 | +}) | ||
| 364 | +</script> | ||
| 365 | + | ||
| 366 | +<style scoped> | ||
| 367 | +.main-layout { | ||
| 368 | + display: flex; | ||
| 369 | + height: 100vh; | ||
| 370 | + background-color: #f5f5f5; | ||
| 371 | +} | ||
| 372 | + | ||
| 373 | +/* 左侧菜单栏 */ | ||
| 374 | +.sidebar { | ||
| 375 | + width: 200px; | ||
| 376 | + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); | ||
| 377 | + color: white; | ||
| 378 | + transition: width 0.3s ease; | ||
| 379 | + overflow: hidden; | ||
| 380 | + box-shadow: 2px 0 5px rgba(0,0,0,0.1); | ||
| 381 | + z-index: 1000; | ||
| 382 | +} | ||
| 383 | + | ||
| 384 | +.sidebar.collapsed { | ||
| 385 | + width: 50px; | ||
| 386 | +} | ||
| 387 | + | ||
| 388 | +.sidebar-header { | ||
| 389 | + display: flex; | ||
| 390 | + align-items: center; | ||
| 391 | + justify-content: space-between; | ||
| 392 | + padding: 16px; | ||
| 393 | + border-bottom: 1px solid rgba(255,255,255,0.1); | ||
| 394 | +} | ||
| 395 | + | ||
| 396 | +.logo { | ||
| 397 | + margin: 0; | ||
| 398 | + font-size: 16px; | ||
| 399 | + font-weight: bold; | ||
| 400 | + color: #ecf0f1; | ||
| 401 | +} | ||
| 402 | + | ||
| 403 | +.collapse-btn { | ||
| 404 | + background: none; | ||
| 405 | + border: none; | ||
| 406 | + color: white; | ||
| 407 | + font-size: 14px; | ||
| 408 | + cursor: pointer; | ||
| 409 | + padding: 4px; | ||
| 410 | + border-radius: 3px; | ||
| 411 | + transition: background-color 0.3s; | ||
| 412 | +} | ||
| 413 | + | ||
| 414 | +.collapse-btn:hover { | ||
| 415 | + background-color: rgba(255,255,255,0.1); | ||
| 416 | +} | ||
| 417 | + | ||
| 418 | +/* 导航菜单 */ | ||
| 419 | +.sidebar-nav { | ||
| 420 | + padding: 16px 0; | ||
| 421 | +} | ||
| 422 | + | ||
| 423 | +.nav-list { | ||
| 424 | + list-style: none; | ||
| 425 | + margin: 0; | ||
| 426 | + padding: 0; | ||
| 427 | +} | ||
| 428 | + | ||
| 429 | +.nav-item { | ||
| 430 | + margin-bottom: 5px; | ||
| 431 | +} | ||
| 432 | + | ||
| 433 | +.nav-link { | ||
| 434 | + display: flex; | ||
| 435 | + align-items: center; | ||
| 436 | + padding: 8px 12px; | ||
| 437 | + color: #bdc3c7; | ||
| 438 | + text-decoration: none; | ||
| 439 | + transition: all 0.3s ease; | ||
| 440 | + border-left: 3px solid transparent; | ||
| 441 | + cursor: pointer; | ||
| 442 | +} | ||
| 443 | + | ||
| 444 | +.nav-link:hover { | ||
| 445 | + background-color: rgba(255,255,255,0.1); | ||
| 446 | + color: white; | ||
| 447 | +} | ||
| 448 | + | ||
| 449 | +.nav-link.active { | ||
| 450 | + background-color: rgba(52, 152, 219, 0.2); | ||
| 451 | + color: #3498db; | ||
| 452 | + border-left-color: #3498db; | ||
| 453 | +} | ||
| 454 | + | ||
| 455 | +.nav-icon { | ||
| 456 | + font-size: 16px; | ||
| 457 | + margin-right: 10px; | ||
| 458 | + min-width: 18px; | ||
| 459 | +} | ||
| 460 | + | ||
| 461 | +.nav-text { | ||
| 462 | + font-size: 13px; | ||
| 463 | + white-space: nowrap; | ||
| 464 | +} | ||
| 465 | + | ||
| 466 | +.nav-arrow { | ||
| 467 | + font-size: 10px; | ||
| 468 | + margin-left: auto; | ||
| 469 | + transition: transform 0.3s ease; | ||
| 470 | +} | ||
| 471 | + | ||
| 472 | +/* 二级菜单样式 */ | ||
| 473 | +.nav-item-group { | ||
| 474 | + margin-bottom: 5px; | ||
| 475 | +} | ||
| 476 | + | ||
| 477 | +.nav-link-parent { | ||
| 478 | + cursor: pointer; | ||
| 479 | + user-select: none; | ||
| 480 | +} | ||
| 481 | + | ||
| 482 | +.nav-link-parent:hover { | ||
| 483 | + background-color: rgba(255,255,255,0.1); | ||
| 484 | + color: white; | ||
| 485 | +} | ||
| 486 | + | ||
| 487 | +.submenu { | ||
| 488 | + list-style: none; | ||
| 489 | + margin: 0; | ||
| 490 | + padding: 0; | ||
| 491 | + background: rgba(0,0,0,0.1); | ||
| 492 | + border-left: 2px solid #3498db; | ||
| 493 | +} | ||
| 494 | + | ||
| 495 | +.submenu-item { | ||
| 496 | + margin: 0; | ||
| 497 | +} | ||
| 498 | + | ||
| 499 | +.nav-link-child { | ||
| 500 | + padding: 6px 12px 6px 32px; | ||
| 501 | + font-size: 12px; | ||
| 502 | + color: #bdc3c7; | ||
| 503 | +} | ||
| 504 | + | ||
| 505 | +.nav-link-child:hover { | ||
| 506 | + background-color: rgba(255,255,255,0.05); | ||
| 507 | + color: white; | ||
| 508 | +} | ||
| 509 | + | ||
| 510 | +.nav-link-child.active { | ||
| 511 | + background-color: rgba(52, 152, 219, 0.2); | ||
| 512 | + color: #3498db; | ||
| 513 | +} | ||
| 514 | + | ||
| 515 | +/* 主内容区域 */ | ||
| 516 | +.main-content { | ||
| 517 | + flex: 1; | ||
| 518 | + display: flex; | ||
| 519 | + flex-direction: column; | ||
| 520 | + overflow: hidden; | ||
| 521 | + position: relative; | ||
| 522 | +} | ||
| 523 | + | ||
| 524 | +/* 顶部导航栏 */ | ||
| 525 | +.top-header { | ||
| 526 | + background: white; | ||
| 527 | + padding: 0 16px; | ||
| 528 | + height: 50px; | ||
| 529 | + display: flex; | ||
| 530 | + align-items: center; | ||
| 531 | + justify-content: space-between; | ||
| 532 | + box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||
| 533 | + border-bottom: 1px solid #e0e0e0; | ||
| 534 | + position: relative; | ||
| 535 | + z-index: 100; | ||
| 536 | + width: 100%; | ||
| 537 | + min-height: 50px; | ||
| 538 | +} | ||
| 539 | + | ||
| 540 | +.header-left { | ||
| 541 | + display: flex; | ||
| 542 | + align-items: center; | ||
| 543 | +} | ||
| 544 | + | ||
| 545 | +.menu-toggle { | ||
| 546 | + background: none; | ||
| 547 | + border: none; | ||
| 548 | + font-size: 16px; | ||
| 549 | + cursor: pointer; | ||
| 550 | + padding: 6px; | ||
| 551 | + margin-right: 12px; | ||
| 552 | + border-radius: 4px; | ||
| 553 | + transition: background-color 0.3s; | ||
| 554 | +} | ||
| 555 | + | ||
| 556 | +.menu-toggle:hover { | ||
| 557 | + background-color: #f0f0f0; | ||
| 558 | +} | ||
| 559 | + | ||
| 560 | +.page-title { | ||
| 561 | + margin: 0; | ||
| 562 | + font-size: 18px; | ||
| 563 | + color: #333; | ||
| 564 | + font-weight: 500; | ||
| 565 | +} | ||
| 566 | + | ||
| 567 | +.header-right { | ||
| 568 | + display: flex; | ||
| 569 | + align-items: center; | ||
| 570 | +} | ||
| 571 | + | ||
| 572 | +.user-info { | ||
| 573 | + display: flex; | ||
| 574 | + align-items: center; | ||
| 575 | + gap: 15px; | ||
| 576 | +} | ||
| 577 | + | ||
| 578 | +.welcome-text { | ||
| 579 | + color: #666; | ||
| 580 | + font-size: 13px; | ||
| 581 | +} | ||
| 582 | + | ||
| 583 | +.logout-btn { | ||
| 584 | + background: #e74c3c; | ||
| 585 | + color: white; | ||
| 586 | + border: none; | ||
| 587 | + padding: 6px 12px; | ||
| 588 | + border-radius: 4px; | ||
| 589 | + cursor: pointer; | ||
| 590 | + font-size: 12px; | ||
| 591 | + transition: background-color 0.3s; | ||
| 592 | +} | ||
| 593 | + | ||
| 594 | +.logout-btn:hover { | ||
| 595 | + background: #c0392b; | ||
| 596 | +} | ||
| 597 | + | ||
| 598 | +/* 标签页导航栏 */ | ||
| 599 | +.tab-navigation { | ||
| 600 | + background: white; | ||
| 601 | + border-bottom: 1px solid #e0e0e0; | ||
| 602 | + display: flex; | ||
| 603 | + align-items: center; | ||
| 604 | + height: 32px; | ||
| 605 | + padding: 0 6px; | ||
| 606 | + box-shadow: 0 1px 3px rgba(0,0,0,0.1); | ||
| 607 | +} | ||
| 608 | + | ||
| 609 | +.tab-scroll-left, | ||
| 610 | +.tab-scroll-right { | ||
| 611 | + background: #f5f5f5; | ||
| 612 | + border: 1px solid #d9d9d9; | ||
| 613 | + padding: 2px 6px; | ||
| 614 | + cursor: pointer; | ||
| 615 | + border-radius: 2px; | ||
| 616 | + margin: 0 1px; | ||
| 617 | + transition: all 0.2s; | ||
| 618 | +} | ||
| 619 | + | ||
| 620 | +.tab-scroll-left:hover, | ||
| 621 | +.tab-scroll-right:hover { | ||
| 622 | + background: #e6f7ff; | ||
| 623 | + border-color: #91d5ff; | ||
| 624 | +} | ||
| 625 | + | ||
| 626 | +.scroll-arrow { | ||
| 627 | + font-size: 10px; | ||
| 628 | + color: #666; | ||
| 629 | + font-weight: bold; | ||
| 630 | +} | ||
| 631 | + | ||
| 632 | +.tab-container { | ||
| 633 | + flex: 1; | ||
| 634 | + display: flex; | ||
| 635 | + overflow-x: auto; | ||
| 636 | + scrollbar-width: none; | ||
| 637 | + -ms-overflow-style: none; | ||
| 638 | +} | ||
| 639 | + | ||
| 640 | +.tab-container::-webkit-scrollbar { | ||
| 641 | + display: none; | ||
| 642 | +} | ||
| 643 | + | ||
| 644 | +.tab-item { | ||
| 645 | + display: flex; | ||
| 646 | + align-items: center; | ||
| 647 | + padding: 4px 8px; | ||
| 648 | + margin: 0 1px; | ||
| 649 | + background: white; | ||
| 650 | + border: 1px solid #d9d9d9; | ||
| 651 | + border-radius: 3px; | ||
| 652 | + cursor: pointer; | ||
| 653 | + transition: all 0.2s; | ||
| 654 | + white-space: nowrap; | ||
| 655 | + min-width: 60px; | ||
| 656 | + position: relative; | ||
| 657 | +} | ||
| 658 | + | ||
| 659 | +.tab-item:hover { | ||
| 660 | + background: #f0f8ff; | ||
| 661 | + border-color: #91d5ff; | ||
| 662 | +} | ||
| 663 | + | ||
| 664 | +.tab-item.active { | ||
| 665 | + background: #1890ff; | ||
| 666 | + border-color: #1890ff; | ||
| 667 | + color: white; | ||
| 668 | +} | ||
| 669 | + | ||
| 670 | +.tab-name { | ||
| 671 | + font-size: 11px; | ||
| 672 | + margin-right: 4px; | ||
| 673 | +} | ||
| 674 | + | ||
| 675 | +.tab-close { | ||
| 676 | + font-size: 12px; | ||
| 677 | + cursor: pointer; | ||
| 678 | + padding: 1px; | ||
| 679 | + border-radius: 2px; | ||
| 680 | + transition: all 0.2s; | ||
| 681 | +} | ||
| 682 | + | ||
| 683 | +.tab-item:not(.active) .tab-close { | ||
| 684 | + color: #999; | ||
| 685 | +} | ||
| 686 | + | ||
| 687 | +.tab-item:not(.active) .tab-close:hover { | ||
| 688 | + background: #f5f5f5; | ||
| 689 | + color: #666; | ||
| 690 | +} | ||
| 691 | + | ||
| 692 | +.tab-item.active .tab-close { | ||
| 693 | + color: white; | ||
| 694 | +} | ||
| 695 | + | ||
| 696 | +.tab-item.active .tab-close:hover { | ||
| 697 | + background: rgba(255,255,255,0.2); | ||
| 698 | +} | ||
| 699 | + | ||
| 700 | +/* 页面内容 */ | ||
| 701 | +.page-content { | ||
| 702 | + flex: 1; | ||
| 703 | + padding: 0; | ||
| 704 | + overflow-y: auto; | ||
| 705 | + background-color: #f8f9fa; | ||
| 706 | + position: relative; | ||
| 707 | +} | ||
| 708 | + | ||
| 709 | +/* 底部栏 */ | ||
| 710 | +.main-footer { | ||
| 711 | + background: white; | ||
| 712 | + color: #333; | ||
| 713 | + padding: 8px 20px; | ||
| 714 | + border-top: 1px solid #e0e0e0; | ||
| 715 | + box-shadow: 0 -2px 4px rgba(0,0,0,0.1); | ||
| 716 | +} | ||
| 717 | + | ||
| 718 | +.footer-content { | ||
| 719 | + display: flex; | ||
| 720 | + justify-content: center; | ||
| 721 | + align-items: center; | ||
| 722 | + max-width: 1200px; | ||
| 723 | + margin: 0 auto; | ||
| 724 | +} | ||
| 725 | + | ||
| 726 | +.copyright { | ||
| 727 | + margin: 0; | ||
| 728 | + font-size: 12px; | ||
| 729 | + color: #666; | ||
| 730 | + text-align: center; | ||
| 731 | +} | ||
| 732 | + | ||
| 733 | +/* 响应式设计 */ | ||
| 734 | +@media (max-width: 768px) { | ||
| 735 | + .sidebar { | ||
| 736 | + position: fixed; | ||
| 737 | + left: -200px; | ||
| 738 | + z-index: 1000; | ||
| 739 | + height: 100vh; | ||
| 740 | + } | ||
| 741 | + | ||
| 742 | + .sidebar:not(.collapsed) { | ||
| 743 | + left: 0; | ||
| 744 | + } | ||
| 745 | + | ||
| 746 | + .main-content { | ||
| 747 | + margin-left: 0; | ||
| 748 | + } | ||
| 749 | + | ||
| 750 | + .tab-navigation { | ||
| 751 | + height: 28px; | ||
| 752 | + } | ||
| 753 | + | ||
| 754 | + .tab-item { | ||
| 755 | + min-width: 50px; | ||
| 756 | + padding: 3px 6px; | ||
| 757 | + } | ||
| 758 | + | ||
| 759 | + .tab-name { | ||
| 760 | + font-size: 10px; | ||
| 761 | + } | ||
| 762 | +} | ||
| 763 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | import { createApp } from 'vue' | 1 | import { createApp } from 'vue' |
| 2 | +import { createPinia } from 'pinia' | ||
| 2 | import App from './App.vue' | 3 | import App from './App.vue' |
| 3 | 4 | ||
| 5 | +import router from './router' | ||
| 6 | + | ||
| 4 | // 创建应用实例 | 7 | // 创建应用实例 |
| 5 | const app = createApp(App) | 8 | const app = createApp(App) |
| 9 | +const pinia = createPinia() | ||
| 6 | 10 | ||
| 11 | +app.use(pinia) | ||
| 12 | +app.use(router) | ||
| 7 | // 挂载应用 | 13 | // 挂载应用 |
| 8 | app.mount('#app') | 14 | app.mount('#app') | ... | ... |
| 1 | import { createRouter, createWebHistory } from 'vue-router' | 1 | import { createRouter, createWebHistory } from 'vue-router' |
| 2 | import type { RouteRecordRaw } from 'vue-router' | 2 | import type { RouteRecordRaw } from 'vue-router' |
| 3 | +import Login from '@/views/login/index.vue' | ||
| 4 | +import MainLayout from '@/layouts/MainLayout.vue' | ||
| 5 | +import Dashboard from '@/views/dashboard/index.vue' | ||
| 6 | +import Users from '@/views/users/index.vue' | ||
| 3 | import Test from '@/views/Test.vue' | 7 | import Test from '@/views/Test.vue' |
| 4 | import NProgress from 'nprogress' | 8 | import NProgress from 'nprogress' |
| 5 | import 'nprogress/nprogress.css' | 9 | import 'nprogress/nprogress.css' |
| ... | @@ -11,6 +15,115 @@ NProgress.configure({ showSpinner: false }) | ... | @@ -11,6 +15,115 @@ NProgress.configure({ showSpinner: false }) |
| 11 | const staticRoutes: RouteRecordRaw[] = [ | 15 | const staticRoutes: RouteRecordRaw[] = [ |
| 12 | { | 16 | { |
| 13 | path: '/', | 17 | path: '/', |
| 18 | + name: 'Login', | ||
| 19 | + component: Login, | ||
| 20 | + meta: { | ||
| 21 | + title: '登录', | ||
| 22 | + requiresAuth: false | ||
| 23 | + } | ||
| 24 | + }, | ||
| 25 | + { | ||
| 26 | + path: '/main', | ||
| 27 | + component: MainLayout, | ||
| 28 | + children: [ | ||
| 29 | + { | ||
| 30 | + path: 'dashboard', | ||
| 31 | + name: 'Dashboard', | ||
| 32 | + component: Dashboard, | ||
| 33 | + meta: { | ||
| 34 | + title: '首页', | ||
| 35 | + requiresAuth: true | ||
| 36 | + } | ||
| 37 | + }, | ||
| 38 | + { | ||
| 39 | + path: 'users', | ||
| 40 | + name: 'Users', | ||
| 41 | + component: Users, | ||
| 42 | + meta: { | ||
| 43 | + title: '用户管理', | ||
| 44 | + requiresAuth: true | ||
| 45 | + } | ||
| 46 | + }, | ||
| 47 | + { | ||
| 48 | + path: 'sys/role', | ||
| 49 | + name: 'Role', | ||
| 50 | + component: () => import('@/views/sys/role/index.vue'), | ||
| 51 | + meta: { | ||
| 52 | + title: '角色管理', | ||
| 53 | + requiresAuth: true | ||
| 54 | + } | ||
| 55 | + }, | ||
| 56 | + { | ||
| 57 | + path: 'sys/menu', | ||
| 58 | + name: 'Menu', | ||
| 59 | + component: () => import('@/views/sys/menu/index.vue'), | ||
| 60 | + meta: { | ||
| 61 | + title: '菜单管理', | ||
| 62 | + requiresAuth: true | ||
| 63 | + } | ||
| 64 | + }, | ||
| 65 | + { | ||
| 66 | + path: 'sys/dict', | ||
| 67 | + name: 'Dict', | ||
| 68 | + component: () => import('@/views/sys/dict/index.vue'), | ||
| 69 | + meta: { | ||
| 70 | + title: '字典管理', | ||
| 71 | + requiresAuth: true | ||
| 72 | + } | ||
| 73 | + }, | ||
| 74 | + { | ||
| 75 | + path: 'sys/dict-item', | ||
| 76 | + name: 'DictItem', | ||
| 77 | + component: () => import('@/views/sys/dict-item/index.vue'), | ||
| 78 | + meta: { | ||
| 79 | + title: '字典项管理', | ||
| 80 | + requiresAuth: true | ||
| 81 | + } | ||
| 82 | + }, | ||
| 83 | + { | ||
| 84 | + path: 'sys/log', | ||
| 85 | + name: 'Log', | ||
| 86 | + component: () => import('@/views/sys/log/index.vue'), | ||
| 87 | + meta: { | ||
| 88 | + title: '日志管理', | ||
| 89 | + requiresAuth: true | ||
| 90 | + } | ||
| 91 | + }, | ||
| 92 | + { | ||
| 93 | + path: 'dicts', | ||
| 94 | + name: 'Dicts', | ||
| 95 | + component: () => import('@/views/dicts/index.vue'), | ||
| 96 | + meta: { | ||
| 97 | + title: '字典管理', | ||
| 98 | + requiresAuth: true | ||
| 99 | + } | ||
| 100 | + }, | ||
| 101 | + { | ||
| 102 | + path: 'logs', | ||
| 103 | + name: 'Logs', | ||
| 104 | + component: () => import('@/views/logs/index.vue'), | ||
| 105 | + meta: { | ||
| 106 | + title: '日志管理', | ||
| 107 | + requiresAuth: true | ||
| 108 | + } | ||
| 109 | + }, | ||
| 110 | + { | ||
| 111 | + path: 'settings', | ||
| 112 | + name: 'Settings', | ||
| 113 | + component: () => import('@/views/settings/index.vue'), | ||
| 114 | + meta: { | ||
| 115 | + title: '系统设置', | ||
| 116 | + requiresAuth: true | ||
| 117 | + } | ||
| 118 | + } | ||
| 119 | + ] | ||
| 120 | + }, | ||
| 121 | + { | ||
| 122 | + path: '/dashboard', | ||
| 123 | + redirect: '/main/dashboard' | ||
| 124 | + }, | ||
| 125 | + { | ||
| 126 | + path: '/test', | ||
| 14 | name: 'Test', | 127 | name: 'Test', |
| 15 | component: Test, | 128 | component: Test, |
| 16 | meta: { | 129 | meta: { |
| ... | @@ -27,8 +140,13 @@ const router = createRouter({ | ... | @@ -27,8 +140,13 @@ const router = createRouter({ |
| 27 | scrollBehavior: () => ({ top: 0 }) | 140 | scrollBehavior: () => ({ top: 0 }) |
| 28 | }) | 141 | }) |
| 29 | 142 | ||
| 30 | -// 路由守卫 | 143 | +// 优化的路由守卫 |
| 31 | -router.beforeEach(async (to, from, next) => { | 144 | +router.beforeEach((to, from, next) => { |
| 145 | + // 只在路由真正变化时记录日志 | ||
| 146 | + if (to.path !== from.path) { | ||
| 147 | + console.log('路由变化:', { from: from.path, to: to.path }) | ||
| 148 | + } | ||
| 149 | + | ||
| 32 | // 开始进度条 | 150 | // 开始进度条 |
| 33 | NProgress.start() | 151 | NProgress.start() |
| 34 | 152 | ||
| ... | @@ -37,7 +155,24 @@ router.beforeEach(async (to, from, next) => { | ... | @@ -37,7 +155,24 @@ router.beforeEach(async (to, from, next) => { |
| 37 | document.title = `${to.meta.title} - Apple经销商ERP系统` | 155 | document.title = `${to.meta.title} - Apple经销商ERP系统` |
| 38 | } | 156 | } |
| 39 | 157 | ||
| 40 | - // 暂时跳过所有权限检查,直接放行 | 158 | + // 检查认证状态 |
| 159 | + if (to.meta.requiresAuth) { | ||
| 160 | + const token = localStorage.getItem('token') | ||
| 161 | + if (!token) { | ||
| 162 | + console.log('未认证,跳转到登录页') | ||
| 163 | + next('/') | ||
| 164 | + return | ||
| 165 | + } | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + // 如果已登录且访问登录页,跳转到首页 | ||
| 169 | + if (to.path === '/' && localStorage.getItem('token')) { | ||
| 170 | + console.log('已登录,跳转到首页') | ||
| 171 | + next('/main/dashboard') | ||
| 172 | + return | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + // 继续导航 | ||
| 41 | next() | 176 | next() |
| 42 | }) | 177 | }) |
| 43 | 178 | ... | ... |
| ... | @@ -12,9 +12,19 @@ const service: AxiosInstance = axios.create({ | ... | @@ -12,9 +12,19 @@ const service: AxiosInstance = axios.create({ |
| 12 | } | 12 | } |
| 13 | }) | 13 | }) |
| 14 | 14 | ||
| 15 | +console.log('Axios实例创建,baseURL:', import.meta.env.VITE_API_BASE_URL) | ||
| 16 | + | ||
| 15 | // 请求拦截器 | 17 | // 请求拦截器 |
| 16 | service.interceptors.request.use( | 18 | service.interceptors.request.use( |
| 17 | (config: any) => { | 19 | (config: any) => { |
| 20 | + console.log('请求拦截器 - 请求配置:', { | ||
| 21 | + url: config.url, | ||
| 22 | + baseURL: config.baseURL, | ||
| 23 | + method: config.method, | ||
| 24 | + params: config.params, | ||
| 25 | + headers: config.headers | ||
| 26 | + }) | ||
| 27 | + | ||
| 18 | const userStore = useUserStore() | 28 | const userStore = useUserStore() |
| 19 | 29 | ||
| 20 | // 添加token到请求头 | 30 | // 添加token到请求头 |
| ... | @@ -105,6 +115,7 @@ service.interceptors.response.use( | ... | @@ -105,6 +115,7 @@ service.interceptors.response.use( |
| 105 | // 请求方法封装 | 115 | // 请求方法封装 |
| 106 | export const request = { | 116 | export const request = { |
| 107 | get<T = any>(url: string, params?: any): Promise<T> { | 117 | get<T = any>(url: string, params?: any): Promise<T> { |
| 118 | + console.log('Request GET:', url, params) | ||
| 108 | return service.get(url, { params }) | 119 | return service.get(url, { params }) |
| 109 | }, | 120 | }, |
| 110 | 121 | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | - <div class="dashboard"> | 2 | + <div class="dashboard-container"> |
| 3 | - <!-- 欢迎区域 --> | 3 | + <div class="dashboard-header"> |
| 4 | - <div class="welcome-section"> | 4 | + <h2>系统概览</h2> |
| 5 | - <el-card class="welcome-card"> | 5 | + <p>欢迎回来,{{ userInfo?.username || '用户' }}!</p> |
| 6 | - <div class="welcome-content"> | ||
| 7 | - <div class="welcome-text"> | ||
| 8 | - <h2>欢迎回来,{{ userStore.username }}!</h2> | ||
| 9 | - <p>今天是 {{ currentDate }},祝您工作愉快!</p> | ||
| 10 | - </div> | ||
| 11 | - <div class="welcome-avatar"> | ||
| 12 | - <el-avatar :size="80" :src="userStore.avatar"> | ||
| 13 | - <el-icon><User /></el-icon> | ||
| 14 | - </el-avatar> | ||
| 15 | - </div> | ||
| 16 | - </div> | ||
| 17 | - </el-card> | ||
| 18 | </div> | 6 | </div> |
| 19 | 7 | ||
| 20 | - <!-- 统计卡片 --> | 8 | + <div class="dashboard-stats"> |
| 21 | - <div class="stats-section"> | 9 | + <div class="stat-card"> |
| 22 | - <el-row :gutter="20"> | 10 | + <div class="stat-icon">👤</div> |
| 23 | - <el-col :xs="24" :sm="12" :md="6" v-for="stat in statsData" :key="stat.title"> | ||
| 24 | - <el-card class="stat-card" :class="stat.type"> | ||
| 25 | <div class="stat-content"> | 11 | <div class="stat-content"> |
| 26 | - <div class="stat-icon"> | 12 | + <h3>用户总数</h3> |
| 27 | - <el-icon :size="32"> | 13 | + <p class="stat-number">1,234</p> |
| 28 | - <component :is="stat.icon" /> | ||
| 29 | - </el-icon> | ||
| 30 | </div> | 14 | </div> |
| 31 | - <div class="stat-info"> | ||
| 32 | - <div class="stat-value">{{ stat.value }}</div> | ||
| 33 | - <div class="stat-title">{{ stat.title }}</div> | ||
| 34 | </div> | 15 | </div> |
| 16 | + | ||
| 17 | + <div class="stat-card"> | ||
| 18 | + <div class="stat-icon">📈</div> | ||
| 19 | + <div class="stat-content"> | ||
| 20 | + <h3>今日访问</h3> | ||
| 21 | + <p class="stat-number">567</p> | ||
| 35 | </div> | 22 | </div> |
| 36 | - </el-card> | ||
| 37 | - </el-col> | ||
| 38 | - </el-row> | ||
| 39 | </div> | 23 | </div> |
| 40 | 24 | ||
| 41 | - <!-- 快捷操作 --> | 25 | + <div class="stat-card"> |
| 42 | - <div class="quick-actions-section"> | 26 | + <div class="stat-icon">✅</div> |
| 43 | - <el-card> | 27 | + <div class="stat-content"> |
| 44 | - <template #header> | 28 | + <h3>系统状态</h3> |
| 45 | - <div class="card-header"> | 29 | + <p class="stat-number">正常</p> |
| 46 | - <span class="title">快捷操作</span> | ||
| 47 | </div> | 30 | </div> |
| 48 | - </template> | ||
| 49 | - <div class="quick-actions"> | ||
| 50 | - <div | ||
| 51 | - v-for="action in quickActions" | ||
| 52 | - :key="action.title" | ||
| 53 | - class="action-item" | ||
| 54 | - @click="handleQuickAction(action)" | ||
| 55 | - > | ||
| 56 | - <div class="action-icon"> | ||
| 57 | - <el-icon :size="24"> | ||
| 58 | - <component :is="action.icon" /> | ||
| 59 | - </el-icon> | ||
| 60 | </div> | 31 | </div> |
| 61 | - <div class="action-title">{{ action.title }}</div> | 32 | + |
| 33 | + <div class="stat-card"> | ||
| 34 | + <div class="stat-icon">⏱️</div> | ||
| 35 | + <div class="stat-content"> | ||
| 36 | + <h3>运行时间</h3> | ||
| 37 | + <p class="stat-number">{{ currentTime }}</p> | ||
| 62 | </div> | 38 | </div> |
| 63 | </div> | 39 | </div> |
| 64 | - </el-card> | ||
| 65 | </div> | 40 | </div> |
| 66 | 41 | ||
| 67 | - <!-- 系统信息 --> | 42 | + <div class="dashboard-content"> |
| 68 | - <div class="system-info-section"> | 43 | + <div class="dashboard-card"> |
| 69 | - <el-row :gutter="20"> | 44 | + <h3>系统信息</h3> |
| 70 | - <el-col :xs="24" :lg="12"> | 45 | + <div class="info-grid"> |
| 71 | - <el-card> | ||
| 72 | - <template #header> | ||
| 73 | - <div class="card-header"> | ||
| 74 | - <span class="title">系统信息</span> | ||
| 75 | - </div> | ||
| 76 | - </template> | ||
| 77 | - <div class="system-info"> | ||
| 78 | <div class="info-item"> | 46 | <div class="info-item"> |
| 79 | - <span class="label">系统版本:</span> | 47 | + <label>用户名:</label> |
| 80 | - <span class="value">v1.0.0</span> | 48 | + <span>{{ userInfo?.username || '未知' }}</span> |
| 81 | </div> | 49 | </div> |
| 82 | <div class="info-item"> | 50 | <div class="info-item"> |
| 83 | - <span class="label">运行时间:</span> | 51 | + <label>角色:</label> |
| 84 | - <span class="value">{{ systemUptime }}</span> | 52 | + <span>{{ userInfo?.roles?.[0]?.roleName || '普通用户' }}</span> |
| 85 | </div> | 53 | </div> |
| 86 | <div class="info-item"> | 54 | <div class="info-item"> |
| 87 | - <span class="label">在线用户:</span> | 55 | + <label>登录时间:</label> |
| 88 | - <span class="value">{{ onlineUsers }}</span> | 56 | + <span>{{ currentTime }}</span> |
| 89 | </div> | 57 | </div> |
| 90 | <div class="info-item"> | 58 | <div class="info-item"> |
| 91 | - <span class="label">最后登录:</span> | 59 | + <label>系统版本:</label> |
| 92 | - <span class="value">{{ lastLoginTime }}</span> | 60 | + <span>v1.0.0</span> |
| 93 | - </div> | ||
| 94 | - </div> | ||
| 95 | - </el-card> | ||
| 96 | - </el-col> | ||
| 97 | - | ||
| 98 | - <el-col :xs="24" :lg="12"> | ||
| 99 | - <el-card> | ||
| 100 | - <template #header> | ||
| 101 | - <div class="card-header"> | ||
| 102 | - <span class="title">最近活动</span> | ||
| 103 | </div> | 61 | </div> |
| 104 | - </template> | ||
| 105 | - <div class="recent-activities"> | ||
| 106 | - <div | ||
| 107 | - v-for="activity in recentActivities" | ||
| 108 | - :key="activity.id" | ||
| 109 | - class="activity-item" | ||
| 110 | - > | ||
| 111 | - <div class="activity-icon"> | ||
| 112 | - <el-icon> | ||
| 113 | - <component :is="activity.icon" /> | ||
| 114 | - </el-icon> | ||
| 115 | </div> | 62 | </div> |
| 116 | - <div class="activity-content"> | ||
| 117 | - <div class="activity-title">{{ activity.title }}</div> | ||
| 118 | - <div class="activity-time">{{ activity.time }}</div> | ||
| 119 | </div> | 63 | </div> |
| 64 | + | ||
| 65 | + <div class="dashboard-card"> | ||
| 66 | + <h3>快速操作</h3> | ||
| 67 | + <div class="quick-actions"> | ||
| 68 | + <button class="action-btn">👤 用户管理</button> | ||
| 69 | + <button class="action-btn">🛡️ 角色管理</button> | ||
| 70 | + <button class="action-btn">⚙️ 系统设置</button> | ||
| 71 | + <button class="action-btn">📊 查看日志</button> | ||
| 120 | </div> | 72 | </div> |
| 121 | </div> | 73 | </div> |
| 122 | - </el-card> | ||
| 123 | - </el-col> | ||
| 124 | - </el-row> | ||
| 125 | </div> | 74 | </div> |
| 126 | </div> | 75 | </div> |
| 127 | </template> | 76 | </template> |
| 128 | 77 | ||
| 129 | <script setup lang="ts"> | 78 | <script setup lang="ts"> |
| 130 | -import { ref, computed, onMounted } from 'vue' | 79 | +import { ref, onMounted } from 'vue' |
| 131 | import { useRouter } from 'vue-router' | 80 | import { useRouter } from 'vue-router' |
| 132 | -import { useUserStore } from '@/stores/modules/user' | ||
| 133 | -import { formatDate } from '@/utils' | ||
| 134 | 81 | ||
| 135 | const router = useRouter() | 82 | const router = useRouter() |
| 136 | -const userStore = useUserStore() | 83 | +const currentTime = ref('') |
| 84 | +const userInfo = ref<any>(null) | ||
| 137 | 85 | ||
| 138 | -// 当前日期 | 86 | +onMounted(() => { |
| 139 | -const currentDate = computed(() => { | 87 | + // 获取当前时间 |
| 140 | - return formatDate(new Date(), 'YYYY年MM月DD日 dddd') | 88 | + currentTime.value = new Date().toLocaleString() |
| 141 | -}) | ||
| 142 | 89 | ||
| 143 | -// 统计数据 | 90 | + // 获取用户信息 |
| 144 | -const statsData = ref([ | 91 | + const storedUserInfo = localStorage.getItem('userInfo') |
| 145 | - { | 92 | + if (storedUserInfo) { |
| 146 | - title: '总用户数', | 93 | + userInfo.value = JSON.parse(storedUserInfo) |
| 147 | - value: '1,234', | ||
| 148 | - icon: 'User', | ||
| 149 | - type: 'primary' | ||
| 150 | - }, | ||
| 151 | - { | ||
| 152 | - title: '今日访问', | ||
| 153 | - value: '5,678', | ||
| 154 | - icon: 'View', | ||
| 155 | - type: 'success' | ||
| 156 | - }, | ||
| 157 | - { | ||
| 158 | - title: '系统消息', | ||
| 159 | - value: '12', | ||
| 160 | - icon: 'Message', | ||
| 161 | - type: 'warning' | ||
| 162 | - }, | ||
| 163 | - { | ||
| 164 | - title: '待处理', | ||
| 165 | - value: '8', | ||
| 166 | - icon: 'Clock', | ||
| 167 | - type: 'danger' | ||
| 168 | } | 94 | } |
| 169 | -]) | 95 | + |
| 170 | - | 96 | + // 检查是否已登录 |
| 171 | -// 快捷操作 | 97 | + const token = localStorage.getItem('token') |
| 172 | -const quickActions = ref([ | 98 | + if (!token) { |
| 173 | - { | 99 | + router.push('/') |
| 174 | - title: '用户管理', | ||
| 175 | - icon: 'User', | ||
| 176 | - path: '/sys/user' | ||
| 177 | - }, | ||
| 178 | - { | ||
| 179 | - title: '角色管理', | ||
| 180 | - icon: 'UserFilled', | ||
| 181 | - path: '/sys/role' | ||
| 182 | - }, | ||
| 183 | - { | ||
| 184 | - title: '菜单管理', | ||
| 185 | - icon: 'Menu', | ||
| 186 | - path: '/sys/menu' | ||
| 187 | - }, | ||
| 188 | - { | ||
| 189 | - title: '字典管理', | ||
| 190 | - icon: 'Document', | ||
| 191 | - path: '/sys/dict' | ||
| 192 | - }, | ||
| 193 | - { | ||
| 194 | - title: '日志管理', | ||
| 195 | - icon: 'DocumentCopy', | ||
| 196 | - path: '/sys/log' | ||
| 197 | - }, | ||
| 198 | - { | ||
| 199 | - title: '系统设置', | ||
| 200 | - icon: 'Setting', | ||
| 201 | - path: '/sys/settings' | ||
| 202 | - } | ||
| 203 | -]) | ||
| 204 | - | ||
| 205 | -// 系统信息 | ||
| 206 | -const systemUptime = ref('7天12小时30分钟') | ||
| 207 | -const onlineUsers = ref('156') | ||
| 208 | -const lastLoginTime = ref('2024-01-15 14:30:25') | ||
| 209 | - | ||
| 210 | -// 最近活动 | ||
| 211 | -const recentActivities = ref([ | ||
| 212 | - { | ||
| 213 | - id: 1, | ||
| 214 | - title: '用户 admin 登录系统', | ||
| 215 | - time: '2分钟前', | ||
| 216 | - icon: 'User' | ||
| 217 | - }, | ||
| 218 | - { | ||
| 219 | - id: 2, | ||
| 220 | - title: '新增用户:张三', | ||
| 221 | - time: '15分钟前', | ||
| 222 | - icon: 'UserFilled' | ||
| 223 | - }, | ||
| 224 | - { | ||
| 225 | - id: 3, | ||
| 226 | - title: '修改角色权限:管理员', | ||
| 227 | - time: '1小时前', | ||
| 228 | - icon: 'Lock' | ||
| 229 | - }, | ||
| 230 | - { | ||
| 231 | - id: 4, | ||
| 232 | - title: '系统备份完成', | ||
| 233 | - time: '2小时前', | ||
| 234 | - icon: 'Download' | ||
| 235 | } | 100 | } |
| 236 | -]) | 101 | +}) |
| 237 | 102 | ||
| 238 | -// 处理快捷操作 | 103 | +const logout = () => { |
| 239 | -const handleQuickAction = (action: any) => { | 104 | + // 清除本地存储 |
| 240 | - router.push(action.path) | 105 | + localStorage.removeItem('token') |
| 241 | -} | 106 | + localStorage.removeItem('userInfo') |
| 242 | 107 | ||
| 243 | -// 组件挂载时初始化 | 108 | + // 跳转到登录页 |
| 244 | -onMounted(() => { | 109 | + router.push('/') |
| 245 | - // 这里可以加载一些初始化数据 | 110 | +} |
| 246 | - console.log('仪表板初始化完成') | ||
| 247 | -}) | ||
| 248 | </script> | 111 | </script> |
| 249 | 112 | ||
| 250 | -<style lang="scss" scoped> | 113 | +<style scoped> |
| 251 | -.dashboard { | 114 | +.dashboard-container { |
| 252 | padding: 20px; | 115 | padding: 20px; |
| 116 | + min-height: 100vh; | ||
| 117 | +} | ||
| 253 | 118 | ||
| 254 | - .welcome-section { | 119 | +.dashboard-header { |
| 255 | - margin-bottom: 20px; | 120 | + margin-bottom: 30px; |
| 256 | - | 121 | +} |
| 257 | - .welcome-card { | ||
| 258 | - .welcome-content { | ||
| 259 | - display: flex; | ||
| 260 | - align-items: center; | ||
| 261 | - justify-content: space-between; | ||
| 262 | 122 | ||
| 263 | - .welcome-text { | 123 | +.dashboard-header h2 { |
| 264 | - h2 { | 124 | + font-size: 20px; |
| 125 | + color: #333; | ||
| 265 | margin: 0 0 8px 0; | 126 | margin: 0 0 8px 0; |
| 266 | - font-size: 24px; | ||
| 267 | - font-weight: 600; | ||
| 268 | - color: var(--el-text-color-primary); | ||
| 269 | } | 127 | } |
| 270 | 128 | ||
| 271 | - p { | 129 | +.dashboard-header p { |
| 130 | + color: #666; | ||
| 272 | margin: 0; | 131 | margin: 0; |
| 273 | - font-size: 14px; | 132 | +} |
| 274 | - color: var(--el-text-color-secondary); | ||
| 275 | - } | ||
| 276 | - } | ||
| 277 | - | ||
| 278 | - .welcome-avatar { | ||
| 279 | - .el-avatar { | ||
| 280 | - background-color: var(--el-color-primary); | ||
| 281 | - color: white; | ||
| 282 | - } | ||
| 283 | - } | ||
| 284 | - } | ||
| 285 | - } | ||
| 286 | - } | ||
| 287 | 133 | ||
| 288 | - .stats-section { | 134 | +.dashboard-stats { |
| 289 | - margin-bottom: 20px; | 135 | + display: grid; |
| 136 | + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | ||
| 137 | + gap: 20px; | ||
| 138 | + margin-bottom: 30px; | ||
| 139 | +} | ||
| 290 | 140 | ||
| 291 | .stat-card { | 141 | .stat-card { |
| 292 | - .stat-content { | 142 | + background: white; |
| 143 | + padding: 20px; | ||
| 144 | + border-radius: 8px; | ||
| 145 | + box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||
| 293 | display: flex; | 146 | display: flex; |
| 294 | align-items: center; | 147 | align-items: center; |
| 148 | + gap: 15px; | ||
| 149 | + transition: transform 0.3s ease; | ||
| 150 | +} | ||
| 295 | 151 | ||
| 296 | - .stat-icon { | 152 | +.stat-card:hover { |
| 297 | - margin-right: 16px; | 153 | + transform: translateY(-2px); |
| 298 | - padding: 12px; | 154 | +} |
| 299 | - border-radius: 8px; | ||
| 300 | - background-color: var(--el-color-primary-light-9); | ||
| 301 | - color: var(--el-color-primary); | ||
| 302 | - } | ||
| 303 | - | ||
| 304 | - .stat-info { | ||
| 305 | - .stat-value { | ||
| 306 | - font-size: 24px; | ||
| 307 | - font-weight: 600; | ||
| 308 | - color: var(--el-text-color-primary); | ||
| 309 | - margin-bottom: 4px; | ||
| 310 | - } | ||
| 311 | - | ||
| 312 | - .stat-title { | ||
| 313 | - font-size: 14px; | ||
| 314 | - color: var(--el-text-color-secondary); | ||
| 315 | - } | ||
| 316 | - } | ||
| 317 | - } | ||
| 318 | - | ||
| 319 | - &.primary .stat-icon { | ||
| 320 | - background-color: var(--el-color-primary-light-9); | ||
| 321 | - color: var(--el-color-primary); | ||
| 322 | - } | ||
| 323 | - | ||
| 324 | - &.success .stat-icon { | ||
| 325 | - background-color: var(--el-color-success-light-9); | ||
| 326 | - color: var(--el-color-success); | ||
| 327 | - } | ||
| 328 | 155 | ||
| 329 | - &.warning .stat-icon { | 156 | +.stat-icon { |
| 330 | - background-color: var(--el-color-warning-light-9); | 157 | + font-size: 20px; |
| 331 | - color: var(--el-color-warning); | 158 | + width: 32px; |
| 332 | - } | 159 | + height: 32px; |
| 160 | + display: flex; | ||
| 161 | + align-items: center; | ||
| 162 | + justify-content: center; | ||
| 163 | + background: #f8f9fa; | ||
| 164 | + border-radius: 6px; | ||
| 165 | +} | ||
| 333 | 166 | ||
| 334 | - &.danger .stat-icon { | 167 | +.stat-content h3 { |
| 335 | - background-color: var(--el-color-danger-light-9); | 168 | + margin: 0 0 5px 0; |
| 336 | - color: var(--el-color-danger); | 169 | + font-size: 12px; |
| 337 | - } | 170 | + color: #666; |
| 338 | - } | 171 | + font-weight: 500; |
| 339 | - } | 172 | +} |
| 340 | 173 | ||
| 341 | - .quick-actions-section { | 174 | +.stat-number { |
| 342 | - margin-bottom: 20px; | 175 | + margin: 0; |
| 176 | + font-size: 18px; | ||
| 177 | + font-weight: bold; | ||
| 178 | + color: #333; | ||
| 179 | +} | ||
| 343 | 180 | ||
| 344 | - .quick-actions { | 181 | +.dashboard-content { |
| 345 | display: grid; | 182 | display: grid; |
| 346 | - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); | 183 | + grid-template-columns: 1fr 1fr; |
| 347 | - gap: 16px; | 184 | + gap: 20px; |
| 185 | +} | ||
| 348 | 186 | ||
| 349 | - .action-item { | 187 | +.dashboard-card { |
| 350 | - display: flex; | 188 | + background: white; |
| 351 | - flex-direction: column; | 189 | + padding: 24px; |
| 352 | - align-items: center; | ||
| 353 | - padding: 20px; | ||
| 354 | border-radius: 8px; | 190 | border-radius: 8px; |
| 355 | - background-color: var(--el-bg-color-page); | 191 | + box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| 356 | - cursor: pointer; | 192 | +} |
| 357 | - transition: all 0.3s ease; | ||
| 358 | - | ||
| 359 | - &:hover { | ||
| 360 | - background-color: var(--el-color-primary-light-9); | ||
| 361 | - transform: translateY(-2px); | ||
| 362 | - } | ||
| 363 | 193 | ||
| 364 | - .action-icon { | 194 | +.dashboard-card h3 { |
| 365 | - margin-bottom: 8px; | 195 | + margin: 0 0 16px 0; |
| 366 | - color: var(--el-color-primary); | 196 | + color: #333; |
| 367 | - } | 197 | + font-size: 16px; |
| 198 | + font-weight: 600; | ||
| 199 | +} | ||
| 368 | 200 | ||
| 369 | - .action-title { | 201 | +.info-grid { |
| 370 | - font-size: 14px; | 202 | + display: grid; |
| 371 | - color: var(--el-text-color-primary); | 203 | + gap: 12px; |
| 372 | - text-align: center; | 204 | +} |
| 373 | - } | ||
| 374 | - } | ||
| 375 | - } | ||
| 376 | - } | ||
| 377 | 205 | ||
| 378 | - .system-info-section { | ||
| 379 | - .system-info { | ||
| 380 | .info-item { | 206 | .info-item { |
| 381 | display: flex; | 207 | display: flex; |
| 382 | justify-content: space-between; | 208 | justify-content: space-between; |
| 383 | align-items: center; | 209 | align-items: center; |
| 384 | - padding: 12px 0; | 210 | + padding: 8px 0; |
| 385 | - border-bottom: 1px solid var(--el-border-color-lighter); | 211 | + border-bottom: 1px solid #f0f0f0; |
| 212 | +} | ||
| 386 | 213 | ||
| 387 | - &:last-child { | 214 | +.info-item:last-child { |
| 388 | border-bottom: none; | 215 | border-bottom: none; |
| 389 | } | 216 | } |
| 390 | 217 | ||
| 391 | - .label { | 218 | +.info-item label { |
| 392 | - font-size: 14px; | ||
| 393 | - color: var(--el-text-color-secondary); | ||
| 394 | - } | ||
| 395 | - | ||
| 396 | - .value { | ||
| 397 | - font-size: 14px; | ||
| 398 | - color: var(--el-text-color-primary); | ||
| 399 | font-weight: 500; | 219 | font-weight: 500; |
| 400 | - } | 220 | + color: #666; |
| 401 | - } | 221 | +} |
| 402 | - } | ||
| 403 | - | ||
| 404 | - .recent-activities { | ||
| 405 | - .activity-item { | ||
| 406 | - display: flex; | ||
| 407 | - align-items: center; | ||
| 408 | - padding: 12px 0; | ||
| 409 | - border-bottom: 1px solid var(--el-border-color-lighter); | ||
| 410 | - | ||
| 411 | - &:last-child { | ||
| 412 | - border-bottom: none; | ||
| 413 | - } | ||
| 414 | - | ||
| 415 | - .activity-icon { | ||
| 416 | - margin-right: 12px; | ||
| 417 | - padding: 8px; | ||
| 418 | - border-radius: 50%; | ||
| 419 | - background-color: var(--el-color-primary-light-9); | ||
| 420 | - color: var(--el-color-primary); | ||
| 421 | - } | ||
| 422 | 222 | ||
| 423 | - .activity-content { | 223 | +.info-item span { |
| 424 | - flex: 1; | 224 | + color: #333; |
| 225 | +} | ||
| 425 | 226 | ||
| 426 | - .activity-title { | 227 | +.quick-actions { |
| 427 | - font-size: 14px; | 228 | + display: grid; |
| 428 | - color: var(--el-text-color-primary); | 229 | + grid-template-columns: 1fr 1fr; |
| 429 | - margin-bottom: 4px; | 230 | + gap: 12px; |
| 430 | - } | 231 | +} |
| 431 | 232 | ||
| 432 | - .activity-time { | 233 | +.action-btn { |
| 234 | + background: #3498db; | ||
| 235 | + color: white; | ||
| 236 | + border: none; | ||
| 237 | + padding: 8px 12px; | ||
| 238 | + border-radius: 4px; | ||
| 239 | + cursor: pointer; | ||
| 433 | font-size: 12px; | 240 | font-size: 12px; |
| 434 | - color: var(--el-text-color-secondary); | 241 | + transition: background-color 0.3s; |
| 435 | - } | ||
| 436 | - } | ||
| 437 | - } | ||
| 438 | - } | ||
| 439 | - } | ||
| 440 | } | 242 | } |
| 441 | 243 | ||
| 442 | -// 响应式设计 | 244 | +.action-btn:hover { |
| 443 | -@media (max-width: 768px) { | 245 | + background: #2980b9; |
| 444 | - .dashboard { | 246 | +} |
| 445 | - padding: 16px; | ||
| 446 | - | ||
| 447 | - .welcome-section { | ||
| 448 | - .welcome-content { | ||
| 449 | - flex-direction: column; | ||
| 450 | - text-align: center; | ||
| 451 | 247 | ||
| 452 | - .welcome-avatar { | 248 | +@media (max-width: 768px) { |
| 453 | - margin-top: 16px; | 249 | + .dashboard-stats { |
| 454 | - } | 250 | + grid-template-columns: 1fr; |
| 455 | } | 251 | } |
| 252 | + | ||
| 253 | + .dashboard-content { | ||
| 254 | + grid-template-columns: 1fr; | ||
| 456 | } | 255 | } |
| 457 | 256 | ||
| 458 | .quick-actions { | 257 | .quick-actions { |
| 459 | - grid-template-columns: repeat(2, 1fr) !important; | 258 | + grid-template-columns: 1fr; |
| 460 | - } | ||
| 461 | } | 259 | } |
| 462 | } | 260 | } |
| 463 | </style> | 261 | </style> |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -3,82 +3,62 @@ | ... | @@ -3,82 +3,62 @@ |
| 3 | <div class="login-form-container"> | 3 | <div class="login-form-container"> |
| 4 | <!-- Logo区域 --> | 4 | <!-- Logo区域 --> |
| 5 | <div class="login-header"> | 5 | <div class="login-header"> |
| 6 | - <img src="/logo.png" alt="Logo" class="login-logo" /> | 6 | + <div class="logo-container"> |
| 7 | + <div class="logo-icon">🍎</div> | ||
| 7 | <h1 class="login-title">Apple经销商ERP系统</h1> | 8 | <h1 class="login-title">Apple经销商ERP系统</h1> |
| 9 | + </div> | ||
| 8 | <p class="login-subtitle">欢迎登录</p> | 10 | <p class="login-subtitle">欢迎登录</p> |
| 9 | </div> | 11 | </div> |
| 10 | 12 | ||
| 11 | <!-- 登录表单 --> | 13 | <!-- 登录表单 --> |
| 12 | - <el-form | 14 | + <form class="login-form" @submit.prevent="handleLogin"> |
| 13 | - ref="loginFormRef" | 15 | + <div class="form-group"> |
| 14 | - :model="loginForm" | 16 | + <div class="input-wrapper"> |
| 15 | - :rules="loginRules" | 17 | + <span class="input-icon">👤</span> |
| 16 | - class="login-form" | 18 | + <input |
| 17 | - @keyup.enter="handleLogin" | ||
| 18 | - > | ||
| 19 | - <el-form-item prop="username"> | ||
| 20 | - <el-input | ||
| 21 | v-model="loginForm.username" | 19 | v-model="loginForm.username" |
| 20 | + type="text" | ||
| 22 | placeholder="请输入用户名" | 21 | placeholder="请输入用户名" |
| 23 | - size="large" | 22 | + class="form-input" |
| 24 | - prefix-icon="User" | 23 | + required |
| 25 | - clearable | ||
| 26 | /> | 24 | /> |
| 27 | - </el-form-item> | 25 | + </div> |
| 26 | + </div> | ||
| 28 | 27 | ||
| 29 | - <el-form-item prop="password"> | 28 | + <div class="form-group"> |
| 30 | - <el-input | 29 | + <div class="input-wrapper"> |
| 30 | + <span class="input-icon">🔒</span> | ||
| 31 | + <input | ||
| 31 | v-model="loginForm.password" | 32 | v-model="loginForm.password" |
| 32 | type="password" | 33 | type="password" |
| 33 | placeholder="请输入密码" | 34 | placeholder="请输入密码" |
| 34 | - size="large" | 35 | + class="form-input" |
| 35 | - prefix-icon="Lock" | 36 | + required |
| 36 | - show-password | ||
| 37 | - clearable | ||
| 38 | - /> | ||
| 39 | - </el-form-item> | ||
| 40 | - | ||
| 41 | - <el-form-item prop="captcha" v-if="showCaptcha"> | ||
| 42 | - <div class="captcha-container"> | ||
| 43 | - <el-input | ||
| 44 | - v-model="loginForm.captcha" | ||
| 45 | - placeholder="请输入验证码" | ||
| 46 | - size="large" | ||
| 47 | - prefix-icon="Picture" | ||
| 48 | - clearable | ||
| 49 | - /> | ||
| 50 | - <img | ||
| 51 | - :src="captchaImage" | ||
| 52 | - alt="验证码" | ||
| 53 | - class="captcha-image" | ||
| 54 | - @click="refreshCaptcha" | ||
| 55 | /> | 37 | /> |
| 56 | </div> | 38 | </div> |
| 57 | - </el-form-item> | 39 | + </div> |
| 58 | 40 | ||
| 59 | - <el-form-item> | 41 | + <div class="form-group"> |
| 60 | - <div class="login-options"> | 42 | + <label class="checkbox-label"> |
| 61 | - <el-checkbox v-model="loginForm.rememberMe"> | 43 | + <input |
| 44 | + v-model="loginForm.rememberMe" | ||
| 45 | + type="checkbox" | ||
| 46 | + class="checkbox-input" | ||
| 47 | + /> | ||
| 62 | 记住我 | 48 | 记住我 |
| 63 | - </el-checkbox> | 49 | + </label> |
| 64 | - <el-link type="primary" class="forgot-password"> | 50 | + <a href="#" class="forgot-password">忘记密码?</a> |
| 65 | - 忘记密码? | ||
| 66 | - </el-link> | ||
| 67 | </div> | 51 | </div> |
| 68 | - </el-form-item> | ||
| 69 | 52 | ||
| 70 | - <el-form-item> | 53 | + <button |
| 71 | - <el-button | 54 | + type="submit" |
| 72 | - type="primary" | ||
| 73 | - size="large" | ||
| 74 | class="login-btn" | 55 | class="login-btn" |
| 75 | - :loading="loading" | 56 | + :disabled="loading" |
| 76 | - @click="handleLogin" | ||
| 77 | > | 57 | > |
| 58 | + <span class="btn-icon">{{ loading ? '⏳' : '🚀' }}</span> | ||
| 78 | {{ loading ? '登录中...' : '登录' }} | 59 | {{ loading ? '登录中...' : '登录' }} |
| 79 | - </el-button> | 60 | + </button> |
| 80 | - </el-form-item> | 61 | + </form> |
| 81 | - </el-form> | ||
| 82 | </div> | 62 | </div> |
| 83 | 63 | ||
| 84 | <!-- 背景装饰 --> | 64 | <!-- 背景装饰 --> |
| ... | @@ -91,174 +71,291 @@ | ... | @@ -91,174 +71,291 @@ |
| 91 | </template> | 71 | </template> |
| 92 | 72 | ||
| 93 | <script setup lang="ts"> | 73 | <script setup lang="ts"> |
| 94 | -import { ref, reactive, onMounted } from 'vue' | 74 | +import { ref, reactive } from 'vue' |
| 95 | -import { useRouter, useRoute } from 'vue-router' | 75 | +import { useRouter } from 'vue-router' |
| 96 | -import { ElMessage, type FormInstance, type FormRules } from 'element-plus' | 76 | +import axios from 'axios' |
| 97 | -import { useUserStore } from '@/stores/modules/user' | ||
| 98 | -import type { LoginForm } from '@/types' | ||
| 99 | 77 | ||
| 100 | const router = useRouter() | 78 | const router = useRouter() |
| 101 | -const route = useRoute() | ||
| 102 | -const userStore = useUserStore() | ||
| 103 | - | ||
| 104 | -// 表单引用 | ||
| 105 | -const loginFormRef = ref<FormInstance>() | ||
| 106 | 79 | ||
| 107 | // 加载状态 | 80 | // 加载状态 |
| 108 | const loading = ref(false) | 81 | const loading = ref(false) |
| 109 | 82 | ||
| 110 | -// 验证码相关 | ||
| 111 | -const showCaptcha = ref(false) | ||
| 112 | -const captchaImage = ref('') | ||
| 113 | - | ||
| 114 | // 登录表单数据 | 83 | // 登录表单数据 |
| 115 | -const loginForm = reactive<LoginForm>({ | 84 | +const loginForm = reactive({ |
| 116 | username: '', | 85 | username: '', |
| 117 | password: '', | 86 | password: '', |
| 118 | - captcha: '', | ||
| 119 | rememberMe: false | 87 | rememberMe: false |
| 120 | }) | 88 | }) |
| 121 | 89 | ||
| 122 | -// 表单验证规则 | ||
| 123 | -const loginRules: FormRules = { | ||
| 124 | - username: [ | ||
| 125 | - { required: true, message: '请输入用户名', trigger: 'blur' }, | ||
| 126 | - { min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' } | ||
| 127 | - ], | ||
| 128 | - password: [ | ||
| 129 | - { required: true, message: '请输入密码', trigger: 'blur' }, | ||
| 130 | - { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' } | ||
| 131 | - ], | ||
| 132 | - captcha: [ | ||
| 133 | - { required: true, message: '请输入验证码', trigger: 'blur' } | ||
| 134 | - ] | ||
| 135 | -} | ||
| 136 | - | ||
| 137 | // 处理登录 | 90 | // 处理登录 |
| 138 | const handleLogin = async () => { | 91 | const handleLogin = async () => { |
| 139 | - if (!loginFormRef.value) return | 92 | + if (!loginForm.username || !loginForm.password) { |
| 93 | + alert('请输入用户名和密码') | ||
| 94 | + return | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + loading.value = true | ||
| 140 | 98 | ||
| 141 | try { | 99 | try { |
| 142 | - const valid = await loginFormRef.value.validate() | 100 | + // 调用后端登录API |
| 143 | - if (!valid) return | 101 | + const response = await axios.post('http://localhost:8083/api/auth/login', { |
| 102 | + username: loginForm.username, | ||
| 103 | + password: loginForm.password, | ||
| 104 | + rememberMe: loginForm.rememberMe | ||
| 105 | + }) | ||
| 106 | + | ||
| 107 | + // 登录成功 | ||
| 108 | + console.log('登录成功:', response) | ||
| 109 | + console.log('响应数据结构:', { | ||
| 110 | + data: response.data, | ||
| 111 | + status: response.status, | ||
| 112 | + headers: response.headers | ||
| 113 | + }) | ||
| 114 | + | ||
| 115 | + // 检查响应数据结构并保存token | ||
| 116 | + let token = null | ||
| 117 | + let userInfo = null | ||
| 118 | + | ||
| 119 | + // 尝试不同的数据结构 | ||
| 120 | + if (response.data && response.data.token) { | ||
| 121 | + token = response.data.token | ||
| 122 | + userInfo = response.data.userInfo | ||
| 123 | + console.log('从response.data获取token:', token) | ||
| 124 | + } else if (response.data && response.data.data && response.data.data.token) { | ||
| 125 | + token = response.data.data.token | ||
| 126 | + userInfo = response.data.data.userInfo | ||
| 127 | + console.log('从response.data.data获取token:', token) | ||
| 128 | + } else if (response.data && response.data.result && response.data.result.token) { | ||
| 129 | + token = response.data.result.token | ||
| 130 | + userInfo = response.data.result.userInfo | ||
| 131 | + console.log('从response.data.result获取token:', token) | ||
| 132 | + } else { | ||
| 133 | + console.error('未找到token,响应结构:', response.data) | ||
| 134 | + } | ||
| 144 | 135 | ||
| 145 | - loading.value = true | 136 | + if (token) { |
| 137 | + localStorage.setItem('token', token) | ||
| 138 | + console.log('Token已保存到localStorage:', token) | ||
| 146 | 139 | ||
| 147 | - await userStore.login(loginForm) | 140 | + if (userInfo) { |
| 141 | + localStorage.setItem('userInfo', JSON.stringify(userInfo)) | ||
| 142 | + console.log('用户信息已保存:', userInfo) | ||
| 143 | + } | ||
| 148 | 144 | ||
| 149 | - // 登录成功,跳转到目标页面或首页 | 145 | + // 验证保存是否成功 |
| 150 | - const redirect = route.query.redirect as string | 146 | + const savedToken = localStorage.getItem('token') |
| 151 | - router.push(redirect || '/') | 147 | + console.log('验证保存的token:', savedToken) |
| 152 | - } catch (error) { | 148 | + } else { |
| 153 | - console.error('登录失败:', error) | 149 | + console.error('无法获取token,请检查后端响应格式') |
| 154 | - // 显示验证码 | 150 | + alert('登录成功,但无法获取认证信息') |
| 155 | - showCaptcha.value = true | ||
| 156 | - await refreshCaptcha() | ||
| 157 | - } finally { | ||
| 158 | - loading.value = false | ||
| 159 | } | 151 | } |
| 160 | -} | ||
| 161 | 152 | ||
| 162 | -// 刷新验证码 | 153 | + alert('登录成功!') |
| 163 | -const refreshCaptcha = async () => { | 154 | + |
| 155 | + // 跳转到首页 | ||
| 156 | + console.log('准备跳转到首页...') | ||
| 157 | + | ||
| 158 | + // 添加短暂延迟,确保localStorage保存完成 | ||
| 159 | + await new Promise(resolve => setTimeout(resolve, 100)) | ||
| 160 | + | ||
| 161 | + // 再次验证token是否存在 | ||
| 162 | + const finalToken = localStorage.getItem('token') | ||
| 163 | + console.log('跳转前最终验证token:', finalToken) | ||
| 164 | + | ||
| 165 | + if (finalToken) { | ||
| 164 | try { | 166 | try { |
| 165 | - // 这里应该调用获取验证码的API | 167 | + await router.push('/main/dashboard') |
| 166 | - // const response = await getCaptchaApi() | 168 | + console.log('跳转成功') |
| 167 | - // captchaImage.value = response.captchaImage | ||
| 168 | - captchaImage.value = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' | ||
| 169 | } catch (error) { | 169 | } catch (error) { |
| 170 | - console.error('获取验证码失败:', error) | 170 | + console.error('跳转失败:', error) |
| 171 | + // 如果路由跳转失败,使用window.location | ||
| 172 | + console.log('使用window.location跳转') | ||
| 173 | + window.location.href = '/main/dashboard' | ||
| 171 | } | 174 | } |
| 172 | -} | 175 | + } else { |
| 176 | + console.error('Token仍然不存在,无法跳转') | ||
| 177 | + alert('认证信息保存失败,请重试') | ||
| 178 | + } | ||
| 179 | + } catch (error: any) { | ||
| 180 | + console.error('登录失败:', error) | ||
| 173 | 181 | ||
| 174 | -// 组件挂载时初始化 | 182 | + // 显示具体错误信息 |
| 175 | -onMounted(() => { | 183 | + const errorMessage = error.response?.data?.message || error.message || '登录失败,请重试' |
| 176 | - // 如果已经登录,直接跳转到首页 | 184 | + alert(errorMessage) |
| 177 | - if (userStore.isLoggedIn) { | 185 | + } finally { |
| 178 | - router.push('/') | 186 | + loading.value = false |
| 179 | } | 187 | } |
| 180 | -}) | 188 | +} |
| 181 | </script> | 189 | </script> |
| 182 | 190 | ||
| 183 | -<style lang="scss" scoped> | 191 | +<style scoped> |
| 184 | .login-container { | 192 | .login-container { |
| 185 | - position: relative; | 193 | + position: fixed; |
| 186 | - width: 100%; | 194 | + top: 0; |
| 195 | + left: 0; | ||
| 196 | + width: 100vw; | ||
| 187 | height: 100vh; | 197 | height: 100vh; |
| 188 | - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | 198 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); |
| 189 | display: flex; | 199 | display: flex; |
| 190 | align-items: center; | 200 | align-items: center; |
| 191 | justify-content: center; | 201 | justify-content: center; |
| 192 | overflow: hidden; | 202 | overflow: hidden; |
| 203 | + margin: 0; | ||
| 204 | + padding: 0; | ||
| 193 | } | 205 | } |
| 194 | 206 | ||
| 195 | .login-form-container { | 207 | .login-form-container { |
| 196 | position: relative; | 208 | position: relative; |
| 197 | z-index: 2; | 209 | z-index: 2; |
| 198 | - width: 400px; | 210 | + width: 420px; |
| 199 | padding: 40px; | 211 | padding: 40px; |
| 200 | background: rgba(255, 255, 255, 0.95); | 212 | background: rgba(255, 255, 255, 0.95); |
| 201 | - border-radius: 12px; | 213 | + border-radius: 20px; |
| 202 | - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | 214 | + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); |
| 203 | - backdrop-filter: blur(10px); | 215 | + backdrop-filter: blur(20px); |
| 216 | + border: 1px solid rgba(255, 255, 255, 0.2); | ||
| 204 | } | 217 | } |
| 205 | 218 | ||
| 206 | .login-header { | 219 | .login-header { |
| 207 | text-align: center; | 220 | text-align: center; |
| 208 | margin-bottom: 32px; | 221 | margin-bottom: 32px; |
| 222 | +} | ||
| 209 | 223 | ||
| 210 | - .login-logo { | 224 | +.logo-container { |
| 211 | - width: 64px; | 225 | + display: flex; |
| 212 | - height: 64px; | 226 | + align-items: center; |
| 227 | + justify-content: center; | ||
| 228 | + gap: 12px; | ||
| 213 | margin-bottom: 16px; | 229 | margin-bottom: 16px; |
| 214 | - } | 230 | +} |
| 215 | 231 | ||
| 216 | - .login-title { | 232 | +.logo-icon { |
| 217 | - margin: 0 0 8px 0; | 233 | + font-size: 32px; |
| 218 | - font-size: 24px; | 234 | + animation: bounce 2s infinite; |
| 219 | - font-weight: 600; | 235 | +} |
| 220 | - color: var(--el-text-color-primary); | 236 | + |
| 221 | - } | 237 | +.login-title { |
| 238 | + margin: 0; | ||
| 239 | + font-size: 22px; | ||
| 240 | + font-weight: 700; | ||
| 241 | + color: #333; | ||
| 242 | + background: linear-gradient(135deg, #667eea, #764ba2); | ||
| 243 | + -webkit-background-clip: text; | ||
| 244 | + -webkit-text-fill-color: transparent; | ||
| 245 | + background-clip: text; | ||
| 246 | +} | ||
| 222 | 247 | ||
| 223 | - .login-subtitle { | 248 | +.login-subtitle { |
| 224 | margin: 0; | 249 | margin: 0; |
| 225 | font-size: 14px; | 250 | font-size: 14px; |
| 226 | - color: var(--el-text-color-secondary); | 251 | + color: #666; |
| 227 | - } | 252 | + font-weight: 500; |
| 228 | } | 253 | } |
| 229 | 254 | ||
| 230 | .login-form { | 255 | .login-form { |
| 231 | - .captcha-container { | 256 | + width: 100%; |
| 257 | +} | ||
| 258 | + | ||
| 259 | +.form-group { | ||
| 260 | + margin-bottom: 20px; | ||
| 261 | +} | ||
| 262 | + | ||
| 263 | +.input-wrapper { | ||
| 264 | + position: relative; | ||
| 232 | display: flex; | 265 | display: flex; |
| 233 | align-items: center; | 266 | align-items: center; |
| 234 | - gap: 12px; | 267 | +} |
| 235 | 268 | ||
| 236 | - .captcha-image { | 269 | +.input-icon { |
| 237 | - width: 100px; | 270 | + position: absolute; |
| 271 | + left: 12px; | ||
| 272 | + font-size: 16px; | ||
| 273 | + color: #667eea; | ||
| 274 | + z-index: 1; | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +.form-input { | ||
| 278 | + width: 100%; | ||
| 238 | height: 40px; | 279 | height: 40px; |
| 239 | - border-radius: 4px; | 280 | + padding: 0 12px 0 40px; |
| 281 | + border: 2px solid #e1e5e9; | ||
| 282 | + border-radius: 10px; | ||
| 283 | + font-size: 14px; | ||
| 284 | + box-sizing: border-box; | ||
| 285 | + transition: all 0.3s ease; | ||
| 286 | + background: rgba(255, 255, 255, 0.9); | ||
| 287 | +} | ||
| 288 | + | ||
| 289 | +.form-input:focus { | ||
| 290 | + outline: none; | ||
| 291 | + border-color: #667eea; | ||
| 292 | + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | ||
| 293 | + background: white; | ||
| 294 | + transform: translateY(-1px); | ||
| 295 | +} | ||
| 296 | + | ||
| 297 | +.checkbox-label { | ||
| 298 | + display: flex; | ||
| 299 | + align-items: center; | ||
| 300 | + font-size: 12px; | ||
| 301 | + color: #666; | ||
| 240 | cursor: pointer; | 302 | cursor: pointer; |
| 241 | - border: 1px solid var(--el-border-color); | 303 | +} |
| 242 | - } | 304 | + |
| 243 | - } | 305 | +.checkbox-input { |
| 306 | + margin-right: 6px; | ||
| 307 | +} | ||
| 244 | 308 | ||
| 245 | - .login-options { | 309 | +.forgot-password { |
| 310 | + color: #667eea; | ||
| 311 | + text-decoration: none; | ||
| 312 | + font-size: 12px; | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +.login-options { | ||
| 246 | display: flex; | 316 | display: flex; |
| 247 | align-items: center; | 317 | align-items: center; |
| 248 | justify-content: space-between; | 318 | justify-content: space-between; |
| 249 | width: 100%; | 319 | width: 100%; |
| 320 | + margin-bottom: 20px; | ||
| 321 | +} | ||
| 250 | 322 | ||
| 251 | - .forgot-password { | 323 | +.login-btn { |
| 252 | - font-size: 14px; | ||
| 253 | - } | ||
| 254 | - } | ||
| 255 | - | ||
| 256 | - .login-btn { | ||
| 257 | width: 100%; | 324 | width: 100%; |
| 258 | height: 44px; | 325 | height: 44px; |
| 326 | + background: linear-gradient(135deg, #667eea, #764ba2); | ||
| 327 | + color: white; | ||
| 328 | + border: none; | ||
| 329 | + border-radius: 12px; | ||
| 259 | font-size: 16px; | 330 | font-size: 16px; |
| 260 | - font-weight: 500; | 331 | + font-weight: 600; |
| 261 | - } | 332 | + cursor: pointer; |
| 333 | + transition: all 0.3s ease; | ||
| 334 | + display: flex; | ||
| 335 | + align-items: center; | ||
| 336 | + justify-content: center; | ||
| 337 | + gap: 8px; | ||
| 338 | + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); | ||
| 339 | +} | ||
| 340 | + | ||
| 341 | +.login-btn:hover:not(:disabled) { | ||
| 342 | + transform: translateY(-2px); | ||
| 343 | + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); | ||
| 344 | +} | ||
| 345 | + | ||
| 346 | +.login-btn:active:not(:disabled) { | ||
| 347 | + transform: translateY(0); | ||
| 348 | +} | ||
| 349 | + | ||
| 350 | +.login-btn:disabled { | ||
| 351 | + background: #ccc; | ||
| 352 | + cursor: not-allowed; | ||
| 353 | + transform: none; | ||
| 354 | + box-shadow: none; | ||
| 355 | +} | ||
| 356 | + | ||
| 357 | +.btn-icon { | ||
| 358 | + font-size: 18px; | ||
| 262 | } | 359 | } |
| 263 | 360 | ||
| 264 | .login-bg { | 361 | .login-bg { |
| ... | @@ -268,37 +365,37 @@ onMounted(() => { | ... | @@ -268,37 +365,37 @@ onMounted(() => { |
| 268 | width: 100%; | 365 | width: 100%; |
| 269 | height: 100%; | 366 | height: 100%; |
| 270 | z-index: 1; | 367 | z-index: 1; |
| 368 | +} | ||
| 271 | 369 | ||
| 272 | - .bg-circle { | 370 | +.bg-circle { |
| 273 | position: absolute; | 371 | position: absolute; |
| 274 | border-radius: 50%; | 372 | border-radius: 50%; |
| 275 | background: rgba(255, 255, 255, 0.1); | 373 | background: rgba(255, 255, 255, 0.1); |
| 276 | animation: float 6s ease-in-out infinite; | 374 | animation: float 6s ease-in-out infinite; |
| 375 | +} | ||
| 277 | 376 | ||
| 278 | - &.bg-circle-1 { | 377 | +.bg-circle-1 { |
| 279 | width: 200px; | 378 | width: 200px; |
| 280 | height: 200px; | 379 | height: 200px; |
| 281 | top: 10%; | 380 | top: 10%; |
| 282 | left: 10%; | 381 | left: 10%; |
| 283 | animation-delay: 0s; | 382 | animation-delay: 0s; |
| 284 | - } | 383 | +} |
| 285 | 384 | ||
| 286 | - &.bg-circle-2 { | 385 | +.bg-circle-2 { |
| 287 | width: 150px; | 386 | width: 150px; |
| 288 | height: 150px; | 387 | height: 150px; |
| 289 | top: 60%; | 388 | top: 60%; |
| 290 | right: 10%; | 389 | right: 10%; |
| 291 | animation-delay: 2s; | 390 | animation-delay: 2s; |
| 292 | - } | 391 | +} |
| 293 | 392 | ||
| 294 | - &.bg-circle-3 { | 393 | +.bg-circle-3 { |
| 295 | width: 100px; | 394 | width: 100px; |
| 296 | height: 100px; | 395 | height: 100px; |
| 297 | bottom: 20%; | 396 | bottom: 20%; |
| 298 | left: 20%; | 397 | left: 20%; |
| 299 | animation-delay: 4s; | 398 | animation-delay: 4s; |
| 300 | - } | ||
| 301 | - } | ||
| 302 | } | 399 | } |
| 303 | 400 | ||
| 304 | @keyframes float { | 401 | @keyframes float { |
| ... | @@ -310,17 +407,39 @@ onMounted(() => { | ... | @@ -310,17 +407,39 @@ onMounted(() => { |
| 310 | } | 407 | } |
| 311 | } | 408 | } |
| 312 | 409 | ||
| 313 | -// 响应式设计 | 410 | +@keyframes bounce { |
| 411 | + 0%, 20%, 50%, 80%, 100% { | ||
| 412 | + transform: translateY(0); | ||
| 413 | + } | ||
| 414 | + 40% { | ||
| 415 | + transform: translateY(-10px); | ||
| 416 | + } | ||
| 417 | + 60% { | ||
| 418 | + transform: translateY(-5px); | ||
| 419 | + } | ||
| 420 | +} | ||
| 421 | + | ||
| 422 | +@keyframes pulse { | ||
| 423 | + 0% { | ||
| 424 | + transform: scale(1); | ||
| 425 | + } | ||
| 426 | + 50% { | ||
| 427 | + transform: scale(1.05); | ||
| 428 | + } | ||
| 429 | + 100% { | ||
| 430 | + transform: scale(1); | ||
| 431 | + } | ||
| 432 | +} | ||
| 433 | + | ||
| 434 | +/* 响应式设计 */ | ||
| 314 | @media (max-width: 768px) { | 435 | @media (max-width: 768px) { |
| 315 | .login-form-container { | 436 | .login-form-container { |
| 316 | width: 90%; | 437 | width: 90%; |
| 317 | padding: 24px; | 438 | padding: 24px; |
| 318 | } | 439 | } |
| 319 | 440 | ||
| 320 | - .login-header { | ||
| 321 | .login-title { | 441 | .login-title { |
| 322 | font-size: 20px; | 442 | font-size: 20px; |
| 323 | } | 443 | } |
| 324 | - } | ||
| 325 | } | 444 | } |
| 326 | </style> | 445 | </style> |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
frontend/src/views/sys/dict-item/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="dict-item-management"> | ||
| 3 | + <!-- 搜索区域 --> | ||
| 4 | + <div class="search-section"> | ||
| 5 | + <div class="search-row"> | ||
| 6 | + <div class="search-item"> | ||
| 7 | + <label>字典类型:</label> | ||
| 8 | + <select v-model="searchForm.dictTypeId" class="search-select"> | ||
| 9 | + <option value="">所有类型</option> | ||
| 10 | + <option v-for="dictType in dictTypeOptions" :key="dictType.dictTypeId" :value="dictType.dictTypeId"> | ||
| 11 | + {{ dictType.dictName }} | ||
| 12 | + </option> | ||
| 13 | + </select> | ||
| 14 | + </div> | ||
| 15 | + <div class="search-item"> | ||
| 16 | + <label>字典标签:</label> | ||
| 17 | + <input v-model="searchForm.dictLabel" type="text" placeholder="请输入字典标签" class="search-input"> | ||
| 18 | + </div> | ||
| 19 | + <div class="search-item"> | ||
| 20 | + <label>字典值:</label> | ||
| 21 | + <input v-model="searchForm.dictValue" type="text" placeholder="请输入字典值" class="search-input"> | ||
| 22 | + </div> | ||
| 23 | + <div class="search-actions"> | ||
| 24 | + <button @click="handleSearch" class="search-btn">🔍 搜索</button> | ||
| 25 | + <button @click="handleReset" class="reset-btn">🔄 重置</button> | ||
| 26 | + </div> | ||
| 27 | + </div> | ||
| 28 | + </div> | ||
| 29 | + | ||
| 30 | + <!-- 操作区域 --> | ||
| 31 | + <div class="action-section"> | ||
| 32 | + <div class="action-buttons"> | ||
| 33 | + <button @click="handleAdd" class="action-btn primary">✨ 新增</button> | ||
| 34 | + <button @click="handleBatchDelete" class="action-btn danger" :disabled="selectedItems.length === 0">🗑️ 删除</button> | ||
| 35 | + </div> | ||
| 36 | + </div> | ||
| 37 | + | ||
| 38 | + <!-- 表格区域 --> | ||
| 39 | + <div class="table-section"> | ||
| 40 | + <div class="table-header"> | ||
| 41 | + <div class="table-controls"> | ||
| 42 | + <button @click="handleTableSearch" class="control-btn" title="搜索">🔍</button> | ||
| 43 | + <button @click="handleTableRefresh" class="control-btn" title="刷新">🔄</button> | ||
| 44 | + <button @click="handleTableExport" class="control-btn" title="导出">📋</button> | ||
| 45 | + <button @click="handleTableViewToggle" class="control-btn" title="视图切换">⊞</button> | ||
| 46 | + </div> | ||
| 47 | + </div> | ||
| 48 | + | ||
| 49 | + <div class="table-container"> | ||
| 50 | + <table class="data-table"> | ||
| 51 | + <thead> | ||
| 52 | + <tr> | ||
| 53 | + <th class="checkbox-col"> | ||
| 54 | + <input type="checkbox" v-model="selectAll" @change="handleSelectAll"> | ||
| 55 | + </th> | ||
| 56 | + <th>字典项ID</th> | ||
| 57 | + <th>字典类型</th> | ||
| 58 | + <th>字典标签</th> | ||
| 59 | + <th>字典值</th> | ||
| 60 | + <th>排序</th> | ||
| 61 | + <th>备注</th> | ||
| 62 | + <th>创建时间</th> | ||
| 63 | + <th>操作</th> | ||
| 64 | + </tr> | ||
| 65 | + </thead> | ||
| 66 | + <tbody> | ||
| 67 | + <tr v-for="item in dictItemList" :key="item.dictItemId" :class="{ selected: selectedItems.includes(item.dictItemId) }"> | ||
| 68 | + <td class="checkbox-col"> | ||
| 69 | + <input type="checkbox" v-model="selectedItems" :value="item.dictItemId" class="table-checkbox"> | ||
| 70 | + </td> | ||
| 71 | + <td>{{ item.dictItemId }}</td> | ||
| 72 | + <td>{{ item.dictType }}</td> | ||
| 73 | + <td>{{ item.dictLabel }}</td> | ||
| 74 | + <td>{{ item.dictValue }}</td> | ||
| 75 | + <td>{{ item.sort }}</td> | ||
| 76 | + <td>{{ item.remark || '-' }}</td> | ||
| 77 | + <td>{{ formatTime(item.createTime) }}</td> | ||
| 78 | + <td> | ||
| 79 | + <button @click="handleEdit(item)" class="table-btn edit">✏️ 编辑</button> | ||
| 80 | + <button @click="handleDelete(item)" class="table-btn delete">🗑️ 删除</button> | ||
| 81 | + </td> | ||
| 82 | + </tr> | ||
| 83 | + </tbody> | ||
| 84 | + </table> | ||
| 85 | + </div> | ||
| 86 | + | ||
| 87 | + <!-- 分页 --> | ||
| 88 | + <div class="table-footer"> | ||
| 89 | + <div class="pagination-left"> | ||
| 90 | + <div class="pagination-info"> | ||
| 91 | + 共 {{ total }} 条记录,第 {{ currentPage }} / {{ totalPages }} 页 | ||
| 92 | + </div> | ||
| 93 | + <div class="page-size-selector"> | ||
| 94 | + <label>每页显示:</label> | ||
| 95 | + <select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> | ||
| 96 | + <option value="10">10</option> | ||
| 97 | + <option value="20">20</option> | ||
| 98 | + <option value="50">50</option> | ||
| 99 | + <option value="100">100</option> | ||
| 100 | + </select> | ||
| 101 | + </div> | ||
| 102 | + </div> | ||
| 103 | + <div class="pagination-container"> | ||
| 104 | + <button @click="goToPage(currentPage - 1)" :disabled="currentPage <= 1" class="pagination-btn">上一页</button> | ||
| 105 | + <div class="pagination-pages"> | ||
| 106 | + <button | ||
| 107 | + v-for="page in getPageNumbers()" | ||
| 108 | + :key="page" | ||
| 109 | + @click="goToPage(page)" | ||
| 110 | + :class="['page-number', { active: page === currentPage }]" | ||
| 111 | + > | ||
| 112 | + {{ page }} | ||
| 113 | + </button> | ||
| 114 | + </div> | ||
| 115 | + <button @click="goToPage(currentPage + 1)" :disabled="currentPage >= totalPages" class="pagination-btn">下一页</button> | ||
| 116 | + </div> | ||
| 117 | + </div> | ||
| 118 | + </div> | ||
| 119 | + | ||
| 120 | + <!-- 字典项对话框 --> | ||
| 121 | + <div v-if="showDictItemDialog" class="dialog-overlay" @click="closeDictItemDialog"> | ||
| 122 | + <div class="dialog-content" @click.stop> | ||
| 123 | + <div class="dialog-header"> | ||
| 124 | + <h3>{{ isEdit ? '编辑字典项' : '新增字典项' }}</h3> | ||
| 125 | + <button @click="closeDictItemDialog" class="dialog-close">×</button> | ||
| 126 | + </div> | ||
| 127 | + <form @submit.prevent="handleSubmit" class="dialog-form"> | ||
| 128 | + <div class="form-row"> | ||
| 129 | + <div class="form-group"> | ||
| 130 | + <label class="required-label">字典类型 <span class="required-asterisk">*</span></label> | ||
| 131 | + <select v-model="dictItemForm.dictTypeId" class="form-select" required> | ||
| 132 | + <option value="">请选择字典类型</option> | ||
| 133 | + <option v-for="dictType in dictTypeOptions" :key="dictType.dictTypeId" :value="dictType.dictTypeId"> | ||
| 134 | + {{ dictType.dictName }} | ||
| 135 | + </option> | ||
| 136 | + </select> | ||
| 137 | + <div v-if="fieldErrors.dictTypeId" class="field-error">{{ fieldErrors.dictTypeId }}</div> | ||
| 138 | + </div> | ||
| 139 | + <div class="form-group"> | ||
| 140 | + <label class="required-label">字典标签 <span class="required-asterisk">*</span></label> | ||
| 141 | + <input | ||
| 142 | + v-model="dictItemForm.dictLabel" | ||
| 143 | + type="text" | ||
| 144 | + placeholder="请输入字典标签" | ||
| 145 | + class="form-input" | ||
| 146 | + required | ||
| 147 | + > | ||
| 148 | + <div v-if="fieldErrors.dictLabel" class="field-error">{{ fieldErrors.dictLabel }}</div> | ||
| 149 | + </div> | ||
| 150 | + </div> | ||
| 151 | + <div class="form-row"> | ||
| 152 | + <div class="form-group"> | ||
| 153 | + <label class="required-label">字典值 <span class="required-asterisk">*</span></label> | ||
| 154 | + <input | ||
| 155 | + v-model="dictItemForm.dictValue" | ||
| 156 | + type="text" | ||
| 157 | + placeholder="请输入字典值" | ||
| 158 | + class="form-input" | ||
| 159 | + required | ||
| 160 | + > | ||
| 161 | + <div v-if="fieldErrors.dictValue" class="field-error">{{ fieldErrors.dictValue }}</div> | ||
| 162 | + </div> | ||
| 163 | + <div class="form-group"> | ||
| 164 | + <label>排序</label> | ||
| 165 | + <input | ||
| 166 | + v-model.number="dictItemForm.sort" | ||
| 167 | + type="number" | ||
| 168 | + placeholder="请输入排序号" | ||
| 169 | + class="form-input" | ||
| 170 | + > | ||
| 171 | + <div v-if="fieldErrors.sort" class="field-error">{{ fieldErrors.sort }}</div> | ||
| 172 | + </div> | ||
| 173 | + </div> | ||
| 174 | + <div class="form-row"> | ||
| 175 | + <div class="form-group full-width"> | ||
| 176 | + <label>备注</label> | ||
| 177 | + <input | ||
| 178 | + v-model="dictItemForm.remark" | ||
| 179 | + type="text" | ||
| 180 | + placeholder="请输入备注" | ||
| 181 | + class="form-input" | ||
| 182 | + > | ||
| 183 | + <div v-if="fieldErrors.remark" class="field-error">{{ fieldErrors.remark }}</div> | ||
| 184 | + </div> | ||
| 185 | + </div> | ||
| 186 | + <div class="dialog-actions"> | ||
| 187 | + <button type="button" @click="closeDictItemDialog" class="btn-cancel">取消</button> | ||
| 188 | + <button type="submit" class="btn-confirm" :disabled="formLoading"> | ||
| 189 | + {{ formLoading ? '保存中...' : '确定' }} | ||
| 190 | + </button> | ||
| 191 | + </div> | ||
| 192 | + </form> | ||
| 193 | + </div> | ||
| 194 | + </div> | ||
| 195 | + </div> | ||
| 196 | +</template> | ||
| 197 | + | ||
| 198 | +<script setup lang="ts"> | ||
| 199 | +import { ref, reactive, onMounted, computed } from 'vue' | ||
| 200 | +import { useRoute } from 'vue-router' | ||
| 201 | +import { dictItemApi, type DictItem, type DictItemSearchParams, type DictItemAddReq, type DictItemUpdateReq } from '../../../api/dictItem' | ||
| 202 | +import { dictApi, type DictType } from '../../../api/dict' | ||
| 203 | + | ||
| 204 | +// 路由 | ||
| 205 | +const route = useRoute() | ||
| 206 | + | ||
| 207 | +// 响应式数据 | ||
| 208 | +const dictItemList = ref<DictItem[]>([]) | ||
| 209 | +const dictTypeOptions = ref<DictType[]>([]) | ||
| 210 | +const loading = ref(false) | ||
| 211 | +const formLoading = ref(false) | ||
| 212 | +const showDictItemDialog = ref(false) | ||
| 213 | +const isEdit = ref(false) | ||
| 214 | + | ||
| 215 | +// 分页数据 | ||
| 216 | +const currentPage = ref(1) | ||
| 217 | +const pageSize = ref(10) | ||
| 218 | +const total = ref(0) | ||
| 219 | +const totalPages = computed(() => Math.ceil(total.value / pageSize.value)) | ||
| 220 | + | ||
| 221 | +// 获取页码数组 | ||
| 222 | +const getPageNumbers = () => { | ||
| 223 | + const pages = [] | ||
| 224 | + const maxVisible = 5 // 最多显示5个页码 | ||
| 225 | + const start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2)) | ||
| 226 | + const end = Math.min(totalPages.value, start + maxVisible - 1) | ||
| 227 | + | ||
| 228 | + for (let i = start; i <= end; i++) { | ||
| 229 | + pages.push(i) | ||
| 230 | + } | ||
| 231 | + return pages | ||
| 232 | +} | ||
| 233 | + | ||
| 234 | +// 搜索表单 | ||
| 235 | +const searchForm = reactive<DictItemSearchParams>({ | ||
| 236 | + pageNum: 1, | ||
| 237 | + pageSize: 10, | ||
| 238 | + dictTypeId: undefined, | ||
| 239 | + dictLabel: '', | ||
| 240 | + dictValue: '' | ||
| 241 | +}) | ||
| 242 | + | ||
| 243 | +// 字典项表单 | ||
| 244 | +const dictItemForm = reactive<DictItemAddReq & { dictItemId?: number }>({ | ||
| 245 | + dictTypeId: 0, | ||
| 246 | + dictLabel: '', | ||
| 247 | + dictValue: '', | ||
| 248 | + sort: 0, | ||
| 249 | + remark: '' | ||
| 250 | +}) | ||
| 251 | + | ||
| 252 | +// 表单验证错误 | ||
| 253 | +const fieldErrors = reactive({ | ||
| 254 | + dictTypeId: '', | ||
| 255 | + dictLabel: '', | ||
| 256 | + dictValue: '', | ||
| 257 | + sort: '', | ||
| 258 | + remark: '' | ||
| 259 | +}) | ||
| 260 | + | ||
| 261 | +// 选择相关 | ||
| 262 | +const selectedItems = ref<number[]>([]) | ||
| 263 | +const selectAll = ref(false) | ||
| 264 | + | ||
| 265 | +// 可见页码 | ||
| 266 | +const visiblePages = computed(() => { | ||
| 267 | + const pages = [] | ||
| 268 | + const start = Math.max(1, currentPage.value - 2) | ||
| 269 | + const end = Math.min(totalPages.value, start + 4) | ||
| 270 | + for (let i = start; i <= end; i++) { | ||
| 271 | + pages.push(i) | ||
| 272 | + } | ||
| 273 | + return pages | ||
| 274 | +}) | ||
| 275 | + | ||
| 276 | +// 获取字典项列表 | ||
| 277 | +const fetchDictItems = async () => { | ||
| 278 | + try { | ||
| 279 | + loading.value = true | ||
| 280 | + const params = { | ||
| 281 | + pageNum: currentPage.value, | ||
| 282 | + pageSize: pageSize.value, | ||
| 283 | + dictTypeId: searchForm.dictTypeId, | ||
| 284 | + dictLabel: searchForm.dictLabel, | ||
| 285 | + dictValue: searchForm.dictValue | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + const response = await dictItemApi.getDictItemList(params) | ||
| 289 | + if (response.code === 200) { | ||
| 290 | + dictItemList.value = response.data.records | ||
| 291 | + total.value = response.data.total | ||
| 292 | + } else { | ||
| 293 | + showMessage(response.message || '获取字典项列表失败', 'error') | ||
| 294 | + } | ||
| 295 | + } catch (error: any) { | ||
| 296 | + console.error('获取字典项列表失败:', error) | ||
| 297 | + showMessage('获取字典项列表失败,请重试', 'error') | ||
| 298 | + } finally { | ||
| 299 | + loading.value = false | ||
| 300 | + } | ||
| 301 | +} | ||
| 302 | + | ||
| 303 | +// 获取字典类型列表 | ||
| 304 | +const fetchDictTypes = async () => { | ||
| 305 | + try { | ||
| 306 | + const response = await dictApi.getDictTypes({ | ||
| 307 | + pageNum: 1, | ||
| 308 | + pageSize: 9999, | ||
| 309 | + dictType: '', | ||
| 310 | + dictName: '', | ||
| 311 | + status: 1 | ||
| 312 | + }) | ||
| 313 | + if (response.code === 200) { | ||
| 314 | + dictTypeOptions.value = response.data.records | ||
| 315 | + } | ||
| 316 | + } catch (error: any) { | ||
| 317 | + console.error('获取字典类型列表失败:', error) | ||
| 318 | + } | ||
| 319 | +} | ||
| 320 | + | ||
| 321 | +// 搜索 | ||
| 322 | +const handleSearch = () => { | ||
| 323 | + currentPage.value = 1 | ||
| 324 | + fetchDictItems() | ||
| 325 | +} | ||
| 326 | + | ||
| 327 | +// 重置 | ||
| 328 | +const handleReset = () => { | ||
| 329 | + Object.assign(searchForm, { | ||
| 330 | + pageNum: 1, | ||
| 331 | + pageSize: 10, | ||
| 332 | + dictTypeId: undefined, | ||
| 333 | + dictLabel: '', | ||
| 334 | + dictValue: '' | ||
| 335 | + }) | ||
| 336 | + currentPage.value = 1 | ||
| 337 | + fetchDictItems() | ||
| 338 | +} | ||
| 339 | + | ||
| 340 | +// 新增 | ||
| 341 | +const handleAdd = () => { | ||
| 342 | + isEdit.value = false | ||
| 343 | + Object.assign(dictItemForm, { | ||
| 344 | + dictTypeId: 0, | ||
| 345 | + dictLabel: '', | ||
| 346 | + dictValue: '', | ||
| 347 | + sort: 0, | ||
| 348 | + remark: '' | ||
| 349 | + }) | ||
| 350 | + clearFieldErrors() | ||
| 351 | + showDictItemDialog.value = true | ||
| 352 | +} | ||
| 353 | + | ||
| 354 | +// 编辑 | ||
| 355 | +const handleEdit = (item: DictItem) => { | ||
| 356 | + isEdit.value = true | ||
| 357 | + Object.assign(dictItemForm, { | ||
| 358 | + dictItemId: item.dictItemId, | ||
| 359 | + dictTypeId: item.dictTypeId, | ||
| 360 | + dictLabel: item.dictLabel, | ||
| 361 | + dictValue: item.dictValue, | ||
| 362 | + sort: item.sort, | ||
| 363 | + remark: item.remark || '' | ||
| 364 | + }) | ||
| 365 | + clearFieldErrors() | ||
| 366 | + showDictItemDialog.value = true | ||
| 367 | +} | ||
| 368 | + | ||
| 369 | +// 删除 | ||
| 370 | +const handleDelete = async (item: DictItem) => { | ||
| 371 | + if (!confirm(`确定要删除字典项"${item.dictLabel}"吗?`)) return | ||
| 372 | + | ||
| 373 | + try { | ||
| 374 | + const response = await dictItemApi.deleteDictItem([item.dictItemId]) | ||
| 375 | + if (response.code === 200) { | ||
| 376 | + showMessage('删除成功') | ||
| 377 | + fetchDictItems() | ||
| 378 | + } else { | ||
| 379 | + showMessage(response.message || '删除失败', 'error') | ||
| 380 | + } | ||
| 381 | + } catch (error: any) { | ||
| 382 | + console.error('删除字典项失败:', error) | ||
| 383 | + showMessage('删除失败,请重试', 'error') | ||
| 384 | + } | ||
| 385 | +} | ||
| 386 | + | ||
| 387 | +// 批量删除 | ||
| 388 | +const handleBatchDelete = async () => { | ||
| 389 | + if (selectedItems.value.length === 0) { | ||
| 390 | + showMessage('请选择要删除的字典项', 'warning') | ||
| 391 | + return | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + if (!confirm(`确定要删除选中的${selectedItems.value.length}个字典项吗?`)) return | ||
| 395 | + | ||
| 396 | + try { | ||
| 397 | + const response = await dictItemApi.deleteDictItem(selectedItems.value) | ||
| 398 | + if (response.code === 200) { | ||
| 399 | + showMessage('批量删除成功') | ||
| 400 | + selectedItems.value = [] | ||
| 401 | + selectAll.value = false | ||
| 402 | + fetchDictItems() | ||
| 403 | + } else { | ||
| 404 | + showMessage(response.message || '批量删除失败', 'error') | ||
| 405 | + } | ||
| 406 | + } catch (error: any) { | ||
| 407 | + console.error('批量删除字典项失败:', error) | ||
| 408 | + showMessage('批量删除失败,请重试', 'error') | ||
| 409 | + } | ||
| 410 | +} | ||
| 411 | + | ||
| 412 | +// 表单提交 | ||
| 413 | +const handleSubmit = async () => { | ||
| 414 | + clearFieldErrors() | ||
| 415 | + let hasError = false | ||
| 416 | + | ||
| 417 | + if (!dictItemForm.dictTypeId) { | ||
| 418 | + fieldErrors.dictTypeId = '请选择字典类型' | ||
| 419 | + hasError = true | ||
| 420 | + } | ||
| 421 | + | ||
| 422 | + if (!dictItemForm.dictLabel.trim()) { | ||
| 423 | + fieldErrors.dictLabel = '字典标签不能为空' | ||
| 424 | + hasError = true | ||
| 425 | + } else if (dictItemForm.dictLabel.length > 100) { | ||
| 426 | + fieldErrors.dictLabel = '字典标签长度不能超过100个字符' | ||
| 427 | + hasError = true | ||
| 428 | + } | ||
| 429 | + | ||
| 430 | + if (!dictItemForm.dictValue.trim()) { | ||
| 431 | + fieldErrors.dictValue = '字典值不能为空' | ||
| 432 | + hasError = true | ||
| 433 | + } else if (dictItemForm.dictValue.length > 50) { | ||
| 434 | + fieldErrors.dictValue = '字典值长度不能超过50个字符' | ||
| 435 | + hasError = true | ||
| 436 | + } | ||
| 437 | + | ||
| 438 | + if (dictItemForm.remark && dictItemForm.remark.length > 500) { | ||
| 439 | + fieldErrors.remark = '备注长度不能超过500个字符' | ||
| 440 | + hasError = true | ||
| 441 | + } | ||
| 442 | + | ||
| 443 | + if (hasError) return | ||
| 444 | + | ||
| 445 | + try { | ||
| 446 | + formLoading.value = true | ||
| 447 | + | ||
| 448 | + if (isEdit.value) { | ||
| 449 | + const updateData: DictItemUpdateReq = { | ||
| 450 | + dictItemId: dictItemForm.dictItemId!, | ||
| 451 | + dictTypeId: dictItemForm.dictTypeId, | ||
| 452 | + dictLabel: dictItemForm.dictLabel, | ||
| 453 | + dictValue: dictItemForm.dictValue, | ||
| 454 | + sort: dictItemForm.sort || 0, | ||
| 455 | + remark: dictItemForm.remark || '' | ||
| 456 | + } | ||
| 457 | + const response = await dictItemApi.updateDictItem(updateData) | ||
| 458 | + if (response.code === 200) { | ||
| 459 | + showMessage('修改字典项成功') | ||
| 460 | + closeDictItemDialog() | ||
| 461 | + fetchDictItems() | ||
| 462 | + } else { | ||
| 463 | + showMessage(response.message || '修改字典项失败', 'error') | ||
| 464 | + } | ||
| 465 | + } else { | ||
| 466 | + const addData: DictItemAddReq = { | ||
| 467 | + dictTypeId: dictItemForm.dictTypeId, | ||
| 468 | + dictLabel: dictItemForm.dictLabel, | ||
| 469 | + dictValue: dictItemForm.dictValue, | ||
| 470 | + sort: dictItemForm.sort || 0, | ||
| 471 | + remark: dictItemForm.remark || '' | ||
| 472 | + } | ||
| 473 | + const response = await dictItemApi.createDictItem(addData) | ||
| 474 | + if (response.code === 200) { | ||
| 475 | + showMessage('新增字典项成功') | ||
| 476 | + closeDictItemDialog() | ||
| 477 | + fetchDictItems() | ||
| 478 | + } else { | ||
| 479 | + showMessage(response.message || '新增字典项失败', 'error') | ||
| 480 | + } | ||
| 481 | + } | ||
| 482 | + } catch (error: any) { | ||
| 483 | + console.error('保存字典项失败:', error) | ||
| 484 | + showMessage('保存字典项失败,请重试', 'error') | ||
| 485 | + } finally { | ||
| 486 | + formLoading.value = false | ||
| 487 | + } | ||
| 488 | +} | ||
| 489 | + | ||
| 490 | +// 关闭对话框 | ||
| 491 | +const closeDictItemDialog = () => { | ||
| 492 | + showDictItemDialog.value = false | ||
| 493 | + clearFieldErrors() | ||
| 494 | +} | ||
| 495 | + | ||
| 496 | +// 清除字段错误 | ||
| 497 | +const clearFieldErrors = () => { | ||
| 498 | + Object.assign(fieldErrors, { | ||
| 499 | + dictTypeId: '', | ||
| 500 | + dictLabel: '', | ||
| 501 | + dictValue: '', | ||
| 502 | + sort: '', | ||
| 503 | + remark: '' | ||
| 504 | + }) | ||
| 505 | +} | ||
| 506 | + | ||
| 507 | +// 全选/取消全选 | ||
| 508 | +const handleSelectAll = () => { | ||
| 509 | + if (selectAll.value) { | ||
| 510 | + selectedItems.value = dictItemList.value.map(item => item.dictItemId) | ||
| 511 | + } else { | ||
| 512 | + selectedItems.value = [] | ||
| 513 | + } | ||
| 514 | +} | ||
| 515 | + | ||
| 516 | +// 分页相关 | ||
| 517 | +const goToPage = (page: number) => { | ||
| 518 | + if (page >= 1 && page <= totalPages.value) { | ||
| 519 | + currentPage.value = page | ||
| 520 | + fetchDictItems() | ||
| 521 | + } | ||
| 522 | +} | ||
| 523 | + | ||
| 524 | +const handlePageSizeChange = () => { | ||
| 525 | + currentPage.value = 1 | ||
| 526 | + fetchDictItems() | ||
| 527 | +} | ||
| 528 | + | ||
| 529 | +// 表格工具栏功能 | ||
| 530 | +const handleTableSearch = () => { | ||
| 531 | + // 滚动到搜索区域 | ||
| 532 | + document.querySelector('.search-section')?.scrollIntoView({ behavior: 'smooth' }) | ||
| 533 | +} | ||
| 534 | + | ||
| 535 | +const handleTableRefresh = () => { | ||
| 536 | + fetchDictItems() | ||
| 537 | +} | ||
| 538 | + | ||
| 539 | +const handleTableExport = () => { | ||
| 540 | + showMessage('导出功能开发中...', 'info') | ||
| 541 | +} | ||
| 542 | + | ||
| 543 | +const handleTableViewToggle = () => { | ||
| 544 | + showMessage('视图切换功能开发中...', 'info') | ||
| 545 | +} | ||
| 546 | + | ||
| 547 | +// 工具函数 | ||
| 548 | +const formatTime = (time: string) => { | ||
| 549 | + if (!time) return '-' | ||
| 550 | + return time.replace('T', ' ').substring(0, 19) | ||
| 551 | +} | ||
| 552 | + | ||
| 553 | +const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'success') => { | ||
| 554 | + // 这里可以集成消息提示组件 | ||
| 555 | + alert(`${type.toUpperCase()}: ${message}`) | ||
| 556 | +} | ||
| 557 | + | ||
| 558 | +// 监听选择变化 | ||
| 559 | +const watchSelectedItems = () => { | ||
| 560 | + selectAll.value = selectedItems.value.length === dictItemList.value.length && dictItemList.value.length > 0 | ||
| 561 | +} | ||
| 562 | + | ||
| 563 | +// 监听selectedItems变化 | ||
| 564 | +import { watch } from 'vue' | ||
| 565 | +watch(selectedItems, watchSelectedItems) | ||
| 566 | + | ||
| 567 | +// 初始化 | ||
| 568 | +onMounted(() => { | ||
| 569 | + fetchDictTypes() | ||
| 570 | + | ||
| 571 | + // 检查URL参数,如果有dictTypeId则自动筛选 | ||
| 572 | + const dictTypeId = route.query.dictTypeId | ||
| 573 | + if (dictTypeId) { | ||
| 574 | + searchForm.dictTypeId = parseInt(dictTypeId as string) | ||
| 575 | + } | ||
| 576 | + | ||
| 577 | + fetchDictItems() | ||
| 578 | +}) | ||
| 579 | +</script> | ||
| 580 | + | ||
| 581 | +<style scoped lang="scss"> | ||
| 582 | +.dict-item-management { | ||
| 583 | + background: #f5f5f5; | ||
| 584 | + min-height: 100vh; | ||
| 585 | + padding: 0; | ||
| 586 | +} | ||
| 587 | + | ||
| 588 | +/* 搜索区域样式 */ | ||
| 589 | +.search-section { | ||
| 590 | + background: white; | ||
| 591 | + padding: 16px; | ||
| 592 | + border-bottom: 1px solid #e0e0e0; | ||
| 593 | +} | ||
| 594 | + | ||
| 595 | +.search-row { | ||
| 596 | + display: flex; | ||
| 597 | + gap: 16px; | ||
| 598 | + align-items: center; | ||
| 599 | + flex-wrap: wrap; | ||
| 600 | +} | ||
| 601 | + | ||
| 602 | +.search-item { | ||
| 603 | + display: flex; | ||
| 604 | + align-items: center; | ||
| 605 | + gap: 8px; | ||
| 606 | +} | ||
| 607 | + | ||
| 608 | +.search-item label { | ||
| 609 | + font-size: 12px; | ||
| 610 | + color: #333; | ||
| 611 | + white-space: nowrap; | ||
| 612 | +} | ||
| 613 | + | ||
| 614 | +.search-input, .search-select { | ||
| 615 | + padding: 6px 8px; | ||
| 616 | + border: 1px solid #d9d9d9; | ||
| 617 | + border-radius: 4px; | ||
| 618 | + font-size: 12px; | ||
| 619 | + width: 120px; | ||
| 620 | +} | ||
| 621 | + | ||
| 622 | +.search-actions { | ||
| 623 | + display: flex; | ||
| 624 | + gap: 8px; | ||
| 625 | + align-items: center; | ||
| 626 | +} | ||
| 627 | + | ||
| 628 | +.search-btn, .reset-btn { | ||
| 629 | + padding: 4px 8px; | ||
| 630 | + border-radius: 3px; | ||
| 631 | + font-size: 11px; | ||
| 632 | + cursor: pointer; | ||
| 633 | + height: 24px; | ||
| 634 | + display: flex; | ||
| 635 | + align-items: center; | ||
| 636 | + gap: 4px; | ||
| 637 | + border: none; | ||
| 638 | +} | ||
| 639 | + | ||
| 640 | +.search-btn { | ||
| 641 | + background: #1890ff; | ||
| 642 | + color: white; | ||
| 643 | +} | ||
| 644 | + | ||
| 645 | +.reset-btn { | ||
| 646 | + background: #ff7875; | ||
| 647 | + color: white; | ||
| 648 | +} | ||
| 649 | + | ||
| 650 | +/* 操作区域样式 */ | ||
| 651 | +.action-section { | ||
| 652 | + background: white; | ||
| 653 | + padding: 12px 16px; | ||
| 654 | + border-bottom: 1px solid #e0e0e0; | ||
| 655 | +} | ||
| 656 | + | ||
| 657 | +.action-buttons { | ||
| 658 | + display: flex; | ||
| 659 | + gap: 6px; | ||
| 660 | +} | ||
| 661 | + | ||
| 662 | +.action-btn { | ||
| 663 | + padding: 4px 8px; | ||
| 664 | + border: none; | ||
| 665 | + border-radius: 3px; | ||
| 666 | + font-size: 11px; | ||
| 667 | + cursor: pointer; | ||
| 668 | + display: flex; | ||
| 669 | + align-items: center; | ||
| 670 | + gap: 4px; | ||
| 671 | + height: 24px; | ||
| 672 | +} | ||
| 673 | + | ||
| 674 | +.action-btn.primary { | ||
| 675 | + background: #1890ff; | ||
| 676 | + color: white; | ||
| 677 | +} | ||
| 678 | + | ||
| 679 | +.action-btn.success { | ||
| 680 | + background: #52c41a; | ||
| 681 | + color: white; | ||
| 682 | +} | ||
| 683 | + | ||
| 684 | +.action-btn.danger { | ||
| 685 | + background: #ff4d4f; | ||
| 686 | + color: white; | ||
| 687 | +} | ||
| 688 | + | ||
| 689 | +.action-btn:disabled { | ||
| 690 | + background: #d9d9d9; | ||
| 691 | + color: #999; | ||
| 692 | + cursor: not-allowed; | ||
| 693 | +} | ||
| 694 | + | ||
| 695 | +/* 表格区域样式 */ | ||
| 696 | +.table-section { | ||
| 697 | + background: white; | ||
| 698 | + margin: 16px; | ||
| 699 | + border-radius: 6px; | ||
| 700 | + overflow: hidden; | ||
| 701 | + box-shadow: 0 2px 8px rgba(0,0,0,0.1); | ||
| 702 | +} | ||
| 703 | + | ||
| 704 | +.table-header { | ||
| 705 | + padding: 8px 16px; | ||
| 706 | + background: #fafafa; | ||
| 707 | + border-bottom: 1px solid #e0e0e0; | ||
| 708 | + display: flex; | ||
| 709 | + justify-content: flex-end; | ||
| 710 | +} | ||
| 711 | + | ||
| 712 | +.table-controls { | ||
| 713 | + display: flex; | ||
| 714 | + gap: 4px; | ||
| 715 | +} | ||
| 716 | + | ||
| 717 | +.control-btn { | ||
| 718 | + background: white; | ||
| 719 | + border: 1px solid #d9d9d9; | ||
| 720 | + border-radius: 3px; | ||
| 721 | + padding: 4px 8px; | ||
| 722 | + font-size: 11px; | ||
| 723 | + cursor: pointer; | ||
| 724 | + transition: all 0.2s; | ||
| 725 | +} | ||
| 726 | + | ||
| 727 | +.control-btn:hover { | ||
| 728 | + background: #f5f5f5; | ||
| 729 | + border-color: #1890ff; | ||
| 730 | +} | ||
| 731 | + | ||
| 732 | +.table-container { | ||
| 733 | + overflow-x: auto; | ||
| 734 | +} | ||
| 735 | + | ||
| 736 | +.data-table { | ||
| 737 | + width: 100%; | ||
| 738 | + border-collapse: collapse; | ||
| 739 | +} | ||
| 740 | + | ||
| 741 | +.data-table th, | ||
| 742 | +.data-table td { | ||
| 743 | + padding: 8px 12px; | ||
| 744 | + text-align: left; | ||
| 745 | + border-bottom: 1px solid #f0f0f0; | ||
| 746 | + font-size: 12px; | ||
| 747 | +} | ||
| 748 | + | ||
| 749 | +.data-table th { | ||
| 750 | + background: #fafafa; | ||
| 751 | + font-weight: 600; | ||
| 752 | + color: #333; | ||
| 753 | +} | ||
| 754 | + | ||
| 755 | +.data-table tbody tr:hover { | ||
| 756 | + background: #f5f5f5; | ||
| 757 | +} | ||
| 758 | + | ||
| 759 | +.data-table tr.selected { | ||
| 760 | + background: #e6f7ff; | ||
| 761 | +} | ||
| 762 | + | ||
| 763 | +.checkbox-col { | ||
| 764 | + width: 40px; | ||
| 765 | + text-align: center; | ||
| 766 | +} | ||
| 767 | + | ||
| 768 | +.table-checkbox { | ||
| 769 | + margin: 0; | ||
| 770 | + cursor: pointer; | ||
| 771 | +} | ||
| 772 | + | ||
| 773 | +.table-btn { | ||
| 774 | + padding: 2px 6px; | ||
| 775 | + margin-right: 4px; | ||
| 776 | + border: none; | ||
| 777 | + border-radius: 3px; | ||
| 778 | + font-size: 10px; | ||
| 779 | + cursor: pointer; | ||
| 780 | + display: inline-flex; | ||
| 781 | + align-items: center; | ||
| 782 | + gap: 2px; | ||
| 783 | +} | ||
| 784 | + | ||
| 785 | +.table-btn.edit { | ||
| 786 | + background: #1890ff; | ||
| 787 | + color: white; | ||
| 788 | +} | ||
| 789 | + | ||
| 790 | +.table-btn.delete { | ||
| 791 | + background: #ff4d4f; | ||
| 792 | + color: white; | ||
| 793 | +} | ||
| 794 | + | ||
| 795 | +/* 分页样式 */ | ||
| 796 | +.table-footer { | ||
| 797 | + display: flex; | ||
| 798 | + justify-content: space-between; | ||
| 799 | + align-items: center; | ||
| 800 | + padding: 8px 16px; | ||
| 801 | + background: #fafafa; | ||
| 802 | + border-top: 1px solid #e0e0e0; | ||
| 803 | +} | ||
| 804 | + | ||
| 805 | +.pagination-left { | ||
| 806 | + display: flex; | ||
| 807 | + align-items: center; | ||
| 808 | + gap: 16px; | ||
| 809 | +} | ||
| 810 | + | ||
| 811 | +.pagination-info { | ||
| 812 | + font-size: 12px; | ||
| 813 | + color: #666; | ||
| 814 | +} | ||
| 815 | + | ||
| 816 | +.page-size-selector { | ||
| 817 | + display: flex; | ||
| 818 | + align-items: center; | ||
| 819 | + gap: 4px; | ||
| 820 | +} | ||
| 821 | + | ||
| 822 | +.page-size-selector label { | ||
| 823 | + font-size: 12px; | ||
| 824 | + color: #666; | ||
| 825 | +} | ||
| 826 | + | ||
| 827 | +.page-size-select { | ||
| 828 | + padding: 4px 8px; | ||
| 829 | + border: 1px solid #d9d9d9; | ||
| 830 | + border-radius: 4px; | ||
| 831 | + font-size: 12px; | ||
| 832 | + background: white; | ||
| 833 | + cursor: pointer; | ||
| 834 | +} | ||
| 835 | + | ||
| 836 | +.page-size-select:hover, | ||
| 837 | +.page-size-select:focus { | ||
| 838 | + border-color: #1890ff; | ||
| 839 | + outline: none; | ||
| 840 | +} | ||
| 841 | + | ||
| 842 | +.pagination-container { | ||
| 843 | + display: flex; | ||
| 844 | + align-items: center; | ||
| 845 | + gap: 4px; | ||
| 846 | +} | ||
| 847 | + | ||
| 848 | +.pagination-btn { | ||
| 849 | + padding: 2px 6px; | ||
| 850 | + border: none; | ||
| 851 | + background: white; | ||
| 852 | + color: #6c757d; | ||
| 853 | + font-size: 11px; | ||
| 854 | + font-weight: 500; | ||
| 855 | + cursor: pointer; | ||
| 856 | + transition: all 0.2s; | ||
| 857 | + border-right: 1px solid #e9ecef; | ||
| 858 | + height: 20px; | ||
| 859 | +} | ||
| 860 | + | ||
| 861 | +.pagination-btn:hover:not(:disabled) { | ||
| 862 | + background: #e9ecef; | ||
| 863 | + color: #495057; | ||
| 864 | +} | ||
| 865 | + | ||
| 866 | +.pagination-btn:disabled { | ||
| 867 | + background: #f8f9fa; | ||
| 868 | + color: #adb5bd; | ||
| 869 | + cursor: not-allowed; | ||
| 870 | +} | ||
| 871 | + | ||
| 872 | +.pagination-pages { | ||
| 873 | + display: flex; | ||
| 874 | + align-items: center; | ||
| 875 | +} | ||
| 876 | + | ||
| 877 | +.page-number { | ||
| 878 | + padding: 2px 6px; | ||
| 879 | + border: none; | ||
| 880 | + background: white; | ||
| 881 | + color: #6c757d; | ||
| 882 | + font-size: 11px; | ||
| 883 | + font-weight: 500; | ||
| 884 | + cursor: pointer; | ||
| 885 | + transition: all 0.2s; | ||
| 886 | + border-right: 1px solid #e9ecef; | ||
| 887 | + min-width: 28px; | ||
| 888 | + height: 20px; | ||
| 889 | +} | ||
| 890 | + | ||
| 891 | +.page-number:hover { | ||
| 892 | + background: #e9ecef; | ||
| 893 | + color: #495057; | ||
| 894 | +} | ||
| 895 | + | ||
| 896 | +.page-number.active { | ||
| 897 | + background: #6c757d; | ||
| 898 | + color: white; | ||
| 899 | + font-weight: 600; | ||
| 900 | +} | ||
| 901 | + | ||
| 902 | +.page-number.active:hover { | ||
| 903 | + background: #5a6268; | ||
| 904 | +} | ||
| 905 | + | ||
| 906 | +/* 对话框样式 */ | ||
| 907 | +.dialog-overlay { | ||
| 908 | + position: fixed; | ||
| 909 | + top: 0; | ||
| 910 | + left: 0; | ||
| 911 | + right: 0; | ||
| 912 | + bottom: 0; | ||
| 913 | + background: rgba(0, 0, 0, 0.5); | ||
| 914 | + display: flex; | ||
| 915 | + justify-content: center; | ||
| 916 | + align-items: center; | ||
| 917 | + z-index: 1000; | ||
| 918 | +} | ||
| 919 | + | ||
| 920 | +.dialog-content { | ||
| 921 | + background: white; | ||
| 922 | + border-radius: 8px; | ||
| 923 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||
| 924 | + width: 90%; | ||
| 925 | + max-width: 600px; | ||
| 926 | + max-height: 80vh; | ||
| 927 | + overflow-y: auto; | ||
| 928 | +} | ||
| 929 | + | ||
| 930 | +.dialog-header { | ||
| 931 | + display: flex; | ||
| 932 | + justify-content: space-between; | ||
| 933 | + align-items: center; | ||
| 934 | + padding: 16px 20px; | ||
| 935 | + border-bottom: 1px solid #e0e0e0; | ||
| 936 | +} | ||
| 937 | + | ||
| 938 | +.dialog-header h3 { | ||
| 939 | + margin: 0; | ||
| 940 | + font-size: 16px; | ||
| 941 | + color: #333; | ||
| 942 | +} | ||
| 943 | + | ||
| 944 | +.dialog-close { | ||
| 945 | + background: none; | ||
| 946 | + border: none; | ||
| 947 | + font-size: 20px; | ||
| 948 | + cursor: pointer; | ||
| 949 | + color: #666; | ||
| 950 | +} | ||
| 951 | + | ||
| 952 | +.dialog-form { | ||
| 953 | + padding: 20px; | ||
| 954 | +} | ||
| 955 | + | ||
| 956 | +.form-row { | ||
| 957 | + display: flex; | ||
| 958 | + gap: 16px; | ||
| 959 | + margin-bottom: 16px; | ||
| 960 | +} | ||
| 961 | + | ||
| 962 | +.form-group { | ||
| 963 | + flex: 1; | ||
| 964 | + display: flex; | ||
| 965 | + flex-direction: column; | ||
| 966 | + gap: 4px; | ||
| 967 | +} | ||
| 968 | + | ||
| 969 | +.form-group.full-width { | ||
| 970 | + flex: 1 1 100%; | ||
| 971 | +} | ||
| 972 | + | ||
| 973 | +.form-group label { | ||
| 974 | + font-size: 12px; | ||
| 975 | + color: #333; | ||
| 976 | + font-weight: 500; | ||
| 977 | +} | ||
| 978 | + | ||
| 979 | +.required-label { | ||
| 980 | + color: #333; | ||
| 981 | + font-weight: 500; | ||
| 982 | +} | ||
| 983 | + | ||
| 984 | +.required-asterisk { | ||
| 985 | + color: #ff4d4f; | ||
| 986 | +} | ||
| 987 | + | ||
| 988 | +.form-input, .form-select { | ||
| 989 | + padding: 6px 8px; | ||
| 990 | + border: 1px solid #d9d9d9; | ||
| 991 | + border-radius: 4px; | ||
| 992 | + font-size: 12px; | ||
| 993 | + transition: border-color 0.2s; | ||
| 994 | +} | ||
| 995 | + | ||
| 996 | +.form-input:focus, .form-select:focus { | ||
| 997 | + outline: none; | ||
| 998 | + border-color: #1890ff; | ||
| 999 | + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); | ||
| 1000 | +} | ||
| 1001 | + | ||
| 1002 | +.field-error { | ||
| 1003 | + color: #ff4d4f; | ||
| 1004 | + font-size: 11px; | ||
| 1005 | + margin-top: 4px; | ||
| 1006 | + display: block; | ||
| 1007 | +} | ||
| 1008 | + | ||
| 1009 | +.dialog-actions { | ||
| 1010 | + display: flex; | ||
| 1011 | + justify-content: flex-end; | ||
| 1012 | + gap: 8px; | ||
| 1013 | + padding-top: 16px; | ||
| 1014 | + border-top: 1px solid #e0e0e0; | ||
| 1015 | +} | ||
| 1016 | + | ||
| 1017 | +.btn-cancel, .btn-confirm { | ||
| 1018 | + padding: 6px 16px; | ||
| 1019 | + border-radius: 4px; | ||
| 1020 | + font-size: 12px; | ||
| 1021 | + cursor: pointer; | ||
| 1022 | + transition: all 0.2s; | ||
| 1023 | +} | ||
| 1024 | + | ||
| 1025 | +.btn-cancel { | ||
| 1026 | + background: #f5f5f5; | ||
| 1027 | + color: #666; | ||
| 1028 | + border: 1px solid #d9d9d9; | ||
| 1029 | +} | ||
| 1030 | + | ||
| 1031 | +.btn-confirm { | ||
| 1032 | + background: #1890ff; | ||
| 1033 | + color: white; | ||
| 1034 | + border: none; | ||
| 1035 | +} | ||
| 1036 | + | ||
| 1037 | +.btn-confirm:disabled { | ||
| 1038 | + background: #d9d9d9; | ||
| 1039 | + color: #999; | ||
| 1040 | + cursor: not-allowed; | ||
| 1041 | +} | ||
| 1042 | + | ||
| 1043 | +/* 响应式设计 */ | ||
| 1044 | +@media (max-width: 768px) { | ||
| 1045 | + .search-row { | ||
| 1046 | + flex-direction: column; | ||
| 1047 | + gap: 12px; | ||
| 1048 | + } | ||
| 1049 | + | ||
| 1050 | + .search-item { | ||
| 1051 | + min-width: auto; | ||
| 1052 | + width: 100%; | ||
| 1053 | + } | ||
| 1054 | + | ||
| 1055 | + .search-input, .search-select { | ||
| 1056 | + width: 100%; | ||
| 1057 | + } | ||
| 1058 | + | ||
| 1059 | + .form-row { | ||
| 1060 | + flex-direction: column; | ||
| 1061 | + gap: 8px; | ||
| 1062 | + } | ||
| 1063 | + | ||
| 1064 | + .table-footer { | ||
| 1065 | + flex-direction: column; | ||
| 1066 | + gap: 12px; | ||
| 1067 | + } | ||
| 1068 | + | ||
| 1069 | + .pagination-left { | ||
| 1070 | + flex-direction: column; | ||
| 1071 | + gap: 8px; | ||
| 1072 | + } | ||
| 1073 | +} | ||
| 1074 | +</style> |
| 1 | <template> | 1 | <template> |
| 2 | <div class="dict-management"> | 2 | <div class="dict-management"> |
| 3 | - <!-- 搜索表单 --> | 3 | + <!-- 搜索筛选区域 --> |
| 4 | - <el-card class="search-form"> | 4 | + <div class="search-section"> |
| 5 | - <el-form :model="searchForm" inline> | 5 | + <div class="search-row"> |
| 6 | - <el-form-item label="字典名称"> | 6 | + <div class="search-item"> |
| 7 | - <el-input v-model="searchForm.dictName" placeholder="请输入字典名称" clearable /> | 7 | + <label>字典类型:</label> |
| 8 | - </el-form-item> | 8 | + <input |
| 9 | - <el-form-item label="字典类型"> | 9 | + v-model="searchParams.dictType" |
| 10 | - <el-input v-model="searchForm.dictType" placeholder="请输入字典类型" clearable /> | 10 | + type="text" |
| 11 | - </el-form-item> | 11 | + class="search-input" |
| 12 | - <el-form-item label="状态"> | 12 | + placeholder="请输入字典类型" |
| 13 | - <el-select v-model="searchForm.status" placeholder="请选择状态" clearable> | 13 | + /> |
| 14 | - <el-option label="正常" :value="1" /> | 14 | + </div> |
| 15 | - <el-option label="停用" :value="0" /> | 15 | + <div class="search-item"> |
| 16 | - </el-select> | 16 | + <label>字典名称:</label> |
| 17 | - </el-form-item> | 17 | + <input |
| 18 | - <el-form-item> | 18 | + v-model="searchParams.dictName" |
| 19 | - <el-button type="primary" @click="handleSearch"> | 19 | + type="text" |
| 20 | - <el-icon><Search /></el-icon> | 20 | + class="search-input" |
| 21 | - 搜索 | 21 | + placeholder="请输入字典名称" |
| 22 | - </el-button> | 22 | + /> |
| 23 | - <el-button @click="handleReset"> | 23 | + </div> |
| 24 | - <el-icon><Refresh /></el-icon> | 24 | + <div class="search-item"> |
| 25 | - 重置 | 25 | + <label>状态:</label> |
| 26 | - </el-button> | 26 | + <select v-model="searchParams.status" class="search-select"> |
| 27 | - </el-form-item> | 27 | + <option value="">所有</option> |
| 28 | - </el-form> | 28 | + <option value="1">正常</option> |
| 29 | - </el-card> | 29 | + <option value="0">停用</option> |
| 30 | - | 30 | + </select> |
| 31 | - <!-- 操作按钮 --> | 31 | + </div> |
| 32 | - <el-card class="operation-buttons"> | 32 | + <div class="search-actions"> |
| 33 | - <el-button type="primary" @click="handleAdd"> | 33 | + <button @click="handleSearch" class="search-btn">🔍 搜索</button> |
| 34 | - <el-icon><Plus /></el-icon> | 34 | + <button @click="handleReset" class="reset-btn">🔄 重置</button> |
| 35 | - 新增字典 | 35 | + </div> |
| 36 | - </el-button> | 36 | + </div> |
| 37 | - <el-button type="danger" :disabled="!multipleSelection.length" @click="handleBatchDelete"> | 37 | + </div> |
| 38 | - <el-icon><Delete /></el-icon> | 38 | + |
| 39 | - 批量删除 | 39 | + <!-- 操作区域 --> |
| 40 | - </el-button> | 40 | + <div class="action-section"> |
| 41 | - <el-button type="success" @click="handleRefreshCache"> | 41 | + <div class="action-buttons"> |
| 42 | - <el-icon><Refresh /></el-icon> | 42 | + <button @click="handleAdd" class="action-btn primary">✨ 新增</button> |
| 43 | - 刷新缓存 | 43 | + <button @click="handleBatchDelete" class="action-btn danger" :disabled="selectedDictTypes.length === 0"> |
| 44 | - </el-button> | 44 | + 🗑️ 删除 |
| 45 | - </el-card> | 45 | + </button> |
| 46 | + <button @click="handleRefreshCache" class="action-btn secondary">🔄 刷新缓存</button> | ||
| 47 | + </div> | ||
| 48 | + </div> | ||
| 46 | 49 | ||
| 47 | <!-- 数据表格 --> | 50 | <!-- 数据表格 --> |
| 48 | - <el-card class="table-container"> | 51 | + <div class="table-section"> |
| 49 | - <el-table | 52 | + <div class="table-header"> |
| 50 | - v-loading="loading" | 53 | + <div class="table-controls"> |
| 51 | - :data="tableData" | 54 | + <button @click="handleTableSearch" class="control-btn" title="搜索"> |
| 52 | - @selection-change="handleSelectionChange" | 55 | + 🔍 |
| 56 | + </button> | ||
| 57 | + <button @click="handleTableRefresh" class="control-btn" title="刷新"> | ||
| 58 | + 🔄 | ||
| 59 | + </button> | ||
| 60 | + <button @click="handleTableExport" class="control-btn" title="导出"> | ||
| 61 | + 📤 | ||
| 62 | + </button> | ||
| 63 | + <button @click="handleTableViewToggle" class="control-btn" title="视图切换"> | ||
| 64 | + 📋 | ||
| 65 | + </button> | ||
| 66 | + </div> | ||
| 67 | + </div> | ||
| 68 | + | ||
| 69 | + <div class="table-container"> | ||
| 70 | + <table class="data-table"> | ||
| 71 | + <thead> | ||
| 72 | + <tr> | ||
| 73 | + <th> | ||
| 74 | + <input | ||
| 75 | + type="checkbox" | ||
| 76 | + v-model="selectAll" | ||
| 77 | + @change="handleSelectAll" | ||
| 78 | + class="table-checkbox" | ||
| 79 | + > | ||
| 80 | + </th> | ||
| 81 | + <th>字典类型</th> | ||
| 82 | + <th>字典名称</th> | ||
| 83 | + <th>状态</th> | ||
| 84 | + <th>备注</th> | ||
| 85 | + <th>创建时间</th> | ||
| 86 | + <th>操作</th> | ||
| 87 | + </tr> | ||
| 88 | + </thead> | ||
| 89 | + <tbody> | ||
| 90 | + <tr v-for="dictType in dictTypeList" :key="dictType.dictTypeId" :class="{ selected: selectedDictTypes.includes(dictType.dictTypeId) }"> | ||
| 91 | + <td> | ||
| 92 | + <input | ||
| 93 | + type="checkbox" | ||
| 94 | + :value="dictType.dictTypeId" | ||
| 95 | + v-model="selectedDictTypes" | ||
| 96 | + class="table-checkbox" | ||
| 97 | + > | ||
| 98 | + </td> | ||
| 99 | + <td> | ||
| 100 | + <span class="dict-type-tag">{{ dictType.dictType }}</span> | ||
| 101 | + </td> | ||
| 102 | + <td>{{ dictType.dictName }}</td> | ||
| 103 | + <td> | ||
| 104 | + <span class="status-tag" :class="dictType.status === 1 ? 'active' : 'inactive'"> | ||
| 105 | + {{ dictType.statusText }} | ||
| 106 | + </span> | ||
| 107 | + </td> | ||
| 108 | + <td>{{ dictType.remark || '-' }}</td> | ||
| 109 | + <td>{{ formatTime(dictType.createTime) }}</td> | ||
| 110 | + <td> | ||
| 111 | + <div class="action-buttons"> | ||
| 112 | + <button @click="handleEdit(dictType)" class="table-btn edit">✏️ 编辑</button> | ||
| 113 | + <button @click="handleDelete(dictType)" class="table-btn delete">🗑️ 删除</button> | ||
| 114 | + <button @click="handleViewItems(dictType)" class="table-btn view">📋 字典项</button> | ||
| 115 | + </div> | ||
| 116 | + </td> | ||
| 117 | + </tr> | ||
| 118 | + </tbody> | ||
| 119 | + </table> | ||
| 120 | + </div> | ||
| 121 | + | ||
| 122 | + <!-- 分页信息 --> | ||
| 123 | + <div class="table-footer"> | ||
| 124 | + <div class="pagination-left"> | ||
| 125 | + <div class="pagination-info"> | ||
| 126 | + 共 {{ total }} 条记录,第 {{ currentPage }} / {{ totalPages }} 页 | ||
| 127 | + </div> | ||
| 128 | + <div class="page-size-selector"> | ||
| 129 | + <label>每页显示:</label> | ||
| 130 | + <select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> | ||
| 131 | + <option value="10">10条</option> | ||
| 132 | + <option value="20">20条</option> | ||
| 133 | + <option value="50">50条</option> | ||
| 134 | + <option value="100">100条</option> | ||
| 135 | + </select> | ||
| 136 | + </div> | ||
| 137 | + </div> | ||
| 138 | + <div class="pagination-container"> | ||
| 139 | + <button | ||
| 140 | + @click="handlePageChange(currentPage - 1)" | ||
| 141 | + :disabled="currentPage <= 1" | ||
| 142 | + class="pagination-btn prev-btn" | ||
| 143 | + > | ||
| 144 | + 上一页 | ||
| 145 | + </button> | ||
| 146 | + <div class="pagination-pages"> | ||
| 147 | + <button | ||
| 148 | + v-for="page in getPageNumbers()" | ||
| 149 | + :key="page" | ||
| 150 | + @click="handlePageChange(page)" | ||
| 151 | + :class="['page-number', { active: page === currentPage }]" | ||
| 152 | + > | ||
| 153 | + {{ page }} | ||
| 154 | + </button> | ||
| 155 | + </div> | ||
| 156 | + <button | ||
| 157 | + @click="handlePageChange(currentPage + 1)" | ||
| 158 | + :disabled="currentPage >= totalPages" | ||
| 159 | + class="pagination-btn next-btn" | ||
| 160 | + > | ||
| 161 | + 下一页 | ||
| 162 | + </button> | ||
| 163 | + </div> | ||
| 164 | + </div> | ||
| 165 | + </div> | ||
| 166 | + | ||
| 167 | + <!-- 新增/编辑对话框 --> | ||
| 168 | + <div v-if="showDictTypeDialog" class="dialog-overlay" @click="closeDictTypeDialog"> | ||
| 169 | + <div class="dialog-content" @click.stop> | ||
| 170 | + <div class="dialog-header"> | ||
| 171 | + <h3>{{ dialogTitle }}</h3> | ||
| 172 | + <button @click="closeDictTypeDialog" class="close-btn">×</button> | ||
| 173 | + </div> | ||
| 174 | + <form @submit.prevent="handleSubmit" class="dialog-form"> | ||
| 175 | + <div class="form-row"> | ||
| 176 | + <div class="form-group"> | ||
| 177 | + <label class="required">字典类型</label> | ||
| 178 | + <input | ||
| 179 | + v-model="dictTypeForm.dictType" | ||
| 180 | + type="text" | ||
| 181 | + placeholder="请输入字典类型(如:user_status)" | ||
| 182 | + class="form-input" | ||
| 183 | + required | ||
| 184 | + > | ||
| 185 | + <span v-if="fieldErrors.dictType" class="error-message">{{ fieldErrors.dictType }}</span> | ||
| 186 | + </div> | ||
| 187 | + <div class="form-group"> | ||
| 188 | + <label class="required">字典名称</label> | ||
| 189 | + <input | ||
| 190 | + v-model="dictTypeForm.dictName" | ||
| 191 | + type="text" | ||
| 192 | + placeholder="请输入字典名称" | ||
| 193 | + class="form-input" | ||
| 194 | + required | ||
| 195 | + > | ||
| 196 | + <span v-if="fieldErrors.dictName" class="error-message">{{ fieldErrors.dictName }}</span> | ||
| 197 | + </div> | ||
| 198 | + </div> | ||
| 199 | + | ||
| 200 | + <div class="form-row"> | ||
| 201 | + <div class="form-group"> | ||
| 202 | + <label>状态</label> | ||
| 203 | + <select v-model="dictTypeForm.status" class="form-select"> | ||
| 204 | + <option value="1">正常</option> | ||
| 205 | + <option value="0">停用</option> | ||
| 206 | + </select> | ||
| 207 | + </div> | ||
| 208 | + <div class="form-group"> | ||
| 209 | + <label>备注</label> | ||
| 210 | + <input | ||
| 211 | + v-model="dictTypeForm.remark" | ||
| 212 | + type="text" | ||
| 213 | + placeholder="请输入备注" | ||
| 214 | + class="form-input" | ||
| 53 | > | 215 | > |
| 54 | - <el-table-column type="selection" width="55" /> | 216 | + </div> |
| 55 | - <el-table-column prop="dictId" label="字典ID" width="80" /> | 217 | + </div> |
| 56 | - <el-table-column prop="dictName" label="字典名称" width="200" /> | 218 | + |
| 57 | - <el-table-column prop="dictType" label="字典类型" width="200" /> | 219 | + <div class="form-actions"> |
| 58 | - <el-table-column prop="status" label="状态" width="80"> | 220 | + <button type="button" @click="closeDictTypeDialog" class="cancel-btn">取消</button> |
| 59 | - <template #default="{ row }"> | 221 | + <button type="submit" :disabled="formLoading" class="submit-btn"> |
| 60 | - <el-tag :type="row.status === 1 ? 'success' : 'danger'"> | 222 | + {{ formLoading ? '保存中...' : '保存' }} |
| 61 | - {{ row.status === 1 ? '正常' : '停用' }} | 223 | + </button> |
| 62 | - </el-tag> | 224 | + </div> |
| 63 | - </template> | 225 | + </form> |
| 64 | - </el-table-column> | 226 | + </div> |
| 65 | - <el-table-column prop="createTime" label="创建时间" width="180" /> | 227 | + </div> |
| 66 | - <el-table-column label="操作" width="250" fixed="right"> | ||
| 67 | - <template #default="{ row }"> | ||
| 68 | - <el-button type="primary" size="small" @click="handleEdit(row)"> | ||
| 69 | - 编辑 | ||
| 70 | - </el-button> | ||
| 71 | - <el-button type="success" size="small" @click="handleManageItems(row)"> | ||
| 72 | - 字典项 | ||
| 73 | - </el-button> | ||
| 74 | - <el-button type="danger" size="small" @click="handleDelete(row)"> | ||
| 75 | - 删除 | ||
| 76 | - </el-button> | ||
| 77 | - </template> | ||
| 78 | - </el-table-column> | ||
| 79 | - </el-table> | ||
| 80 | - | ||
| 81 | - <!-- 分页 --> | ||
| 82 | - <el-pagination | ||
| 83 | - v-model:current-page="pagination.current" | ||
| 84 | - v-model:page-size="pagination.size" | ||
| 85 | - :total="pagination.total" | ||
| 86 | - :page-sizes="[10, 20, 50, 100]" | ||
| 87 | - layout="total, sizes, prev, pager, next, jumper" | ||
| 88 | - @size-change="handleSizeChange" | ||
| 89 | - @current-change="handleCurrentChange" | ||
| 90 | - /> | ||
| 91 | - </el-card> | ||
| 92 | </div> | 228 | </div> |
| 93 | </template> | 229 | </template> |
| 94 | 230 | ||
| 95 | <script setup lang="ts"> | 231 | <script setup lang="ts"> |
| 96 | -import { ref, reactive, onMounted } from 'vue' | 232 | +import { ref, reactive, onMounted, computed } from 'vue' |
| 97 | -import { ElMessage, ElMessageBox } from 'element-plus' | 233 | +import { useRouter } from 'vue-router' |
| 98 | -import type { DictType } from '@/types' | 234 | +import { dictApi, type DictType, type DictTypeSearchParams } from '../../../api/dict' |
| 99 | 235 | ||
| 100 | -// 搜索表单 | 236 | +const router = useRouter() |
| 101 | -const searchForm = reactive({ | 237 | + |
| 102 | - dictName: '', | 238 | +// 响应式数据 |
| 239 | +const dictTypeList = ref<DictType[]>([]) | ||
| 240 | +const selectedDictTypes = ref<number[]>([]) | ||
| 241 | +const selectAll = ref(false) | ||
| 242 | +const loading = ref(false) | ||
| 243 | +const formLoading = ref(false) | ||
| 244 | + | ||
| 245 | +// 搜索参数 | ||
| 246 | +const searchParams = reactive<DictTypeSearchParams>({ | ||
| 103 | dictType: '', | 247 | dictType: '', |
| 104 | - status: undefined | 248 | + dictName: '', |
| 249 | + status: undefined, | ||
| 250 | + pageNum: 1, | ||
| 251 | + pageSize: 10 | ||
| 105 | }) | 252 | }) |
| 106 | 253 | ||
| 107 | -// 表格数据 | 254 | +// 分页数据 |
| 108 | -const tableData = ref<DictType[]>([]) | 255 | +const currentPage = ref(1) |
| 109 | -const loading = ref(false) | 256 | +const pageSize = ref(10) |
| 110 | -const multipleSelection = ref<DictType[]>([]) | 257 | +const total = ref(0) |
| 258 | +const totalPages = computed(() => Math.ceil(total.value / pageSize.value)) | ||
| 111 | 259 | ||
| 112 | -// 分页 | 260 | +// 获取页码数组 |
| 113 | -const pagination = reactive({ | 261 | +const getPageNumbers = () => { |
| 114 | - current: 1, | 262 | + const pages = [] |
| 115 | - size: 10, | 263 | + const maxVisible = 5 // 最多显示5个页码 |
| 116 | - total: 0 | 264 | + const start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2)) |
| 265 | + const end = Math.min(totalPages.value, start + maxVisible - 1) | ||
| 266 | + | ||
| 267 | + for (let i = start; i <= end; i++) { | ||
| 268 | + pages.push(i) | ||
| 269 | + } | ||
| 270 | + return pages | ||
| 271 | +} | ||
| 272 | + | ||
| 273 | +// 对话框相关 | ||
| 274 | +const showDictTypeDialog = ref(false) | ||
| 275 | +const dialogTitle = ref('') | ||
| 276 | + | ||
| 277 | +// 字典类型表单 | ||
| 278 | +const dictTypeForm = reactive({ | ||
| 279 | + dictTypeId: undefined as number | undefined, | ||
| 280 | + dictType: '', | ||
| 281 | + dictName: '', | ||
| 282 | + status: 1, | ||
| 283 | + remark: '' | ||
| 117 | }) | 284 | }) |
| 118 | 285 | ||
| 119 | -// 获取字典列表 | 286 | +const fieldErrors = reactive({ |
| 120 | -const getDictList = async () => { | 287 | + dictType: '', |
| 121 | - loading.value = true | 288 | + dictName: '' |
| 289 | +}) | ||
| 290 | + | ||
| 291 | +// 消息提示 | ||
| 292 | +const showMessage = (message: string, type: 'success' | 'error' | 'warning' = 'success') => { | ||
| 293 | + console.log(`${type.toUpperCase()}: ${message}`) | ||
| 294 | + alert(message) | ||
| 295 | +} | ||
| 296 | + | ||
| 297 | +// 格式化时间 | ||
| 298 | +const formatTime = (time: string) => { | ||
| 299 | + if (!time) return '-' | ||
| 300 | + return time.replace('T', ' ').substring(0, 19) | ||
| 301 | +} | ||
| 302 | + | ||
| 303 | +// 清除字段错误 | ||
| 304 | +const clearFieldErrors = () => { | ||
| 305 | + Object.keys(fieldErrors).forEach(key => { | ||
| 306 | + fieldErrors[key as keyof typeof fieldErrors] = '' | ||
| 307 | + }) | ||
| 308 | +} | ||
| 309 | + | ||
| 310 | +// 获取字典类型列表 | ||
| 311 | +const fetchDictTypes = async () => { | ||
| 122 | try { | 312 | try { |
| 123 | - // 模拟数据 | 313 | + loading.value = true |
| 124 | - tableData.value = [ | 314 | + const params = { |
| 125 | - { | 315 | + ...searchParams, |
| 126 | - dictId: 1, | 316 | + pageNum: currentPage.value, |
| 127 | - dictName: '用户性别', | 317 | + pageSize: pageSize.value |
| 128 | - dictType: 'sys_user_sex', | 318 | + } |
| 129 | - status: 1, | 319 | + |
| 130 | - remark: '用户性别列表', | 320 | + const response = await dictApi.getDictTypes(params) |
| 131 | - createTime: '2024-01-01 10:00:00', | 321 | + if (response.code === 200 && response.data) { |
| 132 | - updateTime: '2024-01-01 10:00:00' | 322 | + dictTypeList.value = response.data.records || [] |
| 133 | - }, | 323 | + total.value = response.data.total || 0 |
| 134 | - { | 324 | + } else { |
| 135 | - dictId: 2, | 325 | + showMessage(response.message || '获取字典类型列表失败', 'error') |
| 136 | - dictName: '菜单状态', | ||
| 137 | - dictType: 'sys_show_hide', | ||
| 138 | - status: 1, | ||
| 139 | - remark: '菜单状态列表', | ||
| 140 | - createTime: '2024-01-01 10:00:00', | ||
| 141 | - updateTime: '2024-01-01 10:00:00' | ||
| 142 | - }, | ||
| 143 | - { | ||
| 144 | - dictId: 3, | ||
| 145 | - dictName: '系统开关', | ||
| 146 | - dictType: 'sys_normal_disable', | ||
| 147 | - status: 1, | ||
| 148 | - remark: '系统开关列表', | ||
| 149 | - createTime: '2024-01-01 10:00:00', | ||
| 150 | - updateTime: '2024-01-01 10:00:00' | ||
| 151 | } | 326 | } |
| 152 | - ] | ||
| 153 | - pagination.total = 3 | ||
| 154 | } catch (error) { | 327 | } catch (error) { |
| 155 | - ElMessage.error('获取字典列表失败') | 328 | + console.error('获取字典类型列表失败:', error) |
| 329 | + showMessage('获取字典类型列表失败,请重试', 'error') | ||
| 156 | } finally { | 330 | } finally { |
| 157 | loading.value = false | 331 | loading.value = false |
| 158 | } | 332 | } |
| 159 | } | 333 | } |
| 160 | 334 | ||
| 161 | -// 搜索 | 335 | +// 搜索功能 |
| 162 | const handleSearch = () => { | 336 | const handleSearch = () => { |
| 163 | - pagination.current = 1 | 337 | + currentPage.value = 1 |
| 164 | - getDictList() | 338 | + fetchDictTypes() |
| 165 | } | 339 | } |
| 166 | 340 | ||
| 167 | -// 重置 | 341 | +// 重置搜索 |
| 168 | const handleReset = () => { | 342 | const handleReset = () => { |
| 169 | - Object.assign(searchForm, { | 343 | + Object.assign(searchParams, { |
| 170 | - dictName: '', | ||
| 171 | dictType: '', | 344 | dictType: '', |
| 172 | - status: undefined | 345 | + dictName: '', |
| 346 | + status: undefined, | ||
| 347 | + pageNum: 1, | ||
| 348 | + pageSize: 10 | ||
| 173 | }) | 349 | }) |
| 174 | - handleSearch() | 350 | + currentPage.value = 1 |
| 351 | + fetchDictTypes() | ||
| 175 | } | 352 | } |
| 176 | 353 | ||
| 177 | -// 新增字典 | 354 | +// 分页处理 |
| 178 | -const handleAdd = () => { | 355 | +const handlePageChange = (page: number) => { |
| 179 | - ElMessage.info('新增字典功能待实现') | 356 | + if (page >= 1 && page <= totalPages.value) { |
| 357 | + currentPage.value = page | ||
| 358 | + fetchDictTypes() | ||
| 359 | + } | ||
| 180 | } | 360 | } |
| 181 | 361 | ||
| 182 | -// 编辑字典 | 362 | +const handlePageSizeChange = () => { |
| 183 | -const handleEdit = (row: DictType) => { | 363 | + currentPage.value = 1 |
| 184 | - ElMessage.info(`编辑字典: ${row.dictName}`) | 364 | + fetchDictTypes() |
| 185 | } | 365 | } |
| 186 | 366 | ||
| 187 | -// 管理字典项 | 367 | +// 全选处理 |
| 188 | -const handleManageItems = (row: DictType) => { | 368 | +const handleSelectAll = (event: Event) => { |
| 189 | - ElMessage.info(`管理字典项: ${row.dictName}`) | 369 | + const target = event.target as HTMLInputElement |
| 370 | + if (target.checked) { | ||
| 371 | + selectedDictTypes.value = dictTypeList.value.map(dictType => dictType.dictTypeId) | ||
| 372 | + } else { | ||
| 373 | + selectedDictTypes.value = [] | ||
| 374 | + } | ||
| 190 | } | 375 | } |
| 191 | 376 | ||
| 192 | -// 删除字典 | 377 | +// 新增字典类型 |
| 193 | -const handleDelete = async (row: DictType) => { | 378 | +const handleAdd = () => { |
| 379 | + dialogTitle.value = '新增字典类型' | ||
| 380 | + clearFieldErrors() | ||
| 381 | + | ||
| 382 | + Object.assign(dictTypeForm, { | ||
| 383 | + dictTypeId: undefined, | ||
| 384 | + dictType: '', | ||
| 385 | + dictName: '', | ||
| 386 | + status: 1, | ||
| 387 | + remark: '' | ||
| 388 | + }) | ||
| 389 | + | ||
| 390 | + showDictTypeDialog.value = true | ||
| 391 | +} | ||
| 392 | + | ||
| 393 | +// 编辑字典类型 | ||
| 394 | +const handleEdit = async (dictType: DictType) => { | ||
| 395 | + dialogTitle.value = '编辑字典类型' | ||
| 396 | + clearFieldErrors() | ||
| 397 | + | ||
| 194 | try { | 398 | try { |
| 195 | - await ElMessageBox.confirm(`确定要删除字典 "${row.dictName}" 吗?`, '提示', { | 399 | + const response = await dictApi.getDictTypeById(dictType.dictTypeId) |
| 196 | - confirmButtonText: '确定', | 400 | + if (response.code === 200 && response.data) { |
| 197 | - cancelButtonText: '取消', | 401 | + const dictTypeDetail = response.data |
| 198 | - type: 'warning' | 402 | + Object.assign(dictTypeForm, { |
| 403 | + dictTypeId: dictTypeDetail.dictTypeId, | ||
| 404 | + dictType: dictTypeDetail.dictType, | ||
| 405 | + dictName: dictTypeDetail.dictName, | ||
| 406 | + status: dictTypeDetail.status, | ||
| 407 | + remark: dictTypeDetail.remark || '' | ||
| 199 | }) | 408 | }) |
| 200 | - ElMessage.success('删除成功') | 409 | + showDictTypeDialog.value = true |
| 201 | - getDictList() | 410 | + } else { |
| 411 | + showMessage(response.message || '获取字典类型详情失败', 'error') | ||
| 412 | + } | ||
| 413 | + } catch (error) { | ||
| 414 | + console.error('获取字典类型详情失败:', error) | ||
| 415 | + showMessage('获取字典类型详情失败,请重试', 'error') | ||
| 416 | + } | ||
| 417 | +} | ||
| 418 | + | ||
| 419 | +// 删除字典类型 | ||
| 420 | +const handleDelete = async (dictType: DictType) => { | ||
| 421 | + if (confirm(`确定要删除字典类型 "${dictType.dictName}" 吗?`)) { | ||
| 422 | + try { | ||
| 423 | + const response = await dictApi.deleteDictTypes([dictType.dictTypeId]) | ||
| 424 | + if (response.code === 200) { | ||
| 425 | + showMessage('删除成功') | ||
| 426 | + fetchDictTypes() | ||
| 427 | + } else { | ||
| 428 | + showMessage(response.message || '删除失败', 'error') | ||
| 429 | + } | ||
| 202 | } catch (error) { | 430 | } catch (error) { |
| 203 | - // 用户取消删除 | 431 | + console.error('删除字典类型失败:', error) |
| 432 | + showMessage('删除字典类型失败,请重试', 'error') | ||
| 433 | + } | ||
| 204 | } | 434 | } |
| 205 | } | 435 | } |
| 206 | 436 | ||
| 207 | -// 批量删除 | 437 | +// 批量删除字典类型 |
| 208 | const handleBatchDelete = async () => { | 438 | const handleBatchDelete = async () => { |
| 439 | + if (selectedDictTypes.value.length === 0) { | ||
| 440 | + showMessage('请选择要删除的字典类型', 'warning') | ||
| 441 | + return | ||
| 442 | + } | ||
| 443 | + | ||
| 444 | + if (confirm(`确定要删除选中的 ${selectedDictTypes.value.length} 个字典类型吗?`)) { | ||
| 209 | try { | 445 | try { |
| 210 | - await ElMessageBox.confirm(`确定要删除选中的 ${multipleSelection.value.length} 个字典吗?`, '提示', { | 446 | + const response = await dictApi.deleteDictTypes(selectedDictTypes.value) |
| 211 | - confirmButtonText: '确定', | 447 | + if (response.code === 200) { |
| 212 | - cancelButtonText: '取消', | 448 | + showMessage('批量删除成功') |
| 213 | - type: 'warning' | 449 | + selectedDictTypes.value = [] |
| 450 | + fetchDictTypes() | ||
| 451 | + } else { | ||
| 452 | + showMessage(response.message || '批量删除失败', 'error') | ||
| 453 | + } | ||
| 454 | + } catch (error) { | ||
| 455 | + console.error('批量删除字典类型失败:', error) | ||
| 456 | + showMessage('批量删除字典类型失败,请重试', 'error') | ||
| 457 | + } | ||
| 458 | + } | ||
| 459 | +} | ||
| 460 | + | ||
| 461 | +// 查看字典项 | ||
| 462 | +const handleViewItems = async (dictType: DictType) => { | ||
| 463 | + // 跳转到字典项管理页面,并传递字典类型ID作为筛选条件 | ||
| 464 | + router.push({ | ||
| 465 | + path: '/main/sys/dict-item', | ||
| 466 | + query: { | ||
| 467 | + dictTypeId: dictType.dictTypeId.toString(), | ||
| 468 | + dictType: dictType.dictType | ||
| 469 | + } | ||
| 214 | }) | 470 | }) |
| 215 | - ElMessage.success('批量删除成功') | 471 | +} |
| 216 | - getDictList() | 472 | + |
| 473 | +// 字典类型表单提交 | ||
| 474 | +const handleSubmit = async () => { | ||
| 475 | + clearFieldErrors() | ||
| 476 | + let hasError = false | ||
| 477 | + | ||
| 478 | + if (!dictTypeForm.dictType.trim()) { | ||
| 479 | + fieldErrors.dictType = '字典类型不能为空' | ||
| 480 | + hasError = true | ||
| 481 | + } else { | ||
| 482 | + // 验证字典类型格式:只能包含小写字母、数字和下划线,且必须以字母开头 | ||
| 483 | + const dictTypePattern = /^[a-z][a-z0-9_]*$/ | ||
| 484 | + if (!dictTypePattern.test(dictTypeForm.dictType)) { | ||
| 485 | + fieldErrors.dictType = '字典类型只能包含小写字母、数字和下划线,且必须以字母开头' | ||
| 486 | + hasError = true | ||
| 487 | + } else if (dictTypeForm.dictType.length > 50) { | ||
| 488 | + fieldErrors.dictType = '字典类型长度不能超过50个字符' | ||
| 489 | + hasError = true | ||
| 490 | + } | ||
| 491 | + } | ||
| 492 | + | ||
| 493 | + if (!dictTypeForm.dictName.trim()) { | ||
| 494 | + fieldErrors.dictName = '字典名称不能为空' | ||
| 495 | + hasError = true | ||
| 496 | + } else if (dictTypeForm.dictName.length > 100) { | ||
| 497 | + fieldErrors.dictName = '字典名称长度不能超过100个字符' | ||
| 498 | + hasError = true | ||
| 499 | + } | ||
| 500 | + | ||
| 501 | + if (hasError) return | ||
| 502 | + | ||
| 503 | + try { | ||
| 504 | + formLoading.value = true | ||
| 505 | + let response | ||
| 506 | + | ||
| 507 | + if (dictTypeForm.dictTypeId) { | ||
| 508 | + response = await dictApi.updateDictType(dictTypeForm) | ||
| 509 | + } else { | ||
| 510 | + response = await dictApi.createDictType(dictTypeForm) | ||
| 511 | + } | ||
| 512 | + | ||
| 513 | + if (response.code === 200) { | ||
| 514 | + showMessage(dictTypeForm.dictTypeId ? '修改成功' : '新增成功') | ||
| 515 | + closeDictTypeDialog() | ||
| 516 | + fetchDictTypes() | ||
| 517 | + } else { | ||
| 518 | + showMessage(response.message || '操作失败', 'error') | ||
| 519 | + } | ||
| 217 | } catch (error) { | 520 | } catch (error) { |
| 218 | - // 用户取消删除 | 521 | + console.error('保存字典类型失败:', error) |
| 522 | + showMessage('保存字典类型失败,请重试', 'error') | ||
| 523 | + } finally { | ||
| 524 | + formLoading.value = false | ||
| 219 | } | 525 | } |
| 220 | } | 526 | } |
| 221 | 527 | ||
| 222 | // 刷新缓存 | 528 | // 刷新缓存 |
| 223 | const handleRefreshCache = async () => { | 529 | const handleRefreshCache = async () => { |
| 224 | try { | 530 | try { |
| 225 | - await ElMessageBox.confirm('确定要刷新字典缓存吗?', '提示', { | 531 | + const response = await dictApi.refreshDictCache() |
| 226 | - confirmButtonText: '确定', | 532 | + if (response.code === 200) { |
| 227 | - cancelButtonText: '取消', | 533 | + showMessage('缓存刷新成功') |
| 228 | - type: 'warning' | 534 | + } else { |
| 229 | - }) | 535 | + showMessage(response.message || '缓存刷新失败', 'error') |
| 230 | - ElMessage.success('缓存刷新成功') | 536 | + } |
| 231 | } catch (error) { | 537 | } catch (error) { |
| 232 | - // 用户取消操作 | 538 | + console.error('刷新缓存失败:', error) |
| 539 | + showMessage('刷新缓存失败,请重试', 'error') | ||
| 233 | } | 540 | } |
| 234 | } | 541 | } |
| 235 | 542 | ||
| 236 | -// 选择变化 | 543 | +// 关闭对话框 |
| 237 | -const handleSelectionChange = (selection: DictType[]) => { | 544 | +const closeDictTypeDialog = () => { |
| 238 | - multipleSelection.value = selection | 545 | + showDictTypeDialog.value = false |
| 546 | + clearFieldErrors() | ||
| 547 | +} | ||
| 548 | + | ||
| 549 | +// 表格工具栏功能 | ||
| 550 | +const handleTableSearch = () => { | ||
| 551 | + document.querySelector('.search-section')?.scrollIntoView({ behavior: 'smooth' }) | ||
| 552 | +} | ||
| 553 | + | ||
| 554 | +const handleTableRefresh = () => { | ||
| 555 | + fetchDictTypes() | ||
| 239 | } | 556 | } |
| 240 | 557 | ||
| 241 | -// 分页大小变化 | 558 | +const handleTableExport = () => { |
| 242 | -const handleSizeChange = (size: number) => { | 559 | + showMessage('导出功能开发中...', 'warning') |
| 243 | - pagination.size = size | ||
| 244 | - getDictList() | ||
| 245 | } | 560 | } |
| 246 | 561 | ||
| 247 | -// 当前页变化 | 562 | +const handleTableViewToggle = () => { |
| 248 | -const handleCurrentChange = (current: number) => { | 563 | + showMessage('视图切换功能开发中...', 'warning') |
| 249 | - pagination.current = current | ||
| 250 | - getDictList() | ||
| 251 | } | 564 | } |
| 252 | 565 | ||
| 253 | -// 组件挂载时获取数据 | 566 | +// 初始化 |
| 254 | onMounted(() => { | 567 | onMounted(() => { |
| 255 | - getDictList() | 568 | + fetchDictTypes() |
| 256 | }) | 569 | }) |
| 257 | </script> | 570 | </script> |
| 258 | 571 | ||
| 259 | -<style lang="scss" scoped> | 572 | +<style scoped lang="scss"> |
| 260 | .dict-management { | 573 | .dict-management { |
| 261 | - .search-form { | 574 | + background: #f5f5f5; |
| 575 | + min-height: 100vh; | ||
| 576 | +} | ||
| 577 | + | ||
| 578 | +// 搜索区域 | ||
| 579 | +.search-section { | ||
| 580 | + background: white; | ||
| 581 | + padding: 16px; | ||
| 582 | + border-bottom: 1px solid #e0e0e0; | ||
| 583 | +} | ||
| 584 | + | ||
| 585 | +.search-row { | ||
| 586 | + display: flex; | ||
| 587 | + gap: 16px; | ||
| 588 | + align-items: center; | ||
| 589 | + flex-wrap: wrap; | ||
| 590 | +} | ||
| 591 | + | ||
| 592 | +.search-item { | ||
| 593 | + display: flex; | ||
| 594 | + align-items: center; | ||
| 595 | + gap: 8px; | ||
| 596 | +} | ||
| 597 | + | ||
| 598 | +.search-item label { | ||
| 599 | + font-size: 12px; | ||
| 600 | + color: #333; | ||
| 601 | + white-space: nowrap; | ||
| 602 | +} | ||
| 603 | + | ||
| 604 | +.search-input, .search-select { | ||
| 605 | + padding: 6px 8px; | ||
| 606 | + border: 1px solid #d9d9d9; | ||
| 607 | + border-radius: 4px; | ||
| 608 | + font-size: 12px; | ||
| 609 | + width: 120px; | ||
| 610 | +} | ||
| 611 | + | ||
| 612 | +.search-input:focus, .search-select:focus { | ||
| 613 | + outline: none; | ||
| 614 | + border-color: #1890ff; | ||
| 615 | +} | ||
| 616 | + | ||
| 617 | +.search-actions { | ||
| 618 | + display: flex; | ||
| 619 | + gap: 8px; | ||
| 620 | +} | ||
| 621 | + | ||
| 622 | +.search-btn { | ||
| 623 | + background: #1890ff; | ||
| 624 | + color: white; | ||
| 625 | + border: none; | ||
| 626 | + padding: 4px 8px; | ||
| 627 | + border-radius: 3px; | ||
| 628 | + font-size: 11px; | ||
| 629 | + cursor: pointer; | ||
| 630 | + height: 24px; | ||
| 631 | + display: flex; | ||
| 632 | + align-items: center; | ||
| 633 | + gap: 4px; | ||
| 634 | + transition: all 0.2s; | ||
| 635 | +} | ||
| 636 | + | ||
| 637 | +.search-btn:hover { | ||
| 638 | + background: #40a9ff; | ||
| 639 | +} | ||
| 640 | + | ||
| 641 | +.reset-btn { | ||
| 642 | + background: #ff7875; | ||
| 643 | + color: white; | ||
| 644 | + border: none; | ||
| 645 | + padding: 4px 8px; | ||
| 646 | + border-radius: 3px; | ||
| 647 | + font-size: 11px; | ||
| 648 | + cursor: pointer; | ||
| 649 | + height: 24px; | ||
| 650 | + display: flex; | ||
| 651 | + align-items: center; | ||
| 652 | + gap: 4px; | ||
| 653 | + transition: all 0.2s; | ||
| 654 | +} | ||
| 655 | + | ||
| 656 | +.reset-btn:hover { | ||
| 657 | + background: #ff9c99; | ||
| 658 | +} | ||
| 659 | + | ||
| 660 | +// 操作区域 | ||
| 661 | +.action-section { | ||
| 662 | + background: white; | ||
| 663 | + padding: 12px 16px; | ||
| 664 | + border-bottom: 1px solid #e0e0e0; | ||
| 665 | +} | ||
| 666 | + | ||
| 667 | +.action-buttons { | ||
| 668 | + display: flex; | ||
| 669 | + gap: 6px; | ||
| 670 | +} | ||
| 671 | + | ||
| 672 | +.action-btn { | ||
| 673 | + padding: 4px 8px; | ||
| 674 | + border: none; | ||
| 675 | + border-radius: 3px; | ||
| 676 | + font-size: 11px; | ||
| 677 | + cursor: pointer; | ||
| 678 | + transition: all 0.2s; | ||
| 679 | +} | ||
| 680 | + | ||
| 681 | +.action-btn.primary { | ||
| 682 | + background: #1890ff; | ||
| 683 | + color: white; | ||
| 684 | +} | ||
| 685 | + | ||
| 686 | +.action-btn.primary:hover { | ||
| 687 | + background: #40a9ff; | ||
| 688 | +} | ||
| 689 | + | ||
| 690 | +.action-btn.danger { | ||
| 691 | + background: #ff4d4f; | ||
| 692 | + color: white; | ||
| 693 | +} | ||
| 694 | + | ||
| 695 | +.action-btn.danger:hover { | ||
| 696 | + background: #ff7875; | ||
| 697 | +} | ||
| 698 | + | ||
| 699 | +.action-btn.secondary { | ||
| 700 | + background: #52c41a; | ||
| 701 | + color: white; | ||
| 702 | +} | ||
| 703 | + | ||
| 704 | +.action-btn.secondary:hover { | ||
| 705 | + background: #73d13d; | ||
| 706 | +} | ||
| 707 | + | ||
| 708 | +.action-btn:disabled { | ||
| 709 | + opacity: 0.5; | ||
| 710 | + cursor: not-allowed; | ||
| 711 | +} | ||
| 712 | + | ||
| 713 | +// 表格区域 | ||
| 714 | +.table-section { | ||
| 715 | + background: white; | ||
| 716 | + margin: 16px; | ||
| 717 | + border-radius: 6px; | ||
| 718 | + overflow: hidden; | ||
| 719 | + box-shadow: 0 2px 8px rgba(0,0,0,0.1); | ||
| 720 | +} | ||
| 721 | + | ||
| 722 | +.table-header { | ||
| 723 | + padding: 8px 16px; | ||
| 724 | + background: #fafafa; | ||
| 725 | + border-bottom: 1px solid #e0e0e0; | ||
| 726 | + display: flex; | ||
| 727 | + justify-content: flex-end; | ||
| 728 | +} | ||
| 729 | + | ||
| 730 | +.table-controls { | ||
| 731 | + display: flex; | ||
| 732 | + gap: 4px; | ||
| 733 | +} | ||
| 734 | + | ||
| 735 | +.control-btn { | ||
| 736 | + background: white; | ||
| 737 | + border: 1px solid #d9d9d9; | ||
| 738 | + border-radius: 3px; | ||
| 739 | + padding: 4px 8px; | ||
| 740 | + font-size: 11px; | ||
| 741 | + cursor: pointer; | ||
| 742 | + transition: all 0.2s; | ||
| 743 | +} | ||
| 744 | + | ||
| 745 | +.control-btn:hover { | ||
| 746 | + background: #f0f8ff; | ||
| 747 | + border-color: #1890ff; | ||
| 748 | + color: #1890ff; | ||
| 749 | +} | ||
| 750 | + | ||
| 751 | +.table-container { | ||
| 752 | + overflow-x: auto; | ||
| 753 | +} | ||
| 754 | + | ||
| 755 | +.data-table { | ||
| 756 | + width: 100%; | ||
| 757 | + border-collapse: collapse; | ||
| 758 | +} | ||
| 759 | + | ||
| 760 | +.data-table th, | ||
| 761 | +.data-table td { | ||
| 762 | + padding: 8px 12px; | ||
| 763 | + text-align: left; | ||
| 764 | + border-bottom: 1px solid #f0f0f0; | ||
| 765 | + font-size: 12px; | ||
| 766 | +} | ||
| 767 | + | ||
| 768 | +.data-table th { | ||
| 769 | + background: #fafafa; | ||
| 770 | + font-weight: 600; | ||
| 771 | + color: #333; | ||
| 772 | +} | ||
| 773 | + | ||
| 774 | +.data-table tbody tr:hover { | ||
| 775 | + background: #f5f5f5; | ||
| 776 | +} | ||
| 777 | + | ||
| 778 | +.data-table tr.selected { | ||
| 779 | + background: #e6f7ff; | ||
| 780 | +} | ||
| 781 | + | ||
| 782 | +.table-checkbox { | ||
| 783 | + margin: 0; | ||
| 784 | + cursor: pointer; | ||
| 785 | +} | ||
| 786 | + | ||
| 787 | +.dict-type-tag { | ||
| 788 | + background: #e6f7ff; | ||
| 789 | + color: #1890ff; | ||
| 790 | + padding: 2px 6px; | ||
| 791 | + border-radius: 3px; | ||
| 792 | + font-size: 11px; | ||
| 793 | + font-family: monospace; | ||
| 794 | +} | ||
| 795 | + | ||
| 796 | +.status-tag { | ||
| 797 | + padding: 2px 6px; | ||
| 798 | + border-radius: 3px; | ||
| 799 | + font-size: 10px; | ||
| 800 | +} | ||
| 801 | + | ||
| 802 | +.status-tag.active { | ||
| 803 | + background: #f6ffed; | ||
| 804 | + color: #52c41a; | ||
| 805 | +} | ||
| 806 | + | ||
| 807 | +.status-tag.inactive { | ||
| 808 | + background: #fff2e8; | ||
| 809 | + color: #fa8c16; | ||
| 810 | +} | ||
| 811 | + | ||
| 812 | +.table-btn { | ||
| 813 | + padding: 2px 6px; | ||
| 814 | + margin-right: 4px; | ||
| 815 | + border: none; | ||
| 816 | + border-radius: 3px; | ||
| 817 | + font-size: 10px; | ||
| 818 | + cursor: pointer; | ||
| 819 | + display: inline-flex; | ||
| 820 | + align-items: center; | ||
| 821 | + gap: 2px; | ||
| 822 | +} | ||
| 823 | + | ||
| 824 | +.table-btn.edit { | ||
| 825 | + background: #1890ff; | ||
| 826 | + color: white; | ||
| 827 | +} | ||
| 828 | + | ||
| 829 | +.table-btn.delete { | ||
| 830 | + background: #ff4d4f; | ||
| 831 | + color: white; | ||
| 832 | +} | ||
| 833 | + | ||
| 834 | +.table-btn.view { | ||
| 835 | + background: #52c41a; | ||
| 836 | + color: white; | ||
| 837 | +} | ||
| 838 | + | ||
| 839 | +// 分页 | ||
| 840 | +.table-footer { | ||
| 841 | + display: flex; | ||
| 842 | + justify-content: space-between; | ||
| 843 | + align-items: center; | ||
| 844 | + padding: 8px 16px; | ||
| 845 | + background: #fafafa; | ||
| 846 | + border-top: 1px solid #e0e0e0; | ||
| 847 | +} | ||
| 848 | + | ||
| 849 | +.pagination-left { | ||
| 850 | + display: flex; | ||
| 851 | + align-items: center; | ||
| 852 | + gap: 20px; | ||
| 853 | +} | ||
| 854 | + | ||
| 855 | +.pagination-info { | ||
| 856 | + font-size: 12px; | ||
| 857 | + color: #666; | ||
| 858 | +} | ||
| 859 | + | ||
| 860 | +.page-size-selector { | ||
| 861 | + display: flex; | ||
| 862 | + align-items: center; | ||
| 863 | + gap: 8px; | ||
| 864 | +} | ||
| 865 | + | ||
| 866 | +.page-size-selector label { | ||
| 867 | + font-size: 12px; | ||
| 868 | + color: #666; | ||
| 869 | +} | ||
| 870 | + | ||
| 871 | +.page-size-select { | ||
| 872 | + padding: 4px 8px; | ||
| 873 | + border: 1px solid #d9d9d9; | ||
| 874 | + border-radius: 4px; | ||
| 875 | + font-size: 12px; | ||
| 876 | + background: white; | ||
| 877 | + cursor: pointer; | ||
| 878 | + transition: border-color 0.3s; | ||
| 879 | +} | ||
| 880 | + | ||
| 881 | +.page-size-select:hover { | ||
| 882 | + border-color: #1890ff; | ||
| 883 | +} | ||
| 884 | + | ||
| 885 | +.page-size-select:focus { | ||
| 886 | + outline: none; | ||
| 887 | + border-color: #1890ff; | ||
| 888 | + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); | ||
| 889 | +} | ||
| 890 | + | ||
| 891 | +/* 新的分页容器样式 - 模拟截图样式 */ | ||
| 892 | +.pagination-container { | ||
| 893 | + display: flex; | ||
| 894 | + align-items: center; | ||
| 895 | + background: #f8f9fa; | ||
| 896 | + border: 1px solid #e9ecef; | ||
| 897 | + border-radius: 8px; | ||
| 898 | + overflow: hidden; | ||
| 899 | + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||
| 900 | +} | ||
| 901 | + | ||
| 902 | +.pagination-btn { | ||
| 903 | + padding: 2px 6px; | ||
| 904 | + border: none; | ||
| 905 | + background: white; | ||
| 906 | + color: #6c757d; | ||
| 907 | + font-size: 11px; | ||
| 908 | + font-weight: 500; | ||
| 909 | + cursor: pointer; | ||
| 910 | + transition: all 0.2s; | ||
| 911 | + border-right: 1px solid #e9ecef; | ||
| 912 | + height: 20px; | ||
| 913 | +} | ||
| 914 | + | ||
| 915 | +.pagination-btn:hover:not(:disabled) { | ||
| 916 | + background: #e9ecef; | ||
| 917 | + color: #495057; | ||
| 918 | +} | ||
| 919 | + | ||
| 920 | +.pagination-btn:disabled { | ||
| 921 | + background: #f8f9fa; | ||
| 922 | + color: #adb5bd; | ||
| 923 | + cursor: not-allowed; | ||
| 924 | +} | ||
| 925 | + | ||
| 926 | +.pagination-pages { | ||
| 927 | + display: flex; | ||
| 928 | + align-items: center; | ||
| 929 | +} | ||
| 930 | + | ||
| 931 | +.page-number { | ||
| 932 | + padding: 2px 6px; | ||
| 933 | + border: none; | ||
| 934 | + background: white; | ||
| 935 | + color: #6c757d; | ||
| 936 | + font-size: 11px; | ||
| 937 | + font-weight: 500; | ||
| 938 | + cursor: pointer; | ||
| 939 | + transition: all 0.2s; | ||
| 940 | + border-right: 1px solid #e9ecef; | ||
| 941 | + min-width: 28px; | ||
| 942 | + text-align: center; | ||
| 943 | + height: 20px; | ||
| 944 | +} | ||
| 945 | + | ||
| 946 | +.page-number:hover { | ||
| 947 | + background: #e9ecef; | ||
| 948 | + color: #495057; | ||
| 949 | +} | ||
| 950 | + | ||
| 951 | +.page-number.active { | ||
| 952 | + background: #6c757d; | ||
| 953 | + color: white; | ||
| 954 | + font-weight: 600; | ||
| 955 | +} | ||
| 956 | + | ||
| 957 | +.page-number.active:hover { | ||
| 958 | + background: #5a6268; | ||
| 959 | +} | ||
| 960 | + | ||
| 961 | +// 对话框 | ||
| 962 | +.dialog-overlay { | ||
| 963 | + position: fixed; | ||
| 964 | + top: 0; | ||
| 965 | + left: 0; | ||
| 966 | + right: 0; | ||
| 967 | + bottom: 0; | ||
| 968 | + background: rgba(0, 0, 0, 0.5); | ||
| 969 | + display: flex; | ||
| 970 | + align-items: center; | ||
| 971 | + justify-content: center; | ||
| 972 | + z-index: 1000; | ||
| 973 | +} | ||
| 974 | + | ||
| 975 | +.dialog-content { | ||
| 976 | + background: white; | ||
| 977 | + border-radius: 8px; | ||
| 978 | + width: 90%; | ||
| 979 | + max-width: 600px; | ||
| 980 | + max-height: 90vh; | ||
| 981 | + overflow-y: auto; | ||
| 982 | +} | ||
| 983 | + | ||
| 984 | +.dialog-header { | ||
| 985 | + padding: 16px 20px; | ||
| 986 | + border-bottom: 1px solid #f0f0f0; | ||
| 987 | + display: flex; | ||
| 988 | + justify-content: space-between; | ||
| 989 | + align-items: center; | ||
| 990 | +} | ||
| 991 | + | ||
| 992 | +.dialog-header h3 { | ||
| 993 | + margin: 0; | ||
| 994 | + font-size: 16px; | ||
| 995 | + color: #333; | ||
| 996 | +} | ||
| 997 | + | ||
| 998 | +.close-btn { | ||
| 999 | + width: 24px; | ||
| 1000 | + height: 24px; | ||
| 1001 | + border: none; | ||
| 1002 | + background: none; | ||
| 1003 | + font-size: 18px; | ||
| 1004 | + cursor: pointer; | ||
| 1005 | + color: #999; | ||
| 1006 | +} | ||
| 1007 | + | ||
| 1008 | +.close-btn:hover { | ||
| 1009 | + color: #333; | ||
| 1010 | +} | ||
| 1011 | + | ||
| 1012 | +.dialog-form { | ||
| 1013 | + padding: 20px; | ||
| 1014 | +} | ||
| 1015 | + | ||
| 1016 | +.form-row { | ||
| 1017 | + display: flex; | ||
| 1018 | + gap: 16px; | ||
| 262 | margin-bottom: 16px; | 1019 | margin-bottom: 16px; |
| 1020 | +} | ||
| 1021 | + | ||
| 1022 | +.form-group { | ||
| 1023 | + flex: 1; | ||
| 1024 | + display: flex; | ||
| 1025 | + flex-direction: column; | ||
| 1026 | + gap: 4px; | ||
| 1027 | +} | ||
| 1028 | + | ||
| 1029 | +.form-group label { | ||
| 1030 | + font-size: 12px; | ||
| 1031 | + color: #333; | ||
| 1032 | + font-weight: 500; | ||
| 1033 | +} | ||
| 1034 | + | ||
| 1035 | +.form-group label.required::after { | ||
| 1036 | + content: ' *'; | ||
| 1037 | + color: #ff4d4f; | ||
| 1038 | +} | ||
| 1039 | + | ||
| 1040 | +.form-input, .form-select { | ||
| 1041 | + padding: 6px 8px; | ||
| 1042 | + border: 1px solid #d9d9d9; | ||
| 1043 | + border-radius: 4px; | ||
| 1044 | + font-size: 12px; | ||
| 1045 | + transition: border-color 0.2s; | ||
| 1046 | +} | ||
| 1047 | + | ||
| 1048 | +.form-input:focus, .form-select:focus { | ||
| 1049 | + border-color: #1890ff; | ||
| 1050 | + outline: none; | ||
| 1051 | +} | ||
| 1052 | + | ||
| 1053 | +.error-message { | ||
| 1054 | + font-size: 10px; | ||
| 1055 | + color: #ff4d4f; | ||
| 1056 | +} | ||
| 1057 | + | ||
| 1058 | +.form-actions { | ||
| 1059 | + display: flex; | ||
| 1060 | + justify-content: flex-end; | ||
| 1061 | + gap: 8px; | ||
| 1062 | + margin-top: 20px; | ||
| 1063 | + padding-top: 16px; | ||
| 1064 | + border-top: 1px solid #f0f0f0; | ||
| 1065 | +} | ||
| 1066 | + | ||
| 1067 | +.cancel-btn, .submit-btn { | ||
| 1068 | + padding: 8px 16px; | ||
| 1069 | + border: 1px solid #d9d9d9; | ||
| 1070 | + border-radius: 4px; | ||
| 1071 | + font-size: 12px; | ||
| 1072 | + cursor: pointer; | ||
| 1073 | + transition: all 0.2s; | ||
| 1074 | +} | ||
| 1075 | + | ||
| 1076 | +.cancel-btn { | ||
| 1077 | + background: white; | ||
| 1078 | + color: #333; | ||
| 1079 | +} | ||
| 1080 | + | ||
| 1081 | +.cancel-btn:hover { | ||
| 1082 | + background: #f5f5f5; | ||
| 1083 | +} | ||
| 1084 | + | ||
| 1085 | +.submit-btn { | ||
| 1086 | + background: #1890ff; | ||
| 1087 | + border-color: #1890ff; | ||
| 1088 | + color: white; | ||
| 1089 | +} | ||
| 1090 | + | ||
| 1091 | +.submit-btn:hover:not(:disabled) { | ||
| 1092 | + background: #40a9ff; | ||
| 1093 | +} | ||
| 1094 | + | ||
| 1095 | +.submit-btn:disabled { | ||
| 1096 | + opacity: 0.5; | ||
| 1097 | + cursor: not-allowed; | ||
| 1098 | +} | ||
| 1099 | + | ||
| 1100 | +// 响应式设计 | ||
| 1101 | +@media (max-width: 768px) { | ||
| 1102 | + .dict-management { | ||
| 1103 | + padding: 10px; | ||
| 263 | } | 1104 | } |
| 264 | 1105 | ||
| 265 | - .operation-buttons { | 1106 | + .search-form { |
| 266 | - margin-bottom: 16px; | 1107 | + flex-direction: column; |
| 1108 | + align-items: stretch; | ||
| 1109 | + } | ||
| 1110 | + | ||
| 1111 | + .form-group { | ||
| 1112 | + min-width: auto; | ||
| 267 | } | 1113 | } |
| 268 | 1114 | ||
| 269 | - .table-container { | 1115 | + .form-row { |
| 270 | - .el-pagination { | 1116 | + flex-direction: column; |
| 271 | - margin-top: 16px; | 1117 | + gap: 8px; |
| 272 | - text-align: right; | ||
| 273 | } | 1118 | } |
| 1119 | + | ||
| 1120 | + .table-footer { | ||
| 1121 | + flex-direction: column; | ||
| 1122 | + gap: 12px; | ||
| 1123 | + align-items: stretch; | ||
| 1124 | + } | ||
| 1125 | + | ||
| 1126 | + .pagination-left { | ||
| 1127 | + flex-direction: column; | ||
| 1128 | + gap: 8px; | ||
| 1129 | + align-items: flex-start; | ||
| 1130 | + } | ||
| 1131 | + | ||
| 1132 | + .pagination-container { | ||
| 1133 | + justify-content: center; | ||
| 1134 | + flex-wrap: wrap; | ||
| 1135 | + } | ||
| 1136 | + | ||
| 1137 | + .pagination-btn { | ||
| 1138 | + padding: 2px 4px; | ||
| 1139 | + font-size: 10px; | ||
| 1140 | + height: 20px; | ||
| 1141 | + } | ||
| 1142 | + | ||
| 1143 | + .page-number { | ||
| 1144 | + padding: 2px 4px; | ||
| 1145 | + font-size: 10px; | ||
| 1146 | + min-width: 24px; | ||
| 1147 | + height: 20px; | ||
| 274 | } | 1148 | } |
| 275 | } | 1149 | } |
| 276 | </style> | 1150 | </style> |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | <div class="menu-management"> | 2 | <div class="menu-management"> |
| 3 | - <!-- 搜索表单 --> | 3 | + <!-- 搜索筛选区域 --> |
| 4 | - <el-card class="search-form"> | 4 | + <div class="search-section"> |
| 5 | - <el-form :model="searchForm" inline> | 5 | + <div class="search-row"> |
| 6 | - <el-form-item label="菜单名称"> | 6 | + <div class="search-item"> |
| 7 | - <el-input v-model="searchForm.menuName" placeholder="请输入菜单名称" clearable /> | 7 | + <label>菜单名称:</label> |
| 8 | - </el-form-item> | 8 | + <input |
| 9 | - <el-form-item label="菜单类型"> | 9 | + v-model="searchParams.menuName" |
| 10 | - <el-select v-model="searchForm.menuType" placeholder="请选择菜单类型" clearable> | 10 | + type="text" |
| 11 | - <el-option label="目录" value="0" /> | 11 | + class="search-input" |
| 12 | - <el-option label="菜单" value="1" /> | 12 | + placeholder="请输入菜单名称" |
| 13 | - <el-option label="按钮" value="2" /> | 13 | + /> |
| 14 | - </el-select> | 14 | + </div> |
| 15 | - </el-form-item> | 15 | + <div class="search-item"> |
| 16 | - <el-form-item label="状态"> | 16 | + <label>菜单类型:</label> |
| 17 | - <el-select v-model="searchForm.status" placeholder="请选择状态" clearable> | 17 | + <select v-model="searchParams.menuType" class="search-select"> |
| 18 | - <el-option label="正常" :value="1" /> | 18 | + <option value="">所有</option> |
| 19 | - <el-option label="停用" :value="0" /> | 19 | + <option value="0">目录</option> |
| 20 | - </el-select> | 20 | + <option value="1">菜单</option> |
| 21 | - </el-form-item> | 21 | + <option value="2">按钮</option> |
| 22 | - <el-form-item> | 22 | + </select> |
| 23 | - <el-button type="primary" @click="handleSearch"> | 23 | + </div> |
| 24 | - <el-icon><Search /></el-icon> | 24 | + <div class="search-item"> |
| 25 | - 搜索 | 25 | + <label>菜单状态:</label> |
| 26 | - </el-button> | 26 | + <select v-model="searchParams.status" class="search-select"> |
| 27 | - <el-button @click="handleReset"> | 27 | + <option value="">所有</option> |
| 28 | - <el-icon><Refresh /></el-icon> | 28 | + <option value="1">正常</option> |
| 29 | - 重置 | 29 | + <option value="0">停用</option> |
| 30 | - </el-button> | 30 | + </select> |
| 31 | - </el-form-item> | 31 | + </div> |
| 32 | - </el-form> | 32 | + <div class="search-actions"> |
| 33 | - </el-card> | 33 | + <button @click="handleSearch" class="search-btn">🔍 搜索</button> |
| 34 | - | 34 | + <button @click="handleReset" class="reset-btn">🔄 重置</button> |
| 35 | - <!-- 操作按钮 --> | 35 | + </div> |
| 36 | - <el-card class="operation-buttons"> | 36 | + </div> |
| 37 | - <el-button type="primary" @click="handleAdd"> | 37 | + </div> |
| 38 | - <el-icon><Plus /></el-icon> | 38 | + |
| 39 | - 新增菜单 | 39 | + <!-- 操作区域 --> |
| 40 | - </el-button> | 40 | + <div class="action-section"> |
| 41 | - <el-button type="danger" :disabled="!multipleSelection.length" @click="handleBatchDelete"> | 41 | + <div class="action-buttons"> |
| 42 | - <el-icon><Delete /></el-icon> | 42 | + <button @click="handleAdd" class="action-btn primary">✨ 新增</button> |
| 43 | - 批量删除 | 43 | + <button @click="handleBatchDelete" class="action-btn danger" :disabled="selectedMenus.length === 0"> |
| 44 | - </el-button> | 44 | + 🗑️ 删除 |
| 45 | - </el-card> | 45 | + </button> |
| 46 | + </div> | ||
| 47 | + </div> | ||
| 46 | 48 | ||
| 47 | <!-- 数据表格 --> | 49 | <!-- 数据表格 --> |
| 48 | - <el-card class="table-container"> | 50 | + <div class="table-section"> |
| 49 | - <el-table | 51 | + <div class="table-header"> |
| 50 | - v-loading="loading" | 52 | + <div class="table-controls"> |
| 51 | - :data="tableData" | 53 | + <button @click="handleTableSearch" class="control-btn" title="搜索"> |
| 52 | - row-key="menuId" | 54 | + 🔍 |
| 53 | - :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" | 55 | + </button> |
| 54 | - @selection-change="handleSelectionChange" | 56 | + <button @click="handleTableRefresh" class="control-btn" title="刷新"> |
| 57 | + 🔄 | ||
| 58 | + </button> | ||
| 59 | + <button @click="handleTableExport" class="control-btn" title="导出"> | ||
| 60 | + 📤 | ||
| 61 | + </button> | ||
| 62 | + <button @click="handleTableViewToggle" class="control-btn" title="视图切换"> | ||
| 63 | + 📋 | ||
| 64 | + </button> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | + | ||
| 68 | + <div class="table-container"> | ||
| 69 | + <table class="data-table"> | ||
| 70 | + <thead> | ||
| 71 | + <tr> | ||
| 72 | + <th> | ||
| 73 | + <input | ||
| 74 | + type="checkbox" | ||
| 75 | + v-model="selectAll" | ||
| 76 | + @change="handleSelectAll" | ||
| 77 | + class="table-checkbox" | ||
| 78 | + > | ||
| 79 | + </th> | ||
| 80 | + <th>菜单名称</th> | ||
| 81 | + <th>图标</th> | ||
| 82 | + <th>排序</th> | ||
| 83 | + <th>权限标识</th> | ||
| 84 | + <th>路由地址</th> | ||
| 85 | + <th>组件路径</th> | ||
| 86 | + <th>菜单类型</th> | ||
| 87 | + <th>状态</th> | ||
| 88 | + <th>创建时间</th> | ||
| 89 | + <th>操作</th> | ||
| 90 | + </tr> | ||
| 91 | + </thead> | ||
| 92 | + <tbody> | ||
| 93 | + <tr v-for="menu in menuList" :key="menu.menuId" :class="{ selected: selectedMenus.includes(menu.menuId) }"> | ||
| 94 | + <td> | ||
| 95 | + <input | ||
| 96 | + type="checkbox" | ||
| 97 | + :value="menu.menuId" | ||
| 98 | + v-model="selectedMenus" | ||
| 99 | + class="table-checkbox" | ||
| 100 | + > | ||
| 101 | + </td> | ||
| 102 | + <td> | ||
| 103 | + <span :style="{ paddingLeft: (menu.level || 0) * 20 + 'px' }"> | ||
| 104 | + {{ menu.menuName }} | ||
| 105 | + </span> | ||
| 106 | + </td> | ||
| 107 | + <td> | ||
| 108 | + <span v-if="menu.icon" class="menu-icon">{{ menu.icon }}</span> | ||
| 109 | + </td> | ||
| 110 | + <td>{{ menu.sort }}</td> | ||
| 111 | + <td> | ||
| 112 | + <span v-if="menu.perms" class="permission-tag">{{ menu.perms }}</span> | ||
| 113 | + </td> | ||
| 114 | + <td>{{ menu.path || '-' }}</td> | ||
| 115 | + <td>{{ menu.component || '-' }}</td> | ||
| 116 | + <td> | ||
| 117 | + <span class="menu-type-tag" :class="`type-${menu.menuType}`"> | ||
| 118 | + {{ menu.menuTypeText }} | ||
| 119 | + </span> | ||
| 120 | + </td> | ||
| 121 | + <td> | ||
| 122 | + <span class="status-tag" :class="menu.status === 1 ? 'active' : 'inactive'"> | ||
| 123 | + {{ menu.statusText }} | ||
| 124 | + </span> | ||
| 125 | + </td> | ||
| 126 | + <td>{{ formatTime(menu.createTime) }}</td> | ||
| 127 | + <td> | ||
| 128 | + <div class="action-buttons"> | ||
| 129 | + <button @click="handleEdit(menu)" class="table-btn edit">✏️ 编辑</button> | ||
| 130 | + <button @click="handleDelete(menu)" class="table-btn delete">🗑️ 删除</button> | ||
| 131 | + </div> | ||
| 132 | + </td> | ||
| 133 | + </tr> | ||
| 134 | + </tbody> | ||
| 135 | + </table> | ||
| 136 | + </div> | ||
| 137 | + | ||
| 138 | + <!-- 分页信息 --> | ||
| 139 | + <div class="table-footer"> | ||
| 140 | + <div class="pagination-left"> | ||
| 141 | + <div class="pagination-info"> | ||
| 142 | + 共 {{ total }} 条记录,第 {{ currentPage }} / {{ totalPages }} 页 | ||
| 143 | + </div> | ||
| 144 | + <div class="page-size-selector"> | ||
| 145 | + <label>每页显示:</label> | ||
| 146 | + <select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> | ||
| 147 | + <option value="10">10条</option> | ||
| 148 | + <option value="20">20条</option> | ||
| 149 | + <option value="50">50条</option> | ||
| 150 | + <option value="100">100条</option> | ||
| 151 | + </select> | ||
| 152 | + </div> | ||
| 153 | + </div> | ||
| 154 | + <div class="pagination-container"> | ||
| 155 | + <button | ||
| 156 | + @click="handlePageChange(currentPage - 1)" | ||
| 157 | + :disabled="currentPage <= 1" | ||
| 158 | + class="pagination-btn prev-btn" | ||
| 159 | + > | ||
| 160 | + 上一页 | ||
| 161 | + </button> | ||
| 162 | + <div class="pagination-pages"> | ||
| 163 | + <button | ||
| 164 | + v-for="page in getPageNumbers()" | ||
| 165 | + :key="page" | ||
| 166 | + @click="handlePageChange(page)" | ||
| 167 | + :class="['page-number', { active: page === currentPage }]" | ||
| 168 | + > | ||
| 169 | + {{ page }} | ||
| 170 | + </button> | ||
| 171 | + </div> | ||
| 172 | + <button | ||
| 173 | + @click="handlePageChange(currentPage + 1)" | ||
| 174 | + :disabled="currentPage >= totalPages" | ||
| 175 | + class="pagination-btn next-btn" | ||
| 176 | + > | ||
| 177 | + 下一页 | ||
| 178 | + </button> | ||
| 179 | + </div> | ||
| 180 | + </div> | ||
| 181 | + </div> | ||
| 182 | + | ||
| 183 | + <!-- 新增/编辑对话框 --> | ||
| 184 | + <div v-if="showMenuDialog" class="dialog-overlay" @click="closeMenuDialog"> | ||
| 185 | + <div class="dialog-content" @click.stop> | ||
| 186 | + <div class="dialog-header"> | ||
| 187 | + <h3>{{ dialogTitle }}</h3> | ||
| 188 | + <button @click="closeMenuDialog" class="close-btn">×</button> | ||
| 189 | + </div> | ||
| 190 | + <form @submit.prevent="handleSubmit" class="dialog-form"> | ||
| 191 | + <div class="form-row"> | ||
| 192 | + <div class="form-group"> | ||
| 193 | + <label class="required">菜单名称</label> | ||
| 194 | + <input | ||
| 195 | + v-model="menuForm.menuName" | ||
| 196 | + type="text" | ||
| 197 | + placeholder="请输入菜单名称" | ||
| 198 | + class="form-input" | ||
| 199 | + required | ||
| 200 | + > | ||
| 201 | + <span v-if="fieldErrors.menuName" class="error-message">{{ fieldErrors.menuName }}</span> | ||
| 202 | + </div> | ||
| 203 | + <div class="form-group"> | ||
| 204 | + <label>父菜单</label> | ||
| 205 | + <select v-model="menuForm.parentId" class="form-select"> | ||
| 206 | + <option value="0">主目录</option> | ||
| 207 | + <option v-for="menu in parentMenus" :key="menu.menuId" :value="menu.menuId"> | ||
| 208 | + {{ menu.menuName }} | ||
| 209 | + </option> | ||
| 210 | + </select> | ||
| 211 | + </div> | ||
| 212 | + </div> | ||
| 213 | + | ||
| 214 | + <div class="form-row"> | ||
| 215 | + <div class="form-group"> | ||
| 216 | + <label>菜单类型</label> | ||
| 217 | + <select v-model="menuForm.menuType" class="form-select"> | ||
| 218 | + <option value="0">目录</option> | ||
| 219 | + <option value="1">菜单</option> | ||
| 220 | + <option value="2">按钮</option> | ||
| 221 | + </select> | ||
| 222 | + </div> | ||
| 223 | + <div class="form-group"> | ||
| 224 | + <label>显示排序</label> | ||
| 225 | + <input | ||
| 226 | + v-model="menuForm.sort" | ||
| 227 | + type="number" | ||
| 228 | + placeholder="请输入显示排序" | ||
| 229 | + class="form-input" | ||
| 230 | + > | ||
| 231 | + </div> | ||
| 232 | + </div> | ||
| 233 | + | ||
| 234 | + <div class="form-row"> | ||
| 235 | + <div class="form-group"> | ||
| 236 | + <label>菜单图标</label> | ||
| 237 | + <input | ||
| 238 | + v-model="menuForm.icon" | ||
| 239 | + type="text" | ||
| 240 | + placeholder="请输入菜单图标" | ||
| 241 | + class="form-input" | ||
| 242 | + > | ||
| 243 | + </div> | ||
| 244 | + <div class="form-group"> | ||
| 245 | + <label>路由地址</label> | ||
| 246 | + <input | ||
| 247 | + v-model="menuForm.path" | ||
| 248 | + type="text" | ||
| 249 | + placeholder="请输入路由地址" | ||
| 250 | + class="form-input" | ||
| 55 | > | 251 | > |
| 56 | - <el-table-column type="selection" width="55" /> | 252 | + </div> |
| 57 | - <el-table-column prop="menuName" label="菜单名称" width="200" /> | 253 | + </div> |
| 58 | - <el-table-column prop="icon" label="图标" width="80" align="center"> | 254 | + |
| 59 | - <template #default="{ row }"> | 255 | + <div class="form-row"> |
| 60 | - <el-icon v-if="row.icon"> | 256 | + <div class="form-group"> |
| 61 | - <component :is="row.icon" /> | 257 | + <label>组件路径</label> |
| 62 | - </el-icon> | 258 | + <input |
| 63 | - </template> | 259 | + v-model="menuForm.component" |
| 64 | - </el-table-column> | 260 | + type="text" |
| 65 | - <el-table-column prop="sort" label="排序" width="80" /> | 261 | + placeholder="请输入组件路径" |
| 66 | - <el-table-column prop="path" label="路由地址" width="200" /> | 262 | + class="form-input" |
| 67 | - <el-table-column prop="component" label="组件路径" width="200" /> | 263 | + > |
| 68 | - <el-table-column prop="perms" label="权限标识" width="200" /> | 264 | + </div> |
| 69 | - <el-table-column prop="menuType" label="菜单类型" width="100"> | 265 | + <div class="form-group"> |
| 70 | - <template #default="{ row }"> | 266 | + <label>权限标识</label> |
| 71 | - <el-tag :type="getMenuTypeTagType(row.menuType)"> | 267 | + <input |
| 72 | - {{ getMenuTypeText(row.menuType) }} | 268 | + v-model="menuForm.perms" |
| 73 | - </el-tag> | 269 | + type="text" |
| 74 | - </template> | 270 | + placeholder="请输入权限标识" |
| 75 | - </el-table-column> | 271 | + class="form-input" |
| 76 | - <el-table-column prop="status" label="状态" width="80"> | 272 | + > |
| 77 | - <template #default="{ row }"> | 273 | + </div> |
| 78 | - <el-tag :type="row.status === 1 ? 'success' : 'danger'"> | 274 | + </div> |
| 79 | - {{ row.status === 1 ? '正常' : '停用' }} | 275 | + |
| 80 | - </el-tag> | 276 | + <div class="form-row"> |
| 81 | - </template> | 277 | + <div class="form-group"> |
| 82 | - </el-table-column> | 278 | + <label>菜单状态</label> |
| 83 | - <el-table-column prop="createTime" label="创建时间" width="180" /> | 279 | + <select v-model="menuForm.status" class="form-select"> |
| 84 | - <el-table-column label="操作" width="200" fixed="right"> | 280 | + <option value="1">正常</option> |
| 85 | - <template #default="{ row }"> | 281 | + <option value="0">停用</option> |
| 86 | - <el-button type="primary" size="small" @click="handleEdit(row)"> | 282 | + </select> |
| 87 | - 编辑 | 283 | + </div> |
| 88 | - </el-button> | 284 | + <div class="form-group"> |
| 89 | - <el-button type="danger" size="small" @click="handleDelete(row)"> | 285 | + <label>是否外链</label> |
| 90 | - 删除 | 286 | + <select v-model="menuForm.isFrame" class="form-select"> |
| 91 | - </el-button> | 287 | + <option value="0">否</option> |
| 92 | - </template> | 288 | + <option value="1">是</option> |
| 93 | - </el-table-column> | 289 | + </select> |
| 94 | - </el-table> | 290 | + </div> |
| 95 | - </el-card> | 291 | + </div> |
| 292 | + | ||
| 293 | + <div class="form-group full-width"> | ||
| 294 | + <label>备注</label> | ||
| 295 | + <textarea | ||
| 296 | + v-model="menuForm.remark" | ||
| 297 | + placeholder="请输入备注" | ||
| 298 | + class="form-textarea" | ||
| 299 | + ></textarea> | ||
| 300 | + </div> | ||
| 301 | + | ||
| 302 | + <div class="form-actions"> | ||
| 303 | + <button type="button" @click="closeMenuDialog" class="cancel-btn">取消</button> | ||
| 304 | + <button type="submit" :disabled="formLoading" class="submit-btn"> | ||
| 305 | + {{ formLoading ? '保存中...' : '保存' }} | ||
| 306 | + </button> | ||
| 307 | + </div> | ||
| 308 | + </form> | ||
| 309 | + </div> | ||
| 310 | + </div> | ||
| 96 | </div> | 311 | </div> |
| 97 | </template> | 312 | </template> |
| 98 | 313 | ||
| 99 | <script setup lang="ts"> | 314 | <script setup lang="ts"> |
| 100 | -import { ref, reactive, onMounted } from 'vue' | 315 | +import { ref, reactive, onMounted, computed } from 'vue' |
| 101 | -import { ElMessage, ElMessageBox } from 'element-plus' | 316 | +import { useRouter } from 'vue-router' |
| 102 | -import type { Menu } from '@/types' | 317 | +import { menuApi, type Menu, type MenuSearchParams } from '../../../api/menu' |
| 318 | + | ||
| 319 | +const router = useRouter() | ||
| 320 | + | ||
| 321 | +// 响应式数据 | ||
| 322 | +const menuList = ref<Menu[]>([]) | ||
| 323 | +const selectedMenus = ref<number[]>([]) | ||
| 324 | +const selectAll = ref(false) | ||
| 325 | +const loading = ref(false) | ||
| 326 | +const formLoading = ref(false) | ||
| 103 | 327 | ||
| 104 | -// 搜索表单 | 328 | +// 搜索参数 |
| 105 | -const searchForm = reactive({ | 329 | +const searchParams = reactive<MenuSearchParams>({ |
| 106 | menuName: '', | 330 | menuName: '', |
| 107 | menuType: '', | 331 | menuType: '', |
| 108 | - status: undefined | 332 | + status: undefined, |
| 333 | + pageNum: 1, | ||
| 334 | + pageSize: 10 | ||
| 109 | }) | 335 | }) |
| 110 | 336 | ||
| 111 | -// 表格数据 | 337 | +// 分页数据 |
| 112 | -const tableData = ref<Menu[]>([]) | 338 | +const currentPage = ref(1) |
| 113 | -const loading = ref(false) | 339 | +const pageSize = ref(10) |
| 114 | -const multipleSelection = ref<Menu[]>([]) | 340 | +const total = ref(0) |
| 341 | +const totalPages = computed(() => Math.ceil(total.value / pageSize.value)) | ||
| 115 | 342 | ||
| 116 | -// 获取菜单列表 | 343 | +// 获取页码数组 |
| 117 | -const getMenuList = async () => { | 344 | +const getPageNumbers = () => { |
| 118 | - loading.value = true | 345 | + const pages = [] |
| 119 | - try { | 346 | + const maxVisible = 5 // 最多显示5个页码 |
| 120 | - // 模拟数据 | 347 | + const start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2)) |
| 121 | - tableData.value = [ | 348 | + const end = Math.min(totalPages.value, start + maxVisible - 1) |
| 122 | - { | 349 | + |
| 123 | - menuId: 1, | 350 | + for (let i = start; i <= end; i++) { |
| 124 | - menuName: '系统管理', | 351 | + pages.push(i) |
| 352 | + } | ||
| 353 | + return pages | ||
| 354 | +} | ||
| 355 | + | ||
| 356 | +// 对话框相关 | ||
| 357 | +const showMenuDialog = ref(false) | ||
| 358 | +const dialogTitle = ref('') | ||
| 359 | +const menuForm = reactive({ | ||
| 360 | + menuId: undefined as number | undefined, | ||
| 361 | + menuName: '', | ||
| 125 | parentId: 0, | 362 | parentId: 0, |
| 126 | - sort: 1, | ||
| 127 | - path: '/system', | ||
| 128 | menuType: '0', | 363 | menuType: '0', |
| 364 | + sort: 0, | ||
| 365 | + icon: '', | ||
| 366 | + path: '', | ||
| 367 | + component: '', | ||
| 129 | perms: '', | 368 | perms: '', |
| 130 | - icon: 'Setting', | ||
| 131 | - status: 1, | ||
| 132 | - createTime: '2024-01-01 10:00:00', | ||
| 133 | - updateTime: '2024-01-01 10:00:00', | ||
| 134 | - children: [ | ||
| 135 | - { | ||
| 136 | - menuId: 2, | ||
| 137 | - menuName: '用户管理', | ||
| 138 | - parentId: 1, | ||
| 139 | - sort: 1, | ||
| 140 | - path: '/system/user', | ||
| 141 | - component: '/sys/user/index', | ||
| 142 | - menuType: '1', | ||
| 143 | - perms: 'sys:user:list', | ||
| 144 | - icon: 'User', | ||
| 145 | status: 1, | 369 | status: 1, |
| 146 | - createTime: '2024-01-01 10:00:00', | 370 | + isFrame: 0, |
| 147 | - updateTime: '2024-01-01 10:00:00' | 371 | + remark: '' |
| 148 | - }, | 372 | +}) |
| 149 | - { | 373 | + |
| 150 | - menuId: 3, | 374 | +const fieldErrors = reactive({ |
| 151 | - menuName: '角色管理', | 375 | + menuName: '' |
| 152 | - parentId: 1, | 376 | +}) |
| 153 | - sort: 2, | 377 | + |
| 154 | - path: '/system/role', | 378 | +const parentMenus = ref<Menu[]>([]) |
| 155 | - component: '/sys/role/index', | 379 | + |
| 156 | - menuType: '1', | 380 | +// 消息提示 |
| 157 | - perms: 'sys:role:list', | 381 | +const showMessage = (message: string, type: 'success' | 'error' | 'warning' = 'success') => { |
| 158 | - icon: 'UserFilled', | 382 | + console.log(`${type.toUpperCase()}: ${message}`) |
| 159 | - status: 1, | 383 | + // 这里可以集成实际的消息提示组件 |
| 160 | - createTime: '2024-01-01 10:00:00', | 384 | + alert(message) |
| 161 | - updateTime: '2024-01-01 10:00:00' | 385 | +} |
| 386 | + | ||
| 387 | +// 格式化时间 | ||
| 388 | +const formatTime = (time: string) => { | ||
| 389 | + if (!time) return '-' | ||
| 390 | + return time.replace('T', ' ').substring(0, 19) | ||
| 391 | +} | ||
| 392 | + | ||
| 393 | +// 清除字段错误 | ||
| 394 | +const clearFieldErrors = () => { | ||
| 395 | + Object.keys(fieldErrors).forEach(key => { | ||
| 396 | + fieldErrors[key as keyof typeof fieldErrors] = '' | ||
| 397 | + }) | ||
| 398 | +} | ||
| 399 | + | ||
| 400 | +// 获取菜单列表 | ||
| 401 | +const fetchMenus = async () => { | ||
| 402 | + try { | ||
| 403 | + loading.value = true | ||
| 404 | + const params = { | ||
| 405 | + ...searchParams, | ||
| 406 | + pageNum: currentPage.value, | ||
| 407 | + pageSize: pageSize.value | ||
| 162 | } | 408 | } |
| 163 | - ] | 409 | + |
| 410 | + const response = await menuApi.getMenuList(params) | ||
| 411 | + if (response.code === 200 && response.data) { | ||
| 412 | + menuList.value = response.data.records || [] | ||
| 413 | + total.value = response.data.total || 0 | ||
| 414 | + } else { | ||
| 415 | + showMessage(response.message || '获取菜单列表失败', 'error') | ||
| 164 | } | 416 | } |
| 165 | - ] | ||
| 166 | } catch (error) { | 417 | } catch (error) { |
| 167 | - ElMessage.error('获取菜单列表失败') | 418 | + console.error('获取菜单列表失败:', error) |
| 419 | + showMessage('获取菜单列表失败,请重试', 'error') | ||
| 168 | } finally { | 420 | } finally { |
| 169 | loading.value = false | 421 | loading.value = false |
| 170 | } | 422 | } |
| 171 | } | 423 | } |
| 172 | 424 | ||
| 173 | -// 获取菜单类型标签类型 | 425 | +// 获取父菜单列表 |
| 174 | -const getMenuTypeTagType = (menuType: string) => { | 426 | +const fetchParentMenus = async () => { |
| 175 | - switch (menuType) { | 427 | + try { |
| 176 | - case '0': | 428 | + const response = await menuApi.getMenuTreeSelect() |
| 177 | - return 'primary' | 429 | + if (response.code === 200 && response.data) { |
| 178 | - case '1': | 430 | + parentMenus.value = response.data |
| 179 | - return 'success' | ||
| 180 | - case '2': | ||
| 181 | - return 'warning' | ||
| 182 | - default: | ||
| 183 | - return 'info' | ||
| 184 | } | 431 | } |
| 185 | -} | 432 | + } catch (error) { |
| 186 | - | 433 | + console.error('获取父菜单列表失败:', error) |
| 187 | -// 获取菜单类型文本 | ||
| 188 | -const getMenuTypeText = (menuType: string) => { | ||
| 189 | - switch (menuType) { | ||
| 190 | - case '0': | ||
| 191 | - return '目录' | ||
| 192 | - case '1': | ||
| 193 | - return '菜单' | ||
| 194 | - case '2': | ||
| 195 | - return '按钮' | ||
| 196 | - default: | ||
| 197 | - return '未知' | ||
| 198 | } | 434 | } |
| 199 | } | 435 | } |
| 200 | 436 | ||
| 201 | -// 搜索 | 437 | +// 搜索功能 |
| 202 | const handleSearch = () => { | 438 | const handleSearch = () => { |
| 203 | - getMenuList() | 439 | + currentPage.value = 1 |
| 440 | + fetchMenus() | ||
| 204 | } | 441 | } |
| 205 | 442 | ||
| 206 | -// 重置 | 443 | +// 重置搜索 |
| 207 | const handleReset = () => { | 444 | const handleReset = () => { |
| 208 | - Object.assign(searchForm, { | 445 | + Object.assign(searchParams, { |
| 209 | menuName: '', | 446 | menuName: '', |
| 210 | menuType: '', | 447 | menuType: '', |
| 211 | - status: undefined | 448 | + status: undefined, |
| 449 | + pageNum: 1, | ||
| 450 | + pageSize: 10 | ||
| 212 | }) | 451 | }) |
| 213 | - handleSearch() | 452 | + currentPage.value = 1 |
| 453 | + fetchMenus() | ||
| 454 | +} | ||
| 455 | + | ||
| 456 | +// 分页处理 | ||
| 457 | +const handlePageChange = (page: number) => { | ||
| 458 | + if (page >= 1 && page <= totalPages.value) { | ||
| 459 | + currentPage.value = page | ||
| 460 | + fetchMenus() | ||
| 461 | + } | ||
| 462 | +} | ||
| 463 | + | ||
| 464 | +const handlePageSizeChange = () => { | ||
| 465 | + currentPage.value = 1 | ||
| 466 | + fetchMenus() | ||
| 467 | +} | ||
| 468 | + | ||
| 469 | +// 全选处理 | ||
| 470 | +const handleSelectAll = (event: Event) => { | ||
| 471 | + const target = event.target as HTMLInputElement | ||
| 472 | + if (target.checked) { | ||
| 473 | + selectedMenus.value = menuList.value.map(menu => menu.menuId) | ||
| 474 | + } else { | ||
| 475 | + selectedMenus.value = [] | ||
| 476 | + } | ||
| 214 | } | 477 | } |
| 215 | 478 | ||
| 216 | // 新增菜单 | 479 | // 新增菜单 |
| 217 | const handleAdd = () => { | 480 | const handleAdd = () => { |
| 218 | - ElMessage.info('新增菜单功能待实现') | 481 | + dialogTitle.value = '新增菜单' |
| 482 | + clearFieldErrors() | ||
| 483 | + | ||
| 484 | + // 重置表单 | ||
| 485 | + Object.assign(menuForm, { | ||
| 486 | + menuId: undefined, | ||
| 487 | + menuName: '', | ||
| 488 | + parentId: 0, | ||
| 489 | + menuType: '0', | ||
| 490 | + sort: 0, | ||
| 491 | + icon: '', | ||
| 492 | + path: '', | ||
| 493 | + component: '', | ||
| 494 | + perms: '', | ||
| 495 | + status: 1, | ||
| 496 | + isFrame: 0, | ||
| 497 | + remark: '' | ||
| 498 | + }) | ||
| 499 | + | ||
| 500 | + showMenuDialog.value = true | ||
| 219 | } | 501 | } |
| 220 | 502 | ||
| 221 | // 编辑菜单 | 503 | // 编辑菜单 |
| 222 | -const handleEdit = (row: Menu) => { | 504 | +const handleEdit = async (menu: Menu) => { |
| 223 | - ElMessage.info(`编辑菜单: ${row.menuName}`) | 505 | + dialogTitle.value = '编辑菜单' |
| 506 | + clearFieldErrors() | ||
| 507 | + | ||
| 508 | + try { | ||
| 509 | + const response = await menuApi.getMenuById(menu.menuId) | ||
| 510 | + if (response.code === 200 && response.data) { | ||
| 511 | + const menuDetail = response.data | ||
| 512 | + Object.assign(menuForm, { | ||
| 513 | + menuId: menuDetail.menuId, | ||
| 514 | + menuName: menuDetail.menuName, | ||
| 515 | + parentId: menuDetail.parentId || 0, | ||
| 516 | + menuType: menuDetail.menuType, | ||
| 517 | + sort: menuDetail.sort, | ||
| 518 | + icon: menuDetail.icon || '', | ||
| 519 | + path: menuDetail.path || '', | ||
| 520 | + component: (menuDetail as any).component || '', | ||
| 521 | + perms: menuDetail.perms || '', | ||
| 522 | + status: menuDetail.status, | ||
| 523 | + isFrame: (menuDetail as any).isFrame || 0, | ||
| 524 | + remark: (menuDetail as any).remark || '' | ||
| 525 | + }) | ||
| 526 | + showMenuDialog.value = true | ||
| 527 | + } else { | ||
| 528 | + showMessage(response.message || '获取菜单详情失败', 'error') | ||
| 529 | + } | ||
| 530 | + } catch (error) { | ||
| 531 | + console.error('获取菜单详情失败:', error) | ||
| 532 | + showMessage('获取菜单详情失败,请重试', 'error') | ||
| 533 | + } | ||
| 224 | } | 534 | } |
| 225 | 535 | ||
| 226 | // 删除菜单 | 536 | // 删除菜单 |
| 227 | -const handleDelete = async (row: Menu) => { | 537 | +const handleDelete = async (menu: Menu) => { |
| 538 | + if (confirm(`确定要删除菜单 "${menu.menuName}" 吗?`)) { | ||
| 228 | try { | 539 | try { |
| 229 | - await ElMessageBox.confirm(`确定要删除菜单 "${row.menuName}" 吗?`, '提示', { | 540 | + const response = await menuApi.deleteMenu([menu.menuId]) |
| 230 | - confirmButtonText: '确定', | 541 | + if (response.code === 200) { |
| 231 | - cancelButtonText: '取消', | 542 | + showMessage('删除成功') |
| 232 | - type: 'warning' | 543 | + fetchMenus() |
| 233 | - }) | 544 | + } else { |
| 234 | - ElMessage.success('删除成功') | 545 | + showMessage(response.message || '删除失败', 'error') |
| 235 | - getMenuList() | 546 | + } |
| 236 | } catch (error) { | 547 | } catch (error) { |
| 237 | - // 用户取消删除 | 548 | + console.error('删除菜单失败:', error) |
| 549 | + showMessage('删除菜单失败,请重试', 'error') | ||
| 550 | + } | ||
| 238 | } | 551 | } |
| 239 | } | 552 | } |
| 240 | 553 | ||
| 241 | // 批量删除 | 554 | // 批量删除 |
| 242 | const handleBatchDelete = async () => { | 555 | const handleBatchDelete = async () => { |
| 556 | + if (selectedMenus.value.length === 0) { | ||
| 557 | + showMessage('请选择要删除的菜单', 'warning') | ||
| 558 | + return | ||
| 559 | + } | ||
| 560 | + | ||
| 561 | + if (confirm(`确定要删除选中的 ${selectedMenus.value.length} 个菜单吗?`)) { | ||
| 243 | try { | 562 | try { |
| 244 | - await ElMessageBox.confirm(`确定要删除选中的 ${multipleSelection.value.length} 个菜单吗?`, '提示', { | 563 | + const response = await menuApi.deleteMenu(selectedMenus.value) |
| 245 | - confirmButtonText: '确定', | 564 | + if (response.code === 200) { |
| 246 | - cancelButtonText: '取消', | 565 | + showMessage('批量删除成功') |
| 247 | - type: 'warning' | 566 | + selectedMenus.value = [] |
| 248 | - }) | 567 | + fetchMenus() |
| 249 | - ElMessage.success('批量删除成功') | 568 | + } else { |
| 250 | - getMenuList() | 569 | + showMessage(response.message || '批量删除失败', 'error') |
| 570 | + } | ||
| 571 | + } catch (error) { | ||
| 572 | + console.error('批量删除菜单失败:', error) | ||
| 573 | + showMessage('批量删除菜单失败,请重试', 'error') | ||
| 574 | + } | ||
| 575 | + } | ||
| 576 | +} | ||
| 577 | + | ||
| 578 | +// 表单提交 | ||
| 579 | +const handleSubmit = async () => { | ||
| 580 | + // 表单验证 | ||
| 581 | + clearFieldErrors() | ||
| 582 | + let hasError = false | ||
| 583 | + | ||
| 584 | + if (!menuForm.menuName.trim()) { | ||
| 585 | + fieldErrors.menuName = '菜单名称不能为空' | ||
| 586 | + hasError = true | ||
| 587 | + } | ||
| 588 | + | ||
| 589 | + if (hasError) return | ||
| 590 | + | ||
| 591 | + try { | ||
| 592 | + formLoading.value = true | ||
| 593 | + let response | ||
| 594 | + | ||
| 595 | + if (menuForm.menuId) { | ||
| 596 | + // 编辑 | ||
| 597 | + response = await menuApi.updateMenu(menuForm) | ||
| 598 | + } else { | ||
| 599 | + // 新增 | ||
| 600 | + response = await menuApi.createMenu(menuForm) | ||
| 601 | + } | ||
| 602 | + | ||
| 603 | + if (response.code === 200) { | ||
| 604 | + showMessage(menuForm.menuId ? '修改成功' : '新增成功') | ||
| 605 | + closeMenuDialog() | ||
| 606 | + fetchMenus() | ||
| 607 | + } else { | ||
| 608 | + showMessage(response.message || '操作失败', 'error') | ||
| 609 | + } | ||
| 251 | } catch (error) { | 610 | } catch (error) { |
| 252 | - // 用户取消删除 | 611 | + console.error('保存菜单失败:', error) |
| 612 | + showMessage('保存菜单失败,请重试', 'error') | ||
| 613 | + } finally { | ||
| 614 | + formLoading.value = false | ||
| 253 | } | 615 | } |
| 254 | } | 616 | } |
| 255 | 617 | ||
| 256 | -// 选择变化 | 618 | +// 关闭对话框 |
| 257 | -const handleSelectionChange = (selection: Menu[]) => { | 619 | +const closeMenuDialog = () => { |
| 258 | - multipleSelection.value = selection | 620 | + showMenuDialog.value = false |
| 621 | + clearFieldErrors() | ||
| 622 | +} | ||
| 623 | + | ||
| 624 | +// 表格工具栏功能 | ||
| 625 | +const handleTableSearch = () => { | ||
| 626 | + // 滚动到搜索区域 | ||
| 627 | + document.querySelector('.search-section')?.scrollIntoView({ behavior: 'smooth' }) | ||
| 259 | } | 628 | } |
| 260 | 629 | ||
| 261 | -// 组件挂载时获取数据 | 630 | +const handleTableRefresh = () => { |
| 631 | + fetchMenus() | ||
| 632 | +} | ||
| 633 | + | ||
| 634 | +const handleTableExport = () => { | ||
| 635 | + showMessage('导出功能开发中...', 'warning') | ||
| 636 | +} | ||
| 637 | + | ||
| 638 | +const handleTableViewToggle = () => { | ||
| 639 | + showMessage('视图切换功能开发中...', 'warning') | ||
| 640 | +} | ||
| 641 | + | ||
| 642 | +// 初始化 | ||
| 262 | onMounted(() => { | 643 | onMounted(() => { |
| 263 | - getMenuList() | 644 | + fetchMenus() |
| 645 | + fetchParentMenus() | ||
| 264 | }) | 646 | }) |
| 265 | </script> | 647 | </script> |
| 266 | 648 | ||
| 267 | -<style lang="scss" scoped> | 649 | +<style scoped lang="scss"> |
| 268 | .menu-management { | 650 | .menu-management { |
| 269 | - .search-form { | 651 | + background: #f5f5f5; |
| 652 | + min-height: 100vh; | ||
| 653 | +} | ||
| 654 | + | ||
| 655 | +// 搜索区域 | ||
| 656 | +.search-section { | ||
| 657 | + background: white; | ||
| 658 | + padding: 16px; | ||
| 659 | + border-bottom: 1px solid #e0e0e0; | ||
| 660 | +} | ||
| 661 | + | ||
| 662 | +.search-row { | ||
| 663 | + display: flex; | ||
| 664 | + gap: 16px; | ||
| 665 | + align-items: center; | ||
| 666 | + flex-wrap: wrap; | ||
| 667 | +} | ||
| 668 | + | ||
| 669 | +.search-item { | ||
| 670 | + display: flex; | ||
| 671 | + align-items: center; | ||
| 672 | + gap: 8px; | ||
| 673 | +} | ||
| 674 | + | ||
| 675 | +.search-item label { | ||
| 676 | + font-size: 12px; | ||
| 677 | + color: #333; | ||
| 678 | + white-space: nowrap; | ||
| 679 | +} | ||
| 680 | + | ||
| 681 | +.search-input, .search-select { | ||
| 682 | + padding: 6px 8px; | ||
| 683 | + border: 1px solid #d9d9d9; | ||
| 684 | + border-radius: 4px; | ||
| 685 | + font-size: 12px; | ||
| 686 | + width: 120px; | ||
| 687 | +} | ||
| 688 | + | ||
| 689 | +.search-input:focus, .search-select:focus { | ||
| 690 | + outline: none; | ||
| 691 | + border-color: #1890ff; | ||
| 692 | +} | ||
| 693 | + | ||
| 694 | +.search-actions { | ||
| 695 | + display: flex; | ||
| 696 | + gap: 8px; | ||
| 697 | +} | ||
| 698 | + | ||
| 699 | +.search-btn { | ||
| 700 | + background: #1890ff; | ||
| 701 | + color: white; | ||
| 702 | + border: none; | ||
| 703 | + padding: 4px 8px; | ||
| 704 | + border-radius: 3px; | ||
| 705 | + font-size: 11px; | ||
| 706 | + cursor: pointer; | ||
| 707 | + height: 24px; | ||
| 708 | + display: flex; | ||
| 709 | + align-items: center; | ||
| 710 | + gap: 4px; | ||
| 711 | + transition: all 0.2s; | ||
| 712 | +} | ||
| 713 | + | ||
| 714 | +.search-btn:hover { | ||
| 715 | + background: #40a9ff; | ||
| 716 | +} | ||
| 717 | + | ||
| 718 | +.reset-btn { | ||
| 719 | + background: #ff7875; | ||
| 720 | + color: white; | ||
| 721 | + border: none; | ||
| 722 | + padding: 4px 8px; | ||
| 723 | + border-radius: 3px; | ||
| 724 | + font-size: 11px; | ||
| 725 | + cursor: pointer; | ||
| 726 | + height: 24px; | ||
| 727 | + display: flex; | ||
| 728 | + align-items: center; | ||
| 729 | + gap: 4px; | ||
| 730 | + transition: all 0.2s; | ||
| 731 | +} | ||
| 732 | + | ||
| 733 | +.reset-btn:hover { | ||
| 734 | + background: #ff9c99; | ||
| 735 | +} | ||
| 736 | + | ||
| 737 | +// 操作区域 | ||
| 738 | +.action-section { | ||
| 739 | + background: white; | ||
| 740 | + padding: 12px 16px; | ||
| 741 | + border-bottom: 1px solid #e0e0e0; | ||
| 742 | +} | ||
| 743 | + | ||
| 744 | +.action-buttons { | ||
| 745 | + display: flex; | ||
| 746 | + gap: 6px; | ||
| 747 | +} | ||
| 748 | + | ||
| 749 | +.action-btn { | ||
| 750 | + padding: 4px 8px; | ||
| 751 | + border: none; | ||
| 752 | + border-radius: 3px; | ||
| 753 | + font-size: 11px; | ||
| 754 | + cursor: pointer; | ||
| 755 | + transition: all 0.2s; | ||
| 756 | +} | ||
| 757 | + | ||
| 758 | +.action-btn.primary { | ||
| 759 | + background: #1890ff; | ||
| 760 | + color: white; | ||
| 761 | +} | ||
| 762 | + | ||
| 763 | +.action-btn.primary:hover { | ||
| 764 | + background: #40a9ff; | ||
| 765 | +} | ||
| 766 | + | ||
| 767 | +.action-btn.danger { | ||
| 768 | + background: #ff4d4f; | ||
| 769 | + color: white; | ||
| 770 | +} | ||
| 771 | + | ||
| 772 | +.action-btn.danger:hover { | ||
| 773 | + background: #ff7875; | ||
| 774 | +} | ||
| 775 | + | ||
| 776 | +.action-btn:disabled { | ||
| 777 | + opacity: 0.5; | ||
| 778 | + cursor: not-allowed; | ||
| 779 | +} | ||
| 780 | + | ||
| 781 | +// 表格区域 | ||
| 782 | +.table-section { | ||
| 783 | + background: white; | ||
| 784 | + margin: 16px; | ||
| 785 | + border-radius: 6px; | ||
| 786 | + overflow: hidden; | ||
| 787 | + box-shadow: 0 2px 8px rgba(0,0,0,0.1); | ||
| 788 | +} | ||
| 789 | + | ||
| 790 | +.table-header { | ||
| 791 | + padding: 8px 16px; | ||
| 792 | + background: #fafafa; | ||
| 793 | + border-bottom: 1px solid #e0e0e0; | ||
| 794 | + display: flex; | ||
| 795 | + justify-content: flex-end; | ||
| 796 | +} | ||
| 797 | + | ||
| 798 | +.table-controls { | ||
| 799 | + display: flex; | ||
| 800 | + gap: 4px; | ||
| 801 | +} | ||
| 802 | + | ||
| 803 | +.control-btn { | ||
| 804 | + background: white; | ||
| 805 | + border: 1px solid #d9d9d9; | ||
| 806 | + border-radius: 3px; | ||
| 807 | + padding: 4px 8px; | ||
| 808 | + font-size: 11px; | ||
| 809 | + cursor: pointer; | ||
| 810 | + transition: all 0.2s; | ||
| 811 | +} | ||
| 812 | + | ||
| 813 | +.control-btn:hover { | ||
| 814 | + background: #f0f8ff; | ||
| 815 | + border-color: #1890ff; | ||
| 816 | + color: #1890ff; | ||
| 817 | +} | ||
| 818 | + | ||
| 819 | +.table-container { | ||
| 820 | + overflow-x: auto; | ||
| 821 | +} | ||
| 822 | + | ||
| 823 | +.data-table { | ||
| 824 | + width: 100%; | ||
| 825 | + border-collapse: collapse; | ||
| 826 | +} | ||
| 827 | + | ||
| 828 | +.data-table th, | ||
| 829 | +.data-table td { | ||
| 830 | + padding: 8px 12px; | ||
| 831 | + text-align: left; | ||
| 832 | + border-bottom: 1px solid #f0f0f0; | ||
| 833 | + font-size: 12px; | ||
| 834 | +} | ||
| 835 | + | ||
| 836 | +.data-table th { | ||
| 837 | + background: #fafafa; | ||
| 838 | + font-weight: 600; | ||
| 839 | + color: #333; | ||
| 840 | +} | ||
| 841 | + | ||
| 842 | +.data-table tbody tr:hover { | ||
| 843 | + background: #f5f5f5; | ||
| 844 | +} | ||
| 845 | + | ||
| 846 | +.data-table tr.selected { | ||
| 847 | + background: #e6f7ff; | ||
| 848 | +} | ||
| 849 | + | ||
| 850 | +.table-checkbox { | ||
| 851 | + margin: 0; | ||
| 852 | + cursor: pointer; | ||
| 853 | +} | ||
| 854 | + | ||
| 855 | +.menu-icon { | ||
| 856 | + font-size: 16px; | ||
| 857 | +} | ||
| 858 | + | ||
| 859 | +.permission-tag { | ||
| 860 | + background: #f0f0f0; | ||
| 861 | + color: #666; | ||
| 862 | + padding: 2px 6px; | ||
| 863 | + border-radius: 3px; | ||
| 864 | + font-size: 11px; | ||
| 865 | + font-family: monospace; | ||
| 866 | +} | ||
| 867 | + | ||
| 868 | +.menu-type-tag { | ||
| 869 | + padding: 2px 6px; | ||
| 870 | + border-radius: 3px; | ||
| 871 | + font-size: 11px; | ||
| 872 | +} | ||
| 873 | + | ||
| 874 | +.menu-type-tag.type-0 { | ||
| 875 | + background: #e6f7ff; | ||
| 876 | + color: #1890ff; | ||
| 877 | +} | ||
| 878 | + | ||
| 879 | +.menu-type-tag.type-1 { | ||
| 880 | + background: #f6ffed; | ||
| 881 | + color: #52c41a; | ||
| 882 | +} | ||
| 883 | + | ||
| 884 | +.menu-type-tag.type-2 { | ||
| 885 | + background: #fff2e8; | ||
| 886 | + color: #fa8c16; | ||
| 887 | +} | ||
| 888 | + | ||
| 889 | +.status-tag { | ||
| 890 | + padding: 2px 6px; | ||
| 891 | + border-radius: 3px; | ||
| 892 | + font-size: 10px; | ||
| 893 | +} | ||
| 894 | + | ||
| 895 | +.status-tag.active { | ||
| 896 | + background: #f6ffed; | ||
| 897 | + color: #52c41a; | ||
| 898 | +} | ||
| 899 | + | ||
| 900 | +.status-tag.inactive { | ||
| 901 | + background: #fff2e8; | ||
| 902 | + color: #fa8c16; | ||
| 903 | +} | ||
| 904 | + | ||
| 905 | +.table-btn { | ||
| 906 | + padding: 2px 6px; | ||
| 907 | + margin-right: 4px; | ||
| 908 | + border: none; | ||
| 909 | + border-radius: 3px; | ||
| 910 | + font-size: 10px; | ||
| 911 | + cursor: pointer; | ||
| 912 | + display: inline-flex; | ||
| 913 | + align-items: center; | ||
| 914 | + gap: 2px; | ||
| 915 | +} | ||
| 916 | + | ||
| 917 | +.table-btn.edit { | ||
| 918 | + background: #1890ff; | ||
| 919 | + color: white; | ||
| 920 | +} | ||
| 921 | + | ||
| 922 | +.table-btn.delete { | ||
| 923 | + background: #ff4d4f; | ||
| 924 | + color: white; | ||
| 925 | +} | ||
| 926 | + | ||
| 927 | +// 分页 | ||
| 928 | +.table-footer { | ||
| 929 | + display: flex; | ||
| 930 | + justify-content: space-between; | ||
| 931 | + align-items: center; | ||
| 932 | + padding: 8px 16px; | ||
| 933 | + background: #fafafa; | ||
| 934 | + border-top: 1px solid #e0e0e0; | ||
| 935 | +} | ||
| 936 | + | ||
| 937 | +.pagination-left { | ||
| 938 | + display: flex; | ||
| 939 | + align-items: center; | ||
| 940 | + gap: 20px; | ||
| 941 | +} | ||
| 942 | + | ||
| 943 | +.pagination-info { | ||
| 944 | + font-size: 12px; | ||
| 945 | + color: #666; | ||
| 946 | +} | ||
| 947 | + | ||
| 948 | +.page-size-selector { | ||
| 949 | + display: flex; | ||
| 950 | + align-items: center; | ||
| 951 | + gap: 8px; | ||
| 952 | +} | ||
| 953 | + | ||
| 954 | +.page-size-selector label { | ||
| 955 | + font-size: 12px; | ||
| 956 | + color: #666; | ||
| 957 | +} | ||
| 958 | + | ||
| 959 | +.page-size-select { | ||
| 960 | + padding: 4px 8px; | ||
| 961 | + border: 1px solid #d9d9d9; | ||
| 962 | + border-radius: 4px; | ||
| 963 | + font-size: 12px; | ||
| 964 | + background: white; | ||
| 965 | + cursor: pointer; | ||
| 966 | + transition: border-color 0.3s; | ||
| 967 | +} | ||
| 968 | + | ||
| 969 | +.page-size-select:hover { | ||
| 970 | + border-color: #1890ff; | ||
| 971 | +} | ||
| 972 | + | ||
| 973 | +.page-size-select:focus { | ||
| 974 | + outline: none; | ||
| 975 | + border-color: #1890ff; | ||
| 976 | + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); | ||
| 977 | +} | ||
| 978 | + | ||
| 979 | +/* 新的分页容器样式 - 模拟截图样式 */ | ||
| 980 | +.pagination-container { | ||
| 981 | + display: flex; | ||
| 982 | + align-items: center; | ||
| 983 | + background: #f8f9fa; | ||
| 984 | + border: 1px solid #e9ecef; | ||
| 985 | + border-radius: 8px; | ||
| 986 | + overflow: hidden; | ||
| 987 | + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||
| 988 | +} | ||
| 989 | + | ||
| 990 | +.pagination-btn { | ||
| 991 | + padding: 2px 6px; | ||
| 992 | + border: none; | ||
| 993 | + background: white; | ||
| 994 | + color: #6c757d; | ||
| 995 | + font-size: 11px; | ||
| 996 | + font-weight: 500; | ||
| 997 | + cursor: pointer; | ||
| 998 | + transition: all 0.2s; | ||
| 999 | + border-right: 1px solid #e9ecef; | ||
| 1000 | + height: 20px; | ||
| 1001 | +} | ||
| 1002 | + | ||
| 1003 | +.pagination-btn:hover:not(:disabled) { | ||
| 1004 | + background: #e9ecef; | ||
| 1005 | + color: #495057; | ||
| 1006 | +} | ||
| 1007 | + | ||
| 1008 | +.pagination-btn:disabled { | ||
| 1009 | + background: #f8f9fa; | ||
| 1010 | + color: #adb5bd; | ||
| 1011 | + cursor: not-allowed; | ||
| 1012 | +} | ||
| 1013 | + | ||
| 1014 | +.page-number { | ||
| 1015 | + padding: 2px 6px; | ||
| 1016 | + border: none; | ||
| 1017 | + background: white; | ||
| 1018 | + color: #6c757d; | ||
| 1019 | + font-size: 11px; | ||
| 1020 | + font-weight: 500; | ||
| 1021 | + cursor: pointer; | ||
| 1022 | + transition: all 0.2s; | ||
| 1023 | + border-right: 1px solid #e9ecef; | ||
| 1024 | + min-width: 28px; | ||
| 1025 | + text-align: center; | ||
| 1026 | + height: 20px; | ||
| 1027 | +} | ||
| 1028 | + | ||
| 1029 | +.page-number:hover { | ||
| 1030 | + background: #e9ecef; | ||
| 1031 | + color: #495057; | ||
| 1032 | +} | ||
| 1033 | + | ||
| 1034 | +.page-number.active { | ||
| 1035 | + background: #6c757d; | ||
| 1036 | + color: white; | ||
| 1037 | + font-weight: 600; | ||
| 1038 | +} | ||
| 1039 | + | ||
| 1040 | +.page-number.active:hover { | ||
| 1041 | + background: #5a6268; | ||
| 1042 | +} | ||
| 1043 | + | ||
| 1044 | +.pagination-pages { | ||
| 1045 | + display: flex; | ||
| 1046 | + align-items: center; | ||
| 1047 | +} | ||
| 1048 | + | ||
| 1049 | +// 对话框 | ||
| 1050 | +.dialog-overlay { | ||
| 1051 | + position: fixed; | ||
| 1052 | + top: 0; | ||
| 1053 | + left: 0; | ||
| 1054 | + right: 0; | ||
| 1055 | + bottom: 0; | ||
| 1056 | + background: rgba(0, 0, 0, 0.5); | ||
| 1057 | + display: flex; | ||
| 1058 | + align-items: center; | ||
| 1059 | + justify-content: center; | ||
| 1060 | + z-index: 1000; | ||
| 1061 | +} | ||
| 1062 | + | ||
| 1063 | +.dialog-content { | ||
| 1064 | + background: white; | ||
| 1065 | + border-radius: 8px; | ||
| 1066 | + width: 90%; | ||
| 1067 | + max-width: 600px; | ||
| 1068 | + max-height: 90vh; | ||
| 1069 | + overflow-y: auto; | ||
| 1070 | +} | ||
| 1071 | + | ||
| 1072 | +.dialog-header { | ||
| 1073 | + padding: 16px 20px; | ||
| 1074 | + border-bottom: 1px solid #f0f0f0; | ||
| 1075 | + display: flex; | ||
| 1076 | + justify-content: space-between; | ||
| 1077 | + align-items: center; | ||
| 1078 | +} | ||
| 1079 | + | ||
| 1080 | +.dialog-header h3 { | ||
| 1081 | + margin: 0; | ||
| 1082 | + font-size: 16px; | ||
| 1083 | + color: #333; | ||
| 1084 | +} | ||
| 1085 | + | ||
| 1086 | +.close-btn { | ||
| 1087 | + width: 24px; | ||
| 1088 | + height: 24px; | ||
| 1089 | + border: none; | ||
| 1090 | + background: none; | ||
| 1091 | + font-size: 18px; | ||
| 1092 | + cursor: pointer; | ||
| 1093 | + color: #999; | ||
| 1094 | +} | ||
| 1095 | + | ||
| 1096 | +.close-btn:hover { | ||
| 1097 | + color: #333; | ||
| 1098 | +} | ||
| 1099 | + | ||
| 1100 | +.dialog-form { | ||
| 1101 | + padding: 20px; | ||
| 1102 | +} | ||
| 1103 | + | ||
| 1104 | +.form-row { | ||
| 1105 | + display: flex; | ||
| 1106 | + gap: 16px; | ||
| 270 | margin-bottom: 16px; | 1107 | margin-bottom: 16px; |
| 1108 | +} | ||
| 1109 | + | ||
| 1110 | +.form-group { | ||
| 1111 | + flex: 1; | ||
| 1112 | + display: flex; | ||
| 1113 | + flex-direction: column; | ||
| 1114 | + gap: 4px; | ||
| 1115 | +} | ||
| 1116 | + | ||
| 1117 | +.form-group.full-width { | ||
| 1118 | + flex: 1 1 100%; | ||
| 1119 | +} | ||
| 1120 | + | ||
| 1121 | +.form-group label { | ||
| 1122 | + font-size: 12px; | ||
| 1123 | + color: #333; | ||
| 1124 | + font-weight: 500; | ||
| 1125 | +} | ||
| 1126 | + | ||
| 1127 | +.form-group label.required::after { | ||
| 1128 | + content: ' *'; | ||
| 1129 | + color: #ff4d4f; | ||
| 1130 | +} | ||
| 1131 | + | ||
| 1132 | +.form-input, .form-select, .form-textarea { | ||
| 1133 | + padding: 6px 8px; | ||
| 1134 | + border: 1px solid #d9d9d9; | ||
| 1135 | + border-radius: 4px; | ||
| 1136 | + font-size: 12px; | ||
| 1137 | + transition: border-color 0.2s; | ||
| 1138 | +} | ||
| 1139 | + | ||
| 1140 | +.form-input:focus, .form-select:focus, .form-textarea:focus { | ||
| 1141 | + border-color: #1890ff; | ||
| 1142 | + outline: none; | ||
| 1143 | +} | ||
| 1144 | + | ||
| 1145 | +.form-textarea { | ||
| 1146 | + resize: vertical; | ||
| 1147 | + min-height: 60px; | ||
| 1148 | +} | ||
| 1149 | + | ||
| 1150 | +.error-message { | ||
| 1151 | + font-size: 10px; | ||
| 1152 | + color: #ff4d4f; | ||
| 1153 | +} | ||
| 1154 | + | ||
| 1155 | +.form-actions { | ||
| 1156 | + display: flex; | ||
| 1157 | + justify-content: flex-end; | ||
| 1158 | + gap: 8px; | ||
| 1159 | + margin-top: 20px; | ||
| 1160 | + padding-top: 16px; | ||
| 1161 | + border-top: 1px solid #f0f0f0; | ||
| 1162 | +} | ||
| 1163 | + | ||
| 1164 | +.cancel-btn, .submit-btn { | ||
| 1165 | + padding: 8px 16px; | ||
| 1166 | + border: 1px solid #d9d9d9; | ||
| 1167 | + border-radius: 4px; | ||
| 1168 | + font-size: 12px; | ||
| 1169 | + cursor: pointer; | ||
| 1170 | + transition: all 0.2s; | ||
| 1171 | +} | ||
| 1172 | + | ||
| 1173 | +.cancel-btn { | ||
| 1174 | + background: white; | ||
| 1175 | + color: #333; | ||
| 1176 | +} | ||
| 1177 | + | ||
| 1178 | +.cancel-btn:hover { | ||
| 1179 | + background: #f5f5f5; | ||
| 1180 | +} | ||
| 1181 | + | ||
| 1182 | +.submit-btn { | ||
| 1183 | + background: #1890ff; | ||
| 1184 | + border-color: #1890ff; | ||
| 1185 | + color: white; | ||
| 1186 | +} | ||
| 1187 | + | ||
| 1188 | +.submit-btn:hover:not(:disabled) { | ||
| 1189 | + background: #40a9ff; | ||
| 1190 | +} | ||
| 1191 | + | ||
| 1192 | +.submit-btn:disabled { | ||
| 1193 | + opacity: 0.5; | ||
| 1194 | + cursor: not-allowed; | ||
| 1195 | +} | ||
| 1196 | + | ||
| 1197 | +// 响应式设计 | ||
| 1198 | +@media (max-width: 768px) { | ||
| 1199 | + .menu-management { | ||
| 1200 | + padding: 10px; | ||
| 271 | } | 1201 | } |
| 272 | 1202 | ||
| 273 | - .operation-buttons { | 1203 | + .search-form { |
| 274 | - margin-bottom: 16px; | 1204 | + flex-direction: column; |
| 1205 | + align-items: stretch; | ||
| 1206 | + } | ||
| 1207 | + | ||
| 1208 | + .form-group { | ||
| 1209 | + min-width: auto; | ||
| 1210 | + } | ||
| 1211 | + | ||
| 1212 | + .form-row { | ||
| 1213 | + flex-direction: column; | ||
| 1214 | + gap: 8px; | ||
| 1215 | + } | ||
| 1216 | + | ||
| 1217 | + .table-footer { | ||
| 1218 | + flex-direction: column; | ||
| 1219 | + gap: 12px; | ||
| 1220 | + align-items: stretch; | ||
| 275 | } | 1221 | } |
| 276 | 1222 | ||
| 277 | - .table-container { | 1223 | + .pagination-left { |
| 278 | - .el-pagination { | 1224 | + flex-direction: column; |
| 279 | - margin-top: 16px; | 1225 | + gap: 8px; |
| 280 | - text-align: right; | 1226 | + align-items: flex-start; |
| 281 | } | 1227 | } |
| 1228 | + | ||
| 1229 | + .pagination-container { | ||
| 1230 | + justify-content: center; | ||
| 1231 | + flex-wrap: wrap; | ||
| 1232 | + } | ||
| 1233 | + | ||
| 1234 | + .pagination-btn { | ||
| 1235 | + padding: 2px 4px; | ||
| 1236 | + font-size: 10px; | ||
| 1237 | + height: 20px; | ||
| 1238 | + } | ||
| 1239 | + | ||
| 1240 | + .page-number { | ||
| 1241 | + padding: 2px 4px; | ||
| 1242 | + font-size: 10px; | ||
| 1243 | + min-width: 24px; | ||
| 1244 | + height: 20px; | ||
| 282 | } | 1245 | } |
| 283 | } | 1246 | } |
| 284 | </style> | 1247 | </style> |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | <div class="role-management"> | 2 | <div class="role-management"> |
| 3 | - <!-- 搜索表单 --> | 3 | + <!-- 搜索筛选区域 --> |
| 4 | - <el-card class="search-form"> | 4 | + <div class="search-section"> |
| 5 | - <el-form :model="searchForm" inline> | 5 | + <div class="search-row"> |
| 6 | - <el-form-item label="角色名称"> | 6 | + <div class="search-item"> |
| 7 | - <el-input v-model="searchForm.roleName" placeholder="请输入角色名称" clearable /> | 7 | + <label>角色名称:</label> |
| 8 | - </el-form-item> | 8 | + <input |
| 9 | - <el-form-item label="角色标识"> | 9 | + v-model="searchForm.roleName" |
| 10 | - <el-input v-model="searchForm.roleKey" placeholder="请输入角色标识" clearable /> | 10 | + type="text" |
| 11 | - </el-form-item> | 11 | + placeholder="请输入角色名称" |
| 12 | - <el-form-item label="状态"> | 12 | + class="search-input" |
| 13 | - <el-select v-model="searchForm.status" placeholder="请选择状态" clearable> | 13 | + /> |
| 14 | - <el-option label="正常" :value="1" /> | 14 | + </div> |
| 15 | - <el-option label="停用" :value="0" /> | 15 | + <div class="search-item"> |
| 16 | - </el-select> | 16 | + <label>角色编码:</label> |
| 17 | - </el-form-item> | 17 | + <input |
| 18 | - <el-form-item> | 18 | + v-model="searchForm.roleCode" |
| 19 | - <el-button type="primary" @click="handleSearch"> | 19 | + type="text" |
| 20 | - <el-icon><Search /></el-icon> | 20 | + placeholder="请输入角色编码" |
| 21 | - 搜索 | 21 | + class="search-input" |
| 22 | - </el-button> | 22 | + /> |
| 23 | - <el-button @click="handleReset"> | 23 | + </div> |
| 24 | - <el-icon><Refresh /></el-icon> | 24 | + <div class="search-item"> |
| 25 | - 重置 | 25 | + <label>状态:</label> |
| 26 | - </el-button> | 26 | + <select v-model="searchForm.status" class="search-select"> |
| 27 | - </el-form-item> | 27 | + <option value="">全部</option> |
| 28 | - </el-form> | 28 | + <option value="1">正常</option> |
| 29 | - </el-card> | 29 | + <option value="0">停用</option> |
| 30 | - | 30 | + </select> |
| 31 | - <!-- 操作按钮 --> | 31 | + </div> |
| 32 | - <el-card class="operation-buttons"> | 32 | + <div class="search-actions"> |
| 33 | - <el-button type="primary" @click="handleAdd"> | 33 | + <button @click="handleSearch" class="search-btn">🔍 搜索</button> |
| 34 | - <el-icon><Plus /></el-icon> | 34 | + <button @click="handleReset" class="reset-btn">🔄 重置</button> |
| 35 | - 新增角色 | 35 | + </div> |
| 36 | - </el-button> | 36 | + </div> |
| 37 | - <el-button type="danger" :disabled="!multipleSelection.length" @click="handleBatchDelete"> | 37 | + </div> |
| 38 | - <el-icon><Delete /></el-icon> | 38 | + |
| 39 | - 批量删除 | 39 | + <!-- 操作按钮区域 --> |
| 40 | - </el-button> | 40 | + <div class="action-section"> |
| 41 | - </el-card> | 41 | + <div class="action-buttons"> |
| 42 | + <button @click="handleAdd" class="action-btn primary">✨ 新增</button> | ||
| 43 | + <button @click="handleBatchDelete" class="action-btn danger">🗑️ 删除</button> | ||
| 44 | + </div> | ||
| 45 | + </div> | ||
| 42 | 46 | ||
| 43 | <!-- 数据表格 --> | 47 | <!-- 数据表格 --> |
| 44 | - <el-card class="table-container"> | 48 | + <div class="table-section"> |
| 45 | - <el-table | 49 | + <div class="table-header"> |
| 46 | - v-loading="loading" | 50 | + <div class="table-controls"> |
| 47 | - :data="tableData" | 51 | + <button @click="handleTableSearch" class="control-btn" title="搜索">🔍</button> |
| 48 | - @selection-change="handleSelectionChange" | 52 | + <button @click="handleTableRefresh" class="control-btn" title="刷新">🔄</button> |
| 53 | + <button @click="handleTableExport" class="control-btn" title="导出">📋</button> | ||
| 54 | + <button @click="handleTableViewToggle" class="control-btn" title="视图切换">⊞</button> | ||
| 55 | + </div> | ||
| 56 | + </div> | ||
| 57 | + | ||
| 58 | + <div class="table-container"> | ||
| 59 | + <table class="data-table"> | ||
| 60 | + <thead> | ||
| 61 | + <tr> | ||
| 62 | + <th class="checkbox-col"> | ||
| 63 | + <input | ||
| 64 | + type="checkbox" | ||
| 65 | + :checked="selectAll" | ||
| 66 | + @change="handleSelectAll" | ||
| 67 | + class="table-checkbox" | ||
| 68 | + /> | ||
| 69 | + </th> | ||
| 70 | + <th>角色ID</th> | ||
| 71 | + <th>角色名称</th> | ||
| 72 | + <th>角色编码</th> | ||
| 73 | + <th>状态</th> | ||
| 74 | + <th>备注</th> | ||
| 75 | + <th>创建时间</th> | ||
| 76 | + <th>操作</th> | ||
| 77 | + </tr> | ||
| 78 | + </thead> | ||
| 79 | + <tbody> | ||
| 80 | + <tr v-for="role in roles" :key="role.roleId"> | ||
| 81 | + <td class="checkbox-col"> | ||
| 82 | + <input | ||
| 83 | + type="checkbox" | ||
| 84 | + :checked="selectedRoles.includes(role.roleId)" | ||
| 85 | + @change="handleSelectRole(role.roleId)" | ||
| 86 | + class="table-checkbox" | ||
| 87 | + /> | ||
| 88 | + </td> | ||
| 89 | + <td>{{ role.roleId }}</td> | ||
| 90 | + <td>{{ role.roleName }}</td> | ||
| 91 | + <td>{{ role.roleCode }}</td> | ||
| 92 | + <td> | ||
| 93 | + <span | ||
| 94 | + :class="['status-badge', role.status === 1 ? 'active' : 'inactive']" | ||
| 95 | + @click="handleStatusToggle(role)" | ||
| 49 | > | 96 | > |
| 50 | - <el-table-column type="selection" width="55" /> | 97 | + {{ role.status === 1 ? '正常' : '停用' }} |
| 51 | - <el-table-column prop="roleId" label="角色ID" width="80" /> | 98 | + </span> |
| 52 | - <el-table-column prop="roleName" label="角色名称" width="120" /> | 99 | + </td> |
| 53 | - <el-table-column prop="roleKey" label="角色标识" width="120" /> | 100 | + <td>{{ role.remark || '-' }}</td> |
| 54 | - <el-table-column prop="roleSort" label="排序" width="80" /> | 101 | + <td>{{ formatTime(role.createTime) }}</td> |
| 55 | - <el-table-column prop="status" label="状态" width="80"> | 102 | + <td> |
| 56 | - <template #default="{ row }"> | 103 | + <button @click="handleEdit(role)" class="table-btn edit">✏️ 编辑</button> |
| 57 | - <el-tag :type="row.status === 1 ? 'success' : 'danger'"> | 104 | + <button @click="handleDelete(role)" class="table-btn delete">🗑️ 删除</button> |
| 58 | - {{ row.status === 1 ? '正常' : '停用' }} | 105 | + </td> |
| 59 | - </el-tag> | 106 | + </tr> |
| 60 | - </template> | 107 | + </tbody> |
| 61 | - </el-table-column> | 108 | + </table> |
| 62 | - <el-table-column prop="createTime" label="创建时间" width="180" /> | 109 | + </div> |
| 63 | - <el-table-column label="操作" width="250" fixed="right"> | ||
| 64 | - <template #default="{ row }"> | ||
| 65 | - <el-button type="primary" size="small" @click="handleEdit(row)"> | ||
| 66 | - 编辑 | ||
| 67 | - </el-button> | ||
| 68 | - <el-button type="success" size="small" @click="handleAssignPermissions(row)"> | ||
| 69 | - 分配权限 | ||
| 70 | - </el-button> | ||
| 71 | - <el-button type="danger" size="small" @click="handleDelete(row)"> | ||
| 72 | - 删除 | ||
| 73 | - </el-button> | ||
| 74 | - </template> | ||
| 75 | - </el-table-column> | ||
| 76 | - </el-table> | ||
| 77 | 110 | ||
| 78 | <!-- 分页 --> | 111 | <!-- 分页 --> |
| 79 | - <el-pagination | 112 | + <div class="table-footer"> |
| 80 | - v-model:current-page="pagination.current" | 113 | + <div class="pagination-left"> |
| 81 | - v-model:page-size="pagination.size" | 114 | + <div class="pagination-info"> |
| 82 | - :total="pagination.total" | 115 | + 共 {{ total }} 条记录,第 {{ currentPage }} / {{ totalPages }} 页 |
| 83 | - :page-sizes="[10, 20, 50, 100]" | 116 | + </div> |
| 84 | - layout="total, sizes, prev, pager, next, jumper" | 117 | + <div class="page-size-selector"> |
| 85 | - @size-change="handleSizeChange" | 118 | + <label>每页显示:</label> |
| 86 | - @current-change="handleCurrentChange" | 119 | + <select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> |
| 120 | + <option value="10">10条</option> | ||
| 121 | + <option value="20">20条</option> | ||
| 122 | + <option value="50">50条</option> | ||
| 123 | + <option value="100">100条</option> | ||
| 124 | + </select> | ||
| 125 | + </div> | ||
| 126 | + </div> | ||
| 127 | + <div class="pagination-container"> | ||
| 128 | + <button | ||
| 129 | + @click="handlePageChange(currentPage - 1)" | ||
| 130 | + :disabled="currentPage <= 1" | ||
| 131 | + class="pagination-btn prev-btn" | ||
| 132 | + > | ||
| 133 | + 上一页 | ||
| 134 | + </button> | ||
| 135 | + <div class="pagination-pages"> | ||
| 136 | + <button | ||
| 137 | + v-for="page in getPageNumbers()" | ||
| 138 | + :key="page" | ||
| 139 | + @click="handlePageChange(page)" | ||
| 140 | + :class="['page-number', { active: page === currentPage }]" | ||
| 141 | + > | ||
| 142 | + {{ page }} | ||
| 143 | + </button> | ||
| 144 | + </div> | ||
| 145 | + <button | ||
| 146 | + @click="handlePageChange(currentPage + 1)" | ||
| 147 | + :disabled="currentPage >= totalPages" | ||
| 148 | + class="pagination-btn next-btn" | ||
| 149 | + > | ||
| 150 | + 下一页 | ||
| 151 | + </button> | ||
| 152 | + </div> | ||
| 153 | + </div> | ||
| 154 | + </div> | ||
| 155 | + | ||
| 156 | + <!-- 角色编辑对话框 --> | ||
| 157 | + <div v-if="showRoleDialog" class="dialog-overlay" @click="closeRoleDialog"> | ||
| 158 | + <div class="dialog-content" @click.stop> | ||
| 159 | + <div class="dialog-header"> | ||
| 160 | + <h3>{{ dialogTitle }}</h3> | ||
| 161 | + <button @click="closeRoleDialog" class="close-btn">×</button> | ||
| 162 | + </div> | ||
| 163 | + <div class="dialog-body"> | ||
| 164 | + <form @submit.prevent="handleSubmit" class="role-form"> | ||
| 165 | + <div class="form-row"> | ||
| 166 | + <div class="form-group"> | ||
| 167 | + <label class="required">角色名称:</label> | ||
| 168 | + <input | ||
| 169 | + v-model="roleForm.roleName" | ||
| 170 | + type="text" | ||
| 171 | + placeholder="请输入角色名称" | ||
| 172 | + class="form-input" | ||
| 173 | + required | ||
| 87 | /> | 174 | /> |
| 88 | - </el-card> | 175 | + <div v-if="fieldErrors.roleName" class="error-message">{{ fieldErrors.roleName }}</div> |
| 176 | + </div> | ||
| 177 | + <div class="form-group"> | ||
| 178 | + <label class="required">角色编码:</label> | ||
| 179 | + <input | ||
| 180 | + v-model="roleForm.roleCode" | ||
| 181 | + type="text" | ||
| 182 | + placeholder="请输入角色编码" | ||
| 183 | + class="form-input" | ||
| 184 | + required | ||
| 185 | + /> | ||
| 186 | + <div v-if="fieldErrors.roleCode" class="error-message">{{ fieldErrors.roleCode }}</div> | ||
| 187 | + </div> | ||
| 188 | + </div> | ||
| 189 | + <div class="form-row"> | ||
| 190 | + <div class="form-group"> | ||
| 191 | + <label>状态:</label> | ||
| 192 | + <select v-model="roleForm.status" class="form-select"> | ||
| 193 | + <option value="1">正常</option> | ||
| 194 | + <option value="0">停用</option> | ||
| 195 | + </select> | ||
| 196 | + </div> | ||
| 197 | + </div> | ||
| 198 | + <div class="form-row"> | ||
| 199 | + <div class="form-group full-width"> | ||
| 200 | + <label>备注:</label> | ||
| 201 | + <textarea | ||
| 202 | + v-model="roleForm.remark" | ||
| 203 | + placeholder="请输入备注信息" | ||
| 204 | + class="form-textarea" | ||
| 205 | + rows="3" | ||
| 206 | + ></textarea> | ||
| 207 | + </div> | ||
| 208 | + </div> | ||
| 209 | + | ||
| 210 | + <!-- 菜单权限选择 --> | ||
| 211 | + <div class="form-row"> | ||
| 212 | + <div class="form-group full-width"> | ||
| 213 | + <label>菜单权限:</label> | ||
| 214 | + <div class="menu-permissions"> | ||
| 215 | + <div class="permission-controls"> | ||
| 216 | + <button type="button" @click="toggleExpandAll" class="control-btn"> | ||
| 217 | + {{ allExpanded ? '折叠' : '展开' }} | ||
| 218 | + </button> | ||
| 219 | + <button type="button" @click="toggleSelectAll" class="control-btn"> | ||
| 220 | + {{ allSelected ? '全不选' : '全选' }} | ||
| 221 | + </button> | ||
| 222 | + <label class="linkage-label"> | ||
| 223 | + <input type="checkbox" v-model="parentChildLinkage" class="linkage-checkbox"> | ||
| 224 | + 父子联动 | ||
| 225 | + </label> | ||
| 226 | + </div> | ||
| 227 | + | ||
| 228 | + <div class="menu-tree"> | ||
| 229 | + <div | ||
| 230 | + v-for="menu in menuTree" | ||
| 231 | + :key="menu.id" | ||
| 232 | + class="menu-item" | ||
| 233 | + :style="{ paddingLeft: (menu.level * 20 + 10) + 'px' }" | ||
| 234 | + > | ||
| 235 | + <div class="menu-row"> | ||
| 236 | + <span | ||
| 237 | + v-if="menu.children && menu.children.length > 0" | ||
| 238 | + @click="toggleMenu(menu)" | ||
| 239 | + class="expand-icon" | ||
| 240 | + > | ||
| 241 | + {{ menu.expanded ? '−' : '+' }} | ||
| 242 | + </span> | ||
| 243 | + <span v-else class="expand-placeholder"></span> | ||
| 244 | + | ||
| 245 | + <input | ||
| 246 | + type="checkbox" | ||
| 247 | + :id="'menu-' + menu.id" | ||
| 248 | + :checked="selectedMenus.includes(menu.id)" | ||
| 249 | + @change="handleMenuSelect(menu)" | ||
| 250 | + class="menu-checkbox" | ||
| 251 | + > | ||
| 252 | + | ||
| 253 | + <span class="menu-icon">📁</span> | ||
| 254 | + <span class="menu-name">{{ menu.name }}</span> | ||
| 255 | + <span v-if="menu.permission" class="menu-permission">{{ menu.permission }}</span> | ||
| 256 | + </div> | ||
| 257 | + | ||
| 258 | + <!-- 递归渲染子菜单 --> | ||
| 259 | + <div v-if="menu.expanded && menu.children" class="submenu"> | ||
| 260 | + <div | ||
| 261 | + v-for="child in menu.children" | ||
| 262 | + :key="child.id" | ||
| 263 | + class="menu-item" | ||
| 264 | + :style="{ paddingLeft: (child.level * 20 + 10) + 'px' }" | ||
| 265 | + > | ||
| 266 | + <div class="menu-row"> | ||
| 267 | + <span | ||
| 268 | + v-if="child.children && child.children.length > 0" | ||
| 269 | + @click="toggleMenu(child)" | ||
| 270 | + class="expand-icon" | ||
| 271 | + > | ||
| 272 | + {{ child.expanded ? '−' : '+' }} | ||
| 273 | + </span> | ||
| 274 | + <span v-else class="expand-placeholder"></span> | ||
| 275 | + | ||
| 276 | + <input | ||
| 277 | + type="checkbox" | ||
| 278 | + :id="'menu-' + child.id" | ||
| 279 | + :checked="selectedMenus.includes(child.id)" | ||
| 280 | + @change="handleMenuSelect(child)" | ||
| 281 | + class="menu-checkbox" | ||
| 282 | + > | ||
| 283 | + | ||
| 284 | + <span class="menu-icon">📁</span> | ||
| 285 | + <span class="menu-name">{{ child.name }}</span> | ||
| 286 | + <span v-if="child.permission" class="menu-permission">{{ child.permission }}</span> | ||
| 287 | + </div> | ||
| 288 | + | ||
| 289 | + <!-- 第三级菜单 --> | ||
| 290 | + <div v-if="child.expanded && child.children" class="submenu"> | ||
| 291 | + <div | ||
| 292 | + v-for="grandChild in child.children" | ||
| 293 | + :key="grandChild.id" | ||
| 294 | + class="menu-item" | ||
| 295 | + :style="{ paddingLeft: (grandChild.level * 20 + 10) + 'px' }" | ||
| 296 | + > | ||
| 297 | + <div class="menu-row"> | ||
| 298 | + <span | ||
| 299 | + v-if="grandChild.children && grandChild.children.length > 0" | ||
| 300 | + @click="toggleMenu(grandChild)" | ||
| 301 | + class="expand-icon" | ||
| 302 | + > | ||
| 303 | + {{ grandChild.expanded ? '−' : '+' }} | ||
| 304 | + </span> | ||
| 305 | + <span v-else class="expand-placeholder"></span> | ||
| 306 | + | ||
| 307 | + <input | ||
| 308 | + type="checkbox" | ||
| 309 | + :id="'menu-' + grandChild.id" | ||
| 310 | + :checked="selectedMenus.includes(grandChild.id)" | ||
| 311 | + @change="handleMenuSelect(grandChild)" | ||
| 312 | + class="menu-checkbox" | ||
| 313 | + > | ||
| 314 | + | ||
| 315 | + <span class="menu-icon">📁</span> | ||
| 316 | + <span class="menu-name">{{ grandChild.name }}</span> | ||
| 317 | + <span v-if="grandChild.permission" class="menu-permission">{{ grandChild.permission }}</span> | ||
| 318 | + </div> | ||
| 319 | + | ||
| 320 | + <!-- 第四级菜单 --> | ||
| 321 | + <div v-if="grandChild.expanded && grandChild.children" class="submenu"> | ||
| 322 | + <div | ||
| 323 | + v-for="greatGrandChild in grandChild.children" | ||
| 324 | + :key="greatGrandChild.id" | ||
| 325 | + class="menu-item" | ||
| 326 | + :style="{ paddingLeft: (greatGrandChild.level * 20 + 10) + 'px' }" | ||
| 327 | + > | ||
| 328 | + <div class="menu-row"> | ||
| 329 | + <span class="expand-placeholder"></span> | ||
| 330 | + | ||
| 331 | + <input | ||
| 332 | + type="checkbox" | ||
| 333 | + :id="'menu-' + greatGrandChild.id" | ||
| 334 | + :checked="selectedMenus.includes(greatGrandChild.id)" | ||
| 335 | + @change="handleMenuSelect(greatGrandChild)" | ||
| 336 | + class="menu-checkbox" | ||
| 337 | + > | ||
| 338 | + | ||
| 339 | + <span class="menu-icon">📁</span> | ||
| 340 | + <span class="menu-name">{{ greatGrandChild.name }}</span> | ||
| 341 | + <span v-if="greatGrandChild.permission" class="menu-permission">{{ greatGrandChild.permission }}</span> | ||
| 342 | + </div> | ||
| 343 | + </div> | ||
| 344 | + </div> | ||
| 345 | + </div> | ||
| 346 | + </div> | ||
| 347 | + </div> | ||
| 348 | + </div> | ||
| 349 | + </div> | ||
| 350 | + </div> | ||
| 351 | + </div> | ||
| 352 | + </div> | ||
| 353 | + </div> | ||
| 354 | + <div class="form-actions"> | ||
| 355 | + <button type="button" @click="closeRoleDialog" class="cancel-btn">取消</button> | ||
| 356 | + <button type="submit" :disabled="formLoading" class="submit-btn"> | ||
| 357 | + {{ formLoading ? '保存中...' : '保存' }} | ||
| 358 | + </button> | ||
| 359 | + </div> | ||
| 360 | + </form> | ||
| 361 | + </div> | ||
| 362 | + </div> | ||
| 363 | + </div> | ||
| 89 | </div> | 364 | </div> |
| 90 | </template> | 365 | </template> |
| 91 | 366 | ||
| 92 | <script setup lang="ts"> | 367 | <script setup lang="ts"> |
| 93 | -import { ref, reactive, onMounted } from 'vue' | 368 | +import { ref, reactive, onMounted, computed } from 'vue' |
| 94 | -import { ElMessage, ElMessageBox } from 'element-plus' | 369 | +import { useRouter } from 'vue-router' |
| 95 | -import type { Role } from '@/types' | 370 | +import { roleApi, type Role, type RoleSearchParams } from '../../../api/role' |
| 371 | + | ||
| 372 | +const router = useRouter() | ||
| 373 | + | ||
| 374 | + | ||
| 375 | +// 导航栏相关 | ||
| 376 | +const sidebarCollapsed = ref(false) | ||
| 377 | +const navTabs = ref([ | ||
| 378 | + { id: 1, name: '角色管理', path: '/sys/role', active: true, closable: false } | ||
| 379 | +]) | ||
| 96 | 380 | ||
| 97 | // 搜索表单 | 381 | // 搜索表单 |
| 98 | const searchForm = reactive({ | 382 | const searchForm = reactive({ |
| 99 | roleName: '', | 383 | roleName: '', |
| 100 | - roleKey: '', | 384 | + roleCode: '', |
| 101 | - status: undefined | 385 | + status: '' |
| 102 | }) | 386 | }) |
| 103 | 387 | ||
| 104 | // 表格数据 | 388 | // 表格数据 |
| 105 | -const tableData = ref<Role[]>([]) | 389 | +const roles = ref<Role[]>([]) |
| 390 | +const selectedRoles = ref<number[]>([]) | ||
| 391 | +const selectAll = ref(false) | ||
| 106 | const loading = ref(false) | 392 | const loading = ref(false) |
| 107 | -const multipleSelection = ref<Role[]>([]) | ||
| 108 | 393 | ||
| 109 | // 分页 | 394 | // 分页 |
| 110 | -const pagination = reactive({ | 395 | +const currentPage = ref(1) |
| 111 | - current: 1, | 396 | +const pageSize = ref(10) |
| 112 | - size: 10, | 397 | +const total = ref(0) |
| 113 | - total: 0 | 398 | +const totalPages = computed(() => Math.ceil(total.value / pageSize.value)) |
| 399 | + | ||
| 400 | +// 获取页码数组 | ||
| 401 | +const getPageNumbers = () => { | ||
| 402 | + const pages = [] | ||
| 403 | + const maxVisible = 5 // 最多显示5个页码 | ||
| 404 | + const start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2)) | ||
| 405 | + const end = Math.min(totalPages.value, start + maxVisible - 1) | ||
| 406 | + | ||
| 407 | + for (let i = start; i <= end; i++) { | ||
| 408 | + pages.push(i) | ||
| 409 | + } | ||
| 410 | + return pages | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +// 对话框相关 | ||
| 414 | +const showRoleDialog = ref(false) | ||
| 415 | +const dialogTitle = ref('') | ||
| 416 | +const formLoading = ref(false) | ||
| 417 | +const roleForm = reactive({ | ||
| 418 | + roleId: 0, | ||
| 419 | + roleName: '', | ||
| 420 | + roleCode: '', | ||
| 421 | + status: 1, | ||
| 422 | + remark: '', | ||
| 423 | + menuIds: [] as number[] | ||
| 424 | +}) | ||
| 425 | + | ||
| 426 | +// 字段错误信息 | ||
| 427 | +const fieldErrors = reactive({ | ||
| 428 | + roleName: '', | ||
| 429 | + roleCode: '' | ||
| 114 | }) | 430 | }) |
| 115 | 431 | ||
| 432 | +// 菜单权限相关 | ||
| 433 | +const menuTree = ref<any[]>([]) | ||
| 434 | + | ||
| 435 | +const selectedMenus = ref<number[]>([]) | ||
| 436 | +const allExpanded = ref(false) | ||
| 437 | +const allSelected = ref(false) | ||
| 438 | +const parentChildLinkage = ref(true) | ||
| 439 | + | ||
| 440 | +// 导航栏功能 | ||
| 441 | +const toggleSidebar = () => { | ||
| 442 | + sidebarCollapsed.value = !sidebarCollapsed.value | ||
| 443 | + console.log('切换侧边栏:', sidebarCollapsed.value) | ||
| 444 | +} | ||
| 445 | + | ||
| 446 | +const switchTab = (tab: any) => { | ||
| 447 | + navTabs.value.forEach(t => t.active = false) | ||
| 448 | + tab.active = true | ||
| 449 | + if (tab.path) { | ||
| 450 | + router.push(tab.path) | ||
| 451 | + } | ||
| 452 | +} | ||
| 453 | + | ||
| 454 | +const closeTab = (tab: any) => { | ||
| 455 | + if (tab.closable) { | ||
| 456 | + const index = navTabs.value.findIndex(t => t.id === tab.id) | ||
| 457 | + if (index > -1) { | ||
| 458 | + navTabs.value.splice(index, 1) | ||
| 459 | + // 如果关闭的是当前激活的标签,激活相邻的标签 | ||
| 460 | + if (tab.active && navTabs.value.length > 0) { | ||
| 461 | + const newActiveIndex = Math.min(index, navTabs.value.length - 1) | ||
| 462 | + navTabs.value[newActiveIndex].active = true | ||
| 463 | + if (navTabs.value[newActiveIndex].path) { | ||
| 464 | + router.push(navTabs.value[newActiveIndex].path) | ||
| 465 | + } | ||
| 466 | + } | ||
| 467 | + } | ||
| 468 | + } | ||
| 469 | +} | ||
| 470 | + | ||
| 471 | +const handleRefresh = () => { | ||
| 472 | + fetchRoles() | ||
| 473 | +} | ||
| 474 | + | ||
| 116 | // 获取角色列表 | 475 | // 获取角色列表 |
| 117 | -const getRoleList = async () => { | 476 | +const fetchRoles = async () => { |
| 118 | loading.value = true | 477 | loading.value = true |
| 119 | try { | 478 | try { |
| 120 | - // 模拟数据 | 479 | + const params: RoleSearchParams = { |
| 121 | - tableData.value = [ | 480 | + pageNum: currentPage.value, |
| 122 | - { | 481 | + pageSize: pageSize.value, |
| 123 | - roleId: 1, | 482 | + roleName: searchForm.roleName || undefined, |
| 124 | - roleName: '超级管理员', | 483 | + roleCode: searchForm.roleCode || undefined, |
| 125 | - roleKey: 'ADMIN', | 484 | + status: searchForm.status ? Number(searchForm.status) : undefined |
| 126 | - roleSort: 1, | ||
| 127 | - status: 1, | ||
| 128 | - remark: '超级管理员', | ||
| 129 | - createTime: '2024-01-01 10:00:00', | ||
| 130 | - updateTime: '2024-01-01 10:00:00', | ||
| 131 | - permissions: [] | ||
| 132 | - }, | ||
| 133 | - { | ||
| 134 | - roleId: 2, | ||
| 135 | - roleName: '普通用户', | ||
| 136 | - roleKey: 'USER', | ||
| 137 | - roleSort: 2, | ||
| 138 | - status: 1, | ||
| 139 | - remark: '普通用户', | ||
| 140 | - createTime: '2024-01-01 10:00:00', | ||
| 141 | - updateTime: '2024-01-01 10:00:00', | ||
| 142 | - permissions: [] | ||
| 143 | } | 485 | } |
| 144 | - ] | 486 | + |
| 145 | - pagination.total = 2 | 487 | + console.log('获取角色列表参数:', params) |
| 146 | - } catch (error) { | 488 | + const response = await roleApi.getRoleList(params) |
| 147 | - ElMessage.error('获取角色列表失败') | 489 | + console.log('角色列表响应:', response) |
| 490 | + | ||
| 491 | + if (response.code === 200) { | ||
| 492 | + roles.value = response.data.records || [] | ||
| 493 | + total.value = response.data.total || 0 | ||
| 494 | + } else { | ||
| 495 | + console.error('获取角色列表失败:', response.message) | ||
| 496 | + showMessage(response.message || '获取角色列表失败', 'error') | ||
| 497 | + } | ||
| 498 | + } catch (error: any) { | ||
| 499 | + console.error('获取角色列表失败:', error) | ||
| 500 | + showMessage(error.response?.data?.message || '获取角色列表失败', 'error') | ||
| 148 | } finally { | 501 | } finally { |
| 149 | loading.value = false | 502 | loading.value = false |
| 150 | } | 503 | } |
| 151 | } | 504 | } |
| 152 | 505 | ||
| 153 | -// 搜索 | 506 | +// 搜索功能 |
| 154 | const handleSearch = () => { | 507 | const handleSearch = () => { |
| 155 | - pagination.current = 1 | 508 | + currentPage.value = 1 |
| 156 | - getRoleList() | 509 | + fetchRoles() |
| 157 | } | 510 | } |
| 158 | 511 | ||
| 159 | -// 重置 | 512 | +// 重置搜索 |
| 160 | const handleReset = () => { | 513 | const handleReset = () => { |
| 161 | Object.assign(searchForm, { | 514 | Object.assign(searchForm, { |
| 162 | roleName: '', | 515 | roleName: '', |
| 163 | - roleKey: '', | 516 | + roleCode: '', |
| 164 | - status: undefined | 517 | + status: '' |
| 165 | }) | 518 | }) |
| 166 | handleSearch() | 519 | handleSearch() |
| 167 | } | 520 | } |
| 168 | 521 | ||
| 522 | +// 表格控制按钮功能 | ||
| 523 | +const handleTableSearch = () => { | ||
| 524 | + const searchSection = document.querySelector('.search-section') | ||
| 525 | + if (searchSection) { | ||
| 526 | + searchSection.scrollIntoView({ behavior: 'smooth' }) | ||
| 527 | + } | ||
| 528 | +} | ||
| 529 | + | ||
| 530 | +const handleTableRefresh = () => { | ||
| 531 | + fetchRoles() | ||
| 532 | +} | ||
| 533 | + | ||
| 534 | +const handleTableExport = () => { | ||
| 535 | + console.log('导出角色数据') | ||
| 536 | + showMessage('导出功能开发中...', 'info') | ||
| 537 | +} | ||
| 538 | + | ||
| 539 | +const handleTableViewToggle = () => { | ||
| 540 | + console.log('切换视图模式') | ||
| 541 | + showMessage('视图切换功能开发中...', 'info') | ||
| 542 | +} | ||
| 543 | + | ||
| 544 | +// 菜单权限相关方法 | ||
| 545 | +const toggleExpandAll = () => { | ||
| 546 | + allExpanded.value = !allExpanded.value | ||
| 547 | + const setExpanded = (menus: any[]) => { | ||
| 548 | + menus.forEach(menu => { | ||
| 549 | + menu.expanded = allExpanded.value | ||
| 550 | + if (menu.children) { | ||
| 551 | + setExpanded(menu.children) | ||
| 552 | + } | ||
| 553 | + }) | ||
| 554 | + } | ||
| 555 | + setExpanded(menuTree.value) | ||
| 556 | +} | ||
| 557 | + | ||
| 558 | +const toggleSelectAll = () => { | ||
| 559 | + allSelected.value = !allSelected.value | ||
| 560 | + if (allSelected.value) { | ||
| 561 | + const getAllMenuIds = (menus: any[]): number[] => { | ||
| 562 | + let ids: number[] = [] | ||
| 563 | + menus.forEach(menu => { | ||
| 564 | + ids.push(menu.id) | ||
| 565 | + if (menu.children) { | ||
| 566 | + ids = ids.concat(getAllMenuIds(menu.children)) | ||
| 567 | + } | ||
| 568 | + }) | ||
| 569 | + return ids | ||
| 570 | + } | ||
| 571 | + selectedMenus.value = getAllMenuIds(menuTree.value) | ||
| 572 | + } else { | ||
| 573 | + selectedMenus.value = [] | ||
| 574 | + } | ||
| 575 | + updateRoleFormMenuIds() | ||
| 576 | +} | ||
| 577 | + | ||
| 578 | +const toggleMenu = (menu: any) => { | ||
| 579 | + menu.expanded = !menu.expanded | ||
| 580 | +} | ||
| 581 | + | ||
| 582 | +const handleMenuSelect = (menu: any) => { | ||
| 583 | + const isSelected = selectedMenus.value.includes(menu.id) | ||
| 584 | + | ||
| 585 | + if (isSelected) { | ||
| 586 | + // 取消选择 | ||
| 587 | + selectedMenus.value = selectedMenus.value.filter(id => id !== menu.id) | ||
| 588 | + if (parentChildLinkage.value && menu.children) { | ||
| 589 | + // 取消选择所有子菜单 | ||
| 590 | + const removeChildIds = (children: any[]) => { | ||
| 591 | + children.forEach(child => { | ||
| 592 | + selectedMenus.value = selectedMenus.value.filter(id => id !== child.id) | ||
| 593 | + if (child.children) { | ||
| 594 | + removeChildIds(child.children) | ||
| 595 | + } | ||
| 596 | + }) | ||
| 597 | + } | ||
| 598 | + removeChildIds(menu.children) | ||
| 599 | + } | ||
| 600 | + } else { | ||
| 601 | + // 选择 | ||
| 602 | + selectedMenus.value.push(menu.id) | ||
| 603 | + if (parentChildLinkage.value && menu.children) { | ||
| 604 | + // 选择所有子菜单 | ||
| 605 | + const addChildIds = (children: any[]) => { | ||
| 606 | + children.forEach(child => { | ||
| 607 | + if (!selectedMenus.value.includes(child.id)) { | ||
| 608 | + selectedMenus.value.push(child.id) | ||
| 609 | + } | ||
| 610 | + if (child.children) { | ||
| 611 | + addChildIds(child.children) | ||
| 612 | + } | ||
| 613 | + }) | ||
| 614 | + } | ||
| 615 | + addChildIds(menu.children) | ||
| 616 | + } | ||
| 617 | + } | ||
| 618 | + | ||
| 619 | + // 更新父菜单选择状态 | ||
| 620 | + if (parentChildLinkage.value) { | ||
| 621 | + updateParentSelection(menuTree.value) | ||
| 622 | + } | ||
| 623 | + | ||
| 624 | + updateRoleFormMenuIds() | ||
| 625 | +} | ||
| 626 | + | ||
| 627 | +const updateParentSelection = (menus: any[]) => { | ||
| 628 | + menus.forEach(menu => { | ||
| 629 | + if (menu.children && menu.children.length > 0) { | ||
| 630 | + const allChildrenSelected = menu.children.every((child: any) => | ||
| 631 | + selectedMenus.value.includes(child.id) | ||
| 632 | + ) | ||
| 633 | + const someChildrenSelected = menu.children.some((child: any) => | ||
| 634 | + selectedMenus.value.includes(child.id) | ||
| 635 | + ) | ||
| 636 | + | ||
| 637 | + if (allChildrenSelected) { | ||
| 638 | + if (!selectedMenus.value.includes(menu.id)) { | ||
| 639 | + selectedMenus.value.push(menu.id) | ||
| 640 | + } | ||
| 641 | + } else if (!someChildrenSelected) { | ||
| 642 | + selectedMenus.value = selectedMenus.value.filter(id => id !== menu.id) | ||
| 643 | + } | ||
| 644 | + | ||
| 645 | + updateParentSelection(menu.children) | ||
| 646 | + } | ||
| 647 | + }) | ||
| 648 | +} | ||
| 649 | + | ||
| 650 | +const updateRoleFormMenuIds = () => { | ||
| 651 | + roleForm.menuIds = [...selectedMenus.value] | ||
| 652 | +} | ||
| 653 | + | ||
| 654 | +// 获取菜单树数据 | ||
| 655 | +const fetchMenuTree = async () => { | ||
| 656 | + try { | ||
| 657 | + const response = await roleApi.getMenuTree() | ||
| 658 | + if (response.code === 200 && response.data) { | ||
| 659 | + // 转换菜单数据格式,添加前端需要的字段 | ||
| 660 | + menuTree.value = response.data.map(menu => convertMenuForTree(menu)) | ||
| 661 | + } else { | ||
| 662 | + console.error('获取菜单数据失败:', response.message) | ||
| 663 | + showMessage('获取菜单数据失败', 'error') | ||
| 664 | + } | ||
| 665 | + } catch (error) { | ||
| 666 | + console.error('获取菜单数据失败:', error) | ||
| 667 | + showMessage('获取菜单数据失败,请重试', 'error') | ||
| 668 | + } | ||
| 669 | +} | ||
| 670 | + | ||
| 671 | +// 转换菜单数据为树形结构格式 | ||
| 672 | +const convertMenuForTree = (menu: any, level: number = 0): any => { | ||
| 673 | + return { | ||
| 674 | + id: menu.menuId, | ||
| 675 | + name: menu.menuName, | ||
| 676 | + permission: menu.perms || '', | ||
| 677 | + level: level, | ||
| 678 | + expanded: false, | ||
| 679 | + children: menu.children ? menu.children.map((child: any) => convertMenuForTree(child, level + 1)) : [] | ||
| 680 | + } | ||
| 681 | +} | ||
| 682 | + | ||
| 683 | +// 更新菜单树选择状态 | ||
| 684 | +const updateMenuTreeSelection = (menus: any[]) => { | ||
| 685 | + // 递归更新菜单树的选择状态 | ||
| 686 | + const updateTreeSelection = (treeMenus: any[]) => { | ||
| 687 | + treeMenus.forEach(treeMenu => { | ||
| 688 | + // 递归处理子菜单 | ||
| 689 | + if (treeMenu.children && treeMenu.children.length > 0) { | ||
| 690 | + updateTreeSelection(treeMenu.children) | ||
| 691 | + } | ||
| 692 | + }) | ||
| 693 | + } | ||
| 694 | + | ||
| 695 | + // 更新整个菜单树 | ||
| 696 | + updateTreeSelection(menuTree.value) | ||
| 697 | + | ||
| 698 | + // 更新表单中的菜单ID | ||
| 699 | + updateRoleFormMenuIds() | ||
| 700 | +} | ||
| 701 | + | ||
| 702 | +// 选择功能 | ||
| 703 | +const handleSelectAll = (event: Event) => { | ||
| 704 | + const target = event.target as HTMLInputElement | ||
| 705 | + selectAll.value = target.checked | ||
| 706 | + if (target.checked) { | ||
| 707 | + selectedRoles.value = roles.value.map(role => role.roleId) | ||
| 708 | + } else { | ||
| 709 | + selectedRoles.value = [] | ||
| 710 | + } | ||
| 711 | +} | ||
| 712 | + | ||
| 713 | +const handleSelectRole = (roleId: number) => { | ||
| 714 | + const index = selectedRoles.value.indexOf(roleId) | ||
| 715 | + if (index > -1) { | ||
| 716 | + selectedRoles.value.splice(index, 1) | ||
| 717 | + } else { | ||
| 718 | + selectedRoles.value.push(roleId) | ||
| 719 | + } | ||
| 720 | + selectAll.value = selectedRoles.value.length === roles.value.length | ||
| 721 | +} | ||
| 722 | + | ||
| 169 | // 新增角色 | 723 | // 新增角色 |
| 170 | const handleAdd = () => { | 724 | const handleAdd = () => { |
| 171 | - ElMessage.info('新增角色功能待实现') | 725 | + dialogTitle.value = '新增角色' |
| 726 | + clearFieldErrors() | ||
| 727 | + Object.assign(roleForm, { | ||
| 728 | + roleId: 0, | ||
| 729 | + roleName: '', | ||
| 730 | + roleCode: '', | ||
| 731 | + status: 1, | ||
| 732 | + remark: '', | ||
| 733 | + menuIds: [] | ||
| 734 | + }) | ||
| 735 | + selectedMenus.value = [] | ||
| 736 | + showRoleDialog.value = true | ||
| 172 | } | 737 | } |
| 173 | 738 | ||
| 174 | // 编辑角色 | 739 | // 编辑角色 |
| 175 | -const handleEdit = (row: Role) => { | 740 | +const handleEdit = async (role: Role) => { |
| 176 | - ElMessage.info(`编辑角色: ${row.roleName}`) | 741 | + dialogTitle.value = '编辑角色' |
| 177 | -} | 742 | + clearFieldErrors() |
| 178 | 743 | ||
| 179 | -// 分配权限 | 744 | + try { |
| 180 | -const handleAssignPermissions = (row: Role) => { | 745 | + // 确保菜单树数据已加载 |
| 181 | - ElMessage.info(`分配权限: ${row.roleName}`) | 746 | + if (menuTree.value.length === 0) { |
| 747 | + console.log('菜单树数据未加载,先加载菜单树') | ||
| 748 | + await fetchMenuTree() | ||
| 749 | + } | ||
| 750 | + | ||
| 751 | + // 获取角色详细信息,包括菜单权限 | ||
| 752 | + const response = await roleApi.getRoleById(role.roleId) | ||
| 753 | + if (response.code === 200 && response.data) { | ||
| 754 | + const roleDetail = response.data | ||
| 755 | + | ||
| 756 | + // 初始化表单数据 | ||
| 757 | + Object.assign(roleForm, { | ||
| 758 | + roleId: roleDetail.roleId, | ||
| 759 | + roleName: roleDetail.roleName, | ||
| 760 | + roleCode: roleDetail.roleCode, | ||
| 761 | + status: roleDetail.status, | ||
| 762 | + remark: roleDetail.remark || '', | ||
| 763 | + menuIds: roleDetail.menuIds || [] | ||
| 764 | + }) | ||
| 765 | + | ||
| 766 | + // 初始化菜单权限选择 | ||
| 767 | + // 从 menus 字段提取菜单ID(后端返回的是菜单对象数组) | ||
| 768 | + let menuIds: number[] = [] | ||
| 769 | + if (roleDetail.menus && Array.isArray(roleDetail.menus)) { | ||
| 770 | + // 递归提取所有菜单ID(包括子菜单) | ||
| 771 | + const extractMenuIds = (menus: any[]): number[] => { | ||
| 772 | + const ids: number[] = [] | ||
| 773 | + menus.forEach(menu => { | ||
| 774 | + if (menu.menuId) { | ||
| 775 | + ids.push(menu.menuId) | ||
| 776 | + } | ||
| 777 | + if (menu.children && menu.children.length > 0) { | ||
| 778 | + ids.push(...extractMenuIds(menu.children)) | ||
| 779 | + } | ||
| 780 | + }) | ||
| 781 | + return ids | ||
| 782 | + } | ||
| 783 | + menuIds = extractMenuIds(roleDetail.menus) | ||
| 784 | + } else if (roleDetail.menuIds && Array.isArray(roleDetail.menuIds)) { | ||
| 785 | + menuIds = roleDetail.menuIds | ||
| 786 | + } | ||
| 787 | + | ||
| 788 | + selectedMenus.value = [...menuIds] | ||
| 789 | + | ||
| 790 | + // 延迟更新菜单树选择状态,确保DOM已更新 | ||
| 791 | + setTimeout(() => { | ||
| 792 | + updateMenuTreeSelection(roleDetail.menus || []) | ||
| 793 | + }, 100) | ||
| 794 | + | ||
| 795 | + showRoleDialog.value = true | ||
| 796 | + } else { | ||
| 797 | + showMessage(response.message || '获取角色详情失败', 'error') | ||
| 798 | + } | ||
| 799 | + } catch (error) { | ||
| 800 | + console.error('获取角色详情失败:', error) | ||
| 801 | + showMessage('获取角色详情失败,请重试', 'error') | ||
| 802 | + } | ||
| 182 | } | 803 | } |
| 183 | 804 | ||
| 184 | // 删除角色 | 805 | // 删除角色 |
| 185 | -const handleDelete = async (row: Role) => { | 806 | +const handleDelete = async (role: Role) => { |
| 807 | + if (confirm(`确定要删除角色 ${role.roleName} 吗?`)) { | ||
| 186 | try { | 808 | try { |
| 187 | - await ElMessageBox.confirm(`确定要删除角色 "${row.roleName}" 吗?`, '提示', { | 809 | + const response = await roleApi.deleteRole([role.roleId]) |
| 188 | - confirmButtonText: '确定', | 810 | + if (response.code === 200) { |
| 189 | - cancelButtonText: '取消', | 811 | + showMessage('删除成功!') |
| 190 | - type: 'warning' | 812 | + fetchRoles() |
| 191 | - }) | 813 | + } else { |
| 192 | - ElMessage.success('删除成功') | 814 | + showMessage(response.message || '删除失败', 'error') |
| 193 | - getRoleList() | 815 | + } |
| 194 | - } catch (error) { | 816 | + } catch (error: any) { |
| 195 | - // 用户取消删除 | 817 | + console.error('删除角色失败:', error) |
| 818 | + showMessage(error.response?.data?.message || '删除失败', 'error') | ||
| 819 | + } | ||
| 196 | } | 820 | } |
| 197 | } | 821 | } |
| 198 | 822 | ||
| 199 | // 批量删除 | 823 | // 批量删除 |
| 200 | const handleBatchDelete = async () => { | 824 | const handleBatchDelete = async () => { |
| 825 | + if (selectedRoles.value.length === 0) { | ||
| 826 | + showMessage('请选择要删除的角色', 'warning') | ||
| 827 | + return | ||
| 828 | + } | ||
| 829 | + | ||
| 830 | + if (confirm(`确定要删除选中的 ${selectedRoles.value.length} 个角色吗?`)) { | ||
| 201 | try { | 831 | try { |
| 202 | - await ElMessageBox.confirm(`确定要删除选中的 ${multipleSelection.value.length} 个角色吗?`, '提示', { | 832 | + const response = await roleApi.deleteRole(selectedRoles.value) |
| 203 | - confirmButtonText: '确定', | 833 | + if (response.code === 200) { |
| 204 | - cancelButtonText: '取消', | 834 | + showMessage('批量删除成功!') |
| 205 | - type: 'warning' | 835 | + selectedRoles.value = [] |
| 206 | - }) | 836 | + selectAll.value = false |
| 207 | - ElMessage.success('批量删除成功') | 837 | + fetchRoles() |
| 208 | - getRoleList() | 838 | + } else { |
| 209 | - } catch (error) { | 839 | + showMessage(response.message || '批量删除失败', 'error') |
| 210 | - // 用户取消删除 | 840 | + } |
| 841 | + } catch (error: any) { | ||
| 842 | + console.error('批量删除角色失败:', error) | ||
| 843 | + showMessage(error.response?.data?.message || '批量删除失败', 'error') | ||
| 844 | + } | ||
| 211 | } | 845 | } |
| 212 | } | 846 | } |
| 213 | 847 | ||
| 214 | -// 选择变化 | 848 | +// 状态切换 |
| 215 | -const handleSelectionChange = (selection: Role[]) => { | 849 | +const handleStatusToggle = async (role: Role) => { |
| 216 | - multipleSelection.value = selection | 850 | + const newStatus = role.status === 1 ? 0 : 1 |
| 851 | + const statusText = newStatus === 1 ? '启用' : '停用' | ||
| 852 | + | ||
| 853 | + if (confirm(`确定要${statusText}角色 ${role.roleName} 吗?`)) { | ||
| 854 | + try { | ||
| 855 | + const response = await roleApi.updateRoleStatus(role.roleId, newStatus) | ||
| 856 | + if (response.code === 200) { | ||
| 857 | + showMessage(`${statusText}成功!`) | ||
| 858 | + fetchRoles() | ||
| 859 | + } else { | ||
| 860 | + showMessage(response.message || `${statusText}失败`, 'error') | ||
| 861 | + } | ||
| 862 | + } catch (error: any) { | ||
| 863 | + console.error(`${statusText}角色失败:`, error) | ||
| 864 | + showMessage(error.response?.data?.message || `${statusText}失败`, 'error') | ||
| 865 | + } | ||
| 866 | + } | ||
| 217 | } | 867 | } |
| 218 | 868 | ||
| 219 | -// 分页大小变化 | 869 | +// 表单提交 |
| 220 | -const handleSizeChange = (size: number) => { | 870 | +const handleSubmit = async () => { |
| 221 | - pagination.size = size | 871 | + if (!validateForm()) { |
| 222 | - getRoleList() | 872 | + return |
| 873 | + } | ||
| 874 | + | ||
| 875 | + formLoading.value = true | ||
| 876 | + | ||
| 877 | + try { | ||
| 878 | + if (dialogTitle.value === '新增角色') { | ||
| 879 | + const response = await roleApi.createRole(roleForm) | ||
| 880 | + if (response.code === 200) { | ||
| 881 | + showMessage('新增角色成功!') | ||
| 882 | + showRoleDialog.value = false | ||
| 883 | + fetchRoles() | ||
| 884 | + } else { | ||
| 885 | + if (response.code === 400 && response.data) { | ||
| 886 | + setFieldErrors(response.data) | ||
| 887 | + showMessage('请检查表单中的错误信息', 'error') | ||
| 888 | + } else { | ||
| 889 | + showMessage(response.message || '新增角色失败', 'error') | ||
| 890 | + } | ||
| 891 | + } | ||
| 892 | + } else { | ||
| 893 | + const response = await roleApi.updateRole(roleForm) | ||
| 894 | + if (response.code === 200) { | ||
| 895 | + showMessage('修改角色成功!') | ||
| 896 | + showRoleDialog.value = false | ||
| 897 | + fetchRoles() | ||
| 898 | + } else { | ||
| 899 | + if (response.code === 400 && response.data) { | ||
| 900 | + setFieldErrors(response.data) | ||
| 901 | + showMessage('请检查表单中的错误信息', 'error') | ||
| 902 | + } else { | ||
| 903 | + showMessage(response.message || '修改角色失败', 'error') | ||
| 904 | + } | ||
| 905 | + } | ||
| 906 | + } | ||
| 907 | + } catch (error: any) { | ||
| 908 | + console.error('保存角色失败:', error) | ||
| 909 | + showMessage(error.response?.data?.message || '保存失败', 'error') | ||
| 910 | + } finally { | ||
| 911 | + formLoading.value = false | ||
| 912 | + } | ||
| 223 | } | 913 | } |
| 224 | 914 | ||
| 225 | -// 当前页变化 | 915 | +// 表单验证 |
| 226 | -const handleCurrentChange = (current: number) => { | 916 | +const validateForm = () => { |
| 227 | - pagination.current = current | 917 | + clearFieldErrors() |
| 228 | - getRoleList() | 918 | + let isValid = true |
| 919 | + | ||
| 920 | + if (!roleForm.roleName.trim()) { | ||
| 921 | + fieldErrors.roleName = '角色名称不能为空' | ||
| 922 | + isValid = false | ||
| 923 | + } | ||
| 924 | + | ||
| 925 | + if (!roleForm.roleCode.trim()) { | ||
| 926 | + fieldErrors.roleCode = '角色编码不能为空' | ||
| 927 | + isValid = false | ||
| 928 | + } | ||
| 929 | + | ||
| 930 | + return isValid | ||
| 931 | +} | ||
| 932 | + | ||
| 933 | +// 设置字段错误 | ||
| 934 | +const setFieldErrors = (errors: any) => { | ||
| 935 | + clearFieldErrors() | ||
| 936 | + if (errors.roleName) { | ||
| 937 | + fieldErrors.roleName = errors.roleName | ||
| 938 | + } | ||
| 939 | + if (errors.roleCode) { | ||
| 940 | + fieldErrors.roleCode = errors.roleCode | ||
| 941 | + } | ||
| 942 | +} | ||
| 943 | + | ||
| 944 | +// 清除字段错误 | ||
| 945 | +const clearFieldErrors = () => { | ||
| 946 | + fieldErrors.roleName = '' | ||
| 947 | + fieldErrors.roleCode = '' | ||
| 948 | +} | ||
| 949 | + | ||
| 950 | +// 关闭对话框 | ||
| 951 | +const closeRoleDialog = () => { | ||
| 952 | + showRoleDialog.value = false | ||
| 953 | + clearFieldErrors() | ||
| 954 | +} | ||
| 955 | + | ||
| 956 | +// 分页功能 | ||
| 957 | +const handlePageChange = (page: number) => { | ||
| 958 | + if (page >= 1 && page <= totalPages.value) { | ||
| 959 | + currentPage.value = page | ||
| 960 | + fetchRoles() | ||
| 961 | + } | ||
| 962 | +} | ||
| 963 | + | ||
| 964 | +const handlePageSizeChange = () => { | ||
| 965 | + currentPage.value = 1 | ||
| 966 | + fetchRoles() | ||
| 967 | +} | ||
| 968 | + | ||
| 969 | +// 时间格式化 | ||
| 970 | +const formatTime = (timeStr: string) => { | ||
| 971 | + if (!timeStr) return '-' | ||
| 972 | + return timeStr.replace('T', ' ').substring(0, 19) | ||
| 973 | +} | ||
| 974 | + | ||
| 975 | +// 消息提示 | ||
| 976 | +const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'success') => { | ||
| 977 | + console.log(`${type.toUpperCase()}: ${message}`) | ||
| 978 | + alert(message) // 简化版本,实际项目中应该使用更好的消息提示组件 | ||
| 229 | } | 979 | } |
| 230 | 980 | ||
| 231 | // 组件挂载时获取数据 | 981 | // 组件挂载时获取数据 |
| 232 | onMounted(() => { | 982 | onMounted(() => { |
| 233 | - getRoleList() | 983 | + fetchRoles() |
| 984 | + fetchMenuTree() | ||
| 234 | }) | 985 | }) |
| 235 | </script> | 986 | </script> |
| 236 | 987 | ||
| 237 | <style lang="scss" scoped> | 988 | <style lang="scss" scoped> |
| 238 | .role-management { | 989 | .role-management { |
| 239 | - .search-form { | 990 | + background: #f5f5f5; |
| 240 | - margin-bottom: 16px; | 991 | + min-height: 100vh; |
| 241 | - } | 992 | + padding: 0; |
| 993 | +} | ||
| 994 | + | ||
| 995 | +/* 顶部导航栏 */ | ||
| 996 | +.top-nav { | ||
| 997 | + background: white; | ||
| 998 | + border-bottom: 1px solid #e0e0e0; | ||
| 999 | + padding: 4px 12px; | ||
| 1000 | + display: flex; | ||
| 1001 | + justify-content: space-between; | ||
| 1002 | + align-items: center; | ||
| 1003 | + box-shadow: 0 1px 2px rgba(0,0,0,0.1); | ||
| 1004 | + min-height: 36px; | ||
| 1005 | +} | ||
| 1006 | + | ||
| 1007 | +.nav-left { | ||
| 1008 | + display: flex; | ||
| 1009 | + align-items: center; | ||
| 1010 | + gap: 8px; | ||
| 1011 | +} | ||
| 1012 | + | ||
| 1013 | +.nav-toggle { | ||
| 1014 | + background: none; | ||
| 1015 | + border: none; | ||
| 1016 | + font-size: 14px; | ||
| 1017 | + cursor: pointer; | ||
| 1018 | + padding: 2px 6px; | ||
| 1019 | + border-radius: 3px; | ||
| 1020 | + width: 24px; | ||
| 1021 | + height: 24px; | ||
| 1022 | + display: flex; | ||
| 1023 | + align-items: center; | ||
| 1024 | + justify-content: center; | ||
| 1025 | +} | ||
| 1026 | + | ||
| 1027 | +.nav-toggle:hover { | ||
| 1028 | + background: #f0f0f0; | ||
| 1029 | +} | ||
| 242 | 1030 | ||
| 243 | - .operation-buttons { | 1031 | +.nav-tabs { |
| 244 | - margin-bottom: 16px; | 1032 | + display: flex; |
| 1033 | + gap: 2px; | ||
| 1034 | +} | ||
| 1035 | + | ||
| 1036 | +.nav-tab { | ||
| 1037 | + padding: 4px 8px; | ||
| 1038 | + background: #f8f9fa; | ||
| 1039 | + border: 1px solid #e0e0e0; | ||
| 1040 | + border-radius: 3px; | ||
| 1041 | + font-size: 11px; | ||
| 1042 | + color: #666; | ||
| 1043 | + cursor: pointer; | ||
| 1044 | + display: flex; | ||
| 1045 | + align-items: center; | ||
| 1046 | + gap: 4px; | ||
| 1047 | + height: 24px; | ||
| 1048 | +} | ||
| 1049 | + | ||
| 1050 | +.nav-tab.active { | ||
| 1051 | + background: #1890ff; | ||
| 1052 | + color: white; | ||
| 1053 | + border-color: #1890ff; | ||
| 1054 | +} | ||
| 1055 | + | ||
| 1056 | +.close-icon { | ||
| 1057 | + font-size: 12px; | ||
| 1058 | + cursor: pointer; | ||
| 1059 | + opacity: 0.7; | ||
| 1060 | + margin-left: 4px; | ||
| 1061 | + padding: 1px 3px; | ||
| 1062 | + border-radius: 2px; | ||
| 1063 | + transition: all 0.2s; | ||
| 1064 | +} | ||
| 1065 | + | ||
| 1066 | +.close-icon:hover { | ||
| 1067 | + opacity: 1; | ||
| 1068 | + color: #ff4d4f; | ||
| 1069 | + background: #fff2f0; | ||
| 1070 | +} | ||
| 1071 | + | ||
| 1072 | +.refresh-btn { | ||
| 1073 | + background: #1890ff; | ||
| 1074 | + color: white; | ||
| 1075 | + border: none; | ||
| 1076 | + padding: 4px 8px; | ||
| 1077 | + border-radius: 3px; | ||
| 1078 | + font-size: 11px; | ||
| 1079 | + cursor: pointer; | ||
| 1080 | + height: 24px; | ||
| 1081 | + display: flex; | ||
| 1082 | + align-items: center; | ||
| 1083 | + gap: 4px; | ||
| 1084 | +} | ||
| 1085 | + | ||
| 1086 | +/* 搜索筛选区域 */ | ||
| 1087 | +.search-section { | ||
| 1088 | + background: white; | ||
| 1089 | + padding: 16px; | ||
| 1090 | + border-bottom: 1px solid #e0e0e0; | ||
| 1091 | +} | ||
| 1092 | + | ||
| 1093 | +.search-row { | ||
| 1094 | + display: flex; | ||
| 1095 | + gap: 16px; | ||
| 1096 | + align-items: center; | ||
| 1097 | + flex-wrap: wrap; | ||
| 1098 | +} | ||
| 1099 | + | ||
| 1100 | +.search-item { | ||
| 1101 | + display: flex; | ||
| 1102 | + align-items: center; | ||
| 1103 | + gap: 8px; | ||
| 1104 | +} | ||
| 1105 | + | ||
| 1106 | +.search-item label { | ||
| 1107 | + font-size: 12px; | ||
| 1108 | + color: #333; | ||
| 1109 | + white-space: nowrap; | ||
| 1110 | +} | ||
| 1111 | + | ||
| 1112 | +.search-input, .search-select { | ||
| 1113 | + padding: 6px 8px; | ||
| 1114 | + border: 1px solid #d9d9d9; | ||
| 1115 | + border-radius: 4px; | ||
| 1116 | + font-size: 12px; | ||
| 1117 | + width: 120px; | ||
| 1118 | +} | ||
| 1119 | + | ||
| 1120 | +.search-input:focus, .search-select:focus { | ||
| 1121 | + outline: none; | ||
| 1122 | + border-color: #1890ff; | ||
| 1123 | +} | ||
| 1124 | + | ||
| 1125 | +.search-actions { | ||
| 1126 | + display: flex; | ||
| 1127 | + gap: 6px; | ||
| 1128 | +} | ||
| 1129 | + | ||
| 1130 | +.search-btn { | ||
| 1131 | + background: #1890ff; | ||
| 1132 | + color: white; | ||
| 1133 | + border: none; | ||
| 1134 | + padding: 4px 8px; | ||
| 1135 | + border-radius: 3px; | ||
| 1136 | + font-size: 11px; | ||
| 1137 | + cursor: pointer; | ||
| 1138 | + height: 24px; | ||
| 1139 | + display: flex; | ||
| 1140 | + align-items: center; | ||
| 1141 | + gap: 4px; | ||
| 1142 | +} | ||
| 1143 | + | ||
| 1144 | +.reset-btn { | ||
| 1145 | + background: #ff7875; | ||
| 1146 | + color: white; | ||
| 1147 | + border: none; | ||
| 1148 | + padding: 4px 8px; | ||
| 1149 | + border-radius: 3px; | ||
| 1150 | + font-size: 11px; | ||
| 1151 | + cursor: pointer; | ||
| 1152 | + height: 24px; | ||
| 1153 | + display: flex; | ||
| 1154 | + align-items: center; | ||
| 1155 | + gap: 4px; | ||
| 1156 | +} | ||
| 1157 | + | ||
| 1158 | +/* 操作按钮区域 */ | ||
| 1159 | +.action-section { | ||
| 1160 | + background: white; | ||
| 1161 | + padding: 12px 16px; | ||
| 1162 | + border-bottom: 1px solid #e0e0e0; | ||
| 1163 | +} | ||
| 1164 | + | ||
| 1165 | +.action-buttons { | ||
| 1166 | + display: flex; | ||
| 1167 | + gap: 6px; | ||
| 1168 | +} | ||
| 1169 | + | ||
| 1170 | +.action-btn { | ||
| 1171 | + padding: 4px 8px; | ||
| 1172 | + border: none; | ||
| 1173 | + border-radius: 3px; | ||
| 1174 | + font-size: 11px; | ||
| 1175 | + cursor: pointer; | ||
| 1176 | + display: flex; | ||
| 1177 | + align-items: center; | ||
| 1178 | + gap: 4px; | ||
| 1179 | + height: 24px; | ||
| 1180 | +} | ||
| 1181 | + | ||
| 1182 | +.action-btn.primary { | ||
| 1183 | + background: #1890ff; | ||
| 1184 | + color: white; | ||
| 1185 | +} | ||
| 1186 | + | ||
| 1187 | +.action-btn.success { | ||
| 1188 | + background: #52c41a; | ||
| 1189 | + color: white; | ||
| 1190 | +} | ||
| 1191 | + | ||
| 1192 | +.action-btn.danger { | ||
| 1193 | + background: #ff4d4f; | ||
| 1194 | + color: white; | ||
| 1195 | +} | ||
| 1196 | + | ||
| 1197 | +.action-btn.info { | ||
| 1198 | + background: #13c2c2; | ||
| 1199 | + color: white; | ||
| 1200 | +} | ||
| 1201 | + | ||
| 1202 | +.action-btn.warning { | ||
| 1203 | + background: #faad14; | ||
| 1204 | + color: white; | ||
| 1205 | +} | ||
| 1206 | + | ||
| 1207 | +/* 表格区域 */ | ||
| 1208 | +.table-section { | ||
| 1209 | + background: white; | ||
| 1210 | + margin: 16px; | ||
| 1211 | + border-radius: 6px; | ||
| 1212 | + overflow: hidden; | ||
| 1213 | + box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||
| 1214 | +} | ||
| 1215 | + | ||
| 1216 | +.table-header { | ||
| 1217 | + padding: 8px 16px; | ||
| 1218 | + background: #fafafa; | ||
| 1219 | + border-bottom: 1px solid #e0e0e0; | ||
| 1220 | + display: flex; | ||
| 1221 | + justify-content: flex-end; | ||
| 1222 | +} | ||
| 1223 | + | ||
| 1224 | +.table-controls { | ||
| 1225 | + display: flex; | ||
| 1226 | + gap: 4px; | ||
| 1227 | +} | ||
| 1228 | + | ||
| 1229 | +.control-btn { | ||
| 1230 | + background: white; | ||
| 1231 | + border: 1px solid #d9d9d9; | ||
| 1232 | + border-radius: 3px; | ||
| 1233 | + padding: 4px 8px; | ||
| 1234 | + font-size: 11px; | ||
| 1235 | + cursor: pointer; | ||
| 1236 | + transition: all 0.2s; | ||
| 1237 | +} | ||
| 1238 | + | ||
| 1239 | +.control-btn:hover { | ||
| 1240 | + background: #f0f8ff; | ||
| 1241 | + border-color: #1890ff; | ||
| 1242 | + color: #1890ff; | ||
| 1243 | + transform: translateY(-1px); | ||
| 1244 | + box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2); | ||
| 1245 | +} | ||
| 1246 | + | ||
| 1247 | +.control-btn:active { | ||
| 1248 | + transform: translateY(0); | ||
| 1249 | + box-shadow: 0 1px 2px rgba(24, 144, 255, 0.2); | ||
| 245 | } | 1250 | } |
| 246 | 1251 | ||
| 247 | .table-container { | 1252 | .table-container { |
| 248 | - .el-pagination { | 1253 | + overflow-x: auto; |
| 249 | - margin-top: 16px; | 1254 | +} |
| 250 | - text-align: right; | 1255 | + |
| 1256 | +.data-table { | ||
| 1257 | + width: 100%; | ||
| 1258 | + border-collapse: collapse; | ||
| 1259 | +} | ||
| 1260 | + | ||
| 1261 | +.data-table th, | ||
| 1262 | +.data-table td { | ||
| 1263 | + padding: 8px 12px; | ||
| 1264 | + text-align: left; | ||
| 1265 | + border-bottom: 1px solid #f0f0f0; | ||
| 1266 | + font-size: 12px; | ||
| 1267 | +} | ||
| 1268 | + | ||
| 1269 | +.data-table th { | ||
| 1270 | + background: #fafafa; | ||
| 1271 | + font-weight: 600; | ||
| 1272 | + color: #333; | ||
| 1273 | +} | ||
| 1274 | + | ||
| 1275 | +.data-table tbody tr:hover { | ||
| 1276 | + background: #f5f5f5; | ||
| 1277 | +} | ||
| 1278 | + | ||
| 1279 | +.checkbox-col { | ||
| 1280 | + width: 40px; | ||
| 1281 | + text-align: center; | ||
| 1282 | +} | ||
| 1283 | + | ||
| 1284 | +.table-checkbox { | ||
| 1285 | + width: 14px; | ||
| 1286 | + height: 14px; | ||
| 1287 | + cursor: pointer; | ||
| 1288 | +} | ||
| 1289 | + | ||
| 1290 | +.status-badge { | ||
| 1291 | + padding: 2px 6px; | ||
| 1292 | + border-radius: 3px; | ||
| 1293 | + font-size: 10px; | ||
| 1294 | + cursor: pointer; | ||
| 1295 | + transition: all 0.2s; | ||
| 1296 | +} | ||
| 1297 | + | ||
| 1298 | +.status-badge.active { | ||
| 1299 | + background: #f6ffed; | ||
| 1300 | + color: #52c41a; | ||
| 1301 | + border: 1px solid #b7eb8f; | ||
| 1302 | +} | ||
| 1303 | + | ||
| 1304 | +.status-badge.inactive { | ||
| 1305 | + background: #fff2f0; | ||
| 1306 | + color: #ff4d4f; | ||
| 1307 | + border: 1px solid #ffccc7; | ||
| 1308 | +} | ||
| 1309 | + | ||
| 1310 | +.status-badge:hover { | ||
| 1311 | + opacity: 0.8; | ||
| 1312 | + transform: scale(1.05); | ||
| 1313 | +} | ||
| 1314 | + | ||
| 1315 | +.table-btn { | ||
| 1316 | + padding: 2px 6px; | ||
| 1317 | + margin-right: 4px; | ||
| 1318 | + border: none; | ||
| 1319 | + border-radius: 3px; | ||
| 1320 | + font-size: 10px; | ||
| 1321 | + cursor: pointer; | ||
| 1322 | + display: inline-flex; | ||
| 1323 | + align-items: center; | ||
| 1324 | + gap: 2px; | ||
| 1325 | +} | ||
| 1326 | + | ||
| 1327 | +.table-btn.edit { | ||
| 1328 | + background: #1890ff; | ||
| 1329 | + color: white; | ||
| 1330 | +} | ||
| 1331 | + | ||
| 1332 | +.table-btn.delete { | ||
| 1333 | + background: #ff4d4f; | ||
| 1334 | + color: white; | ||
| 1335 | +} | ||
| 1336 | + | ||
| 1337 | +/* 分页样式 */ | ||
| 1338 | +.table-footer { | ||
| 1339 | + display: flex; | ||
| 1340 | + justify-content: space-between; | ||
| 1341 | + align-items: center; | ||
| 1342 | + padding: 8px 16px; | ||
| 1343 | + background: #f8f9fa; | ||
| 1344 | + border-top: 1px solid #e0e0e0; | ||
| 1345 | +} | ||
| 1346 | + | ||
| 1347 | +.pagination-left { | ||
| 1348 | + display: flex; | ||
| 1349 | + align-items: center; | ||
| 1350 | + gap: 16px; | ||
| 1351 | +} | ||
| 1352 | + | ||
| 1353 | +.pagination-info { | ||
| 1354 | + font-size: 12px; | ||
| 1355 | + color: #666; | ||
| 1356 | +} | ||
| 1357 | + | ||
| 1358 | +.page-size-selector { | ||
| 1359 | + display: flex; | ||
| 1360 | + align-items: center; | ||
| 1361 | + gap: 4px; | ||
| 1362 | + font-size: 11px; | ||
| 1363 | + color: #666; | ||
| 1364 | +} | ||
| 1365 | + | ||
| 1366 | +.page-size-select { | ||
| 1367 | + padding: 2px 4px; | ||
| 1368 | + border: 1px solid #d9d9d9; | ||
| 1369 | + border-radius: 3px; | ||
| 1370 | + font-size: 10px; | ||
| 1371 | + background: white; | ||
| 1372 | +} | ||
| 1373 | + | ||
| 1374 | +.pagination-container { | ||
| 1375 | + display: flex; | ||
| 1376 | + align-items: center; | ||
| 1377 | + background: #f8f9fa; | ||
| 1378 | + border: 1px solid #e9ecef; | ||
| 1379 | + border-radius: 8px; | ||
| 1380 | + overflow: hidden; | ||
| 1381 | + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||
| 1382 | +} | ||
| 1383 | + | ||
| 1384 | +.pagination-btn { | ||
| 1385 | + padding: 2px 6px; | ||
| 1386 | + border: none; | ||
| 1387 | + background: white; | ||
| 1388 | + color: #6c757d; | ||
| 1389 | + font-size: 11px; | ||
| 1390 | + font-weight: 500; | ||
| 1391 | + cursor: pointer; | ||
| 1392 | + transition: all 0.2s; | ||
| 1393 | + border-right: 1px solid #e9ecef; | ||
| 1394 | + height: 20px; | ||
| 1395 | +} | ||
| 1396 | + | ||
| 1397 | +.pagination-btn:hover:not(:disabled) { | ||
| 1398 | + background: #e9ecef; | ||
| 1399 | + color: #495057; | ||
| 1400 | +} | ||
| 1401 | + | ||
| 1402 | +.pagination-btn:disabled { | ||
| 1403 | + opacity: 0.5; | ||
| 1404 | + cursor: not-allowed; | ||
| 1405 | +} | ||
| 1406 | + | ||
| 1407 | +.page-number { | ||
| 1408 | + padding: 2px 6px; | ||
| 1409 | + border: none; | ||
| 1410 | + background: white; | ||
| 1411 | + color: #6c757d; | ||
| 1412 | + font-size: 11px; | ||
| 1413 | + font-weight: 500; | ||
| 1414 | + cursor: pointer; | ||
| 1415 | + transition: all 0.2s; | ||
| 1416 | + border-right: 1px solid #e9ecef; | ||
| 1417 | + min-width: 28px; | ||
| 1418 | + text-align: center; | ||
| 1419 | + height: 20px; | ||
| 1420 | +} | ||
| 1421 | + | ||
| 1422 | +.page-number:hover { | ||
| 1423 | + background: #e9ecef; | ||
| 1424 | + color: #495057; | ||
| 1425 | +} | ||
| 1426 | + | ||
| 1427 | +.page-number.active { | ||
| 1428 | + background: #1890ff; | ||
| 1429 | + color: white; | ||
| 1430 | + font-weight: 600; | ||
| 1431 | +} | ||
| 1432 | + | ||
| 1433 | +.pagination-pages { | ||
| 1434 | + display: flex; | ||
| 1435 | +} | ||
| 1436 | + | ||
| 1437 | +/* 对话框样式 */ | ||
| 1438 | +.dialog-overlay { | ||
| 1439 | + position: fixed; | ||
| 1440 | + top: 0; | ||
| 1441 | + left: 0; | ||
| 1442 | + right: 0; | ||
| 1443 | + bottom: 0; | ||
| 1444 | + background: rgba(0, 0, 0, 0.5); | ||
| 1445 | + display: flex; | ||
| 1446 | + align-items: center; | ||
| 1447 | + justify-content: center; | ||
| 1448 | + z-index: 1000; | ||
| 1449 | +} | ||
| 1450 | + | ||
| 1451 | +.dialog-content { | ||
| 1452 | + background: white; | ||
| 1453 | + border-radius: 8px; | ||
| 1454 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||
| 1455 | + width: 90%; | ||
| 1456 | + max-width: 600px; | ||
| 1457 | + max-height: 80vh; | ||
| 1458 | + overflow-y: auto; | ||
| 1459 | +} | ||
| 1460 | + | ||
| 1461 | +.dialog-header { | ||
| 1462 | + padding: 16px 20px; | ||
| 1463 | + border-bottom: 1px solid #e0e0e0; | ||
| 1464 | + display: flex; | ||
| 1465 | + justify-content: space-between; | ||
| 1466 | + align-items: center; | ||
| 1467 | +} | ||
| 1468 | + | ||
| 1469 | +.dialog-header h3 { | ||
| 1470 | + margin: 0; | ||
| 1471 | + font-size: 16px; | ||
| 1472 | + color: #333; | ||
| 1473 | +} | ||
| 1474 | + | ||
| 1475 | +.close-btn { | ||
| 1476 | + background: none; | ||
| 1477 | + border: none; | ||
| 1478 | + font-size: 20px; | ||
| 1479 | + cursor: pointer; | ||
| 1480 | + color: #999; | ||
| 1481 | + padding: 0; | ||
| 1482 | + width: 24px; | ||
| 1483 | + height: 24px; | ||
| 1484 | + display: flex; | ||
| 1485 | + align-items: center; | ||
| 1486 | + justify-content: center; | ||
| 1487 | +} | ||
| 1488 | + | ||
| 1489 | +.close-btn:hover { | ||
| 1490 | + color: #666; | ||
| 1491 | +} | ||
| 1492 | + | ||
| 1493 | +.dialog-body { | ||
| 1494 | + padding: 20px; | ||
| 1495 | +} | ||
| 1496 | + | ||
| 1497 | +.role-form { | ||
| 1498 | + display: flex; | ||
| 1499 | + flex-direction: column; | ||
| 1500 | + gap: 16px; | ||
| 1501 | +} | ||
| 1502 | + | ||
| 1503 | +.form-row { | ||
| 1504 | + display: flex; | ||
| 1505 | + gap: 16px; | ||
| 1506 | +} | ||
| 1507 | + | ||
| 1508 | +.form-group { | ||
| 1509 | + flex: 1; | ||
| 1510 | + display: flex; | ||
| 1511 | + flex-direction: column; | ||
| 1512 | + gap: 4px; | ||
| 1513 | +} | ||
| 1514 | + | ||
| 1515 | +.form-group.full-width { | ||
| 1516 | + flex: 1 1 100%; | ||
| 1517 | +} | ||
| 1518 | + | ||
| 1519 | +.form-group label { | ||
| 1520 | + font-size: 12px; | ||
| 1521 | + color: #333; | ||
| 1522 | + font-weight: 500; | ||
| 1523 | +} | ||
| 1524 | + | ||
| 1525 | +.form-group label.required::after { | ||
| 1526 | + content: ' *'; | ||
| 1527 | + color: #ff4d4f; | ||
| 1528 | +} | ||
| 1529 | + | ||
| 1530 | +.form-input, .form-select, .form-textarea { | ||
| 1531 | + padding: 6px 8px; | ||
| 1532 | + border: 1px solid #d9d9d9; | ||
| 1533 | + border-radius: 4px; | ||
| 1534 | + font-size: 12px; | ||
| 1535 | + transition: border-color 0.2s; | ||
| 1536 | +} | ||
| 1537 | + | ||
| 1538 | +.form-input:focus, .form-select:focus, .form-textarea:focus { | ||
| 1539 | + border-color: #1890ff; | ||
| 1540 | + outline: none; | ||
| 1541 | +} | ||
| 1542 | + | ||
| 1543 | +.form-textarea { | ||
| 1544 | + resize: vertical; | ||
| 1545 | + min-height: 60px; | ||
| 1546 | +} | ||
| 1547 | + | ||
| 1548 | +.error-message { | ||
| 1549 | + font-size: 10px; | ||
| 1550 | + color: #ff4d4f; | ||
| 1551 | + margin-top: 2px; | ||
| 1552 | +} | ||
| 1553 | + | ||
| 1554 | +.form-actions { | ||
| 1555 | + display: flex; | ||
| 1556 | + justify-content: flex-end; | ||
| 1557 | + gap: 8px; | ||
| 1558 | + padding-top: 16px; | ||
| 1559 | + border-top: 1px solid #e0e0e0; | ||
| 1560 | +} | ||
| 1561 | + | ||
| 1562 | +.cancel-btn, .submit-btn { | ||
| 1563 | + padding: 6px 16px; | ||
| 1564 | + border-radius: 4px; | ||
| 1565 | + font-size: 12px; | ||
| 1566 | + cursor: pointer; | ||
| 1567 | + transition: all 0.2s; | ||
| 1568 | +} | ||
| 1569 | + | ||
| 1570 | +.cancel-btn { | ||
| 1571 | + background: #f5f5f5; | ||
| 1572 | + color: #666; | ||
| 1573 | + border: 1px solid #d9d9d9; | ||
| 1574 | +} | ||
| 1575 | + | ||
| 1576 | +.cancel-btn:hover { | ||
| 1577 | + background: #e6f7ff; | ||
| 1578 | + color: #1890ff; | ||
| 1579 | + border-color: #91d5ff; | ||
| 1580 | +} | ||
| 1581 | + | ||
| 1582 | +.submit-btn { | ||
| 1583 | + background: #1890ff; | ||
| 1584 | + color: white; | ||
| 1585 | + border: 1px solid #1890ff; | ||
| 1586 | +} | ||
| 1587 | + | ||
| 1588 | +.submit-btn:hover:not(:disabled) { | ||
| 1589 | + background: #40a9ff; | ||
| 1590 | + border-color: #40a9ff; | ||
| 1591 | +} | ||
| 1592 | + | ||
| 1593 | +.submit-btn:disabled { | ||
| 1594 | + opacity: 0.6; | ||
| 1595 | + cursor: not-allowed; | ||
| 1596 | +} | ||
| 1597 | + | ||
| 1598 | +/* 响应式设计 */ | ||
| 1599 | +@media (max-width: 768px) { | ||
| 1600 | + .search-row { | ||
| 1601 | + flex-direction: column; | ||
| 1602 | + align-items: stretch; | ||
| 251 | } | 1603 | } |
| 1604 | + | ||
| 1605 | + .search-item { | ||
| 1606 | + min-width: auto; | ||
| 252 | } | 1607 | } |
| 1608 | + | ||
| 1609 | + .form-row { | ||
| 1610 | + flex-direction: column; | ||
| 1611 | + } | ||
| 1612 | + | ||
| 1613 | + .pagination-btn { | ||
| 1614 | + padding: 2px 4px; | ||
| 1615 | + font-size: 10px; | ||
| 1616 | + height: 20px; | ||
| 1617 | + } | ||
| 1618 | + | ||
| 1619 | + .page-number { | ||
| 1620 | + padding: 2px 4px; | ||
| 1621 | + font-size: 10px; | ||
| 1622 | + min-width: 24px; | ||
| 1623 | + height: 20px; | ||
| 1624 | + } | ||
| 1625 | + | ||
| 1626 | + .dialog-content { | ||
| 1627 | + width: 95%; | ||
| 1628 | + margin: 20px; | ||
| 1629 | + } | ||
| 1630 | +} | ||
| 1631 | + | ||
| 1632 | +/* 菜单权限选择样式 */ | ||
| 1633 | +.menu-permissions { | ||
| 1634 | + border: 1px solid #e0e0e0; | ||
| 1635 | + border-radius: 6px; | ||
| 1636 | + background: white; | ||
| 1637 | + max-height: 400px; | ||
| 1638 | + overflow-y: auto; | ||
| 1639 | +} | ||
| 1640 | + | ||
| 1641 | +.permission-controls { | ||
| 1642 | + padding: 8px 12px; | ||
| 1643 | + border-bottom: 1px solid #f0f0f0; | ||
| 1644 | + background: #fafafa; | ||
| 1645 | + display: flex; | ||
| 1646 | + align-items: center; | ||
| 1647 | + gap: 8px; | ||
| 1648 | +} | ||
| 1649 | + | ||
| 1650 | +.menu-permissions .control-btn { | ||
| 1651 | + background: white; | ||
| 1652 | + border: 1px solid #d9d9d9; | ||
| 1653 | + padding: 4px 12px; | ||
| 1654 | + border-radius: 4px; | ||
| 1655 | + cursor: pointer; | ||
| 1656 | + font-size: 12px; | ||
| 1657 | + transition: all 0.2s; | ||
| 1658 | +} | ||
| 1659 | + | ||
| 1660 | +.menu-permissions .control-btn:hover { | ||
| 1661 | + background: #f0f8ff; | ||
| 1662 | + border-color: #1890ff; | ||
| 1663 | + color: #1890ff; | ||
| 1664 | +} | ||
| 1665 | + | ||
| 1666 | +.linkage-label { | ||
| 1667 | + display: flex; | ||
| 1668 | + align-items: center; | ||
| 1669 | + gap: 6px; | ||
| 1670 | + font-size: 12px; | ||
| 1671 | + color: #666; | ||
| 1672 | + cursor: pointer; | ||
| 1673 | +} | ||
| 1674 | + | ||
| 1675 | +.linkage-checkbox { | ||
| 1676 | + margin: 0; | ||
| 1677 | +} | ||
| 1678 | + | ||
| 1679 | +.menu-tree { | ||
| 1680 | + padding: 4px 0; | ||
| 1681 | +} | ||
| 1682 | + | ||
| 1683 | +.menu-item { | ||
| 1684 | + display: block; | ||
| 1685 | + margin: 0; | ||
| 1686 | +} | ||
| 1687 | + | ||
| 1688 | +.menu-row { | ||
| 1689 | + display: flex; | ||
| 1690 | + align-items: center; | ||
| 1691 | + padding: 3px 12px; | ||
| 1692 | + transition: background-color 0.2s; | ||
| 1693 | + cursor: pointer; | ||
| 1694 | +} | ||
| 1695 | + | ||
| 1696 | +.menu-row:hover { | ||
| 1697 | + background: #f5f5f5; | ||
| 1698 | +} | ||
| 1699 | + | ||
| 1700 | +.expand-icon { | ||
| 1701 | + width: 16px; | ||
| 1702 | + height: 16px; | ||
| 1703 | + display: flex; | ||
| 1704 | + align-items: center; | ||
| 1705 | + justify-content: center; | ||
| 1706 | + cursor: pointer; | ||
| 1707 | + font-size: 12px; | ||
| 1708 | + color: #666; | ||
| 1709 | + border: 1px solid #d9d9d9; | ||
| 1710 | + border-radius: 2px; | ||
| 1711 | + margin-right: 8px; | ||
| 1712 | + background: white; | ||
| 1713 | +} | ||
| 1714 | + | ||
| 1715 | +.expand-icon:hover { | ||
| 1716 | + background: #f0f0f0; | ||
| 1717 | +} | ||
| 1718 | + | ||
| 1719 | +.expand-placeholder { | ||
| 1720 | + width: 16px; | ||
| 1721 | + margin-right: 8px; | ||
| 1722 | +} | ||
| 1723 | + | ||
| 1724 | +.menu-checkbox { | ||
| 1725 | + margin: 0 8px 0 0; | ||
| 1726 | + cursor: pointer; | ||
| 1727 | +} | ||
| 1728 | + | ||
| 1729 | +.menu-icon { | ||
| 1730 | + margin-right: 8px; | ||
| 1731 | + font-size: 14px; | ||
| 1732 | +} | ||
| 1733 | + | ||
| 1734 | +.menu-name { | ||
| 1735 | + flex: 1; | ||
| 1736 | + font-size: 13px; | ||
| 1737 | + color: #333; | ||
| 1738 | + margin-right: 8px; | ||
| 1739 | +} | ||
| 1740 | + | ||
| 1741 | +.menu-permission { | ||
| 1742 | + font-size: 11px; | ||
| 1743 | + color: #999; | ||
| 1744 | + background: #f0f0f0; | ||
| 1745 | + padding: 2px 6px; | ||
| 1746 | + border-radius: 3px; | ||
| 1747 | + font-family: monospace; | ||
| 1748 | + flex-shrink: 0; | ||
| 1749 | +} | ||
| 1750 | + | ||
| 1751 | +.submenu { | ||
| 1752 | + background: #fafafa; | ||
| 1753 | + margin: 0; | ||
| 1754 | + padding: 0; | ||
| 253 | } | 1755 | } |
| 254 | </style> | 1756 | </style> | ... | ... |
| 1 | -<template> | ||
| 2 | - <div class="user-management"> | ||
| 3 | - <!-- 搜索表单 --> | ||
| 4 | - <el-card class="search-form"> | ||
| 5 | - <el-form :model="searchForm" inline> | ||
| 6 | - <el-form-item label="用户名"> | ||
| 7 | - <el-input v-model="searchForm.username" placeholder="请输入用户名" clearable /> | ||
| 8 | - </el-form-item> | ||
| 9 | - <el-form-item label="真实姓名"> | ||
| 10 | - <el-input v-model="searchForm.realName" placeholder="请输入真实姓名" clearable /> | ||
| 11 | - </el-form-item> | ||
| 12 | - <el-form-item label="状态"> | ||
| 13 | - <el-select v-model="searchForm.status" placeholder="请选择状态" clearable> | ||
| 14 | - <el-option label="正常" :value="1" /> | ||
| 15 | - <el-option label="停用" :value="0" /> | ||
| 16 | - </el-select> | ||
| 17 | - </el-form-item> | ||
| 18 | - <el-form-item> | ||
| 19 | - <el-button type="primary" @click="handleSearch"> | ||
| 20 | - <el-icon><Search /></el-icon> | ||
| 21 | - 搜索 | ||
| 22 | - </el-button> | ||
| 23 | - <el-button @click="handleReset"> | ||
| 24 | - <el-icon><Refresh /></el-icon> | ||
| 25 | - 重置 | ||
| 26 | - </el-button> | ||
| 27 | - </el-form-item> | ||
| 28 | - </el-form> | ||
| 29 | - </el-card> | ||
| 30 | - | ||
| 31 | - <!-- 操作按钮 --> | ||
| 32 | - <el-card class="operation-buttons"> | ||
| 33 | - <el-button type="primary" @click="handleAdd"> | ||
| 34 | - <el-icon><Plus /></el-icon> | ||
| 35 | - 新增用户 | ||
| 36 | - </el-button> | ||
| 37 | - <el-button type="danger" :disabled="!multipleSelection.length" @click="handleBatchDelete"> | ||
| 38 | - <el-icon><Delete /></el-icon> | ||
| 39 | - 批量删除 | ||
| 40 | - </el-button> | ||
| 41 | - </el-card> | ||
| 42 | - | ||
| 43 | - <!-- 数据表格 --> | ||
| 44 | - <el-card class="table-container"> | ||
| 45 | - <el-table | ||
| 46 | - v-loading="loading" | ||
| 47 | - :data="tableData" | ||
| 48 | - @selection-change="handleSelectionChange" | ||
| 49 | - > | ||
| 50 | - <el-table-column type="selection" width="55" /> | ||
| 51 | - <el-table-column prop="userId" label="用户ID" width="80" /> | ||
| 52 | - <el-table-column prop="username" label="用户名" width="120" /> | ||
| 53 | - <el-table-column prop="realName" label="真实姓名" width="120" /> | ||
| 54 | - <el-table-column prop="email" label="邮箱" width="200" /> | ||
| 55 | - <el-table-column prop="phone" label="手机号" width="120" /> | ||
| 56 | - <el-table-column prop="status" label="状态" width="80"> | ||
| 57 | - <template #default="{ row }"> | ||
| 58 | - <el-tag :type="row.status === 1 ? 'success' : 'danger'"> | ||
| 59 | - {{ row.status === 1 ? '正常' : '停用' }} | ||
| 60 | - </el-tag> | ||
| 61 | - </template> | ||
| 62 | - </el-table-column> | ||
| 63 | - <el-table-column prop="createTime" label="创建时间" width="180" /> | ||
| 64 | - <el-table-column label="操作" width="200" fixed="right"> | ||
| 65 | - <template #default="{ row }"> | ||
| 66 | - <el-button type="primary" size="small" @click="handleEdit(row)"> | ||
| 67 | - 编辑 | ||
| 68 | - </el-button> | ||
| 69 | - <el-button type="danger" size="small" @click="handleDelete(row)"> | ||
| 70 | - 删除 | ||
| 71 | - </el-button> | ||
| 72 | - </template> | ||
| 73 | - </el-table-column> | ||
| 74 | - </el-table> | ||
| 75 | - | ||
| 76 | - <!-- 分页 --> | ||
| 77 | - <el-pagination | ||
| 78 | - v-model:current-page="pagination.current" | ||
| 79 | - v-model:page-size="pagination.size" | ||
| 80 | - :total="pagination.total" | ||
| 81 | - :page-sizes="[10, 20, 50, 100]" | ||
| 82 | - layout="total, sizes, prev, pager, next, jumper" | ||
| 83 | - @size-change="handleSizeChange" | ||
| 84 | - @current-change="handleCurrentChange" | ||
| 85 | - /> | ||
| 86 | - </el-card> | ||
| 87 | - </div> | ||
| 88 | -</template> | ||
| 89 | - | ||
| 90 | -<script setup lang="ts"> | ||
| 91 | -import { ref, reactive, onMounted } from 'vue' | ||
| 92 | -import { ElMessage, ElMessageBox } from 'element-plus' | ||
| 93 | -import type { User } from '@/types' | ||
| 94 | - | ||
| 95 | -// 搜索表单 | ||
| 96 | -const searchForm = reactive({ | ||
| 97 | - username: '', | ||
| 98 | - realName: '', | ||
| 99 | - status: undefined | ||
| 100 | -}) | ||
| 101 | - | ||
| 102 | -// 表格数据 | ||
| 103 | -const tableData = ref<User[]>([]) | ||
| 104 | -const loading = ref(false) | ||
| 105 | -const multipleSelection = ref<User[]>([]) | ||
| 106 | - | ||
| 107 | -// 分页 | ||
| 108 | -const pagination = reactive({ | ||
| 109 | - current: 1, | ||
| 110 | - size: 10, | ||
| 111 | - total: 0 | ||
| 112 | -}) | ||
| 113 | - | ||
| 114 | -// 获取用户列表 | ||
| 115 | -const getUserList = async () => { | ||
| 116 | - loading.value = true | ||
| 117 | - try { | ||
| 118 | - // 这里应该调用API | ||
| 119 | - // const response = await getUserPageApi({ ...searchForm, ...pagination }) | ||
| 120 | - // tableData.value = response.records | ||
| 121 | - // pagination.total = response.total | ||
| 122 | - | ||
| 123 | - // 模拟数据 | ||
| 124 | - tableData.value = [ | ||
| 125 | - { | ||
| 126 | - userId: 1, | ||
| 127 | - username: 'admin', | ||
| 128 | - realName: '管理员', | ||
| 129 | - email: 'admin@example.com', | ||
| 130 | - phone: '13800138000', | ||
| 131 | - status: 1, | ||
| 132 | - createTime: '2024-01-01 10:00:00', | ||
| 133 | - updateTime: '2024-01-01 10:00:00', | ||
| 134 | - roles: [], | ||
| 135 | - permissions: [] | ||
| 136 | - } | ||
| 137 | - ] | ||
| 138 | - pagination.total = 1 | ||
| 139 | - } catch (error) { | ||
| 140 | - ElMessage.error('获取用户列表失败') | ||
| 141 | - } finally { | ||
| 142 | - loading.value = false | ||
| 143 | - } | ||
| 144 | -} | ||
| 145 | - | ||
| 146 | -// 搜索 | ||
| 147 | -const handleSearch = () => { | ||
| 148 | - pagination.current = 1 | ||
| 149 | - getUserList() | ||
| 150 | -} | ||
| 151 | - | ||
| 152 | -// 重置 | ||
| 153 | -const handleReset = () => { | ||
| 154 | - Object.assign(searchForm, { | ||
| 155 | - username: '', | ||
| 156 | - realName: '', | ||
| 157 | - status: undefined | ||
| 158 | - }) | ||
| 159 | - handleSearch() | ||
| 160 | -} | ||
| 161 | - | ||
| 162 | -// 新增用户 | ||
| 163 | -const handleAdd = () => { | ||
| 164 | - ElMessage.info('新增用户功能待实现') | ||
| 165 | -} | ||
| 166 | - | ||
| 167 | -// 编辑用户 | ||
| 168 | -const handleEdit = (row: User) => { | ||
| 169 | - ElMessage.info(`编辑用户: ${row.username}`) | ||
| 170 | -} | ||
| 171 | - | ||
| 172 | -// 删除用户 | ||
| 173 | -const handleDelete = async (row: User) => { | ||
| 174 | - try { | ||
| 175 | - await ElMessageBox.confirm(`确定要删除用户 "${row.username}" 吗?`, '提示', { | ||
| 176 | - confirmButtonText: '确定', | ||
| 177 | - cancelButtonText: '取消', | ||
| 178 | - type: 'warning' | ||
| 179 | - }) | ||
| 180 | - ElMessage.success('删除成功') | ||
| 181 | - getUserList() | ||
| 182 | - } catch (error) { | ||
| 183 | - // 用户取消删除 | ||
| 184 | - } | ||
| 185 | -} | ||
| 186 | - | ||
| 187 | -// 批量删除 | ||
| 188 | -const handleBatchDelete = async () => { | ||
| 189 | - try { | ||
| 190 | - await ElMessageBox.confirm(`确定要删除选中的 ${multipleSelection.value.length} 个用户吗?`, '提示', { | ||
| 191 | - confirmButtonText: '确定', | ||
| 192 | - cancelButtonText: '取消', | ||
| 193 | - type: 'warning' | ||
| 194 | - }) | ||
| 195 | - ElMessage.success('批量删除成功') | ||
| 196 | - getUserList() | ||
| 197 | - } catch (error) { | ||
| 198 | - // 用户取消删除 | ||
| 199 | - } | ||
| 200 | -} | ||
| 201 | - | ||
| 202 | -// 选择变化 | ||
| 203 | -const handleSelectionChange = (selection: User[]) => { | ||
| 204 | - multipleSelection.value = selection | ||
| 205 | -} | ||
| 206 | - | ||
| 207 | -// 分页大小变化 | ||
| 208 | -const handleSizeChange = (size: number) => { | ||
| 209 | - pagination.size = size | ||
| 210 | - getUserList() | ||
| 211 | -} | ||
| 212 | - | ||
| 213 | -// 当前页变化 | ||
| 214 | -const handleCurrentChange = (current: number) => { | ||
| 215 | - pagination.current = current | ||
| 216 | - getUserList() | ||
| 217 | -} | ||
| 218 | - | ||
| 219 | -// 组件挂载时获取数据 | ||
| 220 | -onMounted(() => { | ||
| 221 | - getUserList() | ||
| 222 | -}) | ||
| 223 | -</script> | ||
| 224 | - | ||
| 225 | -<style lang="scss" scoped> | ||
| 226 | -.user-management { | ||
| 227 | - .search-form { | ||
| 228 | - margin-bottom: 16px; | ||
| 229 | - } | ||
| 230 | - | ||
| 231 | - .operation-buttons { | ||
| 232 | - margin-bottom: 16px; | ||
| 233 | - } | ||
| 234 | - | ||
| 235 | - .table-container { | ||
| 236 | - .el-pagination { | ||
| 237 | - margin-top: 16px; | ||
| 238 | - text-align: right; | ||
| 239 | - } | ||
| 240 | - } | ||
| 241 | -} | ||
| 242 | -</style> |
frontend/src/views/users/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="users-container"> | ||
| 3 | + <!-- 搜索筛选区域 --> | ||
| 4 | + <div class="search-section"> | ||
| 5 | + <div class="search-row"> | ||
| 6 | + <div class="search-item"> | ||
| 7 | + <label>登录名称:</label> | ||
| 8 | + <input | ||
| 9 | + v-model="searchForm.username" | ||
| 10 | + type="text" | ||
| 11 | + class="search-input" | ||
| 12 | + placeholder="请输入登录名称" | ||
| 13 | + /> | ||
| 14 | + </div> | ||
| 15 | + <div class="search-item"> | ||
| 16 | + <label>手机号码:</label> | ||
| 17 | + <input | ||
| 18 | + v-model="searchForm.phone" | ||
| 19 | + type="text" | ||
| 20 | + class="search-input" | ||
| 21 | + placeholder="请输入手机号码" | ||
| 22 | + /> | ||
| 23 | + </div> | ||
| 24 | + <div class="search-item"> | ||
| 25 | + <label>用户状态:</label> | ||
| 26 | + <select v-model="searchForm.status" class="search-select"> | ||
| 27 | + <option value="">所有</option> | ||
| 28 | + <option value="1">正常</option> | ||
| 29 | + <option value="0">停用</option> | ||
| 30 | + </select> | ||
| 31 | + </div> | ||
| 32 | + <div class="search-item"> | ||
| 33 | + <label>创建时间:</label> | ||
| 34 | + <div class="date-range"> | ||
| 35 | + <div class="date-picker-container"> | ||
| 36 | + <input | ||
| 37 | + v-model="searchForm.startTime" | ||
| 38 | + type="text" | ||
| 39 | + class="date-input" | ||
| 40 | + placeholder="开始时间" | ||
| 41 | + readonly | ||
| 42 | + @click="toggleStartDatePicker" | ||
| 43 | + @mouseenter="showStartDateOptions = true" | ||
| 44 | + @mouseleave="hideStartDateOptions" | ||
| 45 | + /> | ||
| 46 | + <div v-if="showStartDateOptions" class="date-options" @mouseenter="showStartDateOptions = true" @mouseleave="hideStartDateOptions"> | ||
| 47 | + <div class="date-option" @click="selectStartDate('today')">今天</div> | ||
| 48 | + <div class="date-option" @click="selectStartDate('yesterday')">昨天</div> | ||
| 49 | + <div class="date-option" @click="selectStartDate('thisWeek')">本周</div> | ||
| 50 | + <div class="date-option" @click="selectStartDate('lastWeek')">上周</div> | ||
| 51 | + <div class="date-option" @click="selectStartDate('thisMonth')">本月</div> | ||
| 52 | + <div class="date-option" @click="selectStartDate('lastMonth')">上月</div> | ||
| 53 | + <div class="date-option" @click="selectStartDate('custom')">自定义</div> | ||
| 54 | + </div> | ||
| 55 | + <!-- 自定义日期选择器 --> | ||
| 56 | + <div v-if="showStartDatePicker" class="custom-date-picker"> | ||
| 57 | + <input | ||
| 58 | + type="date" | ||
| 59 | + v-model="searchForm.startTime" | ||
| 60 | + @change="showStartDatePicker = false" | ||
| 61 | + class="date-input" | ||
| 62 | + /> | ||
| 63 | + </div> | ||
| 64 | + </div> | ||
| 65 | + <span class="date-separator">-</span> | ||
| 66 | + <div class="date-picker-container"> | ||
| 67 | + <input | ||
| 68 | + v-model="searchForm.endTime" | ||
| 69 | + type="text" | ||
| 70 | + class="date-input" | ||
| 71 | + placeholder="结束时间" | ||
| 72 | + readonly | ||
| 73 | + @click="toggleEndDatePicker" | ||
| 74 | + @mouseenter="showEndDateOptions = true" | ||
| 75 | + @mouseleave="hideEndDateOptions" | ||
| 76 | + /> | ||
| 77 | + <div v-if="showEndDateOptions" class="date-options" @mouseenter="showEndDateOptions = true" @mouseleave="hideEndDateOptions"> | ||
| 78 | + <div class="date-option" @click="selectEndDate('today')">今天</div> | ||
| 79 | + <div class="date-option" @click="selectEndDate('yesterday')">昨天</div> | ||
| 80 | + <div class="date-option" @click="selectEndDate('thisWeek')">本周</div> | ||
| 81 | + <div class="date-option" @click="selectEndDate('lastWeek')">上周</div> | ||
| 82 | + <div class="date-option" @click="selectEndDate('thisMonth')">本月</div> | ||
| 83 | + <div class="date-option" @click="selectEndDate('lastMonth')">上月</div> | ||
| 84 | + <div class="date-option" @click="selectEndDate('custom')">自定义</div> | ||
| 85 | + </div> | ||
| 86 | + <!-- 自定义日期选择器 --> | ||
| 87 | + <div v-if="showEndDatePicker" class="custom-date-picker"> | ||
| 88 | + <input | ||
| 89 | + type="date" | ||
| 90 | + v-model="searchForm.endTime" | ||
| 91 | + @change="showEndDatePicker = false" | ||
| 92 | + class="date-input" | ||
| 93 | + /> | ||
| 94 | + </div> | ||
| 95 | + </div> | ||
| 96 | + </div> | ||
| 97 | + </div> | ||
| 98 | + <div class="search-actions"> | ||
| 99 | + <button @click="handleSearch" class="search-btn">🔍 搜索</button> | ||
| 100 | + <button @click="handleReset" class="reset-btn">🔄 重置</button> | ||
| 101 | + </div> | ||
| 102 | + </div> | ||
| 103 | + </div> | ||
| 104 | + | ||
| 105 | + <!-- 操作按钮区域 --> | ||
| 106 | + <div class="action-section"> | ||
| 107 | + <div class="action-buttons"> | ||
| 108 | + <button @click="handleAdd" class="action-btn primary">✨ 新增</button> | ||
| 109 | + <button @click="handleBatchDelete" class="action-btn danger">🗑️ 删除</button> | ||
| 110 | + </div> | ||
| 111 | + </div> | ||
| 112 | + | ||
| 113 | + <!-- 数据表格 --> | ||
| 114 | + <div class="table-section"> | ||
| 115 | + <div class="table-header"> | ||
| 116 | + <div class="table-controls"> | ||
| 117 | + <button @click="handleTableSearch" class="control-btn" title="搜索">🔍</button> | ||
| 118 | + <button @click="handleTableRefresh" class="control-btn" title="刷新">🔄</button> | ||
| 119 | + <button @click="handleTableExport" class="control-btn" title="导出">📋</button> | ||
| 120 | + <button @click="handleTableViewToggle" class="control-btn" title="视图切换">⊞</button> | ||
| 121 | + </div> | ||
| 122 | + </div> | ||
| 123 | + | ||
| 124 | + <div class="table-container"> | ||
| 125 | + <div v-if="loading" class="loading-overlay"> | ||
| 126 | + <div class="loading-spinner">加载中...</div> | ||
| 127 | + </div> | ||
| 128 | + <table class="data-table"> | ||
| 129 | + <thead> | ||
| 130 | + <tr> | ||
| 131 | + <th> | ||
| 132 | + <input | ||
| 133 | + type="checkbox" | ||
| 134 | + class="select-all" | ||
| 135 | + v-model="selectAll" | ||
| 136 | + @change="handleSelectAll" | ||
| 137 | + /> | ||
| 138 | + </th> | ||
| 139 | + <th>用户ID</th> | ||
| 140 | + <th class="sortable">登录名称 ↕️</th> | ||
| 141 | + <th>用户名称</th> | ||
| 142 | + <th>角色</th> | ||
| 143 | + <th>手机</th> | ||
| 144 | + <th>用户状态</th> | ||
| 145 | + <th class="sortable">创建时间 ↕️</th> | ||
| 146 | + <th>操作</th> | ||
| 147 | + </tr> | ||
| 148 | + </thead> | ||
| 149 | + <tbody> | ||
| 150 | + <tr v-for="user in users" :key="user.userId"> | ||
| 151 | + <td> | ||
| 152 | + <input | ||
| 153 | + type="checkbox" | ||
| 154 | + class="row-checkbox" | ||
| 155 | + :checked="selectedUsers.includes(user.userId)" | ||
| 156 | + @change="handleSelectUser(user.userId)" | ||
| 157 | + /> | ||
| 158 | + </td> | ||
| 159 | + <td>{{ user.userId }}</td> | ||
| 160 | + <td><a href="#" class="user-link">{{ user.username }}</a></td> | ||
| 161 | + <td>{{ user.realName }}</td> | ||
| 162 | + <td>{{ user.roles?.[0]?.roleName || '未分配' }}</td> | ||
| 163 | + <td>{{ user.phone }}</td> | ||
| 164 | + <td> | ||
| 165 | + <label class="status-switch"> | ||
| 166 | + <input | ||
| 167 | + type="checkbox" | ||
| 168 | + :checked="user.status === 1" | ||
| 169 | + @change="toggleStatus(user)" | ||
| 170 | + /> | ||
| 171 | + <span class="switch-slider"></span> | ||
| 172 | + </label> | ||
| 173 | + </td> | ||
| 174 | + <td>{{ formatDateTime(user.createTime) }}</td> | ||
| 175 | + <td> | ||
| 176 | + <button @click="handleEdit(user)" class="table-btn edit">✏️ 编辑</button> | ||
| 177 | + <button @click="handleDelete(user)" class="table-btn delete">🗑️ 删除</button> | ||
| 178 | + </td> | ||
| 179 | + </tr> | ||
| 180 | + </tbody> | ||
| 181 | + </table> | ||
| 182 | + </div> | ||
| 183 | + | ||
| 184 | + <!-- 分页信息 --> | ||
| 185 | + <div class="table-footer"> | ||
| 186 | + <div class="pagination-left"> | ||
| 187 | + <div class="pagination-info"> | ||
| 188 | + 共 {{ total }} 条记录,第 {{ currentPage }} / {{ totalPages }} 页 | ||
| 189 | + </div> | ||
| 190 | + <div class="page-size-selector"> | ||
| 191 | + <label>每页显示:</label> | ||
| 192 | + <select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select"> | ||
| 193 | + <option value="10">10条</option> | ||
| 194 | + <option value="20">20条</option> | ||
| 195 | + <option value="50">50条</option> | ||
| 196 | + <option value="100">100条</option> | ||
| 197 | + </select> | ||
| 198 | + </div> | ||
| 199 | + </div> | ||
| 200 | + <div class="pagination-container"> | ||
| 201 | + <button | ||
| 202 | + @click="handlePageChange(currentPage - 1)" | ||
| 203 | + :disabled="currentPage <= 1" | ||
| 204 | + class="pagination-btn prev-btn" | ||
| 205 | + > | ||
| 206 | + 上一页 | ||
| 207 | + </button> | ||
| 208 | + <div class="pagination-pages"> | ||
| 209 | + <button | ||
| 210 | + v-for="page in getPageNumbers()" | ||
| 211 | + :key="page" | ||
| 212 | + @click="handlePageChange(page)" | ||
| 213 | + :class="['page-number', { active: page === currentPage }]" | ||
| 214 | + > | ||
| 215 | + {{ page }} | ||
| 216 | + </button> | ||
| 217 | + </div> | ||
| 218 | + <button | ||
| 219 | + @click="handlePageChange(currentPage + 1)" | ||
| 220 | + :disabled="currentPage >= totalPages" | ||
| 221 | + class="pagination-btn next-btn" | ||
| 222 | + > | ||
| 223 | + 下一页 | ||
| 224 | + </button> | ||
| 225 | + </div> | ||
| 226 | + </div> | ||
| 227 | + </div> | ||
| 228 | + | ||
| 229 | + <!-- 用户表单对话框 --> | ||
| 230 | + <div v-if="showUserDialog" class="dialog-overlay" @click="showUserDialog = false"> | ||
| 231 | + <div class="dialog-content" @click.stop> | ||
| 232 | + <div class="dialog-header"> | ||
| 233 | + <h3>{{ dialogTitle }}</h3> | ||
| 234 | + <button @click="showUserDialog = false" class="close-btn">×</button> | ||
| 235 | + </div> | ||
| 236 | + <div class="dialog-body"> | ||
| 237 | + <form @submit.prevent="handleSave"> | ||
| 238 | + <div class="form-row"> | ||
| 239 | + <div class="form-group"> | ||
| 240 | + <label class="required-label">用户名 <span class="required-asterisk">*</span></label> | ||
| 241 | + <input | ||
| 242 | + v-model="userForm.username" | ||
| 243 | + type="text" | ||
| 244 | + class="form-input" | ||
| 245 | + :class="{ 'error': fieldErrors.username }" | ||
| 246 | + required | ||
| 247 | + /> | ||
| 248 | + <div v-if="fieldErrors.username" class="field-error">{{ fieldErrors.username }}</div> | ||
| 249 | + </div> | ||
| 250 | + <div class="form-group"> | ||
| 251 | + <label class="required-label">真实姓名 <span class="required-asterisk">*</span></label> | ||
| 252 | + <input | ||
| 253 | + v-model="userForm.realName" | ||
| 254 | + type="text" | ||
| 255 | + class="form-input" | ||
| 256 | + :class="{ 'error': fieldErrors.realName }" | ||
| 257 | + required | ||
| 258 | + /> | ||
| 259 | + <div v-if="fieldErrors.realName" class="field-error">{{ fieldErrors.realName }}</div> | ||
| 260 | + </div> | ||
| 261 | + </div> | ||
| 262 | + <div class="form-row"> | ||
| 263 | + <div class="form-group"> | ||
| 264 | + <label class="required-label">手机号 <span class="required-asterisk">*</span></label> | ||
| 265 | + <input | ||
| 266 | + v-model="userForm.phone" | ||
| 267 | + type="text" | ||
| 268 | + class="form-input" | ||
| 269 | + :class="{ 'error': fieldErrors.phone }" | ||
| 270 | + required | ||
| 271 | + /> | ||
| 272 | + <div v-if="fieldErrors.phone" class="field-error">{{ fieldErrors.phone }}</div> | ||
| 273 | + </div> | ||
| 274 | + <div class="form-group"> | ||
| 275 | + <label>邮箱</label> | ||
| 276 | + <input | ||
| 277 | + v-model="userForm.email" | ||
| 278 | + type="email" | ||
| 279 | + class="form-input" | ||
| 280 | + :class="{ 'error': fieldErrors.email }" | ||
| 281 | + /> | ||
| 282 | + <div v-if="fieldErrors.email" class="field-error">{{ fieldErrors.email }}</div> | ||
| 283 | + </div> | ||
| 284 | + </div> | ||
| 285 | + <div class="form-row"> | ||
| 286 | + <div class="form-group"> | ||
| 287 | + <label>状态</label> | ||
| 288 | + <select v-model="userForm.status" class="form-select"> | ||
| 289 | + <option :value="1">正常</option> | ||
| 290 | + <option :value="0">停用</option> | ||
| 291 | + </select> | ||
| 292 | + </div> | ||
| 293 | + <div class="form-group"> | ||
| 294 | + <label>备注</label> | ||
| 295 | + <input | ||
| 296 | + v-model="userForm.remark" | ||
| 297 | + type="text" | ||
| 298 | + class="form-input" | ||
| 299 | + :class="{ 'error': fieldErrors.remark }" | ||
| 300 | + placeholder="请输入备注信息" | ||
| 301 | + /> | ||
| 302 | + <div v-if="fieldErrors.remark" class="field-error">{{ fieldErrors.remark }}</div> | ||
| 303 | + </div> | ||
| 304 | + </div> | ||
| 305 | + <div class="form-row"> | ||
| 306 | + <div class="form-group"> | ||
| 307 | + <label>角色</label> | ||
| 308 | + <select v-model="userForm.roleIds" multiple class="form-select role-select" :disabled="rolesLoading"> | ||
| 309 | + <option v-if="rolesLoading" disabled>正在加载角色列表...</option> | ||
| 310 | + <option v-else-if="roles.length === 0" disabled>暂无可用角色</option> | ||
| 311 | + <option v-else v-for="role in roles" :key="role.roleId" :value="role.roleId"> | ||
| 312 | + {{ role.roleName }} ({{ role.roleCode }}) | ||
| 313 | + </option> | ||
| 314 | + </select> | ||
| 315 | + <small class="form-hint"> | ||
| 316 | + <span v-if="rolesLoading">正在加载有效角色列表...</span> | ||
| 317 | + <span v-else> | ||
| 318 | + 按住Ctrl键可多选角色,已选择 {{ userForm.roleIds.length }} 个角色(仅显示有效角色) | ||
| 319 | + <button @click="fetchRoles" class="refresh-roles-btn" type="button">刷新角色</button> | ||
| 320 | + </span> | ||
| 321 | + </small> | ||
| 322 | + <!-- 显示已选择的角色 --> | ||
| 323 | + <div v-if="userForm.roleIds.length > 0" class="selected-roles"> | ||
| 324 | + <div class="selected-roles-label">已选择的角色:</div> | ||
| 325 | + <div class="role-tags"> | ||
| 326 | + <span v-for="roleId in userForm.roleIds" :key="roleId" class="role-tag"> | ||
| 327 | + {{ getRoleName(roleId) }} ({{ getRoleCode(roleId) }}) | ||
| 328 | + </span> | ||
| 329 | + </div> | ||
| 330 | + </div> | ||
| 331 | + </div> | ||
| 332 | + </div> | ||
| 333 | + </form> | ||
| 334 | + </div> | ||
| 335 | + <div class="dialog-footer"> | ||
| 336 | + <button @click="showUserDialog = false" class="btn-cancel">取消</button> | ||
| 337 | + <button @click="handleSave" class="btn-save" :disabled="formLoading"> | ||
| 338 | + {{ formLoading ? '保存中...' : '保存' }} | ||
| 339 | + </button> | ||
| 340 | + </div> | ||
| 341 | + </div> | ||
| 342 | + </div> | ||
| 343 | + </div> | ||
| 344 | +</template> | ||
| 345 | + | ||
| 346 | +<script setup lang="ts"> | ||
| 347 | +import { ref, reactive, computed, onMounted } from 'vue' | ||
| 348 | +import { useRouter } from 'vue-router' | ||
| 349 | +import { userApi, type User, type UserSearchParams, type UserForm } from '../../api/user' | ||
| 350 | +import { roleApi, type Role } from '../../api/role' | ||
| 351 | + | ||
| 352 | +const router = useRouter() | ||
| 353 | + | ||
| 354 | +// 消息提示函数 | ||
| 355 | +const showMessage = (message: string, type: 'success' | 'error' | 'warning' = 'success') => { | ||
| 356 | + // 这里可以使用更优雅的提示组件,暂时使用alert | ||
| 357 | + alert(message) | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +// 角色列表 | ||
| 361 | +const roles = ref<Role[]>([]) | ||
| 362 | +const rolesLoading = ref(false) | ||
| 363 | + | ||
| 364 | +// 获取角色列表 | ||
| 365 | +const fetchRoles = async () => { | ||
| 366 | + try { | ||
| 367 | + rolesLoading.value = true | ||
| 368 | + console.log('开始获取有效角色列表...') | ||
| 369 | + | ||
| 370 | + // 使用角色列表接口获取所有有效角色(status=1) | ||
| 371 | + const params = { | ||
| 372 | + roleCode: "", | ||
| 373 | + roleName: "", | ||
| 374 | + status: 1, // 只获取有效角色 | ||
| 375 | + pageNum: 1, | ||
| 376 | + pageSize: 9999 // 获取全部有效角色 | ||
| 377 | + } | ||
| 378 | + | ||
| 379 | + console.log('角色列表请求参数:', params) | ||
| 380 | + console.log('API基础URL:', 'http://localhost:8083') | ||
| 381 | + | ||
| 382 | + const response = await roleApi.getRoleList(params) | ||
| 383 | + console.log('角色列表响应:', response) | ||
| 384 | + | ||
| 385 | + // 根据后端响应格式,数据在response.data中 | ||
| 386 | + if (response && response.data && response.data.records) { | ||
| 387 | + roles.value = response.data.records | ||
| 388 | + console.log('有效角色列表加载成功:', roles.value) | ||
| 389 | + console.log('加载的角色数量:', roles.value.length) | ||
| 390 | + } else { | ||
| 391 | + console.warn('角色列表响应格式异常:', response) | ||
| 392 | + roles.value = [] | ||
| 393 | + } | ||
| 394 | + } catch (error: any) { | ||
| 395 | + console.error('获取角色列表失败:', error) | ||
| 396 | + console.error('错误详情:', { | ||
| 397 | + message: error.message, | ||
| 398 | + response: error.response, | ||
| 399 | + request: error.request, | ||
| 400 | + config: error.config | ||
| 401 | + }) | ||
| 402 | + showMessage('获取角色列表失败,使用默认角色', 'warning') | ||
| 403 | + // 网络错误时使用默认角色 | ||
| 404 | + roles.value = [ | ||
| 405 | + { | ||
| 406 | + roleId: 1, | ||
| 407 | + roleCode: 'ADMIN', | ||
| 408 | + roleName: '系统管理员', | ||
| 409 | + status: 1, | ||
| 410 | + statusText: '正常', | ||
| 411 | + remark: '系统管理员角色', | ||
| 412 | + createBy: 'system', | ||
| 413 | + createTime: '2025-09-19T13:27:04', | ||
| 414 | + updateBy: '', | ||
| 415 | + updateTime: '2025-09-19T13:27:04', | ||
| 416 | + menus: null | ||
| 417 | + }, | ||
| 418 | + { | ||
| 419 | + roleId: 2, | ||
| 420 | + roleCode: 'OPERATOR', | ||
| 421 | + roleName: '运营人员', | ||
| 422 | + status: 1, | ||
| 423 | + statusText: '正常', | ||
| 424 | + remark: '运营人员角色', | ||
| 425 | + createBy: 'system', | ||
| 426 | + createTime: '2025-09-19T13:27:04', | ||
| 427 | + updateBy: '', | ||
| 428 | + updateTime: '2025-09-19T13:27:04', | ||
| 429 | + menus: null | ||
| 430 | + }, | ||
| 431 | + { | ||
| 432 | + roleId: 3, | ||
| 433 | + roleCode: 'AUDITOR', | ||
| 434 | + roleName: '审核人员', | ||
| 435 | + status: 1, | ||
| 436 | + statusText: '正常', | ||
| 437 | + remark: '审核人员角色', | ||
| 438 | + createBy: 'system', | ||
| 439 | + createTime: '2025-09-19T13:27:04', | ||
| 440 | + updateBy: '', | ||
| 441 | + updateTime: '2025-09-19T13:27:04', | ||
| 442 | + menus: null | ||
| 443 | + } | ||
| 444 | + ] | ||
| 445 | + } finally { | ||
| 446 | + rolesLoading.value = false | ||
| 447 | + } | ||
| 448 | +} | ||
| 449 | + | ||
| 450 | +// 根据角色ID获取角色名称 | ||
| 451 | +const getRoleName = (roleId: number): string => { | ||
| 452 | + const role = roles.value.find(r => r.roleId === roleId) | ||
| 453 | + return role ? role.roleName : `未知角色(${roleId})` | ||
| 454 | +} | ||
| 455 | + | ||
| 456 | +// 根据角色ID获取角色代码 | ||
| 457 | +const getRoleCode = (roleId: number): string => { | ||
| 458 | + const role = roles.value.find(r => r.roleId === roleId) | ||
| 459 | + return role ? role.roleCode : `UNKNOWN` | ||
| 460 | +} | ||
| 461 | + | ||
| 462 | +// 搜索表单 | ||
| 463 | +const searchForm = reactive({ | ||
| 464 | + username: '', | ||
| 465 | + phone: '', | ||
| 466 | + status: '', | ||
| 467 | + startTime: '', | ||
| 468 | + endTime: '' | ||
| 469 | +}) | ||
| 470 | + | ||
| 471 | +// 时间选择器状态 | ||
| 472 | +const showStartDateOptions = ref(false) | ||
| 473 | +const showEndDateOptions = ref(false) | ||
| 474 | +const showStartDatePicker = ref(false) | ||
| 475 | +const showEndDatePicker = ref(false) | ||
| 476 | + | ||
| 477 | +// 用户列表 | ||
| 478 | +const users = ref<User[]>([]) | ||
| 479 | +const loading = ref(false) | ||
| 480 | + | ||
| 481 | +// 分页相关 | ||
| 482 | +const currentPage = ref(1) | ||
| 483 | +const pageSize = ref(10) | ||
| 484 | +const total = ref(0) | ||
| 485 | +const totalPages = computed(() => Math.ceil(total.value / pageSize.value)) | ||
| 486 | + | ||
| 487 | +// 获取页码数组 | ||
| 488 | +const getPageNumbers = () => { | ||
| 489 | + const pages = [] | ||
| 490 | + const maxVisible = 5 // 最多显示5个页码 | ||
| 491 | + const start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2)) | ||
| 492 | + const end = Math.min(totalPages.value, start + maxVisible - 1) | ||
| 493 | + | ||
| 494 | + for (let i = start; i <= end; i++) { | ||
| 495 | + pages.push(i) | ||
| 496 | + } | ||
| 497 | + return pages | ||
| 498 | +} | ||
| 499 | + | ||
| 500 | +// 选中的用户 | ||
| 501 | +const selectedUsers = ref<number[]>([]) | ||
| 502 | +const selectAll = ref(false) | ||
| 503 | + | ||
| 504 | +// 用户表单 | ||
| 505 | +const userForm = reactive<UserForm>({ | ||
| 506 | + userId: 0, | ||
| 507 | + username: '', | ||
| 508 | + realName: '', | ||
| 509 | + phone: '', | ||
| 510 | + email: '', | ||
| 511 | + status: 1, | ||
| 512 | + remark: '', | ||
| 513 | + roleIds: [], | ||
| 514 | + password: '' | ||
| 515 | +}) | ||
| 516 | + | ||
| 517 | +// 表单字段错误信息 | ||
| 518 | +const fieldErrors = reactive<Record<string, string>>({}) | ||
| 519 | + | ||
| 520 | +// 对话框状态 | ||
| 521 | +const showUserDialog = ref(false) | ||
| 522 | +const dialogTitle = ref('') | ||
| 523 | +const formLoading = ref(false) | ||
| 524 | + | ||
| 525 | +// 格式化时间显示 | ||
| 526 | +const formatDateTime = (dateTime: string | undefined): string => { | ||
| 527 | + if (!dateTime) return '' | ||
| 528 | + | ||
| 529 | + // 将 ISO 格式的时间字符串转换为可读格式 | ||
| 530 | + // 例如: 2025-09-23T17:49:06 -> 2025-09-23 17:49:06 | ||
| 531 | + return dateTime.replace('T', ' ') | ||
| 532 | +} | ||
| 533 | + | ||
| 534 | +// 获取当前日期(YYYY-MM-DD格式) | ||
| 535 | +const getCurrentDate = (): string => { | ||
| 536 | + const now = new Date() | ||
| 537 | + return now.toISOString().split('T')[0] | ||
| 538 | +} | ||
| 539 | + | ||
| 540 | +// 获取指定天数前的日期 | ||
| 541 | +const getDateBefore = (days: number): string => { | ||
| 542 | + const date = new Date() | ||
| 543 | + date.setDate(date.getDate() - days) | ||
| 544 | + return date.toISOString().split('T')[0] | ||
| 545 | +} | ||
| 546 | + | ||
| 547 | +// 获取本周开始日期(周一) | ||
| 548 | +const getWeekStart = (): string => { | ||
| 549 | + const now = new Date() | ||
| 550 | + const day = now.getDay() | ||
| 551 | + const diff = now.getDate() - day + (day === 0 ? -6 : 1) // 调整到周一 | ||
| 552 | + const monday = new Date(now.setDate(diff)) | ||
| 553 | + return monday.toISOString().split('T')[0] | ||
| 554 | +} | ||
| 555 | + | ||
| 556 | +// 获取本月开始日期 | ||
| 557 | +const getMonthStart = (): string => { | ||
| 558 | + const now = new Date() | ||
| 559 | + return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0] | ||
| 560 | +} | ||
| 561 | + | ||
| 562 | +// 获取上月开始日期 | ||
| 563 | +const getLastMonthStart = (): string => { | ||
| 564 | + const now = new Date() | ||
| 565 | + return new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString().split('T')[0] | ||
| 566 | +} | ||
| 567 | + | ||
| 568 | +// 获取上月结束日期 | ||
| 569 | +const getLastMonthEnd = (): string => { | ||
| 570 | + const now = new Date() | ||
| 571 | + return new Date(now.getFullYear(), now.getMonth(), 0).toISOString().split('T')[0] | ||
| 572 | +} | ||
| 573 | + | ||
| 574 | +// 开始时间选择器控制 | ||
| 575 | +const toggleStartDatePicker = () => { | ||
| 576 | + showStartDatePicker.value = !showStartDatePicker.value | ||
| 577 | + showStartDateOptions.value = false | ||
| 578 | +} | ||
| 579 | + | ||
| 580 | +const hideStartDateOptions = () => { | ||
| 581 | + setTimeout(() => { | ||
| 582 | + showStartDateOptions.value = false | ||
| 583 | + }, 200) | ||
| 584 | +} | ||
| 585 | + | ||
| 586 | +// 结束时间选择器控制 | ||
| 587 | +const toggleEndDatePicker = () => { | ||
| 588 | + showEndDatePicker.value = !showEndDatePicker.value | ||
| 589 | + showEndDateOptions.value = false | ||
| 590 | +} | ||
| 591 | + | ||
| 592 | +const hideEndDateOptions = () => { | ||
| 593 | + setTimeout(() => { | ||
| 594 | + showEndDateOptions.value = false | ||
| 595 | + }, 200) | ||
| 596 | +} | ||
| 597 | + | ||
| 598 | +// 选择开始时间 | ||
| 599 | +const selectStartDate = (type: string) => { | ||
| 600 | + switch (type) { | ||
| 601 | + case 'today': | ||
| 602 | + searchForm.startTime = getCurrentDate() | ||
| 603 | + break | ||
| 604 | + case 'yesterday': | ||
| 605 | + searchForm.startTime = getDateBefore(1) | ||
| 606 | + break | ||
| 607 | + case 'thisWeek': | ||
| 608 | + searchForm.startTime = getWeekStart() | ||
| 609 | + break | ||
| 610 | + case 'lastWeek': | ||
| 611 | + searchForm.startTime = getDateBefore(14) // 两周前 | ||
| 612 | + break | ||
| 613 | + case 'thisMonth': | ||
| 614 | + searchForm.startTime = getMonthStart() | ||
| 615 | + break | ||
| 616 | + case 'lastMonth': | ||
| 617 | + searchForm.startTime = getLastMonthStart() | ||
| 618 | + break | ||
| 619 | + case 'custom': | ||
| 620 | + // 这里可以打开自定义日期选择器 | ||
| 621 | + showStartDatePicker.value = true | ||
| 622 | + break | ||
| 623 | + } | ||
| 624 | + showStartDateOptions.value = false | ||
| 625 | +} | ||
| 626 | + | ||
| 627 | +// 选择结束时间 | ||
| 628 | +const selectEndDate = (type: string) => { | ||
| 629 | + switch (type) { | ||
| 630 | + case 'today': | ||
| 631 | + searchForm.endTime = getCurrentDate() | ||
| 632 | + break | ||
| 633 | + case 'yesterday': | ||
| 634 | + searchForm.endTime = getDateBefore(1) | ||
| 635 | + break | ||
| 636 | + case 'thisWeek': | ||
| 637 | + searchForm.endTime = getCurrentDate() | ||
| 638 | + break | ||
| 639 | + case 'lastWeek': | ||
| 640 | + searchForm.endTime = getDateBefore(7) // 一周前 | ||
| 641 | + break | ||
| 642 | + case 'thisMonth': | ||
| 643 | + searchForm.endTime = getCurrentDate() | ||
| 644 | + break | ||
| 645 | + case 'lastMonth': | ||
| 646 | + searchForm.endTime = getLastMonthEnd() | ||
| 647 | + break | ||
| 648 | + case 'custom': | ||
| 649 | + // 这里可以打开自定义日期选择器 | ||
| 650 | + showEndDatePicker.value = true | ||
| 651 | + break | ||
| 652 | + } | ||
| 653 | + showEndDateOptions.value = false | ||
| 654 | +} | ||
| 655 | + | ||
| 656 | +// 获取用户列表 | ||
| 657 | +const fetchUsers = async () => { | ||
| 658 | + try { | ||
| 659 | + loading.value = true | ||
| 660 | + const params: UserSearchParams = { | ||
| 661 | + username: searchForm.username || undefined, | ||
| 662 | + phone: searchForm.phone || undefined, | ||
| 663 | + status: searchForm.status ? parseInt(searchForm.status) : undefined, | ||
| 664 | + startTime: searchForm.startTime || undefined, | ||
| 665 | + endTime: searchForm.endTime || undefined, | ||
| 666 | + page: currentPage.value, | ||
| 667 | + pageSize: pageSize.value | ||
| 668 | + } | ||
| 669 | + | ||
| 670 | + const response = await userApi.getUserList(params) | ||
| 671 | + if (response.code === 200) { | ||
| 672 | + users.value = response.data.records | ||
| 673 | + total.value = response.data.total | ||
| 674 | + } else { | ||
| 675 | + showMessage(response.message || '获取用户列表失败', 'error') | ||
| 676 | + } | ||
| 677 | + } catch (error: any) { | ||
| 678 | + console.error('获取用户列表失败:', error) | ||
| 679 | + showMessage(error.response?.data?.message || '获取用户列表失败', 'error') | ||
| 680 | + } finally { | ||
| 681 | + loading.value = false | ||
| 682 | + } | ||
| 683 | +} | ||
| 684 | + | ||
| 685 | +// 搜索功能 | ||
| 686 | +const handleSearch = () => { | ||
| 687 | + currentPage.value = 1 | ||
| 688 | + fetchUsers() | ||
| 689 | +} | ||
| 690 | + | ||
| 691 | +// 重置搜索 | ||
| 692 | +const handleReset = () => { | ||
| 693 | + Object.assign(searchForm, { | ||
| 694 | + username: '', | ||
| 695 | + phone: '', | ||
| 696 | + status: '', | ||
| 697 | + startTime: '', | ||
| 698 | + endTime: '' | ||
| 699 | + }) | ||
| 700 | + currentPage.value = 1 | ||
| 701 | + fetchUsers() | ||
| 702 | +} | ||
| 703 | + | ||
| 704 | +// 清除字段错误 | ||
| 705 | +const clearFieldErrors = () => { | ||
| 706 | + Object.keys(fieldErrors).forEach(key => { | ||
| 707 | + delete fieldErrors[key] | ||
| 708 | + }) | ||
| 709 | +} | ||
| 710 | + | ||
| 711 | +// 新增用户 | ||
| 712 | +const handleAdd = async () => { | ||
| 713 | + dialogTitle.value = '新增用户' | ||
| 714 | + | ||
| 715 | + // 清除之前的错误信息 | ||
| 716 | + clearFieldErrors() | ||
| 717 | + | ||
| 718 | + // 确保角色列表已加载 | ||
| 719 | + if (roles.value.length === 0) { | ||
| 720 | + console.log('角色列表为空,重新加载角色...') | ||
| 721 | + await fetchRoles() | ||
| 722 | + } | ||
| 723 | + | ||
| 724 | + Object.assign(userForm, { | ||
| 725 | + userId: 0, | ||
| 726 | + username: '', | ||
| 727 | + realName: '', | ||
| 728 | + phone: '', | ||
| 729 | + email: '', | ||
| 730 | + status: 1, | ||
| 731 | + remark: '', | ||
| 732 | + roleIds: [], // 新增用户时默认为空,用户可以选择多个角色 | ||
| 733 | + password: '123456' // 设置默认密码 | ||
| 734 | + }) | ||
| 735 | + showUserDialog.value = true | ||
| 736 | +} | ||
| 737 | + | ||
| 738 | +// 编辑用户 | ||
| 739 | +const handleEdit = async (user: User) => { | ||
| 740 | + dialogTitle.value = '编辑用户' | ||
| 741 | + | ||
| 742 | + // 清除之前的错误信息 | ||
| 743 | + clearFieldErrors() | ||
| 744 | + | ||
| 745 | + // 确保角色列表已加载 | ||
| 746 | + if (roles.value.length === 0) { | ||
| 747 | + console.log('角色列表为空,重新加载角色...') | ||
| 748 | + await fetchRoles() | ||
| 749 | + } | ||
| 750 | + | ||
| 751 | + Object.assign(userForm, { | ||
| 752 | + userId: user.userId, | ||
| 753 | + username: user.username, | ||
| 754 | + realName: user.realName, | ||
| 755 | + phone: user.phone, | ||
| 756 | + email: user.email, | ||
| 757 | + status: user.status, | ||
| 758 | + remark: user.remark || '', | ||
| 759 | + roleIds: user.roles?.map(role => role.roleId) || [], // 编辑时加载用户当前的所有角色 | ||
| 760 | + password: '' // 编辑时不设置密码 | ||
| 761 | + }) | ||
| 762 | + showUserDialog.value = true | ||
| 763 | +} | ||
| 764 | + | ||
| 765 | +// 删除用户 | ||
| 766 | +const handleDelete = async (user: User) => { | ||
| 767 | + if (confirm(`确定要删除用户 ${user.realName} 吗?`)) { | ||
| 768 | + try { | ||
| 769 | + const response = await userApi.deleteUser(user.userId) | ||
| 770 | + if (response.code === 200) { | ||
| 771 | + showMessage('删除成功!') | ||
| 772 | + fetchUsers() | ||
| 773 | + } else { | ||
| 774 | + showMessage(response.message || '删除失败', 'error') | ||
| 775 | + } | ||
| 776 | + } catch (error: any) { | ||
| 777 | + console.error('删除用户失败:', error) | ||
| 778 | + showMessage(error.response?.data?.message || '删除失败', 'error') | ||
| 779 | + } | ||
| 780 | + } | ||
| 781 | +} | ||
| 782 | + | ||
| 783 | +// 批量删除 | ||
| 784 | +const handleBatchDelete = async () => { | ||
| 785 | + if (selectedUsers.value.length === 0) { | ||
| 786 | + showMessage('请选择要删除的用户', 'warning') | ||
| 787 | + return | ||
| 788 | + } | ||
| 789 | + | ||
| 790 | + if (confirm(`确定要删除选中的 ${selectedUsers.value.length} 个用户吗?`)) { | ||
| 791 | + try { | ||
| 792 | + const response = await userApi.batchDeleteUsers(selectedUsers.value) | ||
| 793 | + if (response.code === 200) { | ||
| 794 | + showMessage('批量删除成功!') | ||
| 795 | + selectedUsers.value = [] | ||
| 796 | + selectAll.value = false | ||
| 797 | + fetchUsers() | ||
| 798 | + } else { | ||
| 799 | + showMessage(response.message || '批量删除失败', 'error') | ||
| 800 | + } | ||
| 801 | + } catch (error: any) { | ||
| 802 | + console.error('批量删除用户失败:', error) | ||
| 803 | + showMessage(error.response?.data?.message || '批量删除失败', 'error') | ||
| 804 | + } | ||
| 805 | + } | ||
| 806 | +} | ||
| 807 | + | ||
| 808 | +// 状态切换 | ||
| 809 | +const toggleStatus = async (user: User) => { | ||
| 810 | + const newStatus = user.status === 1 ? 0 : 1 | ||
| 811 | + try { | ||
| 812 | + const response = await userApi.updateUserStatus(user.userId, newStatus) | ||
| 813 | + if (response.code === 200) { | ||
| 814 | + user.status = newStatus | ||
| 815 | + user.statusText = newStatus === 1 ? '正常' : '停用' | ||
| 816 | + showMessage(`用户状态已切换为: ${user.statusText}`) | ||
| 817 | + } else { | ||
| 818 | + showMessage(response.message || '状态切换失败', 'error') | ||
| 819 | + } | ||
| 820 | + } catch (error: any) { | ||
| 821 | + console.error('状态切换失败:', error) | ||
| 822 | + showMessage(error.response?.data?.message || '状态切换失败', 'error') | ||
| 823 | + } | ||
| 824 | +} | ||
| 825 | + | ||
| 826 | +// 全选/取消全选 | ||
| 827 | +const handleSelectAll = () => { | ||
| 828 | + if (selectAll.value) { | ||
| 829 | + selectedUsers.value = users.value.map(user => user.userId) | ||
| 830 | + } else { | ||
| 831 | + selectedUsers.value = [] | ||
| 832 | + } | ||
| 833 | +} | ||
| 834 | + | ||
| 835 | +// 选择单个用户 | ||
| 836 | +const handleSelectUser = (userId: number) => { | ||
| 837 | + const index = selectedUsers.value.indexOf(userId) | ||
| 838 | + if (index > -1) { | ||
| 839 | + selectedUsers.value.splice(index, 1) | ||
| 840 | + } else { | ||
| 841 | + selectedUsers.value.push(userId) | ||
| 842 | + } | ||
| 843 | + | ||
| 844 | + // 更新全选状态 | ||
| 845 | + selectAll.value = selectedUsers.value.length === users.value.length | ||
| 846 | +} | ||
| 847 | + | ||
| 848 | +// 解析字段错误信息 | ||
| 849 | +const parseFieldErrors = (errorData: any) => { | ||
| 850 | + clearFieldErrors() | ||
| 851 | + | ||
| 852 | + if (errorData && typeof errorData === 'object') { | ||
| 853 | + // 遍历错误数据,设置对应的字段错误 | ||
| 854 | + Object.keys(errorData).forEach(field => { | ||
| 855 | + fieldErrors[field] = errorData[field] | ||
| 856 | + }) | ||
| 857 | + } | ||
| 858 | +} | ||
| 859 | + | ||
| 860 | +// 保存用户 | ||
| 861 | +const handleSave = async () => { | ||
| 862 | + // 清除之前的错误信息 | ||
| 863 | + clearFieldErrors() | ||
| 864 | + | ||
| 865 | + if (!userForm.username || !userForm.realName || !userForm.phone) { | ||
| 866 | + showMessage('请填写必填字段(用户名、真实姓名、手机号)', 'warning') | ||
| 867 | + return | ||
| 868 | + } | ||
| 869 | + | ||
| 870 | + try { | ||
| 871 | + formLoading.value = true | ||
| 872 | + | ||
| 873 | + if (dialogTitle.value === '新增用户') { | ||
| 874 | + // 新增用户 - 确保包含默认密码 | ||
| 875 | + const createData = { | ||
| 876 | + ...userForm, | ||
| 877 | + password: userForm.password || '123456' // 确保有默认密码 | ||
| 878 | + } | ||
| 879 | + const response = await userApi.createUser(createData) | ||
| 880 | + if (response.code === 200) { | ||
| 881 | + showMessage('新增用户成功!默认密码为123456') | ||
| 882 | + showUserDialog.value = false | ||
| 883 | + fetchUsers() | ||
| 884 | + } else { | ||
| 885 | + // 检查是否有字段级别的错误信息 | ||
| 886 | + if (response.code === 400 && response.data) { | ||
| 887 | + parseFieldErrors(response.data) | ||
| 888 | + showMessage('请检查表单中的错误信息', 'error') | ||
| 889 | + } else { | ||
| 890 | + showMessage(response.message || '新增用户失败', 'error') | ||
| 891 | + } | ||
| 892 | + } | ||
| 893 | + } else { | ||
| 894 | + // 编辑用户 - 不传递密码字段 | ||
| 895 | + const updateData = { ...userForm } | ||
| 896 | + delete updateData.password // 编辑时不传递密码 | ||
| 897 | + const response = await userApi.updateUser(userForm.userId!, updateData) | ||
| 898 | + if (response.code === 200) { | ||
| 899 | + showMessage('编辑用户成功!') | ||
| 900 | + showUserDialog.value = false | ||
| 901 | + fetchUsers() | ||
| 902 | + } else { | ||
| 903 | + // 检查是否有字段级别的错误信息 | ||
| 904 | + if (response.code === 400 && response.data) { | ||
| 905 | + parseFieldErrors(response.data) | ||
| 906 | + showMessage('请检查表单中的错误信息', 'error') | ||
| 907 | + } else { | ||
| 908 | + showMessage(response.message || '编辑用户失败', 'error') | ||
| 909 | + } | ||
| 910 | + } | ||
| 911 | + } | ||
| 912 | + } catch (error: any) { | ||
| 913 | + console.error('保存用户失败:', error) | ||
| 914 | + | ||
| 915 | + // 处理网络错误或服务器错误 | ||
| 916 | + if (error.response?.data) { | ||
| 917 | + const errorData = error.response.data | ||
| 918 | + if (errorData.code === 400 && errorData.data) { | ||
| 919 | + parseFieldErrors(errorData.data) | ||
| 920 | + showMessage('请检查表单中的错误信息', 'error') | ||
| 921 | + } else { | ||
| 922 | + showMessage(errorData.message || '保存用户失败', 'error') | ||
| 923 | + } | ||
| 924 | + } else { | ||
| 925 | + showMessage('保存用户失败', 'error') | ||
| 926 | + } | ||
| 927 | + } finally { | ||
| 928 | + formLoading.value = false | ||
| 929 | + } | ||
| 930 | +} | ||
| 931 | + | ||
| 932 | + | ||
| 933 | + | ||
| 934 | +// 页面变化 | ||
| 935 | +const handlePageChange = (page: number) => { | ||
| 936 | + currentPage.value = page | ||
| 937 | + fetchUsers() | ||
| 938 | +} | ||
| 939 | + | ||
| 940 | +// 每页条数变化 | ||
| 941 | +const handlePageSizeChange = () => { | ||
| 942 | + currentPage.value = 1 // 重置到第一页 | ||
| 943 | + fetchUsers() | ||
| 944 | +} | ||
| 945 | + | ||
| 946 | +// 导航栏功能 | ||
| 947 | +const sidebarCollapsed = ref(false) | ||
| 948 | + | ||
| 949 | +// 侧边栏切换 | ||
| 950 | +const toggleSidebar = () => { | ||
| 951 | + sidebarCollapsed.value = !sidebarCollapsed.value | ||
| 952 | + // 通知父组件或使用事件总线 | ||
| 953 | + console.log('侧边栏状态:', sidebarCollapsed.value ? '收起' : '展开') | ||
| 954 | +} | ||
| 955 | + | ||
| 956 | +// 导航标签页 | ||
| 957 | +const navTabs = ref([ | ||
| 958 | + { id: 1, name: '首页', path: '/main/dashboard', active: true, closable: false }, | ||
| 959 | + { id: 2, name: '用户管理', path: '/main/users', active: true, closable: true }, | ||
| 960 | + { id: 3, name: '个人中心', path: '/main/profile', active: false, closable: true }, | ||
| 961 | + { id: 4, name: '角色管理', path: '/main/roles', active: false, closable: true }, | ||
| 962 | + { id: 5, name: '菜单管理', path: '/main/menus', active: false, closable: true }, | ||
| 963 | + { id: 6, name: '部门管理', path: '/main/departments', active: false, closable: true } | ||
| 964 | +]) | ||
| 965 | + | ||
| 966 | +// 切换标签页 | ||
| 967 | +const switchTab = (tab: any) => { | ||
| 968 | + // 取消所有标签页的激活状态 | ||
| 969 | + navTabs.value.forEach(t => t.active = false) | ||
| 970 | + // 激活当前标签页 | ||
| 971 | + tab.active = true | ||
| 972 | + | ||
| 973 | + // 如果标签页有路径,进行路由跳转 | ||
| 974 | + if (tab.path) { | ||
| 975 | + router.push(tab.path) | ||
| 976 | + } | ||
| 977 | +} | ||
| 978 | + | ||
| 979 | +// 关闭标签页 | ||
| 980 | +const closeTab = (tab: any) => { | ||
| 981 | + if (!tab.closable) return | ||
| 982 | + | ||
| 983 | + // 如果关闭的是当前激活的标签页,需要激活其他标签页 | ||
| 984 | + if (tab.active) { | ||
| 985 | + const currentIndex = navTabs.value.findIndex(t => t.id === tab.id) | ||
| 986 | + const nextTab = navTabs.value[currentIndex + 1] || navTabs.value[currentIndex - 1] | ||
| 987 | + if (nextTab) { | ||
| 988 | + nextTab.active = true | ||
| 989 | + if (nextTab.path) { | ||
| 990 | + router.push(nextTab.path) | ||
| 991 | + } | ||
| 992 | + } | ||
| 993 | + } | ||
| 994 | + | ||
| 995 | + // 移除标签页 | ||
| 996 | + const index = navTabs.value.findIndex(t => t.id === tab.id) | ||
| 997 | + if (index > -1) { | ||
| 998 | + navTabs.value.splice(index, 1) | ||
| 999 | + } | ||
| 1000 | +} | ||
| 1001 | + | ||
| 1002 | +// 刷新功能 | ||
| 1003 | +const handleRefresh = () => { | ||
| 1004 | + // 刷新当前页面数据 | ||
| 1005 | + fetchUsers() | ||
| 1006 | + console.log('页面已刷新') | ||
| 1007 | +} | ||
| 1008 | + | ||
| 1009 | +// 表格控制按钮功能 | ||
| 1010 | +const handleTableSearch = () => { | ||
| 1011 | + // 聚焦到搜索区域 | ||
| 1012 | + const searchSection = document.querySelector('.search-section') | ||
| 1013 | + if (searchSection) { | ||
| 1014 | + searchSection.scrollIntoView({ behavior: 'smooth' }) | ||
| 1015 | + } | ||
| 1016 | +} | ||
| 1017 | + | ||
| 1018 | +const handleTableRefresh = () => { | ||
| 1019 | + // 刷新用户列表 | ||
| 1020 | + fetchUsers() | ||
| 1021 | +} | ||
| 1022 | + | ||
| 1023 | +const handleTableExport = () => { | ||
| 1024 | + // 导出用户数据 | ||
| 1025 | + console.log('导出用户数据') | ||
| 1026 | + alert('导出功能开发中...') | ||
| 1027 | +} | ||
| 1028 | + | ||
| 1029 | +const handleTableViewToggle = () => { | ||
| 1030 | + // 切换视图模式 | ||
| 1031 | + console.log('切换视图模式') | ||
| 1032 | + alert('视图切换功能开发中...') | ||
| 1033 | +} | ||
| 1034 | + | ||
| 1035 | +onMounted(() => { | ||
| 1036 | + console.log('用户管理页面已加载') | ||
| 1037 | + fetchRoles() // 先获取角色列表 | ||
| 1038 | + fetchUsers() | ||
| 1039 | +}) | ||
| 1040 | +</script> | ||
| 1041 | + | ||
| 1042 | +<style scoped> | ||
| 1043 | +.users-container { | ||
| 1044 | + background: #f5f5f5; | ||
| 1045 | + min-height: 100vh; | ||
| 1046 | + padding: 0; | ||
| 1047 | +} | ||
| 1048 | + | ||
| 1049 | +/* 顶部导航栏 */ | ||
| 1050 | +.top-nav { | ||
| 1051 | + background: white; | ||
| 1052 | + border-bottom: 1px solid #e0e0e0; | ||
| 1053 | + padding: 4px 12px; | ||
| 1054 | + display: flex; | ||
| 1055 | + justify-content: space-between; | ||
| 1056 | + align-items: center; | ||
| 1057 | + box-shadow: 0 1px 2px rgba(0,0,0,0.1); | ||
| 1058 | + min-height: 36px; | ||
| 1059 | +} | ||
| 1060 | + | ||
| 1061 | +.nav-left { | ||
| 1062 | + display: flex; | ||
| 1063 | + align-items: center; | ||
| 1064 | + gap: 8px; | ||
| 1065 | +} | ||
| 1066 | + | ||
| 1067 | +.nav-toggle { | ||
| 1068 | + background: none; | ||
| 1069 | + border: none; | ||
| 1070 | + font-size: 14px; | ||
| 1071 | + cursor: pointer; | ||
| 1072 | + padding: 2px 6px; | ||
| 1073 | + border-radius: 3px; | ||
| 1074 | + width: 24px; | ||
| 1075 | + height: 24px; | ||
| 1076 | + display: flex; | ||
| 1077 | + align-items: center; | ||
| 1078 | + justify-content: center; | ||
| 1079 | +} | ||
| 1080 | + | ||
| 1081 | +.nav-toggle:hover { | ||
| 1082 | + background: #f0f0f0; | ||
| 1083 | +} | ||
| 1084 | + | ||
| 1085 | +.nav-tabs { | ||
| 1086 | + display: flex; | ||
| 1087 | + gap: 2px; | ||
| 1088 | +} | ||
| 1089 | + | ||
| 1090 | +.nav-tab { | ||
| 1091 | + padding: 4px 8px; | ||
| 1092 | + background: #f8f9fa; | ||
| 1093 | + border: 1px solid #e0e0e0; | ||
| 1094 | + border-radius: 3px; | ||
| 1095 | + font-size: 11px; | ||
| 1096 | + color: #666; | ||
| 1097 | + cursor: pointer; | ||
| 1098 | + display: flex; | ||
| 1099 | + align-items: center; | ||
| 1100 | + gap: 4px; | ||
| 1101 | + height: 24px; | ||
| 1102 | +} | ||
| 1103 | + | ||
| 1104 | +.nav-tab.active { | ||
| 1105 | + background: #1890ff; | ||
| 1106 | + color: white; | ||
| 1107 | + border-color: #1890ff; | ||
| 1108 | +} | ||
| 1109 | + | ||
| 1110 | +.close-icon { | ||
| 1111 | + font-size: 12px; | ||
| 1112 | + cursor: pointer; | ||
| 1113 | + opacity: 0.7; | ||
| 1114 | + margin-left: 4px; | ||
| 1115 | + padding: 1px 3px; | ||
| 1116 | + border-radius: 2px; | ||
| 1117 | + transition: all 0.2s; | ||
| 1118 | +} | ||
| 1119 | + | ||
| 1120 | +.close-icon:hover { | ||
| 1121 | + opacity: 1; | ||
| 1122 | + color: #ff4d4f; | ||
| 1123 | + background: #fff2f0; | ||
| 1124 | +} | ||
| 1125 | + | ||
| 1126 | +.refresh-btn { | ||
| 1127 | + background: #1890ff; | ||
| 1128 | + color: white; | ||
| 1129 | + border: none; | ||
| 1130 | + padding: 4px 8px; | ||
| 1131 | + border-radius: 3px; | ||
| 1132 | + font-size: 11px; | ||
| 1133 | + cursor: pointer; | ||
| 1134 | + height: 24px; | ||
| 1135 | + display: flex; | ||
| 1136 | + align-items: center; | ||
| 1137 | + gap: 4px; | ||
| 1138 | +} | ||
| 1139 | + | ||
| 1140 | +/* 搜索筛选区域 */ | ||
| 1141 | +.search-section { | ||
| 1142 | + background: white; | ||
| 1143 | + padding: 16px; | ||
| 1144 | + border-bottom: 1px solid #e0e0e0; | ||
| 1145 | +} | ||
| 1146 | + | ||
| 1147 | +.search-row { | ||
| 1148 | + display: flex; | ||
| 1149 | + gap: 16px; | ||
| 1150 | + align-items: center; | ||
| 1151 | + flex-wrap: wrap; | ||
| 1152 | +} | ||
| 1153 | + | ||
| 1154 | +.search-item { | ||
| 1155 | + display: flex; | ||
| 1156 | + align-items: center; | ||
| 1157 | + gap: 8px; | ||
| 1158 | +} | ||
| 1159 | + | ||
| 1160 | +.search-item label { | ||
| 1161 | + font-size: 12px; | ||
| 1162 | + color: #333; | ||
| 1163 | + white-space: nowrap; | ||
| 1164 | +} | ||
| 1165 | + | ||
| 1166 | +.search-input, .search-select, .date-input { | ||
| 1167 | + padding: 6px 8px; | ||
| 1168 | + border: 1px solid #d9d9d9; | ||
| 1169 | + border-radius: 4px; | ||
| 1170 | + font-size: 12px; | ||
| 1171 | + width: 120px; | ||
| 1172 | +} | ||
| 1173 | + | ||
| 1174 | +.search-input:focus, .search-select:focus, .date-input:focus { | ||
| 1175 | + outline: none; | ||
| 1176 | + border-color: #1890ff; | ||
| 1177 | + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); | ||
| 1178 | +} | ||
| 1179 | + | ||
| 1180 | +.date-range { | ||
| 1181 | + display: flex; | ||
| 1182 | + align-items: center; | ||
| 1183 | + gap: 8px; | ||
| 1184 | +} | ||
| 1185 | + | ||
| 1186 | +.date-separator { | ||
| 1187 | + color: #666; | ||
| 1188 | + font-size: 12px; | ||
| 1189 | +} | ||
| 1190 | + | ||
| 1191 | +/* 时间选择器容器 */ | ||
| 1192 | +.date-picker-container { | ||
| 1193 | + position: relative; | ||
| 1194 | + display: inline-block; | ||
| 1195 | +} | ||
| 1196 | + | ||
| 1197 | +/* 时间选项下拉菜单 */ | ||
| 1198 | +.date-options { | ||
| 1199 | + position: absolute; | ||
| 1200 | + top: 100%; | ||
| 1201 | + left: 0; | ||
| 1202 | + right: 0; | ||
| 1203 | + background: white; | ||
| 1204 | + border: 1px solid #d9d9d9; | ||
| 1205 | + border-radius: 4px; | ||
| 1206 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | ||
| 1207 | + z-index: 1000; | ||
| 1208 | + min-width: 120px; | ||
| 1209 | +} | ||
| 1210 | + | ||
| 1211 | +.date-option { | ||
| 1212 | + padding: 8px 12px; | ||
| 1213 | + cursor: pointer; | ||
| 1214 | + font-size: 12px; | ||
| 1215 | + color: #333; | ||
| 1216 | + transition: background-color 0.2s; | ||
| 1217 | + border-bottom: 1px solid #f0f0f0; | ||
| 1218 | +} | ||
| 1219 | + | ||
| 1220 | +.date-option:last-child { | ||
| 1221 | + border-bottom: none; | ||
| 1222 | +} | ||
| 1223 | + | ||
| 1224 | +.date-option:hover { | ||
| 1225 | + background-color: #f5f5f5; | ||
| 1226 | + color: #1890ff; | ||
| 1227 | +} | ||
| 1228 | + | ||
| 1229 | +.date-option:active { | ||
| 1230 | + background-color: #e6f7ff; | ||
| 1231 | +} | ||
| 1232 | + | ||
| 1233 | +/* 自定义日期选择器 */ | ||
| 1234 | +.custom-date-picker { | ||
| 1235 | + position: absolute; | ||
| 1236 | + top: 100%; | ||
| 1237 | + left: 0; | ||
| 1238 | + right: 0; | ||
| 1239 | + background: white; | ||
| 1240 | + border: 1px solid #d9d9d9; | ||
| 1241 | + border-radius: 4px; | ||
| 1242 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | ||
| 1243 | + z-index: 1000; | ||
| 1244 | + padding: 8px; | ||
| 1245 | +} | ||
| 1246 | + | ||
| 1247 | +.custom-date-picker input { | ||
| 1248 | + width: 100%; | ||
| 1249 | + border: none; | ||
| 1250 | + outline: none; | ||
| 1251 | + font-size: 12px; | ||
| 1252 | +} | ||
| 1253 | + | ||
| 1254 | +.search-actions { | ||
| 1255 | + display: flex; | ||
| 1256 | + gap: 6px; | ||
| 1257 | +} | ||
| 1258 | + | ||
| 1259 | +.search-btn { | ||
| 1260 | + background: #1890ff; | ||
| 1261 | + color: white; | ||
| 1262 | + border: none; | ||
| 1263 | + padding: 4px 8px; | ||
| 1264 | + border-radius: 3px; | ||
| 1265 | + font-size: 11px; | ||
| 1266 | + cursor: pointer; | ||
| 1267 | + height: 24px; | ||
| 1268 | + display: flex; | ||
| 1269 | + align-items: center; | ||
| 1270 | + gap: 4px; | ||
| 1271 | +} | ||
| 1272 | + | ||
| 1273 | +.reset-btn { | ||
| 1274 | + background: #ff7875; | ||
| 1275 | + color: white; | ||
| 1276 | + border: none; | ||
| 1277 | + padding: 4px 8px; | ||
| 1278 | + border-radius: 3px; | ||
| 1279 | + font-size: 11px; | ||
| 1280 | + cursor: pointer; | ||
| 1281 | + height: 24px; | ||
| 1282 | + display: flex; | ||
| 1283 | + align-items: center; | ||
| 1284 | + gap: 4px; | ||
| 1285 | +} | ||
| 1286 | + | ||
| 1287 | +/* 操作按钮区域 */ | ||
| 1288 | +.action-section { | ||
| 1289 | + background: white; | ||
| 1290 | + padding: 12px 16px; | ||
| 1291 | + border-bottom: 1px solid #e0e0e0; | ||
| 1292 | +} | ||
| 1293 | + | ||
| 1294 | +.action-buttons { | ||
| 1295 | + display: flex; | ||
| 1296 | + gap: 6px; | ||
| 1297 | +} | ||
| 1298 | + | ||
| 1299 | +.action-btn { | ||
| 1300 | + padding: 4px 8px; | ||
| 1301 | + border: none; | ||
| 1302 | + border-radius: 3px; | ||
| 1303 | + font-size: 11px; | ||
| 1304 | + cursor: pointer; | ||
| 1305 | + display: flex; | ||
| 1306 | + align-items: center; | ||
| 1307 | + gap: 4px; | ||
| 1308 | + height: 24px; | ||
| 1309 | +} | ||
| 1310 | + | ||
| 1311 | +.action-btn.primary { | ||
| 1312 | + background: #1890ff; | ||
| 1313 | + color: white; | ||
| 1314 | +} | ||
| 1315 | + | ||
| 1316 | +.action-btn.success { | ||
| 1317 | + background: #52c41a; | ||
| 1318 | + color: white; | ||
| 1319 | +} | ||
| 1320 | + | ||
| 1321 | +.action-btn.danger { | ||
| 1322 | + background: #ff4d4f; | ||
| 1323 | + color: white; | ||
| 1324 | +} | ||
| 1325 | + | ||
| 1326 | +.action-btn.info { | ||
| 1327 | + background: #13c2c2; | ||
| 1328 | + color: white; | ||
| 1329 | +} | ||
| 1330 | + | ||
| 1331 | +.action-btn.warning { | ||
| 1332 | + background: #faad14; | ||
| 1333 | + color: white; | ||
| 1334 | +} | ||
| 1335 | + | ||
| 1336 | +/* 表格区域 */ | ||
| 1337 | +.table-section { | ||
| 1338 | + background: white; | ||
| 1339 | + margin: 16px; | ||
| 1340 | + border-radius: 6px; | ||
| 1341 | + overflow: hidden; | ||
| 1342 | + box-shadow: 0 2px 8px rgba(0,0,0,0.1); | ||
| 1343 | +} | ||
| 1344 | + | ||
| 1345 | +.table-header { | ||
| 1346 | + padding: 8px 16px; | ||
| 1347 | + background: #fafafa; | ||
| 1348 | + border-bottom: 1px solid #e0e0e0; | ||
| 1349 | + display: flex; | ||
| 1350 | + justify-content: flex-end; | ||
| 1351 | +} | ||
| 1352 | + | ||
| 1353 | +.table-controls { | ||
| 1354 | + display: flex; | ||
| 1355 | + gap: 4px; | ||
| 1356 | +} | ||
| 1357 | + | ||
| 1358 | +.control-btn { | ||
| 1359 | + background: white; | ||
| 1360 | + border: 1px solid #d9d9d9; | ||
| 1361 | + border-radius: 3px; | ||
| 1362 | + padding: 4px 8px; | ||
| 1363 | + font-size: 11px; | ||
| 1364 | + cursor: pointer; | ||
| 1365 | + transition: all 0.2s; | ||
| 1366 | +} | ||
| 1367 | + | ||
| 1368 | +.control-btn:hover { | ||
| 1369 | + background: #f0f8ff; | ||
| 1370 | + border-color: #1890ff; | ||
| 1371 | + color: #1890ff; | ||
| 1372 | + transform: translateY(-1px); | ||
| 1373 | + box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2); | ||
| 1374 | +} | ||
| 1375 | + | ||
| 1376 | +.control-btn:active { | ||
| 1377 | + transform: translateY(0); | ||
| 1378 | + box-shadow: 0 1px 2px rgba(24, 144, 255, 0.2); | ||
| 1379 | +} | ||
| 1380 | + | ||
| 1381 | +.table-container { | ||
| 1382 | + overflow-x: auto; | ||
| 1383 | +} | ||
| 1384 | + | ||
| 1385 | +.data-table { | ||
| 1386 | + width: 100%; | ||
| 1387 | + border-collapse: collapse; | ||
| 1388 | +} | ||
| 1389 | + | ||
| 1390 | +.data-table th, | ||
| 1391 | +.data-table td { | ||
| 1392 | + padding: 8px 12px; | ||
| 1393 | + text-align: left; | ||
| 1394 | + border-bottom: 1px solid #f0f0f0; | ||
| 1395 | + font-size: 12px; | ||
| 1396 | +} | ||
| 1397 | + | ||
| 1398 | +.data-table th { | ||
| 1399 | + background: #fafafa; | ||
| 1400 | + font-weight: 600; | ||
| 1401 | + color: #333; | ||
| 1402 | +} | ||
| 1403 | + | ||
| 1404 | +.data-table tbody tr:hover { | ||
| 1405 | + background: #f5f5f5; | ||
| 1406 | +} | ||
| 1407 | + | ||
| 1408 | +.select-all, .row-checkbox { | ||
| 1409 | + margin: 0; | ||
| 1410 | +} | ||
| 1411 | + | ||
| 1412 | +.sortable { | ||
| 1413 | + cursor: pointer; | ||
| 1414 | + position: relative; | ||
| 1415 | +} | ||
| 1416 | + | ||
| 1417 | +.sortable:hover { | ||
| 1418 | + background: #f0f0f0; | ||
| 1419 | +} | ||
| 1420 | + | ||
| 1421 | +.user-link { | ||
| 1422 | + color: #1890ff; | ||
| 1423 | + text-decoration: none; | ||
| 1424 | +} | ||
| 1425 | + | ||
| 1426 | +.user-link:hover { | ||
| 1427 | + text-decoration: underline; | ||
| 1428 | +} | ||
| 1429 | + | ||
| 1430 | +/* 状态开关 */ | ||
| 1431 | +.status-switch { | ||
| 1432 | + position: relative; | ||
| 1433 | + display: inline-block; | ||
| 1434 | + width: 40px; | ||
| 1435 | + height: 20px; | ||
| 1436 | +} | ||
| 1437 | + | ||
| 1438 | +.status-switch input { | ||
| 1439 | + opacity: 0; | ||
| 1440 | + width: 0; | ||
| 1441 | + height: 0; | ||
| 1442 | +} | ||
| 1443 | + | ||
| 1444 | +.switch-slider { | ||
| 1445 | + position: absolute; | ||
| 1446 | + cursor: pointer; | ||
| 1447 | + top: 0; | ||
| 1448 | + left: 0; | ||
| 1449 | + right: 0; | ||
| 1450 | + bottom: 0; | ||
| 1451 | + background-color: #ccc; | ||
| 1452 | + transition: .4s; | ||
| 1453 | + border-radius: 20px; | ||
| 1454 | +} | ||
| 1455 | + | ||
| 1456 | +.switch-slider:before { | ||
| 1457 | + position: absolute; | ||
| 1458 | + content: ""; | ||
| 1459 | + height: 16px; | ||
| 1460 | + width: 16px; | ||
| 1461 | + left: 2px; | ||
| 1462 | + bottom: 2px; | ||
| 1463 | + background-color: white; | ||
| 1464 | + transition: .4s; | ||
| 1465 | + border-radius: 50%; | ||
| 1466 | +} | ||
| 1467 | + | ||
| 1468 | +input:checked + .switch-slider { | ||
| 1469 | + background-color: #13c2c2; | ||
| 1470 | +} | ||
| 1471 | + | ||
| 1472 | +input:checked + .switch-slider:before { | ||
| 1473 | + transform: translateX(20px); | ||
| 1474 | +} | ||
| 1475 | + | ||
| 1476 | +/* 表格操作按钮 */ | ||
| 1477 | +.table-btn { | ||
| 1478 | + padding: 2px 6px; | ||
| 1479 | + margin-right: 4px; | ||
| 1480 | + border: none; | ||
| 1481 | + border-radius: 3px; | ||
| 1482 | + font-size: 10px; | ||
| 1483 | + cursor: pointer; | ||
| 1484 | + display: inline-flex; | ||
| 1485 | + align-items: center; | ||
| 1486 | + gap: 2px; | ||
| 1487 | +} | ||
| 1488 | + | ||
| 1489 | +.table-btn.edit { | ||
| 1490 | + background: #1890ff; | ||
| 1491 | + color: white; | ||
| 1492 | +} | ||
| 1493 | + | ||
| 1494 | +.table-btn.delete { | ||
| 1495 | + background: #ff4d4f; | ||
| 1496 | + color: white; | ||
| 1497 | +} | ||
| 1498 | + | ||
| 1499 | + | ||
| 1500 | +/* 表格底部样式已移至分页控制样式 */ | ||
| 1501 | + | ||
| 1502 | +.pagination-info { | ||
| 1503 | + font-size: 12px; | ||
| 1504 | + color: #666; | ||
| 1505 | +} | ||
| 1506 | + | ||
| 1507 | +/* 对话框样式 */ | ||
| 1508 | +.dialog-overlay { | ||
| 1509 | + position: fixed; | ||
| 1510 | + top: 0; | ||
| 1511 | + left: 0; | ||
| 1512 | + width: 100%; | ||
| 1513 | + height: 100%; | ||
| 1514 | + background: rgba(0, 0, 0, 0.5); | ||
| 1515 | + display: flex; | ||
| 1516 | + align-items: center; | ||
| 1517 | + justify-content: center; | ||
| 1518 | + z-index: 1000; | ||
| 1519 | +} | ||
| 1520 | + | ||
| 1521 | +.dialog-content { | ||
| 1522 | + background: white; | ||
| 1523 | + border-radius: 8px; | ||
| 1524 | + width: 600px; | ||
| 1525 | + max-height: 80vh; | ||
| 1526 | + overflow-y: auto; | ||
| 1527 | + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); | ||
| 1528 | +} | ||
| 1529 | + | ||
| 1530 | +.dialog-header { | ||
| 1531 | + display: flex; | ||
| 1532 | + justify-content: space-between; | ||
| 1533 | + align-items: center; | ||
| 1534 | + padding: 16px 20px; | ||
| 1535 | + border-bottom: 1px solid #e0e0e0; | ||
| 1536 | +} | ||
| 1537 | + | ||
| 1538 | +.dialog-header h3 { | ||
| 1539 | + margin: 0; | ||
| 1540 | + font-size: 16px; | ||
| 1541 | + color: #333; | ||
| 1542 | +} | ||
| 1543 | + | ||
| 1544 | +.close-btn { | ||
| 1545 | + background: none; | ||
| 1546 | + border: none; | ||
| 1547 | + font-size: 20px; | ||
| 1548 | + cursor: pointer; | ||
| 1549 | + color: #666; | ||
| 1550 | + padding: 4px; | ||
| 1551 | + border-radius: 4px; | ||
| 1552 | +} | ||
| 1553 | + | ||
| 1554 | +.close-btn:hover { | ||
| 1555 | + background: #f0f0f0; | ||
| 1556 | +} | ||
| 1557 | + | ||
| 1558 | +.dialog-body { | ||
| 1559 | + padding: 20px; | ||
| 1560 | +} | ||
| 1561 | + | ||
| 1562 | +.form-row { | ||
| 1563 | + display: flex; | ||
| 1564 | + gap: 16px; | ||
| 1565 | + margin-bottom: 16px; | ||
| 1566 | +} | ||
| 1567 | + | ||
| 1568 | +.form-group { | ||
| 1569 | + flex: 1; | ||
| 1570 | +} | ||
| 1571 | + | ||
| 1572 | +.form-group label { | ||
| 1573 | + display: block; | ||
| 1574 | + margin-bottom: 6px; | ||
| 1575 | + font-size: 12px; | ||
| 1576 | + color: #333; | ||
| 1577 | + font-weight: 500; | ||
| 1578 | +} | ||
| 1579 | + | ||
| 1580 | +.required-label { | ||
| 1581 | + color: #333; | ||
| 1582 | + font-weight: 500; | ||
| 1583 | +} | ||
| 1584 | + | ||
| 1585 | +.required-asterisk { | ||
| 1586 | + color: #ff4d4f; | ||
| 1587 | + font-weight: bold; | ||
| 1588 | + margin-left: 2px; | ||
| 1589 | +} | ||
| 1590 | + | ||
| 1591 | +.form-hint { | ||
| 1592 | + display: block; | ||
| 1593 | + margin-top: 4px; | ||
| 1594 | + font-size: 11px; | ||
| 1595 | + color: #999; | ||
| 1596 | + font-style: italic; | ||
| 1597 | +} | ||
| 1598 | + | ||
| 1599 | +.form-input, .form-select { | ||
| 1600 | + width: 100%; | ||
| 1601 | + padding: 8px 12px; | ||
| 1602 | + border: 1px solid #d9d9d9; | ||
| 1603 | + border-radius: 4px; | ||
| 1604 | + font-size: 12px; | ||
| 1605 | + box-sizing: border-box; | ||
| 1606 | +} | ||
| 1607 | + | ||
| 1608 | +.form-input:focus, .form-select:focus { | ||
| 1609 | + outline: none; | ||
| 1610 | + border-color: #1890ff; | ||
| 1611 | + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); | ||
| 1612 | +} | ||
| 1613 | + | ||
| 1614 | +/* 错误状态样式 */ | ||
| 1615 | +.form-input.error, .form-select.error { | ||
| 1616 | + border-color: #ff4d4f; | ||
| 1617 | + box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2); | ||
| 1618 | +} | ||
| 1619 | + | ||
| 1620 | +.field-error { | ||
| 1621 | + color: #ff4d4f; | ||
| 1622 | + font-size: 11px; | ||
| 1623 | + margin-top: 4px; | ||
| 1624 | + display: block; | ||
| 1625 | + line-height: 1.4; | ||
| 1626 | +} | ||
| 1627 | + | ||
| 1628 | +/* 多角色选择样式 */ | ||
| 1629 | +.role-select { | ||
| 1630 | + min-height: 80px; | ||
| 1631 | + max-height: 120px; | ||
| 1632 | +} | ||
| 1633 | + | ||
| 1634 | +.role-select option { | ||
| 1635 | + padding: 4px 8px; | ||
| 1636 | +} | ||
| 1637 | + | ||
| 1638 | +.role-select option:checked { | ||
| 1639 | + background-color: #e6f7ff; | ||
| 1640 | + color: #1890ff; | ||
| 1641 | + font-weight: 500; | ||
| 1642 | +} | ||
| 1643 | + | ||
| 1644 | +/* 已选择角色显示样式 */ | ||
| 1645 | +.selected-roles { | ||
| 1646 | + margin-top: 8px; | ||
| 1647 | + padding: 8px; | ||
| 1648 | + background: #f8f9fa; | ||
| 1649 | + border-radius: 4px; | ||
| 1650 | + border: 1px solid #e9ecef; | ||
| 1651 | +} | ||
| 1652 | + | ||
| 1653 | +.selected-roles-label { | ||
| 1654 | + font-size: 11px; | ||
| 1655 | + color: #666; | ||
| 1656 | + margin-bottom: 6px; | ||
| 1657 | + font-weight: 500; | ||
| 1658 | +} | ||
| 1659 | + | ||
| 1660 | +.role-tags { | ||
| 1661 | + display: flex; | ||
| 1662 | + flex-wrap: wrap; | ||
| 1663 | + gap: 4px; | ||
| 1664 | +} | ||
| 1665 | + | ||
| 1666 | +.role-tag { | ||
| 1667 | + display: inline-block; | ||
| 1668 | + padding: 2px 8px; | ||
| 1669 | + background: #1890ff; | ||
| 1670 | + color: white; | ||
| 1671 | + border-radius: 12px; | ||
| 1672 | + font-size: 11px; | ||
| 1673 | + font-weight: 500; | ||
| 1674 | +} | ||
| 1675 | + | ||
| 1676 | +/* 刷新角色按钮样式 */ | ||
| 1677 | +.refresh-roles-btn { | ||
| 1678 | + margin-left: 8px; | ||
| 1679 | + padding: 2px 6px; | ||
| 1680 | + background: #52c41a; | ||
| 1681 | + color: white; | ||
| 1682 | + border: none; | ||
| 1683 | + border-radius: 3px; | ||
| 1684 | + font-size: 10px; | ||
| 1685 | + cursor: pointer; | ||
| 1686 | + transition: all 0.3s; | ||
| 1687 | +} | ||
| 1688 | + | ||
| 1689 | +.refresh-roles-btn:hover { | ||
| 1690 | + background: #73d13d; | ||
| 1691 | +} | ||
| 1692 | + | ||
| 1693 | +.dialog-footer { | ||
| 1694 | + display: flex; | ||
| 1695 | + justify-content: flex-end; | ||
| 1696 | + gap: 12px; | ||
| 1697 | + padding: 16px 20px; | ||
| 1698 | + border-top: 1px solid #e0e0e0; | ||
| 1699 | +} | ||
| 1700 | + | ||
| 1701 | +.btn-cancel, .btn-save { | ||
| 1702 | + padding: 8px 16px; | ||
| 1703 | + border: none; | ||
| 1704 | + border-radius: 4px; | ||
| 1705 | + font-size: 12px; | ||
| 1706 | + cursor: pointer; | ||
| 1707 | + transition: all 0.3s; | ||
| 1708 | +} | ||
| 1709 | + | ||
| 1710 | +.btn-cancel { | ||
| 1711 | + background: #f5f5f5; | ||
| 1712 | + color: #666; | ||
| 1713 | + border: 1px solid #d9d9d9; | ||
| 1714 | +} | ||
| 1715 | + | ||
| 1716 | +.btn-cancel:hover { | ||
| 1717 | + background: #e6f7ff; | ||
| 1718 | + border-color: #91d5ff; | ||
| 1719 | +} | ||
| 1720 | + | ||
| 1721 | +.btn-save { | ||
| 1722 | + background: #1890ff; | ||
| 1723 | + color: white; | ||
| 1724 | +} | ||
| 1725 | + | ||
| 1726 | +.btn-save:hover:not(:disabled) { | ||
| 1727 | + background: #40a9ff; | ||
| 1728 | +} | ||
| 1729 | + | ||
| 1730 | +.btn-save:disabled { | ||
| 1731 | + background: #ccc; | ||
| 1732 | + cursor: not-allowed; | ||
| 1733 | +} | ||
| 1734 | + | ||
| 1735 | +/* 分页控制样式 */ | ||
| 1736 | +.table-footer { | ||
| 1737 | + display: flex; | ||
| 1738 | + justify-content: space-between; | ||
| 1739 | + align-items: center; | ||
| 1740 | + padding: 8px 16px; | ||
| 1741 | + background: #fafafa; | ||
| 1742 | + border-top: 1px solid #e0e0e0; | ||
| 1743 | +} | ||
| 1744 | + | ||
| 1745 | +.pagination-left { | ||
| 1746 | + display: flex; | ||
| 1747 | + align-items: center; | ||
| 1748 | + gap: 20px; | ||
| 1749 | +} | ||
| 1750 | + | ||
| 1751 | +.page-size-selector { | ||
| 1752 | + display: flex; | ||
| 1753 | + align-items: center; | ||
| 1754 | + gap: 8px; | ||
| 1755 | +} | ||
| 1756 | + | ||
| 1757 | +.page-size-selector label { | ||
| 1758 | + font-size: 12px; | ||
| 1759 | + color: #666; | ||
| 1760 | +} | ||
| 1761 | + | ||
| 1762 | +.page-size-select { | ||
| 1763 | + padding: 4px 8px; | ||
| 1764 | + border: 1px solid #d9d9d9; | ||
| 1765 | + border-radius: 4px; | ||
| 1766 | + font-size: 12px; | ||
| 1767 | + background: white; | ||
| 1768 | + cursor: pointer; | ||
| 1769 | + transition: border-color 0.3s; | ||
| 1770 | +} | ||
| 1771 | + | ||
| 1772 | +.page-size-select:hover { | ||
| 1773 | + border-color: #1890ff; | ||
| 1774 | +} | ||
| 1775 | + | ||
| 1776 | +.page-size-select:focus { | ||
| 1777 | + outline: none; | ||
| 1778 | + border-color: #1890ff; | ||
| 1779 | + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); | ||
| 1780 | +} | ||
| 1781 | + | ||
| 1782 | +/* 新的分页容器样式 - 模拟截图样式 */ | ||
| 1783 | +.pagination-container { | ||
| 1784 | + display: flex; | ||
| 1785 | + align-items: center; | ||
| 1786 | + background: #f8f9fa; | ||
| 1787 | + border: 1px solid #e9ecef; | ||
| 1788 | + border-radius: 8px; | ||
| 1789 | + overflow: hidden; | ||
| 1790 | + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||
| 1791 | +} | ||
| 1792 | + | ||
| 1793 | +.pagination-btn { | ||
| 1794 | + padding: 2px 6px; | ||
| 1795 | + border: none; | ||
| 1796 | + background: white; | ||
| 1797 | + color: #6c757d; | ||
| 1798 | + font-size: 11px; | ||
| 1799 | + font-weight: 500; | ||
| 1800 | + cursor: pointer; | ||
| 1801 | + transition: all 0.2s; | ||
| 1802 | + border-right: 1px solid #e9ecef; | ||
| 1803 | + height: 20px; | ||
| 1804 | +} | ||
| 1805 | + | ||
| 1806 | +.pagination-btn:hover:not(:disabled) { | ||
| 1807 | + background: #e9ecef; | ||
| 1808 | + color: #495057; | ||
| 1809 | +} | ||
| 1810 | + | ||
| 1811 | +.pagination-btn:disabled { | ||
| 1812 | + background: #f8f9fa; | ||
| 1813 | + color: #adb5bd; | ||
| 1814 | + cursor: not-allowed; | ||
| 1815 | +} | ||
| 1816 | + | ||
| 1817 | +.pagination-pages { | ||
| 1818 | + display: flex; | ||
| 1819 | + align-items: center; | ||
| 1820 | +} | ||
| 1821 | + | ||
| 1822 | +.page-number { | ||
| 1823 | + padding: 2px 6px; | ||
| 1824 | + border: none; | ||
| 1825 | + background: white; | ||
| 1826 | + color: #6c757d; | ||
| 1827 | + font-size: 11px; | ||
| 1828 | + font-weight: 500; | ||
| 1829 | + cursor: pointer; | ||
| 1830 | + transition: all 0.2s; | ||
| 1831 | + border-right: 1px solid #e9ecef; | ||
| 1832 | + min-width: 28px; | ||
| 1833 | + text-align: center; | ||
| 1834 | + height: 20px; | ||
| 1835 | +} | ||
| 1836 | + | ||
| 1837 | +.page-number:hover { | ||
| 1838 | + background: #e9ecef; | ||
| 1839 | + color: #495057; | ||
| 1840 | +} | ||
| 1841 | + | ||
| 1842 | +.page-number.active { | ||
| 1843 | + background: #6c757d; | ||
| 1844 | + color: white; | ||
| 1845 | + font-weight: 600; | ||
| 1846 | +} | ||
| 1847 | + | ||
| 1848 | +.page-number.active:hover { | ||
| 1849 | + background: #5a6268; | ||
| 1850 | +} | ||
| 1851 | + | ||
| 1852 | +/* 加载状态样式 */ | ||
| 1853 | +.loading-overlay { | ||
| 1854 | + position: absolute; | ||
| 1855 | + top: 0; | ||
| 1856 | + left: 0; | ||
| 1857 | + right: 0; | ||
| 1858 | + bottom: 0; | ||
| 1859 | + background: rgba(255, 255, 255, 0.8); | ||
| 1860 | + display: flex; | ||
| 1861 | + align-items: center; | ||
| 1862 | + justify-content: center; | ||
| 1863 | + z-index: 10; | ||
| 1864 | +} | ||
| 1865 | + | ||
| 1866 | +.loading-spinner { | ||
| 1867 | + padding: 20px; | ||
| 1868 | + background: white; | ||
| 1869 | + border-radius: 8px; | ||
| 1870 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||
| 1871 | + font-size: 14px; | ||
| 1872 | + color: #666; | ||
| 1873 | +} | ||
| 1874 | + | ||
| 1875 | +/* 响应式设计 */ | ||
| 1876 | +@media (max-width: 768px) { | ||
| 1877 | + .search-row { | ||
| 1878 | + flex-direction: column; | ||
| 1879 | + align-items: stretch; | ||
| 1880 | + } | ||
| 1881 | + | ||
| 1882 | + .search-item { | ||
| 1883 | + width: 100%; | ||
| 1884 | + } | ||
| 1885 | + | ||
| 1886 | + .search-input, .search-select, .date-input { | ||
| 1887 | + width: 100%; | ||
| 1888 | + } | ||
| 1889 | + | ||
| 1890 | + .action-buttons { | ||
| 1891 | + flex-wrap: wrap; | ||
| 1892 | + } | ||
| 1893 | + | ||
| 1894 | + .nav-tabs { | ||
| 1895 | + overflow-x: auto; | ||
| 1896 | + } | ||
| 1897 | + | ||
| 1898 | + .dialog-content { | ||
| 1899 | + width: 90%; | ||
| 1900 | + margin: 20px; | ||
| 1901 | + } | ||
| 1902 | + | ||
| 1903 | + .form-row { | ||
| 1904 | + flex-direction: column; | ||
| 1905 | + gap: 0; | ||
| 1906 | + } | ||
| 1907 | + | ||
| 1908 | + /* 分页组件响应式 */ | ||
| 1909 | + .table-footer { | ||
| 1910 | + flex-direction: column; | ||
| 1911 | + gap: 12px; | ||
| 1912 | + align-items: stretch; | ||
| 1913 | + } | ||
| 1914 | + | ||
| 1915 | + .pagination-left { | ||
| 1916 | + flex-direction: column; | ||
| 1917 | + gap: 8px; | ||
| 1918 | + align-items: flex-start; | ||
| 1919 | + } | ||
| 1920 | + | ||
| 1921 | + .pagination-container { | ||
| 1922 | + justify-content: center; | ||
| 1923 | + flex-wrap: wrap; | ||
| 1924 | + } | ||
| 1925 | + | ||
| 1926 | + .pagination-btn { | ||
| 1927 | + padding: 2px 4px; | ||
| 1928 | + font-size: 10px; | ||
| 1929 | + height: 20px; | ||
| 1930 | + } | ||
| 1931 | + | ||
| 1932 | + .page-number { | ||
| 1933 | + padding: 2px 4px; | ||
| 1934 | + font-size: 10px; | ||
| 1935 | + min-width: 24px; | ||
| 1936 | + height: 20px; | ||
| 1937 | + } | ||
| 1938 | +} | ||
| 1939 | +</style> |
| 1 | import { defineConfig } from 'vite' | 1 | import { defineConfig } from 'vite' |
| 2 | import vue from '@vitejs/plugin-vue' | 2 | import vue from '@vitejs/plugin-vue' |
| 3 | import { resolve } from 'path' | 3 | import { resolve } from 'path' |
| 4 | +import AutoImport from 'unplugin-auto-import/vite' | ||
| 5 | +import Components from 'unplugin-vue-components/vite' | ||
| 6 | +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' | ||
| 4 | 7 | ||
| 5 | // https://vitejs.dev/config/ | 8 | // https://vitejs.dev/config/ |
| 6 | export default defineConfig({ | 9 | export default defineConfig({ |
| 7 | - plugins: [vue()], | 10 | + plugins: [ |
| 11 | + vue(), | ||
| 12 | + AutoImport({ | ||
| 13 | + resolvers: [ElementPlusResolver()], | ||
| 14 | + }), | ||
| 15 | + Components({ | ||
| 16 | + resolvers: [ElementPlusResolver()], | ||
| 17 | + }), | ||
| 18 | + ], | ||
| 8 | resolve: { | 19 | resolve: { |
| 9 | alias: { | 20 | alias: { |
| 10 | '@': resolve(__dirname, 'src') | 21 | '@': resolve(__dirname, 'src') | ... | ... |
frontend/字段验证错误显示功能说明.md
0 → 100644
| 1 | +# 字段验证错误显示功能说明 | ||
| 2 | + | ||
| 3 | +## 功能概述 | ||
| 4 | + | ||
| 5 | +前端用户管理页面现在支持显示后端返回的详细字段验证错误信息,提供更好的用户体验。 | ||
| 6 | + | ||
| 7 | +## 功能特性 | ||
| 8 | + | ||
| 9 | +### 1. 字段级错误显示 | ||
| 10 | +- 当后端返回 `400` 状态码和详细的字段错误信息时,前端会自动解析并在对应字段下方显示错误信息 | ||
| 11 | +- 支持的错误格式:`{"code":400,"message":"参数验证失败","data":{"email":"邮箱格式不正确"}}` | ||
| 12 | + | ||
| 13 | +### 2. 视觉反馈 | ||
| 14 | +- 有错误的字段会显示红色边框 | ||
| 15 | +- 错误信息以红色文字显示在字段下方 | ||
| 16 | +- 错误信息字体较小,不影响整体布局 | ||
| 17 | + | ||
| 18 | +### 3. 错误清除机制 | ||
| 19 | +- 打开新增/编辑对话框时自动清除之前的错误信息 | ||
| 20 | +- 重新提交表单时清除之前的错误信息 | ||
| 21 | + | ||
| 22 | +## 支持的字段 | ||
| 23 | + | ||
| 24 | +以下字段支持错误显示: | ||
| 25 | +- `username` - 用户名 | ||
| 26 | +- `realName` - 真实姓名 | ||
| 27 | +- `phone` - 手机号 | ||
| 28 | +- `email` - 邮箱 | ||
| 29 | +- `remark` - 备注 | ||
| 30 | + | ||
| 31 | +## 后端错误格式要求 | ||
| 32 | + | ||
| 33 | +后端需要返回以下格式的错误信息: | ||
| 34 | + | ||
| 35 | +```json | ||
| 36 | +{ | ||
| 37 | + "code": 400, | ||
| 38 | + "message": "参数验证失败", | ||
| 39 | + "data": { | ||
| 40 | + "username": "用户名已存在", | ||
| 41 | + "email": "邮箱格式不正确", | ||
| 42 | + "phone": "手机号格式不正确" | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +## 使用示例 | ||
| 48 | + | ||
| 49 | +### 1. 邮箱格式错误 | ||
| 50 | +当用户输入无效邮箱时: | ||
| 51 | +- 邮箱输入框显示红色边框 | ||
| 52 | +- 输入框下方显示"邮箱格式不正确" | ||
| 53 | + | ||
| 54 | +### 2. 用户名重复 | ||
| 55 | +当用户名已存在时: | ||
| 56 | +- 用户名输入框显示红色边框 | ||
| 57 | +- 输入框下方显示"用户名已存在" | ||
| 58 | + | ||
| 59 | +### 3. 多个字段错误 | ||
| 60 | +当多个字段都有错误时: | ||
| 61 | +- 所有有错误的字段都会显示红色边框 | ||
| 62 | +- 每个字段下方显示对应的错误信息 | ||
| 63 | + | ||
| 64 | +## 技术实现 | ||
| 65 | + | ||
| 66 | +### 1. 响应式错误状态 | ||
| 67 | +```javascript | ||
| 68 | +const fieldErrors = reactive<Record<string, string>>({}) | ||
| 69 | +``` | ||
| 70 | + | ||
| 71 | +### 2. 错误解析函数 | ||
| 72 | +```javascript | ||
| 73 | +const parseFieldErrors = (errorData: any) => { | ||
| 74 | + clearFieldErrors() | ||
| 75 | + | ||
| 76 | + if (errorData && typeof errorData === 'object') { | ||
| 77 | + Object.keys(errorData).forEach(field => { | ||
| 78 | + fieldErrors[field] = errorData[field] | ||
| 79 | + }) | ||
| 80 | + } | ||
| 81 | +} | ||
| 82 | +``` | ||
| 83 | + | ||
| 84 | +### 3. 模板绑定 | ||
| 85 | +```vue | ||
| 86 | +<input | ||
| 87 | + v-model="userForm.email" | ||
| 88 | + type="email" | ||
| 89 | + class="form-input" | ||
| 90 | + :class="{ 'error': fieldErrors.email }" | ||
| 91 | +/> | ||
| 92 | +<div v-if="fieldErrors.email" class="field-error">{{ fieldErrors.email }}</div> | ||
| 93 | +``` | ||
| 94 | + | ||
| 95 | +## 样式说明 | ||
| 96 | + | ||
| 97 | +### 错误状态样式 | ||
| 98 | +```css | ||
| 99 | +.form-input.error, .form-select.error { | ||
| 100 | + border-color: #ff4d4f; | ||
| 101 | + box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2); | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +.field-error { | ||
| 105 | + color: #ff4d4f; | ||
| 106 | + font-size: 11px; | ||
| 107 | + margin-top: 4px; | ||
| 108 | + display: block; | ||
| 109 | + line-height: 1.4; | ||
| 110 | +} | ||
| 111 | +``` | ||
| 112 | + | ||
| 113 | +## 测试方法 | ||
| 114 | + | ||
| 115 | +1. 打开用户管理页面 | ||
| 116 | +2. 点击"新增用户"按钮 | ||
| 117 | +3. 输入无效的邮箱格式(如:`invalid-email`) | ||
| 118 | +4. 点击保存 | ||
| 119 | +5. 观察邮箱字段是否显示红色边框和错误信息 | ||
| 120 | + | ||
| 121 | +## 注意事项 | ||
| 122 | + | ||
| 123 | +1. 错误信息会在用户重新打开对话框时自动清除 | ||
| 124 | +2. 只有后端返回 `400` 状态码且包含 `data` 字段时才会显示字段级错误 | ||
| 125 | +3. 如果后端返回其他格式的错误,仍会显示通用错误信息 | ||
| 126 | +4. 错误信息支持中文,建议后端返回中文错误信息以提供更好的用户体验 |
frontend/时间格式修改说明.md
0 → 100644
| 1 | +# 用户管理时间格式修改说明 | ||
| 2 | + | ||
| 3 | +## 修改内容 | ||
| 4 | + | ||
| 5 | +将用户管理列表中的创建时间格式从 ISO 格式改为更易读的格式: | ||
| 6 | + | ||
| 7 | +- **修改前**: `2025-09-23T17:49:06` | ||
| 8 | +- **修改后**: `2025-09-23 17:49:06` | ||
| 9 | + | ||
| 10 | +## 技术实现 | ||
| 11 | + | ||
| 12 | +### 1. 添加时间格式化函数 | ||
| 13 | + | ||
| 14 | +```javascript | ||
| 15 | +// 格式化时间显示 | ||
| 16 | +const formatDateTime = (dateTime: string | undefined): string => { | ||
| 17 | + if (!dateTime) return '' | ||
| 18 | + | ||
| 19 | + // 将 ISO 格式的时间字符串转换为可读格式 | ||
| 20 | + // 例如: 2025-09-23T17:49:06 -> 2025-09-23 17:49:06 | ||
| 21 | + return dateTime.replace('T', ' ') | ||
| 22 | +} | ||
| 23 | +``` | ||
| 24 | + | ||
| 25 | +### 2. 修改模板显示 | ||
| 26 | + | ||
| 27 | +```vue | ||
| 28 | +<!-- 修改前 --> | ||
| 29 | +<td>{{ user.createTime }}</td> | ||
| 30 | + | ||
| 31 | +<!-- 修改后 --> | ||
| 32 | +<td>{{ formatDateTime(user.createTime) }}</td> | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +## 功能特点 | ||
| 36 | + | ||
| 37 | +1. **空值处理**: 如果时间字段为空或未定义,返回空字符串 | ||
| 38 | +2. **简单替换**: 使用 `replace('T', ' ')` 将 ISO 格式中的 `T` 替换为空格 | ||
| 39 | +3. **类型安全**: 使用 TypeScript 确保类型安全 | ||
| 40 | +4. **性能优化**: 简单的字符串替换,性能开销很小 | ||
| 41 | + | ||
| 42 | +## 影响范围 | ||
| 43 | + | ||
| 44 | +- **用户管理列表**: 创建时间列显示格式已修改 | ||
| 45 | +- **其他时间字段**: 目前只修改了 `createTime` 字段 | ||
| 46 | +- **搜索功能**: 时间搜索功能不受影响 | ||
| 47 | + | ||
| 48 | +## 测试方法 | ||
| 49 | + | ||
| 50 | +1. 打开用户管理页面 | ||
| 51 | +2. 查看用户列表中的"创建时间"列 | ||
| 52 | +3. 确认时间格式显示为 `2025-09-23 17:49:06` 而不是 `2025-09-23T17:49:06` | ||
| 53 | + | ||
| 54 | +## 扩展说明 | ||
| 55 | + | ||
| 56 | +如果需要修改其他时间字段(如 `updateTime`),可以按照相同的方式: | ||
| 57 | + | ||
| 58 | +```vue | ||
| 59 | +<td>{{ formatDateTime(user.updateTime) }}</td> | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +## 注意事项 | ||
| 63 | + | ||
| 64 | +1. 这个修改只影响前端显示,不影响后端数据存储格式 | ||
| 65 | +2. 时间搜索功能仍然使用原始的 ISO 格式 | ||
| 66 | +3. 如果后端返回的时间格式发生变化,这个函数仍然可以正常工作 |
frontend/时间选择器功能说明.md
0 → 100644
| 1 | +# 时间选择器功能说明 | ||
| 2 | + | ||
| 3 | +## 功能概述 | ||
| 4 | + | ||
| 5 | +用户管理页面的时间搜索组件已升级为具有悬停弹出选项功能的高级时间选择器,提供更便捷的时间范围选择体验。 | ||
| 6 | + | ||
| 7 | +## 功能特性 | ||
| 8 | + | ||
| 9 | +### 1. 悬停弹出选项 | ||
| 10 | +- 鼠标悬停在时间输入框上时,自动弹出时间选项菜单 | ||
| 11 | +- 支持快速选择常用的时间范围 | ||
| 12 | +- 提供直观的视觉反馈 | ||
| 13 | + | ||
| 14 | +### 2. 预设时间选项 | ||
| 15 | +- **今天**: 选择当前日期 | ||
| 16 | +- **昨天**: 选择昨天的日期 | ||
| 17 | +- **本周**: 选择本周开始日期(周一) | ||
| 18 | +- **上周**: 选择上周的时间范围 | ||
| 19 | +- **本月**: 选择本月开始日期 | ||
| 20 | +- **上月**: 选择上月的时间范围 | ||
| 21 | +- **自定义**: 打开原生日期选择器 | ||
| 22 | + | ||
| 23 | +### 3. 智能时间范围 | ||
| 24 | +- 开始时间和结束时间都有独立的选项菜单 | ||
| 25 | +- 结束时间会自动选择合理的结束日期 | ||
| 26 | +- 支持时间范围的逻辑验证 | ||
| 27 | + | ||
| 28 | +## 技术实现 | ||
| 29 | + | ||
| 30 | +### 1. 组件结构 | ||
| 31 | +```vue | ||
| 32 | +<div class="date-picker-container"> | ||
| 33 | + <input | ||
| 34 | + v-model="searchForm.startTime" | ||
| 35 | + type="text" | ||
| 36 | + class="date-input" | ||
| 37 | + readonly | ||
| 38 | + @mouseenter="showStartDateOptions = true" | ||
| 39 | + @mouseleave="hideStartDateOptions" | ||
| 40 | + /> | ||
| 41 | + <div v-if="showStartDateOptions" class="date-options"> | ||
| 42 | + <!-- 时间选项列表 --> | ||
| 43 | + </div> | ||
| 44 | +</div> | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +### 2. 状态管理 | ||
| 48 | +```javascript | ||
| 49 | +// 时间选择器状态 | ||
| 50 | +const showStartDateOptions = ref(false) | ||
| 51 | +const showEndDateOptions = ref(false) | ||
| 52 | +const showStartDatePicker = ref(false) | ||
| 53 | +const showEndDatePicker = ref(false) | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +### 3. 时间计算函数 | ||
| 57 | +- `getCurrentDate()`: 获取当前日期 | ||
| 58 | +- `getDateBefore(days)`: 获取指定天数前的日期 | ||
| 59 | +- `getWeekStart()`: 获取本周开始日期 | ||
| 60 | +- `getMonthStart()`: 获取本月开始日期 | ||
| 61 | +- `getLastMonthStart()`: 获取上月开始日期 | ||
| 62 | +- `getLastMonthEnd()`: 获取上月结束日期 | ||
| 63 | + | ||
| 64 | +## 使用方法 | ||
| 65 | + | ||
| 66 | +### 1. 快速选择时间 | ||
| 67 | +1. 将鼠标悬停在"开始时间"或"结束时间"输入框上 | ||
| 68 | +2. 在弹出的选项菜单中选择所需的时间范围 | ||
| 69 | +3. 系统自动填充对应的日期 | ||
| 70 | + | ||
| 71 | +### 2. 自定义时间选择 | ||
| 72 | +1. 悬停在时间输入框上 | ||
| 73 | +2. 选择"自定义"选项 | ||
| 74 | +3. 使用原生日期选择器选择具体日期 | ||
| 75 | + | ||
| 76 | +### 3. 时间范围搜索 | ||
| 77 | +1. 选择开始时间和结束时间 | ||
| 78 | +2. 点击"搜索"按钮执行查询 | ||
| 79 | +3. 系统会搜索指定时间范围内的用户 | ||
| 80 | + | ||
| 81 | +## 样式设计 | ||
| 82 | + | ||
| 83 | +### 1. 选项菜单样式 | ||
| 84 | +```css | ||
| 85 | +.date-options { | ||
| 86 | + position: absolute; | ||
| 87 | + top: 100%; | ||
| 88 | + background: white; | ||
| 89 | + border: 1px solid #d9d9d9; | ||
| 90 | + border-radius: 4px; | ||
| 91 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | ||
| 92 | + z-index: 1000; | ||
| 93 | +} | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +### 2. 选项项样式 | ||
| 97 | +```css | ||
| 98 | +.date-option { | ||
| 99 | + padding: 8px 12px; | ||
| 100 | + cursor: pointer; | ||
| 101 | + transition: background-color 0.2s; | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +.date-option:hover { | ||
| 105 | + background-color: #f5f5f5; | ||
| 106 | + color: #1890ff; | ||
| 107 | +} | ||
| 108 | +``` | ||
| 109 | + | ||
| 110 | +## 交互逻辑 | ||
| 111 | + | ||
| 112 | +### 1. 悬停显示 | ||
| 113 | +- 鼠标进入输入框时显示选项菜单 | ||
| 114 | +- 鼠标离开输入框时延迟隐藏菜单(200ms) | ||
| 115 | +- 鼠标进入选项菜单时保持显示状态 | ||
| 116 | + | ||
| 117 | +### 2. 选择处理 | ||
| 118 | +- 点击选项后自动填充对应日期 | ||
| 119 | +- 选择"自定义"时显示原生日期选择器 | ||
| 120 | +- 选择完成后自动隐藏选项菜单 | ||
| 121 | + | ||
| 122 | +### 3. 时间范围逻辑 | ||
| 123 | +- 开始时间不能晚于结束时间 | ||
| 124 | +- 结束时间不能早于开始时间 | ||
| 125 | +- 支持空值选择(只选择开始时间或结束时间) | ||
| 126 | + | ||
| 127 | +## 兼容性 | ||
| 128 | + | ||
| 129 | +- 支持现代浏览器的原生日期选择器 | ||
| 130 | +- 降级到文本输入框(readonly) | ||
| 131 | +- 响应式设计,适配不同屏幕尺寸 | ||
| 132 | + | ||
| 133 | +## 扩展功能 | ||
| 134 | + | ||
| 135 | +### 1. 可添加的选项 | ||
| 136 | +- 最近7天 | ||
| 137 | +- 最近30天 | ||
| 138 | +- 最近3个月 | ||
| 139 | +- 最近一年 | ||
| 140 | + | ||
| 141 | +### 2. 可优化的功能 | ||
| 142 | +- 时间范围验证 | ||
| 143 | +- 快捷清除功能 | ||
| 144 | +- 时间格式显示优化 | ||
| 145 | + | ||
| 146 | +## 测试方法 | ||
| 147 | + | ||
| 148 | +1. 打开用户管理页面 | ||
| 149 | +2. 将鼠标悬停在"创建时间"的输入框上 | ||
| 150 | +3. 观察是否弹出选项菜单 | ||
| 151 | +4. 点击不同选项测试功能 | ||
| 152 | +5. 选择"自定义"测试原生日期选择器 | ||
| 153 | +6. 执行搜索验证时间范围功能 | ||
| 154 | + | ||
| 155 | +## 注意事项 | ||
| 156 | + | ||
| 157 | +1. 时间格式统一使用 YYYY-MM-DD 格式 | ||
| 158 | +2. 时区处理基于浏览器本地时间 | ||
| 159 | +3. 选项菜单的 z-index 设置为 1000,确保显示在最上层 | ||
| 160 | +4. 延迟隐藏机制避免鼠标快速移动时菜单闪烁 |
-
Please register or login to post a comment