init
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/**
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
obj
|
||||||
|
bin
|
||||||
|
.vs
|
||||||
|
logs
|
||||||
|
upload
|
||||||
|
data1
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"presets": ["es2015"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_BASE_URL=/
|
||||||
|
VITE_API_URL=http://localhost:10101
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_BASE_URL=/
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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 版本)
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</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>
|
||||||
@@ -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 版本)
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</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>
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "ESNext",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [require('tailwindcss'), require('autoprefixer')],
|
||||||
|
};
|
||||||
@@ -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 |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -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 |
@@ -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>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 58 KiB |
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Guider from './Guider.vue';
|
||||||
|
export type { GuideTarget, GuiderOption } from './interface';
|
||||||
|
|
||||||
|
export default Guider;
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
<router-view></router-view>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup></script>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div>link view</div>
|
||||||
|
</template>
|
||||||
@@ -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>
|
||||||
@@ -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';
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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 {};
|
||||||
@@ -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');
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import CookieTable from './CookieTable.vue';
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="table w-full">
|
||||||
|
<CookieTable />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import Table from './Table.vue';
|
||||||
|
export default Table;
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Init from './Init.vue';
|
||||||
|
|
||||||
|
export default Init;
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Login from './Login.vue';
|
||||||
|
export { default as LoginModal } from './LoginModal.vue';
|
||||||
|
export default Login;
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Logs from './MyLogs.vue';
|
||||||
|
|
||||||
|
export default Logs;
|
||||||
@@ -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="默认465,yeah-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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import MyPersonal from './MyPersonal.vue';
|
||||||
|
import EmailSet from './EmailSet.vue';
|
||||||
|
|
||||||
|
export { MyPersonal, EmailSet };
|
||||||