zhouhui.jiang

添加 用户管理、角色管理、菜单管理、字典管理、日志管理

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 }
......
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
......
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
......
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
......
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>
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')
......
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. 错误信息支持中文,建议后端返回中文错误信息以提供更好的用户体验
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. 如果后端返回的时间格式发生变化,这个函数仍然可以正常工作
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. 延迟隐藏机制避免鼠标快速移动时菜单闪烁