This commit is contained in:
lijianyou
2025-09-30 15:00:32 +08:00
parent 1b262f06b8
commit b4e759cf69
202 changed files with 22035 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.2",
"commands": [
"dotnet-ef"
]
}
}
}
+30
View File
@@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**
+6
View File
@@ -0,0 +1,6 @@
obj
bin
.vs
logs
upload
data1
+77
View File
@@ -0,0 +1,77 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using System;
using System.Collections.Generic;
using System.Linq;
namespace dy.net
{
/// <summary>
/// appsettings.json操作类
/// </summary>
public class Appsettings
{
public static IConfiguration Configuration { get; private set; }
public Appsettings(string contentPath)
{
//如果你把配置文件 是 根据环境变量来分开了,可以这样写
string Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json";
Configuration = new ConfigurationBuilder()
.SetBasePath(contentPath)
.Add(new JsonConfigurationSource { Path = Path, Optional = false, ReloadOnChange = true })//这样的话,可以直接读目录里的json文件,而不是 bin 文件夹下的,所以不用修改复制属性
.Build();
}
public Appsettings(IConfiguration configuration)
{
Configuration = configuration;
}
/// <summary>
/// 封装要操作的字符
/// </summary>
/// <param name="sections">节点配置</param>
/// <returns></returns>
public static string Get(params string[] sections)
{
try
{
if (sections.Any())
{
return Configuration[string.Join(":", sections)];
}
}
catch (Exception) { throw; }
return "";
}
/// <summary>
/// 递归获取配置信息数组
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sections"></param>
/// <returns></returns>
public static List<T> Get<T>(params string[] sections)
{
List<T> list = new List<T>();
// 引用 Microsoft.Extensions.Configuration.Binder 包
Configuration.Bind(string.Join(":", sections), list);
return list;
}
public static T Get<T>(string key) where T : new()
{
return Configuration.GetSection(key).Get<T>();
}
public static string Get(string Key)
{
return Configuration[Key];
}
}
}
+164
View File
@@ -0,0 +1,164 @@
using ClockSnowFlake;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using dy.net.service;
using dy.net.utils;
using dy.net.dto;
namespace dy.net.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IWebHostEnvironment webHostEnvironment;
private readonly UserService _userService;
public AuthController(UserService userService, IWebHostEnvironment webHostEnvironment )
{
_userService=userService;
this.webHostEnvironment = webHostEnvironment;
}
/// <summary>
/// 修改密码
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> UpdatePwd(UpdatePwdRequest user)
{
var (code, erro) = await _userService.UpdatePwd(user);
return Ok(new { code, erro });
}
[Authorize]
[HttpGet]
public async Task<IActionResult> GetUserAvatar()
{
var user = await _userService.GetUser();
return Ok(new { code = 0, error = "", data = new { user?.Avatar, user?.Id,user?.UserName } });
}
/// <summary>
/// 修改用户头像
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> UpdateUserAvatar(IFormFile file)
{
if (file != null && file.Length > 0)
{
long maxFileSize = 5 * 1024 * 1024; // 限制文件大小为5MB
if (file.Length > maxFileSize)
{
return Ok(new { code = -1, erro = "文件最大只能上传5M" });
}
var fileName = $"{IdGener.GetGuid()}_{file.FileName}";
var filePath = webHostEnvironment.IsProduction() ?
Path.Combine(Md5Util.UPLOAD_PATH_PRO, fileName) : Path.Combine(Md5Util.UPLOAD_PATH_DEV, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
try
{
// 问了节约空间,删除文件夹下的所有文件
string[] files = Directory.GetFiles(webHostEnvironment.IsProduction() ? Md5Util.UPLOAD_PATH_PRO : Md5Util.UPLOAD_PATH_DEV);
foreach (string mfile in files)
{
if (!mfile.Contains(fileName))
System.IO.File.Delete(mfile);
}
}
catch (Exception ex)
{
Serilog.Log.Error($"delete file error ,{ex.Message}");
}
var update = await _userService.UpdateAvatar( fileName);
return Ok(new { code = update ? 0 : -1, erro = update ? "" : "上传失败", data = update ? fileName : "" });
}
}
else
{
return Ok(new { code = -1, erro = "空文件" });
}
}
/// <summary>
/// 登录获取token
/// </summary>
/// <param name="loginUserInfo"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Login(LoginRequest loginUserInfo)
{
if (loginUserInfo == null)
{
return Ok(new { code = -1, erro = "参数不能为空" });
}
else
{
var user = await _userService.GetUser();
if (user == null)
{
return Ok(new { code = -1, erro = "用户名或密码不正确" });
}
else
{
if (user.Password == Md5Util.Md5(loginUserInfo.Password))
{
var tokenString = GenerateJwtToken(user.UserName);
return Ok(new { code = 0, erro = "", token = tokenString, expires = 24 * 60 * 60 * 1000 });
}
else
{
return Ok(new { code = -1, erro = "用户名或密码不正确" });
}
}
}
}
private string GenerateJwtToken(string username)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var k = Md5Util.JWT_TOKEN_KEY;
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(k));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddDays(1);
var token = new JwtSecurityToken(
issuer: IdGener.GetLong().ToString(),
audience: IdGener.GetLong().ToString(),
claims: claims,
expires: expires,
signingCredentials: credentials
);
var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
return jwtToken;
}
}
}
+147
View File
@@ -0,0 +1,147 @@
using ClockSnowFlake;
using dy.net.dto;
using dy.net.model;
using dy.net.service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static Dm.net.buffer.ByteArrayBuffer;
namespace dy.net.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ConfigController : ControllerBase
{
private readonly DyCookieService dyCookieService;
private readonly CommonService commonService;
private readonly QuartzJobService quartzJobService;
public ConfigController(DyCookieService dyCookieService, CommonService commonService,QuartzJobService quartzJobService)
{
this.dyCookieService = dyCookieService;
this.commonService = commonService;
this.quartzJobService = quartzJobService;
}
/// <summary>
/// 分页查询
/// </summary>
/// <returns>分页结果(视频列表和总数)</returns>
[HttpPost("paged")]
public async Task<IActionResult> GetPagedAsync(
PageRequestDto dto)
{
var (list, totalCount) = await dyCookieService.GetPagedAsync(dto.PageIndex, dto.PageSize);
return Ok(new
{
code = 0,
data = new
{
data = list,
total=totalCount,
pageIndex= dto.PageIndex,
pageSize= dto.PageSize
}
}
);
}
/// <summary>
/// 新增用户Cookie
/// </summary>
[HttpPost("add")]
public async Task<IActionResult> AddAsync([FromBody] DyUserCookies dyUserCookies)
{
var result = await dyCookieService.Add(dyUserCookies);
if (result)
{
await ReStartJob();
return Ok(new { code = 0 });
}
return BadRequest(new { code=-1, message = "添加失败" });
}
/// <summary>
/// 更新用户Cookie
/// </summary>
[HttpPost("update")]
public async Task<IActionResult> UpdateAsync([FromBody] DyUserCookies dyUserCookies)
{
if (dyUserCookies.Id == "0")
{
dyUserCookies.Id=IdGener.GetLong().ToString();
var result = await dyCookieService.Add(dyUserCookies);
if (result)
{
await ReStartJob();
return Ok(new { code = 0 });
}
return BadRequest(new { code = -1, message = "添加失败" });
}
else {
var result = await dyCookieService.UpdateAsync(dyUserCookies);
if (result)
{
await ReStartJob();
return Ok(new { code = 0 });
}
return BadRequest(new { code = -1, message = "更新失败" });
}
}
/// <summary>
/// 批量删除用户Cookie
/// </summary>
[HttpGet("delete")]
public async Task<IActionResult> DeleteAsync(string id)
{
var count = await dyCookieService.DeleteByIdsAsync(new List<string> { id});
if (count > 0) {
await ReStartJob();
}
return Ok(new { code = 0, deletedCount = count });
}
[HttpGet("GetConfig")]
public IActionResult GetConfig()
{
var data = commonService.GetConfig();
return Ok(new { code = 0, data = data });
}
[HttpPost("UpdateConfig")]
public async Task<IActionResult> UpdateConfig(AppConfig config)
{
var data = await commonService.UpdateConfig(config);
if (data) {
await ReStartJob();
}
return Ok(new { code = 0, data = data });
}
/// <summary>
///
/// </summary>
/// <returns></returns>
[HttpGet("ExecuteJobNow")]
[Authorize]
public async Task<IActionResult> ExecuteJobNow()
{
var config = commonService.GetConfig();
await quartzJobService.StartJob(config.Cron);
return Ok(new { code = 0 , error = "" });
}
private async Task ReStartJob() {
var config= commonService.GetConfig();
if(config!=null)
await quartzJobService.StartJob(config.Cron);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
namespace dy.net.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class LogsController : ControllerBase
{
private readonly IWebHostEnvironment webHostEnvironment;
public LogsController(IWebHostEnvironment webHostEnvironment)
{
this.webHostEnvironment = webHostEnvironment;
}
[HttpGet]
public async Task<IActionResult> GetLog(string type, string date)
{
var filePath = Path.Combine(webHostEnvironment.IsDevelopment() ? Directory.GetCurrentDirectory() : AppDomain.CurrentDomain.BaseDirectory, "logs", $"log-{type}-{date}.txt");
if (!System.IO.File.Exists(filePath))
{
var msg = $"Log file log-{type}-{date}.txt not found.";
//Serilog.Log.Error(msg);
return Ok(msg);
}
return PhysicalFile(filePath, "text/plain; charset=utf-8");
//下面的方案提示文件被占用
//var encoding = Encoding.GetEncoding("UTF-8"); // 指定文本文件的编码
//var fileBytes = await System.IO.File.ReadAllBytesAsync(filePath);
//var fileContent = encoding.GetString(fileBytes);
//return Content (fileContent, "text/plain", encoding);
}
}
}
+56
View File
@@ -0,0 +1,56 @@
using dy.net.dto;
using dy.net.service;
using Microsoft.AspNetCore.Mvc;
using System.Drawing.Printing;
namespace dy.net.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class VideoController : ControllerBase
{
private readonly DyCollectVideoService dyCollectVideoService;
public VideoController(DyCollectVideoService dyCollectVideoService)
{
this.dyCollectVideoService = dyCollectVideoService;
}
/// <summary>
/// 分页查询收藏视频
/// </summary>
/// <param name="dto"></param>
[HttpPost("paged")]
public async Task<IActionResult> GetPagedAsync(VideoPageRequestDTO dto)
{
var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Author,dto.ViedoType,dto.Dates);
return Ok(new
{
code = 0,
data = new
{
data = list,
total=totalCount,
pageIndex=dto.PageIndex,
pageSize=dto.PageSize
}
});
}
/// <summary>
/// 查询统计数据
/// </summary>
/// <returns></returns>
[HttpGet("statics")]
public async Task<IActionResult> GetStaticsAsync()
{
var data = await dyCollectVideoService.GetStatics();
return Ok(new
{
code = 0,
data
});
}
}
}
+10
View File
@@ -0,0 +1,10 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 10101
COPY . .
ENV ASPNETCORE_URLS http://*:10101
ENTRYPOINT ["dotnet", "dy.net.dll"]
ENV TZ=Asia/Shanghai
+326
View File
@@ -0,0 +1,326 @@
using ClockSnowFlake;
using dy.net.dto;
using dy.net.extension;
using dy.net.model;
using dy.net.service;
using dy.net.utils;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens;
using Serilog;
using SqlSugar;
using System.Reflection;
using System.Text;
namespace dy.net
{
public class Program
{
// 常量定义
private const string DefaultListenUrl = "http://*:10101";
private const string SpaRootPath = "app/dist";
private const string SpaSourcePath = "app/";
private const string SwaggerDocTitle = "dy.net WebApi Docs";
public static void Main(string[] args)
{
// 初始化编码提供器
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// 构建Web应用
var builder = WebApplication.CreateBuilder(args);
var isDevelopment = builder.Environment.IsDevelopment();
// 配置主机
ConfigureHost(builder, isDevelopment);
// 配置服务
var services = builder.Services;
ConfigureServices(services, builder.Configuration, isDevelopment);
// 构建应用
var app = builder.Build();
// 配置中间件
ConfigureMiddleware(app, isDevelopment);
// 初始化应用服务
InitApplicationServices(app);
// 启动应用
Serilog.Log.Debug("dy.net service started successfully");
app.Run();
}
/// <summary>
/// 配置主机设置
/// </summary>
private static void ConfigureHost(WebApplicationBuilder builder, bool isDevelopment)
{
// 设置监听地址
builder.WebHost.UseUrls(DefaultListenUrl);
// 配置配置文件
builder.Host.ConfigureAppConfiguration((context, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
});
// 配置日志
builder.Host.ConfigureLogging(logging => logging.ClearProviders())
.UseSerilog();
builder.ConfigureLogging();
}
/// <summary>
/// 配置依赖注入服务
/// </summary>
private static void ConfigureServices(IServiceCollection services, IConfiguration config, bool isDevelopment)
{
// 雪花ID生成器
services.AddSnowFlakeId(options => options.WorkId = new Random().Next(1, 127));
// MVC控制器
services.AddControllers();
// HTTP客户端
services.AddHttpClients();
// 数据库
services.AddSqlsugar(config);
// 定时任务
services.AddQuartzService(config);
// 仓储和服务注册
services.AddServicesFromNamespace("dy.net.repository")
.AddServicesFromNamespace("dy.net.service");
// SPA静态文件支持
services.AddSpaStaticFiles(options => options.RootPath = SpaRootPath);
// 开发环境启用Swagger
if (isDevelopment)
{
services.AddSwagger();
}
// 响应压缩
services.AddResponseCompression();
// JWT认证
ConfigureJwtAuthentication(services);
}
/// <summary>
/// 配置JWT认证
/// </summary>
private static void ConfigureJwtAuthentication(IServiceCollection services)
{
var key = Encoding.ASCII.GetBytes(Md5Util.JWT_TOKEN_KEY);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Headers["Authorization"]
.FirstOrDefault()?.Split(" ").Last();
return Task.CompletedTask;
}
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(60)
};
});
}
/// <summary>
/// 配置中间件
/// </summary>
private static void ConfigureMiddleware(WebApplication app, bool isDevelopment)
{
// 响应压缩
app.UseResponseCompression();
// 开发环境启用SwaggerUI
if (isDevelopment)
{
app.UseCustomSwaggerUI(options => options.Title = SwaggerDocTitle);
}
// 路由
app.UseRouting();
// 认证授权
app.UseAuthentication();
app.UseAuthorization();
// 配置文件上传路径
//ConfigureUploadPath(app, isDevelopment);
// API路由映射
app.MapControllers();
// 生产环境启用SPA
if (!isDevelopment)
{
app.UseSpaStaticFiles();
app.UseSpa(spa => spa.Options.SourcePath = SpaSourcePath);
}
}
/// <summary>
/// 配置上传路径
/// </summary>
private static void ConfigureUploadPath(WebApplication app, bool isDevelopment)
{
var uploadPath = isDevelopment
? Md5Util.UPLOAD_PATH_DEV
: Md5Util.UPLOAD_PATH_PRO;
if (!Directory.Exists(uploadPath))
{
Directory.CreateDirectory(uploadPath);
}
// 如需启用文件上传访问可取消注释
// app.UseStaticFiles(new StaticFileOptions
// {
// FileProvider = new PhysicalFileProvider(uploadPath),
// RequestPath = "/upload"
// });
}
/// <summary>
/// 初始化应用服务数据
/// </summary>
private static void InitApplicationServices(WebApplication app)
{
using var scope = app.Services.CreateScope();
var services = scope.ServiceProvider;
try
{
// 初始化用户
var userService = services.GetRequiredService<UserService>();
userService.InitUser(new LoginUserInfo
{
UserName = "douyin",
Password = "douyin2025",
CreateTime = DateTime.Now
});
// 初始化Cookie
var cookieService = services.GetRequiredService<DyCookieService>();
cookieService.Init(new DyUserCookies
{
UserName = "douyin",
Cookies = "--",
Id = "2026",
SavePath = "/app/collect",
Status = 0,
SecUserId = "--",
FavSavePath = "/app/favorite"
});
// 初始化配置
var commonService = services.GetRequiredService<CommonService>();
var config = commonService.InitConfig(new AppConfig
{
Id = IdGener.GetLong().ToString(),
Cron = "30",
BatchCount = 10
});
// 更新收藏视频类型--兼容老版本-原来的旧数据没有这个类型字段
commonService.UpdateCollectViedoType();
// 启动定时任务
var quartzJobService = services.GetRequiredService<QuartzJobService>();
quartzJobService.StartJob(config?.Cron ?? "30");
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Failed to initialize services on startup");
}
}
/// <summary>
/// 创建SQLite数据库连接字符串
/// </summary>
private static string CreateSqliteDBConn()
{
var dbFolder = Path.Combine(Environment.CurrentDirectory, "db");
Directory.CreateDirectory(dbFolder); // 不存在则创建,无需判断
var dbPath = Path.Combine(dbFolder, "dy.sqlite");
if (!File.Exists(dbPath))
{
using (File.Create(dbPath)) { } // 使用using确保文件流关闭
}
return $"DataSource={dbPath}";
}
/// <summary>
/// 初始化数据库
/// </summary>
private static ISqlSugarClient InitDataBase(DbType dbType, string connString)
{
// 处理SQLite连接字符串
if (dbType == DbType.Sqlite)
{
connString = CreateSqliteDBConn();
}
if (string.IsNullOrEmpty(connString))
{
return null;
}
return new SqlSugarClient(new ConnectionConfig
{
ConnectionString = connString,
InitKeyType = InitKeyType.Attribute,
DbType = dbType,
IsAutoCloseConnection = true
}, db =>
{
// 日志配置
db.Aop.OnLogExecuting = (sql, pars) => Serilog.Log.Debug(sql);
db.Aop.OnError = e =>
{
Serilog.Log.Error(e.Message);
Serilog.Log.Error(e.Sql);
};
// 创建数据库和表
db.DbMaintenance.CreateDatabase();
var modelTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.Namespace?.StartsWith("dy.net.model") ?? false)
.ToArray();
db.CodeFirst.InitTables(modelTypes);
});
}
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>bin\Release\net6.0\publish\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<SiteUrlToLaunchAfterPublish />
<TargetFramework>net6.0</TargetFramework>
<ProjectGuid>680660ef-acae-43a9-ab6c-b75532e758ad</ProjectGuid>
<SelfContained>false</SelfContained>
</PropertyGroup>
</Project>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<_PublishTargetUrl>F:\work\code\me\dy-sync\bin\Release\net6.0\publish\</_PublishTargetUrl>
<History>True|2025-09-30T05:29:42.2354235Z||;True|2025-09-25T11:44:52.6858389+08:00||;True|2025-09-25T11:08:27.8810109+08:00||;True|2025-09-25T09:28:50.2563374+08:00||;True|2025-09-24T16:14:02.3072184+08:00||;True|2025-09-23T22:38:52.4414757+08:00||;True|2025-09-23T22:19:07.1006356+08:00||;True|2025-09-23T22:18:01.5945225+08:00||;True|2025-09-23T22:06:15.6293613+08:00||;True|2025-09-23T21:53:14.2977444+08:00||;True|2025-09-23T21:46:55.2322411+08:00||;False|2025-09-23T21:45:23.7858854+08:00||;False|2025-09-23T21:03:51.8325689+08:00||;True|2025-09-23T10:03:32.2251920+08:00||;False|2025-09-23T09:38:08.2372201+08:00||;False|2025-09-23T09:37:49.9390545+08:00||;True|2024-03-11T21:31:45.8102398+08:00||;True|2024-03-11T07:26:24.7660541+08:00||;True|2024-03-08T22:08:40.0154831+08:00||;True|2024-03-03T10:14:36.8109114+08:00||;True|2024-03-02T18:44:57.3288537+08:00||;True|2024-01-24T17:51:37.9164415+08:00||;True|2024-01-24T16:36:50.5612157+08:00||;True|2024-01-24T15:51:35.7556653+08:00||;True|2024-01-17T23:40:40.7526618+08:00||;True|2024-01-17T23:36:10.3692844+08:00||;True|2024-01-17T23:22:03.2378834+08:00||;True|2024-01-03T11:35:44.7118292+08:00||;True|2024-01-03T11:11:23.4270453+08:00||;True|2024-01-03T11:04:35.2081526+08:00||;True|2024-01-03T10:57:03.7053107+08:00||;True|2024-01-03T10:51:50.7463989+08:00||;False|2024-01-03T10:50:24.9775312+08:00||;True|2024-01-03T10:47:30.1128183+08:00||;True|2024-01-03T10:42:55.8640657+08:00||;True|2024-01-03T09:24:24.3436056+08:00||;True|2024-01-02T23:40:38.2001198+08:00||;True|2024-01-02T23:08:36.7230444+08:00||;True|2024-01-02T22:53:43.9658255+08:00||;True|2024-01-02T22:25:38.5545279+08:00||;False|2024-01-02T22:24:42.6577609+08:00||;True|2024-01-02T22:04:33.4708889+08:00||;True|2024-01-02T21:49:57.9249898+08:00||;True|2023-12-28T22:42:58.5976180+08:00||;True|2023-12-28T22:24:09.2051205+08:00||;True|2023-12-28T22:05:48.4865497+08:00||;True|2023-12-28T21:50:57.4549336+08:00||;True|2023-12-28T21:45:30.4557939+08:00||;True|2023-12-28T21:19:59.3614594+08:00||;False|2023-12-28T21:17:04.3069097+08:00||;False|2023-12-28T21:15:56.7788810+08:00||;False|2023-12-28T21:14:57.3617402+08:00||;False|2023-12-28T21:10:45.4382469+08:00||;True|2023-12-28T19:45:33.1431914+08:00||;True|2023-12-26T11:30:36.6418716+08:00||;True|2023-12-26T11:17:40.6433651+08:00||;True|2023-12-26T09:13:56.0741559+08:00||;True|2023-12-26T09:10:55.5358737+08:00||;True|2023-12-25T23:15:34.6534359+08:00||;True|2023-12-25T23:09:47.1566176+08:00||;False|2023-12-25T23:07:32.1118803+08:00||;True|2023-12-25T22:59:50.4643459+08:00||;True|2023-12-25T22:50:14.3584798+08:00||;False|2023-12-25T22:49:20.0279513+08:00||;True|2023-12-25T22:44:07.4183973+08:00||;True|2023-12-25T22:35:27.3847085+08:00||;True|2023-12-25T22:23:21.3542658+08:00||;True|2023-12-25T22:12:00.5219522+08:00||;True|2023-12-25T16:52:12.5089621+08:00||;True|2023-12-25T16:43:30.8348193+08:00||;True|2023-12-25T16:35:08.2421925+08:00||;True|2023-12-25T16:25:21.3233455+08:00||;False|2023-12-25T16:24:39.1703901+08:00||;True|2023-12-25T16:24:01.4340091+08:00||;True|2023-12-25T16:10:06.5213850+08:00||;True|2023-12-25T15:42:17.1376331+08:00||;True|2023-12-25T15:29:47.8537015+08:00||;True|2023-12-25T14:04:54.3750643+08:00||;False|2023-12-25T14:04:07.9330700+08:00||;True|2023-12-25T13:51:58.1949269+08:00||;True|2023-12-25T13:41:42.4832670+08:00||;True|2023-12-25T13:00:25.3861450+08:00||;True|2023-12-25T12:48:45.3851245+08:00||;True|2023-12-25T12:46:54.1266013+08:00||;True|2023-12-25T12:37:03.2615780+08:00||;False|2023-12-25T12:36:06.2657224+08:00||;True|2023-12-25T12:31:59.6032354+08:00||;True|2023-12-25T12:26:50.2316015+08:00||;True|2023-12-25T12:18:27.4149004+08:00||;True|2023-12-25T11:59:46.1381276+08:00||;True|2023-12-25T00:36:00.0643971+08:00||;True|2023-12-25T00:28:55.0051504+08:00||;False|2023-12-25T00:21:35.4266163+08:00||;True|2023-12-25T00:18:47.0638530+08:00||;True|2023-12-25T00:12:09.6195055+08:00||;True|2023-12-25T00:01:34.9698196+08:00||;False|2023-12-24T23:50:48.6479274+08:00||;True|2023-12-22T09:54:04.1399592+08:00||;True|2023-12-22T08:59:53.2795721+08:00||;False|2023-12-18T23:40:16.8523325+08:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
+37
View File
@@ -0,0 +1,37 @@
{
"profiles": {
"dy.net": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5193"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast",
"publishAllPorts": true
}
},
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:35772",
"sslPort": 0
}
}
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}
+2
View File
@@ -0,0 +1,2 @@
VITE_BASE_URL=/
VITE_API_URL=http://localhost
+2
View File
@@ -0,0 +1,2 @@
VITE_BASE_URL=/
VITE_API_URL=http://localhost:10101
+1
View File
@@ -0,0 +1 @@
VITE_BASE_URL=/
+16
View File
@@ -0,0 +1,16 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.vscode
.history
.idea
*.log
*session.sql
package-lock.json
yarn.lock
stepin-template.session.sql
docs/.vitepress/cache
target
components.d.ts
+22
View File
@@ -0,0 +1,22 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"singleAttributePerLine": false,
"bracketSameLine": false,
"jsxBracketSameLine": false,
"bracketLine": false,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"vueIndentScriptAndStyle": true
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 stepui
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.
+83
View File
@@ -0,0 +1,83 @@
<h1 align="center">Stepin Template</h1>
<div align="center">
开箱即用的中后台前端/设计解决方案
<br/>
(原[ Vue Antd Admin](https://github.com/iczer/vue-antd-admin/) Vue3 版本)
![GitHub package.json version (subfolder of monorepo)](https://img.shields.io/github/package-json/v/stepui/stepin-template)
![GitHub repo size](https://img.shields.io/github/repo-size/stepui/stepin-template)
![GitHub last commit](https://img.shields.io/github/last-commit/stepui/stepin-template)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/stepui/stepin-template)
![Docs by iczer](https://img.shields.io/badge/docs%20by-iczer-green)
![preview](./docs/images/preview.png)
</div>
English | [简体中文](./README.md)
- 预览地址: https://stepui.gitee.io/stepin-template
- 使用文档: http://stepui.gitee.io/stepin-template-docs/
- 常见问题: http://stepui.gitee.io/stepin-template-docs/issue
- 国内镜像: https://gitee.com/stepui/stepin-template
## 特性
- 强大的主题定制功能,实时动态切换
- 多页签功能,助力后台管理高效开发
- 内置权限控制,兼容 ABAC 和 RBAC 模型
- 丰富的内置业务组件和常用页面模板
## 浏览器支持
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 环境要求
|git|node|yarn|
|---|----|----|
|`^2.15.0`|`^16.14.0`|`^1.21.1`|
## 使用
### 拉取代码
```bash
git clone https://github.com/stepui/stepin-template.git
```
### 安装依赖
```bash
yarn install
```
### 启动
```
yarn dev
```
### 预览
启动成功后,控制台会显示本地访问地址:http://127.0.0.1:5173,浏览器打开即可预览。
更新多信息请参考 [使用文档](http://stepui.gitee.io/stepin-template-docs/)
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2::
- 在你的公司或个人项目中使用 Stepin Template。
- 通过 [Issue](https://github.com/stepui/stepin-template/issues/new) 报告:bug:或进行咨询。
- 提交 [Pull Request](https://github.com/stepui/stepin-template/pulls) 改进 Stepin Template 的代码。
- 加入社群,与小伙伴们一同交流心得。QQ 群:441231578
## 打赏作者
如果该项目对您有所帮助,可以请作者喝一杯咖啡。
<p>
<img src="./docs/images/alipay.png" width="320px" style="display: inline-block; border-radius: 8px;" />
<img src="./docs/images/wechatpay.png" width="320px" style="display: inline-block; margin-left: 24px; border-radius: 8px;" />
</p>
+84
View File
@@ -0,0 +1,84 @@
<h1 align="center">Stepin Template</h1>
<div align="center">
开箱即用的中后台前端/设计解决方案
<br/>
(原[ Vue Antd Admin](https://github.com/iczer/vue-antd-admin/) Vue3 版本)
![GitHub package.json version (subfolder of monorepo)](https://img.shields.io/github/package-json/v/stepui/stepin-template)
![GitHub repo size](https://img.shields.io/github/repo-size/stepui/stepin-template)
![GitHub last commit](https://img.shields.io/github/last-commit/stepui/stepin-template)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/stepui/stepin-template)
![Docs by iczer](https://img.shields.io/badge/docs%20by-iczer-green)
![preview](./docs/images/preview.png)
</div>
简体中文 | [English](./README-en_US.md)
- 预览地址: https://stepui.gitee.io/stepin-template
- 使用文档: http://stepui.gitee.io/stepin-template-docs/
- 常见问题: http://stepui.gitee.io/stepin-template-docs/issue
- 国内镜像: https://gitee.com/stepui/stepin-template
## 特性
- 强大的主题定制功能,实时动态切换
- 多页签功能,助力后台管理高效开发
- 内置权限控制,兼容 ABAC 和 RBAC 模型
- 丰富的内置业务组件和常用页面模板
## 浏览器支持
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 环境要求
| git | node | yarn |
| --------- | ---------- | --------- |
| `^2.15.0` | `^16.14.0` | `^1.21.1` |
## 使用
### 拉取代码
```sh
git clone https://github.com/stepui/stepin-template.git
```
### 安装依赖
```sh
yarn install
```
### 启动
```sh
yarn dev
```
### 预览
启动成功后,控制台会显示本地访问地址:http://127.0.0.1:5173,浏览器打开即可预览。
更新多信息请参考 [使用文档](http://stepui.gitee.io/stepin-template-docs/)
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2::
- 在你的公司或个人项目中使用 Stepin Template。
- 通过 [Issue](https://github.com/stepui/stepin-template/issues/new) 报告:bug:或进行咨询。
- 提交 [Pull Request](https://github.com/stepui/stepin-template/pulls) 改进 Stepin Template 的代码。
- 加入社群,与小伙伴们一同交流心得。QQ 群:441231578
- less报错
yarn remove less && yarn add less@4.1.3 -D
## 打赏作者
如果该项目对您有所帮助,可以请作者喝一杯咖啡。
<p>
<img src="./docs/images/alipay.png" width="320px" style="display: inline-block; border-radius: 8px;" />
<img src="./docs/images/wechatpay.png" width="320px" style="display: inline-block; margin-left: 24px; border-radius: 8px;" />
</p>
+86
View File
@@ -0,0 +1,86 @@
import { series, src, dest } from 'gulp';
import clean from 'gulp-clean';
import replace from 'gulp-replace';
import vueTsc from './vueTemplateTsc';
export { default as tsc } from './vueTemplateTsc';
function cleanAll() {
return src(['./target/*', '!./target/node_modules'], { allowEmpty: true }).pipe(clean({ force: true }));
}
function copyIndex() {
return src(['./index.html']).pipe(replace('.ts', '.js')).pipe(replace(' + TS', '')).pipe(dest('./target'));
}
function copyPublic() {
return src(['./public/**/*']).pipe(dest('./target/public'));
}
function copyStyleAndJs() {
return src(['./src/**/*.{less,css,js}']).pipe(dest('./target/src'));
}
function copyAssets() {
return src(['./src/assets/**/*']).pipe(dest('./target/src/assets'));
}
function copyConfig() {
return src([
'./.env*',
'./.gitignore',
'.babelrc',
'./*.{json,cjs}',
'./LICENSE',
'!./tsconfig.*',
'!./package*.json',
]).pipe(dest('./target'));
}
function copyReadme() {
return src(['./*.md']).pipe(replace('stepin-template.git', 'stepin-template-js.git')).pipe(dest('./target'));
}
function copyPackageJson() {
return src(['./package.json'])
.pipe(replace(/,?[\r\n]+.*"(gulp[\-\w]*|vue-tsc|typescript)"[^,\r\n]*/g, ''))
.pipe(replace(/vue-tsc --noEmit && /g, ''))
.pipe(dest('./target'));
}
function cleanJsRepository() {
return src([
'../stepin-template-js/*',
'!../stepin-template-js/node_modules',
'!../stepin-template-js/.git',
'!../stepin-template-js/.history',
'!../stepin-template-js/.vscode',
]).pipe(clean({ force: true }));
}
export function copyToJsRepository() {
return src([
'./target/**/*',
'./.env*',
'./.gitignore',
'.babelrc',
'!./target/node_modules/**/*',
'!./target/node_modules',
]).pipe(dest('../stepin-template-js'));
}
export function copyDocs() {
return src(['./docs/**/*', '!./docs/.vitepress/**/*', '!./docs/.vitepress']).pipe(dest('../stepin-template-js/docs'));
}
export const makeJs = series(cleanJsRepository, copyToJsRepository, copyDocs);
export default series(
cleanAll,
vueTsc,
copyIndex,
copyPublic,
copyStyleAndJs,
copyConfig,
copyReadme,
copyAssets,
copyPackageJson
);
+19
View File
@@ -0,0 +1,19 @@
import { Transform } from 'stream';
export class BufferTransform extends Transform {
constructor(transform) {
super({
objectMode: true,
transform(file, enc, callback) {
if (file.isBuffer()) {
file.contents = Buffer.from(transform(new String(file.contents)));
}
if (file.isStream()) {
console.log('stream file', file.path);
}
return callback(null, file);
},
});
return this;
}
}
+152
View File
@@ -0,0 +1,152 @@
import { src, dest, series } from 'gulp';
import rename from 'gulp-rename';
import replace from 'gulp-replace';
import { BufferTransform } from './transform';
import ts from 'typescript';
import prettier from 'prettier';
import tsConfig from '../tsconfig.json';
tsConfig.compilerOptions.sourceMap = false;
tsConfig.compilerOptions.importsNotUsedAsValues = 'preserve';
/**
* vue 文件代码格式化
* @param {*} code
* @returns
*/
const vueFormatter = () =>
new BufferTransform((code) =>
prettier.format(code, {
trailingComma: 'es5',
parser: 'vue',
vueIndentScriptAndStyle: true,
singleQuote: true,
})
);
/**
* ts 文件代码格式化
* @param {*} code
* @returns
*/
const tsFormatter = () =>
new BufferTransform((code) =>
prettier.format(code, {
trailingComma: 'es5',
parser: 'typescript',
singleQuote: true,
})
);
/** ts编译器 */
const tsToJs = (tsStrCode) => {
const code = tsStrCode
.replace(/defineEmits<[\s\S]*>\(\);/g, (match) =>
match.replace(/'?([\w:]*)'?:\s*\[.*\];/g, "'$1',").replace(/\(e:\s*(\'[^',]*\')[^;]*;/g, '$1,')
)
.replace(/<{([^\{\}<>]*)}>\(\)/g, '([$1])');
const result = ts.transpileModule(code, tsConfig);
const jsStrCode = result.outputText;
return jsStrCode;
};
/**
* SFC 文件 ts 代码编译
* @param {*} tmpContent
* @returns
*/
const handleTmpContent = (tmpContent) => {
if (!tmpContent) {
return tmpContent;
}
return tmpContent
.replace(/(?<=((@|:|v-)[\w\-]*="))([^"]*)(?=")/g, function (match) {
if (/\s*{[\s\S]*}\s*/.test(match)) {
return tsToJs(`const patch = ${match}`).replace(/;\s*/g, '').replace('const patch = ', '');
}
return `${tsToJs(match).replace(/;\s*/g, '')}`;
})
.replace(/(?<=({{))([^{}]*)(?=}})/g, function (match) {
if (/\s*{[\s\S]*}\s*/.test(match)) {
return tsToJs(`const patch = ${match}`).replace(/;\s*/g, '').replace('const patch = ', '');
}
return `${tsToJs(match).replace(/;\s*$/, '')}`;
});
};
function transformVue(options) {
return new BufferTransform((content) =>
content
.replace(/(?<=(\<template>))([\s\S]*)(?=(\<\/template>))/, function (match) {
return handleTmpContent(match);
})
.replace(/(?<=(\<script lang="ts".*>))([\s\S]*)(?=(\<\/script>))/, (match) => tsToJs(match))
.replace(/lang="ts"/, '')
);
}
function cleanTypeImport() {
const types = [
'MenuProps',
'GuiderOption',
'IconSelectGroup',
'IconSelectOption',
'PropType',
'EChartsType',
'Color',
'FormInstance',
'PaginationProps',
'Dayjs',
'Ref',
'TreeSelectProps',
'ComponentPublicInstance',
];
const typeImportRegexp = new RegExp(
'(?<=(import\\s+[\\w,\\s]*{[^{]*[,\\s]+))(' + types.join('|') + '),?(?=([^}]*\\s*}\\s*from))',
'g'
);
return new BufferTransform((content) =>
content
.replace(/import\s+{\s*Response\s*}\s+from\s+\'.*\';?/g, '')
.replace(typeImportRegexp, '')
.replace(/import\s+.*from\s+\'.*\/interface(.d.ts)?\';?/g, '')
.replace(/import\s+{\s*}\s*from\s+\'.*\'/g, '')
);
}
function transformTs() {
return new BufferTransform((content) => tsToJs(content));
}
function tsc() {
tsConfig.compilerOptions.importsNotUsedAsValues = 'preserve';
tsConfig.compilerOptions.preserveValueImports = true;
return src(['./src/**/*.vue'])
.pipe(transformVue())
.pipe(cleanTypeImport())
.pipe(vueFormatter())
.pipe(dest('./target/src'));
}
function tsTsc() {
tsConfig.compilerOptions.importsNotUsedAsValues = 'remove';
tsConfig.compilerOptions.preserveValueImports = false;
return src(['./src/**/*.ts', '!./src/**/*.d.ts'])
.pipe(transformTs())
.pipe(replace('{ts,vue,tsx}', '{js,vue}'))
.pipe(replace('(js|ts|tsx|vue)', '(js|jsx|vue)'))
.pipe(cleanTypeImport())
.pipe(tsFormatter())
.pipe(rename((path) => (path.extname = '.js')))
.pipe(dest('./target/src'));
}
function compileViteConfig() {
return src(['./vite.config.ts'])
.pipe(transformTs())
.pipe(tsFormatter())
.pipe(rename((path) => (path.extname = '.js')))
.pipe(dest('./target'));
}
export default series(tsc, tsTsc, compileViteConfig);
+128
View File
@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo1.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>抖音同步</title>
</head>
<body>
<div id="stepin-app">
<style>
html {
overflow: hidden;
}
body {
height: 100vh;
text-align: center;
align-items: center;
background-color: #011627;
display: flex;
justify-content: center;
overflow: hidden;
}
.loader {
color: rgb(201, 195, 195);
font-family: "Poppins", sans-serif;
font-weight: 500;
font-size: 20px;
-webkit-box-sizing: content-box;
box-sizing: content-box;
height: 40px;
padding: 10px 10px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
text-align: center;
justify-content: center;
border-radius: 8px;
}
.words {
overflow: hidden;
}
.word {
display: block;
height: 100%;
padding-left: 6px;
color: #ffca29;
animation: spin_4991 4s infinite;
}
@keyframes spin_4991 {
10% {
-webkit-transform: translateY(-105%);
transform: translateY(-105%);
}
25% {
-webkit-transform: translateY(-100%);
transform: translateY(-100%);
}
35% {
-webkit-transform: translateY(-205%);
transform: translateY(-205%);
}
50% {
-webkit-transform: translateY(-200%);
transform: translateY(-200%);
}
60% {
-webkit-transform: translateY(-305%);
transform: translateY(-305%);
}
75% {
-webkit-transform: translateY(-300%);
transform: translateY(-300%);
}
85% {
-webkit-transform: translateY(-405%);
transform: translateY(-405%);
}
100% {
-webkit-transform: translateY(-400%);
transform: translateY(-400%);
}
}
</style>
<!-- <img style="margin-bottom: 8px;" width="64px" src="/vite.svg" /> -->
<div class="loader">
<!-- <img src="/vite.svg"> -->
<div> loading ...</div>
<!-- <div class="words">
<span class="word">buttons</span>
<span class="word">forms</span>
<span class="word">switches</span>
<span class="word">cards</span>
<span class="word">buttons</span>
</div> -->
</div>
<div class="" style="font-size: 15px; color: rgba(249, 244, 244, 0.55)">首次加载可能较慢,请耐心等待...</div>
</div>
<script type="module" src="/src/main.ts"></script>
<script>
if (!global) {
var global = globalThis;
}
</script>
</body>
<!-- <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/5.15.4/css/all.min.css"> -->
<style>
html {
overflow: hidden;
}
</style>
</html>
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"target": "ES6",
"module": "ESNext",
"allowSyntheticDefaultImports": true
},
"exclude": ["node_modules", "dist"]
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+74
View File
@@ -0,0 +1,74 @@
{
"name": "stepin-template",
"private": true,
"version": "1.2.0-preview",
"type": "module",
"scripts": {
"dev": "vite",
"api": "node service/index.js",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"deploy": "yarn build && gh-pages -d dist -b pages -r https://gitee.com/stepui/stepin-template.git",
"deploy:github": "yarn build --mode github && gh-pages -d dist -b master -r git@github.com:stepui/stepui.github.io.git",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:serve": "vitepress serve docs",
"docs:preview": "vitepress preview docs",
"docs:deploy": "vitepress build docs && gh-pages -d docs/.vitepress/dist -b main -r https://gitee.com/stepui/stepin-template-docs.git"
},
"dependencies": {
"@ant-design/icons-vue": "^6.1.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@vueuse/core": "^10.1.0",
"ant-design-vue": "^3.2.15",
"axios": "^0.21.1",
"clipboard": "^2.0.8",
"dayjs": "^1.11.6",
"default-passive-events": "^2.0.0",
"echarts": "^5.3.3",
"enquire.js": "^2.1.6",
"js-cookie": "^2.2.1",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"pinia": "^2.0.33",
"qs": "^6.10.1",
"stepin": "2.1.42-beta",
"vue": "^3.3.4",
"vue-json-editor": "^1.4.3",
"vue-request": "^2.0.4",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@tailwindcss/container-queries": "^0.1.0",
"@types/js-cookie": "^3.0.2",
"@types/lodash": "^4.14.191",
"@types/mockjs": "^1.0.3",
"@types/node": "^14.14.37",
"@types/nprogress": "^0.2.0",
"@types/qs": "^6.9.6",
"@vitejs/plugin-vue": "^4.1.0",
"autoprefixer": "^10.4.7",
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.26.0",
"body-parser": "^1.20.1",
"crypto-js": "^4.1.1",
"gh-pages": "^3.1.0",
"gulp": "^4.0.2",
"gulp-clean": "^0.4.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.1.4",
"highlight.js": "^11.6.0",
"less": "4.1.3",
"less-loader": "^12.3.0",
"mockjs": "^1.1.0",
"postcss": "^8.4.14",
"prettier": "^2.8.7",
"tailwindcss": "^3.2.4",
"typescript": "^4.6.4",
"unplugin-vue-components": "^0.24.1",
"vite": "^4.2.1",
"vite-plugin-compression": "^0.5.1",
"vitepress": "^1.0.0-alpha.61",
"vue-tsc": "^1.0.13"
}
}
+7510
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
plugins: [require('tailwindcss'), require('autoprefixer')],
};
+7
View File
@@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect fill="rgba(0, 0, 0, 0)" width="20" height="20" />
<g fill="rgba(0, 0, 220, 0.05)" >
<rect width="10" height="10" />
<rect x="10" y="10" width="10" height="10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+147
View File
@@ -0,0 +1,147 @@
<template>
<ThemeProvider is-root v-bind="themeConfig" :apply-style="false">
<stepin-view system-name="douyin.sync.net" :class="`${contentClass}`" :user="user" :navMode="navigation" :useTabs="useTabs" :themeList="themeList" v-model:show-setting="showSetting" v-model:theme="theme" @themeSelect="configTheme" logo-src="@/assets/logo1.png">
<template #headerActions>
<HeaderActions @showSetting="showSetting = true" />
</template>
<template #pageFooter>
<PageFooter />
</template>
<template #themeEditorTab>
<a-tab-pane tab="其它" key="other">
<Setting />
</a-tab-pane>
</template>
</stepin-view>
</ThemeProvider>
<my-personal ref="personalRef" />
<email-set ref="emailRef" />
<!-- <login-modal :unless="['/login']" /> -->
</template>
<script lang="ts" setup>
import { reactive, ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useAccountStore, useMenuStore, useSettingStore, storeToRefs, useApiStore } from '@/store';
import avatar from '@/assets/avatar.png';
import { PageFooter, HeaderActions } from '@/components/layout';
import Setting from './components/setting';
import { LoginModal } from '@/pages/login';
import { MyPersonal, EmailSet } from '@/pages/personal';
import { configTheme, themeList } from '@/theme';
import { ThemeProvider } from 'stepin';
// logout,profile
const { logout } = useAccountStore();
const showPersonalDrawer = ref<boolean>(false);
const personalRef = ref(null);
const emailRef = ref(null);
const showSetting = ref(false);
const router = useRouter();
// useMenuStore().getMenuList();
const { navigation, useTabs, theme, contentClass } = storeToRefs(useSettingStore());
const themeConfig = computed(() => themeList.find((item) => item.key === theme.value)?.config ?? {});
const user = reactive({
name: 'admin',
avatar: avatar,
menuList: [
// { title: '个人中心', key: 'personal', icon: 'UserOutlined', onClick: () => router.push('/profile') },
// { title: '设置', key: 'setting', icon: 'SettingOutlined', onClick: () => (showSetting.value = true) },
// { type: 'divider' },
{
title: '个人设置',
key: 'seting',
icon: 'SmileOutlined',
onClick: () => {
personalRef.value.show(true);
},
},
{ type: 'divider' },
// {
// title: '邮件通知',
// key: 'email',
// icon: 'BellOutlined',
// onClick: () => {
// emailRef.value.showEmail(true);
// },
// },
{ type: 'divider' },
{
title: '退出登录',
key: 'logout',
icon: 'LogoutOutlined',
onClick: () => logout().then(() => router.push('/login')),
},
],
});
onMounted(() => {
useApiStore()
.apiCheckInitStatus()
.then((res) => {
if (res.code === 0) {
useApiStore()
.apiUserInfo()
.then((res) => {
if (res.code === 0 && res.code !== '') {
if (res.data.avatar && res.data.avatar != null) {
user.avatar = `/upload/${res.data.avatar}`;
}
user.name = res.data.userName;
}
});
}
});
});
</script>
<style lang="less">
.stepin-view {
::-webkit-scrollbar {
width: 4px;
height: 4px;
border-radius: 4px;
background-color: theme('colors.primary.500');
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: theme('colors.primary.400');
&:hover {
background-color: theme('colors.primary.500');
}
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0);
border-radius: 4px;
background: theme('backgroundColor.layout');
}
}
html {
height: 100vh;
overflow-y: hidden;
}
body {
margin: 0;
height: 100vh;
overflow-y: hidden;
}
.stepin-img-checkbox {
@apply transition-transform;
&:hover {
@apply scale-105 ~"-translate-y-[2px]";
}
img {
@apply shadow-low rounded-md transition-transform;
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+30
View File
@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { PropType } from 'vue';
export type AvatarType = {
nickname: string;
avatar: string;
};
defineProps({
source: Array as PropType<AvatarType[]>,
size: {
type: Number,
default: 22,
required: false,
},
})
</script>
<template>
<div class="avatar-list">
<a-avatar :style="`margin-left: -${size / 2}px`" v-for="(item, i) in source" :size="size" :key="i"
:src="item.avatar" />
</div>
</template>
<style lang="less" scoped>
.avatar-list {
:deep(.ant-avatar) {
@apply outline-1 outline-white outline;
}
}
</style>
+110
View File
@@ -0,0 +1,110 @@
<template>
<div style="height: 240px" ref="container" class="bar-chart"></div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, PropType, ref } from 'vue';
import { EChartsType, Color } from 'echarts';
import * as echarts from 'echarts';
const container = ref<HTMLElement>();
let chart: EChartsType | null = null;
const props = defineProps({
color: Array as PropType<Color[]>,
list: Array,
});
function resize() {
chart?.resize();
}
onMounted(() => {
chart = echarts.init(container.value!);
chart.setOption({
color: props.color ?? ['#ff0000'],
backgroundColor: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{
offset: 0,
color: '#00369e',
},
{
offset: 0.33,
color: '#005cfd',
},
{
offset: 1,
color: '#a18dff',
},
],
},
grid: [
{
top: 40,
left: 56,
right: 20,
bottom: 40,
},
],
xAxis: [
{
name: '时间',
nameTextStyle: { color: 'rgba(0, 0, 0, 0)' },
type: 'category',
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { color: '#fff' },
splitLine: {
show: false,
},
},
],
darkMode: true,
yAxis: [
{
name: '销售额',
nameTextStyle: { color: 'rgba(0, 0, 0, 0)' },
type: 'value',
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { color: '#fff' },
splitLine: {
lineStyle: {
type: 'dashed',
width: 2,
color: 'rgba(255, 255, 255, 0.25)',
},
},
},
],
series: [
{
type: 'bar',
barWidth: 24,
itemStyle: {
borderRadius: 4,
},
data: props.list,
},
],
});
window.addEventListener('resize', resize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resize);
});
</script>
<style scoped lang="less">
.bar-chart {
:deep(canvas) {
@apply rounded-lg;
}
}
</style>
+116
View File
@@ -0,0 +1,116 @@
<template>
<div
style="width: 100%; height: 400px"
class="line-chart"
ref="container"
></div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue';
import type { EChartsType } from 'echarts';
import * as echarts from 'echarts';
let chart: EChartsType | null = null;
const container = ref<HTMLElement>();
function resize() {
chart?.resize();
}
onMounted(() => {
chart = echarts.init(container.value!);
chart.setOption({
color: ['#005af9', '#985af9'],
grid: [
{
top: 100,
left: 32,
right: 12,
bottom: 20,
},
],
xAxis: [
{
name: '时间',
nameTextStyle: { color: 'rgba(0 , 0, 0, 0)' },
type: 'category',
axisTick: { show: false },
axisLine: { show: false },
boundaryGap: 0,
splitLine: {
show: false,
},
},
],
yAxis: [
{
name: '销售额',
nameTextStyle: { color: 'rgba(0 , 0, 0, 0)' },
type: 'value',
axisTick: { show: false },
axisLine: { show: false },
splitLine: {
lineStyle: {
type: 'dashed',
width: 2,
color: 'rgba(0, 0, 0, 0.15)',
},
},
},
],
legend: {
show: true,
right: '8',
top: 0,
orient: 'vertical',
},
tooltip: {
show: true,
trigger: 'axis',
},
series: [
{
name: '销售额',
type: 'line',
smooth: true,
lineStyle: {
width: 3,
},
data: [
['一月', 12],
['二月', 8],
['三月', 92],
['四月', 32],
['五月', 22],
['六月', 89],
['七月', 72],
],
},
{
name: '订单',
type: 'line',
smooth: true,
width: 4,
lineStyle: {
width: 3,
},
data: [
['一月', 12],
['二月', 8],
['三月', 24],
['四月', 32],
['五月', 56],
['六月', 56],
['七月', 56],
],
},
],
});
window.addEventListener('resize', resize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resize);
});
</script>
@@ -0,0 +1,142 @@
<script lang="ts" setup>
import { computed, nextTick, PropType, ref, toRefs } from 'vue';
export type AntInputType =
| 'input'
| 'textarea'
| 'radio'
| 'timePicker'
| 'datePicker'
| 'rangePicker'
| 'select'
| 'mention'
| 'rate'
| 'upload'
| 'treeSelect'
| 'transfer'
| 'checkbox'
| 'cascader'
| 'autoComplete'
| 'inputNumber'
| 'slider'
| 'switch';
const emit = defineEmits<{
(e: 'update:edit', edit: boolean): void;
(e: 'update:value', value: any): void;
(e: 'pressEnter', event: KeyboardEvent): void;
}>();
const props = defineProps({
value: [String, Number, Boolean, Object],
edit: {
type: Boolean,
default: null,
},
editOnClick: {
type: Boolean,
default: true,
},
type: {
type: String as PropType<AntInputType>,
default: 'input',
validator(val: string) {
return [
'input',
'radio',
'timePicker',
'datePicker',
'rangePicker',
'select',
'mention',
'rate',
'upload',
'treeSelect',
'transfer',
'checkbox',
'cascader',
'autoComplete',
'inputNumber',
'slider',
'switch',
'textarea',
].includes(val);
},
},
options: Object,
});
const { type } = toRefs(props);
const component = computed(() => {
const _type = type.value;
return 'A' + _type.substring(0, 1).toUpperCase() + _type.substring(1);
});
const cacheEdit = ref(false);
const _edit = computed({
get() {
if (props.edit !== null) {
cacheEdit.value = props.edit;
}
return props.edit ?? cacheEdit.value;
},
set(val) {
cacheEdit.value = val;
emit('update:edit', val);
},
});
const input = ref();
function editCell() {
if (props.editOnClick) {
_edit.value = true;
nextTick(() => input.value?.focus());
}
}
function complete() {
_edit.value = false;
}
const cacheVal: any = ref(null);
const _value = computed({
get() {
if (props.value !== undefined) {
cacheVal.value = props.value;
}
return props.value ?? cacheVal.value;
},
set(val) {
cacheVal.value = val;
emit('update:value', val);
},
});
</script>
<template>
<slot v-if="_edit" class="editable-cell-input" name="input">
<component
ref="input"
@keyup.enter="complete"
@blur="complete"
v-model:value="_value"
class="editable-cell-input-component"
v-bind="options"
:is="component"
/>
</slot>
<div v-else @click="editCell" class="editable-cell-show">
<slot>
{{ value }}
</slot>
</div>
</template>
<style lang="less" scoped>
.editable-cell {
&-input {
&-component {
}
}
}
</style>
@@ -0,0 +1,2 @@
import EditableCell from './EditableCell.vue';
export default EditableCell;
+49
View File
@@ -0,0 +1,49 @@
<template>
<div v-if="parts" class="splitter">
<template v-for="(part, i) in parts" :key="i">
<span class="splitter-part">{{ part }}</span>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
export default defineComponent({
name: 'Splitter',
props: {
value: String,
partLength: { type: Number, default: 4 },
sensitive: Array as PropType<number[]>,
},
setup(props, { attrs, slots, emit }) {},
computed: {
parts(): string[] {
let { value = '', partLength, sensitive = [] } = this;
const [start = -1, end = -1] = sensitive;
let sense = '';
for (let i = 0; i < end - start; i++) {
sense += '*';
}
value = `${value.substring(0, start)}${sense}${value.substring(
end,
value.length
)}`;
const parts = [];
for (let i = 0; i < value.length; i += partLength) {
parts.push(value.substring(i, i + partLength));
}
return parts;
},
},
});
</script>
<style lang="less" scoped>
.splitter {
&-part {
&:not(:first-child) {
@apply ml-2;
}
}
}
</style>
@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { ref, PropType, watch } from 'vue';
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
import { useFullScreen } from '@/utils/htmlHelper';
const prop = defineProps({
target: { type: [String, Object] as PropType<string | HTMLElement>, required: true },
});
const emit = defineEmits<{
(e: 'change', value: boolean);
}>();
const { enterFullScreen, exitFullscreen, isEnter } = useFullScreen(prop.target);
function toggle() {
if (isEnter.value) {
exitFullscreen();
} else {
enterFullScreen();
}
}
watch(isEnter, (val) => emit('change', val));
</script>
<template>
<div class="inline-block text-lg" @click="toggle">
<FullscreenExitOutlined v-if="isEnter" />
<FullscreenOutlined v-else />
</div>
</template>
<style scoped lang="less"></style>
+366
View File
@@ -0,0 +1,366 @@
<script lang="ts" setup>
import { offsetScreen } from '@/utils/htmlHelper';
import { GuiderOption, GuideTarget } from './interface';
import { PropType, watch, reactive, computed, ref, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps({
current: [HTMLElement, Object, String] as PropType<GuideTarget>,
options: {
required: true,
type: Array<GuiderOption>,
},
show: Boolean,
});
const index = ref(0);
const currentIndex = computed({
get(): number {
if (!props.current) {
return index.value;
}
index.value = props.options.findIndex((item) => item.target === props.current);
return index.value;
},
set(val) {
const target = props.options[val];
if (target) {
index.value = val;
emit('update:current', target.target);
} else {
index.value = 0;
emit('close');
emit('update:current', props.options[0].target);
emit('update:show', false);
}
},
});
const doc = ref<HTMLElement>();
const flag = ref(props.show);
const location = reactive({
width: 0,
height: 0,
left: 0,
top: 0,
right: 0,
bottom: 0,
});
// 事件
// defineEmits(['close', 'update:show', 'update:current'])
const emit = defineEmits<{
(e: 'close'): void;
(e: 'update:show', show: boolean): void;
(e: 'update:current', target?: GuideTarget): void;
}>();
// 目标 html 元素
const targetEl = computed<HTMLElement>(() => {
const _el = props.current ?? props.options[index.value]?.target;
if (!_el) {
return;
}
if (typeof _el === 'string') {
return document.querySelector(_el);
} else {
// @ts-ignore
return _el instanceof HTMLElement ? _el : _el.$el;
}
});
// 方位
type Direction = 'vertical' | 'horizontal';
// 朝向
type Site = 'top' | 'right' | 'bottom' | 'left';
// 位置
type Placement = {
main: Direction;
sub: Direction;
vertical: Site | 'center';
horizontal: Site | 'center';
};
// doc 文档显示位置
const placement = computed<Placement>(() => {
const { offsetWidth: docWidth, offsetHeight: docHeight } = doc.value || {
offsetWidth: 0,
offsetHeight: 0,
};
const { left: tLeft, top: tTop, right: tRight, bottom: tBottom, height: tHeight, width: tWidth } = location;
const p: Placement = {
main: 'horizontal',
sub: 'vertical',
horizontal: 'left',
vertical: 'top',
};
// 判断文档位置在主方向为水平还是垂直
if (Math.max(tLeft, tRight) >= Math.max(tTop, tBottom)) {
p.main = 'horizontal';
p.sub = 'vertical';
p.horizontal = tLeft > tRight ? 'left' : 'right';
if (tTop + tHeight / 2 < docHeight / 2 && tBottom + tHeight / 2 > docHeight / 2) {
p.vertical = 'top';
} else if (tTop + tHeight / 2 > docHeight / 2 && tBottom + tHeight / 2 < docHeight / 2) {
p.vertical = 'bottom';
} else {
p.vertical = 'center';
}
} else {
p.main = 'vertical';
p.sub = 'horizontal';
p.vertical = tTop > tBottom ? 'top' : 'bottom';
if (tLeft + tWidth / 2 < docWidth / 2 && tRight + tWidth / 2 > docWidth / 2) {
p.horizontal = 'left';
} else if (tLeft + tWidth / 2 > docWidth / 2 && tRight + tWidth / 2 < docWidth / 2) {
p.horizontal = 'right';
} else {
p.horizontal = 'center';
}
}
return p;
});
type Position = {
top?: number;
right?: number;
bottom?: number;
left?: number;
};
// 指引文档位置
const docPosition = computed(() => {
const p: Position = { left: 0, top: 0 };
if (!props.show || !doc.value || !flag.value) {
return p;
}
const { top, left, right, bottom, height, width } = location;
const place = placement.value;
const main = place[place.main] as Site;
const sub = place[place.sub];
const margin = 10;
const offset = (place.main === 'horizontal' ? doc.value?.offsetWidth : doc.value?.offsetHeight) ?? 0;
p[main] = location[main] - offset - margin;
if (main === 'right') {
p.left = left + width + margin;
} else if (main === 'bottom') {
p.top = top + height + margin;
}
if (sub === 'center') {
if (place.main === 'horizontal') {
p.top = top + height / 2 - (doc.value?.offsetHeight ?? 0) / 2;
} else {
p.left = left + width / 2 - (doc.value?.offsetWidth ?? 0) / 2;
}
} else {
p[sub] = location[sub];
}
if (p.left === undefined) {
p.left = window.innerWidth - p.right! - doc.value?.offsetWidth!;
}
if (p.top === undefined) {
p.top = window.innerHeight - p.bottom! - doc.value?.offsetHeight!;
}
return p;
});
// 指示箭头位置
const arrowStyle = computed(() => {
const p: Position = { left: -16, top: -16 };
if (!props.show || !doc.value || !flag.value) {
return p;
}
const place = placement.value;
const main = place[place.main] as Site;
const sub = place[place.sub];
const { offsetHeight = 0, offsetWidth = 0 } = doc.value ?? {};
if (main === 'left') {
p.left = (offsetWidth ?? 0) - 4;
} else if (main === 'top') {
p.top = (offsetHeight ?? 0) - 4;
}
if (place.main === 'horizontal') {
if (sub === 'center') {
p.top = offsetHeight / 2 - 10;
} else if (sub === 'bottom') {
p.top = offsetHeight - location.height / 2 - 10;
} else {
p.top = location.height / 2 - 10;
}
}
if (place.main === 'vertical') {
if (sub === 'center') {
p.left = offsetWidth / 2 - 10;
} else if (sub === 'right') {
p.left = offsetWidth - location.width / 2 - 10;
} else {
p.left = location.width / 2 - 10;
}
}
return p;
});
/**
* 设置目标元素位置
*/
function setPosition() {
const el = targetEl.value;
if (!el) {
return;
}
const p = offsetScreen(el);
location.left = p[0];
location.top = p[1];
location.width = el.offsetWidth;
location.height = el.offsetHeight;
location.right = window.innerWidth - location.width - location.left;
location.bottom = window.innerHeight - location.height - location.top;
}
watch(() => [targetEl.value, props.show], setPosition);
watch(
() => props.show,
(val) => {
flag.value = val;
},
{
flush: 'post',
}
);
onMounted(() => {
window.addEventListener('resize', setPosition);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', setPosition);
});
function nextStep() {
currentIndex.value += 1;
}
function onClose() {
emit('close');
emit('update:show', false);
}
</script>
<template>
<Teleport to="body">
<div class="guider" :style="`display: ${show ? 'static' : 'none'}`">
<div class="guider-left" :style="`border-left: ${location.left - 2}px solid rgba(0, 0, 0, 0.25);`"></div>
<div
class="guider-top"
:style="`left: ${location.left - 2}px; width: ${location.width + 4}px; border-top: ${
location.top - 2
}px solid rgba(0, 0, 0, 0.25);`"
></div>
<div
class="guider-right"
:style="`left: ${location.left + location.width + 2}px; background-color: rgba(0, 0, 0, 0.25)`"
></div>
<div
class="guider-bottom"
:style="`left: ${location.left - 2}px; width: ${location.width + 4}px; top: ${
location.top + location.height + 2
}px`"
></div>
<div
ref="doc"
class="guider-doc flex flex-col justify-between rounded-md"
:style="`left: ${docPosition.left}px;top:${docPosition.top}px`"
>
<div
class="arrow"
:style="`left: ${arrowStyle.left}px; top: ${arrowStyle.top}px; border-${
placement[placement.main]
}-color: white`"
></div>
<div class="guider-content">
<h1>第一步</h1>
<div>
<slot></slot>
</div>
</div>
<div class="guider-footer flex justify-between w-full">
<a-button @click="onClose">关闭</a-button>
<a-button type="primary" @click="nextStep">下一步</a-button>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped lang="less">
.guider-left {
position: fixed;
left: 0px;
top: 0px;
height: 100vh;
z-index: 99;
}
.guider-top {
position: fixed;
z-index: 99;
top: 0;
}
.guider-right {
top: 0;
right: 0;
position: fixed;
z-index: 99;
height: 100vh;
}
.guider-bottom {
position: fixed;
background-color: rgba(0, 0, 0, 0.25);
bottom: 0;
z-index: 99;
}
.guider-doc {
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.125);
width: 20%;
height: 200px;
background-color: white;
position: fixed;
z-index: 100;
box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.5);
padding: 6px 8px;
.arrow {
transition: all 0.25s ease-in;
border-width: 10px;
z-index: 9;
border-style: solid;
position: absolute;
border-color: transparent;
}
.guider-footer {
}
}
</style>
+4
View File
@@ -0,0 +1,4 @@
import Guider from './Guider.vue';
export type { GuideTarget, GuiderOption } from './interface';
export default Guider;
+7
View File
@@ -0,0 +1,7 @@
import { ComponentPublicInstance, FunctionalComponent, AsyncComponentOptions, AsyncComponentLoader } from 'vue';
export type GuideTarget = HTMLElement | ComponentPublicInstance | String;
export type GuiderOption = {
target?: GuideTarget;
doc?: GuideTarget | FunctionalComponent | AsyncComponentOptions | AsyncComponentLoader;
};
+4
View File
@@ -0,0 +1,4 @@
<template>
<router-view></router-view>
</template>
<script lang="ts" setup></script>
+86
View File
@@ -0,0 +1,86 @@
<template>
<div class="common-view">
<div class="common-header">
<div class="common-header-main">
<div class="logo">
<img class="img" src="@/assets/vite.svg" />
</div>
<div class="navigation">
<div class="nav-item">
<stepin-link to="/login"> 文档 </stepin-link>
</div>
<div class="nav-item">
<stepin-link to="/login"> API </stepin-link>
</div>
<div class="nav-item">
<stepin-link to="/login"> 关于 </stepin-link>
</div>
<div class="nav-item">
<stepin-link to="/login"> 商业合作 </stepin-link>
</div>
</div>
<div class="actions">
<a-button class="login-btn" type="primary">注册</a-button>
</div>
</div>
</div>
<div class="common-content">
<div class="main">
<router-view />
</div>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style scoped lang="less">
.common-view {
display: grid;
min-height: 100vh;
grid-template-rows: 64px 1fr;
@apply bg-gray-800;
.common-header {
@apply bg-gray-800 flex items-center pt-lg;
&-main {
width: 1400px;
margin: 0 auto;
@apply flex items-center;
.logo {
flex: none;
.img {
height: 36px;
}
}
.navigation {
flex: 1;
display: flex;
align-items: center;
.nav-item {
font-size: 18px;
margin-left: 64px;
.stepin-link {
color: theme('colors.text-inverse');
&:hover {
color: theme('colors.primary.500');
}
}
}
}
.actions {
flex: none;
.login-btn {
height: 38px;
width: 88px;
font-size: 16px;
@apply bg-gray-700 border-gray-600 rounded-md hover:bg-gray-600;
}
}
}
}
.common-content {
.main {
width: 1400px;
margin: 0 auto;
}
}
}
</style>
@@ -0,0 +1,118 @@
<script lang="ts" setup>
import { LogoutOutlined } from '@ant-design/icons-vue';
import { onMounted } from 'vue';
import { ThemeProvider, alert } from 'stepin';
onMounted(() => {
// alert.info(
// `<div class="text-text">
// Stepin is a fast, light framework to Vue3 try it out today with the
// <span class="underline">Stepin Template Beta</span>.
// </div>`,
// { renderRaw: true, duration: -1 }
// );
});
const navList = [
{
title: 'Products',
children: [
{
title: 'Stepin Template',
list: ['Stepin Pro', 'Stepin Style', 'Stepin Admin'],
},
{
title: 'Stepin',
list: ['Stepin Vue', 'Stepin React', 'Stepin Angular'],
},
],
},
{
title: 'Developers',
children: [
{
title: 'Developers',
list: ['Docs', 'Get Started', 'UI Library', 'Community', 'Open Source'],
},
],
},
{
title: 'Sponsors',
},
{
title: 'Business',
children: [{ title: 'Business', list: ['Contact Us', 'Cooperation', 'Support'] }],
},
{
title: 'About Us',
},
];
</script>
<template>
<ThemeProvider :color="{ middle: { 'bg-base': '#1896ff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true">
<div class="front-view flex flex-col">
<div class="text-text flex-1">
<div class="front-header flex items-baseline py-md px-xl">
<div class="text-xxl text-text hover:text-text">
<!-- <img src="@/assets/png.svg" /> -->
dy.sync.net
</div>
<!-- <div style="width: calc(100% - 430px)" class="front-navigation mx-xl flex overflow-hidden items-center text-lg overflow-ellipsis whitespace-nowrap">
<div :class="`front-nav-item flex items-center cursor-pointer mx-base ${nav.children ? 'with-list' : ''}`" v-for="nav in navList">
<template v-if="!nav.children">
{{ nav.title }}
</template>
<a-popover :mouseEnterDelay="0.1" v-else placement="bottom">
<div class="front-nav-item-content">
{{ nav.title }}
</div>
<template #content>
<div class="flex">
<div class="not-[:first-child]:ml-lg" v-for="group in nav.children">
<h3>{{ group.title }}</h3>
<div class="cursor-pointer hover:text-text text-subtext font-light py-xs text-lg" v-for="item in group.list">
{{ item }}
</div>
</div>
</div>
</template>
</a-popover>
</div>
</div>
<div>
<router-link to="/login" class="h-[46px] border-transparent hover:text-text hover:border-transparent text-lg text-text">
<LogoutOutlined class="mr-xs" />
Sign In
</router-link>
<a-button class="ml-md px-lg border-text hover:border-text hover:bg-text border-2 h-[46px] hover:text-bg-container" size="large">Get Started</a-button>
</div> -->
</div>
<div class="front-content ">
<router-view />
</div>
</div>
</div>
</ThemeProvider>
</template>
<style lang="less" scoped>
.front-view {
.front-header {
.front-nav-item {
&.with-list .front-nav-item-content {
&:after {
content: '';
@apply ~"h-[8px]" ~"w-[8px]" transition-transform ml-2 inline-block border-text border-l-0 border-t-0 border-r-2 border-b-2 border-solid ~"rotate-[-135deg]" translate-y-1/4;
}
&:hover {
&:after {
@apply ~"rotate-[45deg]" translate-y-0;
}
}
}
}
}
.front-content {
height: 100vh;
}
}
</style>
+65
View File
@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { LogoutOutlined } from '@ant-design/icons-vue';
import { onMounted } from 'vue';
import { ThemeProvider, alert } from 'stepin';
import http from '@/store/http';
import { useRouter } from 'vue-router';
const router = useRouter();
// console.log(router.getRoutes());
onMounted(() => {
// alert.info(
// `<div class="text-text">
// Stepin is a fast, light framework to Vue3 try it out today with the
// <span class="underline">Stepin Template Beta</span>.
// </div>`,
// { renderRaw: true, duration: -1 }
// );
// console.log(router.getRoutes());
if (http.checkAuthorization()) {
// console.log(22222);
router.push('/dashboard');
} else {
router.push('/login');
}
});
</script>
<template>
<ThemeProvider :color="{ middle: { 'bg-base': '#fff','bg-container':'#fff','bg-container-light':'#fff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true">
<div class="front-view flex flex-col" style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)">
<div class="text-xxl text-text hover:text-text" style="margin-left:20px;">
<img src="/logo1.png" />
douyin.sync.net
</div>
<div class="front-content ">
<router-view />
</div>
</div>
</ThemeProvider>
</template>
<style lang="less" scoped>
.front-view {
.front-header {
.front-nav-item {
&.with-list .front-nav-item-content {
&:after {
content: '';
@apply ~"h-[8px]" ~"w-[8px]" transition-transform ml-2 inline-block border-text border-l-0 border-t-0 border-r-2 border-b-2 border-solid ~"rotate-[-135deg]" translate-y-1/4;
}
&:hover {
&:after {
@apply ~"rotate-[45deg]" translate-y-0;
}
}
}
}
}
.front-content {
height: 100vh;
}
.front-view {
height: 100vh;
}
}
</style>
+100
View File
@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { reactive } from 'vue';
import { StepinHeaderAction } from 'stepin';
import Notice from '@/components/notice/Notice.vue';
import DayNightSwitch from '@/components/switch/DayNightSwitch.vue';
import { BellOutlined } from '@ant-design/icons-vue';
import Fullscreen from '../fullscreen/Fullscreen.vue';
defineEmits<{
(e: 'showSetting'): void;
}>();
const noticeList = reactive([
{
title: '消息',
list: [
{
title: '影佑',
content: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
img: 'src/assets/avatar/face-1.jpg',
time: 0,
},
],
},
]);
</script>
<template>
<!-- <StepinHeaderAction>
<a-input placeholder="开始搜索...">
<template #prefix>
<search-outlined />
</template>
</a-input>
</StepinHeaderAction> -->
<StepinHeaderAction>
<DayNightSwitch />
</StepinHeaderAction>
<StepinHeaderAction>
<a-tooltip>
<!-- placement="rightTop" -->
<template #title><span style="color:yellow">Swagger Api</span></template>
<a class="action-item" href="/swagger" target="_blank">
<!-- <ApiOutlined /> -->
<img class="gitee-logo" alt="swagger api" src="@/assets/swagger.png" />
</a>
</a-tooltip>
</StepinHeaderAction>
<!-- <StepinHeaderAction>
<a class="action-item" href="http://github.com/stepui/stepin-template" target="_blank">
<GithubOutlined />
</a>
</StepinHeaderAction>
<StepinHeaderAction>
<a class="action-item" href="http://gitee.com/stepui/stepin-template" target="_blank">
<img class="gitee-logo" src="@/assets/gitee.svg" />
</a>
</StepinHeaderAction> -->
<!-- <StepinHeaderAction>
<div class="action-item setting" @click="$emit('showSetting')">
<SettingOutlined />
</div>
</StepinHeaderAction> -->
<!-- <a-popover placement="bottomRight">
<StepinHeaderAction>
<div class="action-item notice">
<BellOutlined />
</div>
</StepinHeaderAction>
<template #content>
<Notice :data-source="noticeList" />
</template>
</a-popover> -->
<!-- <StepinHeaderAction>
<Fullscreen class="-mx-xs -my-sm h-[56px] px-xs py-sm flex items-center" target=".stepin-layout" />
</StepinHeaderAction> -->
</template>
<style scoped lang="less">
.gitee-logo {
width: 20px;
}
.action-item {
font-size: 20px;
height: 100%;
margin: 0 -8px;
padding: 0 4px;
line-height: 40px;
display: flex;
align-items: center;
&.setting {
font-size: 18px;
}
&.notice {
font-size: 18px;
}
}
</style>
+3
View File
@@ -0,0 +1,3 @@
<template>
<div>link view</div>
</template>
+41
View File
@@ -0,0 +1,41 @@
<script lang="ts" setup></script>
<template>
<div class="page-footer">
<div class="links">
<!-- <a class="link" href="https://github.com/stepui/stepin-template" target="_blank"> Stepin 首页 </a> -->
<!-- <a class="link" href="https://github.com/stepui/stepin-template" target="_blank">
<GithubOutlined />
</a> -->
<!-- <a class="link" href="https://www.antdv.com/docs/vue/introduce-cn/" target="_blank"> Ant Design </a> -->
</div>
<div class="copyright">
Copyright
<CopyrightOutlined class="icon-copyright" />
2023 dy.net
</div>
</div>
</template>
<style scoped lang="less">
.page-footer {
text-align: center;
@apply text-gray-400;
.links {
display: flex;
justify-content: center;
.link {
@apply hover:text-gray-400 pl-4 pr-4;
}
}
.copyright {
margin-top: 8px;
.icon-copyright {
margin: 0;
}
}
}
</style>
+5
View File
@@ -0,0 +1,5 @@
export { default as HeaderActions } from './HeaderActions.vue';
export { default as PageFooter } from './PageFooter.vue';
export { default as BlankView } from './BlankView.vue';
// export { default as CommonView } from './CommonView.vue';
export { default as FrontView } from './FrontView.vue';
+59
View File
@@ -0,0 +1,59 @@
<script lang="ts" setup>
import { ref, PropType, computed } from 'vue';
import { PaginationProps } from 'ant-design-vue';
const props = defineProps({
pagination: { type: [Object, Boolean] as PropType<PaginationProps> },
column: { type: Number, default: 8 },
gap: [Array<Number>, Number],
dataSource: { type: Array<any>, default: [] },
});
const _pagination = computed<PaginationProps>(() => {
if (props.pagination && typeof props.pagination === 'boolean') {
return {};
}
return props.pagination ?? {};
});
const list = computed(() => {
if (typeof props.pagination === 'boolean' && !props.pagination) {
return props.dataSource?.slice(0);
}
const { current = 1, pageSize = 10 } = _pagination.value;
let start = 0;
let end = pageSize;
if (props.dataSource.length > pageSize) {
start = (current - 1) * pageSize;
end = current * pageSize;
}
return props.dataSource?.slice(start, end);
});
const col = computed(() => (list.value.length > 0 ? props.column : 1));
</script>
<template>
<div
v-bind="$attrs"
class="grid-list grid"
:style="`${column ? 'grid-template-columns:repeat(' + col + ', minmax(0, 1fr))' : ''}`"
>
<template v-if="list.length > 0" v-for="item in list">
<slot name="renderItem" :item="item">
{{ item }}
</slot>
</template>
<template v-else>
<a-empty />
</template>
</div>
<a-pagination
class="mt-3"
v-if="pagination"
v-bind="{ total: dataSource?.length, ...(pagination as PaginationProps) }"
/>
</template>
<style lang="less" scoped>
.grid-list {
}
</style>
+71
View File
@@ -0,0 +1,71 @@
<template>
<div class="loader relative">
<span class="absolute -bottom-12 text-primary-500">loading...</span>
</div>
</template>
<style scoped lang="css">
.loader {
width: 48px;
height: 48px;
margin: auto;
position: relative;
}
.loader:before {
content: '';
width: 48px;
height: 5px;
background: var(--color-primary-6);
position: absolute;
top: 60px;
left: 0;
border-radius: 50%;
animation: shadow324 0.5s linear infinite;
}
.loader:after {
content: '';
width: 100%;
height: 100%;
background: var(--color-primary-4);
position: absolute;
top: 0;
left: 0;
border-radius: 4px;
animation: jump7456 0.5s linear infinite;
}
@keyframes jump7456 {
15% {
border-bottom-right-radius: 3px;
}
25% {
transform: translateY(9px) rotate(22.5deg);
}
50% {
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
border-bottom-right-radius: 40px;
}
75% {
transform: translateY(9px) rotate(67.5deg);
}
100% {
transform: translateY(0) rotate(90deg);
}
}
@keyframes shadow324 {
0%,
100% {
transform: scale(1, 1);
}
50% {
transform: scale(1.2, 1);
}
}
</style>
+529
View File
@@ -0,0 +1,529 @@
<template>
<div class="socket">
<div class="gel center-gel">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c1 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c2 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c3 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c4 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c5 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c6 r1">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c7 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c8 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c9 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c10 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c11 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c12 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c13 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c14 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c15 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c16 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c17 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c18 r2">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c19 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c20 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c21 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c22 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c23 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c24 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c25 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c26 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c28 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c29 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c30 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c31 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c32 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c33 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c34 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c35 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c36 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
<div class="gel c37 r3">
<div class="hex-brick h1"></div>
<div class="hex-brick h2"></div>
<div class="hex-brick h3"></div>
</div>
</div>
</template>
<style scoped lang="css">
.socket {
width: 200px;
height: 200px;
position: absolute;
left: 50%;
margin-left: -100px;
top: 50%;
margin-top: -100px;
}
.hex-brick {
background: var(--color-primary-7);
width: 30px;
height: 17px;
position: absolute;
top: 5px;
animation-name: fade00;
animation-duration: 2s;
animation-iteration-count: infinite;
-webkit-animation-name: fade00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
}
.h2 {
transform: rotate(60deg);
-webkit-transform: rotate(60deg);
}
.h3 {
transform: rotate(-60deg);
-webkit-transform: rotate(-60deg);
}
.gel {
height: 30px;
width: 30px;
transition: all 0.3s;
-webkit-transition: all 0.3s;
position: absolute;
top: 50%;
left: 50%;
}
.center-gel {
margin-left: -15px;
margin-top: -15px;
animation-name: pulse00;
animation-duration: 2s;
animation-iteration-count: infinite;
-webkit-animation-name: pulse00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
}
.c1 {
margin-left: -47px;
margin-top: -15px;
}
.c2 {
margin-left: -31px;
margin-top: -43px;
}
.c3 {
margin-left: 1px;
margin-top: -43px;
}
.c4 {
margin-left: 17px;
margin-top: -15px;
}
.c5 {
margin-left: -31px;
margin-top: 13px;
}
.c6 {
margin-left: 1px;
margin-top: 13px;
}
.c7 {
margin-left: -63px;
margin-top: -43px;
}
.c8 {
margin-left: 33px;
margin-top: -43px;
}
.c9 {
margin-left: -15px;
margin-top: 41px;
}
.c10 {
margin-left: -63px;
margin-top: 13px;
}
.c11 {
margin-left: 33px;
margin-top: 13px;
}
.c12 {
margin-left: -15px;
margin-top: -71px;
}
.c13 {
margin-left: -47px;
margin-top: -71px;
}
.c14 {
margin-left: 17px;
margin-top: -71px;
}
.c15 {
margin-left: -47px;
margin-top: 41px;
}
.c16 {
margin-left: 17px;
margin-top: 41px;
}
.c17 {
margin-left: -79px;
margin-top: -15px;
}
.c18 {
margin-left: 49px;
margin-top: -15px;
}
.c19 {
margin-left: -63px;
margin-top: -99px;
}
.c20 {
margin-left: 33px;
margin-top: -99px;
}
.c21 {
margin-left: 1px;
margin-top: -99px;
}
.c22 {
margin-left: -31px;
margin-top: -99px;
}
.c23 {
margin-left: -63px;
margin-top: 69px;
}
.c24 {
margin-left: 33px;
margin-top: 69px;
}
.c25 {
margin-left: 1px;
margin-top: 69px;
}
.c26 {
margin-left: -31px;
margin-top: 69px;
}
.c27 {
margin-left: -79px;
margin-top: -15px;
}
.c28 {
margin-left: -95px;
margin-top: -43px;
}
.c29 {
margin-left: -95px;
margin-top: 13px;
}
.c30 {
margin-left: 49px;
margin-top: 41px;
}
.c31 {
margin-left: -79px;
margin-top: -71px;
}
.c32 {
margin-left: -111px;
margin-top: -15px;
}
.c33 {
margin-left: 65px;
margin-top: -43px;
}
.c34 {
margin-left: 65px;
margin-top: 13px;
}
.c35 {
margin-left: -79px;
margin-top: 41px;
}
.c36 {
margin-left: 49px;
margin-top: -71px;
}
.c37 {
margin-left: 81px;
margin-top: -15px;
}
.r1 {
animation-name: pulse00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.2s;
-webkit-animation-name: pulse00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.2s;
}
.r2 {
animation-name: pulse00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.4s;
-webkit-animation-name: pulse00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.4s;
}
.r3 {
animation-name: pulse00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.6s;
-webkit-animation-name: pulse00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.6s;
}
.r1 > .hex-brick {
animation-name: fade00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.2s;
-webkit-animation-name: fade00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.2s;
}
.r2 > .hex-brick {
animation-name: fade00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.4s;
-webkit-animation-name: fade00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.4s;
}
.r3 > .hex-brick {
animation-name: fade00;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.6s;
-webkit-animation-name: fade00;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.6s;
}
@keyframes pulse00 {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
-webkit-transform: scale(0.01);
transform: scale(0.01);
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes fade00 {
0% {
background: var(--color-primary-5);
}
50% {
background: var(--color-primary-7);
}
100% {
background: var(--color-primary-5);
}
}
</style>
@@ -0,0 +1,252 @@
<template>
<div class="loader">
<div>
<ul>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
<li>
<svg fill="currentColor" viewBox="0 0 90 120">
<path
d="M90,0 L90,120 L11,120 C4.92486775,120 0,115.075132 0,109 L0,11 C0,4.92486775 4.92486775,0 11,0 L90,0 Z M71.5,81 L18.5,81 C17.1192881,81 16,82.1192881 16,83.5 C16,84.8254834 17.0315359,85.9100387 18.3356243,85.9946823 L18.5,86 L71.5,86 C72.8807119,86 74,84.8807119 74,83.5 C74,82.1745166 72.9684641,81.0899613 71.6643757,81.0053177 L71.5,81 Z M71.5,57 L18.5,57 C17.1192881,57 16,58.1192881 16,59.5 C16,60.8254834 17.0315359,61.9100387 18.3356243,61.9946823 L18.5,62 L71.5,62 C72.8807119,62 74,60.8807119 74,59.5 C74,58.1192881 72.8807119,57 71.5,57 Z M71.5,33 L18.5,33 C17.1192881,33 16,34.1192881 16,35.5 C16,36.8254834 17.0315359,37.9100387 18.3356243,37.9946823 L18.5,38 L71.5,38 C72.8807119,38 74,36.8807119 74,35.5 C74,34.1192881 72.8807119,33 71.5,33 Z"
></path>
</svg>
</li>
</ul>
</div>
<span class="text-primary-500 text-xxxl">Loading...</span>
</div>
</template>
<style scoped lang="css">
.loader {
--background: linear-gradient(135deg, var(--color-primary-3), var(--color-primary-6));
--shadow: var(--color-primary-7);
--text: var(--color-primary-6);
--page: rgba(255, 255, 255, 0.36);
--page-fold: rgba(255, 255, 255, 0.52);
--duration: 4s;
width: 200px;
height: 140px;
position: relative;
transform: scale(0.65);
}
.loader:before,
.loader:after {
--r: -6deg;
content: '';
position: absolute;
bottom: 8px;
width: 120px;
top: 80%;
box-shadow: 0 16px 12px var(--shadow);
transform: rotate(var(--r));
}
.loader:before {
left: 4px;
}
.loader:after {
--r: 6deg;
right: 4px;
}
.loader div {
width: 100%;
height: 100%;
border-radius: 13px;
position: relative;
z-index: 1;
perspective: 600px;
box-shadow: 0 4px 6px var(--shadow);
background-image: var(--background);
}
.loader div ul {
margin: 0;
padding: 0;
list-style: none;
position: relative;
}
.loader div ul li {
--r: 180deg;
--o: 0;
--c: var(--page);
position: absolute;
top: 10px;
left: 10px;
transform-origin: 100% 50%;
color: var(--c);
opacity: var(--o);
transform: rotateY(var(--r));
-webkit-animation: var(--duration) ease infinite;
animation: var(--duration) ease infinite;
}
.loader div ul li:nth-child(2) {
--c: var(--page-fold);
-webkit-animation-name: page-2;
animation-name: page-2;
}
.loader div ul li:nth-child(3) {
--c: var(--page-fold);
-webkit-animation-name: page-3;
animation-name: page-3;
}
.loader div ul li:nth-child(4) {
--c: var(--page-fold);
-webkit-animation-name: page-4;
animation-name: page-4;
}
.loader div ul li:nth-child(5) {
--c: var(--page-fold);
-webkit-animation-name: page-5;
animation-name: page-5;
}
.loader div ul li svg {
width: 90px;
height: 120px;
display: block;
}
.loader div ul li:first-child {
--r: 0deg;
--o: 1;
}
.loader div ul li:last-child {
--o: 1;
}
.loader span {
display: block;
left: 0;
right: 0;
top: 100%;
margin-top: 20px;
text-align: center;
color: var(--text);
}
@keyframes page-2 {
0% {
transform: rotateY(180deg);
opacity: 0;
}
20% {
opacity: 1;
}
35%,
100% {
opacity: 0;
}
50%,
100% {
transform: rotateY(0deg);
}
}
@keyframes page-3 {
15% {
transform: rotateY(180deg);
opacity: 0;
}
35% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
65%,
100% {
transform: rotateY(0deg);
}
}
@keyframes page-4 {
30% {
transform: rotateY(180deg);
opacity: 0;
}
50% {
opacity: 1;
}
65%,
100% {
opacity: 0;
}
80%,
100% {
transform: rotateY(0deg);
}
}
@keyframes page-5 {
45% {
transform: rotateY(180deg);
opacity: 0;
}
65% {
opacity: 1;
}
80%,
100% {
opacity: 0;
}
95%,
100% {
transform: rotateY(0deg);
}
}
</style>
+54
View File
@@ -0,0 +1,54 @@
<script lang="ts" setup>
import { ref, PropType } from 'vue';
import dayjs from 'dayjs';
export type Notice = {
img: string;
title: string;
content: string;
time: number;
};
export type NoticeGroup = {
title: string;
list: Notice[];
};
defineProps({
dataSource: Array as PropType<NoticeGroup[]>,
});
const active = ref(0);
</script>
<template>
<a-tabs class="w-60" v-model:active="active">
<a-tab-pane :key="i" :tab="group.title" v-for="(group, i) in dataSource">
<div class="list max-h-40 px-2 overflow-y-auto overflow-x-hidden">
<div class="not-[:first-child]:mt-3 flex items-center" v-for="item in group.list">
<img class="w-11 rounded-full" :src="item.img" />
<div class="flex flex-col ml-2">
<div class="text-title text-xs font-semibold">
{{ item.title }}
<span class="text-subtext text-xs ml-1 font-normal">
{{ dayjs(item.time).format('hh:mm') }}
</span>
</div>
<div class="text-subtext">{{ item.content }}</div>
</div>
</div>
</div>
</a-tab-pane>
</a-tabs>
</template>
<style lang="less" scoped>
:deep(.ant-tabs) {
&-tab {
@apply flex-1 justify-center;
}
&-nav {
&-list {
@apply w-full;
}
}
}
</style>
@@ -0,0 +1,289 @@
<script lang="ts" setup>
import { ref, computed, Ref } from 'vue';
import type { Component, PropType } from 'vue';
import GridList from '../list/GridList.vue';
import { PaginationProps } from 'ant-design-vue';
import { debounce } from 'lodash';
import useModelValue from '@/utils/useModelValue';
export interface IconSelectOption {
label?: string;
component: string | Component;
value: string | number;
}
export interface IconSelectGroup {
title: string;
key: string | number;
list: IconSelectOption[];
}
type SelectMode = 'multiple' | 'single';
const props = defineProps({
mode: {
type: String as PropType<SelectMode>,
default: 'multiple',
},
value: {
type: [Array<string | number>, String, Number],
default(rawProps: any) {
if (rawProps.mode === 'multiple' || !rawProps.mode) {
return undefined;
} else {
return undefined;
}
},
},
column: {
type: Number,
default: 8,
},
placeholder: {
type: String,
default: '选择图标,输入文字搜索...',
},
options: {
type: [Array<IconSelectOption>, Array<IconSelectGroup>],
default: [],
},
});
const emit = defineEmits<{
(e: 'update:value', args: (string | number)[] | undefined | string | number): void;
}>();
// 分页
const pageBase: PaginationProps = {
pageSize: props.column * 5,
hideOnSinglePage: true,
showSizeChanger: false,
size: 'small',
};
const visible = ref(false);
const isMultiple = computed(() => props.mode === 'multiple');
/**
* 格式化分组
*/
const groupList = computed<(IconSelectGroup & { _searchList: IconSelectOption[]; _current: Ref<number> })[]>(() => {
if (props.options.length === 0 || (props.options as IconSelectGroup[])[0].title !== undefined) {
return (props.options as IconSelectGroup[]).map((group) => ({
...group,
_current: ref(1),
_searchList: group.list,
}));
}
return [
{
title: '全部图标',
key: '__dft',
list: props.options as IconSelectOption[],
_current: ref(1),
_searchList: [...props.options] as IconSelectOption[],
},
];
});
/**
* icon 字典 (方便搜索和查找)
*/
const iconMap = computed(() => {
const map = new Map<string | number, IconSelectOption>();
groupList.value
.flatMap((group) => group.list)
.forEach((item) => {
map.set(item.value, item);
});
return map;
});
const { value: select } = useModelValue(
() =>
Array.isArray(props.value) || props.value === undefined ? (props.value as Array<string | number>) : [props.value],
(val) => emit('update:value', isMultiple.value ? val : val?.[0])
);
/**
* 选中图标
* @param icon
*/
function onSelect(icon: IconSelectOption) {
const index = select.value?.indexOf(icon.value) ?? -1;
if (index === -1) {
select.value = isMultiple.value ? [...(select.value ?? []), icon.value] : [icon.value];
} else if (isMultiple.value) {
remove(icon.value);
}
if (!isMultiple.value) {
visible.value = false;
}
searchValue.value = '';
searchIcon('');
}
/**
* 移除选中
* @param icon
*/
function remove(iconKey: string | number) {
const index = select.value?.findIndex((icon) => icon === iconKey) ?? -1;
if (index >= 0) {
select.value = select.value?.filter((icon) => icon !== iconKey);
}
}
/**
* 搜索图标
* @param keyword
*/
function searchIcon(keyword: string) {
const empty = keyword === '';
const group = groupList.value.find((item) => item.key === active.value)!;
const reg = new RegExp(keyword.toLowerCase());
const filterIcon = (reg: RegExp, list: IconSelectOption[]) => {
return list.filter((icon) => reg.test(icon.label!.toLocaleLowerCase()));
};
group._searchList = empty ? [...group.list] : filterIcon(reg, group.list);
setTimeout(() => {
groupList.value
.filter((item) => item !== group)
.forEach((g) => {
g._searchList = empty ? [...g.list] : filterIcon(reg, g.list);
});
});
loading.value = false;
}
/**
* 搜索防抖
*/
const _search = debounce(searchIcon, 300);
/**
* 搜索监听
* @param value
*/
function onSearch(value: string) {
searchValue.value = value;
loading.value = true;
_search(value);
}
// 当前激活分组
const active = ref(groupList.value[0]?.key);
// 搜索关键字
const searchValue = ref('');
const loading = ref(false);
function selected(icon: IconSelectOption) {
return select.value?.includes(icon.value);
}
</script>
<template>
<a-select
@click="() => (visible = true)"
:showSearch="true"
mode="multiple"
v-model:value="select"
:open="visible"
@blur="() => (visible = false)"
@search="onSearch"
:searchValue="searchValue"
v-bind="{ placeholder }"
allow-clear
>
<template #dropdownRender>
<a-spin tip="搜索中..." :spinning="loading">
<a-tabs
v-model:activeKey="active"
:class="[
'icon-selector',
'px-base',
'pb-base',
{ 'no-group pt-[12px]': groupList.length === 1 && groupList[0].key === '__dft' },
]"
@mousedown.prevent
>
<a-tab-pane :key="group.key" v-for="group in groupList">
<template #tab>
<a-badge
:class="{ 'text-primary-500': active === group.key }"
:count="group._searchList.length === group.list.length ? undefined : group._searchList.length"
showZero
>
{{ group.title }}
</a-badge>
</template>
<GridList
class="icon-container"
:dataSource="group._searchList"
:column="column"
:pagination="{
...pageBase,
current: group._current.value,
'onUpdate:current': (val) => (group._current.value = val),
}"
>
<template #renderItem="{ item }">
<div
@click="onSelect(item)"
:class="`icon-item bg-container cursor-pointer h-10 w-10 flex justify-center items-center ${
selected(item) ? 'bg-primary-100' : ''
}`"
>
<component class="icon transition" :is="item.component" />
</div>
</template>
</GridList>
</a-tab-pane>
</a-tabs>
</a-spin>
</template>
<template #tagRender="item">
<div class="mx-0.5 bg-bg-disabled p-1 rounded-sm flex items-center cursor-pointer">
<component :is="iconMap.get(item.value)?.component" />
<CloseOutlined
v-if="isMultiple"
class="text-subtext text-[10px] ml-1 hover:text-text"
@click="remove(item.value)"
/>
</div>
</template>
</a-select>
</template>
<style lang="less" scoped>
.icon-selector {
:deep(.icon-container) {
@apply p-2 text-xl grid gap-2 bg-layout rounded;
.icon-item {
@apply rounded-sm border border-solid border-transparent;
&:hover {
@apply border-primary-500;
.icon {
@apply scale-125;
}
}
}
}
&.no-group {
:deep(.ant-tabs-nav) {
@apply hidden;
}
}
}
</style>
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { LabelWrapper } from 'stepin';
import { useSettingStore } from '@/store';
const setting = useSettingStore();
</script>
<template>
<div class="setting px-md">
<LabelWrapper justify="between" label="导航模式">
<a-radio-group v-model:value="setting.navigation" button-style="solid">
<a-radio value="side">侧边</a-radio>
<a-radio value="head">顶部</a-radio>
<a-radio style="margin-right: 0" value="mix">混合</a-radio>
</a-radio-group>
</LabelWrapper>
<LabelWrapper justify="between" label="多页签">
<a-switch v-model:checked="setting.useTabs" />
</LabelWrapper>
<LabelWrapper justify="between" label="过滤菜单">
<a-switch v-model:checked="setting.filterMenu" />
</LabelWrapper>
</div>
</template>
<style scoped lang="less"></style>
+2
View File
@@ -0,0 +1,2 @@
import Setting from './Setting.vue';
export default Setting;
@@ -0,0 +1,31 @@
<template>
<div class="mini-statistic-card overflow-hidden relative min-h-[112px] bg-container inline-flex items-center justify-between drop-shadow-sm p-md border-border rounded-lg">
<div class="statistic-main flex-1">
<div class="statistic-title text-subtext text-xs">{{ title }}</div>
<div class="statistic-content flex items-baseline">
<span class="value text-title text-xxl font-bold">{{ value }}</span>
<span class="suffix ml-1 text-xs text-green-500 font-bold">+30%</span>
</div>
</div>
<div class="statistic-icon absolute bottom-0 right-0">
<slot name="icon"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {
title: String,
value: [String, Number] as PropType<string | number>,
},
name: 'MiniStatisticCard',
});
</script>
<style lang="less" scoped>
.mini-statistic-card {
}
</style>
@@ -0,0 +1,41 @@
<template>
<div class="overview-title">
{{ title }}
<div class="subtitle">
{{ subtitle }}
<span :class="{ change: true, up, down }">{{ change }}</span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
export default defineComponent({
name: 'OverviewTitle',
props: {
title: String,
subtitle: String,
change: [String, Number] as PropType<string | number>,
up: { type: Boolean, required: false },
down: { type: Boolean, required: false },
},
setup(props, { attrs, slots, emit }) {},
});
</script>
<style scoped lang="less">
.overview-title {
@apply text-title font-bold text-base;
.subtitle {
@apply text-subtext text-xs;
.change {
@apply text-primary-500 text-sm ml-xs;
&.up {
@apply text-success-500;
}
&.down {
@apply text-error-500;
}
}
}
}
</style>
@@ -0,0 +1,85 @@
<script lang="ts" setup>
import { PropType, watch, computed } from 'vue';
import useModelValue from '@/utils/useModelValue';
import cloneDeep from 'lodash/cloneDeep';
import { storeToRefs } from 'pinia';
import { useThemeStore, ThemeProvider } from 'stepin/es/theme-provider';
export type Type = 'day' | 'night';
const props = defineProps({
value: { type: String as PropType<Type> },
nightColor: { type: String, default: '#1D1D1D' },
});
const emit = defineEmits<{
(e: 'update:value', value: Type): void;
}>();
const { value: _value } = useModelValue(
() => props.value,
(val) => emit('update:value', val),
'day'
);
const switcher: { [key in Type]: Type } = {
day: 'night',
night: 'day',
};
const { theme } = storeToRefs(useThemeStore());
// 监听主题色变换,更新缓存
let cachedMiddleColors = cloneDeep(theme.value.color.middle);
watch(
theme,
(val) => {
if (val.color.middle['bg-base'] !== props.nightColor) {
cachedMiddleColors = cloneDeep(val.color.middle);
_value.value = 'day';
} else {
_value.value = 'night';
}
},
{ deep: true }
);
// 主题颜色配置
const colorCfg = computed(() => {
if (_value.value === 'day') {
return { middle: cachedMiddleColors };
}
return { middle: { 'bg-base': props.nightColor } };
});
</script>
<template>
<ThemeProvider is-root :color="colorCfg">
<div
@click="() => (_value = switcher[_value])"
class="bg-fill-2 day-night-switch hover:border-border relative border-border-2 text-lg rounded-full border border-solid flex items-center"
>
<div :class="`spot transition-[left] duration-300 h-full absolute rounded-full bg-container ${_value}`"></div>
<IconFont :class="`day-night-switch-item ${_value === 'day' ? 'checked' : ''}`" name="icon-sun" />
<IconFont :class="`day-night-switch-item ${_value === 'night' ? 'checked' : ''}`" name="icon-moono" />
</div>
</ThemeProvider>
</template>
<style scoped lang="less">
.day-night-switch {
.spot {
width: calc(50% - 1px);
z-index: 1;
left: 0;
&.night {
left: calc(50% + 1px);
@apply bg-layout;
}
}
&-item {
@apply z-20 bg-transparent p-xxs rounded-full text-disabled ~"last:ml-[2px]";
&.checked {
@apply text-text;
}
}
}
</style>
+28
View File
@@ -0,0 +1,28 @@
import { AlertApi } from 'stepin/es/alert-message';
import { MessageApi } from 'ant-design-vue/es/message';
import IconFont from '@/plugins/iconfont/IconFont.vue';
declare module 'vue' {
export interface ComponentCustomProperties {
$message: MessageApi;
$alert: AlertApi;
}
}
declare module 'vue-router' {
interface RouteMeta {
cacheable?: boolean;
closeable?: boolean;
icon?: DefineComponent | FunctionalComponent | string;
badge?: string | number | boolean;
href?: string;
target?: '_blank' | '_self';
permission?: string;
title?: string;
renderMenu?: boolean;
_cache?: RouteMeta;
view?: string;
_is404Page?: boolean;
}
}
export {};
+26
View File
@@ -0,0 +1,26 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from '@/router';
import stepin from 'stepin/es';
import pinia from '@/store';
// import '@/mock';
// 生产打包时可去除 ant-design-vue/dist/antd.variable.less 的引用。
// 开发引入此包是为了加载优化,防止首次打开页面过慢
import 'ant-design-vue/dist/antd.variable.less';
import 'stepin/es/style';
// import 'default-passive-events';
import '@/theme/index.less';
import { AuthPlugin, IconfontPlugin } from '@/plugins';
const app = createApp(App);
app.use(pinia);
app.use(router);
app.use(stepin, { router });
app.use(AuthPlugin, { action: 'disable' });
// iconfont 插件。url为你的 iconfont 图标资源地址(你的iconfont 仓库可获取此地址)
app.use(IconfontPlugin, { url: '//at.alicdn.com/t/c/font_3805284_ulvha6ct7d.js' });
app.config.errorHandler = function (err) {
console.error('未捕获的异常,', err);
};
app.mount('#stepin-app');
+9
View File
@@ -0,0 +1,9 @@
<template>
<ThemeProvider :color="{ middle: { 'bg-base': '#f9e9f9' } }">
<div ref="demo" class="demo p-8 mb-4">demo</div>
</ThemeProvider>
</template>
<script lang="ts" setup>
import { ThemeProvider } from 'stepin';
</script>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { useRoute } from 'vue-router';
defineProps({
permission: String,
path: String,
});
console.log(useRoute());
</script>
<template>
<div>
<div>403 Forbidden (no permission)</div>
<div>path: {{ path }}</div>
<div>
need permission: <a-tag color="blue">{{ permission }}</a-tag>
</div>
</div>
</template>
+42
View File
@@ -0,0 +1,42 @@
<template>
<div v-if="!loading">
<div>404 Not Found</div>
<div>path: {{ $route.path }}</div>
</div>
<div v-else class="loading flex items-center justify-center">
<ReadingLoader />
</div>
</template>
<script lang="ts" setup>
import ReadingLoader from '@/components/loaders/ReadingLoader.vue';
import { configPage } from 'stepin/es/tabs-view';
import { useRoute, useRouter } from 'vue-router';
import { useMenuStore, storeToRefs } from '@/store';
import { watch } from 'vue';
const props = defineProps({
loading: Boolean,
});
const route = useRoute();
const { loading: _loading } = storeToRefs(useMenuStore());
const router = useRouter();
if (props.loading) {
if (!_loading.value) {
router.push(route.fullPath);
} else {
watch(_loading, () => {
router.push(route.fullPath);
});
}
configPage(route, { title: 'loading' });
configPage(route, { title: undefined });
}
</script>
<style scoped>
.loading {
min-height: calc(100vh - theme(height.header) - 182px);
}
</style>
+107
View File
@@ -0,0 +1,107 @@
<template>
<div class="test-page">
<div>
<a-button v-auth="`personal:edit`" @click="showGuid = true">新手引导1</a-button>
<a-button class="btn1" ref="btn1" type="primary" @click="target = btn2">button 1</a-button>
<a-button class="btn2" ref="btn2" type="primary" @click="target = btn1">button 2</a-button>
<span v-auth="`hello`" @click="onClick" class="ml-40 p-2">test</span>
</div>
<Guider :current="target" :options="options" v-model:show="showGuid">
<div ref="doc" @click="sayHello">功能指引</div>
</Guider>
</div>
</template>
<script lang="ts" setup>
import { ComponentPublicInstance, onMounted, reactive, ref } from 'vue';
import Guider, { GuiderOption } from '@/components/guider';
import { useAuthStore } from '@/plugins';
const authStore = useAuthStore();
authStore.setAuthorities(['personal:edit', 'personal:remove']);
const sayHi = authStore.useAuth('personal:edit', (name: string) => console.log('hi, ' + name));
const onClick = () => console.log('say hi');
sayHi('jack');
const btn1 = ref<ComponentPublicInstance>();
const btn2 = ref<ComponentPublicInstance>();
const doc = ref<HTMLElement>();
let options: GuiderOption[] = [];
const target = ref<ComponentPublicInstance | HTMLElement>();
const showGuid = ref(false);
function sayHello() {
console.log('hello');
}
onMounted(() => {
target.value = btn1.value;
options.push(
{
target: btn1.value,
doc: doc.value,
},
{
target: btn2.value,
doc: doc.value,
}
);
});
</script>
<style lang="less" scoped>
.test-page {
height: calc(100vh);
padding: 24px;
position: relative;
.ant-btn {
position: absolute;
}
.btn1 {
left: 0px;
top: 92px;
}
.btn2 {
top: 92px;
right: 0;
}
.btn3 {
bottom: 0;
right: 0;
}
.btn4 {
bottom: 0;
left: 0px;
}
.btn5 {
left: calc(50% - 85px);
top: calc(50% - 32px);
}
.btn6 {
left: calc(50%);
top: calc(50% - 32px);
}
.btn7 {
left: calc(50% - 85px);
top: calc(50%);
}
.btn8 {
left: calc(50%);
top: calc(50%);
}
}
</style>
+419
View File
@@ -0,0 +1,419 @@
<script lang="ts" setup>
import { getBase64 } from '@/utils/file';
import { FormInstance } from 'ant-design-vue';
import { reactive, ref, onMounted } from 'vue';
import dayjs from 'dayjs';
import { Dayjs } from 'dayjs';
import { EditFilled } from '@ant-design/icons-vue';
import { useApiStore } from '@/store';
import type { UnwrapRef } from 'vue';
const columns = [
{
title: '用户昵称',
dataIndex: 'userName',
},
{ title: '状态', dataIndex: 'status' },
{ title: '收藏文件存储路径', dataIndex: 'savePath' },
{ title: '喜欢文件存储路径', dataIndex: 'favSavePath' },
{ title: 'Cookie', dataIndex: 'cookies' },
{ title: 'SecUserId', dataIndex: 'secUserId' },
{ title: '操作', dataIndex: 'edit', width: 200 },
// { title: 'id', dataIndex: 'id', width: 200, hiden: false },
];
type DataItem = {
id?: string;
userName?: string;
cookies?: string;
savePath?: string;
favSavePath?: string;
secUserId?: string;
status?: number;
_isNew?: boolean;
};
// const dataSource = reactive<DataItem[]>([
// {
// userName: 'Li Zhi',
// cookies: '131231',
// savePath: 'x',
// status: 1,
// id: '1',
// },
// ]);
const loading = ref(false);
const datas: UnwrapRef<DataItem[]> = reactive([]);
const pagination = ref({
current: 1,
defaultPageSize: 10,
total: 0,
showTotal: () => `${0}`,
});
interface QuaryParam {
pageIndex: number;
pageSize: number;
}
const quaryData: UnwrapRef<QuaryParam> = reactive({
pageIndex: 0,
pageSize: 20,
});
const GetRecords = () => {
loading.value = true;
quaryData.pageIndex = pagination.value.current;
quaryData.pageSize = pagination.value.defaultPageSize;
useApiStore()
.CookiePageList(quaryData)
.then((res) => {
loading.value = false;
if (res.code === 0) {
dataSource.value = res.data.data;
pagination.value.current = res.data.pageIndex;
pagination.value.defaultPageSize = res.data.pageSize;
pagination.value.total = res.data.total;
pagination.value.showTotal = () => `${res.data.total}`;
}
});
};
onMounted(() => {
GetRecords();
});
function addNew() {
showModal.value = true;
form._isNew = true;
}
const showModal = ref(false);
const newAuthor = (author?: DataItem) => {
if (!author) {
author = { _isNew: true };
}
author.userName = undefined;
author.cookies = undefined;
author.savePath = undefined;
author.favSavePath = undefined;
author.secUserId = undefined;
author.status = 0;
author.id = '0';
return author;
};
const copyObject = (target: any, source?: any) => {
if (!source) {
return target;
}
Object.keys(target).forEach((key) => (target[key] = source[key]));
};
const form = reactive<DataItem>(newAuthor());
function reset() {
return newAuthor(form);
}
function cancel() {
showModal.value = false;
reset();
}
const formModel = ref<FormInstance>();
const formLoading = ref(false);
function submit() {
formLoading.value = true;
let self = this;
formModel.value
?.validateFields()
.then((resData: DataItem) => {
if (form._isNew) {
// authors.push({ ...res });
} else {
copyObject(editRecord.value, resData);
}
// console.log('1', authors);
// console.log('2', res);
useApiStore()
.UpdateConfig(resData)
.then((res) => {
loading.value = false;
if (res.code === 0) {
showModal.value = false;
reset();
GetRecords();
}
});
})
.catch((e) => {
console.error(e);
})
.finally(() => {
formLoading.value = false;
});
}
const editRecord = ref<DataItem>();
import { Modal } from 'ant-design-vue'; // 假设使用Ant Design Vue的Modal组件
const deleted = (id: string) => {
// 显示确认对话框
Modal.confirm({
title: '确认删除',
content: '确定要删除这条记录吗?此操作不可撤销。',
okText: '确认',
cancelText: '取消',
onOk: () => {
// 用户确认后执行删除操作
useApiStore()
.deleteCookie(id)
.then((res) => {
loading.value = false;
if (res.code === 0) {
showModal.value = false;
reset();
GetRecords();
}
});
},
onCancel: () => {
// 用户取消删除,不执行任何操作
console.log('已取消删除');
},
});
};
/**
* 编辑
* @param record
*/
function edit(record: DataItem) {
editRecord.value = record;
copyObject(form, record);
showModal.value = true;
}
type Status = 0 | 1;
const StatusDict = {
0: '关闭',
1: '开启',
};
const dataSource = ref(datas);
const showCookiesModal = ref(false);
let showCookiesData = '';
const showCookies = (recode: DataItem) => {
showCookiesModal.value = true;
showCookiesData = recode.cookies;
};
</script>
<template>
<a-modal :title="form._isNew ? '新增Cookie' : '编辑Cookie'" v-model:visible="showModal" @ok="submit" @cancel="cancel" width="1000px">
<a-form ref="formModel" :model="form" :labelCol="{ span: 3 }" :wrapperCol="{ span: 20 }">
<a-form-item label="用户名" required name="userName">
<a-input v-model:value="form.userName" />
</a-form-item>
<a-form-item label="id" required name="id" v-show="false">
<a-input v-model:value="form.id" />
</a-form-item>
<a-form-item required label="收藏存储路径" name="savePath">
<a-input v-model:value="form.savePath" />
</a-form-item>
<a-form-item required label="Cookie" name="cookies">
<a-textarea v-model:value="form.cookies" rows='13' />
</a-form-item>
<a-form-item label="喜欢存储路径" name="favSavePath">
<a-input v-model:value="form.favSavePath" />
<a-alert message="如果要同步“我喜欢的”视频,需要填写!!!" type="warning" />
</a-form-item>
<a-form-item label="SecUserId" name="secUserId">
<a-input v-model:value="form.secUserId" />
<a-alert message="如果要同步“我喜欢的”视频,需要填写!!!" type="warning" />
</a-form-item>
<a-form-item required label="状态" name="status">
<a-select style="width: 90px" v-model:value="form.status" :options="[
{ label: '关闭', value: 0 },
{ label: '开启', value: 1 },
]" />
</a-form-item>
</a-form>
</a-modal>
<!-- 成员表格 -->
<a-table v-bind="$attrs" :columns="columns" :dataSource="dataSource" :pagination="false">
<template #title>
<!-- 关键修改 justify-between 改为 justify-end使子元素靠右侧对齐 -->
<div class="flex justify-end pr-4">
<a-button type="primary" @click="GetRecords()" :loading="formLoading" class="mr-2">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button type="primary" @click="addNew" :loading="formLoading">
<template #icon>
<PlusOutlined />
</template>
新增
</a-button>
</div>
</template>
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'status'">
<a-badge class="text-subtext" :color="'green'">
<template #text>
<span class="text-subtext">{{ StatusDict[text as Status] }}</span>
</template>
</a-badge>
</template>
<template v-else-if="column.dataIndex === 'cookies'">
<!-- 触发按钮 -->
<a-button @click="showCookies(record)">查看</a-button>
</template>
<template v-else-if="column.dataIndex === 'edit'">
<a-button :disabled="showModal" type="link" @click="edit(record)">
<template #icon>
<EditFilled />
</template>
编辑
</a-button>
<a-button type="link" @click="deleted(record.id)" danger>
<template #icon>
<DeleteFilled />
</template>
删除
</a-button>
</template>
<div v-else class="text-subtext">
{{ text }}
</div>
</template>
</a-table>
<!-- Modal弹窗 -->
<a-modal title="Cookie 详情" :visible="showCookiesModal" style="width:1200px;" @cancel="showCookiesModal = false" @ok="showCookiesModal = false">
<!-- 弹窗内容 -->
<div class="cookie-content">
{{ showCookiesData || '无Cookie数据' }}
</div>
</a-modal>
</template>
<style scoped>
.cookie-content {
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
}
/* 透明滚动条样式 - WebKit内核浏览器 */
.cookie-content::-webkit-scrollbar {
width: 6px; /* 更细的滚动条 */
}
/* 轨道完全透明 */
.cookie-content::-webkit-scrollbar-track {
background: transparent;
}
/* 滑块半透明(默认几乎看不见) */
.cookie-content::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.2); /* 浅灰透明 */
border-radius: 3px;
}
/* hover时稍微显示一点 */
.cookie-content::-webkit-scrollbar-thumb:hover {
background: rgba(150, 150, 150, 0.4); /* 略深一点的透明 */
}
/* 角落也透明 */
.cookie-content::-webkit-scrollbar-corner {
background: transparent;
}
/* Firefox 透明滚动条适配 */
.cookie-content {
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
}
.cookie-content {
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
padding: 10px;
box-sizing: border-box;
}
.cookie-content::-webkit-scrollbar {
width: 6px;
}
.cookie-content::-webkit-scrollbar-track {
background: transparent;
}
.cookie-content::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.2);
border-radius: 3px;
}
.cookie-content::-webkit-scrollbar-thumb:hover {
background: rgba(150, 150, 150, 0.4);
}
.cookie-content::-webkit-scrollbar-corner {
background: transparent;
}
.cookie-content {
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
}
/* ---------------------- 新增:a-textarea 透明滚动条 ---------------------- */
/* 1. 穿透 scoped,定位 a-textarea 内部的原生 textarea 元素 */
:deep(.ant-input-textarea-input) {
/* 确保内容超出时显示滚动条(a-textarea 默认已配置,可省略) */
overflow-y: auto;
/* Firefox 透明滚动条:thin 细滚动条 + 滑块颜色/轨道颜色 */
scrollbar-width: thin;
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
}
/* 2. WebKit 浏览器(Chrome/Safari/Edge)透明滚动条 */
/* 滚动条宽度 */
:deep(.ant-input-textarea-input)::-webkit-scrollbar {
width: 6px; /* 与 cookie-content 保持一致的细滚动条 */
height: 6px; /* 横向滚动条(如需) */
}
/* 滚动条轨道(完全透明) */
:deep(.ant-input-textarea-input)::-webkit-scrollbar-track {
background: transparent;
}
/* 滚动条滑块(半透明,hover 时加深) */
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb {
background: rgba(150, 150, 150, 0.2); /* 浅灰透明,默认几乎看不见 */
border-radius: 3px; /* 圆角优化 */
}
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb:hover {
background: rgba(150, 150, 150, 0.4); /* hover 时略深,提升交互感知 */
}
/* 滚动条角落(完全透明,避免留白) */
:deep(.ant-input-textarea-input)::-webkit-scrollbar-corner {
background: transparent;
}
</style>
+8
View File
@@ -0,0 +1,8 @@
<script lang="ts" setup>
import CookieTable from './CookieTable.vue';
</script>
<template>
<div class="table w-full">
<CookieTable />
</div>
</template>
+2
View File
@@ -0,0 +1,2 @@
import Table from './Table.vue';
export default Table;
+17
View File
@@ -0,0 +1,17 @@
import { Component, DefineComponent } from 'vue';
type LazyComponent = () => Promise<Component | DefineComponent>;
const modules: Record<string, LazyComponent> = import.meta.glob('./**/*.{ts,vue,tsx}');
type DynamicModule = { [key: string]: LazyComponent };
const Pages = Object.entries(modules).reduce((r, [key, _module]) => {
key = key.replace(/^.\//, '@/pages/');
r[key] = _module;
if (/\/index.(js|ts|tsx|vue)/.test(key)) {
r[key.replace(/\/index.(js|ts|tsx|vue)/, '')] = _module;
}
return r;
}, {} as DynamicModule);
export default Pages;
+129
View File
@@ -0,0 +1,129 @@
<template>
<a-card :bordered="false" :bodyStyle='{"padding-top":"0px","padding-bottom":"100px"}'>
<a-form :model="formState" :label-col="labelCol" :rules="rules" :wrapper-col="wrapperCol" ref="formRef">
<a-divider orientation="left"></a-divider>
<a-form-item has-feedback label="同步周期(分钟)" ref="Cron" name="Cron">
<a-input v-model:value="formState.Cron" placeholder="1:数字-例如20-表示20分钟执行一次;2:cron表达式,根据表达式周期执行" />
</a-form-item>
<a-form-item label="在线Cron表达式">
<a target="_blank" href="https://www.bejson.com/othertools/cron/">查看示例</a>
</a-form-item>
<a-form-item :wrapper-col="{ span: 10, offset: 3 }">
<a-button type="primary" danger @click="onSubmit">进入系统</a-button>
</a-form-item>
</a-form>
</a-card>
</template>
<script lang="ts" setup>
import { reactive, toRaw, ref, watch, createVNode, h } from 'vue';
import type { UnwrapRef } from 'vue';
import { Form } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import type { FormInstance } from 'ant-design-vue';
import { useApiStore } from '@/store';
import { Modal } from 'ant-design-vue';
import { checkDomain, checksubDomainPrefix, checkPass, checkUserName } from '@/utils/regexHelper';
const formRef = ref<FormInstance>();
const SK = ref(null);
interface FormState {
cloudName: string;
domainName: string;
recordType: string;
ipv6Prefix: string;
domainRecord: string;
AK: string;
SK: string;
DbType: number;
ConnString: string;
Cron: string;
UserName: string;
UswePwd: string;
}
interface CloudInfo {
key: string;
value: string;
doc: string;
}
interface DbTypeInfo {
key: number;
value: string;
conn: string;
}
interface showDBconn {
value: boolean;
}
const showDBconn: UnwrapRef<showDBconn> = reactive({
value: false,
});
const formState: UnwrapRef<FormState> = reactive({
cloudName: 'aliyun',
domainName: '',
recordType: '',
ipv6Prefix: '',
domainRecord: '',
AK: '',
SK: '',
DbType: 2,
ConnString: '',
Cron: '30',
UserName: '',
UswePwd: '',
});
const rules: Record<string, Rule[]> = {
Cron: [{ required: true, message: '请输入任务调度周期', trigger: 'change' }],
};
watch(
() => formState.DbType,
() => {
formRef.value.validateFields(['ConnString']);
},
{ flush: 'post' }
);
//检查数据库连接
import { message } from 'ant-design-vue';
import router from '@/router';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
const isFirst = ref(false);
//提交初始化
const onSubmit = () => {
// console.log('submit!', toRaw(formState));
formRef.value
.validate()
.then(() => {
console.log('values', formState, toRaw(formState));
useApiStore()
.apiInit(toRaw(formState))
.then((res) => {
if (res.code === 0) {
message.success('初始化成功');
setTimeout(() => {
router.push('/login');
}, 1000);
} else {
message.error(res.erro, 8);
}
});
})
.catch((error) => {
console.log('error', error);
});
};
const labelCol = { style: { width: '150px' } };
const wrapperCol = { span: 4 };
</script>
<style scoped>
.ant-card-body {
padding: 5px !important;
}
</style>
+3
View File
@@ -0,0 +1,3 @@
import Init from './Init.vue';
export default Init;
+56
View File
@@ -0,0 +1,56 @@
<template>
<div class="login flex items-center justify-center">
<login-box class="shadow-lg" @success="onLoginSuccess" @failure="onLoginFail" />
</div>
</template>
<script lang="ts" setup>
import LoginBox from './LoginBox.vue';
import { useRouter } from 'vue-router';
// import http from '@/store/http';
const router = useRouter();
function onLoginSuccess() {
router.push('/dashboard');
}
import { message } from 'ant-design-vue';
function onLoginFail(status, res) {
console.log(res);
if (res.code === 0) {
} else {
message.error(res.erro, 5);
}
}
</script>
<style scoped lang="less">
.login {
height: 100vh;
// 与深色登录框搭配的渐变背景
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
// 可以添加一些装饰性背景元素
&::before {
content: '';
position: absolute;
width: 400px;
height: 400px;
border-radius: 50%;
background: rgba(54, 191, 250, 0.1);
top: 20%;
left: 15%;
filter: blur(80px);
}
&::after {
content: '';
position: absolute;
width: 300px;
height: 300px;
border-radius: 50%;
background: rgba(54, 191, 250, 0.08);
bottom: 10%;
right: 10%;
filter: blur(60px);
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<ThemeProvider :color="{ middle: { 'bg-base': '#1a1a1a' }, primary: { DEFAULT: '#36bffA' } }">
<!-- 加宽后的登录框使用更大的宽度设置 -->
<div class="login-box rounded-lg bg-gray-900 shadow-lg p-8 max-w-2xl mx-auto my-10 border border-gray-800 transition-all duration-300 hover:shadow-xl">
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full p-lg text-gray-200">
<div class="third-platform">
<div class="third-title mb-6 text-xl text-center font-semibold text-white">抖音同步工具</div>
</div>
<a-divider class="my-6 bg-gray-700"></a-divider>
<!-- 增加输入框宽度并调整间距 -->
<a-form-item :required="true" name="username" class="mb-5">
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[45px] rounded-md bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 focus:border-primary text-lg" />
</a-form-item>
<a-form-item :required="true" name="password" class="mb-6">
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[45px] rounded-md bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 focus:border-primary text-lg" type="password" />
</a-form-item>
<a-button htmlType="submit" class="h-[48px] w-full rounded-md transition-colors hover:opacity-90 bg-primary border-primary text-lg" type="primary" :loading="loading">
登录
</a-button>
</a-form>
</div>
</ThemeProvider>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useAccountStore } from '@/store';
import { ThemeProvider } from 'stepin';
export interface LoginFormProps {
username: string;
password: string;
}
const loading = ref(false);
const form = reactive({
username: undefined,
password: undefined,
});
const emit = defineEmits<{
(e: 'success', fields: LoginFormProps): void;
(e: 'failure', reason: string, fields: LoginFormProps): void;
}>();
const accountStore = useAccountStore();
function login(params: LoginFormProps) {
loading.value = true;
accountStore
.login(params.username, params.password)
.then((res) => {
emit('success', params);
})
.catch((e) => {
emit('failure', e.message, e.data);
})
.finally(() => (loading.value = false));
}
</script>
+50
View File
@@ -0,0 +1,50 @@
<template>
<a-modal
width="460px"
v-model:visible="_visible"
wrap-class-name="login-modal"
:closable="false"
:footer="null"
:body-style="{ padding: 0 }"
>
<login-box />
</a-modal>
</template>
<script lang="ts" setup>
import LoginBox from './LoginBox.vue';
import useModelValue from '@/utils/useModelValue';
import { useAccountStore } from '@/store';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
const props = defineProps({
visible: { type: Boolean, default: undefined },
unless: Array<String>,
});
const accountStore = useAccountStore();
const route = useRoute();
const emit = defineEmits<{
(e: 'update:visible', visible?: boolean): void;
}>();
const _visible = computed({
get(): boolean {
return !!sVisible.value && !props.unless?.includes(route.fullPath);
},
set(val: boolean) {
sVisible.value = val;
},
});
const { value: sVisible } = useModelValue(
() => props.visible ?? !accountStore.logged,
(val) => emit('update:visible', val)
);
</script>
<style lang="less">
.login-modal .ant-modal-content {
@apply bg-transparent;
}
</style>
+3
View File
@@ -0,0 +1,3 @@
import Login from './Login.vue';
export { default as LoginModal } from './LoginModal.vue';
export default Login;
+79
View File
@@ -0,0 +1,79 @@
<template>
<a-form layout="inline" style="margin-top:5px;margin-bottom:5px;">
<a-form-item>
<a-date-picker v-model:value="dateValue" format="YYYYMMDD" :locale="locale" @change="datePickChange" />
</a-form-item>
<a-form-item>
<a-radio-group v-model:value="typeValue" button-style="solid" @change="typeChange">
<a-radio-button value="debug">debug</a-radio-button>
<a-radio-button value="error">error</a-radio-button>
</a-radio-group>
</a-form-item>
</a-form>
<div class="container">
<a-card title="" :bordered="true">
<pre>{{ logs }}</pre>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { defineComponent, reactive, ref, watch, onMounted } from 'vue';
import { useApiStore } from '@/store';
import type { UnwrapRef } from 'vue';
import dayjs, { Dayjs } from 'dayjs';
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
type RangeValue = [Dayjs, Dayjs];
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
const dateValue = ref<Dayjs>(dayjs(Date()));
const typeValue = ref<string>('debug');
const iframeUrl = ref<string>('');
const dateValue1 = ref<string>();
const logs = ref<string>('');
dateValue1.value = dayjs(Date()).format('YYYYMMDD');
iframeUrl.value = `type=${typeValue.value}&date=${dateValue1.value}`;
const datePickChange = (e, dateStr) => {
dateValue1.value = dateStr;
iframeUrl.value = `type=${typeValue.value}&date=${dateValue1.value}`;
console.log(iframeUrl);
loadLogs();
};
const typeChange = (e) => {
console.log(e.target);
iframeUrl.value = `type=${e.target.value}&date=${dateValue1.value}`;
loadLogs();
};
const mIfrm = ref<any>(null);
onMounted(() => {
// console.log(mIfrm.value);
loadLogs();
});
const loadLogs = () => {
useApiStore()
.apiGetLogs(iframeUrl.value)
.then((log) => {
// console.log(log);
const lines = log.split('\n'); // 将文本按换行符分割为行数组
const reversedLines = lines.reverse(); // 对行数组进行倒序操作
const reversedText = reversedLines.join('\n'); // 将行数组重新连接为一个字符串
logs.value = reversedText;
});
};
</script>
<style lang='less' scoped>
html {
height: 100vh;
}
.container {
width: 100%;
height: 100%;
// max-height: 400px;
iframe {
.word-wrap {
color: white !important;
}
}
}
</style>
+3
View File
@@ -0,0 +1,3 @@
import Logs from './MyLogs.vue';
export default Logs;
+173
View File
@@ -0,0 +1,173 @@
<template>
<a-drawer v-model:visible="visible" class="custom-class" title="邮件配置" placement="right" @after-visible-change="afterVisibleChange" :maskClosable="true">
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1">
<template #tab>
<span>
<!-- <BellOutlined /> -->
邮件STMP配置
</span>
</template>
<a-form ref="emailFromRef" :model="EmailFormData" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finishFailed="onFinishFailed">
<a-form-item label="是否开启" ref="Open">
<a-checkbox v-model:checked="EmailFormData.Open">解析结果邮件通知</a-checkbox>
</a-form-item>
<a-form-item v-if="EmailFormData.Open" label="Stmp地址" ref="Stmp" name="Stmp" :rules="[{ required: EmailFormData.Open, message: '请输入stmp服务地址!',validator:validateStmp }]">
<a-input v-model:value="EmailFormData.Stmp" />
</a-form-item>
<a-form-item v-if="EmailFormData.Open" label="Stmp端口" ref="Port" name="Port" :rules="[{ required: EmailFormData.Open, message: '请输入stmp服务端口!',validator:validatePort }]">
<a-input v-model:value="EmailFormData.Port" placeholder="默认465yeah-587" />
</a-form-item>
<a-form-item v-if="EmailFormData.Open" label="收件Email" type='email' ref="From" name="From" :rules="[{ required: EmailFormData.Open, message: '请输入收件Email地址!',validator:validateEmail }]">
<a-input v-model:value="EmailFormData.From" />
<span style="color:#888">此处收件人亦是发件人</span>
</a-form-item>
<a-form-item v-if="EmailFormData.Open" label="Stmp授权码" ref="Code" name="Code" :rules="[{ required: EmailFormData.Open, message: '请输入Stmp授权码!' }]">
<a-input v-model:value="EmailFormData.Code" />
<span style="color:#888">邮箱设置开启stmp服务会提示</span>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">确认</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-drawer>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { defineComponent, ref, onMounted, reactive, watch } from 'vue';
import type { FormInstance } from 'ant-design-vue';
import { useApiStore, useAccountStore } from '@/store';
import http from '@/store/http';
import type { Rule } from 'ant-design-vue/es/form';
import { checkEmail, checkDomain, checkPort } from '@/utils/regexHelper';
const activeKey = ref<string>('1');
const visible = ref<boolean>(false);
function showEmail(vis: boolean) {
visible.value = vis;
if (vis) loadStmpInfo();
}
const emailFromRef = ref<FormInstance>();
const afterVisibleChange = (bool: boolean) => {
if (!bool) {
emailFromRef.value.resetFields();
}
};
const loadStmpInfo = () => {
// useApiStore()
// .apiGetStmpConfig()
// .then((res) => {
// if (res.code === 0) {
// EmailFormData.Open = res.data.open;
// EmailFormData.Stmp = res.data.stmp;
// EmailFormData.Port = res.data.port;
// EmailFormData.Code = res.data.code;
// EmailFormData.From = res.data.from;
// EmailFormData.Id = res.data.id;
// }
// });
};
interface EmailFormState {
Open: boolean;
Stmp: string;
Port: number;
Code: string;
From: string;
Id: string;
To: string;
}
const EmailFormData = reactive<EmailFormState>({
Open: false,
Stmp: 'smtp.qq.com',
Port: 465,
Code: '',
From: '',
Id: '',
To: '',
});
const onFinish = (values: EmailFormState) => {
EmailFormData.To = EmailFormData.From;
// useApiStore()
// .apiSaveStmpConfig(EmailFormData)
// .then((res) => {
// console.log(res);
// if (res.code === 0) {
// message.success('修改配置成功');
// visible.value = false;
// } else {
// message.error(res.erro, 8);
// }
// });
};
//验证邮箱
const validateEmail = async (_rule: Rule, value: string) => {
if (checkEmail(value)) {
return Promise.resolve();
} else {
return Promise.reject('请输入正确的邮箱地址');
}
};
//验证stmp服务
const validateStmp = async (_rule: Rule, value: string) => {
if (checkDomain(value)) {
return Promise.resolve();
} else {
return Promise.reject('请输入正确的stmp-server地址');
}
};
//验证端口
const validatePort = async (_rule: Rule, value: string) => {
if (checkPort(value)) {
return Promise.resolve();
} else {
return Promise.reject('请输入正确的端口');
}
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
watch(
() => EmailFormData.Open,
() => {
emailFromRef.value.validateFields(['Stmp', 'Port', 'From', 'Code']);
},
{ flush: 'post' }
);
// 将updateMessage方法暴露给父组件调用
defineExpose({
showEmail,
});
</script>
<style>
.avatar-uploader > .ant-upload {
width: 100px;
height: 100px;
}
.ant-upload-picture-card-wrapper {
height: 100%;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
margin-top: 50px !important;
}
.ant-upload-select-picture-card {
margin-top: 50px !important;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
.ant-tabs-content {
text-align: center;
}
</style>
+206
View File
@@ -0,0 +1,206 @@
<template>
<a-drawer v-model:visible="visible" class="custom-class" title="个人设置" placement="right" @after-visible-change="afterVisibleChange" :maskClosable="true">
<a-tabs v-model:activeKey="activeKey">
<!-- <a-tab-pane key="1" style="height:100%">
<template #tab>
<span>
<user-outlined />
修改头像
</span>
</template>
<a-upload style="" v-model:file-list="fileList" name="file" list-type="picture-card" class="avatar-uploader" :show-upload-list="false" :action="uploadAction" :before-upload="beforeUpload" @change="handleChange" accept=".jpg, .jpeg, .png">
<img v-if="imageUrl" :src="imageUrl" alt="avatar" style="height: 112px; width: 112px; border-radius: 50%;" />
<div v-else>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">选择图片</div>
</div>
</a-upload>
<div style="margin-top:20px;">
上传成功即修改成功
</div>
</a-tab-pane> -->
<a-tab-pane key="2">
<template #tab>
<span>
<safety-outlined />
修改用户信息
</span>
</template>
<a-form ref="passFromRef" :model="passwordFormData" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finishFailed="onFinishFailed" validateTrigger="blur">
<a-form-item label="原密码" ref="OldPassword" name="OldPassword" :rules="[{ required: true, message: '请输入原密码!' }]">
<a-input-password v-model:value="passwordFormData.OldPassword" />
</a-form-item>
<a-form-item label="新密码" ref="Password" name="Password" :rules="[{ required: true, message: '请输入正确新密码!密码要求6位以上包含大小写与数字' ,validator:checkPassword}]">
<a-input-password v-model:value="passwordFormData.Password" />
</a-form-item>
<a-form-item label="确认密码" ref="ConfirmPassword" name="ConfirmPassword" :rules="[{ required: true, message: '请输入正确新密码!',validator:checkPassword}]">
<a-input-password v-model:value="passwordFormData.ConfirmPassword" />
</a-form-item>
<a-form-item label="修改账户" ref="UserName" name="UserName" :rules="[{ required: true, message: '请输入新账号!' }]">
<a-input v-model:value="passwordFormData.UserName" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">确认</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-drawer>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { defineComponent, ref, onMounted, reactive } from 'vue';
import type { UploadChangeParam, UploadProps, FormInstance } from 'ant-design-vue';
import { useApiStore, useAccountStore } from '@/store';
import http from '@/store/http';
import { checkPass } from '@/utils/regexHelper';
import type { Rule } from 'ant-design-vue/es/form';
const activeKey = ref<string>('2');
const uploadAction = ref<string>('');
const visible = ref<boolean>(false);
function show(vis: boolean) {
visible.value = vis;
if (vis) loadUserInfo();
}
const passFromRef = ref<FormInstance>();
const afterVisibleChange = (bool: boolean) => {
if (!bool) {
if (passFromRef != null && passFromRef.value != null) passFromRef.value.resetFields();
}
};
function getBase64(img: Blob, callback: (base64Url: string) => void) {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result as string));
reader.readAsDataURL(img);
}
const fileList = ref([]);
const loading = ref<boolean>(false);
const imageUrl = ref<string>('');
const handleChange = (info: UploadChangeParam) => {
if (info.file.status === 'uploading') {
loading.value = true;
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, (base64Url: string) => {
imageUrl.value = base64Url;
loading.value = false;
});
}
if (info.file.status === 'error') {
loading.value = false;
message.error('upload error');
}
};
const beforeUpload = (file: UploadProps['fileList'][number]) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg';
if (!isJpgOrPng) {
message.error('仅允许上传jpg|png|jpeg格式');
}
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('最大允许上传5M的文件!');
}
return isJpgOrPng && isLt10M;
};
// const userInfo = ref<string>();
const loadUserInfo = () => {
useApiStore()
.apiUserInfo()
.then((res) => {
if (res.code === 0) {
if (res.data.avatar != null && res.data.avatar !== '') imageUrl.value = `/upload/${res.data.avatar}`;
passwordFormData.UserId = res.data.id;
uploadAction.value = `/api/auth/UpdateUserAvatar?Uid=${res.data.id}`;
}
});
};
interface PassFormState {
UserId: string;
OldPassword: string;
Password: string;
ConfirmPassword: string;
UserName: string;
}
const onFinish = (values: PassFormState) => {
if (values.Password !== values.ConfirmPassword) {
message.error('新密码与确认密码不一致');
return;
} else {
useApiStore()
.apiChangePwd(passwordFormData)
.then((res) => {
console.log(res);
if (res.code === 0) {
message.success('修改密码成功,请重新登陆!');
useAccountStore().setLogged(false);
visible.value = false;
http.removeAuthorization();
} else {
message.error(res.erro, 8);
}
});
}
};
const checkPassword = async (_rule: Rule, value: string) => {
if (checkPass(value)) {
return Promise.resolve();
} else {
return Promise.reject('密码要求6位以上包含大小写与数字');
}
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const passwordFormData = reactive<PassFormState>({
UserId: '',
OldPassword: '',
Password: '',
ConfirmPassword: '',
UserName: '',
});
// 将updateMessage方法暴露给父组件调用
defineExpose({
show,
});
</script>
<style>
.avatar-uploader > .ant-upload {
width: 100px;
height: 100px;
}
.ant-upload-picture-card-wrapper {
height: 100%;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
margin-top: 50px !important;
}
.ant-upload-select-picture-card {
margin-top: 50px !important;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
.ant-tabs-content {
text-align: center;
}
</style>
+4
View File
@@ -0,0 +1,4 @@
import MyPersonal from './MyPersonal.vue';
import EmailSet from './EmailSet.vue';
export { MyPersonal, EmailSet };

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