Compare commits

..

No commits in common. "eda41878a6bb1bdb00e47c61eab401f24b1bf9f4" and "49a7bb41b67ffe3b4b5e280fee0dda7b6f42043c" have entirely different histories.

339 changed files with 361 additions and 52976 deletions

View File

@ -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 格式和内容
- 验证分页逻辑
- 验证搜索过滤逻辑
- 验证权限控制逻辑

View File

@ -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 包含电源控制菜单项

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,2 +0,0 @@
*.html linguist-detectable=false
*.vue linguist-detectable=true

View File

@ -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

View File

@ -1 +0,0 @@
pnpm dlx commitlint --edit $1

View File

@ -1 +0,0 @@
pnpm run lint:lint-staged

View File

@ -1,3 +0,0 @@
/node_modules/*
/dist/*
/src/main.ts

View File

@ -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
}

View File

@ -1,9 +0,0 @@
dist
node_modules
public
.husky
.vscode
src/components/Layout/MenuLeft/index.vue
src/assets
stats.html

View File

@ -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'
]
}
]
}
}

View File

@ -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.

View File

@ -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: ''
}
}

View File

@ -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
]

View File

@ -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>

View File

@ -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

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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)
})

View File

@ -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>

View File

@ -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'
// }
})
}

View File

@ -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'
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 B

View File

@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -1,2 +0,0 @@
// 导入暗黑主题
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

View File

@ -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'
)
);

View File

@ -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;
}
}
}
// 隐藏 selectdropdown 的三角
.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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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');
}

View File

@ -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;
}
}
}
}
}

View File

@ -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);
}

View File

@ -1,11 +0,0 @@
// 主题切换过渡优化优化除视觉上的不适感
.theme-change {
* {
transition: 0s !important;
}
.el-switch__core,
.el-switch__action {
transition: all 0.3s !important;
}
}

View File

@ -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;
}

View File

@ -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';

View File

@ -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>
`

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More