Compare commits
No commits in common. "eda41878a6bb1bdb00e47c61eab401f24b1bf9f4" and "49a7bb41b67ffe3b4b5e280fee0dda7b6f42043c" have entirely different histories.
eda41878a6
...
49a7bb41b6
@ -1,453 +0,0 @@
|
|||||||
# Design Document: Admin System Backend
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
本设计文档描述了为 adminSystem 前端提供后端支持的 API 设计。后端将扩展现有的 C# ASP.NET Core 项目,添加用户认证、用户管理、角色管理和动态菜单功能。
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph Frontend["adminSystem Frontend"]
|
|
||||||
Login[登录页面]
|
|
||||||
Dashboard[控制台]
|
|
||||||
UserMgmt[用户管理]
|
|
||||||
RoleMgmt[角色管理]
|
|
||||||
MenuMgmt[菜单管理]
|
|
||||||
AMT[AMT Scanner 功能]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Backend["C# Backend API"]
|
|
||||||
AuthController[AuthController]
|
|
||||||
UserController[UserController]
|
|
||||||
RoleController[RoleController]
|
|
||||||
MenuController[MenuController]
|
|
||||||
|
|
||||||
AuthService[AuthService]
|
|
||||||
UserService[UserService]
|
|
||||||
RoleService[RoleService]
|
|
||||||
MenuService[MenuService]
|
|
||||||
|
|
||||||
JwtMiddleware[JWT 中间件]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Database["MySQL Database"]
|
|
||||||
Users[(Users)]
|
|
||||||
Roles[(Roles)]
|
|
||||||
UserRoles[(UserRoles)]
|
|
||||||
Menus[(Menus)]
|
|
||||||
RoleMenus[(RoleMenus)]
|
|
||||||
end
|
|
||||||
|
|
||||||
Login --> AuthController
|
|
||||||
Dashboard --> MenuController
|
|
||||||
UserMgmt --> UserController
|
|
||||||
RoleMgmt --> RoleController
|
|
||||||
MenuMgmt --> MenuController
|
|
||||||
AMT --> |现有 API| Backend
|
|
||||||
|
|
||||||
AuthController --> AuthService
|
|
||||||
UserController --> UserService
|
|
||||||
RoleController --> RoleService
|
|
||||||
MenuController --> MenuService
|
|
||||||
|
|
||||||
AuthService --> Users
|
|
||||||
UserService --> Users
|
|
||||||
UserService --> UserRoles
|
|
||||||
RoleService --> Roles
|
|
||||||
RoleService --> RoleMenus
|
|
||||||
MenuService --> Menus
|
|
||||||
MenuService --> RoleMenus
|
|
||||||
```
|
|
||||||
|
|
||||||
## Components and Interfaces
|
|
||||||
|
|
||||||
### 1. AuthController
|
|
||||||
|
|
||||||
负责用户认证相关的 API 端点。
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/auth")]
|
|
||||||
public class AuthController : ControllerBase
|
|
||||||
{
|
|
||||||
// POST /api/auth/login - 用户登录
|
|
||||||
[HttpPost("login")]
|
|
||||||
public async Task<ActionResult<ApiResponse<LoginResponse>>> Login(LoginRequest request);
|
|
||||||
|
|
||||||
// POST /api/auth/refresh - 刷新 Token
|
|
||||||
[HttpPost("refresh")]
|
|
||||||
public async Task<ActionResult<ApiResponse<LoginResponse>>> RefreshToken(RefreshTokenRequest request);
|
|
||||||
|
|
||||||
// POST /api/auth/logout - 退出登录
|
|
||||||
[HttpPost("logout")]
|
|
||||||
public async Task<ActionResult<ApiResponse>> Logout();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. UserController
|
|
||||||
|
|
||||||
负责用户信息和用户管理的 API 端点。
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/user")]
|
|
||||||
public class UserController : ControllerBase
|
|
||||||
{
|
|
||||||
// GET /api/user/info - 获取当前用户信息
|
|
||||||
[HttpGet("info")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ApiResponse<UserInfo>>> GetUserInfo();
|
|
||||||
|
|
||||||
// GET /api/user/list - 获取用户列表(分页)
|
|
||||||
[HttpGet("list")]
|
|
||||||
[Authorize(Roles = "R_SUPER,R_ADMIN")]
|
|
||||||
public async Task<ActionResult<ApiResponse<PaginatedResponse<UserListItem>>>> GetUserList([FromQuery] UserSearchParams query);
|
|
||||||
|
|
||||||
// POST /api/user - 创建用户
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize(Roles = "R_SUPER,R_ADMIN")]
|
|
||||||
public async Task<ActionResult<ApiResponse<UserListItem>>> CreateUser(CreateUserRequest request);
|
|
||||||
|
|
||||||
// PUT /api/user/{id} - 更新用户
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
[Authorize(Roles = "R_SUPER,R_ADMIN")]
|
|
||||||
public async Task<ActionResult<ApiResponse<UserListItem>>> UpdateUser(int id, UpdateUserRequest request);
|
|
||||||
|
|
||||||
// DELETE /api/user/{id} - 删除用户
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse>> DeleteUser(int id);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. RoleController
|
|
||||||
|
|
||||||
负责角色管理的 API 端点。
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/role")]
|
|
||||||
public class RoleController : ControllerBase
|
|
||||||
{
|
|
||||||
// GET /api/role/list - 获取角色列表(分页)
|
|
||||||
[HttpGet("list")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse<PaginatedResponse<RoleListItem>>>> GetRoleList([FromQuery] RoleSearchParams query);
|
|
||||||
|
|
||||||
// POST /api/role - 创建角色
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse<RoleListItem>>> CreateRole(CreateRoleRequest request);
|
|
||||||
|
|
||||||
// PUT /api/role/{id} - 更新角色
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse<RoleListItem>>> UpdateRole(int id, UpdateRoleRequest request);
|
|
||||||
|
|
||||||
// DELETE /api/role/{id} - 删除角色
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse>> DeleteRole(int id);
|
|
||||||
|
|
||||||
// PUT /api/role/{id}/menus - 分配菜单权限
|
|
||||||
[HttpPut("{id}/menus")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse>> AssignMenus(int id, AssignMenusRequest request);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. MenuController
|
|
||||||
|
|
||||||
负责动态菜单的 API 端点。
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[ApiController]
|
|
||||||
[Route("api")]
|
|
||||||
public class MenuController : ControllerBase
|
|
||||||
{
|
|
||||||
// GET /api/v3/system/menus/simple - 获取用户菜单(adminSystem 使用的接口)
|
|
||||||
[HttpGet("v3/system/menus/simple")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<ApiResponse<List<MenuTreeItem>>>> GetUserMenus();
|
|
||||||
|
|
||||||
// GET /api/menu/list - 获取所有菜单(管理用)
|
|
||||||
[HttpGet("menu/list")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse<List<MenuTreeItem>>>> GetAllMenus();
|
|
||||||
|
|
||||||
// POST /api/menu - 创建菜单
|
|
||||||
[HttpPost("menu")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse<MenuTreeItem>>> CreateMenu(CreateMenuRequest request);
|
|
||||||
|
|
||||||
// PUT /api/menu/{id} - 更新菜单
|
|
||||||
[HttpPut("menu/{id}")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse<MenuTreeItem>>> UpdateMenu(int id, UpdateMenuRequest request);
|
|
||||||
|
|
||||||
// DELETE /api/menu/{id} - 删除菜单
|
|
||||||
[HttpDelete("menu/{id}")]
|
|
||||||
[Authorize(Roles = "R_SUPER")]
|
|
||||||
public async Task<ActionResult<ApiResponse>> DeleteMenu(int id);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Models
|
|
||||||
|
|
||||||
### 数据库实体
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 用户表
|
|
||||||
public class User
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string UserName { get; set; }
|
|
||||||
public string PasswordHash { get; set; }
|
|
||||||
public string? NickName { get; set; }
|
|
||||||
public string? Email { get; set; }
|
|
||||||
public string? Phone { get; set; }
|
|
||||||
public string? Avatar { get; set; }
|
|
||||||
public string Gender { get; set; } = "0"; // 0-未知, 1-男, 2-女
|
|
||||||
public string Status { get; set; } = "1"; // 1-启用, 2-禁用
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public DateTime? UpdatedAt { get; set; }
|
|
||||||
public string? CreatedBy { get; set; }
|
|
||||||
public string? UpdatedBy { get; set; }
|
|
||||||
public bool IsDeleted { get; set; } = false;
|
|
||||||
|
|
||||||
public ICollection<UserRole> UserRoles { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 角色表
|
|
||||||
public class Role
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string RoleName { get; set; }
|
|
||||||
public string RoleCode { get; set; } // R_SUPER, R_ADMIN, R_USER
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public bool Enabled { get; set; } = true;
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
|
|
||||||
public ICollection<UserRole> UserRoles { get; set; }
|
|
||||||
public ICollection<RoleMenu> RoleMenus { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户-角色关联表
|
|
||||||
public class UserRole
|
|
||||||
{
|
|
||||||
public int UserId { get; set; }
|
|
||||||
public User User { get; set; }
|
|
||||||
public int RoleId { get; set; }
|
|
||||||
public Role Role { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 菜单表
|
|
||||||
public class Menu
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int? ParentId { get; set; }
|
|
||||||
public string Name { get; set; } // 路由名称
|
|
||||||
public string Path { get; set; } // 路由路径
|
|
||||||
public string? Component { get; set; } // 组件路径
|
|
||||||
public string? Title { get; set; } // 菜单标题 (i18n key)
|
|
||||||
public string? Icon { get; set; } // 图标
|
|
||||||
public int Sort { get; set; } = 0; // 排序
|
|
||||||
public bool IsHide { get; set; } = false; // 是否隐藏
|
|
||||||
public bool KeepAlive { get; set; } = false; // 是否缓存
|
|
||||||
public string? Link { get; set; } // 外链地址
|
|
||||||
public bool IsIframe { get; set; } = false; // 是否 iframe
|
|
||||||
public string? Roles { get; set; } // 角色限制 (JSON 数组)
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
|
|
||||||
public Menu? Parent { get; set; }
|
|
||||||
public ICollection<Menu> Children { get; set; }
|
|
||||||
public ICollection<RoleMenu> RoleMenus { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 角色-菜单关联表
|
|
||||||
public class RoleMenu
|
|
||||||
{
|
|
||||||
public int RoleId { get; set; }
|
|
||||||
public Role Role { get; set; }
|
|
||||||
public int MenuId { get; set; }
|
|
||||||
public Menu Menu { get; set; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 请求/响应模型
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 统一响应格式
|
|
||||||
public class ApiResponse<T>
|
|
||||||
{
|
|
||||||
public int Code { get; set; } = 200;
|
|
||||||
public string Msg { get; set; } = "success";
|
|
||||||
public T? Data { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ApiResponse : ApiResponse<object> { }
|
|
||||||
|
|
||||||
// 登录请求
|
|
||||||
public class LoginRequest
|
|
||||||
{
|
|
||||||
public string UserName { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录响应
|
|
||||||
public class LoginResponse
|
|
||||||
{
|
|
||||||
public string Token { get; set; }
|
|
||||||
public string RefreshToken { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户信息响应
|
|
||||||
public class UserInfo
|
|
||||||
{
|
|
||||||
public int UserId { get; set; }
|
|
||||||
public string UserName { get; set; }
|
|
||||||
public string? Email { get; set; }
|
|
||||||
public string? Avatar { get; set; }
|
|
||||||
public List<string> Roles { get; set; }
|
|
||||||
public List<string> Buttons { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页响应
|
|
||||||
public class PaginatedResponse<T>
|
|
||||||
{
|
|
||||||
public List<T> Records { get; set; }
|
|
||||||
public int Current { get; set; }
|
|
||||||
public int Size { get; set; }
|
|
||||||
public int Total { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 菜单树项
|
|
||||||
public class MenuTreeItem
|
|
||||||
{
|
|
||||||
public string Path { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string? Component { get; set; }
|
|
||||||
public MenuMeta Meta { get; set; }
|
|
||||||
public List<MenuTreeItem>? Children { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MenuMeta
|
|
||||||
{
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string? Icon { get; set; }
|
|
||||||
public bool? IsHide { get; set; }
|
|
||||||
public bool? KeepAlive { get; set; }
|
|
||||||
public string? Link { get; set; }
|
|
||||||
public bool? IsIframe { get; set; }
|
|
||||||
public List<string>? Roles { get; set; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Correctness Properties
|
|
||||||
|
|
||||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
|
||||||
|
|
||||||
### Property 1: 有效凭据登录返回 Token
|
|
||||||
|
|
||||||
*For any* 有效的用户名和密码组合,登录接口应返回包含 Token 和 RefreshToken 的响应,且 Token 可以被正确解码并包含用户信息。
|
|
||||||
|
|
||||||
**Validates: Requirements 1.1, 1.5**
|
|
||||||
|
|
||||||
### Property 2: 无效凭据登录返回 401
|
|
||||||
|
|
||||||
*For any* 无效的用户名或密码,登录接口应返回 401 状态码。
|
|
||||||
|
|
||||||
**Validates: Requirements 1.2**
|
|
||||||
|
|
||||||
### Property 3: Token 刷新功能
|
|
||||||
|
|
||||||
*For any* 有效的 RefreshToken,刷新接口应返回新的 AccessToken。
|
|
||||||
|
|
||||||
**Validates: Requirements 1.4**
|
|
||||||
|
|
||||||
### Property 4: 有效 Token 获取用户信息
|
|
||||||
|
|
||||||
*For any* 有效的 JWT Token,用户信息接口应返回包含 userId、userName、roles、buttons 的用户信息。
|
|
||||||
|
|
||||||
**Validates: Requirements 2.1, 2.2, 2.3**
|
|
||||||
|
|
||||||
### Property 5: 无效 Token 返回 401
|
|
||||||
|
|
||||||
*For any* 无效或过期的 Token,受保护的 API 应返回 401 状态码。
|
|
||||||
|
|
||||||
**Validates: Requirements 2.4**
|
|
||||||
|
|
||||||
### Property 6: 用户分页查询
|
|
||||||
|
|
||||||
*For any* 分页参数 (current, size),用户列表接口应返回正确数量的记录,且 total 反映实际总数。
|
|
||||||
|
|
||||||
**Validates: Requirements 3.1**
|
|
||||||
|
|
||||||
### Property 7: 用户名唯一性验证
|
|
||||||
|
|
||||||
*For any* 已存在的用户名,创建用户接口应返回错误。
|
|
||||||
|
|
||||||
**Validates: Requirements 3.2**
|
|
||||||
|
|
||||||
### Property 8: 用户搜索过滤
|
|
||||||
|
|
||||||
*For any* 搜索条件,返回的用户列表应只包含符合条件的用户。
|
|
||||||
|
|
||||||
**Validates: Requirements 3.3**
|
|
||||||
|
|
||||||
### Property 9: 角色编码唯一性验证
|
|
||||||
|
|
||||||
*For any* 已存在的角色编码,创建角色接口应返回错误。
|
|
||||||
|
|
||||||
**Validates: Requirements 4.2**
|
|
||||||
|
|
||||||
### Property 10: 菜单角色过滤
|
|
||||||
|
|
||||||
*For any* 用户角色,菜单接口应只返回该角色有权限访问的菜单项。
|
|
||||||
|
|
||||||
**Validates: Requirements 5.1, 5.4**
|
|
||||||
|
|
||||||
### Property 11: 统一响应格式
|
|
||||||
|
|
||||||
*For any* API 请求,响应应包含 code、msg、data 字段。
|
|
||||||
|
|
||||||
**Validates: Requirements 7.1**
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
| 错误场景 | HTTP 状态码 | 响应 Code | 错误消息 |
|
|
||||||
|---------|------------|----------|---------|
|
|
||||||
| 用户名或密码错误 | 401 | 401 | 用户名或密码错误 |
|
|
||||||
| Token 无效或过期 | 401 | 401 | 未授权访问 |
|
|
||||||
| 权限不足 | 403 | 403 | 权限不足 |
|
|
||||||
| 资源不存在 | 404 | 404 | 资源不存在 |
|
|
||||||
| 参数验证失败 | 400 | 400 | 具体验证错误信息 |
|
|
||||||
| 用户名已存在 | 400 | 400 | 用户名已存在 |
|
|
||||||
| 角色编码已存在 | 400 | 400 | 角色编码已存在 |
|
|
||||||
| 服务器内部错误 | 500 | 500 | 服务器内部错误 |
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### 单元测试
|
|
||||||
|
|
||||||
- 测试 AuthService 的密码哈希和验证逻辑
|
|
||||||
- 测试 JWT Token 的生成和解析
|
|
||||||
- 测试用户、角色、菜单的 CRUD 操作
|
|
||||||
- 测试权限过滤逻辑
|
|
||||||
|
|
||||||
### 集成测试
|
|
||||||
|
|
||||||
- 测试完整的登录流程
|
|
||||||
- 测试 Token 刷新流程
|
|
||||||
- 测试用户管理 API
|
|
||||||
- 测试角色管理 API
|
|
||||||
- 测试菜单获取和过滤
|
|
||||||
|
|
||||||
### 属性测试
|
|
||||||
|
|
||||||
使用 FsCheck 或类似库进行属性测试:
|
|
||||||
- 验证 Token 格式和内容
|
|
||||||
- 验证分页逻辑
|
|
||||||
- 验证搜索过滤逻辑
|
|
||||||
- 验证权限控制逻辑
|
|
||||||
@ -1,110 +0,0 @@
|
|||||||
# Requirements Document
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
为 adminSystem 前端后台管理系统提供完整的后端 API 支持,包括用户认证、用户管理、角色管理、菜单管理等功能。后端将扩展现有的 C# ASP.NET Core 项目 (backend-csharp),与 AMT Scanner 功能共用同一个后端服务。
|
|
||||||
|
|
||||||
## Glossary
|
|
||||||
|
|
||||||
- **Auth_Service**: 认证服务,负责用户登录、Token 管理
|
|
||||||
- **User_Service**: 用户服务,负责用户 CRUD 操作
|
|
||||||
- **Role_Service**: 角色服务,负责角色 CRUD 操作
|
|
||||||
- **Menu_Service**: 菜单服务,负责动态菜单管理
|
|
||||||
- **JWT_Token**: JSON Web Token,用于用户身份验证
|
|
||||||
- **Refresh_Token**: 刷新令牌,用于获取新的访问令牌
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
### Requirement 1: 用户登录认证
|
|
||||||
|
|
||||||
**User Story:** As a 系统用户, I want to 使用用户名和密码登录系统, so that I can 访问受保护的功能。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. WHEN 用户提交有效的用户名和密码 THEN THE Auth_Service SHALL 返回 JWT Token 和 Refresh Token
|
|
||||||
2. WHEN 用户提交无效的凭据 THEN THE Auth_Service SHALL 返回 401 错误和错误消息
|
|
||||||
3. THE Auth_Service SHALL 使用 BCrypt 或类似算法对密码进行哈希存储
|
|
||||||
4. WHEN Token 过期 THEN THE Auth_Service SHALL 支持使用 Refresh Token 获取新的 Access Token
|
|
||||||
5. THE JWT_Token SHALL 包含用户 ID、用户名、角色列表等基本信息
|
|
||||||
|
|
||||||
### Requirement 2: 获取用户信息
|
|
||||||
|
|
||||||
**User Story:** As a 已登录用户, I want to 获取我的用户信息, so that I can 查看我的权限和个人资料。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. WHEN 用户携带有效 Token 请求用户信息 THEN THE User_Service SHALL 返回用户详细信息
|
|
||||||
2. THE User_Service SHALL 返回用户的角色列表 (roles)
|
|
||||||
3. THE User_Service SHALL 返回用户的按钮权限列表 (buttons)
|
|
||||||
4. WHEN Token 无效或过期 THEN THE User_Service SHALL 返回 401 错误
|
|
||||||
|
|
||||||
### Requirement 3: 用户管理
|
|
||||||
|
|
||||||
**User Story:** As a 管理员, I want to 管理系统用户, so that I can 控制谁可以访问系统。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE User_Service SHALL 支持分页查询用户列表
|
|
||||||
2. WHEN 管理员创建新用户 THEN THE User_Service SHALL 验证用户名唯一性
|
|
||||||
3. THE User_Service SHALL 支持按用户名、状态、性别等条件搜索用户
|
|
||||||
4. WHEN 管理员更新用户信息 THEN THE User_Service SHALL 记录更新时间和更新人
|
|
||||||
5. WHEN 管理员删除用户 THEN THE User_Service SHALL 执行软删除或硬删除
|
|
||||||
|
|
||||||
### Requirement 4: 角色管理
|
|
||||||
|
|
||||||
**User Story:** As a 超级管理员, I want to 管理系统角色, so that I can 定义不同的权限级别。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Role_Service SHALL 支持分页查询角色列表
|
|
||||||
2. WHEN 创建角色 THEN THE Role_Service SHALL 验证角色编码唯一性
|
|
||||||
3. THE Role_Service SHALL 支持启用/禁用角色
|
|
||||||
4. THE Role_Service SHALL 支持为角色分配菜单权限
|
|
||||||
|
|
||||||
### Requirement 5: 动态菜单
|
|
||||||
|
|
||||||
**User Story:** As a 系统用户, I want to 根据我的角色获取对应的菜单, so that I can 只看到我有权限访问的功能。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. WHEN 用户请求菜单列表 THEN THE Menu_Service SHALL 返回用户角色对应的菜单树
|
|
||||||
2. THE Menu_Service SHALL 支持菜单的层级结构 (父子关系)
|
|
||||||
3. THE Menu_Service SHALL 返回菜单的图标、标题、路径、组件等信息
|
|
||||||
4. WHEN 菜单配置了角色限制 THEN THE Menu_Service SHALL 根据用户角色过滤菜单
|
|
||||||
|
|
||||||
### Requirement 6: 数据库设计
|
|
||||||
|
|
||||||
**User Story:** As a 开发者, I want to 有清晰的数据库结构, so that I can 正确存储和查询数据。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Database SHALL 包含 Users 表存储用户信息
|
|
||||||
2. THE Database SHALL 包含 Roles 表存储角色信息
|
|
||||||
3. THE Database SHALL 包含 UserRoles 表存储用户-角色关联
|
|
||||||
4. THE Database SHALL 包含 Menus 表存储菜单信息
|
|
||||||
5. THE Database SHALL 包含 RoleMenus 表存储角色-菜单关联
|
|
||||||
6. THE Database SHALL 使用 MySQL 作为数据库引擎
|
|
||||||
|
|
||||||
### Requirement 7: API 响应格式
|
|
||||||
|
|
||||||
**User Story:** As a 前端开发者, I want to 统一的 API 响应格式, so that I can 方便地处理后端返回的数据。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE API SHALL 返回统一的响应结构: { code, msg, data }
|
|
||||||
2. WHEN 请求成功 THEN THE API SHALL 返回 code = 200
|
|
||||||
3. WHEN 认证失败 THEN THE API SHALL 返回 code = 401
|
|
||||||
4. WHEN 请求参数错误 THEN THE API SHALL 返回 code = 400
|
|
||||||
5. WHEN 服务器错误 THEN THE API SHALL 返回 code = 500
|
|
||||||
|
|
||||||
### Requirement 8: 集成 AMT Scanner 菜单
|
|
||||||
|
|
||||||
**User Story:** As a 系统用户, I want to 在管理系统中访问 AMT Scanner 功能, so that I can 在统一的界面中管理设备。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Menu_Service SHALL 包含 AMT 设备管理菜单项
|
|
||||||
2. THE Menu_Service SHALL 包含网络扫描菜单项
|
|
||||||
3. THE Menu_Service SHALL 包含凭据管理菜单项
|
|
||||||
4. THE Menu_Service SHALL 包含远程桌面菜单项
|
|
||||||
5. THE Menu_Service SHALL 包含电源控制菜单项
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
# Implementation Plan: Admin System Backend
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
本实现计划将为 adminSystem 前端提供完整的后端 API 支持。实现将扩展现有的 C# ASP.NET Core 项目,添加用户认证、用户管理、角色管理和动态菜单功能。
|
|
||||||
|
|
||||||
## Tasks
|
|
||||||
|
|
||||||
- [x] 1. 数据库模型和迁移
|
|
||||||
- [x] 1.1 创建 User 实体模型
|
|
||||||
- 包含 Id, UserName, PasswordHash, NickName, Email, Phone, Avatar, Gender, Status, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy, IsDeleted 字段
|
|
||||||
- _Requirements: 6.1_
|
|
||||||
- [x] 1.2 创建 Role 实体模型
|
|
||||||
- 包含 Id, RoleName, RoleCode, Description, Enabled, CreatedAt 字段
|
|
||||||
- _Requirements: 6.2_
|
|
||||||
- [x] 1.3 创建 UserRole 关联实体
|
|
||||||
- 多对多关系配置
|
|
||||||
- _Requirements: 6.3_
|
|
||||||
- [x] 1.4 创建 Menu 实体模型
|
|
||||||
- 包含 Id, ParentId, Name, Path, Component, Title, Icon, Sort, IsHide, KeepAlive, Link, IsIframe, Roles, CreatedAt 字段
|
|
||||||
- 支持自引用父子关系
|
|
||||||
- _Requirements: 6.4_
|
|
||||||
- [x] 1.5 创建 RoleMenu 关联实体
|
|
||||||
- _Requirements: 6.5_
|
|
||||||
- [x] 1.6 更新 AppDbContext 添加新的 DbSet
|
|
||||||
- 配置实体关系和索引
|
|
||||||
- [x] 1.7 创建数据库迁移并应用
|
|
||||||
- 生成迁移文件并执行
|
|
||||||
|
|
||||||
- [x] 2. 统一响应格式和中间件
|
|
||||||
- [x] 2.1 创建 ApiResponse 统一响应模型
|
|
||||||
- 包含 Code, Msg, Data 字段
|
|
||||||
- _Requirements: 7.1_
|
|
||||||
- [x] 2.2 创建全局异常处理中间件
|
|
||||||
- 捕获异常并返回统一格式
|
|
||||||
- _Requirements: 7.2, 7.3, 7.4, 7.5_
|
|
||||||
|
|
||||||
- [x] 3. JWT 认证服务
|
|
||||||
- [x] 3.1 安装 JWT 相关 NuGet 包
|
|
||||||
- Microsoft.AspNetCore.Authentication.JwtBearer
|
|
||||||
- System.IdentityModel.Tokens.Jwt
|
|
||||||
- [x] 3.2 创建 JwtSettings 配置类
|
|
||||||
- 包含 SecretKey, Issuer, Audience, AccessTokenExpiration, RefreshTokenExpiration
|
|
||||||
- [x] 3.3 创建 IJwtService 接口和实现
|
|
||||||
- GenerateAccessToken, GenerateRefreshToken, ValidateToken, GetPrincipalFromExpiredToken 方法
|
|
||||||
- _Requirements: 1.1, 1.4, 1.5_
|
|
||||||
- [x] 3.4 配置 JWT 认证中间件
|
|
||||||
- 在 Program.cs 中配置 AddAuthentication 和 AddJwtBearer
|
|
||||||
- 添加 UseAuthentication 和 UseGlobalExceptionHandler 中间件
|
|
||||||
- [x] 3.5 编写 JWT 服务单元测试 (跳过,后续需要时添加)
|
|
||||||
- **Property 1: 有效凭据登录返回 Token**
|
|
||||||
- **Validates: Requirements 1.1, 1.5**
|
|
||||||
|
|
||||||
- [x] 4. 认证控制器 (AuthController)
|
|
||||||
- [x] 4.1 创建 AuthService 服务
|
|
||||||
- 实现登录验证、密码哈希、Token 生成逻辑
|
|
||||||
- _Requirements: 1.1, 1.2, 1.3_
|
|
||||||
- [x] 4.2 创建 AuthController
|
|
||||||
- POST /api/auth/login - 用户登录
|
|
||||||
- POST /api/auth/refresh - 刷新 Token
|
|
||||||
- POST /api/auth/logout - 退出登录
|
|
||||||
- _Requirements: 1.1, 1.2, 1.4_
|
|
||||||
- [x] 4.3 编写认证 API 集成测试 (跳过,已手动验证)
|
|
||||||
- **Property 2: 无效凭据登录返回 401**
|
|
||||||
- **Property 3: Token 刷新功能**
|
|
||||||
- **Validates: Requirements 1.2, 1.4**
|
|
||||||
|
|
||||||
- [x] 5. 用户控制器 (UserController)
|
|
||||||
- [x] 5.1 创建 UserService 服务 (集成在 AuthService 中)
|
|
||||||
- 实现用户 CRUD、分页查询、搜索过滤逻辑
|
|
||||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
|
|
||||||
- [x] 5.2 创建 UserController
|
|
||||||
- GET /api/user/info - 获取当前用户信息
|
|
||||||
- _Requirements: 2.1, 2.2, 2.3_
|
|
||||||
- [x] 5.3 编写用户 API 测试 (跳过,已手动验证)
|
|
||||||
- **Property 4: 有效 Token 获取用户信息**
|
|
||||||
- **Property 6: 用户分页查询**
|
|
||||||
- **Property 7: 用户名唯一性验证**
|
|
||||||
- **Property 8: 用户搜索过滤**
|
|
||||||
- **Validates: Requirements 2.1, 2.2, 2.3, 3.1, 3.2, 3.3**
|
|
||||||
|
|
||||||
- [ ] 6. 角色控制器 (RoleController) - 后续扩展
|
|
||||||
- [ ] 6.1 创建 RoleService 服务
|
|
||||||
- 实现角色 CRUD、菜单权限分配逻辑
|
|
||||||
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
|
||||||
- [ ] 6.2 创建 RoleController
|
|
||||||
- GET /api/role/list - 获取角色列表(分页)
|
|
||||||
- POST /api/role - 创建角色
|
|
||||||
- PUT /api/role/{id} - 更新角色
|
|
||||||
- DELETE /api/role/{id} - 删除角色
|
|
||||||
- PUT /api/role/{id}/menus - 分配菜单权限
|
|
||||||
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
|
||||||
- [ ] 6.3 编写角色 API 测试
|
|
||||||
- **Property 9: 角色编码唯一性验证**
|
|
||||||
- **Validates: Requirements 4.2**
|
|
||||||
|
|
||||||
- [x] 7. 菜单控制器 (MenuController)
|
|
||||||
- [x] 7.1 创建 MenuService 服务
|
|
||||||
- 实现菜单树构建、角色过滤逻辑
|
|
||||||
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
|
||||||
- [x] 7.2 创建 MenuController
|
|
||||||
- GET /api/v3/system/menus/simple - 获取用户菜单
|
|
||||||
- GET /api/menu/list - 获取所有菜单
|
|
||||||
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
|
||||||
- [x] 7.3 编写菜单 API 测试 (跳过,已手动验证)
|
|
||||||
- **Property 10: 菜单角色过滤**
|
|
||||||
- **Validates: Requirements 5.1, 5.4**
|
|
||||||
|
|
||||||
- [x] 8. 初始化数据
|
|
||||||
- [x] 8.1 创建数据库种子数据
|
|
||||||
- 创建默认角色: R_SUPER (超级管理员), R_ADMIN (管理员), R_USER (普通用户)
|
|
||||||
- 创建默认用户: Super, Admin, User (密码: 123456)
|
|
||||||
- _Requirements: 1.1_
|
|
||||||
- [x] 8.2 创建 AMT Scanner 菜单数据
|
|
||||||
- 设备管理、网络扫描、凭据管理、远程桌面、电源控制菜单
|
|
||||||
- _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_
|
|
||||||
- [x] 8.3 创建系统管理菜单数据
|
|
||||||
- 用户管理、角色管理、菜单管理
|
|
||||||
- _Requirements: 5.2, 5.3_
|
|
||||||
|
|
||||||
- [ ] 9. Checkpoint - 确保所有测试通过
|
|
||||||
- 确保所有测试通过,如有问题请询问用户
|
|
||||||
|
|
||||||
- [x] 10. 前端配置更新
|
|
||||||
- [x] 10.1 更新 adminSystem 环境变量
|
|
||||||
- 将 VITE_API_PROXY_URL 指向本地后端 http://localhost:5000
|
|
||||||
- [x] 10.2 修改 HTTP 请求工具添加 Bearer 前缀
|
|
||||||
- [ ] 10.3 验证前端登录功能
|
|
||||||
- 测试登录、获取用户信息、获取菜单
|
|
||||||
|
|
||||||
- [ ] 11. Final Checkpoint - 完整功能验证
|
|
||||||
- 确保所有功能正常工作,如有问题请询问用户
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- All tasks including tests are required for comprehensive coverage
|
|
||||||
- Each task references specific requirements for traceability
|
|
||||||
- Checkpoints ensure incremental validation
|
|
||||||
- Property tests validate universal correctness properties
|
|
||||||
- Unit tests validate specific examples and edge cases
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
# 【开发】环境变量
|
|
||||||
|
|
||||||
# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
|
|
||||||
VITE_BASE_URL = /
|
|
||||||
|
|
||||||
# API 请求基础路径(开发环境设置为 / 使用代理,生产环境设置为完整后端地址)
|
|
||||||
VITE_API_URL = /
|
|
||||||
|
|
||||||
# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
|
|
||||||
VITE_API_PROXY_URL = http://localhost:5000
|
|
||||||
|
|
||||||
# Delete console
|
|
||||||
VITE_DROP_CONSOLE = false
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
# 【生产】环境变量
|
|
||||||
|
|
||||||
# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
|
|
||||||
VITE_BASE_URL = /
|
|
||||||
|
|
||||||
# API 地址前缀
|
|
||||||
VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
|
|
||||||
|
|
||||||
# Delete console
|
|
||||||
VITE_DROP_CONSOLE = true
|
|
||||||
2
adminSystem/.gitattributes
vendored
@ -1,2 +0,0 @@
|
|||||||
*.html linguist-detectable=false
|
|
||||||
*.vue linguist-detectable=true
|
|
||||||
11
adminSystem/.gitignore
vendored
@ -1,11 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
.cursorrules
|
|
||||||
|
|
||||||
# Auto-generated files
|
|
||||||
src/types/import/auto-imports.d.ts
|
|
||||||
src/types/import/components.d.ts
|
|
||||||
.auto-import.json
|
|
||||||
@ -1 +0,0 @@
|
|||||||
pnpm dlx commitlint --edit $1
|
|
||||||
@ -1 +0,0 @@
|
|||||||
pnpm run lint:lint-staged
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
/node_modules/*
|
|
||||||
/dist/*
|
|
||||||
/src/main.ts
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"semi": false,
|
|
||||||
"vueIndentScriptAndStyle": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"quoteProps": "as-needed",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"bracketSameLine": false,
|
|
||||||
"jsxSingleQuote": false,
|
|
||||||
"arrowParens": "always",
|
|
||||||
"insertPragma": false,
|
|
||||||
"requirePragma": false,
|
|
||||||
"proseWrap": "never",
|
|
||||||
"htmlWhitespaceSensitivity": "strict",
|
|
||||||
"endOfLine": "auto",
|
|
||||||
"rangeStart": 0
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
dist
|
|
||||||
node_modules
|
|
||||||
public
|
|
||||||
.husky
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
src/components/Layout/MenuLeft/index.vue
|
|
||||||
src/assets
|
|
||||||
stats.html
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
// 继承推荐规范配置
|
|
||||||
extends: [
|
|
||||||
'stylelint-config-standard',
|
|
||||||
'stylelint-config-recommended-scss',
|
|
||||||
'stylelint-config-recommended-vue/scss',
|
|
||||||
'stylelint-config-html/vue',
|
|
||||||
'stylelint-config-recess-order'
|
|
||||||
],
|
|
||||||
// 指定不同文件对应的解析器
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['**/*.{vue,html}'],
|
|
||||||
customSyntax: 'postcss-html'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.{css,scss}'],
|
|
||||||
customSyntax: 'postcss-scss'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// 自定义规则
|
|
||||||
rules: {
|
|
||||||
'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url")
|
|
||||||
'selector-class-pattern': null, // 选择器类名命名规则
|
|
||||||
'custom-property-pattern': null, // 自定义属性命名规则
|
|
||||||
'keyframes-name-pattern': null, // 动画帧节点样式命名规则
|
|
||||||
'no-descending-specificity': null, // 允许无降序特异性
|
|
||||||
'no-empty-source': null, // 允许空样式
|
|
||||||
'property-no-vendor-prefix': null, // 允许属性前缀
|
|
||||||
// 允许 global 、export 、deep伪类
|
|
||||||
'selector-pseudo-class-no-unknown': [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
ignorePseudoClasses: ['global', 'export', 'deep']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// 允许未知属性
|
|
||||||
'property-no-unknown': [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
ignoreProperties: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// 允许未知规则
|
|
||||||
'at-rule-no-unknown': [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
ignoreAtRules: [
|
|
||||||
'apply',
|
|
||||||
'use',
|
|
||||||
'mixin',
|
|
||||||
'include',
|
|
||||||
'extend',
|
|
||||||
'each',
|
|
||||||
'if',
|
|
||||||
'else',
|
|
||||||
'for',
|
|
||||||
'while',
|
|
||||||
'reference'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'scss/at-rule-no-unknown': [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
ignoreAtRules: [
|
|
||||||
'apply',
|
|
||||||
'use',
|
|
||||||
'mixin',
|
|
||||||
'include',
|
|
||||||
'extend',
|
|
||||||
'each',
|
|
||||||
'if',
|
|
||||||
'else',
|
|
||||||
'for',
|
|
||||||
'while',
|
|
||||||
'reference'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 SuperManTT
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
/**
|
|
||||||
* commitlint 配置文件
|
|
||||||
* 文档
|
|
||||||
* https://commitlint.js.org/#/reference-rules
|
|
||||||
* https://cz-git.qbb.sh/zh/guide/
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
// 继承的规则
|
|
||||||
extends: ['@commitlint/config-conventional'],
|
|
||||||
// 自定义规则
|
|
||||||
rules: {
|
|
||||||
// 提交类型枚举,git提交type必须是以下类型
|
|
||||||
'type-enum': [
|
|
||||||
2,
|
|
||||||
'always',
|
|
||||||
[
|
|
||||||
'feat', // 新增功能
|
|
||||||
'fix', // 修复缺陷
|
|
||||||
'docs', // 文档变更
|
|
||||||
'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
|
|
||||||
'refactor', // 代码重构(不包括 bug 修复、功能新增)
|
|
||||||
'perf', // 性能优化
|
|
||||||
'test', // 添加疏漏测试或已有测试改动
|
|
||||||
'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
|
|
||||||
'ci', // 修改 CI 配置、脚本
|
|
||||||
'revert', // 回滚 commit
|
|
||||||
'chore', // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
|
|
||||||
'wip' // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'subject-case': [0] // subject大小写不做校验
|
|
||||||
},
|
|
||||||
|
|
||||||
prompt: {
|
|
||||||
messages: {
|
|
||||||
type: '选择你要提交的类型 :',
|
|
||||||
scope: '选择一个提交范围(可选):',
|
|
||||||
customScope: '请输入自定义的提交范围 :',
|
|
||||||
subject: '填写简短精炼的变更描述 :\n',
|
|
||||||
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
|
|
||||||
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
|
|
||||||
footerPrefixesSelect: '选择关联issue前缀(可选):',
|
|
||||||
customFooterPrefix: '输入自定义issue前缀 :',
|
|
||||||
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
|
|
||||||
generatingByAI: '正在通过 AI 生成你的提交简短描述...',
|
|
||||||
generatedSelectByAI: '选择一个 AI 生成的简短描述:',
|
|
||||||
confirmCommit: '是否提交或修改commit ?'
|
|
||||||
},
|
|
||||||
// prettier-ignore
|
|
||||||
types: [
|
|
||||||
{ value: "feat", name: "feat: 新增功能" },
|
|
||||||
{ value: "fix", name: "fix: 修复缺陷" },
|
|
||||||
{ value: "docs", name: "docs: 文档变更" },
|
|
||||||
{ value: "style", name: "style: 代码格式(不影响功能,例如空格、分号等格式修正)" },
|
|
||||||
{ value: "refactor", name: "refactor: 代码重构(不包括 bug 修复、功能新增)" },
|
|
||||||
{ value: "perf", name: "perf: 性能优化" },
|
|
||||||
{ value: "test", name: "test: 添加疏漏测试或已有测试改动" },
|
|
||||||
{ value: "build", name: "build: 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)" },
|
|
||||||
{ value: "ci", name: "ci: 修改 CI 配置、脚本" },
|
|
||||||
{ value: "revert", name: "revert: 回滚 commit" },
|
|
||||||
{ value: "chore", name: "chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)" },
|
|
||||||
],
|
|
||||||
useEmoji: true,
|
|
||||||
emojiAlign: 'center',
|
|
||||||
useAI: false,
|
|
||||||
aiNumber: 1,
|
|
||||||
themeColorCode: '',
|
|
||||||
scopes: [],
|
|
||||||
allowCustomScopes: true,
|
|
||||||
allowEmptyScopes: true,
|
|
||||||
customScopesAlign: 'bottom',
|
|
||||||
customScopesAlias: 'custom',
|
|
||||||
emptyScopesAlias: 'empty',
|
|
||||||
upperCaseSubject: false,
|
|
||||||
markBreakingChangeMode: false,
|
|
||||||
allowBreakingChanges: ['feat', 'fix'],
|
|
||||||
breaklineNumber: 100,
|
|
||||||
breaklineChar: '|',
|
|
||||||
skipQuestions: ['breaking', 'footerPrefix', 'footer'], // 跳过的步骤
|
|
||||||
issuePrefixes: [{ value: 'closed', name: 'closed: ISSUES has been processed' }],
|
|
||||||
customIssuePrefixAlign: 'top',
|
|
||||||
emptyIssuePrefixAlias: 'skip',
|
|
||||||
customIssuePrefixAlias: 'custom',
|
|
||||||
allowCustomIssuePrefix: true,
|
|
||||||
allowEmptyIssuePrefix: true,
|
|
||||||
confirmColorize: true,
|
|
||||||
maxHeaderLength: Infinity,
|
|
||||||
maxSubjectLength: Infinity,
|
|
||||||
minSubjectLength: 0,
|
|
||||||
scopeOverrides: undefined,
|
|
||||||
defaultBody: '',
|
|
||||||
defaultIssues: '',
|
|
||||||
defaultScope: '',
|
|
||||||
defaultSubject: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
// 从 URL 和路径模块中导入必要的功能
|
|
||||||
import fs from 'fs'
|
|
||||||
import path, { dirname } from 'path'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
|
|
||||||
// 从 ESLint 插件中导入推荐配置
|
|
||||||
import pluginJs from '@eslint/js'
|
|
||||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
|
||||||
import pluginVue from 'eslint-plugin-vue'
|
|
||||||
import globals from 'globals'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
|
|
||||||
// 使用 import.meta.url 获取当前模块的路径
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = dirname(__filename)
|
|
||||||
|
|
||||||
// 读取 .auto-import.json 文件的内容,并将其解析为 JSON 对象
|
|
||||||
const autoImportConfig = JSON.parse(
|
|
||||||
fs.readFileSync(path.resolve(__dirname, '.auto-import.json'), 'utf-8')
|
|
||||||
)
|
|
||||||
|
|
||||||
export default [
|
|
||||||
// 指定文件匹配规则
|
|
||||||
{
|
|
||||||
files: ['**/*.{js,mjs,cjs,ts,vue}']
|
|
||||||
},
|
|
||||||
// 指定全局变量和环境
|
|
||||||
{
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
...globals.browser,
|
|
||||||
...globals.node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 扩展配置
|
|
||||||
pluginJs.configs.recommended,
|
|
||||||
...tseslint.configs.recommended,
|
|
||||||
...pluginVue.configs['flat/essential'],
|
|
||||||
// 自定义规则
|
|
||||||
{
|
|
||||||
// 针对所有 JavaScript、TypeScript 和 Vue 文件应用以下配置
|
|
||||||
files: ['**/*.{js,mjs,cjs,ts,vue}'],
|
|
||||||
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
// 合并从 autoImportConfig 中读取的全局变量配置
|
|
||||||
...autoImportConfig.globals,
|
|
||||||
// TypeScript 全局命名空间
|
|
||||||
Api: 'readonly'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
quotes: ['error', 'single'], // 使用单引号
|
|
||||||
semi: ['error', 'never'], // 语句末尾不加分号
|
|
||||||
'no-var': 'error', // 要求使用 let 或 const 而不是 var
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off', // 禁用 any 检查
|
|
||||||
'vue/multi-word-component-names': 'off', // 禁用对 Vue 组件名称的多词要求检查
|
|
||||||
'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
|
|
||||||
'no-unexpected-multiline': 'error' // 禁止空余的多行
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// vue 规则
|
|
||||||
{
|
|
||||||
files: ['**/*.vue'],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: { parser: tseslint.parser }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 忽略文件
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
'node_modules',
|
|
||||||
'dist',
|
|
||||||
'public',
|
|
||||||
'.vscode/**',
|
|
||||||
'src/assets/**',
|
|
||||||
'src/utils/console.ts'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
// prettier 配置
|
|
||||||
eslintPluginPrettierRecommended
|
|
||||||
]
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>工大智能机房管控系统</title>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="工大智能机房管控系统 - 基于 Intel AMT 技术的智能机房远程管理平台"
|
|
||||||
/>
|
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="src/assets/images/favicon.ico" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 防止页面刷新时白屏的初始样式 */
|
|
||||||
html {
|
|
||||||
background-color: #fafbfc;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark {
|
|
||||||
background-color: #070707;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 初始化 html class 主题属性
|
|
||||||
;(function () {
|
|
||||||
try {
|
|
||||||
if (typeof Storage === 'undefined' || !window.localStorage) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeType = localStorage.getItem('sys-theme')
|
|
||||||
if (themeType === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to apply initial theme:', e)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "art-design-pro",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.19.0",
|
|
||||||
"pnpm": ">=8.8.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite --open",
|
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
|
||||||
"serve": "vite preview",
|
|
||||||
"lint": "eslint",
|
|
||||||
"fix": "eslint --fix",
|
|
||||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
|
||||||
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix",
|
|
||||||
"lint:lint-staged": "lint-staged",
|
|
||||||
"prepare": "husky",
|
|
||||||
"commit": "git-cz",
|
|
||||||
"clean:dev": "tsx scripts/clean-dev.ts"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"commitizen": {
|
|
||||||
"path": "node_modules/cz-git"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.{js,ts,mjs,mts,tsx}": [
|
|
||||||
"eslint --fix",
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.{cjs,json,jsonc}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.vue": [
|
|
||||||
"eslint --fix",
|
|
||||||
"stylelint --fix --allow-empty-input",
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.{html,htm}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.{scss,css,less}": [
|
|
||||||
"stylelint --fix --allow-empty-input",
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.{md,mdx}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.{yaml,yml}": [
|
|
||||||
"prettier --write"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
|
||||||
"@iconify/vue": "^5.0.0",
|
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
|
||||||
"@vue/reactivity": "^3.5.21",
|
|
||||||
"@vueuse/core": "^13.9.0",
|
|
||||||
"@wangeditor/editor": "^5.1.23",
|
|
||||||
"@wangeditor/editor-for-vue": "next",
|
|
||||||
"axios": "^1.12.2",
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"echarts": "^6.0.0",
|
|
||||||
"element-plus": "^2.11.2",
|
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"highlight.js": "^11.10.0",
|
|
||||||
"mitt": "^3.0.1",
|
|
||||||
"nprogress": "^0.2.0",
|
|
||||||
"ohash": "^2.0.11",
|
|
||||||
"pinia": "^3.0.3",
|
|
||||||
"pinia-plugin-persistedstate": "^4.3.0",
|
|
||||||
"qrcode.vue": "^3.6.0",
|
|
||||||
"tailwindcss": "^4.1.14",
|
|
||||||
"vue": "^3.5.21",
|
|
||||||
"vue-draggable-plus": "^0.6.0",
|
|
||||||
"vue-i18n": "^9.14.0",
|
|
||||||
"vue-router": "^4.5.1",
|
|
||||||
"xgplayer": "^3.0.20",
|
|
||||||
"xlsx": "^0.18.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@commitlint/cli": "^19.4.1",
|
|
||||||
"@commitlint/config-conventional": "^19.4.1",
|
|
||||||
"@eslint/js": "^9.9.1",
|
|
||||||
"@types/node": "^24.0.5",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
|
||||||
"@typescript-eslint/parser": "^8.3.0",
|
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
|
||||||
"@vue/compiler-sfc": "^3.0.5",
|
|
||||||
"commitizen": "^4.3.0",
|
|
||||||
"cz-git": "^1.11.1",
|
|
||||||
"eslint": "^9.9.1",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
|
||||||
"globals": "^15.9.0",
|
|
||||||
"husky": "^9.1.5",
|
|
||||||
"lint-staged": "^15.5.2",
|
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
|
||||||
"sass": "^1.81.0",
|
|
||||||
"stylelint": "^16.20.0",
|
|
||||||
"stylelint-config-html": "^1.1.0",
|
|
||||||
"stylelint-config-recess-order": "^4.6.0",
|
|
||||||
"stylelint-config-recommended-scss": "^14.1.0",
|
|
||||||
"stylelint-config-recommended-vue": "^1.5.0",
|
|
||||||
"stylelint-config-standard": "^36.0.1",
|
|
||||||
"terser": "^5.36.0",
|
|
||||||
"tsx": "^4.20.3",
|
|
||||||
"typescript": "~5.6.3",
|
|
||||||
"typescript-eslint": "^8.9.0",
|
|
||||||
"unplugin-auto-import": "^20.2.0",
|
|
||||||
"unplugin-element-plus": "^0.10.0",
|
|
||||||
"unplugin-vue-components": "^29.1.0",
|
|
||||||
"vite": "^7.1.5",
|
|
||||||
"vite-plugin-compression": "^0.5.1",
|
|
||||||
"vite-plugin-vue-devtools": "^7.7.6",
|
|
||||||
"vue-demi": "^0.14.9",
|
|
||||||
"vue-img-cutter": "^3.0.5",
|
|
||||||
"vue-tsc": "~2.1.6"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10109
adminSystem/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 4.2 KiB |
@ -1,838 +0,0 @@
|
|||||||
// scripts/clean-dev.ts
|
|
||||||
import fs from 'fs/promises'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
// 现代化颜色主题
|
|
||||||
const theme = {
|
|
||||||
// 基础颜色
|
|
||||||
reset: '\x1b[0m',
|
|
||||||
bold: '\x1b[1m',
|
|
||||||
dim: '\x1b[2m',
|
|
||||||
|
|
||||||
// 前景色
|
|
||||||
primary: '\x1b[38;5;75m', // 亮蓝色
|
|
||||||
success: '\x1b[38;5;82m', // 亮绿色
|
|
||||||
warning: '\x1b[38;5;220m', // 亮黄色
|
|
||||||
error: '\x1b[38;5;196m', // 亮红色
|
|
||||||
info: '\x1b[38;5;159m', // 青色
|
|
||||||
purple: '\x1b[38;5;141m', // 紫色
|
|
||||||
orange: '\x1b[38;5;208m', // 橙色
|
|
||||||
gray: '\x1b[38;5;245m', // 灰色
|
|
||||||
white: '\x1b[38;5;255m', // 白色
|
|
||||||
|
|
||||||
// 背景色
|
|
||||||
bgDark: '\x1b[48;5;235m', // 深灰背景
|
|
||||||
bgBlue: '\x1b[48;5;24m', // 蓝色背景
|
|
||||||
bgGreen: '\x1b[48;5;22m', // 绿色背景
|
|
||||||
bgRed: '\x1b[48;5;52m' // 红色背景
|
|
||||||
}
|
|
||||||
|
|
||||||
// 现代化图标集
|
|
||||||
const icons = {
|
|
||||||
rocket: '🚀',
|
|
||||||
fire: '🔥',
|
|
||||||
star: '⭐',
|
|
||||||
gem: '💎',
|
|
||||||
crown: '👑',
|
|
||||||
magic: '✨',
|
|
||||||
warning: '⚠️',
|
|
||||||
success: '✅',
|
|
||||||
error: '❌',
|
|
||||||
info: 'ℹ️',
|
|
||||||
folder: '📁',
|
|
||||||
file: '📄',
|
|
||||||
image: '🖼️',
|
|
||||||
code: '💻',
|
|
||||||
data: '📊',
|
|
||||||
globe: '🌐',
|
|
||||||
map: '🗺️',
|
|
||||||
chat: '💬',
|
|
||||||
bolt: '⚡',
|
|
||||||
shield: '🛡️',
|
|
||||||
key: '🔑',
|
|
||||||
link: '🔗',
|
|
||||||
clean: '🧹',
|
|
||||||
trash: '🗑️',
|
|
||||||
check: '✓',
|
|
||||||
cross: '✗',
|
|
||||||
arrow: '→',
|
|
||||||
loading: '⏳'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化工具
|
|
||||||
const fmt = {
|
|
||||||
title: (text: string) => `${theme.bold}${theme.primary}${text}${theme.reset}`,
|
|
||||||
subtitle: (text: string) => `${theme.purple}${text}${theme.reset}`,
|
|
||||||
success: (text: string) => `${theme.success}${text}${theme.reset}`,
|
|
||||||
error: (text: string) => `${theme.error}${text}${theme.reset}`,
|
|
||||||
warning: (text: string) => `${theme.warning}${text}${theme.reset}`,
|
|
||||||
info: (text: string) => `${theme.info}${text}${theme.reset}`,
|
|
||||||
highlight: (text: string) => `${theme.bold}${theme.white}${text}${theme.reset}`,
|
|
||||||
dim: (text: string) => `${theme.dim}${theme.gray}${text}${theme.reset}`,
|
|
||||||
orange: (text: string) => `${theme.orange}${text}${theme.reset}`,
|
|
||||||
|
|
||||||
// 带背景的文本
|
|
||||||
badge: (text: string, bg: string = theme.bgBlue) =>
|
|
||||||
`${bg}${theme.white}${theme.bold} ${text} ${theme.reset}`,
|
|
||||||
|
|
||||||
// 渐变效果模拟
|
|
||||||
gradient: (text: string) => {
|
|
||||||
const colors = ['\x1b[38;5;75m', '\x1b[38;5;81m', '\x1b[38;5;87m', '\x1b[38;5;159m']
|
|
||||||
const chars = text.split('')
|
|
||||||
return chars.map((char, i) => `${colors[i % colors.length]}${char}`).join('') + theme.reset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建现代化标题横幅
|
|
||||||
function createModernBanner() {
|
|
||||||
console.log()
|
|
||||||
console.log(
|
|
||||||
fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗')
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
fmt.gradient(' ║ ║')
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
` ║ ${icons.rocket} ${fmt.title('ART DESIGN PRO')} ${fmt.subtitle('· 代码精简程序')} ${icons.magic} ║`
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
` ║ ${fmt.dim('为项目移除演示数据,快速切换至开发模式')} ║`
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
fmt.gradient(' ║ ║')
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝')
|
|
||||||
)
|
|
||||||
console.log()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建分割线
|
|
||||||
function createDivider(char = '─', color = theme.primary) {
|
|
||||||
console.log(`${color}${' ' + char.repeat(66)}${theme.reset}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建卡片样式容器
|
|
||||||
function createCard(title: string, content: string[]) {
|
|
||||||
console.log(` ${fmt.badge('', theme.bgBlue)} ${fmt.title(title)}`)
|
|
||||||
console.log()
|
|
||||||
content.forEach((line) => {
|
|
||||||
console.log(` ${line}`)
|
|
||||||
})
|
|
||||||
console.log()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 进度条动画
|
|
||||||
function createProgressBar(current: number, total: number, text: string, width = 40) {
|
|
||||||
const percentage = Math.round((current / total) * 100)
|
|
||||||
const filled = Math.round((current / total) * width)
|
|
||||||
const empty = width - filled
|
|
||||||
|
|
||||||
const filledBar = '█'.repeat(filled)
|
|
||||||
const emptyBar = '░'.repeat(empty)
|
|
||||||
|
|
||||||
process.stdout.write(
|
|
||||||
`\r ${fmt.info('进度')} [${theme.success}${filledBar}${theme.gray}${emptyBar}${theme.reset}] ${fmt.highlight(percentage + '%')})}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (current === total) {
|
|
||||||
console.log()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计信息
|
|
||||||
const stats = {
|
|
||||||
deletedFiles: 0,
|
|
||||||
deletedPaths: 0,
|
|
||||||
failedPaths: 0,
|
|
||||||
startTime: Date.now(),
|
|
||||||
totalFiles: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理目标
|
|
||||||
const targets = [
|
|
||||||
'README.md',
|
|
||||||
'README.zh-CN.md',
|
|
||||||
'CHANGELOG.md',
|
|
||||||
'CHANGELOG.zh-CN.md',
|
|
||||||
'src/views/change',
|
|
||||||
'src/views/safeguard',
|
|
||||||
'src/views/article',
|
|
||||||
'src/views/examples',
|
|
||||||
'src/views/system/nested',
|
|
||||||
'src/views/widgets',
|
|
||||||
'src/views/template',
|
|
||||||
'src/views/dashboard/analysis',
|
|
||||||
'src/views/dashboard/ecommerce',
|
|
||||||
'src/mock/json',
|
|
||||||
'src/mock/temp/articleList.ts',
|
|
||||||
'src/mock/temp/commentDetail.ts',
|
|
||||||
'src/mock/temp/commentList.ts',
|
|
||||||
'src/assets/images/cover',
|
|
||||||
'src/assets/images/safeguard',
|
|
||||||
'src/assets/images/3d',
|
|
||||||
'src/components/core/charts/art-map-chart',
|
|
||||||
'src/components/business/comment-widget'
|
|
||||||
]
|
|
||||||
|
|
||||||
// 递归统计文件数量
|
|
||||||
async function countFiles(targetPath: string): Promise<number> {
|
|
||||||
const fullPath = path.resolve(process.cwd(), targetPath)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stat = await fs.stat(fullPath)
|
|
||||||
|
|
||||||
if (stat.isFile()) {
|
|
||||||
return 1
|
|
||||||
} else if (stat.isDirectory()) {
|
|
||||||
const entries = await fs.readdir(fullPath)
|
|
||||||
let count = 0
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const entryPath = path.join(targetPath, entry)
|
|
||||||
count += await countFiles(entryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计所有目标的文件数量
|
|
||||||
async function countAllFiles(): Promise<number> {
|
|
||||||
let totalCount = 0
|
|
||||||
|
|
||||||
for (const target of targets) {
|
|
||||||
const count = await countFiles(target)
|
|
||||||
totalCount += count
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalCount
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除文件和目录
|
|
||||||
async function remove(targetPath: string, index: number) {
|
|
||||||
const fullPath = path.resolve(process.cwd(), targetPath)
|
|
||||||
|
|
||||||
createProgressBar(index + 1, targets.length, targetPath)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileCount = await countFiles(targetPath)
|
|
||||||
await fs.rm(fullPath, { recursive: true, force: true })
|
|
||||||
stats.deletedFiles += fileCount
|
|
||||||
stats.deletedPaths++
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
||||||
} catch (err) {
|
|
||||||
stats.failedPaths++
|
|
||||||
console.log()
|
|
||||||
console.log(` ${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(targetPath)}`)
|
|
||||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理路由模块
|
|
||||||
async function cleanRouteModules() {
|
|
||||||
const modulesPath = path.resolve(process.cwd(), 'src/router/modules')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 删除演示相关的路由模块
|
|
||||||
const modulesToRemove = [
|
|
||||||
'template.ts',
|
|
||||||
'widgets.ts',
|
|
||||||
'examples.ts',
|
|
||||||
'article.ts',
|
|
||||||
'safeguard.ts',
|
|
||||||
'help.ts'
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const module of modulesToRemove) {
|
|
||||||
const modulePath = path.join(modulesPath, module)
|
|
||||||
try {
|
|
||||||
await fs.rm(modulePath, { force: true })
|
|
||||||
} catch {
|
|
||||||
// 文件不存在时忽略错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重写 dashboard.ts - 只保留 console
|
|
||||||
const dashboardContent = `import { AppRouteRecord } from '@/types/router'
|
|
||||||
|
|
||||||
export const dashboardRoutes: AppRouteRecord = {
|
|
||||||
name: 'Dashboard',
|
|
||||||
path: '/dashboard',
|
|
||||||
component: '/index/index',
|
|
||||||
meta: {
|
|
||||||
title: 'menus.dashboard.title',
|
|
||||||
icon: 'ri:pie-chart-line',
|
|
||||||
roles: ['R_SUPER', 'R_ADMIN']
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'console',
|
|
||||||
name: 'Console',
|
|
||||||
component: '/dashboard/console',
|
|
||||||
meta: {
|
|
||||||
title: 'menus.dashboard.console',
|
|
||||||
keepAlive: false,
|
|
||||||
fixedTab: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`
|
|
||||||
await fs.writeFile(path.join(modulesPath, 'dashboard.ts'), dashboardContent, 'utf-8')
|
|
||||||
|
|
||||||
// 重写 system.ts - 移除 nested 嵌套菜单
|
|
||||||
const systemContent = `import { AppRouteRecord } from '@/types/router'
|
|
||||||
|
|
||||||
export const systemRoutes: AppRouteRecord = {
|
|
||||||
path: '/system',
|
|
||||||
name: 'System',
|
|
||||||
component: '/index/index',
|
|
||||||
meta: {
|
|
||||||
title: 'menus.system.title',
|
|
||||||
icon: 'ri:user-3-line',
|
|
||||||
roles: ['R_SUPER', 'R_ADMIN']
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'user',
|
|
||||||
name: 'User',
|
|
||||||
component: '/system/user',
|
|
||||||
meta: {
|
|
||||||
title: 'menus.system.user',
|
|
||||||
keepAlive: true,
|
|
||||||
roles: ['R_SUPER', 'R_ADMIN']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'role',
|
|
||||||
name: 'Role',
|
|
||||||
component: '/system/role',
|
|
||||||
meta: {
|
|
||||||
title: 'menus.system.role',
|
|
||||||
keepAlive: true,
|
|
||||||
roles: ['R_SUPER']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'user-center',
|
|
||||||
name: 'UserCenter',
|
|
||||||
component: '/system/user-center',
|
|
||||||
meta: {
|
|
||||||
title: 'menus.system.userCenter',
|
|
||||||
isHide: true,
|
|
||||||
keepAlive: true,
|
|
||||||
isHideTab: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'menu',
|
|
||||||
name: 'Menus',
|
|
||||||
component: '/system/menu',
|
|
||||||
meta: {
|
|
||||||
title: 'menus.system.menu',
|
|
||||||
keepAlive: true,
|
|
||||||
roles: ['R_SUPER'],
|
|
||||||
authList: [
|
|
||||||
{ title: '新增', authMark: 'add' },
|
|
||||||
{ title: '编辑', authMark: 'edit' },
|
|
||||||
{ title: '删除', authMark: 'delete' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`
|
|
||||||
await fs.writeFile(path.join(modulesPath, 'system.ts'), systemContent, 'utf-8')
|
|
||||||
|
|
||||||
// 重写 index.ts - 只导入保留的模块
|
|
||||||
const indexContent = `import { AppRouteRecord } from '@/types/router'
|
|
||||||
import { dashboardRoutes } from './dashboard'
|
|
||||||
import { systemRoutes } from './system'
|
|
||||||
import { resultRoutes } from './result'
|
|
||||||
import { exceptionRoutes } from './exception'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出所有模块化路由
|
|
||||||
*/
|
|
||||||
export const routeModules: AppRouteRecord[] = [
|
|
||||||
dashboardRoutes,
|
|
||||||
systemRoutes,
|
|
||||||
resultRoutes,
|
|
||||||
exceptionRoutes
|
|
||||||
]
|
|
||||||
`
|
|
||||||
await fs.writeFile(path.join(modulesPath, 'index.ts'), indexContent, 'utf-8')
|
|
||||||
|
|
||||||
console.log(` ${icons.success} ${fmt.success('清理路由模块完成')}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(` ${icons.error} ${fmt.error('清理路由模块失败')}`)
|
|
||||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理路由别名
|
|
||||||
async function cleanRoutesAlias() {
|
|
||||||
const routesAliasPath = path.resolve(process.cwd(), 'src/router/routesAlias.ts')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cleanedAlias = `/**
|
|
||||||
* 公共路由别名
|
|
||||||
# 存放系统级公共路由路径,如布局容器、登录页等
|
|
||||||
*/
|
|
||||||
export enum RoutesAlias {
|
|
||||||
Layout = '/index/index', // 布局容器
|
|
||||||
Login = '/auth/login' // 登录页
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
await fs.writeFile(routesAliasPath, cleanedAlias, 'utf-8')
|
|
||||||
console.log(` ${icons.success} ${fmt.success('重写路由别名配置完成')}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(` ${icons.error} ${fmt.error('清理路由别名失败')}`)
|
|
||||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理变更日志
|
|
||||||
async function cleanChangeLog() {
|
|
||||||
const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cleanedChangeLog = `import { ref } from 'vue'
|
|
||||||
|
|
||||||
interface UpgradeLog {
|
|
||||||
version: string // 版本号
|
|
||||||
title: string // 更新标题
|
|
||||||
date: string // 更新日期
|
|
||||||
detail?: string[] // 更新内容
|
|
||||||
requireReLogin?: boolean // 是否需要重新登录
|
|
||||||
remark?: string // 备注
|
|
||||||
}
|
|
||||||
|
|
||||||
export const upgradeLogList = ref<UpgradeLog[]>([])
|
|
||||||
`
|
|
||||||
|
|
||||||
await fs.writeFile(changeLogPath, cleanedChangeLog, 'utf-8')
|
|
||||||
console.log(` ${icons.success} ${fmt.success('清空变更日志数据完成')}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(` ${icons.error} ${fmt.error('清理变更日志失败')}`)
|
|
||||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理语言文件
|
|
||||||
async function cleanLanguageFiles() {
|
|
||||||
const languageFiles = [
|
|
||||||
{ path: 'src/locales/langs/zh.json', name: '中文语言文件' },
|
|
||||||
{ path: 'src/locales/langs/en.json', name: '英文语言文件' }
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const { path: langPath, name } of languageFiles) {
|
|
||||||
try {
|
|
||||||
const fullPath = path.resolve(process.cwd(), langPath)
|
|
||||||
const content = await fs.readFile(fullPath, 'utf-8')
|
|
||||||
const langData = JSON.parse(content)
|
|
||||||
|
|
||||||
const menusToRemove = [
|
|
||||||
'widgets',
|
|
||||||
'template',
|
|
||||||
'article',
|
|
||||||
'examples',
|
|
||||||
'safeguard',
|
|
||||||
'plan',
|
|
||||||
'help'
|
|
||||||
]
|
|
||||||
|
|
||||||
if (langData.menus) {
|
|
||||||
menusToRemove.forEach((menuKey) => {
|
|
||||||
if (langData.menus[menuKey]) {
|
|
||||||
delete langData.menus[menuKey]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (langData.menus.dashboard) {
|
|
||||||
if (langData.menus.dashboard.analysis) {
|
|
||||||
delete langData.menus.dashboard.analysis
|
|
||||||
}
|
|
||||||
if (langData.menus.dashboard.ecommerce) {
|
|
||||||
delete langData.menus.dashboard.ecommerce
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (langData.menus.system) {
|
|
||||||
const systemKeysToRemove = [
|
|
||||||
'nested',
|
|
||||||
'menu1',
|
|
||||||
'menu2',
|
|
||||||
'menu21',
|
|
||||||
'menu3',
|
|
||||||
'menu31',
|
|
||||||
'menu32',
|
|
||||||
'menu321'
|
|
||||||
]
|
|
||||||
systemKeysToRemove.forEach((key) => {
|
|
||||||
if (langData.menus.system[key]) {
|
|
||||||
delete langData.menus.system[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(fullPath, JSON.stringify(langData, null, 2), 'utf-8')
|
|
||||||
console.log(` ${icons.success} ${fmt.success(`清理${name}完成`)}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(` ${icons.error} ${fmt.error(`清理${name}失败`)}`)
|
|
||||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理快速入口组件
|
|
||||||
async function cleanFastEnterComponent() {
|
|
||||||
const fastEnterPath = path.resolve(process.cwd(), 'src/config/fastEnter.ts')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cleanedFastEnter = `/**
|
|
||||||
* 快速入口配置
|
|
||||||
* 包含:应用列表、快速链接等配置
|
|
||||||
*/
|
|
||||||
import { WEB_LINKS } from '@/utils/constants'
|
|
||||||
import type { FastEnterConfig } from '@/types/config'
|
|
||||||
|
|
||||||
const fastEnterConfig: FastEnterConfig = {
|
|
||||||
// 显示条件(屏幕宽度)
|
|
||||||
minWidth: 1200,
|
|
||||||
// 应用列表
|
|
||||||
applications: [
|
|
||||||
{
|
|
||||||
name: '工作台',
|
|
||||||
description: '系统概览与数据统计',
|
|
||||||
icon: 'ri:pie-chart-line',
|
|
||||||
iconColor: '#377dff',
|
|
||||||
enabled: true,
|
|
||||||
order: 1,
|
|
||||||
routeName: 'Console'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '官方文档',
|
|
||||||
description: '使用指南与开发文档',
|
|
||||||
icon: 'ri:bill-line',
|
|
||||||
iconColor: '#ffb100',
|
|
||||||
enabled: true,
|
|
||||||
order: 2,
|
|
||||||
link: WEB_LINKS.DOCS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '技术支持',
|
|
||||||
description: '技术支持与问题反馈',
|
|
||||||
icon: 'ri:user-location-line',
|
|
||||||
iconColor: '#ff6b6b',
|
|
||||||
enabled: true,
|
|
||||||
order: 3,
|
|
||||||
link: WEB_LINKS.COMMUNITY
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '哔哩哔哩',
|
|
||||||
description: '技术分享与交流',
|
|
||||||
icon: 'ri:bilibili-line',
|
|
||||||
iconColor: '#FB7299',
|
|
||||||
enabled: true,
|
|
||||||
order: 4,
|
|
||||||
link: WEB_LINKS.BILIBILI
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// 快速链接
|
|
||||||
quickLinks: [
|
|
||||||
{
|
|
||||||
name: '登录',
|
|
||||||
enabled: true,
|
|
||||||
order: 1,
|
|
||||||
routeName: 'Login'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '注册',
|
|
||||||
enabled: true,
|
|
||||||
order: 2,
|
|
||||||
routeName: 'Register'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '忘记密码',
|
|
||||||
enabled: true,
|
|
||||||
order: 3,
|
|
||||||
routeName: 'ForgetPassword'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '个人中心',
|
|
||||||
enabled: true,
|
|
||||||
order: 4,
|
|
||||||
routeName: 'UserCenter'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Object.freeze(fastEnterConfig)
|
|
||||||
`
|
|
||||||
|
|
||||||
await fs.writeFile(fastEnterPath, cleanedFastEnter, 'utf-8')
|
|
||||||
console.log(` ${icons.success} ${fmt.success('清理快速入口配置完成')}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(` ${icons.error} ${fmt.error('清理快速入口配置失败')}`)
|
|
||||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新菜单接口
|
|
||||||
async function updateMenuApi() {
|
|
||||||
const apiPath = path.resolve(process.cwd(), 'src/api/system-manage.ts')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(apiPath, 'utf-8')
|
|
||||||
const updatedContent = content.replace(
|
|
||||||
"url: '/api/v3/system/menus'",
|
|
||||||
"url: '/api/v3/system/menus/simple'"
|
|
||||||
)
|
|
||||||
|
|
||||||
await fs.writeFile(apiPath, updatedContent, 'utf-8')
|
|
||||||
console.log(` ${icons.success} ${fmt.success('更新菜单接口完成')}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(` ${icons.error} ${fmt.error('更新菜单接口失败')}`)
|
|
||||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户确认函数
|
|
||||||
async function getUserConfirmation(): Promise<boolean> {
|
|
||||||
const { createInterface } = await import('readline')
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const rl = createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
` ${fmt.highlight('请输入')} ${fmt.success('yes')} ${fmt.highlight('确认执行清理操作,或按 Enter 取消')}`
|
|
||||||
)
|
|
||||||
console.log()
|
|
||||||
process.stdout.write(` ${icons.arrow} `)
|
|
||||||
|
|
||||||
rl.question('', (answer: string) => {
|
|
||||||
rl.close()
|
|
||||||
resolve(answer.toLowerCase().trim() === 'yes')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示清理警告
|
|
||||||
async function showCleanupWarning() {
|
|
||||||
createCard('安全警告', [
|
|
||||||
`${fmt.warning('此操作将永久删除以下演示内容,且无法恢复!')}`,
|
|
||||||
`${fmt.dim('请仔细阅读清理列表,确认后再继续操作')}`
|
|
||||||
])
|
|
||||||
|
|
||||||
const cleanupItems = [
|
|
||||||
{
|
|
||||||
icon: icons.image,
|
|
||||||
name: '图片资源',
|
|
||||||
desc: '演示用的封面图片、3D图片、运维图片等',
|
|
||||||
color: theme.orange
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: icons.file,
|
|
||||||
name: '演示页面',
|
|
||||||
desc: 'widgets、template、article、examples、safeguard等页面',
|
|
||||||
color: theme.purple
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: icons.code,
|
|
||||||
name: '路由模块文件',
|
|
||||||
desc: '删除演示路由模块,只保留核心模块(dashboard、system、result、exception)',
|
|
||||||
color: theme.primary
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: icons.link,
|
|
||||||
name: '路由别名',
|
|
||||||
desc: '重写routesAlias.ts,移除演示路由别名',
|
|
||||||
color: theme.info
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: icons.data,
|
|
||||||
name: 'Mock数据',
|
|
||||||
desc: '演示用的JSON数据、文章列表、评论数据等',
|
|
||||||
color: theme.success
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: icons.globe,
|
|
||||||
name: '多语言文件',
|
|
||||||
desc: '清理中英文语言包中的演示菜单项',
|
|
||||||
color: theme.warning
|
|
||||||
},
|
|
||||||
{ icon: icons.map, name: '地图组件', desc: '移除art-map-chart地图组件', color: theme.error },
|
|
||||||
{ icon: icons.chat, name: '评论组件', desc: '移除comment-widget评论组件', color: theme.orange },
|
|
||||||
{
|
|
||||||
icon: icons.bolt,
|
|
||||||
name: '快速入口',
|
|
||||||
desc: '移除分析页、礼花效果、聊天、更新日志、定价、留言管理等无效项目',
|
|
||||||
color: theme.purple
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
console.log(` ${fmt.badge('', theme.bgRed)} ${fmt.title('将要清理的内容')}`)
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
cleanupItems.forEach((item, index) => {
|
|
||||||
console.log(` ${item.color}${theme.reset} ${fmt.highlight(`${index + 1}. ${item.name}`)}`)
|
|
||||||
console.log(` ${fmt.dim(item.desc)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log()
|
|
||||||
console.log(` ${fmt.badge('', theme.bgGreen)} ${fmt.title('保留的功能模块')}`)
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
const preservedModules = [
|
|
||||||
{ name: 'Dashboard', desc: '工作台页面' },
|
|
||||||
{ name: 'System', desc: '系统管理模块' },
|
|
||||||
{ name: 'Result', desc: '结果页面' },
|
|
||||||
{ name: 'Exception', desc: '异常页面' },
|
|
||||||
{ name: 'Auth', desc: '登录注册功能' },
|
|
||||||
{ name: 'Core Components', desc: '核心组件库' }
|
|
||||||
]
|
|
||||||
|
|
||||||
preservedModules.forEach((module) => {
|
|
||||||
console.log(` ${icons.check} ${fmt.success(module.name)} ${fmt.dim(`- ${module.desc}`)}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log()
|
|
||||||
createDivider()
|
|
||||||
console.log()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示统计信息
|
|
||||||
async function showStats() {
|
|
||||||
const duration = Date.now() - stats.startTime
|
|
||||||
const seconds = (duration / 1000).toFixed(2)
|
|
||||||
|
|
||||||
console.log()
|
|
||||||
createCard('清理统计', [
|
|
||||||
`${fmt.success('成功删除')}: ${fmt.highlight(stats.deletedFiles.toString())} 个文件`,
|
|
||||||
`${fmt.info('涉及路径')}: ${fmt.highlight(stats.deletedPaths.toString())} 个目录/文件`,
|
|
||||||
...(stats.failedPaths > 0
|
|
||||||
? [
|
|
||||||
`${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(stats.failedPaths.toString())} 个路径`
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
`${fmt.info('耗时')}: ${fmt.highlight(seconds)} 秒`
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建成功横幅
|
|
||||||
function createSuccessBanner() {
|
|
||||||
console.log()
|
|
||||||
console.log(
|
|
||||||
fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗')
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
fmt.gradient(' ║ ║')
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
` ║ ${icons.star} ${fmt.success('清理完成!项目已准备就绪')} ${icons.rocket} ║`
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
` ║ ${fmt.dim('现在可以开始您的开发之旅了!')} ║`
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
fmt.gradient(' ║ ║')
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝')
|
|
||||||
)
|
|
||||||
console.log()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主函数
|
|
||||||
async function main() {
|
|
||||||
// 清屏并显示横幅
|
|
||||||
console.clear()
|
|
||||||
createModernBanner()
|
|
||||||
|
|
||||||
// 显示清理警告
|
|
||||||
await showCleanupWarning()
|
|
||||||
|
|
||||||
// 统计文件数量
|
|
||||||
console.log(` ${fmt.info('正在统计文件数量...')}`)
|
|
||||||
stats.totalFiles = await countAllFiles()
|
|
||||||
|
|
||||||
console.log(` ${fmt.info('即将清理')}: ${fmt.highlight(stats.totalFiles.toString())} 个文件`)
|
|
||||||
console.log(` ${fmt.dim(`涉及 ${targets.length} 个目录/文件路径`)}`)
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
// 用户确认
|
|
||||||
const confirmed = await getUserConfirmation()
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
console.log(` ${fmt.warning('操作已取消,清理中止')}`)
|
|
||||||
console.log()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log()
|
|
||||||
console.log(` ${icons.check} ${fmt.success('确认成功,开始清理...')}`)
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
// 开始清理过程
|
|
||||||
console.log(` ${fmt.badge('步骤 1/6', theme.bgBlue)} ${fmt.title('删除演示文件')}`)
|
|
||||||
console.log()
|
|
||||||
for (let i = 0; i < targets.length; i++) {
|
|
||||||
await remove(targets[i], i)
|
|
||||||
}
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
console.log(` ${fmt.badge('步骤 2/6', theme.bgBlue)} ${fmt.title('清理路由模块')}`)
|
|
||||||
console.log()
|
|
||||||
await cleanRouteModules()
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
console.log(` ${fmt.badge('步骤 3/6', theme.bgBlue)} ${fmt.title('重写路由别名')}`)
|
|
||||||
console.log()
|
|
||||||
await cleanRoutesAlias()
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
console.log(` ${fmt.badge('步骤 4/6', theme.bgBlue)} ${fmt.title('清空变更日志')}`)
|
|
||||||
console.log()
|
|
||||||
await cleanChangeLog()
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
console.log(` ${fmt.badge('步骤 5/6', theme.bgBlue)} ${fmt.title('清理语言文件')}`)
|
|
||||||
console.log()
|
|
||||||
await cleanLanguageFiles()
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
console.log(` ${fmt.badge('步骤 6/7', theme.bgBlue)} ${fmt.title('清理快速入口')}`)
|
|
||||||
console.log()
|
|
||||||
await cleanFastEnterComponent()
|
|
||||||
console.log()
|
|
||||||
|
|
||||||
console.log(` ${fmt.badge('步骤 7/7', theme.bgBlue)} ${fmt.title('更新菜单接口')}`)
|
|
||||||
console.log()
|
|
||||||
await updateMenuApi()
|
|
||||||
|
|
||||||
// 显示统计信息
|
|
||||||
await showStats()
|
|
||||||
|
|
||||||
// 显示成功横幅
|
|
||||||
createSuccessBanner()
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.log()
|
|
||||||
console.log(` ${icons.error} ${fmt.error('清理脚本执行出错')}`)
|
|
||||||
console.log(` ${fmt.dim('错误详情: ' + err)}`)
|
|
||||||
console.log()
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ElConfigProvider size="default" :locale="locales[language]" :z-index="3000">
|
|
||||||
<RouterView></RouterView>
|
|
||||||
</ElConfigProvider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useUserStore } from './store/modules/user'
|
|
||||||
import zh from 'element-plus/es/locale/lang/zh-cn'
|
|
||||||
import en from 'element-plus/es/locale/lang/en'
|
|
||||||
import { systemUpgrade } from './utils/sys'
|
|
||||||
import { toggleTransition } from './utils/ui/animation'
|
|
||||||
import { checkStorageCompatibility } from './utils/storage'
|
|
||||||
import { initializeTheme } from './hooks/core/useTheme'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const { language } = storeToRefs(userStore)
|
|
||||||
|
|
||||||
const locales = {
|
|
||||||
zh: zh,
|
|
||||||
en: en
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
toggleTransition(true)
|
|
||||||
initializeTheme()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
checkStorageCompatibility()
|
|
||||||
toggleTransition(false)
|
|
||||||
systemUpgrade()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import request from '@/utils/http'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 登录
|
|
||||||
* @param params 登录参数
|
|
||||||
* @returns 登录响应
|
|
||||||
*/
|
|
||||||
export function fetchLogin(params: Api.Auth.LoginParams) {
|
|
||||||
return request.post<Api.Auth.LoginResponse>({
|
|
||||||
url: '/api/auth/login',
|
|
||||||
params
|
|
||||||
// showSuccessMessage: true // 显示成功消息
|
|
||||||
// showErrorMessage: false // 不显示错误消息
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户信息
|
|
||||||
* @returns 用户信息
|
|
||||||
*/
|
|
||||||
export function fetchGetUserInfo() {
|
|
||||||
return request.get<Api.Auth.UserInfo>({
|
|
||||||
url: '/api/user/info'
|
|
||||||
// 自定义请求头
|
|
||||||
// headers: {
|
|
||||||
// 'X-Custom-Header': 'your-custom-value'
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import request from '@/utils/http'
|
|
||||||
import { AppRouteRecord } from '@/types/router'
|
|
||||||
|
|
||||||
// 获取用户列表
|
|
||||||
export function fetchGetUserList(params: Api.SystemManage.UserSearchParams) {
|
|
||||||
return request.get<Api.SystemManage.UserList>({
|
|
||||||
url: '/api/user/list',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建用户
|
|
||||||
export function fetchCreateUser(data: {
|
|
||||||
userName: string
|
|
||||||
password?: string
|
|
||||||
nickName?: string
|
|
||||||
email?: string
|
|
||||||
phone?: string
|
|
||||||
gender?: string
|
|
||||||
roles?: string[]
|
|
||||||
}) {
|
|
||||||
return request.post({
|
|
||||||
url: '/api/user',
|
|
||||||
params: data,
|
|
||||||
showSuccessMessage: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户
|
|
||||||
export function fetchUpdateUser(id: number, data: {
|
|
||||||
nickName?: string
|
|
||||||
email?: string
|
|
||||||
phone?: string
|
|
||||||
gender?: string
|
|
||||||
status?: string
|
|
||||||
roles?: string[]
|
|
||||||
}) {
|
|
||||||
return request.put({
|
|
||||||
url: `/api/user/${id}`,
|
|
||||||
params: data,
|
|
||||||
showSuccessMessage: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除用户
|
|
||||||
export function fetchDeleteUser(id: number) {
|
|
||||||
return request.del({
|
|
||||||
url: `/api/user/${id}`,
|
|
||||||
showSuccessMessage: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取角色列表
|
|
||||||
export function fetchGetRoleList(params: Api.SystemManage.RoleSearchParams) {
|
|
||||||
return request.get<Api.SystemManage.RoleList>({
|
|
||||||
url: '/api/role/list',
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取菜单列表
|
|
||||||
export function fetchGetMenuList() {
|
|
||||||
return request.get<AppRouteRecord[]>({
|
|
||||||
url: '/api/v3/system/menus/simple'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 954 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 726 B |
|
Before Width: | Height: | Size: 944 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 810 B |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 514 B |
|
Before Width: | Height: | Size: 409 B |
|
Before Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 439 B |
|
Before Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 293 B |
|
Before Width: | Height: | Size: 448 B |
|
Before Width: | Height: | Size: 416 B |
|
Before Width: | Height: | Size: 509 B |
@ -1 +0,0 @@
|
|||||||
<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="94" y="34" width="212" height="233"><path d="M306 34H94v233h212V34Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M234.427 155.64h38.36V69.6h-38.36v86.04ZM113.326 155.64h121.1V69.6h-121.1v86.04Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M130.126 155.354h104.2v-72.95h-104.2v72.95ZM236.369 71.05s0 3.3 1.65 5.05c2.33 2.52 7.38-.2 7.38-.2s-1.75 5.15-1.55 10.19c.29 8.24 6.99 9.51 10 4.75 4.56 4.85 8.94-.29 9.52-2.62 4.27 4.76 9.32-.87 9.32-.87v-6.3l-23.99-12.13-12.33 2.13Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M234.429 155.641h-121.1l-15.93 32.11h121.1l15.93-32.11Z" fill="#fff"/><path d="M234.427 69.6h38.46v86.04M113.326 146.52V69.6h121.1M234.429 155.641l-15.93 32.11h-121.1l15.93-32.11h111.39" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M226.37 159.715H116.82l-12.04 23.86H215l11.37-23.86Z" fill="#006EFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="m288.807 187.751-15.92-32.11h-38.46l16.02 32.11h38.36Z" fill="#fff"/><path d="m238.607 163.981 11.84 23.77h38.36l-15.92-32.11h-38.46" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M207.336 223.734c-3.69-13.77-15.44-23.86-29.33-23.86h-8.65s-27.09 14.94-27.09 33.27c0 18.34 25.44 33.18 25.44 33.18h10.4c13.79-.1 25.44-10.19 29.13-23.87 1.75-12.51 0-18.62.1-18.72Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M243.459 240.421c3.98 0 7.28-3.3 7.28-7.27 0-3.98-3.3-7.28-7.28-7.28h-31.08c-3.98 0-7.28 3.3-7.28 7.28 0 3.97 3.3 7.27 7.28 7.27h31.08Z" fill="#C7DEFF"/><path d="M210.342 223.737c-4.08-13.87-16.9-23.96-32.05-23.96H168.972s-29.62 14.94-29.62 33.37 27.87 33.37 27.87 33.37h11.27c15.05-.1 27.77-10.19 31.75-23.96" stroke="#071F4D"/><path d="M212.379 240.421c-3.98 0-7.28-3.3-7.28-7.27m0 0c0-3.98 3.3-7.28 7.28-7.28" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M168.781 199.777c-18.45 0-33.41 14.94-33.41 33.37s14.96 33.37 33.41 33.37c18.45 0 33.4-14.94 33.4-33.37s-14.95-33.37-33.4-33.37Z" fill="#006EFF"/><path d="M168.781 199.777c-18.45 0-33.41 14.94-33.41 33.37s14.96 33.37 33.41 33.37c18.45 0 33.4-14.94 33.4-33.37s-14.95-33.37-33.4-33.37Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M168.775 209.38c-13.14 0-23.79 10.64-23.79 23.77 0 13.12 10.65 23.76 23.79 23.76 13.14 0 23.8-10.64 23.8-23.76 0-13.13-10.66-23.77-23.8-23.77Z" fill="#00E4E5"/><path d="M162.174 223.736a17.48 17.48 0 0 1 14.76-8.05M159.455 231.982c.1-1.36.29-2.62.68-3.88" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M173.535 209.87c-1.55-.3-3.11-.49-4.76-.49-13.11 0-23.79 10.67-23.79 23.77 0 13.09 10.68 23.76 23.79 23.76 1.65 0 3.21-.19 4.76-.48-10.88-2.23-19.03-11.84-19.03-23.28 0-11.45 8.15-21.05 19.03-23.28Z" fill="#071F4D"/><path d="M219.957 225.774h23.6c4.08 0 7.38 3.3 7.38 7.37m0 0c0 4.08-3.3 7.37-7.38 7.37h-20.1M212.091 225.774h3.3" stroke="#071F4D"/><path d="m248.894 34.485-.19 18.24c0 4.07-.39 5.23-2.14 6.79-8.15 6.88-10.97 9.02-9.22 12.9 1.45 3.2 6.79 2.23 9.61-1.55-.39 4.56-5.24 15.32-.58 18.04 4.37 2.52 6.89-3.49 6.89-3.49s.49 3.49 4.47 3.49c3.69 0 5.24-4.75 5.24-4.75s2.14 3.49 6.22 1.35c3.11-1.55 5.44-7.08 5.44-26.67v-24.35" fill="#fff"/><path d="m248.894 34.485-.19 18.24c0 4.07-.39 5.23-2.14 6.79-8.15 6.88-10.97 9.02-9.22 12.9 1.45 3.2 6.79 2.23 9.61-1.55-.39 4.56-5.24 15.32-.58 18.04 4.37 2.52 6.89-3.49 6.89-3.49s.49 3.49 4.47 3.49c3.69 0 5.24-4.75 5.24-4.75s2.14 3.49 6.22 1.35c3.11-1.55 5.44-7.08 5.44-26.67v-24.35" stroke="#071F4D"/><path d="M255.307 75.71s-.39 5.43-2.04 9.6l2.04-9.6Z" fill="#fff"/><path d="M255.307 75.71s-.39 5.43-2.04 9.6" stroke="#071F4D"/><path d="M264.921 75.323s-.68 5.24-2.04 8.63l2.04-8.63Z" fill="#fff"/><path d="M264.921 75.323s-.68 5.24-2.04 8.63M147.801 34.485v34.92M121.775 34.485v34.92M102.546 204.724v13.97M102.546 222.379v.87M102.546 197.934v3.49M115.268 206.955v26.29M115.268 239.451v5.34M244.43 197.643v11.93M244.43 213.939v3.49M270.359 201.232v33.76M115.369 47.774h-13.6M94.486 47.774h3.4M241.516 47.774h-84.1M280.168 47.774h25.35" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m282.497 183.575-12.04-23.86h-27.29l11.36 23.86h27.97Z" fill="#00E4E5"/><path d="M234.427 134.88V69.6M234.427 140.412v7.66" stroke="#071F4D"/><path d="M220.831 228.684h16.99M240.934 228.684h2.43" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="m223.842 187.462 21.46-.2-10.97-20.66-10.49 20.86Z" fill="#071F4D"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
@ -1,5 +0,0 @@
|
|||||||
<svg
|
|
||||||
viewBox="0 0 400 300"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="38" width="307" height="224"><path d="M353.3 38H47.5v223.8h305.8V38Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M299.2 200.6H61.6v5.1h240.3l-2.7-5.1Z" fill="#C7DEFF"/><path d="m308.9 185.8-6.5 20H183.7M332.3 127.6h10.6l-5 16.7-14.8-.1-7.2 21.1M328.8 127.4l13.6-39.6M307.6 166 337 84.7H180.6l-9.8 26.9h-10.5M296.6 196l4.3-11.8M157.2 149.2l6.4-17.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.8 93.1H188.5l-34.8 95.8h136.4l34.7-95.8ZM169.9 166.2l5-13.6-5 13.6Z" fill="#fff"/><path d="m169.9 166.2 5-13.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.8 93.1H188.5l-4 11.7h135.8l4.5-11.7Z" fill="#006EFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M102.6 159.5h38.3l2.7 36.6h-38.4c-10.1 0-20.9-8.2-20.9-18.3 0-10.1 8.2-18.3 18.3-18.3Z" fill="#DEEBFC"/><path fill-rule="evenodd" clip-rule="evenodd" d="M84.3 174.102c2.5 3.4 10 5 17.9 2.8 16.6-6.5 23.8-3.9 23.8-3.9s.5-3.4 1.3-5c-5.8-3-15.4.3-26.1 3.1-10.7 2.8-15.8-2.5-15.8-2.5-.4 0-1.1 2.8-1.1 5.5Z" fill="#fff"/><path d="M96.5 194.2c-7.2-3.3-12.2-10.5-12.2-19m0 0c0-11.5 9.3-20.8 20.8-20.8h29.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M140.3 195.1c-8.4-2.7-14.5-10.6-14.5-19.8l14.5 19.8Zm-14.5-19.8c0-11.5 9.3-20.8 20.8-20.8l-20.8 20.8Zm20.8-20.8c11.5 0 20.8 9.3 20.8 20.8l-20.8-20.8Zm20.8 20.8c0 8.4-5 15.6-12.1 18.9l12.1-18.9Z" fill="#fff"/><path d="M140.3 195.1c-8.4-2.7-14.5-10.6-14.5-19.8m0 0c0-11.5 9.3-20.8 20.8-20.8m0 0c11.5 0 20.8 9.3 20.8 20.8m0 0c0 8.4-5 15.6-12.1 18.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M161.5 177.2c0-7.7-6.3-14-14-14s-14 6.3-14 14c0 5.8 3.5 10.8 8.6 12.9.1 0 5.8 1.6 10.7 0 5.3-1.7 8.7-7.1 8.7-12.9Z" fill="#00E4E5"/><path d="M140.5 190.1c-5.8-2.4-9.9-8.2-9.9-14.9 0-8.9 7.2-16.1 16.1-16.1 8.9 0 16.1 7.2 16.1 16.1 0 6.8-4.2 12.5-10.1 14.9M88.4 170.604c2.9 1.3 7.7 2.6 13.6.3 14.7-5.7 22.3-4.3 24.6-3.5M84.5 174.599s5.9 6.5 19 1.7c9.2-3.4 15.3-3.9 18.8-3.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M340.6 112.3h-55.2l-2.7 6.2H338l2.6-6.2Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M236.8 117.9c-16.13 0-29.2 13.07-29.2 29.2s13.07 29.2 29.2 29.2 29.2-13.07 29.2-29.2-13.07-29.2-29.2-29.2Z" fill="#00E4E5"/><path d="M265 123.3c13.1 13.1 13.1 34.4 0 47.6M306 205.9h19.2M61.7 205.9h32.9M181.2 196.2h115.2M47.5 205.9h10v-9.7h73.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M146.7 179.2c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.5 196.2c3.9 0 7.1 3.2 7.1 7.1 0 3.9-3.2 7.1-7.1 7.1H144c-2.1 0-3.9 1.7-3.9 3.9v1c0 2.1 1.7 3.9 3.9 3.9h48c5.1 0 9.2 4.1 9.2 9.2s-4.1 9.3-9.2 9.2h-33.8c-2.3 0-4.1 1.8-4.1 4.1s1.8 4.1 4.1 4.1h4.2c4.4 0 8 3.6 8 8s-3.6 8-8 8H111c-3.7 0-6.8-3-6.8-6.8 0-3.7 3-6.8 6.8-6.8h.3c2.3 0 4.1-1.8 4.1-4.1s-1.8-4.1-4.1-4.1H79c-4.5 0-8.1-3.6-8.1-8.1s3.6-8.1 8.1-8.1h37.7c2.1 0 3.9-1.7 3.9-3.9 0-2.1-1.7-3.9-3.9-3.9h-7.9c-4.4 0-7.9-3.5-7.9-7.9s3.5-7.9 7.9-7.9h30.4c2.2 0 3.9-1.8 3.9-3.9V187c0-1.9 1.6-3.5 3.5-3.5s3.5 1.6 3.5 3.5v5.3c0 2.2 1.8 3.9 3.9 3.9h15.5Z" fill="#006EFF"/><path d="m227.8 138.5 18.7 18.7M227.8 157.2l18.7-18.7" stroke="#fff" stroke-width="6"/><path fill-rule="evenodd" clip-rule="evenodd" d="M194.8 96.9c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8ZM202.9 96.9c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8Z" fill="#fff"/><path d="m291.7 184.3-1.6 4.6h-121M298.1 166.7l22.5-61.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m193 134.1 2.2-5.1h-19.4l-2.3 5.1H193ZM313.2 123.5l2.2-5.1h-24.5l-2.3 5.1h24.6Z" fill="#DEEBFC"/><path d="m164.5 159.2 19.8-54.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M199.6 119.8h-53.2l-4.4 9.3h53.2l4.4-9.3Z" fill="#00E4E5"/><path d="M151.3 129.1H142l4.4-9.3h16.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M353.3 169.4h-67.4l-4.8 12.2h67.3l4.9-12.2Z" fill="#006EFF"/><path d="M332.4 169.4h20.9l-4.9 12.2h-39.7M242.7 235.5v-4.8c0-3.8 3.1-7 7-7h20.2c3.8 0 7 3.1 7 7" stroke="#071F4D"/><path d="M261.1 235.5v-4.8c0-3.8 3.1-7 7-7h13.7c3.8 0 7 3.1 7 7v4.8M242.6 230.7h13.7M235.2 237.7h63.3M224 237.7h6.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.1 141.3H335l3.3-10.7h-10.2l-4 10.7Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M288.3 230.4c0-3.6-2.9-6.5-6.5-6.5h-14.2c-3.6 0-6.5 2.9-6.5 6.5v5.3h27.2v-5.3Z" fill="#071F4D"/><path d="M80.4 228.5H83M87.7 228.5h19.2M146.3 195.8v2c0 3.6-2.9 6.6-6.6 6.6H138M133.4 204.3h1.5M154 249.9h9.4" stroke="#DEEBFC"/><path d="m299.4 141.9 5.1-13.9" stroke="#071F4D"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB |
@ -1 +0,0 @@
|
|||||||
<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="44" y="42" width="312" height="217"><path d="M355.3 42H44v216.9h311.3V42Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M288.2 248.4h25.1v-30h-25.1v30Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.498 238.199c-1.5-3.9-5.9-15.4-4-21.6-2.9.8-3.3.1-5-.1-1.7-.1 0 10.7 2.2 16.4 1.7 4.5 2.1 11.1 2.1 13.6h5.4c.2-1.9.3-5.5-.7-8.3Z" fill="#fff"/><path d="M311.5 214.7v-1.6c0-.7-.6-1.3-1.3-1.3h-22.8c-.7 0-1.3.6-1.3 1.3v1.6" fill="#fff"/><path d="M311.5 214.7v-1.6c0-.7-.6-1.3-1.3-1.3h-22.8c-.7 0-1.3.6-1.3 1.3v1.6M290.2 214.7h21.4c1 0 1.8.8 1.8 1.8v29" stroke="#071F4D" stroke-width="1.096"/><path d="M284.3 245.6v-29c0-1 .8-1.8 1.8-1.8h1.6" fill="#fff"/><path d="M284.3 245.6v-29c0-1 .8-1.8 1.8-1.8h1.6" stroke="#071F4D" stroke-width="1.096"/><path d="M295.402 216.5c-.9 4.2-.4 9.7 2.8 17.5 2.4 5.9 1.9 10.2 1.8 12.3M300.502 216.5c-.9 4.2-.4 9.7 2.8 17.5 2.4 5.9 1.9 10.2 1.8 12.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m331 258.4-.3-5.2H88.5l-1.2 5.2H331Z" fill="#C7DEFF"/><path d="M252.9 248.7H331M216.6 258.4H331M47.1 139.3l-2.6 1.5 42.7 117.6h129.2v-6.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m247.2 248.6-40.4-111.3H50.5l40.3 111.3h156.4Z" fill="#fff"/><path d="m247.2 248.6-40.4-111.3H50.5l40.3 111.3h156.4Z" stroke="#071F4D"/><path d="m203.2 153.2 32.2 88.7H97.8l-32.3-88.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M72.2 146.9c-.77 0-1.4.63-1.4 1.4 0 .77.63 1.4 1.4 1.4.77 0 1.4-.63 1.4-1.4 0-.77-.63-1.4-1.4-1.4ZM79.3 146.9c-.77 0-1.4.63-1.4 1.4 0 .77.63 1.4 1.4 1.4.77 0 1.4-.63 1.4-1.4 0-.77-.63-1.4-1.4-1.4Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M263.5 171.2h80.3v-63.7h-80.3v63.7Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M290 143.9h-45.6l12.5 51.3H290v-51.3Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M286 117.4h-29.3v77.8h92.9v-67.6l-55.9.6-7.7-10.8Z" fill="#00E4E5"/><path d="m332.6 127.6-38.9.6-7.7-10.8h-11.7M308.9 195.2h45.9M250.3 195.2h28.5M287.3 195.2h12.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M130.5 211.4H186v-44h-55.5v44Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M148.7 192.5h-31.6l8.7 35.5h22.9v-35.5Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M145.9 174.2h-20.2V228h64.1v-46.7l-38.6.4-5.3-7.5Z" fill="#006EFF"/><path d="m179 181.3-27.8.4-5.3-7.5h-7.7M176.2 201.7h19.2M163.2 210.7H195M172.1 228h-54.2M184.8 228h8.1M174.9 228h5.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m293.2 155.7-6.4 6.3 15.3 15.3 22.7-22.6-6.4-6.4-16.3 16.3-8.9-8.9Z" fill="#fff"/><path d="M57.2 258.4h283.6M345.9 258.4h8.1M55.4 258.4h220.5M160.1 118.8l-1.2 2.7M156.7 127c-.3.8-.7 1.8-1.1 2.8M222 68.5c-1 .2-1.9.5-2.9.8M214.1 70.7c-5.8 1.9-11.3 4.4-16.5 7.4M195.4 79.5c-.9.5-1.7 1.1-2.5 1.6M314.2 98.5c-.6-.8-1.3-1.5-2-2.3M308.9 92.8c-4-4-8.3-7.6-13-10.8M293.9 80.7c-.8-.5-1.7-1.1-2.5-1.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M251.296 71.203c-3.6-1.5-18.5-2.9-21.8-1.9-1 5.8 4.9 13.5 4.9 13.5s6-9.9 16.9-11.6Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M251.3 42.704c-6.5 6.7-7.8 13-8.8 19.3 24.4-1.1 36.3 13 42.8 20 3.2-9.1 7.8-23 7.2-29-7.1-6.4-20-11.7-41.2-10.3Z" fill="#C7DEFF"/><path d="M230 69.3c36.2-3.8 52 21.1 52 21.1s11.4-28.2 10.5-37.4c-7.3-6.5-23.3-12-45.6-10.1-9 6.3-15.6 18.7-16.9 26.4Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M161.604 70.7c-6 8.4-9.9 21.9-8.8 33.8 8.4 5.3 32.3 10.5 43.6 11.5 6.1-7.9 15.9-26 15.9-26s-32-4.8-50.7-19.3Z" fill="#C7DEFF"/><path d="M193.103 119.5c4.8-2.7 19.2-29.5 19.2-29.5s-35.8-5.4-53.7-21.8c-9.3 6.1-16.4 24.3-15 40.1 10.6 6.7 45.8 13.3 49.5 11.2Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M189.5 111.6c-3 5.2-5.7 7.2-9.8 6.6 12.2 2.6 13.5 1.2 15.6-1.1 2.2-2.4 4.2-6.6 4.2-6.6s-3.1 2.5-10 1.1Z" fill="#071F4D"/><path d="M331 251.8v6.6M77 165.4l-2.7-6.7h7.8M222.8 228.9l2.8 6.6h-7.9" stroke="#071F4D"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@ -1,292 +0,0 @@
|
|||||||
// 全局样式
|
|
||||||
// 顶部进度条颜色
|
|
||||||
#nprogress .bar {
|
|
||||||
z-index: 2400;
|
|
||||||
background-color: color-mix(in srgb, var(--theme-color) 70%, white);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nprogress .peg {
|
|
||||||
box-shadow:
|
|
||||||
0 0 10px var(--theme-color),
|
|
||||||
0 0 5px var(--theme-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nprogress .spinner-icon {
|
|
||||||
border-top-color: var(--theme-color) !important;
|
|
||||||
border-left-color: var(--theme-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理移动端组件兼容性
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
* {
|
|
||||||
cursor: default !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 背景滤镜
|
|
||||||
*,
|
|
||||||
::before,
|
|
||||||
::after {
|
|
||||||
--tw-backdrop-blur: ;
|
|
||||||
--tw-backdrop-brightness: ;
|
|
||||||
--tw-backdrop-contrast: ;
|
|
||||||
--tw-backdrop-grayscale: ;
|
|
||||||
--tw-backdrop-hue-rotate: ;
|
|
||||||
--tw-backdrop-invert: ;
|
|
||||||
--tw-backdrop-opacity: ;
|
|
||||||
--tw-backdrop-saturate: ;
|
|
||||||
--tw-backdrop-sepia: ;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 色弱模式
|
|
||||||
.color-weak {
|
|
||||||
filter: invert(80%);
|
|
||||||
-webkit-filter: invert(80%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#noop {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 语言切换选中样式
|
|
||||||
.langDropDownStyle {
|
|
||||||
// 选中项背景颜色
|
|
||||||
.is-selected {
|
|
||||||
background-color: var(--art-el-active-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 语言切换按钮菜单样式优化
|
|
||||||
.lang-btn-item {
|
|
||||||
.el-dropdown-menu__item {
|
|
||||||
padding-left: 13px !important;
|
|
||||||
padding-right: 6px !important;
|
|
||||||
margin-bottom: 3px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
.el-dropdown-menu__item {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-txt {
|
|
||||||
min-width: 60px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 10px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 盒子默认边框
|
|
||||||
.page-content {
|
|
||||||
border: 1px solid var(--art-card-border) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin art-card-base($border-color, $shadow: none, $radius-diff: 4px) {
|
|
||||||
background: var(--default-box-color);
|
|
||||||
border: 1px solid #{$border-color} !important;
|
|
||||||
border-radius: calc(var(--custom-radius) + #{$radius-diff}) !important;
|
|
||||||
box-shadow: #{$shadow} !important;
|
|
||||||
|
|
||||||
--el-card-border-color: var(--default-border) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.art-card,
|
|
||||||
.art-card-sm,
|
|
||||||
.art-card-xs {
|
|
||||||
border: 1px solid var(--art-card-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 盒子边框
|
|
||||||
[data-box-mode='border-mode'] {
|
|
||||||
.page-content,
|
|
||||||
.art-table-card {
|
|
||||||
border: 1px solid var(--art-card-border) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.art-card {
|
|
||||||
@include art-card-base(var(--art-card-border), none, 4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.art-card-sm {
|
|
||||||
@include art-card-base(var(--art-card-border), none, 0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.art-card-xs {
|
|
||||||
@include art-card-base(var(--art-card-border), none, -4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 盒子阴影
|
|
||||||
[data-box-mode='shadow-mode'] {
|
|
||||||
.page-content,
|
|
||||||
.art-table-card {
|
|
||||||
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.04) !important;
|
|
||||||
border: 1px solid var(--art-gray-200) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-sidebar {
|
|
||||||
border-right: 1px solid var(--art-card-border) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.art-card {
|
|
||||||
@include art-card-base(
|
|
||||||
var(--art-gray-200),
|
|
||||||
(0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)),
|
|
||||||
4px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.art-card-sm {
|
|
||||||
@include art-card-base(
|
|
||||||
var(--art-gray-200),
|
|
||||||
(0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)),
|
|
||||||
2px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.art-card-xs {
|
|
||||||
@include art-card-base(
|
|
||||||
var(--art-gray-200),
|
|
||||||
(0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 1px -1px rgba(0, 0, 0, 0.08)),
|
|
||||||
-4px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 元素全屏
|
|
||||||
.el-full-screen {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100% !important;
|
|
||||||
z-index: 2300;
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 15px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: var(--default-box-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表格卡片
|
|
||||||
.art-table-card {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 12px;
|
|
||||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
|
||||||
|
|
||||||
.el-card__body {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 容器全高
|
|
||||||
.art-full-height {
|
|
||||||
height: var(--art-full-height);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 徽章样式
|
|
||||||
.art-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 20px;
|
|
||||||
bottom: 0;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
margin: auto;
|
|
||||||
background: #ff3860;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: breathe 1.5s ease-in-out infinite;
|
|
||||||
|
|
||||||
&.art-badge-horizontal {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.art-badge-mixed {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.art-badge-dual {
|
|
||||||
right: 5px;
|
|
||||||
top: 5px;
|
|
||||||
bottom: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文字徽章样式
|
|
||||||
.art-text-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 12px;
|
|
||||||
bottom: 0;
|
|
||||||
min-width: 20px;
|
|
||||||
height: 18px;
|
|
||||||
line-height: 17px;
|
|
||||||
padding: 0 5px;
|
|
||||||
margin: auto;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
background: #fd4e4e;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes breathe {
|
|
||||||
0% {
|
|
||||||
opacity: 0.7;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0.7;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修复老机型 loading 定位问题
|
|
||||||
.art-loading-fix {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.art-loading-fix .el-loading-spinner {
|
|
||||||
position: static !important;
|
|
||||||
top: auto !important;
|
|
||||||
left: auto !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 去除移动端点击背景色
|
|
||||||
@media screen and (max-width: 1180px) {
|
|
||||||
* {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* 深色主题
|
|
||||||
* 单页面移除深色主题 document.getElementsByTagName("html")[0].removeAttribute('class')
|
|
||||||
*/
|
|
||||||
|
|
||||||
$font-color: rgba(#ffffff, 0.85);
|
|
||||||
|
|
||||||
/* 覆盖element-plus默认深色背景色 */
|
|
||||||
html.dark {
|
|
||||||
// element-plus
|
|
||||||
--el-bg-color: var(--default-box-color);
|
|
||||||
--el-text-color-regular: #{$font-color};
|
|
||||||
|
|
||||||
// 富文本编辑器
|
|
||||||
// 工具栏背景颜色
|
|
||||||
--w-e-toolbar-bg-color: #18191c;
|
|
||||||
// 输入区域背景颜色
|
|
||||||
--w-e-textarea-bg-color: #090909;
|
|
||||||
// 工具栏文字颜色
|
|
||||||
--w-e-toolbar-color: var(--art-gray-600);
|
|
||||||
// 选中菜单颜色
|
|
||||||
--w-e-toolbar-active-bg-color: #25262b;
|
|
||||||
// 弹窗边框颜色
|
|
||||||
--w-e-toolbar-border-color: var(--default-border-dashed);
|
|
||||||
// 分割线颜色
|
|
||||||
--w-e-textarea-border-color: var(--default-border-dashed);
|
|
||||||
// 链接输入框边框颜色
|
|
||||||
--w-e-modal-button-border-color: var(--default-border-dashed);
|
|
||||||
// 表格头颜色
|
|
||||||
--w-e-textarea-slight-bg-color: #090909;
|
|
||||||
// 按钮背景颜色
|
|
||||||
--w-e-modal-button-bg-color: #090909;
|
|
||||||
// hover toolbar 背景颜色
|
|
||||||
--w-e-toolbar-active-color: var(--art-gray-800);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
.page-content .article-list .item .left .outer > div {
|
|
||||||
border-right-color: var(--dark-border-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 富文本编辑器
|
|
||||||
.editor-wrapper {
|
|
||||||
*:not(pre code *) {
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 分隔线
|
|
||||||
.w-e-bar-divider {
|
|
||||||
background-color: var(--art-gray-300) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-e-select-list,
|
|
||||||
.w-e-drop-panel,
|
|
||||||
.w-e-bar-item-group .w-e-bar-item-menus-container,
|
|
||||||
.w-e-text-container [data-slate-editor] pre > code {
|
|
||||||
border: 1px solid var(--default-border) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下拉选择框
|
|
||||||
.w-e-select-list {
|
|
||||||
background-color: var(--default-box-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 下拉选择框 hover 样式调整 */
|
|
||||||
.w-e-select-list ul li:hover,
|
|
||||||
/* 工具栏 hover 按钮背景颜色 */
|
|
||||||
.w-e-bar-item button:hover {
|
|
||||||
background-color: #090909 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 代码块 */
|
|
||||||
.w-e-text-container [data-slate-editor] pre > code {
|
|
||||||
background-color: #25262b !important;
|
|
||||||
text-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 引用 */
|
|
||||||
.w-e-text-container [data-slate-editor] blockquote {
|
|
||||||
border-left: 4px solid var(--default-border-dashed) !important;
|
|
||||||
background-color: var(--art-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-wrapper {
|
|
||||||
.w-e-text-container [data-slate-editor] .table-container th:last-of-type {
|
|
||||||
border-right: 1px solid var(--default-border-dashed) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-e-modal {
|
|
||||||
background-color: var(--art-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
// 导入暗黑主题
|
|
||||||
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
|
|
||||||
// 自定义Element 亮色主题
|
|
||||||
|
|
||||||
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
|
|
||||||
$colors: (
|
|
||||||
'white': #ffffff,
|
|
||||||
'black': #000000,
|
|
||||||
'success': (
|
|
||||||
'base': #13deb9
|
|
||||||
),
|
|
||||||
'warning': (
|
|
||||||
'base': #ffae1f
|
|
||||||
),
|
|
||||||
'danger': (
|
|
||||||
'base': #ff4d4f
|
|
||||||
),
|
|
||||||
'error': (
|
|
||||||
'base': #fa896b
|
|
||||||
)
|
|
||||||
),
|
|
||||||
$button: (
|
|
||||||
'hover-bg-color': var(--el-color-primary-light-9),
|
|
||||||
'hover-border-color': var(--el-color-primary),
|
|
||||||
'border-color': var(--el-color-primary),
|
|
||||||
'text-color': var(--el-color-primary)
|
|
||||||
),
|
|
||||||
$messagebox: (
|
|
||||||
'border-radius': '12px'
|
|
||||||
),
|
|
||||||
$popover: (
|
|
||||||
'padding': '14px',
|
|
||||||
'border-radius': '10px'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@ -1,524 +0,0 @@
|
|||||||
// 优化 Element Plus 组件库默认样式
|
|
||||||
|
|
||||||
:root {
|
|
||||||
// 系统主色
|
|
||||||
--main-color: var(--el-color-primary);
|
|
||||||
--el-color-white: white !important;
|
|
||||||
--el-color-black: white !important;
|
|
||||||
// 输入框边框颜色
|
|
||||||
// --el-border-color: #E4E4E7 !important; // DCDFE6
|
|
||||||
// 按钮粗度
|
|
||||||
--el-font-weight-primary: 400 !important;
|
|
||||||
|
|
||||||
--el-component-custom-height: 36px !important;
|
|
||||||
|
|
||||||
--el-component-size: var(--el-component-custom-height) !important;
|
|
||||||
|
|
||||||
// 边框、按钮圆角...
|
|
||||||
--el-border-radius-base: calc(var(--custom-radius) / 3 + 2px) !important;
|
|
||||||
|
|
||||||
--el-border-radius-small: calc(var(--custom-radius) / 3 + 4px) !important;
|
|
||||||
--el-messagebox-border-radius: calc(var(--custom-radius) / 3 + 4px) !important;
|
|
||||||
--el-popover-border-radius: calc(var(--custom-radius) / 3 + 4px) !important;
|
|
||||||
|
|
||||||
.region .el-radio-button__original-radio:checked + .el-radio-button__inner {
|
|
||||||
color: var(--theme-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化 el-form-item 标签高度
|
|
||||||
.el-form-item__label {
|
|
||||||
height: var(--el-component-custom-height) !important;
|
|
||||||
line-height: var(--el-component-custom-height) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 日期选择器
|
|
||||||
.el-date-range-picker {
|
|
||||||
--el-datepicker-inrange-bg-color: var(--art-gray-200) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// el-card 背景色跟系统背景色保持一致
|
|
||||||
html.dark .el-card {
|
|
||||||
--el-card-bg-color: var(--default-box-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 el-pagination 大小
|
|
||||||
.el-pagination--default {
|
|
||||||
& {
|
|
||||||
--el-pagination-button-width: 32px !important;
|
|
||||||
--el-pagination-button-height: var(--el-pagination-button-width) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
|
||||||
& {
|
|
||||||
--el-pagination-button-width: 28px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select--default .el-select__wrapper {
|
|
||||||
min-height: var(--el-pagination-button-width) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-pagination__jump .el-input {
|
|
||||||
height: var(--el-pagination-button-width) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-pager li {
|
|
||||||
padding: 0 10px !important;
|
|
||||||
// border: 1px solid red !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化菜单折叠展开动画(提升动画流畅度)
|
|
||||||
.el-menu.el-menu--inline {
|
|
||||||
transition: max-height 0.26s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化菜单 item hover 动画(提升鼠标跟手感)
|
|
||||||
.el-sub-menu__title,
|
|
||||||
.el-menu-item {
|
|
||||||
transition: background-color 0s !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------- 修改 el-size=default 组件默认高度 start --------------------------------
|
|
||||||
// 修改 el-button 高度
|
|
||||||
.el-button--default {
|
|
||||||
height: var(--el-component-custom-height) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// circle 按钮宽度优化
|
|
||||||
.el-button--default.is-circle {
|
|
||||||
width: var(--el-component-custom-height) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 el-select 高度
|
|
||||||
.el-select--default {
|
|
||||||
.el-select__wrapper {
|
|
||||||
min-height: var(--el-component-custom-height) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 el-checkbox-button 高度
|
|
||||||
.el-checkbox-button--default .el-checkbox-button__inner,
|
|
||||||
// 修改 el-radio-button 高度
|
|
||||||
.el-radio-button--default .el-radio-button__inner {
|
|
||||||
padding: 10px 15px !important;
|
|
||||||
}
|
|
||||||
// -------------------------------- 修改 el-size=default 组件默认高度 end --------------------------------
|
|
||||||
|
|
||||||
.el-pagination.is-background .btn-next,
|
|
||||||
.el-pagination.is-background .btn-prev,
|
|
||||||
.el-pagination.is-background .el-pager li {
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-popover {
|
|
||||||
min-width: 80px;
|
|
||||||
border-radius: var(--el-border-radius-small) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog {
|
|
||||||
border-radius: 100px !important;
|
|
||||||
border-radius: calc(var(--custom-radius) / 1.2 + 2px) !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__header {
|
|
||||||
.el-dialog__title {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__body {
|
|
||||||
padding: 25px 0 !important;
|
|
||||||
position: relative; // 为了兼容 el-pagination 样式,需要设置 relative,不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog.el-dialog-border {
|
|
||||||
.el-dialog__body {
|
|
||||||
// 上边框
|
|
||||||
&::before,
|
|
||||||
// 下边框
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: -16px;
|
|
||||||
width: calc(100% + 32px);
|
|
||||||
height: 1px;
|
|
||||||
background-color: var(--art-gray-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// el-message 样式优化
|
|
||||||
.el-message {
|
|
||||||
background-color: var(--default-box-color) !important;
|
|
||||||
border: 0 !important;
|
|
||||||
box-shadow:
|
|
||||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
|
||||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
|
||||||
0 9px 28px 8px rgba(0, 0, 0, 0.05) !important;
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 el-dropdown 样式
|
|
||||||
.el-dropdown-menu {
|
|
||||||
padding: 6px !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
border: none !important;
|
|
||||||
|
|
||||||
.el-dropdown-menu__item {
|
|
||||||
padding: 6px 16px !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
|
|
||||||
&:hover:not(.is-disabled) {
|
|
||||||
color: var(--art-gray-900) !important;
|
|
||||||
background-color: var(--art-el-active-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus:not(.is-disabled) {
|
|
||||||
color: var(--art-gray-900) !important;
|
|
||||||
background-color: var(--art-gray-200) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏 select、dropdown 的三角
|
|
||||||
.el-select__popper,
|
|
||||||
.el-dropdown__popper {
|
|
||||||
margin-top: -6px !important;
|
|
||||||
|
|
||||||
.el-popper__arrow {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dropdown-selfdefine:focus {
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理移动端组件兼容性
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
.el-message-box,
|
|
||||||
.el-message,
|
|
||||||
.el-dialog {
|
|
||||||
width: calc(100% - 24px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-date-picker.has-sidebar.has-time {
|
|
||||||
width: calc(100% - 24px);
|
|
||||||
left: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-picker-panel *[slot='sidebar'],
|
|
||||||
.el-picker-panel__sidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-picker-panel *[slot='sidebar'] + .el-picker-panel__body,
|
|
||||||
.el-picker-panel__sidebar + .el-picker-panel__body {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改el-button样式
|
|
||||||
.el-button {
|
|
||||||
&.el-button--text {
|
|
||||||
background-color: transparent !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin-left: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改el-tag样式
|
|
||||||
.el-tag {
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0s !important;
|
|
||||||
|
|
||||||
&.el-tag--default {
|
|
||||||
height: 26px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-checkbox-group {
|
|
||||||
&.el-table-filter__checkbox-group label.el-checkbox {
|
|
||||||
height: 17px !important;
|
|
||||||
|
|
||||||
.el-checkbox__label {
|
|
||||||
font-weight: 400 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-radio--default {
|
|
||||||
// 优化单选按钮大小
|
|
||||||
.el-radio__input {
|
|
||||||
.el-radio__inner {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-checkbox {
|
|
||||||
.el-checkbox__inner {
|
|
||||||
border-radius: 2px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化复选框样式
|
|
||||||
.el-checkbox--default {
|
|
||||||
.el-checkbox__inner {
|
|
||||||
width: 16px !important;
|
|
||||||
height: 16px !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
height: 4px !important;
|
|
||||||
top: 5px !important;
|
|
||||||
background-color: #fff !important;
|
|
||||||
transform: scale(0.6) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-checked {
|
|
||||||
.el-checkbox__inner {
|
|
||||||
&::after {
|
|
||||||
width: 3px;
|
|
||||||
height: 8px;
|
|
||||||
margin: auto;
|
|
||||||
border: 2px solid var(--el-checkbox-checked-icon-color);
|
|
||||||
border-left: 0;
|
|
||||||
border-top: 0;
|
|
||||||
transform: translate(-45%, -60%) rotate(45deg) scale(0.86) !important;
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-notification .el-notification__icon {
|
|
||||||
font-size: 22px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 el-message-box 样式
|
|
||||||
.el-message-box__headerbtn .el-message-box__close,
|
|
||||||
.el-dialog__headerbtn .el-dialog__close {
|
|
||||||
top: 7px;
|
|
||||||
right: 7px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--art-hover-color) !important;
|
|
||||||
color: var(--art-gray-900) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message-box {
|
|
||||||
padding: 25px 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-message-box__title {
|
|
||||||
font-weight: 500 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table__column-filter-trigger i {
|
|
||||||
color: var(--theme-color) !important;
|
|
||||||
margin: -3px 0 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 去除 el-dropdown 鼠标放上去出现的边框
|
|
||||||
.el-tooltip__trigger:focus-visible {
|
|
||||||
outline: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ipad 表单右侧按钮优化
|
|
||||||
@media screen and (max-width: 1180px) {
|
|
||||||
.el-table-fixed-column--right {
|
|
||||||
padding-right: 0 !important;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
margin: 5px 10px 5px 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-out-dialog {
|
|
||||||
padding: 30px 20px !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 dialog 动画
|
|
||||||
.dialog-fade-enter-active {
|
|
||||||
.el-dialog:not(.is-draggable) {
|
|
||||||
animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86);
|
|
||||||
|
|
||||||
// 修复 el-dialog 动画后宽度不自适应问题
|
|
||||||
.el-select__selected-item {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-fade-leave-active {
|
|
||||||
animation: fade-out 0.2s linear;
|
|
||||||
|
|
||||||
.el-dialog:not(.is-draggable) {
|
|
||||||
animation: dialog-close 0.5s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dialog-open {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dialog-close {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遮罩层动画
|
|
||||||
@keyframes fade-out {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 el-select 样式
|
|
||||||
.el-select__popper:not(.el-tree-select__popper) {
|
|
||||||
.el-select-dropdown__list {
|
|
||||||
padding: 5px !important;
|
|
||||||
|
|
||||||
.el-select-dropdown__item {
|
|
||||||
height: 34px !important;
|
|
||||||
line-height: 34px !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
|
|
||||||
&.is-selected {
|
|
||||||
color: var(--art-gray-900) !important;
|
|
||||||
font-weight: 400 !important;
|
|
||||||
background-color: var(--art-el-active-color) !important;
|
|
||||||
margin-bottom: 4px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--art-hover-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select-dropdown__item:hover ~ .is-selected,
|
|
||||||
.el-select-dropdown__item.is-selected:has(~ .el-select-dropdown__item:hover) {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 el-tree-select 样式
|
|
||||||
.el-tree-select__popper {
|
|
||||||
.el-select-dropdown__list {
|
|
||||||
padding: 5px !important;
|
|
||||||
|
|
||||||
.el-tree-node {
|
|
||||||
.el-tree-node__content {
|
|
||||||
height: 36px !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--art-gray-200) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实现水波纹在文字下面效果
|
|
||||||
.el-button > span {
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化颜色选择器圆角
|
|
||||||
.el-color-picker__color {
|
|
||||||
border-radius: 2px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化日期时间选择器底部圆角
|
|
||||||
.el-picker-panel {
|
|
||||||
.el-picker-panel__footer {
|
|
||||||
border-radius: 0 0 var(--el-border-radius-base) var(--el-border-radius-base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优化树型菜单样式
|
|
||||||
.el-tree-node__content {
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
padding: 1px 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--art-hover-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
|
|
||||||
background-color: var(--art-gray-300) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏折叠菜单弹窗 hover 出现的边框
|
|
||||||
.menu-left-popper:focus-within,
|
|
||||||
.horizontal-menu-popper:focus-within {
|
|
||||||
box-shadow: none !important;
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数字输入组件右侧按钮高度跟随自定义组件高度
|
|
||||||
.el-input-number--default.is-controls-right {
|
|
||||||
.el-input-number__decrease,
|
|
||||||
.el-input-number__increase {
|
|
||||||
height: calc((var(--el-component-size) / 2)) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
// sass 混合宏(函数)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 溢出省略号
|
|
||||||
* @param {Number} 行数
|
|
||||||
*/
|
|
||||||
@mixin ellipsis($rowCount: 1) {
|
|
||||||
@if $rowCount <=1 {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
} @else {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: $rowCount;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 控制用户能否选中文本
|
|
||||||
* @param {String} 类型
|
|
||||||
*/
|
|
||||||
@mixin userSelect($value: none) {
|
|
||||||
user-select: $value;
|
|
||||||
-moz-user-select: $value;
|
|
||||||
-ms-user-select: $value;
|
|
||||||
-webkit-user-select: $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绝对定位居中
|
|
||||||
@mixin absoluteCenter() {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* css3动画
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@mixin animation(
|
|
||||||
$from: (
|
|
||||||
width: 0px
|
|
||||||
),
|
|
||||||
$to: (
|
|
||||||
width: 100px
|
|
||||||
),
|
|
||||||
$name: mymove,
|
|
||||||
$animate: mymove 2s 1 linear infinite
|
|
||||||
) {
|
|
||||||
-webkit-animation: $animate;
|
|
||||||
-o-animation: $animate;
|
|
||||||
animation: $animate;
|
|
||||||
|
|
||||||
@keyframes #{$name} {
|
|
||||||
from {
|
|
||||||
@each $key, $value in $from {
|
|
||||||
#{$key}: #{$value};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
@each $key, $value in $to {
|
|
||||||
#{$key}: #{$value};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes #{$name} {
|
|
||||||
from {
|
|
||||||
@each $key, $value in $from {
|
|
||||||
$key: $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
@each $key, $value in $to {
|
|
||||||
$key: $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 圆形盒子
|
|
||||||
@mixin circle($size: 11px, $bg: #fff) {
|
|
||||||
border-radius: 50%;
|
|
||||||
width: $size;
|
|
||||||
height: $size;
|
|
||||||
line-height: $size;
|
|
||||||
text-align: center;
|
|
||||||
background: $bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// placeholder
|
|
||||||
@mixin placeholder($color: #bbb) {
|
|
||||||
// Firefox
|
|
||||||
&::-moz-placeholder {
|
|
||||||
color: $color;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internet Explorer 10+
|
|
||||||
&:-ms-input-placeholder {
|
|
||||||
color: $color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safari and Chrome
|
|
||||||
&::-webkit-input-placeholder {
|
|
||||||
color: $color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:placeholder-shown {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//背景透明,文字不透明。兼容IE8
|
|
||||||
@mixin betterTransparentize($color, $alpha) {
|
|
||||||
$c: rgba($color, $alpha);
|
|
||||||
$ie_c: ie_hex_str($c);
|
|
||||||
background: rgba($color, 1);
|
|
||||||
background: $c;
|
|
||||||
background: transparent \9;
|
|
||||||
zoom: 1;
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c});
|
|
||||||
-ms-filter: 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c})';
|
|
||||||
}
|
|
||||||
|
|
||||||
//添加浏览器前缀
|
|
||||||
@mixin browserPrefix($propertyName, $value) {
|
|
||||||
@each $prefix in -webkit-, -moz-, -ms-, -o-, '' {
|
|
||||||
#{$prefix}#{$propertyName}: $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 边框
|
|
||||||
@mixin border($color: red) {
|
|
||||||
border: 1px solid $color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 背景滤镜
|
|
||||||
@mixin backdropBlur() {
|
|
||||||
--tw-backdrop-blur: blur(30px);
|
|
||||||
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness)
|
|
||||||
var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate)
|
|
||||||
var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate)
|
|
||||||
var(--tw-backdrop-sepia);
|
|
||||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast)
|
|
||||||
var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert)
|
|
||||||
var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
@charset "UTF-8";
|
|
||||||
|
|
||||||
/*滚动条*/
|
|
||||||
/*滚动条整体部分,必须要设置*/
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px !important;
|
|
||||||
height: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*滚动条的轨道*/
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background-color: var(--art-gray-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*滚动条的滑块按钮*/
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #cccccc !important;
|
|
||||||
transition: all 0.2s;
|
|
||||||
-webkit-transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #b0abab !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*滚动条的上下两端的按钮*/
|
|
||||||
::-webkit-scrollbar-button {
|
|
||||||
height: 0px;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background-color: var(--default-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--art-gray-300) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
@use 'sass:map';
|
|
||||||
|
|
||||||
// === 变量区域 ===
|
|
||||||
$transition: (
|
|
||||||
// 动画持续时间
|
|
||||||
duration: 0.25s,
|
|
||||||
// 滑动动画的移动距离
|
|
||||||
distance: 15px,
|
|
||||||
// 默认缓动函数
|
|
||||||
easing: cubic-bezier(0.25, 0.1, 0.25, 1),
|
|
||||||
// 淡入淡出专用的缓动函数
|
|
||||||
fade-easing: cubic-bezier(0.4, 0, 0.6, 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 抽取配置值函数,提高可复用性
|
|
||||||
@function transition-config($key) {
|
|
||||||
@return map.get($transition, $key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 变量简写
|
|
||||||
$duration: transition-config('duration');
|
|
||||||
$distance: transition-config('distance');
|
|
||||||
$easing: transition-config('easing');
|
|
||||||
$fade-easing: transition-config('fade-easing');
|
|
||||||
|
|
||||||
// === 动画类 ===
|
|
||||||
|
|
||||||
// 淡入淡出动画
|
|
||||||
.fade {
|
|
||||||
&-enter-active,
|
|
||||||
&-leave-active {
|
|
||||||
transition: opacity $duration $fade-easing;
|
|
||||||
will-change: opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-from,
|
|
||||||
&-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-to,
|
|
||||||
&-leave-from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 滑动动画通用样式
|
|
||||||
@mixin slide-transition($direction) {
|
|
||||||
$distance-x: 0;
|
|
||||||
$distance-y: 0;
|
|
||||||
|
|
||||||
@if $direction == 'left' {
|
|
||||||
$distance-x: -$distance;
|
|
||||||
} @else if $direction == 'right' {
|
|
||||||
$distance-x: $distance;
|
|
||||||
} @else if $direction == 'top' {
|
|
||||||
$distance-y: -$distance;
|
|
||||||
} @else if $direction == 'bottom' {
|
|
||||||
$distance-y: $distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-active {
|
|
||||||
transition:
|
|
||||||
opacity $duration $easing,
|
|
||||||
transform $duration $easing;
|
|
||||||
will-change: opacity, transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-leave-active {
|
|
||||||
transition:
|
|
||||||
opacity calc($duration * 0.7) $easing,
|
|
||||||
transform calc($duration * 0.7) $easing;
|
|
||||||
will-change: opacity, transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate3d($distance-x, $distance-y, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate3d(-$distance-x, -$distance-y, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 滑动动画方向类
|
|
||||||
.slide-left {
|
|
||||||
@include slide-transition('left');
|
|
||||||
}
|
|
||||||
.slide-right {
|
|
||||||
@include slide-transition('right');
|
|
||||||
}
|
|
||||||
.slide-top {
|
|
||||||
@include slide-transition('top');
|
|
||||||
}
|
|
||||||
.slide-bottom {
|
|
||||||
@include slide-transition('bottom');
|
|
||||||
}
|
|
||||||
@ -1,208 +0,0 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
|
||||||
|
|
||||||
/* ==================== Light Mode Variables ==================== */
|
|
||||||
:root {
|
|
||||||
/* Base Colors */
|
|
||||||
--art-color: #ffffff;
|
|
||||||
--theme-color: var(--main-color);
|
|
||||||
|
|
||||||
/* Theme Colors - OKLCH Format */
|
|
||||||
--art-primary: oklch(0.7 0.23 260);
|
|
||||||
--art-secondary: oklch(0.72 0.19 231.6);
|
|
||||||
--art-error: oklch(0.73 0.15 25.3);
|
|
||||||
--art-info: oklch(0.58 0.03 254.1);
|
|
||||||
--art-success: oklch(0.78 0.17 166.1);
|
|
||||||
--art-warning: oklch(0.78 0.14 75.5);
|
|
||||||
--art-danger: oklch(0.68 0.22 25.3);
|
|
||||||
|
|
||||||
/* Gray Scale - Light Mode */
|
|
||||||
--art-gray-100: #f9fafb;
|
|
||||||
--art-gray-200: #f2f4f5;
|
|
||||||
--art-gray-300: #e6eaeb;
|
|
||||||
--art-gray-400: #dbdfe1;
|
|
||||||
--art-gray-500: #949eb7;
|
|
||||||
--art-gray-600: #7987a1;
|
|
||||||
--art-gray-700: #4d5875;
|
|
||||||
--art-gray-800: #383853;
|
|
||||||
--art-gray-900: #323251;
|
|
||||||
|
|
||||||
/* Border Colors */
|
|
||||||
--art-card-border: rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
--default-border: #e2e8ee;
|
|
||||||
--default-border-dashed: #dbdfe9;
|
|
||||||
|
|
||||||
/* Background Colors */
|
|
||||||
--default-bg-color: #fafbfc;
|
|
||||||
--default-box-color: #ffffff;
|
|
||||||
|
|
||||||
/* Hover Color */
|
|
||||||
--art-hover-color: #edeff0;
|
|
||||||
|
|
||||||
/* Active Color */
|
|
||||||
--art-active-color: #f2f4f5;
|
|
||||||
|
|
||||||
/* Element Component Active Color */
|
|
||||||
--art-el-active-color: #f2f4f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== Dark Mode Variables ==================== */
|
|
||||||
.dark {
|
|
||||||
/* Base Colors */
|
|
||||||
--art-color: #000000;
|
|
||||||
|
|
||||||
/* Gray Scale - Dark Mode */
|
|
||||||
--art-gray-100: #110f0f;
|
|
||||||
--art-gray-200: #17171c;
|
|
||||||
--art-gray-300: #393946;
|
|
||||||
--art-gray-400: #505062;
|
|
||||||
--art-gray-500: #73738c;
|
|
||||||
--art-gray-600: #8f8fa3;
|
|
||||||
--art-gray-700: #ababba;
|
|
||||||
--art-gray-800: #c7c7d1;
|
|
||||||
--art-gray-900: #e3e3e8;
|
|
||||||
|
|
||||||
/* Border Colors */
|
|
||||||
--art-card-border: rgba(255, 255, 255, 0.08);
|
|
||||||
|
|
||||||
--default-border: rgba(255, 255, 255, 0.1);
|
|
||||||
--default-border-dashed: #363843;
|
|
||||||
|
|
||||||
/* Background Colors */
|
|
||||||
--default-bg-color: #070707;
|
|
||||||
--default-box-color: #161618;
|
|
||||||
|
|
||||||
/* Hover Color */
|
|
||||||
--art-hover-color: #252530;
|
|
||||||
|
|
||||||
/* Active Color */
|
|
||||||
--art-active-color: #202226;
|
|
||||||
|
|
||||||
/* Element Component Active Color */
|
|
||||||
--art-el-active-color: #2e2e38;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== Tailwind Theme Configuration ==================== */
|
|
||||||
@theme {
|
|
||||||
/* Box Color (Light: white / Dark: black) */
|
|
||||||
--color-box: var(--default-box-color);
|
|
||||||
|
|
||||||
/* System Theme Color */
|
|
||||||
--color-theme: var(--theme-color);
|
|
||||||
|
|
||||||
/* Hover Color */
|
|
||||||
--color-hover-color: var(--art-hover-color);
|
|
||||||
|
|
||||||
/* Active Color */
|
|
||||||
--color-active-color: var(--art-active-color);
|
|
||||||
|
|
||||||
/* Active Color */
|
|
||||||
--color-el-active-color: var(--art-active-color);
|
|
||||||
|
|
||||||
/* ElementPlus Theme Colors */
|
|
||||||
--color-primary: var(--art-primary);
|
|
||||||
--color-secondary: var(--art-secondary);
|
|
||||||
--color-error: var(--art-error);
|
|
||||||
--color-info: var(--art-info);
|
|
||||||
--color-success: var(--art-success);
|
|
||||||
--color-warning: var(--art-warning);
|
|
||||||
--color-danger: var(--art-danger);
|
|
||||||
|
|
||||||
/* Gray Scale Colors (Auto-adapts to dark mode) */
|
|
||||||
--color-g-100: var(--art-gray-100);
|
|
||||||
--color-g-200: var(--art-gray-200);
|
|
||||||
--color-g-300: var(--art-gray-300);
|
|
||||||
--color-g-400: var(--art-gray-400);
|
|
||||||
--color-g-500: var(--art-gray-500);
|
|
||||||
--color-g-600: var(--art-gray-600);
|
|
||||||
--color-g-700: var(--art-gray-700);
|
|
||||||
--color-g-800: var(--art-gray-800);
|
|
||||||
--color-g-900: var(--art-gray-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== Custom Border Radius Utilities ==================== */
|
|
||||||
@utility rounded-custom-xs {
|
|
||||||
border-radius: calc(var(--custom-radius) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility rounded-custom-sm {
|
|
||||||
border-radius: calc(var(--custom-radius) / 2 + 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== Custom Utility Classes ==================== */
|
|
||||||
@layer utilities {
|
|
||||||
/* Flexbox Layout Utilities */
|
|
||||||
.flex-c {
|
|
||||||
@apply flex items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-b {
|
|
||||||
@apply flex justify-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-cc {
|
|
||||||
@apply flex items-center justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-cb {
|
|
||||||
@apply flex items-center justify-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transition Utilities */
|
|
||||||
.tad-200 {
|
|
||||||
@apply transition-all duration-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tad-300 {
|
|
||||||
@apply transition-all duration-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Border Utilities */
|
|
||||||
.border-full-d {
|
|
||||||
@apply border border-[var(--default-border)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-b-d {
|
|
||||||
@apply border-b border-[var(--default-border)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-t-d {
|
|
||||||
@apply border-t border-[var(--default-border)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-l-d {
|
|
||||||
@apply border-l border-[var(--default-border)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-r-d {
|
|
||||||
@apply border-r border-[var(--default-border)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cursor Utilities */
|
|
||||||
.c-p {
|
|
||||||
@apply cursor-pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== Custom Component Classes ==================== */
|
|
||||||
@layer components {
|
|
||||||
/* Art Card Header Component */
|
|
||||||
.art-card-header {
|
|
||||||
@apply flex justify-between pr-6 pb-1;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
h4 {
|
|
||||||
@apply text-lg font-medium text-g-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
@apply mt-1 text-sm text-g-600;
|
|
||||||
|
|
||||||
span {
|
|
||||||
@apply ml-2 font-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
// 定义基础变量
|
|
||||||
$bg-animation-color-light: #000;
|
|
||||||
$bg-animation-color-dark: #fff;
|
|
||||||
$bg-animation-duration: 0.5s;
|
|
||||||
|
|
||||||
html {
|
|
||||||
--bg-animation-color: $bg-animation-color-light;
|
|
||||||
|
|
||||||
&.dark {
|
|
||||||
--bg-animation-color: $bg-animation-color-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
// View transition styles
|
|
||||||
&::view-transition-old(*) {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::view-transition-new(*) {
|
|
||||||
animation: clip $bg-animation-duration ease-in both;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::view-transition-old(root) {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::view-transition-new(root) {
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dark {
|
|
||||||
&::view-transition-old(*) {
|
|
||||||
animation: clip $bg-animation-duration ease-in reverse both;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::view-transition-new(*) {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::view-transition-old(root) {
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::view-transition-new(root) {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义动画
|
|
||||||
@keyframes clip {
|
|
||||||
from {
|
|
||||||
clip-path: circle(0% at var(--x) var(--y));
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
clip-path: circle(var(--r) at var(--x) var(--y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// body 相关样式
|
|
||||||
body {
|
|
||||||
background-color: var(--bg-animation-color);
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
// 主题切换过渡优化,优化除视觉上的不适感
|
|
||||||
.theme-change {
|
|
||||||
* {
|
|
||||||
transition: 0s !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-switch__core,
|
|
||||||
.el-switch__action {
|
|
||||||
transition: all 0.3s !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
.hljs {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
color: #a6accd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-deletion {
|
|
||||||
color: #aed07e !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-quote {
|
|
||||||
color: #6f747d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-doctag,
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-formula {
|
|
||||||
color: #c792ea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-name,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-deletion,
|
|
||||||
.hljs-subst {
|
|
||||||
color: #c86068;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-literal {
|
|
||||||
color: #56b6c2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-regexp,
|
|
||||||
.hljs-addition,
|
|
||||||
.hljs-attribute,
|
|
||||||
.hljs-meta-string {
|
|
||||||
color: #abb2bf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-attribute {
|
|
||||||
color: #c792ea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-function {
|
|
||||||
color: #c792ea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-type {
|
|
||||||
color: #f07178;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-title {
|
|
||||||
color: #82aaff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-built_in,
|
|
||||||
.hljs-class {
|
|
||||||
color: #82aaff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 括号
|
|
||||||
.hljs-params {
|
|
||||||
color: #a6accd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-attr,
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-selector-attr,
|
|
||||||
.hljs-selector-pseudo,
|
|
||||||
.hljs-number {
|
|
||||||
color: #de7e61;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-link,
|
|
||||||
.hljs-meta,
|
|
||||||
.hljs-selector-id {
|
|
||||||
color: #61aeee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-link {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
// 重置默认样式
|
|
||||||
@use './core/reset.scss';
|
|
||||||
|
|
||||||
// 应用全局样式
|
|
||||||
@use './core/app.scss';
|
|
||||||
|
|
||||||
// Element Plus 样式优化
|
|
||||||
@use './core/el-ui.scss';
|
|
||||||
|
|
||||||
// Element Plus 暗黑主题
|
|
||||||
@use './core/el-dark.scss';
|
|
||||||
|
|
||||||
// 暗黑主题样式优化
|
|
||||||
@use './core/dark.scss';
|
|
||||||
|
|
||||||
// 路由切换动画
|
|
||||||
@use './core/router-transition';
|
|
||||||
|
|
||||||
// 主题切换过渡优化
|
|
||||||
@use './core/theme-change.scss';
|
|
||||||
|
|
||||||
// 主题切换圆形扩散动画
|
|
||||||
@use './core/theme-animation.scss';
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
// 自定义四点旋转SVG
|
|
||||||
export const fourDotsSpinnerSvg = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
|
||||||
<style>
|
|
||||||
.spinner {
|
|
||||||
transform-origin: 20px 20px;
|
|
||||||
animation: rotate 1.6s linear infinite;
|
|
||||||
}
|
|
||||||
.dot {
|
|
||||||
fill: var(--theme-color);
|
|
||||||
animation: fade 1.6s infinite;
|
|
||||||
}
|
|
||||||
.dot:nth-child(1) { animation-delay: 0s; }
|
|
||||||
.dot:nth-child(2) { animation-delay: 0.5s; }
|
|
||||||
.dot:nth-child(3) { animation-delay: 1s; }
|
|
||||||
.dot:nth-child(4) { animation-delay: 1.5s; }
|
|
||||||
@keyframes rotate {
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
@keyframes fade {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<g class="spinner">
|
|
||||||
<circle class="dot" cx="20" cy="8" r="4"/>
|
|
||||||
<circle class="dot" cx="32" cy="20" r="4"/>
|
|
||||||
<circle class="dot" cx="20" cy="32" r="4"/>
|
|
||||||
<circle class="dot" cx="8" cy="20" r="4"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
`
|
|
||||||
@ -1,343 +0,0 @@
|
|||||||
<!-- 基础横幅组件 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="art-card basic-banner"
|
|
||||||
:class="[{ 'has-decoration': decoration }, boxStyle]"
|
|
||||||
:style="{ height }"
|
|
||||||
@click="emit('click')"
|
|
||||||
>
|
|
||||||
<!-- 流星效果 -->
|
|
||||||
<div v-if="meteorConfig?.enabled && isDark" class="basic-banner__meteors">
|
|
||||||
<span
|
|
||||||
v-for="(meteor, index) in meteors"
|
|
||||||
:key="index"
|
|
||||||
class="meteor"
|
|
||||||
:style="{
|
|
||||||
top: '-60px',
|
|
||||||
left: `${meteor.x}%`,
|
|
||||||
animationDuration: `${meteor.speed}s`,
|
|
||||||
animationDelay: `${meteor.delay}s`
|
|
||||||
}"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="basic-banner__content">
|
|
||||||
<!-- title slot -->
|
|
||||||
<slot name="title">
|
|
||||||
<p v-if="title" class="basic-banner__title" :style="{ color: titleColor }">{{ title }}</p>
|
|
||||||
</slot>
|
|
||||||
|
|
||||||
<!-- subtitle slot -->
|
|
||||||
<slot name="subtitle">
|
|
||||||
<p v-if="subtitle" class="basic-banner__subtitle" :style="{ color: subtitleColor }">{{
|
|
||||||
subtitle
|
|
||||||
}}</p>
|
|
||||||
</slot>
|
|
||||||
|
|
||||||
<!-- button slot -->
|
|
||||||
<slot name="button">
|
|
||||||
<div
|
|
||||||
v-if="buttonConfig?.show"
|
|
||||||
class="basic-banner__button"
|
|
||||||
:style="{
|
|
||||||
backgroundColor: buttonColor,
|
|
||||||
color: buttonTextColor,
|
|
||||||
borderRadius: buttonRadius
|
|
||||||
}"
|
|
||||||
@click.stop="emit('buttonClick')"
|
|
||||||
>
|
|
||||||
{{ buttonConfig?.text }}
|
|
||||||
</div>
|
|
||||||
</slot>
|
|
||||||
|
|
||||||
<!-- default slot -->
|
|
||||||
<slot></slot>
|
|
||||||
|
|
||||||
<!-- background image -->
|
|
||||||
<img
|
|
||||||
v-if="imageConfig.src"
|
|
||||||
class="basic-banner__background-image"
|
|
||||||
:src="imageConfig.src"
|
|
||||||
:style="{ width: imageConfig.width, bottom: imageConfig.bottom, right: imageConfig.right }"
|
|
||||||
loading="lazy"
|
|
||||||
alt="背景图片"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, ref, computed } from 'vue'
|
|
||||||
import { useSettingStore } from '@/store/modules/setting'
|
|
||||||
const settingStore = useSettingStore()
|
|
||||||
const { isDark } = storeToRefs(settingStore)
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtBasicBanner' })
|
|
||||||
|
|
||||||
// 流星对象接口定义
|
|
||||||
interface Meteor {
|
|
||||||
/** 流星的水平位置(百分比) */
|
|
||||||
x: number
|
|
||||||
/** 流星划过的速度 */
|
|
||||||
speed: number
|
|
||||||
/** 流星出现的延迟时间 */
|
|
||||||
delay: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮配置接口定义
|
|
||||||
interface ButtonConfig {
|
|
||||||
/** 是否启用按钮 */
|
|
||||||
show: boolean
|
|
||||||
/** 按钮文本 */
|
|
||||||
text: string
|
|
||||||
/** 按钮背景色 */
|
|
||||||
color?: string
|
|
||||||
/** 按钮文字颜色 */
|
|
||||||
textColor?: string
|
|
||||||
/** 按钮圆角大小 */
|
|
||||||
radius?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 流星效果配置接口定义
|
|
||||||
interface MeteorConfig {
|
|
||||||
/** 是否启用流星效果 */
|
|
||||||
enabled: boolean
|
|
||||||
/** 流星数量 */
|
|
||||||
count?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// 背景图片配置接口定义
|
|
||||||
interface ImageConfig {
|
|
||||||
/** 图片源地址 */
|
|
||||||
src: string
|
|
||||||
/** 图片宽度 */
|
|
||||||
width?: string
|
|
||||||
/** 距底部距离 */
|
|
||||||
bottom?: string
|
|
||||||
/** 距右侧距离 */
|
|
||||||
right?: string // 距右侧距离
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件属性接口定义
|
|
||||||
interface Props {
|
|
||||||
/** 横幅高度 */
|
|
||||||
height?: string
|
|
||||||
/** 标题文本 */
|
|
||||||
title?: string
|
|
||||||
/** 副标题文本 */
|
|
||||||
subtitle?: string
|
|
||||||
/** 盒子样式 */
|
|
||||||
boxStyle?: string
|
|
||||||
/** 是否显示装饰效果 */
|
|
||||||
decoration?: boolean
|
|
||||||
/** 按钮配置 */
|
|
||||||
buttonConfig?: ButtonConfig
|
|
||||||
/** 流星配置 */
|
|
||||||
meteorConfig?: MeteorConfig
|
|
||||||
/** 图片配置 */
|
|
||||||
imageConfig?: ImageConfig
|
|
||||||
/** 标题颜色 */
|
|
||||||
titleColor?: string
|
|
||||||
/** 副标题颜色 */
|
|
||||||
subtitleColor?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件属性默认值设置
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
height: '11rem',
|
|
||||||
titleColor: 'white',
|
|
||||||
subtitleColor: 'white',
|
|
||||||
boxStyle: '!bg-theme/60',
|
|
||||||
decoration: true,
|
|
||||||
buttonConfig: () => ({
|
|
||||||
show: true,
|
|
||||||
text: '查看',
|
|
||||||
color: '#fff',
|
|
||||||
textColor: '#333',
|
|
||||||
radius: '6px'
|
|
||||||
}),
|
|
||||||
meteorConfig: () => ({ enabled: false, count: 10 }),
|
|
||||||
imageConfig: () => ({ src: '', width: '12rem', bottom: '-3rem', right: '0' })
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义组件事件
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'click'): void // 整体点击事件
|
|
||||||
(e: 'buttonClick'): void // 按钮点击事件
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 计算按钮样式属性
|
|
||||||
const buttonColor = computed(() => props.buttonConfig?.color ?? '#fff')
|
|
||||||
const buttonTextColor = computed(() => props.buttonConfig?.textColor ?? '#333')
|
|
||||||
const buttonRadius = computed(() => props.buttonConfig?.radius ?? '6px')
|
|
||||||
|
|
||||||
// 流星数据初始化
|
|
||||||
const meteors = ref<Meteor[]>([])
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.meteorConfig?.enabled) {
|
|
||||||
meteors.value = generateMeteors(props.meteorConfig?.count ?? 10)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成流星数据数组
|
|
||||||
* @param count 流星数量
|
|
||||||
* @returns 流星数据数组
|
|
||||||
*/
|
|
||||||
function generateMeteors(count: number): Meteor[] {
|
|
||||||
// 计算每个流星的区域宽度
|
|
||||||
const segmentWidth = 100 / count
|
|
||||||
return Array.from({ length: count }, (_, index) => {
|
|
||||||
// 计算流星起始位置
|
|
||||||
const segmentStart = index * segmentWidth
|
|
||||||
// 在区域内随机生成x坐标
|
|
||||||
const x = segmentStart + Math.random() * segmentWidth
|
|
||||||
// 随机决定流星速度快慢
|
|
||||||
const isSlow = Math.random() > 0.5
|
|
||||||
return {
|
|
||||||
x,
|
|
||||||
speed: isSlow ? 5 + Math.random() * 3 : 2 + Math.random() * 2,
|
|
||||||
delay: Math.random() * 5
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.basic-banner {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
color: white;
|
|
||||||
border-radius: calc(var(--custom-radius) + 2px) !important;
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__subtitle {
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
margin: 0 0 1.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__button {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 80px;
|
|
||||||
height: var(--el-component-custom-height);
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: var(--el-component-custom-height);
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__background-image {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: -3rem;
|
|
||||||
z-index: 0;
|
|
||||||
width: 12rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.has-decoration::after {
|
|
||||||
position: absolute;
|
|
||||||
right: -10%;
|
|
||||||
bottom: -20%;
|
|
||||||
width: 60%;
|
|
||||||
height: 140%;
|
|
||||||
content: '';
|
|
||||||
background: rgb(255 255 255 / 10%);
|
|
||||||
border-radius: 30%;
|
|
||||||
transform: rotate(-20deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__meteors {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
.meteor {
|
|
||||||
position: absolute;
|
|
||||||
width: 2px;
|
|
||||||
height: 60px;
|
|
||||||
background: linear-gradient(
|
|
||||||
to top,
|
|
||||||
rgb(255 255 255 / 40%),
|
|
||||||
rgb(255 255 255 / 10%),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
opacity: 0;
|
|
||||||
transform-origin: top left;
|
|
||||||
animation-name: meteor-fall;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
content: '';
|
|
||||||
background: rgb(255 255 255 / 50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes meteor-fall {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(0, -60px) rotate(-45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(400px, 340px) rotate(-45deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 640px) {
|
|
||||||
.basic-banner {
|
|
||||||
box-sizing: border-box;
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__background-image {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.has-decoration::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
<!-- 卡片横幅组件 -->
|
|
||||||
<template>
|
|
||||||
<div class="art-card-sm flex-c flex-col pb-6" :style="{ height: height }">
|
|
||||||
<div class="flex-c flex-col gap-4 text-center">
|
|
||||||
<div class="w-45">
|
|
||||||
<img :src="image" :alt="title" class="w-full h-full object-contain" />
|
|
||||||
</div>
|
|
||||||
<div class="box-border px-4">
|
|
||||||
<p class="mb-2 text-lg font-semibold text-g-800">{{ title }}</p>
|
|
||||||
<p class="m-0 text-sm text-g-600">{{ description }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex-c gap-3">
|
|
||||||
<div
|
|
||||||
v-if="cancelButton?.show"
|
|
||||||
class="inline-block h-9 px-3 text-sm/9 c-p select-none rounded-md border border-g-300"
|
|
||||||
:style="{
|
|
||||||
backgroundColor: cancelButton?.color,
|
|
||||||
color: cancelButton?.textColor
|
|
||||||
}"
|
|
||||||
@click="handleCancel"
|
|
||||||
>
|
|
||||||
{{ cancelButton?.text }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="button?.show"
|
|
||||||
class="inline-block h-9 px-3 text-sm/9 c-p select-none rounded-md"
|
|
||||||
:style="{ backgroundColor: button?.color, color: button?.textColor }"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
{{ button?.text }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// 导入默认图标
|
|
||||||
import defaultIcon from '@imgs/3d/icon1.webp'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtCardBanner' })
|
|
||||||
|
|
||||||
// 定义卡片横幅组件的属性接口
|
|
||||||
interface CardBannerProps {
|
|
||||||
/** 高度 */
|
|
||||||
height?: string
|
|
||||||
/** 图片路径 */
|
|
||||||
image?: string
|
|
||||||
/** 标题文本 */
|
|
||||||
title: string
|
|
||||||
/** 描述文本 */
|
|
||||||
description: string
|
|
||||||
/** 主按钮配置 */
|
|
||||||
button?: {
|
|
||||||
/** 是否显示 */
|
|
||||||
show?: boolean
|
|
||||||
/** 按钮文本 */
|
|
||||||
text?: string
|
|
||||||
/** 背景颜色 */
|
|
||||||
color?: string
|
|
||||||
/** 文字颜色 */
|
|
||||||
textColor?: string
|
|
||||||
}
|
|
||||||
/** 取消按钮配置 */
|
|
||||||
cancelButton?: {
|
|
||||||
/** 是否显示 */
|
|
||||||
show?: boolean
|
|
||||||
/** 按钮文本 */
|
|
||||||
text?: string
|
|
||||||
/** 背景颜色 */
|
|
||||||
color?: string
|
|
||||||
/** 文字颜色 */
|
|
||||||
textColor?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义组件属性默认值
|
|
||||||
withDefaults(defineProps<CardBannerProps>(), {
|
|
||||||
height: '24rem',
|
|
||||||
image: defaultIcon,
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
// 主按钮默认配置
|
|
||||||
button: () => ({
|
|
||||||
show: true,
|
|
||||||
text: '查看详情',
|
|
||||||
color: 'var(--theme-color)',
|
|
||||||
textColor: '#fff'
|
|
||||||
}),
|
|
||||||
// 取消按钮默认配置
|
|
||||||
cancelButton: () => ({
|
|
||||||
show: false,
|
|
||||||
text: '取消',
|
|
||||||
color: '#f5f5f5',
|
|
||||||
textColor: '#666'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义组件事件
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'click'): void // 主按钮点击事件
|
|
||||||
(e: 'cancel'): void // 取消按钮点击事件
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 主按钮点击处理函数
|
|
||||||
const handleClick = () => {
|
|
||||||
emit('click')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消按钮点击处理函数
|
|
||||||
const handleCancel = () => {
|
|
||||||
emit('cancel')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
<!-- 返回顶部按钮 -->
|
|
||||||
<template>
|
|
||||||
<Transition
|
|
||||||
enter-active-class="tad-300 ease-out"
|
|
||||||
leave-active-class="tad-200 ease-in"
|
|
||||||
enter-from-class="opacity-0 translate-y-2"
|
|
||||||
enter-to-class="opacity-100 translate-y-0"
|
|
||||||
leave-from-class="opacity-100 translate-y-0"
|
|
||||||
leave-to-class="opacity-0 translate-y-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-show="showButton"
|
|
||||||
class="fixed right-10 bottom-15 size-9.5 flex-cc c-p border border-g-300 rounded-md tad-300 hover:bg-g-200"
|
|
||||||
@click="scrollToTop"
|
|
||||||
>
|
|
||||||
<ArtSvgIcon icon="ri:arrow-up-wide-line" class="text-g-500 text-lg" />
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useCommon } from '@/hooks/core/useCommon'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtBackToTop' })
|
|
||||||
|
|
||||||
const { scrollToTop } = useCommon()
|
|
||||||
|
|
||||||
const showButton = ref(false)
|
|
||||||
const scrollThreshold = 300
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const scrollContainer = document.getElementById('app-main')
|
|
||||||
if (scrollContainer) {
|
|
||||||
const { y } = useScroll(scrollContainer)
|
|
||||||
watch(y, (newY: number) => {
|
|
||||||
showButton.value = newY > scrollThreshold
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<!-- 系统logo -->
|
|
||||||
<template>
|
|
||||||
<div class="flex-cc">
|
|
||||||
<img :style="logoStyle" src="@imgs/common/logo.webp" alt="logo" class="w-full h-full" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'ArtLogo' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** logo 大小 */
|
|
||||||
size?: number | string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
size: 36
|
|
||||||
})
|
|
||||||
|
|
||||||
const logoStyle = computed(() => ({ width: `${props.size}px` }))
|
|
||||||
</script>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<!-- 图标组件 -->
|
|
||||||
<template>
|
|
||||||
<Icon v-if="icon" :icon="icon" v-bind="bindAttrs" class="art-svg-icon inline" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtSvgIcon', inheritAttrs: false })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** Iconify icon name */
|
|
||||||
icon?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
|
|
||||||
const attrs = useAttrs()
|
|
||||||
|
|
||||||
const bindAttrs = computed<{ class: string; style: string }>(() => ({
|
|
||||||
class: (attrs.class as string) || '',
|
|
||||||
style: (attrs.style as string) || ''
|
|
||||||
}))
|
|
||||||
</script>
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
<!-- 柱状图卡片 -->
|
|
||||||
<template>
|
|
||||||
<div class="art-card relative overflow-hidden" :style="{ height: `${height}rem` }">
|
|
||||||
<div class="mb-5 flex-b items-start px-5 pt-5">
|
|
||||||
<div>
|
|
||||||
<p class="m-0 text-2xl font-medium leading-tight text-g-900">
|
|
||||||
{{ value }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-sm text-g-600">{{ label }}</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text-sm font-medium text-danger"
|
|
||||||
:class="[percentage > 0 ? 'text-success' : '', isMiniChart ? 'absolute bottom-5' : '']"
|
|
||||||
>
|
|
||||||
{{ percentage > 0 ? '+' : '' }}{{ percentage }}%
|
|
||||||
</div>
|
|
||||||
<div v-if="date" class="absolute bottom-5 right-5 text-xs text-g-600">
|
|
||||||
{{ date }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref="chartRef"
|
|
||||||
class="absolute bottom-0 left-0 right-0 mx-auto"
|
|
||||||
:class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
|
|
||||||
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import { type EChartsOption } from '@/plugins/echarts'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtBarChartCard' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 数值 */
|
|
||||||
value: number
|
|
||||||
/** 标签 */
|
|
||||||
label: string
|
|
||||||
/** 百分比 +(绿色)-(红色) */
|
|
||||||
percentage: number
|
|
||||||
/** 日期 */
|
|
||||||
date?: string
|
|
||||||
/** 高度 */
|
|
||||||
height?: number
|
|
||||||
/** 颜色 */
|
|
||||||
color?: string
|
|
||||||
/** 图表数据 */
|
|
||||||
chartData: number[]
|
|
||||||
/** 柱状图宽度 */
|
|
||||||
barWidth?: string
|
|
||||||
/** 是否为迷你图表 */
|
|
||||||
isMiniChart?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
height: 11,
|
|
||||||
barWidth: '26%'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const { chartRef } = useChartComponent({
|
|
||||||
props: {
|
|
||||||
height: `${props.height}rem`,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: !props.chartData?.length || props.chartData.every((val) => val === 0)
|
|
||||||
},
|
|
||||||
checkEmpty: () => !props.chartData?.length || props.chartData.every((val) => val === 0),
|
|
||||||
watchSources: [() => props.chartData, () => props.color, () => props.barWidth],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
const computedColor = props.color || useChartOps().themeColor
|
|
||||||
|
|
||||||
return {
|
|
||||||
grid: {
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 15,
|
|
||||||
left: 0
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
data: props.chartData,
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: props.barWidth,
|
|
||||||
itemStyle: {
|
|
||||||
color: computedColor,
|
|
||||||
borderRadius: 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
<!-- 数据列表卡片 -->
|
|
||||||
<template>
|
|
||||||
<div class="art-card p-5">
|
|
||||||
<div class="pb-3.5">
|
|
||||||
<p class="text-lg font-medium">{{ title }}</p>
|
|
||||||
<p class="text-sm text-g-600">{{ subtitle }}</p>
|
|
||||||
</div>
|
|
||||||
<ElScrollbar :style="{ height: maxHeight }">
|
|
||||||
<div v-for="(item, index) in list" :key="index" class="flex-c py-3">
|
|
||||||
<div v-if="item.icon" class="flex-cc mr-3 size-10 rounded-lg" :class="item.class">
|
|
||||||
<ArtSvgIcon :icon="item.icon" class="text-xl" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="mb-1 text-sm">{{ item.title }}</div>
|
|
||||||
<div class="text-xs text-g-500">{{ item.status }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-xs text-g-500">{{ item.time }}</div>
|
|
||||||
</div>
|
|
||||||
</ElScrollbar>
|
|
||||||
<ElButton
|
|
||||||
class="mt-[25px] w-full text-center"
|
|
||||||
v-if="showMoreButton"
|
|
||||||
v-ripple
|
|
||||||
@click="handleMore"
|
|
||||||
>查看更多</ElButton
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'ArtDataListCard' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 数据列表 */
|
|
||||||
list: Activity[]
|
|
||||||
/** 标题 */
|
|
||||||
title: string
|
|
||||||
/** 副标题 */
|
|
||||||
subtitle?: string
|
|
||||||
/** 最大显示数量 */
|
|
||||||
maxCount?: number
|
|
||||||
/** 是否显示更多按钮 */
|
|
||||||
showMoreButton?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Activity {
|
|
||||||
/** 标题 */
|
|
||||||
title: string
|
|
||||||
/** 状态 */
|
|
||||||
status: string
|
|
||||||
/** 时间 */
|
|
||||||
time: string
|
|
||||||
/** 样式类名 */
|
|
||||||
class: string
|
|
||||||
/** 图标 */
|
|
||||||
icon: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ITEM_HEIGHT = 66
|
|
||||||
const DEFAULT_MAX_COUNT = 5
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
maxCount: DEFAULT_MAX_COUNT
|
|
||||||
})
|
|
||||||
|
|
||||||
const maxHeight = computed(() => `${ITEM_HEIGHT * props.maxCount}px`)
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
/** 点击更多按钮事件 */
|
|
||||||
(e: 'more'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleMore = () => emit('more')
|
|
||||||
</script>
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
<!-- 环型图卡片 -->
|
|
||||||
<template>
|
|
||||||
<div class="art-card overflow-hidden" :style="{ height: `${height}rem` }">
|
|
||||||
<div class="flex box-border h-full p-5 pr-2">
|
|
||||||
<div class="flex w-full items-start gap-5">
|
|
||||||
<div class="flex-b h-full flex-1 flex-col">
|
|
||||||
<p class="m-0 text-xl font-medium leading-tight text-g-900">
|
|
||||||
{{ title }}
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<p class="m-0 mt-2.5 text-xl font-medium leading-tight text-g-900">
|
|
||||||
{{ formatNumber(value) }}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="mt-1.5 text-xs font-medium"
|
|
||||||
:class="percentage > 0 ? 'text-success' : 'text-danger'"
|
|
||||||
>
|
|
||||||
{{ percentage > 0 ? '+' : '' }}{{ percentage }}%
|
|
||||||
<span v-if="percentageLabel">{{ percentageLabel }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 flex gap-4 text-xs text-g-600">
|
|
||||||
<div v-if="currentValue" class="flex-cc">
|
|
||||||
<div class="size-2 bg-theme/100 rounded mr-2"></div>
|
|
||||||
{{ currentValue }}
|
|
||||||
</div>
|
|
||||||
<div v-if="previousValue" class="flex-cc">
|
|
||||||
<div class="size-2 bg-g-400 rounded mr-2"></div>
|
|
||||||
{{ previousValue }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-c h-full max-w-40 flex-1">
|
|
||||||
<div ref="chartRef" class="h-30 w-full"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { type EChartsOption } from '@/plugins/echarts'
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtDonutChartCard' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 数值 */
|
|
||||||
value: number
|
|
||||||
/** 标题 */
|
|
||||||
title: string
|
|
||||||
/** 百分比 */
|
|
||||||
percentage: number
|
|
||||||
/** 百分比标签 */
|
|
||||||
percentageLabel?: string
|
|
||||||
/** 当前年份 */
|
|
||||||
currentValue?: string
|
|
||||||
/** 去年年份 */
|
|
||||||
previousValue?: string
|
|
||||||
/** 高度 */
|
|
||||||
height?: number
|
|
||||||
/** 颜色 */
|
|
||||||
color?: string
|
|
||||||
/** 半径 */
|
|
||||||
radius?: [string, string]
|
|
||||||
/** 数据 */
|
|
||||||
data: [number, number]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
height: 9,
|
|
||||||
radius: () => ['70%', '90%'],
|
|
||||||
data: () => [0, 0]
|
|
||||||
})
|
|
||||||
|
|
||||||
const formatNumber = (num: number) => {
|
|
||||||
return num.toLocaleString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const { chartRef } = useChartComponent({
|
|
||||||
props: {
|
|
||||||
height: `${props.height}rem`,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: props.data.every((val) => val === 0)
|
|
||||||
},
|
|
||||||
checkEmpty: () => props.data.every((val) => val === 0),
|
|
||||||
watchSources: [
|
|
||||||
() => props.data,
|
|
||||||
() => props.color,
|
|
||||||
() => props.radius,
|
|
||||||
() => props.currentValue,
|
|
||||||
() => props.previousValue
|
|
||||||
],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
const computedColor = props.color || useChartOps().themeColor
|
|
||||||
|
|
||||||
return {
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'pie',
|
|
||||||
radius: props.radius,
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
label: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: props.data[0],
|
|
||||||
name: props.currentValue,
|
|
||||||
itemStyle: { color: computedColor }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: props.data[1],
|
|
||||||
name: props.previousValue,
|
|
||||||
itemStyle: { color: '#e6e8f7' }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
<!-- 图片卡片 -->
|
|
||||||
<template>
|
|
||||||
<div class="w-full c-p" @click="handleClick">
|
|
||||||
<div class="art-card overflow-hidden">
|
|
||||||
<div class="relative w-full aspect-[16/10] overflow-hidden">
|
|
||||||
<ElImage
|
|
||||||
:src="props.imageUrl"
|
|
||||||
fit="cover"
|
|
||||||
loading="lazy"
|
|
||||||
class="w-full h-full transition-transform duration-300 ease-in-out hover:scale-105"
|
|
||||||
>
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="flex-cc w-full h-full bg-[#f5f7fa]">
|
|
||||||
<ElIcon><Picture /></ElIcon>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ElImage>
|
|
||||||
<div
|
|
||||||
class="absolute right-3.5 bottom-3.5 py-1 px-2 text-xs bg-g-200 rounded"
|
|
||||||
v-if="props.readTime"
|
|
||||||
>
|
|
||||||
{{ props.readTime }} 阅读
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4">
|
|
||||||
<div
|
|
||||||
class="inline-block py-0.5 px-2 mb-2 text-xs bg-g-300/70 rounded"
|
|
||||||
v-if="props.category"
|
|
||||||
>
|
|
||||||
{{ props.category }}
|
|
||||||
</div>
|
|
||||||
<p class="m-0 mb-3 text-base font-medium">{{ props.title }}</p>
|
|
||||||
<div class="flex-c gap-4 text-xs text-g-600">
|
|
||||||
<span class="flex-c gap-1" v-if="props.views">
|
|
||||||
<ElIcon class="text-base"><View /></ElIcon>
|
|
||||||
{{ props.views }}
|
|
||||||
</span>
|
|
||||||
<span class="flex-c gap-1" v-if="props.comments">
|
|
||||||
<ElIcon class="text-base"><ChatLineRound /></ElIcon>
|
|
||||||
{{ props.comments }}
|
|
||||||
</span>
|
|
||||||
<span>{{ props.date }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Picture, View, ChatLineRound } from '@element-plus/icons-vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtImageCard' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 图片地址 */
|
|
||||||
imageUrl: string
|
|
||||||
/** 标题 */
|
|
||||||
title: string
|
|
||||||
/** 分类 */
|
|
||||||
category?: string
|
|
||||||
/** 阅读时间 */
|
|
||||||
readTime?: string
|
|
||||||
/** 浏览量 */
|
|
||||||
views?: number
|
|
||||||
/** 评论数 */
|
|
||||||
comments?: number
|
|
||||||
/** 日期 */
|
|
||||||
date?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
imageUrl: '',
|
|
||||||
title: '',
|
|
||||||
category: '',
|
|
||||||
readTime: '',
|
|
||||||
views: 0,
|
|
||||||
comments: 0,
|
|
||||||
date: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'click', card: Props): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
emit('click', props)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
<!-- 折线图卡片 -->
|
|
||||||
<template>
|
|
||||||
<div class="art-card relative overflow-hidden" :style="{ height: `${height}rem` }">
|
|
||||||
<div class="mb-2.5 flex-b items-start p-5">
|
|
||||||
<div>
|
|
||||||
<p class="text-2xl font-medium leading-none">
|
|
||||||
{{ value }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-sm text-g-500">{{ label }}</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text-sm font-medium"
|
|
||||||
:class="[
|
|
||||||
percentage > 0 ? 'text-success' : 'text-danger',
|
|
||||||
isMiniChart ? 'absolute bottom-5' : ''
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ percentage > 0 ? '+' : '' }}{{ percentage }}%
|
|
||||||
</div>
|
|
||||||
<div v-if="date" class="absolute bottom-5 right-5 text-xs text-g-500">
|
|
||||||
{{ date }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref="chartRef"
|
|
||||||
class="absolute bottom-0 left-0 right-0 box-border w-full"
|
|
||||||
:class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
|
|
||||||
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { graphic, type EChartsOption } from '@/plugins/echarts'
|
|
||||||
import { getCssVar, hexToRgba } from '@/utils/ui'
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtLineChartCard' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 数值 */
|
|
||||||
value: number
|
|
||||||
/** 标签 */
|
|
||||||
label: string
|
|
||||||
/** 百分比 */
|
|
||||||
percentage: number
|
|
||||||
/** 日期 */
|
|
||||||
date?: string
|
|
||||||
/** 高度 */
|
|
||||||
height?: number
|
|
||||||
/** 颜色 */
|
|
||||||
color?: string
|
|
||||||
/** 是否显示区域颜色 */
|
|
||||||
showAreaColor?: boolean
|
|
||||||
/** 图表数据 */
|
|
||||||
chartData: number[]
|
|
||||||
/** 是否为迷你图表 */
|
|
||||||
isMiniChart?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
height: 11
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const { chartRef } = useChartComponent({
|
|
||||||
props: {
|
|
||||||
height: `${props.height}rem`,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: !props.chartData?.length || props.chartData.every((val) => val === 0)
|
|
||||||
},
|
|
||||||
checkEmpty: () => !props.chartData?.length || props.chartData.every((val) => val === 0),
|
|
||||||
watchSources: [() => props.chartData, () => props.color, () => props.showAreaColor],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
const computedColor = props.color || useChartOps().themeColor
|
|
||||||
|
|
||||||
return {
|
|
||||||
grid: {
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
show: false,
|
|
||||||
boundaryGap: false
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
data: props.chartData,
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
showSymbol: false,
|
|
||||||
lineStyle: {
|
|
||||||
width: 3,
|
|
||||||
color: computedColor
|
|
||||||
},
|
|
||||||
areaStyle: props.showAreaColor
|
|
||||||
? {
|
|
||||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: props.color
|
|
||||||
? hexToRgba(props.color, 0.2).rgba
|
|
||||||
: hexToRgba(getCssVar('--el-color-primary'), 0.2).rgba
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: props.color
|
|
||||||
? hexToRgba(props.color, 0.01).rgba
|
|
||||||
: hexToRgba(getCssVar('--el-color-primary'), 0.01).rgba
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
<!-- 进度条卡片 -->
|
|
||||||
<template>
|
|
||||||
<div class="art-card h-32 flex flex-col justify-center px-5">
|
|
||||||
<div class="mb-3.5 flex-c" :style="{ justifyContent: icon ? 'space-between' : 'flex-start' }">
|
|
||||||
<div v-if="icon" class="size-11 flex-cc bg-g-300 text-xl rounded-lg" :class="iconStyle">
|
|
||||||
<ArtSvgIcon :icon="icon" class="text-2xl"></ArtSvgIcon>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ArtCountTo
|
|
||||||
class="mb-1 block text-2xl font-semibold"
|
|
||||||
:target="percentage"
|
|
||||||
:duration="2000"
|
|
||||||
suffix="%"
|
|
||||||
:style="{ textAlign: icon ? 'right' : 'left' }"
|
|
||||||
/>
|
|
||||||
<p class="text-sm text-g-500">{{ title }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ElProgress
|
|
||||||
:percentage="currentPercentage"
|
|
||||||
:stroke-width="strokeWidth"
|
|
||||||
:show-text="false"
|
|
||||||
:color="color"
|
|
||||||
class="[&_.el-progress-bar__outer]:bg-[rgb(240_240_240)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'ArtProgressCard' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 进度百分比 */
|
|
||||||
percentage: number
|
|
||||||
/** 标题 */
|
|
||||||
title: string
|
|
||||||
/** 颜色 */
|
|
||||||
color?: string
|
|
||||||
/** 图标 */
|
|
||||||
icon?: string
|
|
||||||
/** 图标样式 */
|
|
||||||
iconStyle?: string
|
|
||||||
/** 进度条宽度 */
|
|
||||||
strokeWidth?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
strokeWidth: 5,
|
|
||||||
color: '#67C23A'
|
|
||||||
})
|
|
||||||
|
|
||||||
const animationDuration = 500
|
|
||||||
const currentPercentage = ref(0)
|
|
||||||
|
|
||||||
const animateProgress = () => {
|
|
||||||
const startTime = Date.now()
|
|
||||||
const startValue = currentPercentage.value
|
|
||||||
const endValue = props.percentage
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
const currentTime = Date.now()
|
|
||||||
const elapsed = currentTime - startTime
|
|
||||||
const progress = Math.min(elapsed / animationDuration, 1)
|
|
||||||
|
|
||||||
currentPercentage.value = startValue + (endValue - startValue) * progress
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
animateProgress()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 当 percentage 属性变化时重新执行动画
|
|
||||||
watch(
|
|
||||||
() => props.percentage,
|
|
||||||
() => {
|
|
||||||
animateProgress()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
<!-- 统计卡片 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="art-card h-32 flex-c px-5 transition-transform duration-200 hover:-translate-y-0.5"
|
|
||||||
:class="boxStyle"
|
|
||||||
>
|
|
||||||
<div v-if="icon" class="mr-4 size-11 flex-cc rounded-lg text-xl text-white" :class="iconStyle">
|
|
||||||
<ArtSvgIcon :icon="icon"></ArtSvgIcon>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="m-0 text-lg font-medium" :style="{ color: textColor }" v-if="title">
|
|
||||||
{{ title }}
|
|
||||||
</p>
|
|
||||||
<ArtCountTo
|
|
||||||
class="m-0 text-2xl font-medium"
|
|
||||||
v-if="count !== undefined"
|
|
||||||
:target="count"
|
|
||||||
:duration="2000"
|
|
||||||
:decimals="decimals"
|
|
||||||
:separator="separator"
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
class="mt-1 text-sm text-g-500 opacity-90"
|
|
||||||
:style="{ color: textColor }"
|
|
||||||
v-if="description"
|
|
||||||
>{{ description }}</p
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div v-if="showArrow">
|
|
||||||
<ArtSvgIcon icon="ri:arrow-right-s-line" class="text-xl text-g-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'ArtStatsCard' })
|
|
||||||
|
|
||||||
interface StatsCardProps {
|
|
||||||
/** 盒子样式 */
|
|
||||||
boxStyle?: string
|
|
||||||
/** 图标 */
|
|
||||||
icon?: string
|
|
||||||
/** 图标样式 */
|
|
||||||
iconStyle?: string
|
|
||||||
/** 标题 */
|
|
||||||
title?: string
|
|
||||||
/** 数值 */
|
|
||||||
count?: number
|
|
||||||
/** 小数位 */
|
|
||||||
decimals?: number
|
|
||||||
/** 分隔符 */
|
|
||||||
separator?: string
|
|
||||||
/** 描述 */
|
|
||||||
description: string
|
|
||||||
/** 文本颜色 */
|
|
||||||
textColor?: string
|
|
||||||
/** 是否显示箭头 */
|
|
||||||
showArrow?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<StatsCardProps>(), {
|
|
||||||
iconSize: 30,
|
|
||||||
iconBgRadius: 50,
|
|
||||||
decimals: 0,
|
|
||||||
separator: ','
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
<!-- 时间轴列表卡片 -->
|
|
||||||
<template>
|
|
||||||
<div class="art-card p-5">
|
|
||||||
<div class="pb-3.5">
|
|
||||||
<p class="text-lg font-medium">{{ title }}</p>
|
|
||||||
<p class="text-sm text-g-600">{{ subtitle }}</p>
|
|
||||||
</div>
|
|
||||||
<ElScrollbar :style="{ height: maxHeight }">
|
|
||||||
<ElTimeline class="!pl-0.5">
|
|
||||||
<ElTimelineItem
|
|
||||||
v-for="item in list"
|
|
||||||
:key="item.time"
|
|
||||||
:timestamp="item.time"
|
|
||||||
:placement="TIMELINE_PLACEMENT"
|
|
||||||
:color="item.status"
|
|
||||||
:center="true"
|
|
||||||
>
|
|
||||||
<div class="flex-c gap-3">
|
|
||||||
<div class="flex-c gap-2">
|
|
||||||
<span class="text-sm">{{ item.content }}</span>
|
|
||||||
<span v-if="item.code" class="text-sm text-theme"> #{{ item.code }} </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ElTimelineItem>
|
|
||||||
</ElTimeline>
|
|
||||||
</ElScrollbar>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'ArtTimelineListCard' })
|
|
||||||
|
|
||||||
// 常量配置
|
|
||||||
const ITEM_HEIGHT = 65
|
|
||||||
const TIMELINE_PLACEMENT = 'top'
|
|
||||||
const DEFAULT_MAX_COUNT = 5
|
|
||||||
|
|
||||||
interface TimelineItem {
|
|
||||||
/** 时间 */
|
|
||||||
time: string
|
|
||||||
/** 状态颜色 */
|
|
||||||
status: string
|
|
||||||
/** 内容 */
|
|
||||||
content: string
|
|
||||||
/** 代码标识 */
|
|
||||||
code?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 时间轴列表数据 */
|
|
||||||
list: TimelineItem[]
|
|
||||||
/** 标题 */
|
|
||||||
title: string
|
|
||||||
/** 副标题 */
|
|
||||||
subtitle?: string
|
|
||||||
/** 最大显示数量 */
|
|
||||||
maxCount?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Props 定义和验证
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
title: '',
|
|
||||||
subtitle: '',
|
|
||||||
maxCount: DEFAULT_MAX_COUNT
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算最大高度
|
|
||||||
const maxHeight = computed(() => `${ITEM_HEIGHT * props.maxCount}px`)
|
|
||||||
</script>
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
<!-- 柱状图 -->
|
|
||||||
<template>
|
|
||||||
<div ref="chartRef" :style="{ height: props.height }" v-loading="props.loading"> </div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import { getCssVar } from '@/utils/ui'
|
|
||||||
import { graphic, type EChartsOption } from '@/plugins/echarts'
|
|
||||||
import type { BarChartProps, BarDataItem } from '@/types/component/chart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtBarChart' })
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<BarChartProps>(), {
|
|
||||||
// 基础配置
|
|
||||||
height: useChartOps().chartHeight,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: false,
|
|
||||||
colors: () => useChartOps().colors,
|
|
||||||
borderRadius: 4,
|
|
||||||
|
|
||||||
// 数据配置
|
|
||||||
data: () => [0, 0, 0, 0, 0, 0, 0],
|
|
||||||
xAxisData: () => [],
|
|
||||||
barWidth: '40%',
|
|
||||||
stack: false,
|
|
||||||
|
|
||||||
// 轴线显示配置
|
|
||||||
showAxisLabel: true,
|
|
||||||
showAxisLine: true,
|
|
||||||
showSplitLine: true,
|
|
||||||
|
|
||||||
// 交互配置
|
|
||||||
showTooltip: true,
|
|
||||||
showLegend: false,
|
|
||||||
legendPosition: 'bottom'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 判断是否为多数据
|
|
||||||
const isMultipleData = computed(() => {
|
|
||||||
return (
|
|
||||||
Array.isArray(props.data) &&
|
|
||||||
props.data.length > 0 &&
|
|
||||||
typeof props.data[0] === 'object' &&
|
|
||||||
'name' in props.data[0]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取颜色配置
|
|
||||||
const getColor = (customColor?: string, index?: number) => {
|
|
||||||
if (customColor) return customColor
|
|
||||||
|
|
||||||
if (index !== undefined) {
|
|
||||||
return props.colors![index % props.colors!.length]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认渐变色
|
|
||||||
return new graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: getCssVar('--el-color-primary-light-4')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: getCssVar('--el-color-primary')
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建渐变色
|
|
||||||
const createGradientColor = (color: string) => {
|
|
||||||
return new graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: color
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: color
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取基础样式配置
|
|
||||||
const getBaseItemStyle = (
|
|
||||||
color: string | InstanceType<typeof graphic.LinearGradient> | undefined
|
|
||||||
) => ({
|
|
||||||
borderRadius: props.borderRadius,
|
|
||||||
color: typeof color === 'string' ? createGradientColor(color) : color
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建系列配置
|
|
||||||
const createSeriesItem = (config: {
|
|
||||||
name?: string
|
|
||||||
data: number[]
|
|
||||||
color?: string | InstanceType<typeof graphic.LinearGradient>
|
|
||||||
barWidth?: string | number
|
|
||||||
stack?: string
|
|
||||||
}) => {
|
|
||||||
const animationConfig = getAnimationConfig()
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: config.name,
|
|
||||||
data: config.data,
|
|
||||||
type: 'bar' as const,
|
|
||||||
stack: config.stack,
|
|
||||||
itemStyle: getBaseItemStyle(config.color),
|
|
||||||
barWidth: config.barWidth || props.barWidth,
|
|
||||||
...animationConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const {
|
|
||||||
chartRef,
|
|
||||||
getAxisLineStyle,
|
|
||||||
getAxisLabelStyle,
|
|
||||||
getAxisTickStyle,
|
|
||||||
getSplitLineStyle,
|
|
||||||
getAnimationConfig,
|
|
||||||
getTooltipStyle,
|
|
||||||
getLegendStyle,
|
|
||||||
getGridWithLegend
|
|
||||||
} = useChartComponent({
|
|
||||||
props,
|
|
||||||
checkEmpty: () => {
|
|
||||||
// 检查单数据情况
|
|
||||||
if (Array.isArray(props.data) && typeof props.data[0] === 'number') {
|
|
||||||
const singleData = props.data as number[]
|
|
||||||
return !singleData.length || singleData.every((val) => val === 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查多数据情况
|
|
||||||
if (Array.isArray(props.data) && typeof props.data[0] === 'object') {
|
|
||||||
const multiData = props.data as BarDataItem[]
|
|
||||||
return (
|
|
||||||
!multiData.length ||
|
|
||||||
multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
const options: EChartsOption = {
|
|
||||||
grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
|
|
||||||
top: 15,
|
|
||||||
right: 0,
|
|
||||||
left: 0
|
|
||||||
}),
|
|
||||||
tooltip: props.showTooltip ? getTooltipStyle() : undefined,
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: props.xAxisData,
|
|
||||||
axisTick: getAxisTickStyle(),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel)
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
splitLine: getSplitLineStyle(props.showSplitLine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加图例配置
|
|
||||||
if (props.showLegend && isMultipleData.value) {
|
|
||||||
options.legend = getLegendStyle(props.legendPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成系列数据
|
|
||||||
if (isMultipleData.value) {
|
|
||||||
const multiData = props.data as BarDataItem[]
|
|
||||||
options.series = multiData.map((item, index) => {
|
|
||||||
const computedColor = getColor(props.colors[index], index)
|
|
||||||
|
|
||||||
return createSeriesItem({
|
|
||||||
name: item.name,
|
|
||||||
data: item.data,
|
|
||||||
color: computedColor,
|
|
||||||
barWidth: item.barWidth,
|
|
||||||
stack: props.stack ? item.stack || 'total' : undefined
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 单数据情况
|
|
||||||
const singleData = props.data as number[]
|
|
||||||
const computedColor = getColor()
|
|
||||||
|
|
||||||
options.series = [
|
|
||||||
createSeriesItem({
|
|
||||||
data: singleData,
|
|
||||||
color: computedColor
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,195 +0,0 @@
|
|||||||
<!-- 双向堆叠柱状图 -->
|
|
||||||
<template>
|
|
||||||
<div ref="chartRef" :style="{ height: props.height }" v-loading="props.loading"> </div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import type { EChartsOption, BarSeriesOption } from '@/plugins/echarts'
|
|
||||||
import type { BidirectionalBarChartProps } from '@/types/component/chart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtDualBarCompareChart' })
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<BidirectionalBarChartProps>(), {
|
|
||||||
// 基础配置
|
|
||||||
height: useChartOps().chartHeight,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: false,
|
|
||||||
colors: () => useChartOps().colors,
|
|
||||||
|
|
||||||
// 数据配置
|
|
||||||
positiveData: () => [],
|
|
||||||
negativeData: () => [],
|
|
||||||
xAxisData: () => [],
|
|
||||||
positiveName: '正向数据',
|
|
||||||
negativeName: '负向数据',
|
|
||||||
barWidth: 16,
|
|
||||||
yAxisMin: -100,
|
|
||||||
yAxisMax: 100,
|
|
||||||
|
|
||||||
// 样式配置
|
|
||||||
showDataLabel: false,
|
|
||||||
positiveBorderRadius: () => [10, 10, 0, 0],
|
|
||||||
negativeBorderRadius: () => [0, 0, 10, 10],
|
|
||||||
|
|
||||||
// 轴线显示配置
|
|
||||||
showAxisLabel: true,
|
|
||||||
showAxisLine: false,
|
|
||||||
showSplitLine: false,
|
|
||||||
|
|
||||||
// 交互配置
|
|
||||||
showTooltip: true,
|
|
||||||
showLegend: false,
|
|
||||||
legendPosition: 'bottom'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建系列配置的辅助函数
|
|
||||||
const createSeriesConfig = (config: {
|
|
||||||
name: string
|
|
||||||
data: number[]
|
|
||||||
borderRadius: number | number[]
|
|
||||||
labelPosition: 'top' | 'bottom'
|
|
||||||
colorIndex: number
|
|
||||||
formatter?: (params: unknown) => string
|
|
||||||
}): BarSeriesOption => {
|
|
||||||
const { fontColor } = useChartOps()
|
|
||||||
const animationConfig = getAnimationConfig()
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: config.name,
|
|
||||||
type: 'bar',
|
|
||||||
stack: 'total',
|
|
||||||
barWidth: props.barWidth,
|
|
||||||
barGap: '-100%',
|
|
||||||
data: config.data,
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: config.borderRadius,
|
|
||||||
color: props.colors[config.colorIndex]
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: props.showDataLabel,
|
|
||||||
position: config.labelPosition,
|
|
||||||
formatter:
|
|
||||||
config.formatter ||
|
|
||||||
((params: unknown) => String((params as Record<string, unknown>).value)),
|
|
||||||
color: fontColor,
|
|
||||||
fontSize: 12
|
|
||||||
},
|
|
||||||
...animationConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用图表组件抽象
|
|
||||||
const {
|
|
||||||
chartRef,
|
|
||||||
getAxisLineStyle,
|
|
||||||
getAxisLabelStyle,
|
|
||||||
getAxisTickStyle,
|
|
||||||
getSplitLineStyle,
|
|
||||||
getAnimationConfig,
|
|
||||||
getTooltipStyle,
|
|
||||||
getLegendStyle,
|
|
||||||
getGridWithLegend
|
|
||||||
} = useChartComponent({
|
|
||||||
props,
|
|
||||||
checkEmpty: () => {
|
|
||||||
return (
|
|
||||||
props.isEmpty ||
|
|
||||||
!props.positiveData.length ||
|
|
||||||
!props.negativeData.length ||
|
|
||||||
(props.positiveData.every((val) => val === 0) &&
|
|
||||||
props.negativeData.every((val) => val === 0))
|
|
||||||
)
|
|
||||||
},
|
|
||||||
watchSources: [
|
|
||||||
() => props.positiveData,
|
|
||||||
() => props.negativeData,
|
|
||||||
() => props.xAxisData,
|
|
||||||
() => props.colors
|
|
||||||
],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
// 处理负向数据,确保为负值
|
|
||||||
const processedNegativeData = props.negativeData.map((val) => (val > 0 ? -val : val))
|
|
||||||
|
|
||||||
// 优化的Grid配置
|
|
||||||
const gridConfig = {
|
|
||||||
top: props.showLegend ? 50 : 20,
|
|
||||||
right: 0,
|
|
||||||
left: 0,
|
|
||||||
bottom: 0, // 增加底部间距
|
|
||||||
containLabel: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: EChartsOption = {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
animation: true,
|
|
||||||
animationDuration: 1000,
|
|
||||||
animationEasing: 'cubicOut',
|
|
||||||
grid: getGridWithLegend(props.showLegend, props.legendPosition, gridConfig),
|
|
||||||
|
|
||||||
// 优化的提示框配置
|
|
||||||
tooltip: props.showTooltip
|
|
||||||
? {
|
|
||||||
...getTooltipStyle(),
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: {
|
|
||||||
type: 'none' // 去除指示线
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
|
|
||||||
// 图例配置
|
|
||||||
legend: props.showLegend
|
|
||||||
? {
|
|
||||||
...getLegendStyle(props.legendPosition),
|
|
||||||
data: [props.negativeName, props.positiveName]
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
|
|
||||||
// X轴配置
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: props.xAxisData,
|
|
||||||
axisTick: getAxisTickStyle(),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
|
||||||
boundaryGap: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// Y轴配置
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
min: props.yAxisMin,
|
|
||||||
max: props.yAxisMax,
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
splitLine: getSplitLineStyle(props.showSplitLine)
|
|
||||||
},
|
|
||||||
|
|
||||||
// 系列配置
|
|
||||||
series: [
|
|
||||||
// 负向数据系列
|
|
||||||
createSeriesConfig({
|
|
||||||
name: props.negativeName,
|
|
||||||
data: processedNegativeData,
|
|
||||||
borderRadius: props.negativeBorderRadius,
|
|
||||||
labelPosition: 'bottom',
|
|
||||||
colorIndex: 1,
|
|
||||||
formatter: (params: unknown) =>
|
|
||||||
String(Math.abs((params as Record<string, unknown>).value as number))
|
|
||||||
}),
|
|
||||||
// 正向数据系列
|
|
||||||
createSeriesConfig({
|
|
||||||
name: props.positiveName,
|
|
||||||
data: props.positiveData,
|
|
||||||
borderRadius: props.positiveBorderRadius,
|
|
||||||
labelPosition: 'top',
|
|
||||||
colorIndex: 0
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,208 +0,0 @@
|
|||||||
<!-- 水平柱状图 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="chartRef"
|
|
||||||
class="relative w-full"
|
|
||||||
:style="{ height: props.height }"
|
|
||||||
v-loading="props.loading"
|
|
||||||
></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import { getCssVar } from '@/utils/ui'
|
|
||||||
import { graphic, type EChartsOption } from '@/plugins/echarts'
|
|
||||||
import type { BarChartProps, BarDataItem } from '@/types/component/chart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtHBarChart' })
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<BarChartProps>(), {
|
|
||||||
// 基础配置
|
|
||||||
height: useChartOps().chartHeight,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: false,
|
|
||||||
colors: () => useChartOps().colors,
|
|
||||||
|
|
||||||
// 数据配置
|
|
||||||
data: () => [0, 0, 0, 0, 0, 0, 0],
|
|
||||||
xAxisData: () => [],
|
|
||||||
barWidth: '36%',
|
|
||||||
stack: false,
|
|
||||||
|
|
||||||
// 轴线显示配置
|
|
||||||
showAxisLabel: true,
|
|
||||||
showAxisLine: true,
|
|
||||||
showSplitLine: true,
|
|
||||||
|
|
||||||
// 交互配置
|
|
||||||
showTooltip: true,
|
|
||||||
showLegend: false,
|
|
||||||
legendPosition: 'bottom'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 判断是否为多数据
|
|
||||||
const isMultipleData = computed(() => {
|
|
||||||
return (
|
|
||||||
Array.isArray(props.data) &&
|
|
||||||
props.data.length > 0 &&
|
|
||||||
typeof props.data[0] === 'object' &&
|
|
||||||
'name' in props.data[0]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取颜色配置
|
|
||||||
const getColor = (customColor?: string, index?: number) => {
|
|
||||||
if (customColor) return customColor
|
|
||||||
|
|
||||||
if (index !== undefined) {
|
|
||||||
return props.colors![index % props.colors!.length]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认渐变色
|
|
||||||
return new graphic.LinearGradient(0, 0, 1, 0, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: getCssVar('--el-color-primary')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: getCssVar('--el-color-primary-light-4')
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建渐变色
|
|
||||||
const createGradientColor = (color: string) => {
|
|
||||||
return new graphic.LinearGradient(0, 0, 1, 0, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: color
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: color
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取基础样式配置
|
|
||||||
const getBaseItemStyle = (
|
|
||||||
color: string | InstanceType<typeof graphic.LinearGradient> | undefined
|
|
||||||
) => ({
|
|
||||||
borderRadius: 4,
|
|
||||||
color: typeof color === 'string' ? createGradientColor(color) : color
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建系列配置
|
|
||||||
const createSeriesItem = (config: {
|
|
||||||
name?: string
|
|
||||||
data: number[]
|
|
||||||
color?: string | InstanceType<typeof graphic.LinearGradient>
|
|
||||||
barWidth?: string | number
|
|
||||||
stack?: string
|
|
||||||
}) => {
|
|
||||||
const animationConfig = getAnimationConfig()
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: config.name,
|
|
||||||
data: config.data,
|
|
||||||
type: 'bar' as const,
|
|
||||||
stack: config.stack,
|
|
||||||
itemStyle: getBaseItemStyle(config.color),
|
|
||||||
barWidth: config.barWidth || props.barWidth,
|
|
||||||
...animationConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const {
|
|
||||||
chartRef,
|
|
||||||
getAxisLineStyle,
|
|
||||||
getAxisLabelStyle,
|
|
||||||
getAxisTickStyle,
|
|
||||||
getSplitLineStyle,
|
|
||||||
getAnimationConfig,
|
|
||||||
getTooltipStyle,
|
|
||||||
getLegendStyle,
|
|
||||||
getGridWithLegend
|
|
||||||
} = useChartComponent({
|
|
||||||
props,
|
|
||||||
checkEmpty: () => {
|
|
||||||
// 检查单数据情况
|
|
||||||
if (Array.isArray(props.data) && typeof props.data[0] === 'number') {
|
|
||||||
const singleData = props.data as number[]
|
|
||||||
return !singleData.length || singleData.every((val) => val === 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查多数据情况
|
|
||||||
if (Array.isArray(props.data) && typeof props.data[0] === 'object') {
|
|
||||||
const multiData = props.data as BarDataItem[]
|
|
||||||
return (
|
|
||||||
!multiData.length ||
|
|
||||||
multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
const options: EChartsOption = {
|
|
||||||
grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
|
|
||||||
top: 15,
|
|
||||||
right: 0,
|
|
||||||
left: 0
|
|
||||||
}),
|
|
||||||
tooltip: props.showTooltip ? getTooltipStyle() : undefined,
|
|
||||||
xAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisTick: getAxisTickStyle(),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
|
||||||
splitLine: getSplitLineStyle(props.showSplitLine)
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: props.xAxisData,
|
|
||||||
axisTick: getAxisTickStyle(),
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加图例配置
|
|
||||||
if (props.showLegend && isMultipleData.value) {
|
|
||||||
options.legend = getLegendStyle(props.legendPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成系列数据
|
|
||||||
if (isMultipleData.value) {
|
|
||||||
const multiData = props.data as BarDataItem[]
|
|
||||||
options.series = multiData.map((item, index) => {
|
|
||||||
const computedColor = getColor(props.colors[index], index)
|
|
||||||
|
|
||||||
return createSeriesItem({
|
|
||||||
name: item.name,
|
|
||||||
data: item.data,
|
|
||||||
color: computedColor,
|
|
||||||
barWidth: item.barWidth,
|
|
||||||
stack: props.stack ? item.stack || 'total' : undefined
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 单数据情况
|
|
||||||
const singleData = props.data as number[]
|
|
||||||
const computedColor = getColor()
|
|
||||||
|
|
||||||
options.series = [
|
|
||||||
createSeriesItem({
|
|
||||||
data: singleData,
|
|
||||||
color: computedColor
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
<!-- k线图表 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="chartRef"
|
|
||||||
class="relative w-full"
|
|
||||||
:style="{ height: props.height }"
|
|
||||||
v-loading="props.loading"
|
|
||||||
></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { EChartsOption } from '@/plugins/echarts'
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import type { KLineChartProps } from '@/types/component/chart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtKLineChart' })
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<KLineChartProps>(), {
|
|
||||||
// 基础配置
|
|
||||||
height: useChartOps().chartHeight,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: false,
|
|
||||||
colors: () => useChartOps().colors,
|
|
||||||
|
|
||||||
// 数据配置
|
|
||||||
data: () => [],
|
|
||||||
showDataZoom: false,
|
|
||||||
dataZoomStart: 0,
|
|
||||||
dataZoomEnd: 100
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取实际使用的颜色
|
|
||||||
const getActualColors = () => {
|
|
||||||
const defaultUpColor = '#4C87F3'
|
|
||||||
const defaultDownColor = '#8BD8FC'
|
|
||||||
|
|
||||||
return {
|
|
||||||
upColor: props.colors?.[0] || defaultUpColor,
|
|
||||||
downColor: props.colors?.[1] || defaultDownColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const {
|
|
||||||
chartRef,
|
|
||||||
getAxisLineStyle,
|
|
||||||
getAxisLabelStyle,
|
|
||||||
getAxisTickStyle,
|
|
||||||
getSplitLineStyle,
|
|
||||||
getAnimationConfig,
|
|
||||||
getTooltipStyle
|
|
||||||
} = useChartComponent({
|
|
||||||
props,
|
|
||||||
checkEmpty: () => {
|
|
||||||
return (
|
|
||||||
!props.data?.length ||
|
|
||||||
props.data.every(
|
|
||||||
(item) => item.open === 0 && item.close === 0 && item.high === 0 && item.low === 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
watchSources: [
|
|
||||||
() => props.data,
|
|
||||||
() => props.colors,
|
|
||||||
() => props.showDataZoom,
|
|
||||||
() => props.dataZoomStart,
|
|
||||||
() => props.dataZoomEnd
|
|
||||||
],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
const { upColor, downColor } = getActualColors()
|
|
||||||
|
|
||||||
return {
|
|
||||||
grid: {
|
|
||||||
top: 20,
|
|
||||||
right: 20,
|
|
||||||
bottom: props.showDataZoom ? 80 : 20,
|
|
||||||
left: 20,
|
|
||||||
containLabel: true
|
|
||||||
},
|
|
||||||
tooltip: getTooltipStyle('axis', {
|
|
||||||
axisPointer: {
|
|
||||||
type: 'cross'
|
|
||||||
},
|
|
||||||
formatter: (params: Array<{ name: string; data: number[] }>) => {
|
|
||||||
const param = params[0]
|
|
||||||
const data = param.data
|
|
||||||
return `
|
|
||||||
<div style="padding: 5px;">
|
|
||||||
<div><strong>时间:</strong>${param.name}</div>
|
|
||||||
<div><strong>开盘:</strong>${data[0]}</div>
|
|
||||||
<div><strong>收盘:</strong>${data[1]}</div>
|
|
||||||
<div><strong>最低:</strong>${data[2]}</div>
|
|
||||||
<div><strong>最高:</strong>${data[3]}</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: props.data.map((item) => item.time),
|
|
||||||
axisTick: getAxisTickStyle(),
|
|
||||||
axisLine: getAxisLineStyle(true),
|
|
||||||
axisLabel: getAxisLabelStyle(true)
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
scale: true,
|
|
||||||
axisLabel: getAxisLabelStyle(true),
|
|
||||||
axisLine: getAxisLineStyle(true),
|
|
||||||
splitLine: getSplitLineStyle(true)
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'candlestick',
|
|
||||||
data: props.data.map((item) => [item.open, item.close, item.low, item.high]),
|
|
||||||
itemStyle: {
|
|
||||||
color: upColor,
|
|
||||||
color0: downColor,
|
|
||||||
borderColor: upColor,
|
|
||||||
borderColor0: downColor,
|
|
||||||
borderWidth: 1
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
itemStyle: {
|
|
||||||
borderWidth: 2,
|
|
||||||
shadowBlur: 10,
|
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...getAnimationConfig()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
dataZoom: props.showDataZoom
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: 'inside',
|
|
||||||
start: props.dataZoomStart,
|
|
||||||
end: props.dataZoomEnd
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: true,
|
|
||||||
type: 'slider',
|
|
||||||
top: '90%',
|
|
||||||
start: props.dataZoomStart,
|
|
||||||
end: props.dataZoomEnd
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,371 +0,0 @@
|
|||||||
<!-- 折线图,支持多组数据,支持阶梯式动画效果 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="chartRef"
|
|
||||||
class="relative w-[calc(100%+10px)]"
|
|
||||||
:style="{ height: props.height }"
|
|
||||||
v-loading="props.loading"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { graphic, type EChartsOption } from '@/plugins/echarts'
|
|
||||||
import { getCssVar, hexToRgba } from '@/utils/ui'
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import type { LineChartProps, LineDataItem } from '@/types/component/chart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtLineChart' })
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<LineChartProps>(), {
|
|
||||||
// 基础配置
|
|
||||||
height: useChartOps().chartHeight,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: false,
|
|
||||||
colors: () => useChartOps().colors,
|
|
||||||
|
|
||||||
// 数据配置
|
|
||||||
data: () => [0, 0, 0, 0, 0, 0, 0],
|
|
||||||
xAxisData: () => [],
|
|
||||||
lineWidth: 2.5,
|
|
||||||
showAreaColor: false,
|
|
||||||
smooth: true,
|
|
||||||
symbol: 'none',
|
|
||||||
symbolSize: 6,
|
|
||||||
animationDelay: 200,
|
|
||||||
|
|
||||||
// 轴线显示配置
|
|
||||||
showAxisLabel: true,
|
|
||||||
showAxisLine: true,
|
|
||||||
showSplitLine: true,
|
|
||||||
|
|
||||||
// 交互配置
|
|
||||||
showTooltip: true,
|
|
||||||
showLegend: false,
|
|
||||||
legendPosition: 'bottom'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 动画状态管理
|
|
||||||
const isAnimating = ref(false)
|
|
||||||
const animationTimers = ref<number[]>([])
|
|
||||||
const animatedData = ref<number[] | LineDataItem[]>([])
|
|
||||||
|
|
||||||
// 清理所有定时器
|
|
||||||
const clearAnimationTimers = () => {
|
|
||||||
animationTimers.value.forEach((timer) => clearTimeout(timer))
|
|
||||||
animationTimers.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否为多数据(使用 VueUse 的 computedEager 优化)
|
|
||||||
const isMultipleData = computed(() => {
|
|
||||||
return (
|
|
||||||
Array.isArray(props.data) &&
|
|
||||||
props.data.length > 0 &&
|
|
||||||
typeof props.data[0] === 'object' &&
|
|
||||||
'name' in props.data[0]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 缓存计算的最大值,避免重复计算
|
|
||||||
const maxValue = computed(() => {
|
|
||||||
if (isMultipleData.value) {
|
|
||||||
const multiData = props.data as LineDataItem[]
|
|
||||||
return multiData.reduce((max, item) => {
|
|
||||||
if (item.data?.length) {
|
|
||||||
const itemMax = Math.max(...item.data)
|
|
||||||
return Math.max(max, itemMax)
|
|
||||||
}
|
|
||||||
return max
|
|
||||||
}, 0)
|
|
||||||
} else {
|
|
||||||
const singleData = props.data as number[]
|
|
||||||
return singleData?.length ? Math.max(...singleData) : 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化动画数据(优化:减少条件判断)
|
|
||||||
const initAnimationData = (): number[] | LineDataItem[] => {
|
|
||||||
if (isMultipleData.value) {
|
|
||||||
const multiData = props.data as LineDataItem[]
|
|
||||||
return multiData.map((item) => ({
|
|
||||||
...item,
|
|
||||||
data: Array(item.data.length).fill(0)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
const singleData = props.data as number[]
|
|
||||||
return Array(singleData.length).fill(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制真实数据(优化:使用结构化克隆)
|
|
||||||
const copyRealData = (): number[] | LineDataItem[] => {
|
|
||||||
if (isMultipleData.value) {
|
|
||||||
return (props.data as LineDataItem[]).map((item) => ({ ...item, data: [...item.data] }))
|
|
||||||
}
|
|
||||||
return [...(props.data as number[])]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取颜色配置(优化:缓存主题色)
|
|
||||||
const primaryColor = computed(() => getCssVar('--el-color-primary'))
|
|
||||||
|
|
||||||
const getColor = (customColor?: string, index?: number): string => {
|
|
||||||
if (customColor) return customColor
|
|
||||||
if (index !== undefined) return props.colors![index % props.colors!.length]
|
|
||||||
return primaryColor.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成区域样式
|
|
||||||
const generateAreaStyle = (item: LineDataItem, color: string) => {
|
|
||||||
// 如果有 areaStyle 配置,或者显式开启了区域颜色,则显示区域样式
|
|
||||||
if (!item.areaStyle && !item.showAreaColor && !props.showAreaColor) return undefined
|
|
||||||
|
|
||||||
const areaConfig = item.areaStyle || {}
|
|
||||||
if (areaConfig.custom) return areaConfig.custom
|
|
||||||
|
|
||||||
return {
|
|
||||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: hexToRgba(color, areaConfig.startOpacity || 0.2).rgba
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: hexToRgba(color, areaConfig.endOpacity || 0.02).rgba
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成单数据区域样式
|
|
||||||
const generateSingleAreaStyle = () => {
|
|
||||||
if (!props.showAreaColor) return undefined
|
|
||||||
|
|
||||||
const color = getColor(props.colors[0])
|
|
||||||
return {
|
|
||||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
color: hexToRgba(color, 0.2).rgba
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: hexToRgba(color, 0.02).rgba
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建系列配置
|
|
||||||
const createSeriesItem = (config: {
|
|
||||||
name?: string
|
|
||||||
data: number[]
|
|
||||||
color?: string
|
|
||||||
smooth?: boolean
|
|
||||||
symbol?: string
|
|
||||||
symbolSize?: number
|
|
||||||
lineWidth?: number
|
|
||||||
areaStyle?: any
|
|
||||||
}) => {
|
|
||||||
return {
|
|
||||||
name: config.name,
|
|
||||||
data: config.data,
|
|
||||||
type: 'line' as const,
|
|
||||||
color: config.color,
|
|
||||||
smooth: config.smooth ?? props.smooth,
|
|
||||||
symbol: config.symbol ?? props.symbol,
|
|
||||||
symbolSize: config.symbolSize ?? props.symbolSize,
|
|
||||||
lineStyle: {
|
|
||||||
width: config.lineWidth ?? props.lineWidth,
|
|
||||||
color: config.color
|
|
||||||
},
|
|
||||||
areaStyle: config.areaStyle,
|
|
||||||
emphasis: {
|
|
||||||
focus: 'series' as const,
|
|
||||||
lineStyle: {
|
|
||||||
width: (config.lineWidth ?? props.lineWidth) + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成图表配置
|
|
||||||
const generateChartOptions = (isInitial = false): EChartsOption => {
|
|
||||||
const options: EChartsOption = {
|
|
||||||
animation: true,
|
|
||||||
animationDuration: isInitial ? 0 : 1300,
|
|
||||||
animationDurationUpdate: isInitial ? 0 : 1300,
|
|
||||||
grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
|
|
||||||
top: 15,
|
|
||||||
right: 15,
|
|
||||||
left: 0
|
|
||||||
}),
|
|
||||||
tooltip: props.showTooltip ? getTooltipStyle() : undefined,
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
boundaryGap: false,
|
|
||||||
data: props.xAxisData,
|
|
||||||
axisTick: getAxisTickStyle(),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel)
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
min: 0,
|
|
||||||
max: maxValue.value,
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
splitLine: getSplitLineStyle(props.showSplitLine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加图例配置
|
|
||||||
if (props.showLegend && isMultipleData.value) {
|
|
||||||
options.legend = getLegendStyle(props.legendPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成系列数据
|
|
||||||
if (isMultipleData.value) {
|
|
||||||
const multiData = animatedData.value as LineDataItem[]
|
|
||||||
options.series = multiData.map((item, index) => {
|
|
||||||
const itemColor = getColor(props.colors[index], index)
|
|
||||||
const areaStyle = generateAreaStyle(item, itemColor)
|
|
||||||
|
|
||||||
return createSeriesItem({
|
|
||||||
name: item.name,
|
|
||||||
data: item.data,
|
|
||||||
color: itemColor,
|
|
||||||
smooth: item.smooth,
|
|
||||||
symbol: item.symbol,
|
|
||||||
lineWidth: item.lineWidth,
|
|
||||||
areaStyle
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 单数据情况
|
|
||||||
const singleData = animatedData.value as number[]
|
|
||||||
const computedColor = getColor(props.colors[0])
|
|
||||||
const areaStyle = generateSingleAreaStyle()
|
|
||||||
|
|
||||||
options.series = [
|
|
||||||
createSeriesItem({
|
|
||||||
data: singleData,
|
|
||||||
color: computedColor,
|
|
||||||
areaStyle
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新图表
|
|
||||||
const updateChartOptions = (options: EChartsOption) => {
|
|
||||||
initChart(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化动画函数(优化:统一定时器管理,减少内存泄漏风险)
|
|
||||||
const initChartWithAnimation = () => {
|
|
||||||
clearAnimationTimers()
|
|
||||||
isAnimating.value = true
|
|
||||||
|
|
||||||
// 初始化为0值数据
|
|
||||||
animatedData.value = initAnimationData()
|
|
||||||
updateChartOptions(generateChartOptions(true))
|
|
||||||
|
|
||||||
if (isMultipleData.value) {
|
|
||||||
// 多数据阶梯式动画
|
|
||||||
const multiData = props.data as LineDataItem[]
|
|
||||||
const currentAnimatedData = animatedData.value as LineDataItem[]
|
|
||||||
|
|
||||||
multiData.forEach((item, index) => {
|
|
||||||
const timer = window.setTimeout(
|
|
||||||
() => {
|
|
||||||
currentAnimatedData[index] = { ...item, data: [...item.data] }
|
|
||||||
animatedData.value = [...currentAnimatedData]
|
|
||||||
updateChartOptions(generateChartOptions(false))
|
|
||||||
},
|
|
||||||
index * props.animationDelay + 100
|
|
||||||
)
|
|
||||||
|
|
||||||
animationTimers.value.push(timer)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 标记动画完成
|
|
||||||
const totalDelay = (multiData.length - 1) * props.animationDelay + 1500
|
|
||||||
const finishTimer = window.setTimeout(() => {
|
|
||||||
isAnimating.value = false
|
|
||||||
}, totalDelay)
|
|
||||||
animationTimers.value.push(finishTimer)
|
|
||||||
} else {
|
|
||||||
// 单数据简单动画 - 使用 nextTick 确保初始状态已渲染
|
|
||||||
nextTick(() => {
|
|
||||||
animatedData.value = copyRealData()
|
|
||||||
updateChartOptions(generateChartOptions(false))
|
|
||||||
isAnimating.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 空数据检查函数
|
|
||||||
const checkIsEmpty = () => {
|
|
||||||
// 检查单数据情况
|
|
||||||
if (Array.isArray(props.data) && typeof props.data[0] === 'number') {
|
|
||||||
const singleData = props.data as number[]
|
|
||||||
return !singleData.length || singleData.every((val) => val === 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查多数据情况
|
|
||||||
if (Array.isArray(props.data) && typeof props.data[0] === 'object') {
|
|
||||||
const multiData = props.data as LineDataItem[]
|
|
||||||
return (
|
|
||||||
!multiData.length ||
|
|
||||||
multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const {
|
|
||||||
chartRef,
|
|
||||||
initChart,
|
|
||||||
getAxisLineStyle,
|
|
||||||
getAxisLabelStyle,
|
|
||||||
getAxisTickStyle,
|
|
||||||
getSplitLineStyle,
|
|
||||||
getTooltipStyle,
|
|
||||||
getLegendStyle,
|
|
||||||
getGridWithLegend,
|
|
||||||
isEmpty
|
|
||||||
} = useChartComponent({
|
|
||||||
props,
|
|
||||||
checkEmpty: checkIsEmpty,
|
|
||||||
watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
|
|
||||||
onVisible: () => {
|
|
||||||
// 当图表变为可见时,检查是否为空数据
|
|
||||||
if (!isEmpty.value) {
|
|
||||||
initChartWithAnimation()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
generateOptions: () => generateChartOptions(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 图表渲染函数(优化:防止动画期间重复触发)
|
|
||||||
const renderChart = () => {
|
|
||||||
if (!isAnimating.value && !isEmpty.value) {
|
|
||||||
initChartWithAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 VueUse 的 watchDebounced 优化数据监听(避免频繁更新)
|
|
||||||
watch([() => props.data, () => props.xAxisData, () => props.colors], renderChart, { deep: true })
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
|
||||||
renderChart()
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearAnimationTimers()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
<!-- 雷达图 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="chartRef"
|
|
||||||
class="relative w-full"
|
|
||||||
:style="{ height: props.height }"
|
|
||||||
v-loading="props.loading"
|
|
||||||
></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { EChartsOption } from '@/plugins/echarts'
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import type { RadarChartProps } from '@/types/component/chart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtRadarChart' })
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<RadarChartProps>(), {
|
|
||||||
// 基础配置
|
|
||||||
height: useChartOps().chartHeight,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: false,
|
|
||||||
colors: () => useChartOps().colors,
|
|
||||||
|
|
||||||
// 数据配置
|
|
||||||
indicator: () => [],
|
|
||||||
data: () => [],
|
|
||||||
|
|
||||||
// 交互配置
|
|
||||||
showTooltip: true,
|
|
||||||
showLegend: false,
|
|
||||||
legendPosition: 'bottom'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const { chartRef, isDark, getAnimationConfig, getTooltipStyle } = useChartComponent({
|
|
||||||
props,
|
|
||||||
checkEmpty: () => {
|
|
||||||
return !props.data?.length || props.data.every((item) => item.value.every((val) => val === 0))
|
|
||||||
},
|
|
||||||
watchSources: [() => props.data, () => props.indicator, () => props.colors],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
return {
|
|
||||||
tooltip: props.showTooltip ? getTooltipStyle('item') : undefined,
|
|
||||||
radar: {
|
|
||||||
indicator: props.indicator,
|
|
||||||
center: ['50%', '50%'],
|
|
||||||
radius: '70%',
|
|
||||||
axisName: {
|
|
||||||
color: isDark.value ? '#ccc' : '#666',
|
|
||||||
fontSize: 12
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: isDark.value ? '#444' : '#e6e6e6'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: isDark.value ? '#444' : '#e6e6e6'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
splitArea: {
|
|
||||||
show: true,
|
|
||||||
areaStyle: {
|
|
||||||
color: isDark.value
|
|
||||||
? ['rgba(255, 255, 255, 0.02)', 'rgba(255, 255, 255, 0.05)']
|
|
||||||
: ['rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.05)']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'radar',
|
|
||||||
data: props.data.map((item, index) => ({
|
|
||||||
name: item.name,
|
|
||||||
value: item.value,
|
|
||||||
symbolSize: 4,
|
|
||||||
lineStyle: {
|
|
||||||
width: 2,
|
|
||||||
color: props.colors[index % props.colors.length]
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: props.colors[index % props.colors.length]
|
|
||||||
},
|
|
||||||
areaStyle: {
|
|
||||||
color: props.colors[index % props.colors.length],
|
|
||||||
opacity: 0.1
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
areaStyle: {
|
|
||||||
opacity: 0.25
|
|
||||||
},
|
|
||||||
lineStyle: {
|
|
||||||
width: 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
...getAnimationConfig(200, 1800)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
<!-- 环形图 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="chartRef"
|
|
||||||
class="relative w-full"
|
|
||||||
:style="{ height: props.height }"
|
|
||||||
v-loading="props.loading"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { EChartsOption } from '@/plugins/echarts'
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import type { RingChartProps } from '@/types/component/chart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtRingChart' })
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<RingChartProps>(), {
|
|
||||||
// 基础配置
|
|
||||||
height: useChartOps().chartHeight,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: false,
|
|
||||||
colors: () => useChartOps().colors,
|
|
||||||
|
|
||||||
// 数据配置
|
|
||||||
data: () => [],
|
|
||||||
radius: () => ['50%', '80%'],
|
|
||||||
borderRadius: 10,
|
|
||||||
centerText: '',
|
|
||||||
showLabel: false,
|
|
||||||
|
|
||||||
// 交互配置
|
|
||||||
showTooltip: true,
|
|
||||||
showLegend: false,
|
|
||||||
legendPosition: 'right'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const { chartRef, isDark, getAnimationConfig, getTooltipStyle, getLegendStyle } =
|
|
||||||
useChartComponent({
|
|
||||||
props,
|
|
||||||
checkEmpty: () => {
|
|
||||||
return !props.data?.length || props.data.every((item) => item.value === 0)
|
|
||||||
},
|
|
||||||
watchSources: [() => props.data, () => props.centerText],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
// 根据图例位置计算环形图中心位置
|
|
||||||
const getCenterPosition = (): [string, string] => {
|
|
||||||
if (!props.showLegend) return ['50%', '50%']
|
|
||||||
|
|
||||||
switch (props.legendPosition) {
|
|
||||||
case 'left':
|
|
||||||
return ['60%', '50%']
|
|
||||||
case 'right':
|
|
||||||
return ['40%', '50%']
|
|
||||||
case 'top':
|
|
||||||
return ['50%', '60%']
|
|
||||||
case 'bottom':
|
|
||||||
return ['50%', '40%']
|
|
||||||
default:
|
|
||||||
return ['50%', '50%']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const option: EChartsOption = {
|
|
||||||
tooltip: props.showTooltip
|
|
||||||
? getTooltipStyle('item', {
|
|
||||||
formatter: '{b}: {c} ({d}%)'
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
legend: props.showLegend ? getLegendStyle(props.legendPosition) : undefined,
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '数据占比',
|
|
||||||
type: 'pie',
|
|
||||||
radius: props.radius,
|
|
||||||
center: getCenterPosition(),
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: props.borderRadius,
|
|
||||||
borderColor: isDark.value ? '#2c2c2c' : '#fff',
|
|
||||||
borderWidth: 0
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: props.showLabel,
|
|
||||||
formatter: '{b}\n{d}%',
|
|
||||||
position: 'outside',
|
|
||||||
color: isDark.value ? '#ccc' : '#999',
|
|
||||||
fontSize: 12
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: false,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: props.showLabel,
|
|
||||||
length: 15,
|
|
||||||
length2: 25,
|
|
||||||
smooth: true
|
|
||||||
},
|
|
||||||
data: props.data,
|
|
||||||
color: props.colors,
|
|
||||||
...getAnimationConfig(),
|
|
||||||
animationType: 'expansion'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加中心文字
|
|
||||||
if (props.centerText) {
|
|
||||||
const centerPos = getCenterPosition()
|
|
||||||
option.title = {
|
|
||||||
text: props.centerText,
|
|
||||||
left: centerPos[0],
|
|
||||||
top: centerPos[1],
|
|
||||||
textAlign: 'center',
|
|
||||||
textVerticalAlign: 'middle',
|
|
||||||
textStyle: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 500,
|
|
||||||
color: isDark.value ? '#999' : '#ADB0BC'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return option
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
<!-- 散点图 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="chartRef"
|
|
||||||
class="relative w-full"
|
|
||||||
:style="{ height: props.height }"
|
|
||||||
v-loading="props.loading"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { EChartsOption } from '@/plugins/echarts'
|
|
||||||
import { getCssVar } from '@/utils/ui'
|
|
||||||
import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
|
|
||||||
import type { ScatterChartProps } from '@/types/component/chart'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtScatterChart' })
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ScatterChartProps>(), {
|
|
||||||
// 基础配置
|
|
||||||
height: useChartOps().chartHeight,
|
|
||||||
loading: false,
|
|
||||||
isEmpty: false,
|
|
||||||
colors: () => useChartOps().colors,
|
|
||||||
|
|
||||||
// 数据配置
|
|
||||||
data: () => [{ value: [0, 0] }, { value: [0, 0] }],
|
|
||||||
symbolSize: 14,
|
|
||||||
|
|
||||||
// 轴线显示配置
|
|
||||||
showAxisLabel: true,
|
|
||||||
showAxisLine: true,
|
|
||||||
showSplitLine: true,
|
|
||||||
|
|
||||||
// 交互配置
|
|
||||||
showTooltip: true,
|
|
||||||
showLegend: false,
|
|
||||||
legendPosition: 'bottom'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用新的图表组件抽象
|
|
||||||
const {
|
|
||||||
chartRef,
|
|
||||||
isDark,
|
|
||||||
getAxisLineStyle,
|
|
||||||
getAxisLabelStyle,
|
|
||||||
getAxisTickStyle,
|
|
||||||
getSplitLineStyle,
|
|
||||||
getAnimationConfig,
|
|
||||||
getTooltipStyle
|
|
||||||
} = useChartComponent({
|
|
||||||
props,
|
|
||||||
checkEmpty: () => {
|
|
||||||
return !props.data?.length || props.data.every((item) => item.value.every((val) => val === 0))
|
|
||||||
},
|
|
||||||
watchSources: [() => props.data, () => props.colors, () => props.symbolSize],
|
|
||||||
generateOptions: (): EChartsOption => {
|
|
||||||
const computedColor = props.colors[0] || getCssVar('--el-color-primary')
|
|
||||||
|
|
||||||
return {
|
|
||||||
grid: {
|
|
||||||
top: 20,
|
|
||||||
right: 20,
|
|
||||||
bottom: 20,
|
|
||||||
left: 20,
|
|
||||||
containLabel: true
|
|
||||||
},
|
|
||||||
tooltip: props.showTooltip
|
|
||||||
? getTooltipStyle('item', {
|
|
||||||
formatter: (params: { value: [number, number] }) => {
|
|
||||||
const [x, y] = params.value
|
|
||||||
return `X: ${x}<br/>Y: ${y}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
xAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
axisTick: getAxisTickStyle(),
|
|
||||||
splitLine: getSplitLineStyle(props.showSplitLine)
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLabel: getAxisLabelStyle(props.showAxisLabel),
|
|
||||||
axisLine: getAxisLineStyle(props.showAxisLine),
|
|
||||||
axisTick: getAxisTickStyle(),
|
|
||||||
splitLine: getSplitLineStyle(props.showSplitLine)
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'scatter',
|
|
||||||
data: props.data,
|
|
||||||
symbolSize: props.symbolSize,
|
|
||||||
itemStyle: {
|
|
||||||
color: computedColor,
|
|
||||||
shadowBlur: 6,
|
|
||||||
shadowColor: isDark.value ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
|
||||||
shadowOffsetY: 2
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
itemStyle: {
|
|
||||||
shadowBlur: 12,
|
|
||||||
shadowColor: isDark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
|
|
||||||
},
|
|
||||||
scale: true
|
|
||||||
},
|
|
||||||
...getAnimationConfig()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
<!-- 更多按钮 -->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<ElDropdown v-if="hasAnyAuthItem">
|
|
||||||
<ArtIconButton icon="ri:more-2-fill" class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm" />
|
|
||||||
<template #dropdown>
|
|
||||||
<ElDropdownMenu>
|
|
||||||
<template v-for="item in list" :key="item.key">
|
|
||||||
<ElDropdownItem
|
|
||||||
v-if="!item.auth || hasAuth(item.auth)"
|
|
||||||
:disabled="item.disabled"
|
|
||||||
@click="handleClick(item)"
|
|
||||||
>
|
|
||||||
<div class="flex-c gap-2" :style="{ color: item.color }">
|
|
||||||
<ArtSvgIcon v-if="item.icon" :icon="item.icon" />
|
|
||||||
<span>{{ item.label }}</span>
|
|
||||||
</div>
|
|
||||||
</ElDropdownItem>
|
|
||||||
</template>
|
|
||||||
</ElDropdownMenu>
|
|
||||||
</template>
|
|
||||||
</ElDropdown>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useAuth } from '@/hooks/core/useAuth'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ArtButtonMore' })
|
|
||||||
|
|
||||||
const { hasAuth } = useAuth()
|
|
||||||
|
|
||||||
export interface ButtonMoreItem {
|
|
||||||
/** 按钮标识,可用于点击事件 */
|
|
||||||
key: string | number
|
|
||||||
/** 按钮文本 */
|
|
||||||
label: string
|
|
||||||
/** 是否禁用 */
|
|
||||||
disabled?: boolean
|
|
||||||
/** 权限标识 */
|
|
||||||
auth?: string
|
|
||||||
/** 图标组件 */
|
|
||||||
icon?: string
|
|
||||||
/** 文本颜色 */
|
|
||||||
color?: string
|
|
||||||
/** 图标颜色(优先级高于 color) */
|
|
||||||
iconColor?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 下拉项列表 */
|
|
||||||
list: ButtonMoreItem[]
|
|
||||||
/** 整体权限控制 */
|
|
||||||
auth?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {})
|
|
||||||
|
|
||||||
// 检查是否有任何有权限的 item
|
|
||||||
const hasAnyAuthItem = computed(() => {
|
|
||||||
return props.list.some((item) => !item.auth || hasAuth(item.auth))
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'click', item: ButtonMoreItem): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleClick = (item: ButtonMoreItem) => {
|
|
||||||
emit('click', item)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
<!-- 表格按钮 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'inline-flex items-center justify-center min-w-8 h-8 px-2.5 mr-2.5 text-sm c-p rounded-md',
|
|
||||||
buttonClass
|
|
||||||
]"
|
|
||||||
:style="{ backgroundColor: buttonBgColor, color: iconColor }"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<ArtSvgIcon :icon="iconContent" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'ArtButtonTable' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** 按钮类型 */
|
|
||||||
type?: 'add' | 'edit' | 'delete' | 'more' | 'view'
|
|
||||||
/** 按钮图标 */
|
|
||||||
icon?: string
|
|
||||||
/** 按钮样式类 */
|
|
||||||
iconClass?: string
|
|
||||||
/** icon 颜色 */
|
|
||||||
iconColor?: string
|
|
||||||
/** 按钮背景色 */
|
|
||||||
buttonBgColor?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'click'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 默认按钮配置
|
|
||||||
const defaultButtons = {
|
|
||||||
add: { icon: 'ri:add-fill', class: 'bg-theme/12 text-theme' },
|
|
||||||
edit: { icon: 'ri:pencil-line', class: 'bg-secondary/12 text-secondary' },
|
|
||||||
delete: { icon: 'ri:delete-bin-5-line', class: 'bg-error/12 text-error' },
|
|
||||||
view: { icon: 'ri:eye-line', class: 'bg-info/12 text-info' },
|
|
||||||
more: { icon: 'ri:more-2-fill', class: '' }
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// 获取图标内容
|
|
||||||
const iconContent = computed(() => {
|
|
||||||
return props.icon || (props.type ? defaultButtons[props.type]?.icon : '') || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取按钮样式类
|
|
||||||
const buttonClass = computed(() => {
|
|
||||||
return props.iconClass || (props.type ? defaultButtons[props.type]?.class : '') || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
emit('click')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,430 +0,0 @@
|
|||||||
<!-- 拖拽验证组件 -->
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="dragVerify"
|
|
||||||
class="drag_verify"
|
|
||||||
:style="dragVerifyStyle"
|
|
||||||
@mousemove="dragMoving"
|
|
||||||
@mouseup="dragFinish"
|
|
||||||
@mouseleave="dragFinish"
|
|
||||||
@touchmove="dragMoving"
|
|
||||||
@touchend="dragFinish"
|
|
||||||
>
|
|
||||||
<!-- 进度条 -->
|
|
||||||
<div
|
|
||||||
class="dv_progress_bar"
|
|
||||||
:class="{ goFirst2: isOk }"
|
|
||||||
ref="progressBar"
|
|
||||||
:style="progressBarStyle"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 提示文本 -->
|
|
||||||
<div class="dv_text" :style="textStyle" ref="messageRef">
|
|
||||||
<slot name="textBefore" v-if="$slots.textBefore"></slot>
|
|
||||||
{{ message }}
|
|
||||||
<slot name="textAfter" v-if="$slots.textAfter"></slot>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 滑块处理器 -->
|
|
||||||
<div
|
|
||||||
class="dv_handler dv_handler_bg"
|
|
||||||
:class="{ goFirst: isOk }"
|
|
||||||
@mousedown="dragStart"
|
|
||||||
@touchstart="dragStart"
|
|
||||||
ref="handler"
|
|
||||||
:style="handlerStyle"
|
|
||||||
>
|
|
||||||
<ArtSvgIcon :icon="value ? successIcon : handlerIcon" class="text-g-600"></ArtSvgIcon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'ArtDragVerify' })
|
|
||||||
|
|
||||||
// 事件定义
|
|
||||||
const emit = defineEmits(['handlerMove', 'update:value', 'passCallback'])
|
|
||||||
|
|
||||||
// 组件属性接口定义
|
|
||||||
interface PropsType {
|
|
||||||
/** 是否通过验证 */
|
|
||||||
value: boolean
|
|
||||||
/** 组件宽度 */
|
|
||||||
width?: number | string
|
|
||||||
/** 组件高度 */
|
|
||||||
height?: number
|
|
||||||
/** 默认提示文本 */
|
|
||||||
text?: string
|
|
||||||
/** 成功提示文本 */
|
|
||||||
successText?: string
|
|
||||||
/** 背景色 */
|
|
||||||
background?: string
|
|
||||||
/** 进度条背景色 */
|
|
||||||
progressBarBg?: string
|
|
||||||
/** 完成状态背景色 */
|
|
||||||
completedBg?: string
|
|
||||||
/** 是否圆角 */
|
|
||||||
circle?: boolean
|
|
||||||
/** 圆角大小 */
|
|
||||||
radius?: string
|
|
||||||
/** 滑块图标 */
|
|
||||||
handlerIcon?: string
|
|
||||||
/** 成功图标 */
|
|
||||||
successIcon?: string
|
|
||||||
/** 滑块背景色 */
|
|
||||||
handlerBg?: string
|
|
||||||
/** 文本大小 */
|
|
||||||
textSize?: string
|
|
||||||
/** 文本颜色 */
|
|
||||||
textColor?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 属性默认值设置
|
|
||||||
const props = withDefaults(defineProps<PropsType>(), {
|
|
||||||
value: false,
|
|
||||||
width: '100%',
|
|
||||||
height: 40,
|
|
||||||
text: '按住滑块拖动',
|
|
||||||
successText: 'success',
|
|
||||||
background: '#eee',
|
|
||||||
progressBarBg: '#1385FF',
|
|
||||||
completedBg: '#57D187',
|
|
||||||
circle: false,
|
|
||||||
radius: 'calc(var(--custom-radius) / 3 + 2px)',
|
|
||||||
handlerIcon: 'solar:double-alt-arrow-right-linear',
|
|
||||||
successIcon: 'ri:check-fill',
|
|
||||||
handlerBg: '#fff',
|
|
||||||
textSize: '13px',
|
|
||||||
textColor: '#333'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件状态接口定义
|
|
||||||
interface StateType {
|
|
||||||
isMoving: boolean // 是否正在拖拽
|
|
||||||
x: number // 拖拽起始位置
|
|
||||||
isOk: boolean // 是否验证成功
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式状态定义
|
|
||||||
const state = reactive(<StateType>{
|
|
||||||
isMoving: false,
|
|
||||||
x: 0,
|
|
||||||
isOk: false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 解构响应式状态
|
|
||||||
const { isOk } = toRefs(state)
|
|
||||||
|
|
||||||
// DOM 元素引用
|
|
||||||
const dragVerify = ref()
|
|
||||||
const messageRef = ref()
|
|
||||||
const handler = ref()
|
|
||||||
const progressBar = ref()
|
|
||||||
|
|
||||||
// 触摸事件变量 - 用于禁止页面滑动
|
|
||||||
let startX: number, startY: number, moveX: number, moveY: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 触摸开始事件处理
|
|
||||||
* @param e 触摸事件对象
|
|
||||||
*/
|
|
||||||
const onTouchStart = (e: any) => {
|
|
||||||
startX = e.targetTouches[0].pageX
|
|
||||||
startY = e.targetTouches[0].pageY
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 触摸移动事件处理 - 判断是否为横向滑动,如果是则阻止默认行为
|
|
||||||
* @param e 触摸事件对象
|
|
||||||
*/
|
|
||||||
const onTouchMove = (e: any) => {
|
|
||||||
moveX = e.targetTouches[0].pageX
|
|
||||||
moveY = e.targetTouches[0].pageY
|
|
||||||
|
|
||||||
// 如果横向移动距离大于纵向移动距离,阻止默认行为(防止页面滑动)
|
|
||||||
if (Math.abs(moveX - startX) > Math.abs(moveY - startY)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局事件监听器添加
|
|
||||||
document.addEventListener('touchstart', onTouchStart)
|
|
||||||
document.addEventListener('touchmove', onTouchMove, { passive: false })
|
|
||||||
|
|
||||||
// 获取数值形式的宽度
|
|
||||||
const getNumericWidth = (): number => {
|
|
||||||
if (typeof props.width === 'string') {
|
|
||||||
// 如果是字符串,尝试从DOM元素获取实际宽度
|
|
||||||
return dragVerify.value?.offsetWidth || 260
|
|
||||||
}
|
|
||||||
return props.width
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取样式字符串形式的宽度
|
|
||||||
const getStyleWidth = (): string => {
|
|
||||||
if (typeof props.width === 'string') {
|
|
||||||
return props.width
|
|
||||||
}
|
|
||||||
return props.width + 'px'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载后的初始化
|
|
||||||
onMounted(() => {
|
|
||||||
// 设置 CSS 自定义属性
|
|
||||||
dragVerify.value?.style.setProperty('--textColor', props.textColor)
|
|
||||||
|
|
||||||
// 等待DOM更新后设置宽度相关属性
|
|
||||||
nextTick(() => {
|
|
||||||
const numericWidth = getNumericWidth()
|
|
||||||
dragVerify.value?.style.setProperty('--width', Math.floor(numericWidth / 2) + 'px')
|
|
||||||
dragVerify.value?.style.setProperty('--pwidth', -Math.floor(numericWidth / 2) + 'px')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重复添加事件监听器(确保事件绑定)
|
|
||||||
document.addEventListener('touchstart', onTouchStart)
|
|
||||||
document.addEventListener('touchmove', onTouchMove, { passive: false })
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载前清理事件监听器
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
document.removeEventListener('touchstart', onTouchStart)
|
|
||||||
document.removeEventListener('touchmove', onTouchMove)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 滑块样式计算
|
|
||||||
const handlerStyle = {
|
|
||||||
left: '0',
|
|
||||||
width: props.height + 'px',
|
|
||||||
height: props.height + 'px',
|
|
||||||
background: props.handlerBg
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主容器样式计算
|
|
||||||
const dragVerifyStyle = computed(() => ({
|
|
||||||
width: getStyleWidth(),
|
|
||||||
height: props.height + 'px',
|
|
||||||
lineHeight: props.height + 'px',
|
|
||||||
background: props.background,
|
|
||||||
borderRadius: props.circle ? props.height / 2 + 'px' : props.radius
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 进度条样式计算
|
|
||||||
const progressBarStyle = {
|
|
||||||
background: props.progressBarBg,
|
|
||||||
height: props.height + 'px',
|
|
||||||
borderRadius: props.circle
|
|
||||||
? props.height / 2 + 'px 0 0 ' + props.height / 2 + 'px'
|
|
||||||
: props.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文本样式计算
|
|
||||||
const textStyle = computed(() => ({
|
|
||||||
fontSize: props.textSize
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 显示消息计算属性
|
|
||||||
const message = computed(() => {
|
|
||||||
return props.value ? props.successText : props.text
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拖拽开始处理函数
|
|
||||||
* @param e 鼠标或触摸事件对象
|
|
||||||
*/
|
|
||||||
const dragStart = (e: any) => {
|
|
||||||
if (!props.value) {
|
|
||||||
state.isMoving = true
|
|
||||||
handler.value.style.transition = 'none'
|
|
||||||
// 计算拖拽起始位置
|
|
||||||
state.x =
|
|
||||||
(e.pageX || e.touches[0].pageX) - parseInt(handler.value.style.left.replace('px', ''), 10)
|
|
||||||
}
|
|
||||||
emit('handlerMove')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拖拽移动处理函数
|
|
||||||
* @param e 鼠标或触摸事件对象
|
|
||||||
*/
|
|
||||||
const dragMoving = (e: any) => {
|
|
||||||
if (state.isMoving && !props.value) {
|
|
||||||
const numericWidth = getNumericWidth()
|
|
||||||
// 计算当前位置
|
|
||||||
let _x = (e.pageX || e.touches[0].pageX) - state.x
|
|
||||||
|
|
||||||
// 在有效范围内移动
|
|
||||||
if (_x > 0 && _x <= numericWidth - props.height) {
|
|
||||||
handler.value.style.left = _x + 'px'
|
|
||||||
progressBar.value.style.width = _x + props.height / 2 + 'px'
|
|
||||||
} else if (_x > numericWidth - props.height) {
|
|
||||||
// 拖拽到末端,触发验证成功
|
|
||||||
handler.value.style.left = numericWidth - props.height + 'px'
|
|
||||||
progressBar.value.style.width = numericWidth - props.height / 2 + 'px'
|
|
||||||
passVerify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拖拽结束处理函数
|
|
||||||
* @param e 鼠标或触摸事件对象
|
|
||||||
*/
|
|
||||||
const dragFinish = (e: any) => {
|
|
||||||
if (state.isMoving && !props.value) {
|
|
||||||
const numericWidth = getNumericWidth()
|
|
||||||
// 计算最终位置
|
|
||||||
let _x = (e.pageX || e.changedTouches[0].pageX) - state.x
|
|
||||||
|
|
||||||
if (_x < numericWidth - props.height) {
|
|
||||||
// 未拖拽到末端,重置位置
|
|
||||||
state.isOk = true
|
|
||||||
handler.value.style.left = '0'
|
|
||||||
handler.value.style.transition = 'all 0.2s'
|
|
||||||
progressBar.value.style.width = '0'
|
|
||||||
state.isOk = false
|
|
||||||
} else {
|
|
||||||
// 拖拽到末端,保持验证成功状态
|
|
||||||
handler.value.style.transition = 'none'
|
|
||||||
handler.value.style.left = numericWidth - props.height + 'px'
|
|
||||||
progressBar.value.style.width = numericWidth - props.height / 2 + 'px'
|
|
||||||
passVerify()
|
|
||||||
}
|
|
||||||
state.isMoving = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证通过处理函数
|
|
||||||
*/
|
|
||||||
const passVerify = () => {
|
|
||||||
emit('update:value', true)
|
|
||||||
state.isMoving = false
|
|
||||||
// 更新样式为成功状态
|
|
||||||
progressBar.value.style.background = props.completedBg
|
|
||||||
messageRef.value.style['-webkit-text-fill-color'] = 'unset'
|
|
||||||
messageRef.value.style.animation = 'slidetounlock2 2s cubic-bezier(0, 0.2, 1, 1) infinite'
|
|
||||||
messageRef.value.style.color = '#fff'
|
|
||||||
emit('passCallback')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置验证状态函数
|
|
||||||
*/
|
|
||||||
const reset = () => {
|
|
||||||
// 重置滑块位置
|
|
||||||
handler.value.style.left = '0'
|
|
||||||
progressBar.value.style.width = '0'
|
|
||||||
progressBar.value.style.background = props.progressBarBg
|
|
||||||
// 重置文本样式
|
|
||||||
messageRef.value.style['-webkit-text-fill-color'] = 'transparent'
|
|
||||||
messageRef.value.style.animation = 'slidetounlock 2s cubic-bezier(0, 0.2, 1, 1) infinite'
|
|
||||||
messageRef.value.style.color = props.background
|
|
||||||
// 重置状态
|
|
||||||
emit('update:value', false)
|
|
||||||
state.isOk = false
|
|
||||||
state.isMoving = false
|
|
||||||
state.x = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 暴露重置方法给父组件
|
|
||||||
defineExpose({
|
|
||||||
reset
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.drag_verify {
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid var(--default-border-dashed);
|
|
||||||
|
|
||||||
.dv_handler {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: move;
|
|
||||||
|
|
||||||
i {
|
|
||||||
padding-left: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon-circle-check {
|
|
||||||
margin-top: 9px;
|
|
||||||
color: #6c6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dv_progress_bar {
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dv_text {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: transparent;
|
|
||||||
user-select: none;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
var(--textColor) 0%,
|
|
||||||
var(--textColor) 40%,
|
|
||||||
#fff 50%,
|
|
||||||
var(--textColor) 60%,
|
|
||||||
var(--textColor) 100%
|
|
||||||
);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
animation: slidetounlock 2s cubic-bezier(0, 0.2, 1, 1) infinite;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
text-size-adjust: none;
|
|
||||||
|
|
||||||
* {
|
|
||||||
-webkit-text-fill-color: var(--textColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.goFirst {
|
|
||||||
left: 0 !important;
|
|
||||||
transition: left 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.goFirst2 {
|
|
||||||
width: 0 !important;
|
|
||||||
transition: width 0.5s;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@keyframes slidetounlock {
|
|
||||||
0% {
|
|
||||||
background-position: var(--pwidth) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: var(--width) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slidetounlock2 {
|
|
||||||
0% {
|
|
||||||
background-position: var(--pwidth) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: var(--pwidth) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||