Compare commits
240 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4375d22d5 | |||
| 956b5d614e | |||
| b8d1fcae5a | |||
| a67843bc8f | |||
| 93945ce25d | |||
| d0ac3d98b0 | |||
| 3759f264a5 | |||
| 93abc85cff | |||
| d1f411ccf7 | |||
| a7741b9de2 | |||
| 1055b6f01f | |||
| 9038c39afa | |||
| 145c6b48f3 | |||
| 1899f90b5c | |||
| a0133fe02d | |||
| 45d3a3de4d | |||
| cf40a7e89a | |||
| e6c5587637 | |||
| 30060fe29c | |||
| c5922f9b08 | |||
| e83ad8d85c | |||
| cc96e03cb9 | |||
| 522fc6f699 | |||
| c88afc2fd0 | |||
| e48bada605 | |||
| fdd19cd22b | |||
| 83e185a1f8 | |||
| 5e89b9c66f | |||
| 432ab60dd0 | |||
| 8e8d256e9a | |||
| 467e8cbad5 | |||
| 66b3f2f4db | |||
| d76c1cad87 | |||
| 2a4ce563b0 | |||
| 0848ebc272 | |||
| a21f745eac | |||
| dd22d46f26 | |||
| 4a7aeeed8c | |||
| b1bf488e84 | |||
| a67093405a | |||
| 01073fce52 | |||
| fc29ee21ea | |||
| 63f44adeca | |||
| 8f738f1714 | |||
| 77dbb8048e | |||
| 2aef300ab0 | |||
| 36d301d32b | |||
| 3a47b1e028 | |||
| 0900d2d754 | |||
| c87bfcd3a7 | |||
| 1fa67daab1 | |||
| 21e1aca7b5 | |||
| dbf98c781d | |||
| e82e3a9140 | |||
| 5c57a2b040 | |||
| cd24804273 | |||
| 9784114c90 | |||
| 650e1ac3fc | |||
| 3439a7ad38 | |||
| 8b98abc94d | |||
| c1d5db18a9 | |||
| 46bec7a0d4 | |||
| 77f227a6c5 | |||
| a3e6af672e | |||
| 8fd06cf2a0 | |||
| 3578962a4c | |||
| 8af041cf91 | |||
| edbcc13086 | |||
| 85986cdaa3 | |||
| f13d73e023 | |||
| 66093d3ed0 | |||
| b63621593b | |||
| e92e290a4e | |||
| 3f6b1874eb | |||
| 60f6e3847d | |||
| c609eb4e6f | |||
| 36f11d4d79 | |||
| bdd70f73e4 | |||
| 8390511675 | |||
| e011bd089f | |||
| b331e73bd3 | |||
| 357b20d211 | |||
| b53a41d3f1 | |||
| a8c3201c1d | |||
| eb0769d595 | |||
| 33888aabdd | |||
| e270397334 | |||
| 7175b3858a | |||
| 7c6ee0a8a6 | |||
| 65e6b48d8e | |||
| f5449f3ace | |||
| d9cb721f64 | |||
| dc38a25239 | |||
| 41a21e33fd | |||
| f376498fe5 | |||
| ff80250012 | |||
| 8da82889d1 | |||
| 1f96f31c69 | |||
| 186179bf14 | |||
| 0727999cd6 | |||
| e32d783de4 | |||
| 858a21ec9e | |||
| 12b9190f45 | |||
| 70c3dc71e5 | |||
| f4f87a8634 | |||
| 00e28fc334 | |||
| a943a13347 | |||
| 46b6a0269c | |||
| 3d9a5fddfb | |||
| aa5d4878cb | |||
| 1006de4a0d | |||
| 4e0169da90 | |||
| b866cab3d8 | |||
| 0d35e4665c | |||
| e34a1d3666 | |||
| 00b9ec3013 | |||
| e8852868cd | |||
| 66718ff478 | |||
| 0532ef1006 | |||
| 5a907e24f7 | |||
| 87f154a8b7 | |||
| b9b3731bbe | |||
| 214d1259f4 | |||
| d02a784c1b | |||
| 66bd441378 | |||
| 96a66a9f54 | |||
| 727dba6e08 | |||
| 3db406f699 | |||
| 220c3ff140 | |||
| 59b454023d | |||
| 8ae0506d96 | |||
| e717672e1e | |||
| c3e9e9c17d | |||
| 9e5e51fea8 | |||
| cc6906ea63 | |||
| 44e3962093 | |||
| 413f19c537 | |||
| 7666ab109d | |||
| 0549688e6f | |||
| 086a0ea5af | |||
| 20e399b2e2 | |||
| 9391ace8c7 | |||
| 6c34e7e163 | |||
| 936e4a8ca1 | |||
| c99a195ad4 | |||
| 6fd92a6662 | |||
| ad9769c07c | |||
| 1574a7ceeb | |||
| 34910bb121 | |||
| d0c4322907 | |||
| fc7886a76e | |||
| 1a9af56e05 | |||
| 90a1115c2e | |||
| 5c05e6465b | |||
| 2550a1fde8 | |||
| 772859eb63 | |||
| dba6bcdb36 | |||
| 26380b9b4d | |||
| 7b5d535f6b | |||
| 41e2bf6ce0 | |||
| 123124bb51 | |||
| 72497c5fae | |||
| c60f7bdf57 | |||
| 91056d9cfa | |||
| b58a091b53 | |||
| a36c815861 | |||
| c03dc5beff | |||
| 385e53561a | |||
| 18d1c4100d | |||
| f5c4459ad9 | |||
| 4df896b30f | |||
| ceb08a5136 | |||
| edd0e71f8b | |||
| 29d34201bd | |||
| 25e2a22dd2 | |||
| bd3a103f8f | |||
| 4663a03483 | |||
| 4b2f9aaa7c | |||
| de5813a874 | |||
| 6c99825548 | |||
| 4e561a8e94 | |||
| 2fedbafca8 | |||
| b599880d7c | |||
| 072314fc52 | |||
| 4804a82967 | |||
| 13dfe09d93 | |||
| b6b915b0c5 | |||
| d947393b1d | |||
| 830b6769f9 | |||
| c09b30de63 | |||
| 8744d280c7 | |||
| 77628c9833 | |||
| 89d311901d | |||
| 635d1c003b | |||
| 50149a280b | |||
| e42a004765 | |||
| d4b3e36591 | |||
| ea8f8948f8 | |||
| 3de4b60005 | |||
| e456982334 | |||
| e6e9ff3cc3 | |||
| 63b51d211e | |||
| 3093f2c78a | |||
| e59f3031da | |||
| 02200a4595 | |||
| f501b218ca | |||
| 6bedb30602 | |||
| 22a65b9833 | |||
| 4ff41dbaa0 | |||
| 12532f8128 | |||
| a9bb8f5f48 | |||
| ac51f7a50f | |||
| eecc0be509 | |||
| 639a457b17 | |||
| dd2b8da08a | |||
| def3c442a5 | |||
| 6bde9b75ff | |||
| 5b41e5eaeb | |||
| 975baf80cd | |||
| b52acfdc62 | |||
| 898e4c115b | |||
| 00aa82e763 | |||
| 2db61f4958 | |||
| 609649057f | |||
| 116aa0c3e0 | |||
| ef5cb09cb1 | |||
| 225e1614d7 | |||
| 578131ac55 | |||
| 309a0c1350 | |||
| bd616e9b4f | |||
| eadb3bb20d | |||
| 0fd7938901 | |||
| b4cee4f358 | |||
| db008b2c45 | |||
| 73d612fe50 | |||
| e5f22e45c3 | |||
| 85d74c7de3 | |||
| 8cef13c926 | |||
| 7cacdaedd4 | |||
| 8048f7e1e6 |
@@ -1,67 +1,15 @@
|
|||||||
# Git
|
**/node_modules
|
||||||
.git
|
**/.git
|
||||||
.gitignore
|
logs
|
||||||
.gitattributes
|
videos
|
||||||
|
db
|
||||||
# IDE
|
bin
|
||||||
.vs
|
obj
|
||||||
.vscode
|
.claude
|
||||||
|
.playwright-mcp
|
||||||
.idea
|
.idea
|
||||||
*.swp
|
.codepilot-uploads
|
||||||
*.swo
|
*.tar.gz
|
||||||
*~
|
|
||||||
|
|
||||||
# Build outputs
|
|
||||||
bin/
|
|
||||||
obj/
|
|
||||||
app/dist/
|
|
||||||
app/node_modules/
|
|
||||||
app/.vite/
|
|
||||||
|
|
||||||
# Logs and temp
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
*.tmp
|
|
||||||
|
|
||||||
# Local databases (mounted via volume in prod)
|
|
||||||
db/*.sqlite
|
|
||||||
db/*.db
|
|
||||||
|
|
||||||
# Video storage (mounted via volume)
|
|
||||||
videos/
|
|
||||||
collect/
|
|
||||||
favorite/
|
|
||||||
uper/
|
|
||||||
|
|
||||||
# Docs (Chinese filenames covered by *.md)
|
|
||||||
*.md
|
|
||||||
docs/
|
|
||||||
README*.md
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
test/
|
|
||||||
tests/
|
|
||||||
*.test.*
|
|
||||||
*.spec.*
|
|
||||||
|
|
||||||
# Env
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
._*
|
**/.DS_Store
|
||||||
|
smoke-*.png
|
||||||
# Windows
|
|
||||||
Thumbs.db
|
|
||||||
ehthumbs.db
|
|
||||||
Desktop.ini
|
|
||||||
|
|
||||||
# Linux
|
|
||||||
.directory
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
*.bak
|
|
||||||
*.backup
|
|
||||||
*.old
|
|
||||||
|
|||||||
@@ -10,17 +10,27 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: 安装 Docker CLI
|
- name: 安装 Docker CLI
|
||||||
run: |
|
run: |
|
||||||
# 替换 Debian 源为清华镜像,避免国内连 deb.debian.org 超时
|
if command -v docker >/dev/null 2>&1; then
|
||||||
sed -i 's|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null || true
|
echo "Docker 已预装:$(docker --version)"
|
||||||
sed -i 's|security.debian.org|mirrors.tuna.tsinghua.edu.cn/debian-security|g' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null || true
|
exit 0
|
||||||
apt-get update -qq
|
fi
|
||||||
apt-get install -y -qq docker.io
|
echo "Docker 未预装,换 apt 源到阿里云再安装(gitea act_runner 实际是 Debian 11)"
|
||||||
|
# Debian 源(当前 runner 实际用的)
|
||||||
|
sed -i 's|deb.debian.org|mirrors.aliyun.com|g;s|security.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
|
||||||
|
sed -i 's|deb.debian.org|mirrors.aliyun.com|g;s|security.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/*.list 2>/dev/null || true
|
||||||
|
# Ubuntu 源兼容(万一以后 runner 镜像换成 Ubuntu)
|
||||||
|
sed -i 's|archive.ubuntu.com|mirrors.aliyun.com|g;s|security.ubuntu.com|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
|
||||||
|
sed -i 's|archive.ubuntu.com|mirrors.aliyun.com|g;s|security.ubuntu.com|mirrors.aliyun.com|g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true
|
||||||
|
timeout 90 apt-get update -qq || { echo "apt-get update 超时/失败"; exit 1; }
|
||||||
|
timeout 240 apt-get install -y -qq docker.io || { echo "docker.io 安装超时/失败"; exit 1; }
|
||||||
|
docker --version
|
||||||
|
|
||||||
- name: 拉取代码
|
- name: 拉取代码
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 构建镜像
|
- name: 构建镜像
|
||||||
run: docker build -f Dockerfile.prod -t dysync:latest .
|
# --no-cache:绕开 buildx 旧 layer 缓存(曾出现 CI 用几天前缓存的 statics.vue 导致 vue-tsc 报孤儿引用错误)
|
||||||
|
run: docker build --no-cache --pull -f Dockerfile.prod -t dysync:latest .
|
||||||
|
|
||||||
- name: 停止旧容器
|
- name: 停止旧容器
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -18,13 +18,6 @@ videos/
|
|||||||
|
|
||||||
# SQLite 数据库及日志(仅忽略实例数据,不忽略迁移脚本)
|
# SQLite 数据库及日志(仅忽略实例数据,不忽略迁移脚本)
|
||||||
/db/*.sqlite
|
/db/*.sqlite
|
||||||
/db/*.db-journal
|
/appsettings.json
|
||||||
/db/*.db-wal
|
/Properties/PublishProfiles/fn.pubxml.user
|
||||||
/db/*.db-shm
|
/Properties/PublishProfiles/*.user
|
||||||
|
|
||||||
# 飞牛 NAS 相关(当前只部署群晖,不入库)
|
|
||||||
*feiniu*
|
|
||||||
|
|
||||||
# 手动部署脚本(与 Gitea CI 流程重复,本地保留即可,不入库以免与 CI 冲突)
|
|
||||||
/deploy.sh
|
|
||||||
/docker-compose.yml
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration.Json;
|
||||||
using Microsoft.Extensions.Configuration.Json;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace dy.net
|
namespace dy.net
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
using ClockSnowFlake;
|
using ClockSnowFlake;
|
||||||
|
using dy.net.model.dto;
|
||||||
|
using dy.net.service;
|
||||||
|
using dy.net.utils;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using dy.net.service;
|
|
||||||
using dy.net.utils;
|
|
||||||
using dy.net.dto;
|
|
||||||
|
|
||||||
namespace dy.net.Controllers
|
namespace dy.net.Controllers
|
||||||
{
|
{
|
||||||
@@ -17,10 +17,10 @@ namespace dy.net.Controllers
|
|||||||
{
|
{
|
||||||
private readonly IWebHostEnvironment webHostEnvironment;
|
private readonly IWebHostEnvironment webHostEnvironment;
|
||||||
|
|
||||||
private readonly UserService _userService;
|
private readonly AdminUserService _userService;
|
||||||
public AuthController(UserService userService, IWebHostEnvironment webHostEnvironment )
|
public AuthController(AdminUserService userService, IWebHostEnvironment webHostEnvironment)
|
||||||
{
|
{
|
||||||
_userService=userService;
|
_userService = userService;
|
||||||
this.webHostEnvironment = webHostEnvironment;
|
this.webHostEnvironment = webHostEnvironment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,66 +35,21 @@ namespace dy.net.Controllers
|
|||||||
public async Task<IActionResult> UpdatePwd(UpdatePwdRequest user)
|
public async Task<IActionResult> UpdatePwd(UpdatePwdRequest user)
|
||||||
{
|
{
|
||||||
var (code, erro) = await _userService.UpdatePwd(user);
|
var (code, erro) = await _userService.UpdatePwd(user);
|
||||||
return Ok(new { code, erro });
|
return ApiResult.Success("", erro);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
[Authorize]
|
/// 获取头像
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetUserAvatar()
|
public async Task<IActionResult> GetUserAvatar()
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUser();
|
var user = await _userService.GetUser();
|
||||||
return Ok(new { code = 0, error = "", data = new { user?.Avatar, user?.Id,user?.UserName } });
|
return ApiResult.Success(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 = "空文件" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +63,7 @@ namespace dy.net.Controllers
|
|||||||
{
|
{
|
||||||
if (loginUserInfo == null)
|
if (loginUserInfo == null)
|
||||||
{
|
{
|
||||||
return Ok(new { code = -1, erro = "参数不能为空" });
|
return ApiResult.Fail("参数不能为空");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -116,7 +71,7 @@ namespace dy.net.Controllers
|
|||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return Ok(new { code = -1, erro = "用户名或密码不正确" });
|
return ApiResult.Fail("用户名或密码不正确");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -124,30 +79,29 @@ namespace dy.net.Controllers
|
|||||||
{
|
{
|
||||||
|
|
||||||
var tokenString = GenerateJwtToken(user.UserName);
|
var tokenString = GenerateJwtToken(user.UserName);
|
||||||
|
return Ok(new { code = 0, erro = "", token = tokenString, expires = 24 * 60 * 60 * 1000, data = user.UserName });
|
||||||
return Ok(new { code = 0, erro = "", token = tokenString, expires = 24 * 60 * 60 * 1000 });
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return Ok(new { code = -1, erro = "用户名或密码不正确" });
|
return ApiResult.Fail("用户名或密码不正确");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private string GenerateJwtToken(string username)
|
private static string GenerateJwtToken(string username)
|
||||||
{
|
{
|
||||||
var claims = new[]
|
var claims = new[]
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Name, username),
|
new Claim(ClaimTypes.Name, username),
|
||||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||||
};
|
};
|
||||||
var k = Md5Util.JWT_TOKEN_KEY;
|
var k = Md5Util.JWT_TOKEN_KEY;
|
||||||
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(k));
|
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(k));
|
||||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
var expires = DateTime.Now.AddDays(1);
|
var expires = DateTime.Now.AddDays(7);
|
||||||
|
|
||||||
var token = new JwtSecurityToken(
|
var token = new JwtSecurityToken(
|
||||||
issuer: IdGener.GetLong().ToString(),
|
issuer: IdGener.GetLong().ToString(),
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using dy.net.model.dto;
|
||||||
|
using dy.net.service;
|
||||||
|
using dy.net.utils;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace dy.net.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class CateController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly DouyinCollectCateService _douyinCollectCateService;
|
||||||
|
private readonly DouyinQuartzJobService _douyinQuartzJobService;
|
||||||
|
|
||||||
|
public CateController(DouyinCollectCateService douyinCollectCateService, DouyinQuartzJobService douyinQuartzJobService)
|
||||||
|
{
|
||||||
|
this._douyinCollectCateService = douyinCollectCateService;
|
||||||
|
_douyinQuartzJobService = douyinQuartzJobService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>分页结果</returns>
|
||||||
|
[HttpPost("paged")]
|
||||||
|
public async Task<IActionResult> GetPagedAsync(
|
||||||
|
DouyinCollectCateRequestDto dto)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dto.cookieId))
|
||||||
|
{
|
||||||
|
return ApiResult.Success(new { });
|
||||||
|
}
|
||||||
|
var (list, totalCount) = await _douyinCollectCateService.GetPagedAsync(dto);
|
||||||
|
|
||||||
|
return ApiResult.Success(new
|
||||||
|
{
|
||||||
|
data = list,
|
||||||
|
total = totalCount,
|
||||||
|
pageIndex = dto.PageIndex,
|
||||||
|
pageSize = dto.PageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改关注同步状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("BatchSave")]
|
||||||
|
public async Task<IActionResult> BatchSave(List<DouyinCollectCateSwitchDto> dto)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (dto.Any(x => !DouyinFileNameHelper.IsValidWithoutSpecialChars(x.SaveFolder)))
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("有部分文件名不符合要求,请检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _douyinCollectCateService.BatchSwitchSync(dto);
|
||||||
|
return ApiResult.SuccOrFail(result, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,108 +1,306 @@
|
|||||||
using ClockSnowFlake;
|
using ClockSnowFlake;
|
||||||
using dy.net.dto;
|
using dy.net.extension;
|
||||||
using dy.net.model;
|
using dy.net.model.dto;
|
||||||
|
using dy.net.model.entity;
|
||||||
using dy.net.service;
|
using dy.net.service;
|
||||||
|
using dy.net.utils;
|
||||||
|
using jzc.http.lib;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using static Dm.net.buffer.ByteArrayBuffer;
|
using System.Net;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace dy.net.Controllers
|
namespace dy.net.Controllers
|
||||||
{
|
{
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class ConfigController : ControllerBase
|
public class ConfigController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly DyCookieService dyCookieService;
|
private readonly DouyinCookieService dyCookieService;
|
||||||
|
|
||||||
private readonly CommonService commonService;
|
private readonly DouyinCommonService commonService;
|
||||||
private readonly QuartzJobService quartzJobService;
|
private readonly DouyinQuartzJobService quartzJobService;
|
||||||
|
private readonly DouyinFollowService douyinFollowService;
|
||||||
|
private readonly DouyinCookieService douyinCookieService;
|
||||||
|
private readonly DouyinHttpClientService httpClientService;
|
||||||
|
|
||||||
public ConfigController(DyCookieService dyCookieService, CommonService commonService,QuartzJobService quartzJobService)
|
public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService, DouyinQuartzJobService quartzJobService, DouyinFollowService douyinFollowService, DouyinCookieService douyinCookieService, DouyinHttpClientService httpClientService)
|
||||||
{
|
{
|
||||||
this.dyCookieService = dyCookieService;
|
this.dyCookieService = dyCookieService;
|
||||||
this.commonService = commonService;
|
this.commonService = commonService;
|
||||||
this.quartzJobService = quartzJobService;
|
this.quartzJobService = quartzJobService;
|
||||||
|
this.douyinFollowService = douyinFollowService;
|
||||||
|
this.douyinCookieService = douyinCookieService;
|
||||||
|
this.httpClientService = httpClientService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出配置
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("exportConf")]
|
||||||
|
public async Task<IActionResult> ExportConf()
|
||||||
|
{
|
||||||
|
|
||||||
|
// 1. 组装导出数据
|
||||||
|
var dto = new AppConfigImportDto()
|
||||||
|
{
|
||||||
|
follows = await douyinFollowService.GetHandFollows(),
|
||||||
|
conf = commonService.GetConfig(),
|
||||||
|
cookies = await douyinCookieService.GetAllAsync()
|
||||||
|
};
|
||||||
|
|
||||||
|
return ApiResult.Success(dto);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost("importConf")]
|
||||||
|
public async Task<IActionResult> ImportConf(AppConfigImportDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null)
|
||||||
|
return ApiResult.Fail("json数据为空");
|
||||||
|
|
||||||
|
await HandleFollowsImport(dto.follows);
|
||||||
|
await HandleConfigImport(dto.conf);
|
||||||
|
await HandleCookiesImport(dto.cookies);
|
||||||
|
|
||||||
|
return ApiResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleFollowsImport(List<DouyinFollowed> follows)
|
||||||
|
{
|
||||||
|
if (follows?.Count > 0)
|
||||||
|
{
|
||||||
|
var added = await douyinFollowService.AddHandFollows(follows);
|
||||||
|
if (added)
|
||||||
|
Serilog.Log.Debug("关注列表导入成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleConfigImport(AppConfig conf)
|
||||||
|
{
|
||||||
|
if (conf == null) return;
|
||||||
|
|
||||||
|
if (conf.BatchCount > 30)
|
||||||
|
{
|
||||||
|
Serilog.Log.Debug("对不起,为了项目能长久稳定运行,还是最大不要超过30吧。。。");
|
||||||
|
conf.BatchCount = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updated = await commonService.UpdateConfig(conf);
|
||||||
|
if (updated)
|
||||||
|
Serilog.Log.Debug("系统配置导入成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleCookiesImport(List<DouyinCookie> cookies)
|
||||||
|
{
|
||||||
|
if (cookies?.Count > 0)
|
||||||
|
{
|
||||||
|
var imported = await douyinCookieService.ImportCookies(cookies);
|
||||||
|
if (imported)
|
||||||
|
Serilog.Log.Debug("抖音Cookie配置导入成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询
|
/// 分页查询
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>分页结果(视频列表和总数)</returns>
|
/// <returns>分页结果</returns>
|
||||||
[HttpPost("paged")]
|
[HttpPost("paged")]
|
||||||
public async Task<IActionResult> GetPagedAsync(
|
public async Task<IActionResult> GetPagedAsync(
|
||||||
PageRequestDto dto)
|
PageRequestDto dto)
|
||||||
{
|
{
|
||||||
var (list, totalCount) = await dyCookieService.GetPagedAsync(dto.PageIndex, dto.PageSize);
|
var (list, totalCount) = await dyCookieService.GetPagedAsync(dto.PageIndex, dto.PageSize);
|
||||||
return Ok(new
|
|
||||||
|
return ApiResult.Success(new
|
||||||
{
|
{
|
||||||
code = 0,
|
data = list,
|
||||||
data = new
|
total = totalCount,
|
||||||
{
|
pageIndex = dto.PageIndex,
|
||||||
data = list,
|
pageSize = dto.PageSize
|
||||||
total=totalCount,
|
});
|
||||||
pageIndex= dto.PageIndex,
|
|
||||||
pageSize= dto.PageSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// 新增用户Cookie
|
|
||||||
/// </summary>
|
|
||||||
[HttpPost("add")]
|
|
||||||
public async Task<IActionResult> AddAsync([FromBody] DyUserCookies dyUserCookies)
|
|
||||||
{
|
|
||||||
if(dyUserCookies.UpSecUserIdsJson!=null)
|
|
||||||
{
|
|
||||||
dyUserCookies.UpSecUserIds = JsonConvert.SerializeObject( dyUserCookies.UpSecUserIdsJson);
|
|
||||||
}
|
|
||||||
var result = await dyCookieService.Add(dyUserCookies);
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
await ReStartJob();
|
|
||||||
return Ok(new { code = 0 });
|
|
||||||
}
|
|
||||||
return BadRequest(new { code=-1, message = "添加失败" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询所有用户Cookie
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("list")]
|
||||||
|
public async Task<IActionResult> GetAllList()
|
||||||
|
{
|
||||||
|
var follows = await douyinFollowService.GetGroupByCookieAsync();
|
||||||
|
return ApiResult.Success(follows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否已经初始化了
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("isInit")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> IsInit()
|
||||||
|
{
|
||||||
|
var init = await dyCookieService.IsInit();
|
||||||
|
return ApiResult.Success(init);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配合工具自动设置cookie
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("FastResetCookie")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> FastResetCookie([FromBody] DouyinCookieResetDto dto)
|
||||||
|
{
|
||||||
|
var cookieValid = await httpClientService.CheckCookie(new DouyinCookie { Cookies = dto.cookie });
|
||||||
|
if (!cookieValid)
|
||||||
|
return ApiResult.Fail("Cookie无效或已过期,请按照文档提示重新获取有效Cookie,不要使用插件获取cookie");
|
||||||
|
var result = await dyCookieService.FastResetCookie(dto.id, dto.cookie);
|
||||||
|
|
||||||
|
return result ? ApiResult.Success() : ApiResult.Fail("cookie设置失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配合工具自动设置cookie,获取当前设置的所有Cookie
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("Cookies")]
|
||||||
|
public async Task<IActionResult> GetAllCookies()
|
||||||
|
{
|
||||||
|
var cookies = await dyCookieService.GetAllAsync();
|
||||||
|
if (cookies != null)
|
||||||
|
return ApiResult.Success(cookies.Select(x => new { id= x.Id,name= x.UserName, status=x.StatusMsg }));
|
||||||
|
return ApiResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非docker初始化
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("deskinit")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> DeskInitAsync([FromBody] DouyinCookie dyUserCookies)
|
||||||
|
{
|
||||||
|
// 1. 基础赋值
|
||||||
|
dyUserCookies.Id = IdGener.GetLong().ToString();
|
||||||
|
|
||||||
|
// 2. 路径权限校验
|
||||||
|
var (Success, Message) = ValidatePaths(dyUserCookies);
|
||||||
|
if (!Success)
|
||||||
|
return ApiResult.Fail(Message);
|
||||||
|
|
||||||
|
RemoveCookieLineString(dyUserCookies);
|
||||||
|
|
||||||
|
// 3. Cookie 有效性校验
|
||||||
|
var cookieValid = await httpClientService.CheckCookie(dyUserCookies);
|
||||||
|
if (!cookieValid)
|
||||||
|
return ApiResult.Fail("Cookie无效或已过期,请按照文档提示重新获取有效Cookie,不要使用插件获取cookie");
|
||||||
|
|
||||||
|
dyUserCookies.StatusCode = 0;
|
||||||
|
dyUserCookies.StatusMsg = "正常";
|
||||||
|
// 4. 保存到数据库
|
||||||
|
var saved = await dyCookieService.Add(dyUserCookies);
|
||||||
|
return saved ? ApiResult.Success() : ApiResult.Fail("添加失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (bool Success, string Message) ValidatePaths(DouyinCookie cookie)
|
||||||
|
{
|
||||||
|
var pathsToCheck = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "收藏存储路径", cookie.SavePath },
|
||||||
|
{ "喜欢视频存储路径", cookie.FavSavePath },
|
||||||
|
{ "上传视频存储路径", cookie.UpSavePath },
|
||||||
|
// { "图片存储路径", cookie.ImgSavePath } // 可随时启用
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (label, path) in pathsToCheck)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
if (!DouyinFileUtils.HasDirectoryReadWritePermission(path))
|
||||||
|
{
|
||||||
|
return (false, $"请在飞牛应用设置里面将 {path} 添加读写权限({label})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(cookie.SavePath))
|
||||||
|
{
|
||||||
|
return (false, "收藏存储路径不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速开启或停止
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("switch")]
|
||||||
|
public async Task<IActionResult> SwitchAsync([FromBody] DouyinCookieSwitchDto dto)
|
||||||
|
{
|
||||||
|
var result = await dyCookieService.Switch(dto);
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
ReStartJob();
|
||||||
|
return ApiResult.Success();
|
||||||
|
}
|
||||||
|
return ApiResult.Fail("添加失败");
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 更新用户Cookie
|
/// 更新用户Cookie
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("update")]
|
[HttpPost("update")]
|
||||||
public async Task<IActionResult> UpdateAsync([FromBody] DyUserCookies dyUserCookies)
|
public async Task<IActionResult> AddOrUpdateAsync([FromBody] DouyinCookie dyUserCookies)
|
||||||
{
|
{
|
||||||
|
RemoveCookieLineString(dyUserCookies);
|
||||||
|
|
||||||
if (dyUserCookies.UpSecUserIdsJson != null )
|
var checkCk = await httpClientService.CheckCookie(dyUserCookies);
|
||||||
|
if (!checkCk)
|
||||||
{
|
{
|
||||||
dyUserCookies.UpSecUserIds = JsonConvert.SerializeObject(dyUserCookies.UpSecUserIdsJson);
|
return ApiResult.Fail("Cookie无效或已过期,请按照文档提示重新获取有效Cookie,不要使用插件获取cookie");
|
||||||
}
|
}
|
||||||
|
dyUserCookies.StatusCode = 0;
|
||||||
|
dyUserCookies.StatusMsg = "正常";
|
||||||
if (dyUserCookies.Id == "0")
|
if (dyUserCookies.Id == "0")
|
||||||
{
|
{
|
||||||
dyUserCookies.Id=IdGener.GetLong().ToString();
|
dyUserCookies.Id = IdGener.GetLong().ToString();
|
||||||
var result = await dyCookieService.Add(dyUserCookies);
|
var result = await dyCookieService.Add(dyUserCookies);
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
await ReStartJob();
|
ReStartJob();
|
||||||
return Ok(new { code = 0 });
|
return ApiResult.Success();
|
||||||
}
|
}
|
||||||
return BadRequest(new { code = -1, message = "添加失败" });
|
return ApiResult.Fail("添加失败");
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
var result = await dyCookieService.UpdateAsync(dyUserCookies);
|
{
|
||||||
|
var result = await dyCookieService.UpdateCookieAsync(dyUserCookies);
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
await ReStartJob();
|
ReStartJob();
|
||||||
return Ok(new { code = 0 });
|
return ApiResult.Success();
|
||||||
}
|
}
|
||||||
return BadRequest(new { code = -1, message = "更新失败" });
|
return ApiResult.Fail("更新失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveCookieLineString(DouyinCookie dyUserCookies)
|
||||||
|
{
|
||||||
|
if (dyUserCookies != null && !string.IsNullOrWhiteSpace(dyUserCookies.Cookies))
|
||||||
|
{
|
||||||
|
var s = dyUserCookies.Cookies.Replace("\\r\\n", "\r\n");
|
||||||
|
var ss = s.Trim(new char[] { '\r', '\n' });
|
||||||
|
dyUserCookies.Cookies = ss;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -111,28 +309,36 @@ namespace dy.net.Controllers
|
|||||||
[HttpGet("delete")]
|
[HttpGet("delete")]
|
||||||
public async Task<IActionResult> DeleteAsync(string id)
|
public async Task<IActionResult> DeleteAsync(string id)
|
||||||
{
|
{
|
||||||
var count = await dyCookieService.DeleteByIdsAsync(new List<string> { id});
|
var count = await dyCookieService.DeleteByIdsAsync(new List<string> { id });
|
||||||
if (count > 0) {
|
if (count > 0)
|
||||||
await ReStartJob();
|
{
|
||||||
|
ReStartJob();
|
||||||
}
|
}
|
||||||
return Ok(new { code = 0, deletedCount = count });
|
return ApiResult.Success(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("GetConfig")]
|
[HttpGet("GetConfig")]
|
||||||
public IActionResult GetConfig()
|
public IActionResult GetConfig()
|
||||||
{
|
{
|
||||||
var data = commonService.GetConfig();
|
var data = commonService.GetConfig();
|
||||||
return Ok(new { code = 0, data = data });
|
return ApiResult.Success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("UpdateConfig")]
|
[HttpPost("UpdateConfig")]
|
||||||
public async Task<IActionResult> UpdateConfig(AppConfig config)
|
public async Task<IActionResult> UpdateConfig(AppConfig config)
|
||||||
{
|
{
|
||||||
var data = await commonService.UpdateConfig(config);
|
var update = await commonService.UpdateConfig(config);
|
||||||
if (data) {
|
if (update)
|
||||||
await ReStartJob();
|
{
|
||||||
|
if (config.OnlySyncNew)
|
||||||
|
{
|
||||||
|
Serilog.Log.Debug("仅同步新视频配置已生效,后续所有类型的视频同步将只会读取最近一页约20条数据");
|
||||||
|
}
|
||||||
|
|
||||||
|
ReStartJob();
|
||||||
}
|
}
|
||||||
return Ok(new { code = 0, data = data });
|
return ApiResult.Success(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -140,12 +346,13 @@ namespace dy.net.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("ExecuteJobNow")]
|
[HttpGet("ExecuteJobNow")]
|
||||||
[Authorize]
|
//[Authorize]
|
||||||
public async Task<IActionResult> ExecuteJobNow()
|
public async Task<IActionResult> ExecuteJobNow()
|
||||||
{
|
{
|
||||||
var config = commonService.GetConfig();
|
var config = commonService.GetConfig();
|
||||||
await quartzJobService.StartJob(config.Cron);
|
if (config != null)
|
||||||
return Ok(new { code = 0 , error = "" });
|
await quartzJobService.InitOrReStartAllJobs(config.Cron.ToString());
|
||||||
|
return ApiResult.Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -198,22 +405,132 @@ namespace dy.net.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task ReStartJob() {
|
private void ReStartJob()
|
||||||
var config= commonService.GetConfig();
|
{
|
||||||
|
var config = commonService.GetConfig();
|
||||||
//var cookies = await dyCookieService.GetAllCookies();
|
if (config != null)
|
||||||
//重置同步状态
|
quartzJobService.InitOrReStartAllJobs(config.Cron.ToString());
|
||||||
//foreach (var cookie in cookies)
|
|
||||||
//{
|
|
||||||
// cookie.CollHasSyncd = 0;
|
|
||||||
// cookie.FavHasSyncd = 0;
|
|
||||||
// cookie.UperSyncd = 0;
|
|
||||||
// await dyCookieService.UpdateAsync(cookie);
|
|
||||||
//}
|
|
||||||
|
|
||||||
if (config!=null)
|
|
||||||
quartzJobService.StartJob(config.Cron);
|
|
||||||
//避免前端等待
|
//避免前端等待
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 镜像标签
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("mytag")]
|
||||||
|
public async Task<IActionResult> GetMyTag()
|
||||||
|
{
|
||||||
|
var deploy = Appsettings.Get("deploy");
|
||||||
|
var tag = Appsettings.Get("tagName");
|
||||||
|
tag = deploy == "fn" ? "fn_" + Appsettings.Get("fnVersion") : tag;
|
||||||
|
deploy = deploy == "fn" ? "fnos" : "docker";
|
||||||
|
return ApiResult.Success(new { tag, deploy });
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("checktag")]
|
||||||
|
public async Task<IActionResult> CheckTag()
|
||||||
|
{
|
||||||
|
|
||||||
|
var deploy = Appsettings.Get("deploy");
|
||||||
|
if (string.IsNullOrWhiteSpace(deploy))
|
||||||
|
{
|
||||||
|
return await GetDockerTagVersions();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (deploy == "fn")//飞牛
|
||||||
|
{
|
||||||
|
return ApiResult.Success(new List<string> { "fn_" + Appsettings.Get("fnVersion") });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return await GetDockerTagVersions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IActionResult> GetDockerTagVersions()
|
||||||
|
{
|
||||||
|
var data = await JianZhiChuHttpHelper.GetTenImage(Appsettings.Get("tagName"));
|
||||||
|
if (data.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var content = await data.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
var tagData = JsonConvert.DeserializeObject<DouyinApiResponse<List<string>>>(content);
|
||||||
|
if (tagData != null && tagData.Data != null && tagData.Data.Count > 0)
|
||||||
|
return ApiResult.Success(tagData.Data);
|
||||||
|
return ApiResult.Fail();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("请求失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询mp3目录下有没有音频文件
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("mp3List")]
|
||||||
|
public async Task<IActionResult> GetExistMps()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(AppContext.BaseDirectory, "mp3");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(ServiceExtension.FnDataFolder))
|
||||||
|
{
|
||||||
|
path = ServiceExtension.FnDataFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
var allowedExtensions = new HashSet<string> { ".mp3", ".wav" };
|
||||||
|
|
||||||
|
var customMusics = Directory.GetFiles(path)
|
||||||
|
.Where(filePath =>
|
||||||
|
allowedExtensions.Contains(Path.GetExtension(filePath).ToLowerInvariant()) &&
|
||||||
|
Path.GetFileNameWithoutExtension(filePath) != "silent_10")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var fileNames = customMusics.Select(f => new { filename = Path.GetFileName(f) }).Where(x => x.filename != "silent_10.mp3").ToList();
|
||||||
|
return ApiResult.Success(fileNames);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("没有找到默认音频文件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 播放音频流
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("getmp3")]
|
||||||
|
public async Task<IActionResult> GetMp3([FromQuery] string name)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(AppContext.BaseDirectory, "mp3", name);
|
||||||
|
if (!string.IsNullOrWhiteSpace(ServiceExtension.FnDataFolder))
|
||||||
|
{
|
||||||
|
path = Path.Combine(ServiceExtension.FnDataFolder, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(path))
|
||||||
|
{
|
||||||
|
//返回mp3文件流
|
||||||
|
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
|
return new FileStreamResult(fileStream, "audio/mpeg")
|
||||||
|
{
|
||||||
|
FileDownloadName = name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("文件不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using dy.net.model.dto;
|
||||||
|
using dy.net.model.entity;
|
||||||
|
using dy.net.service;
|
||||||
|
using dy.net.utils;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace dy.net.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class FollowController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly DouyinFollowService _douyinFollowService;
|
||||||
|
private readonly DouyinQuartzJobService _douyinQuartzJobService;
|
||||||
|
|
||||||
|
public FollowController(DouyinFollowService douyinFollowService, DouyinQuartzJobService douyinQuartzJobService)
|
||||||
|
{
|
||||||
|
this._douyinFollowService = douyinFollowService;
|
||||||
|
_douyinQuartzJobService = douyinQuartzJobService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>分页结果</returns>
|
||||||
|
[HttpPost("paged")]
|
||||||
|
public async Task<IActionResult> GetPagedAsync(
|
||||||
|
FollowRequestDto dto)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dto.MySelfId))
|
||||||
|
{
|
||||||
|
return ApiResult.Success(new { });
|
||||||
|
}
|
||||||
|
var (list, totalCount) = await _douyinFollowService.GetPagedAsync(dto);
|
||||||
|
|
||||||
|
return ApiResult.Success(new
|
||||||
|
{
|
||||||
|
data = list,
|
||||||
|
total = totalCount,
|
||||||
|
pageIndex = dto.PageIndex,
|
||||||
|
pageSize = dto.PageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 重新同步-单次
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("sync")]
|
||||||
|
public async Task<IActionResult> SyncFollowList()
|
||||||
|
{
|
||||||
|
//后台异步
|
||||||
|
_douyinQuartzJobService.StartFollowJobOnceAsync();
|
||||||
|
await Task.Delay(1000);
|
||||||
|
return ApiResult.Success();
|
||||||
|
}
|
||||||
|
[HttpPost("add")]
|
||||||
|
public async Task<IActionResult> AddFollow(DouyinFollowed followed)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(followed.mySelfId))
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("请先配置抖音授权信息,抖音授权配置里面填写你的uid");
|
||||||
|
}
|
||||||
|
var res = await _douyinFollowService.AddAsync(followed);
|
||||||
|
return ApiResult.SuccOrFail(res, "", res ? "" : "添加失败,或者已存在相同secuid和uid");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改关注同步状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("openOrCloseSync")]
|
||||||
|
public async Task<IActionResult> OpenOrCloseSync(FollowUpdateDto dto)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (dto.OpenSync)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(dto.SavePath))
|
||||||
|
{
|
||||||
|
if (!DouyinFileNameHelper.IsValidWithoutSpecialChars(dto.SavePath))
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("请输入有效文件夹名称(字母数字中文简体)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.SavePath.Length > 20)
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("请输入有效文件夹名称(最长20)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result = await _douyinFollowService.OpenOrCloseSync(dto);
|
||||||
|
return ApiResult.SuccOrFail(result, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改关注全量同步状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("openOrCloseFullSync")]
|
||||||
|
public async Task<IActionResult> OpenOrCloseFullSync(FollowUpdateDto dto)
|
||||||
|
{
|
||||||
|
return await OpenOrCloseSync(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除关注对象
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("delete")]
|
||||||
|
public async Task<IActionResult> DeleteFollow(FollowUpdateDto dto)
|
||||||
|
{
|
||||||
|
var result = await _douyinFollowService.DeleteFollow(dto);
|
||||||
|
return ApiResult.SuccOrFail(result, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using dy.net.model.dto;
|
||||||
|
using dy.net.service;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace dy.net.Controllers
|
namespace dy.net.Controllers
|
||||||
{
|
{
|
||||||
@@ -9,19 +9,21 @@ namespace dy.net.Controllers
|
|||||||
public class LogsController : ControllerBase
|
public class LogsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IWebHostEnvironment webHostEnvironment;
|
private readonly IWebHostEnvironment webHostEnvironment;
|
||||||
|
private readonly LogInfoService logInfoService;
|
||||||
|
|
||||||
public LogsController(IWebHostEnvironment webHostEnvironment)
|
public LogsController(IWebHostEnvironment webHostEnvironment, LogInfoService logInfoService)
|
||||||
{
|
{
|
||||||
this.webHostEnvironment = webHostEnvironment;
|
this.webHostEnvironment = webHostEnvironment;
|
||||||
|
this.logInfoService = logInfoService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet("/api/logs/GetLog/{type}/{date}")]
|
||||||
public async Task<IActionResult> GetLog(string type, string date)
|
public async Task<IActionResult> GetLog([FromRoute] string type, [FromRoute] string date)
|
||||||
{
|
{
|
||||||
var filePath = Path.Combine(webHostEnvironment.IsDevelopment() ? Directory.GetCurrentDirectory() : AppDomain.CurrentDomain.BaseDirectory, "logs", $"log-{type}-{date}.txt");
|
var filePath = Path.Combine(webHostEnvironment.IsDevelopment() ? Directory.GetCurrentDirectory() : AppDomain.CurrentDomain.BaseDirectory, "logs", $"log-{type}-{date}.txt");
|
||||||
if (!System.IO.File.Exists(filePath))
|
if (!System.IO.File.Exists(filePath))
|
||||||
{
|
{
|
||||||
var msg = $"Log file log-{type}-{date}.txt not found.";
|
var msg = $"{date},没有发现{type}的日志";
|
||||||
//Serilog.Log.Error(msg);
|
//Serilog.Log.Error(msg);
|
||||||
return Ok(msg);
|
return Ok(msg);
|
||||||
}
|
}
|
||||||
@@ -35,5 +37,57 @@ namespace dy.net.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最近10天的日志文件列表
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/api/logs/list")]
|
||||||
|
public IActionResult GetLogFiles()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var _logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||||
|
var files = logInfoService.GetLogFiles(_logDirectory);
|
||||||
|
return ApiResult.Success(files);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("获取日志列表失败," + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取日志文件内容流
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/api/logs/content")]
|
||||||
|
public IActionResult GetLogContent([FromQuery] LogContentRequest request)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var _logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||||
|
var stream = logInfoService.GetLogFileStream(_logDirectory, request.Type, request.Date);
|
||||||
|
// 返回文件流,指定MIME类型为文本
|
||||||
|
return File(stream, "text/plain", $"log-{request.Type}-{request.Date}.txt");
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { message = ex.Message });
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = ex.Message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { message = "读取日志文件失败", error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using dy.net.dto;
|
using dy.net.model.dto;
|
||||||
|
using dy.net.model.entity;
|
||||||
using dy.net.service;
|
using dy.net.service;
|
||||||
using dy.net.utils;
|
using dy.net.utils;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Drawing.Printing;
|
|
||||||
|
|
||||||
namespace dy.net.Controllers
|
namespace dy.net.Controllers
|
||||||
{
|
{
|
||||||
@@ -12,48 +11,29 @@ namespace dy.net.Controllers
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class VideoController : ControllerBase
|
public class VideoController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly DyCollectVideoService dyCollectVideoService;
|
private readonly DouyinVideoService douyinVideoService;
|
||||||
|
private readonly DouyinCommonService douyinCommonService;
|
||||||
|
|
||||||
public VideoController(DyCollectVideoService dyCollectVideoService)
|
public VideoController(DouyinVideoService dyCollectVideoService, DouyinCommonService douyinCommonService)
|
||||||
{
|
{
|
||||||
this.dyCollectVideoService = dyCollectVideoService;
|
this.douyinVideoService = dyCollectVideoService;
|
||||||
|
this.douyinCommonService = douyinCommonService;
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询收藏视频
|
/// 分页查询收藏视频
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
|
[Authorize]
|
||||||
[HttpPost("paged")]
|
[HttpPost("paged")]
|
||||||
public async Task<IActionResult> GetPagedAsync(VideoPageRequestDTO dto)
|
public async Task<IActionResult> GetPagedAsync(DouyinVideoPageRequestDto dto)
|
||||||
{
|
{
|
||||||
// 调试日志:输出接收到的排序参数
|
var (list, totalCount) = await douyinVideoService.GetPagedAsync(dto);
|
||||||
Console.WriteLine($"排序参数 - SortField: {dto.SortField}, SortOrder: {dto.SortOrder}");
|
return ApiResult.Success(new
|
||||||
|
|
||||||
var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Tags, dto.Author, dto.ViedoType, dto.Dates, dto.IsDeleted, dto.SortField, dto.SortOrder);
|
|
||||||
return Ok(new
|
|
||||||
{
|
{
|
||||||
code = 0,
|
data = list,
|
||||||
data = new
|
total = totalCount,
|
||||||
{
|
pageIndex = dto.PageIndex,
|
||||||
data = list,
|
pageSize = dto.PageSize
|
||||||
total=totalCount,
|
|
||||||
pageIndex=dto.PageIndex,
|
|
||||||
pageSize=dto.PageSize
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取所有视频类型列表(用于下拉框)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpGet("categories")]
|
|
||||||
public async Task<IActionResult> GetAllCategories()
|
|
||||||
{
|
|
||||||
var categories = await dyCollectVideoService.GetAllCategoriesAsync();
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
code = 0,
|
|
||||||
data = categories
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,158 +41,199 @@ namespace dy.net.Controllers
|
|||||||
/// 查询统计数据
|
/// 查询统计数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[Authorize]
|
||||||
[HttpGet("statics")]
|
[HttpGet("statics")]
|
||||||
public async Task<IActionResult> GetStaticsAsync()
|
public async Task<IActionResult> GetStaticsAsync()
|
||||||
{
|
{
|
||||||
var data = await dyCollectVideoService.GetStatics();
|
return ApiResult.Success(await douyinVideoService.GetStatics());
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
code = 0,
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清理数据库中文件不存在的视频记录
|
/// 播放视频
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="vid"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[Authorize]
|
[AllowAnonymous]
|
||||||
[HttpPost("cleanup")]
|
[HttpGet("play/{vid}")]
|
||||||
public async Task<IActionResult> CleanupMissingFilesAsync()
|
public async Task<IActionResult> StreamVideo([FromRoute] string vid)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Serilog.Log.Information("开始清理文件不存在的视频记录");
|
var viedo = await douyinVideoService.GetById(vid);
|
||||||
var deletedCount = await dyCollectVideoService.CleanupMissingFilesAsync();
|
|
||||||
Serilog.Log.Information($"清理完成,删除了 {deletedCount} 条记录");
|
if (viedo == null)
|
||||||
return Ok(new
|
|
||||||
{
|
{
|
||||||
code = 0,
|
return ApiResult.Fail($"视频不存在:{vid}");
|
||||||
data = new
|
}
|
||||||
{
|
return await PlayVideoAsync(viedo);
|
||||||
deletedCount = deletedCount,
|
|
||||||
message = $"清理完成,删除了 {deletedCount} 条文件不存在的记录"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Serilog.Log.Error(ex, "清理文件不存在的记录失败");
|
return ApiResult.Fail($"视频加载失败:{ex.Message}");
|
||||||
return StatusCode(500, new { code = -1, error = "清理失败:" + ex.Message });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 删除指定作者的所有视频数据
|
/// 抖音视频播放接口(优化版)
|
||||||
|
/// 解决内存泄漏、资源释放不及时、鲁棒性不足等问题
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authorName">作者名称</param>
|
/// <param name="video">视频实体(修正拼写错误:viedo -> video)</param>
|
||||||
/// <returns></returns>
|
/// <returns>视频流响应</returns>
|
||||||
[Authorize]
|
public async Task<IActionResult> PlayVideoAsync(DouyinVideo video)
|
||||||
[HttpPost("delete-author")]
|
|
||||||
public async Task<IActionResult> DeleteAuthorVideosAsync([FromBody] DeleteAuthorRequest request)
|
|
||||||
{
|
{
|
||||||
|
// 空值校验
|
||||||
|
if (video == null)
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("视频信息不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 获取完整物理路径并校验
|
||||||
|
string videoFullPath = video.VideoSavePath;
|
||||||
|
if (string.IsNullOrWhiteSpace(videoFullPath))
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("视频保存路径不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全校验:防止路径遍历攻击
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request?.AuthorName))
|
videoFullPath = Path.GetFullPath(videoFullPath);
|
||||||
{
|
|
||||||
return BadRequest(new { code = -1, error = "作者名称不能为空" });
|
|
||||||
}
|
|
||||||
|
|
||||||
Serilog.Log.Information($"开始删除作者 {request.AuthorName} 的视频数据");
|
|
||||||
var deletedCount = await dyCollectVideoService.DeleteAuthorVideosAsync(request.AuthorName);
|
|
||||||
Serilog.Log.Information($"删除完成,删除了 {deletedCount} 条记录");
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
code = 0,
|
|
||||||
data = new
|
|
||||||
{
|
|
||||||
deletedCount = deletedCount,
|
|
||||||
message = $"已删除作者 {request.AuthorName} 的 {deletedCount} 条视频记录"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
Serilog.Log.Error(ex, $"删除作者视频失败: {request?.AuthorName}");
|
return ApiResult.Fail("视频路径格式非法");
|
||||||
return StatusCode(500, new { code = -1, error = "删除失败:" + ex.Message });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
// 2. 验证文件是否存在
|
||||||
/// 删除作者请求DTO
|
if (!System.IO.File.Exists(videoFullPath))
|
||||||
/// </summary>
|
{
|
||||||
public class DeleteAuthorRequest
|
return ApiResult.Fail($"视频文件不存在:{videoFullPath}");
|
||||||
{
|
}
|
||||||
public string AuthorName { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取视频流(支持Range请求,用于视频播放)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">视频ID</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Authorize]
|
|
||||||
[HttpGet("stream/{id}")]
|
|
||||||
public async Task<IActionResult> GetVideoStream(string id)
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var video = await dyCollectVideoService.GetByIdAsync(id);
|
// 3. 获取文件信息(使用using确保FileInfo资源释放)
|
||||||
if (video == null || string.IsNullOrWhiteSpace(video.VideoSavePath))
|
var fileInfo = new FileInfo(videoFullPath);
|
||||||
|
long fileSize = fileInfo.Length;
|
||||||
|
|
||||||
|
// 校验空文件
|
||||||
|
if (fileSize == 0)
|
||||||
{
|
{
|
||||||
return NotFound(new { code = -1, error = "视频不存在" });
|
return ApiResult.Fail($"视频文件为空:{videoFullPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!System.IO.File.Exists(video.VideoSavePath))
|
string contentType = GetContentType(videoFullPath);
|
||||||
|
|
||||||
|
// 4. 处理分片请求(Range)
|
||||||
|
if (Request.Headers.ContainsKey("Range"))
|
||||||
{
|
{
|
||||||
return NotFound(new { code = -1, error = "视频文件不存在" });
|
return await HandleRangeRequestAsync(videoFullPath, fileSize, contentType);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var fileInfo = new FileInfo(video.VideoSavePath);
|
{
|
||||||
var contentType = GetContentType(video.VideoSavePath);
|
// 完整文件请求(启用分片处理,兼容前端断点续传)
|
||||||
|
return PhysicalFile(videoFullPath, contentType, enableRangeProcessing: true);
|
||||||
// 记录日志,便于调试
|
}
|
||||||
var rangeHeader = Request.Headers["Range"].ToString();
|
}
|
||||||
Serilog.Log.Debug($"视频流请求: ID={id}, 路径={video.VideoSavePath}, Content-Type={contentType}, Range={rangeHeader}");
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
var fileStream = new FileStream(video.VideoSavePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
return ApiResult.Fail($"没有权限访问视频文件:{videoFullPath}");
|
||||||
|
}
|
||||||
// 使用ASP.NET Core内置的Range支持
|
catch (IOException ex)
|
||||||
Response.Headers.Add("Accept-Ranges", "bytes");
|
{
|
||||||
|
// 记录日志(建议添加日志框架,如Serilog/NLog)
|
||||||
// 添加缓存控制头,确保浏览器可以缓存视频元数据
|
// _logger.LogError(ex, "读取视频文件失败:{Path}", videoFullPath);
|
||||||
// 但允许 Range 请求,这样浏览器可以快速开始播放
|
return ApiResult.Fail($"读取视频文件失败:{ex.Message}");
|
||||||
Response.Headers.Add("Cache-Control", "public, max-age=3600");
|
|
||||||
|
|
||||||
// 添加 CORS 头(如果需要)
|
|
||||||
Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
|
||||||
Response.Headers.Add("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
||||||
Response.Headers.Add("Access-Control-Allow-Headers", "Range");
|
|
||||||
Response.Headers.Add("Access-Control-Expose-Headers", "Content-Range, Accept-Ranges, Content-Length");
|
|
||||||
|
|
||||||
return File(fileStream, contentType, enableRangeProcessing: true);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Serilog.Log.Error(ex, $"获取视频流失败: {id}");
|
// _logger.LogError(ex, "视频播放接口异常:{Path}", videoFullPath);
|
||||||
return StatusCode(500, new { code = -1, error = "获取视频流失败" });
|
return ApiResult.Fail($"服务器内部错误:{ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理分片请求(Range),异步安全处理流
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">文件路径</param>
|
||||||
|
/// <param name="fileSize">文件总大小</param>
|
||||||
|
/// <param name="contentType">内容类型</param>
|
||||||
|
/// <returns>分片响应</returns>
|
||||||
|
private async Task<IActionResult> HandleRangeRequestAsync(string filePath, long fileSize, string contentType)
|
||||||
|
{
|
||||||
|
string rangeHeader = Request.Headers["Range"].ToString();
|
||||||
|
|
||||||
|
// 安全解析Range头
|
||||||
|
if (!rangeHeader.StartsWith("bytes="))
|
||||||
|
{
|
||||||
|
// 416 - 请求的范围无法满足
|
||||||
|
Response.StatusCode = StatusCodes.Status416RequestedRangeNotSatisfiable;
|
||||||
|
Response.Headers.Add("Content-Range", $"bytes */{fileSize}");
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析起始位置
|
||||||
|
string[] rangeParts = rangeHeader.Split('=')[1].Split('-');
|
||||||
|
if (!long.TryParse(rangeParts[0], out long start) || start < 0 || start >= fileSize)
|
||||||
|
{
|
||||||
|
Response.StatusCode = StatusCodes.Status416RequestedRangeNotSatisfiable;
|
||||||
|
Response.Headers.Add("Content-Range", $"bytes */{fileSize}");
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算分片结束位置(每片2MB,可配置)
|
||||||
|
long end = Math.Min(start + 1024 * 1024 * 2, fileSize - 1);
|
||||||
|
long chunkSize = end - start + 1;
|
||||||
|
|
||||||
|
// 设置206分片响应头
|
||||||
|
Response.StatusCode = StatusCodes.Status206PartialContent;
|
||||||
|
Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileSize}");
|
||||||
|
Response.Headers.Add("Accept-Ranges", "bytes");
|
||||||
|
Response.Headers.Add("Content-Length", chunkSize.ToString());
|
||||||
|
Response.ContentType = contentType;
|
||||||
|
|
||||||
|
// 核心改进:使用异步流 + using确保释放(通过管道直接写入响应流)
|
||||||
|
await using var fileStream = new FileStream(
|
||||||
|
filePath,
|
||||||
|
FileMode.Open,
|
||||||
|
FileAccess.Read,
|
||||||
|
FileShare.Read,
|
||||||
|
bufferSize: 8192, // 增大缓冲区提升性能,减少IO次数
|
||||||
|
FileOptions.Asynchronous | FileOptions.SequentialScan); // 顺序扫描优化
|
||||||
|
|
||||||
|
// 定位到分片起始位置
|
||||||
|
fileStream.Seek(start, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// 直接写入响应流,避免FileStreamResult的延迟释放问题
|
||||||
|
var buffer = new byte[8192];
|
||||||
|
long remainingBytes = chunkSize;
|
||||||
|
|
||||||
|
while (remainingBytes > 0)
|
||||||
|
{
|
||||||
|
int bytesRead = await fileStream.ReadAsync(buffer, 0, (int)Math.Min(remainingBytes, buffer.Length));
|
||||||
|
if (bytesRead == 0) break;
|
||||||
|
|
||||||
|
await Response.Body.WriteAsync(buffer.AsMemory(0, bytesRead));
|
||||||
|
remainingBytes -= bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Response.Body.FlushAsync();
|
||||||
|
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取视频封面图片
|
/// 获取视频封面图片
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">视频ID</param>
|
/// <param name="id">视频ID</param>
|
||||||
/// <returns></returns>
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpGet("cover/{id}")]
|
[HttpGet("cover/{id}")]
|
||||||
public async Task<IActionResult> GetVideoCover(string id)
|
public async Task<IActionResult> GetVideoCover([FromRoute] string id)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var video = await dyCollectVideoService.GetByIdAsync(id);
|
var video = await douyinVideoService.GetById(id);
|
||||||
if (video == null)
|
if (video == null)
|
||||||
{
|
{
|
||||||
return GetDefaultCoverImage();
|
return GetDefaultCoverImage();
|
||||||
@@ -221,13 +242,12 @@ namespace dy.net.Controllers
|
|||||||
// 优先使用本地保存的封面
|
// 优先使用本地保存的封面
|
||||||
if (!string.IsNullOrWhiteSpace(video.VideoCoverSavePath) && System.IO.File.Exists(video.VideoCoverSavePath))
|
if (!string.IsNullOrWhiteSpace(video.VideoCoverSavePath) && System.IO.File.Exists(video.VideoCoverSavePath))
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(video.VideoCoverSavePath);
|
|
||||||
var contentType = GetImageContentType(video.VideoCoverSavePath);
|
var contentType = GetImageContentType(video.VideoCoverSavePath);
|
||||||
var fileStream = System.IO.File.OpenRead(video.VideoCoverSavePath);
|
var fileStream = System.IO.File.OpenRead(video.VideoCoverSavePath);
|
||||||
return File(fileStream, contentType);
|
return File(fileStream, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有本地封面,返回默认图片
|
// 本地封面缺失时回落到默认占位图
|
||||||
return GetDefaultCoverImage();
|
return GetDefaultCoverImage();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -238,7 +258,7 @@ namespace dy.net.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 返回默认封面图片(SVG 占位图)
|
/// 默认封面占位图(SVG)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IActionResult GetDefaultCoverImage()
|
private IActionResult GetDefaultCoverImage()
|
||||||
{
|
{
|
||||||
@@ -259,7 +279,6 @@ namespace dy.net.Controllers
|
|||||||
{
|
{
|
||||||
return "image/jpeg";
|
return "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||||
return extension switch
|
return extension switch
|
||||||
{
|
{
|
||||||
@@ -273,74 +292,252 @@ namespace dy.net.Controllers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 播放视频
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vid"></param>
|
||||||
|
/// <param name="k"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("/share/{vid}/{k}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> Share([FromRoute] string vid, [FromRoute] string k)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var viedo = await douyinVideoService.GetById(vid);
|
||||||
|
|
||||||
|
if (viedo == null)
|
||||||
|
{
|
||||||
|
return ApiResult.Fail($"视频不存在:{vid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedKey = (viedo.FileHash + viedo.AuthorId).Md5();
|
||||||
|
if (expectedKey != k)
|
||||||
|
{
|
||||||
|
return ApiResult.Fail($"视频地址无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await PlayVideoAsync(viedo);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResult.Fail($"视频加载失败:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据文件扩展名获取Content-Type
|
/// 辅助方法:根据文件名获取 MIME 类型(确保前端正确识别视频格式)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string GetContentType(string fileName)
|
private static string GetContentType(string filename)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(fileName))
|
string extension = Path.GetExtension(filename).ToLowerInvariant();
|
||||||
{
|
return extension switch
|
||||||
Serilog.Log.Warning($"文件名为空,使用默认 video/mp4");
|
|
||||||
return "video/mp4"; // 抖音视频默认是 MP4
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
|
||||||
|
|
||||||
// 如果没有扩展名,尝试从文件名推断(抖音视频格式通常是 mp4)
|
|
||||||
if (string.IsNullOrEmpty(extension))
|
|
||||||
{
|
|
||||||
Serilog.Log.Warning($"文件 {fileName} 没有扩展名,使用默认 video/mp4");
|
|
||||||
return "video/mp4";
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentType = extension switch
|
|
||||||
{
|
{
|
||||||
".mp4" => "video/mp4",
|
".mp4" => "video/mp4",
|
||||||
".webm" => "video/webm",
|
_ => "application/octet-stream" // 默认二进制流
|
||||||
".ogg" => "video/ogg",
|
|
||||||
".mov" => "video/quicktime",
|
|
||||||
".avi" => "video/x-msvideo",
|
|
||||||
".mkv" => "video/x-matroska",
|
|
||||||
".flv" => "video/x-flv",
|
|
||||||
".wmv" => "video/x-ms-wmv",
|
|
||||||
".3gp" => "video/3gpp",
|
|
||||||
".m3u8" => "application/x-mpegURL", // HLS 流
|
|
||||||
_ => null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果无法识别扩展名,默认使用 video/mp4(抖音视频通常是 MP4)
|
|
||||||
if (contentType == null)
|
|
||||||
{
|
|
||||||
Serilog.Log.Warning($"无法识别的视频扩展名: {extension},文件: {fileName},使用默认 video/mp4");
|
|
||||||
return "video/mp4";
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
List<MyClass> myClasses = new List<MyClass>();
|
|
||||||
foreach (var drive in DriveInfo.GetDrives())
|
|
||||||
{
|
|
||||||
myClasses.Add(new MyClass { name=drive.Name, x1= drive.TotalSize, x2= drive.TotalFreeSpace });
|
|
||||||
Serilog.Log.Debug($"Drive: {drive.Name}, Total Size: {drive.TotalSize}, Free Space: {drive.TotalFreeSpace}");
|
|
||||||
}
|
|
||||||
//var disk=DiskInfoHelper.GetDockerHostTotalDiskSpaceGB();
|
|
||||||
return Ok(JsonConvert.SerializeObject(myClasses)+"\r\n"+JsonConvert.SerializeObject(myClasses.Sum(x=>x.x1)+"\r\n"+ JsonConvert.SerializeObject(myClasses.Sum(x => x.x2))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class MyClass
|
/// <summary>
|
||||||
|
/// 重新下载
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("redown")]
|
||||||
|
public async Task<IActionResult> ReDownload(ReDownViedoDto dto)
|
||||||
{
|
{
|
||||||
public string name { get; set; }
|
if (dto == null)
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("参数错误");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var result = await douyinVideoService.ReDownloadViedoAsync(dto);
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
return ApiResult.Success(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public long x1 { get; set; }
|
/// <summary>
|
||||||
|
/// 批量删除
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("vdelete/batch")]
|
||||||
|
public async Task<IActionResult> BathRealDelete(ReDownViedoDto dto)
|
||||||
|
{
|
||||||
|
var result = await douyinVideoService.RealDeleteVideos(dto.Ids);
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
return ApiResult.Success(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public long x2 { get; set; }
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除视频-不再下载
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vid"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("vdelete/{vid}")]
|
||||||
|
public async Task<IActionResult> DeleteVideo([FromRoute] string vid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(vid))
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("参数错误");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var res = await douyinVideoService.RealDeleteVideos(new List<string> { vid });
|
||||||
|
if (res)
|
||||||
|
{
|
||||||
|
return ApiResult.Success("删除成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("删除失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询已删除视频列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("vdelete/get")]
|
||||||
|
public async Task<IActionResult> GetDeleteVideo()
|
||||||
|
{
|
||||||
|
return ApiResult.Success(await douyinCommonService.GetDouyinDeleteVideos());
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 根据博主id删除博主所有视频
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uperUid"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("vdelete/byauthor/{uperUid}")]
|
||||||
|
public async Task<IActionResult> DeleteByAuthor([FromRoute] string uperUid)
|
||||||
|
{
|
||||||
|
var videos = await douyinVideoService.GetByAuthorId(uperUid);
|
||||||
|
if (videos != null && videos.Any())
|
||||||
|
{
|
||||||
|
var res = await douyinVideoService.RealDeleteVideos(videos.Select(x => x.Id).ToList());
|
||||||
|
if (res)
|
||||||
|
{
|
||||||
|
return ApiResult.Success("删除成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResult.Fail("删除失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ApiResult.Fail("未找到该博主视频");
|
||||||
|
}
|
||||||
|
|
||||||
|
//private async Task<(bool flowControl, IActionResult value)> BatchDeleteVideos(List<DouyinVideo> videos)
|
||||||
|
//{
|
||||||
|
|
||||||
|
// return (flowControl: true, value: null);
|
||||||
|
//}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询最新N条数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="top"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("top{top}")]
|
||||||
|
public async Task<IActionResult> GetLastSyncTop([FromRoute] int top = 5)
|
||||||
|
{
|
||||||
|
return ApiResult.Success(await douyinVideoService.GetLastSyncTop(top));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除无效视频记录
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("removeInvalid")]
|
||||||
|
public async Task<IActionResult> RemoveInvalidVideo()
|
||||||
|
{
|
||||||
|
var data = await douyinVideoService.DeleteInvalidVideo();
|
||||||
|
return Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// 所有视频重新生成nfo文件
|
||||||
|
///// </summary>
|
||||||
|
///// <returns></returns>
|
||||||
|
//[HttpGet("renfo")]
|
||||||
|
//public async Task<IActionResult> ReCreateNfo()
|
||||||
|
//{
|
||||||
|
// var videos = await douyinVideoService.GetAllAsync();
|
||||||
|
// if (videos == null || videos.Count == 0)
|
||||||
|
// {
|
||||||
|
// return ApiResult.Success("暂无视频数据需要生成NFO文件");
|
||||||
|
// }
|
||||||
|
// _ = Task.Run(async () =>
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var totalCount = videos.Count;
|
||||||
|
// foreach (var video in videos)
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// NfoFileGenerator.GenerateVideoNfoFile(video);
|
||||||
|
// Serilog.Log.Debug($"刮削视频(Path:{video.VideoSavePath})生成NFO成功!");
|
||||||
|
// await Task.Delay(50);
|
||||||
|
// }
|
||||||
|
// catch (Exception singleEx)
|
||||||
|
// {
|
||||||
|
// Serilog.Log.Error($"刮削视频(Path:{video.VideoSavePath})生成NFO失败:{singleEx.Message}");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// Serilog.Log.Error($"NFO文件生成任务执行异常:{ex.Message}\n{ex.StackTrace}");
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return ApiResult.Success();
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取7天视频同步趋势数据(曲线图)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>7天图表数据列表</returns>
|
||||||
|
[HttpGet("chart/{day}")]
|
||||||
|
public async Task<IActionResult> Chart([FromRoute] int day = 7)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var chartData = await douyinVideoService.GetChartData(day);
|
||||||
|
return ApiResult.Success(chartData);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResult.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("/Move")]
|
||||||
|
public async Task<IActionResult> Move()
|
||||||
|
{
|
||||||
|
await douyinVideoService.HandOldFolderVideos();
|
||||||
|
|
||||||
|
return ApiResult.Success(DateTime.Now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
# 多阶段构建 Dockerfile
|
# 多阶段构建 Dockerfile
|
||||||
# 第一阶段:构建前端
|
# 第一阶段:构建前端
|
||||||
# 使用官方 Docker Hub(mcr 能访问的环境一般也能访问 docker.io)
|
|
||||||
FROM node:18-alpine AS frontend-builder
|
FROM node:18-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
# 复制前端依赖文件
|
|
||||||
COPY app/package.json app/pnpm-lock.yaml* ./
|
COPY app/package.json app/pnpm-lock.yaml* ./
|
||||||
|
|
||||||
# 配置 npm 和 pnpm 使用国内镜像源
|
|
||||||
RUN npm config set registry https://registry.npmmirror.com && \
|
RUN npm config set registry https://registry.npmmirror.com && \
|
||||||
npm install -g pnpm && \
|
npm install -g pnpm && \
|
||||||
pnpm config set registry https://registry.npmmirror.com && \
|
pnpm config set registry https://registry.npmmirror.com && \
|
||||||
if [ -f pnpm-lock.yaml ]; then \
|
if [ -f pnpm-lock.yaml ]; then \
|
||||||
pnpm install --frozen-lockfile; \
|
pnpm install --no-frozen-lockfile; \
|
||||||
else \
|
else \
|
||||||
npm install; \
|
npm install; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 复制前端源码
|
|
||||||
COPY app/ ./
|
COPY app/ ./
|
||||||
|
|
||||||
# 构建前端
|
|
||||||
RUN pnpm build || npm run build
|
RUN pnpm build || npm run build
|
||||||
|
|
||||||
# 第二阶段:构建后端
|
# 第二阶段:构建后端
|
||||||
@@ -29,41 +23,30 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0 AS backend-builder
|
|||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# 复制项目文件
|
|
||||||
COPY *.csproj ./
|
COPY *.csproj ./
|
||||||
RUN dotnet restore dy.net.csproj
|
RUN dotnet restore dy.net.csproj
|
||||||
|
|
||||||
# 复制所有文件
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 删除 macOS 资源分叉文件(._* 文件)
|
|
||||||
RUN find . -name "._*" -type f -delete
|
RUN find . -name "._*" -type f -delete
|
||||||
|
|
||||||
# 复制前端构建产物
|
|
||||||
COPY --from=frontend-builder /app/frontend/dist ./app/dist
|
COPY --from=frontend-builder /app/frontend/dist ./app/dist
|
||||||
|
|
||||||
# 发布后端
|
|
||||||
RUN dotnet publish dy.net.csproj -c Release -o /app/publish
|
RUN dotnet publish dy.net.csproj -c Release -o /app/publish
|
||||||
|
|
||||||
# 第三阶段:运行镜像
|
# 第三阶段:运行镜像(Alpine 版:apk 包更小、代理下更易通过)
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS final
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 设置时区
|
|
||||||
ENV TZ=Asia/Shanghai
|
ENV TZ=Asia/Shanghai
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
RUN apk add --no-cache tzdata ffmpeg && \
|
||||||
|
cp /usr/share/zoneinfo/$TZ /etc/localtime && \
|
||||||
|
echo $TZ > /etc/timezone
|
||||||
|
|
||||||
# 从构建阶段复制发布文件
|
|
||||||
COPY --from=backend-builder /app/publish .
|
COPY --from=backend-builder /app/publish .
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE 10101
|
EXPOSE 10101
|
||||||
|
|
||||||
# 设置环境变量
|
|
||||||
ENV ASPNETCORE_URLS=http://*:10101
|
ENV ASPNETCORE_URLS=http://*:10101
|
||||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
ENTRYPOINT ["dotnet", "dy.net.dll"]
|
ENTRYPOINT ["dotnet", "dy.net.dll"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
using ClockSnowFlake;
|
using dy.net.extension;
|
||||||
using dy.net.dto;
|
|
||||||
using dy.net.extension;
|
|
||||||
using dy.net.model;
|
|
||||||
using dy.net.service;
|
using dy.net.service;
|
||||||
using dy.net.utils;
|
using dy.net.utils;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using SqlSugar;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -16,136 +9,190 @@ namespace dy.net
|
|||||||
{
|
{
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
// 常量定义
|
// 常量定义
|
||||||
private const string DefaultListenUrl = "http://*:10101";
|
private static readonly string DefaultListenUrl = "http://*:10101";
|
||||||
private const string SpaRootPath = "app/dist";
|
private const string SpaRootPath = "app/dist";
|
||||||
private const string SpaSourcePath = "app/";
|
private const string SpaSourcePath = "app/";
|
||||||
private const string SwaggerDocTitle = "dy.net WebApi Docs";
|
private const string SwaggerDocTitle = "dysync.net WebApi Docs";
|
||||||
|
|
||||||
public static void Main(string[] args)
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
// 初始化编码提供器
|
// 初始化编码提供器
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
// 构建Web应用
|
||||||
// 构建Web应用
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var isDevelopment = builder.Environment.IsDevelopment();
|
var isDevelopment = builder.Environment.IsDevelopment();
|
||||||
|
// 配置主机
|
||||||
// 配置主机
|
|
||||||
ConfigureHost(builder, isDevelopment);
|
ConfigureHost(builder, isDevelopment);
|
||||||
|
// 配置服务--数据库保存路径从命令行参数传入的第一个参数
|
||||||
// 配置服务
|
string dbPath = args.Length > 0 ? args[0] : "";
|
||||||
var services = builder.Services;
|
ConfigureServices(builder.Services, builder.Configuration, builder.Environment, dbPath);
|
||||||
ConfigureServices(services, builder.Configuration, isDevelopment);
|
// 构建应用
|
||||||
|
|
||||||
// 构建应用
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
// 配置中间件
|
||||||
|
ConfigureMiddleware(app, builder.Environment);
|
||||||
|
Log.Debug($"dy.sync app is started successfully on {DefaultListenUrl}");
|
||||||
|
// 初始化应用服务
|
||||||
|
await InitApplicationServices(app, isDevelopment, dbPath);
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
// 配置中间件
|
await app.RunAsync();
|
||||||
ConfigureMiddleware(app, isDevelopment);
|
|
||||||
|
|
||||||
// 初始化应用服务
|
|
||||||
InitApplicationServices(app);
|
|
||||||
|
|
||||||
// 启动应用
|
|
||||||
Serilog.Log.Debug("dy.net service started successfully");
|
|
||||||
app.Run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置主机设置
|
/// 配置主机设置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ConfigureHost(WebApplicationBuilder builder, bool isDevelopment)
|
private static void ConfigureHost(WebApplicationBuilder builder, bool isDevelopment)
|
||||||
{
|
{
|
||||||
// 设置监听地址
|
InitAppsettings(builder);
|
||||||
|
//from docker yaml file 环境变量 或者 dockerfile 或appsettings.json
|
||||||
|
// 设置监听地址
|
||||||
builder.WebHost.UseUrls(DefaultListenUrl);
|
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();
|
builder.ConfigureLogging();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static void InitAppsettings(WebApplicationBuilder builder)
|
||||||
/// 配置依赖注入服务
|
|
||||||
/// </summary>
|
|
||||||
private static void ConfigureServices(IServiceCollection services, IConfiguration config, bool isDevelopment)
|
|
||||||
{
|
{
|
||||||
// 雪花ID生成器
|
// 配置配置文件
|
||||||
services.AddSnowFlakeId(options => options.WorkId = new Random().Next(1, 127));
|
builder.Host.ConfigureAppConfiguration((context, config) =>
|
||||||
|
{
|
||||||
|
// 定义配置文件名(统一小写,适配Linux大小写敏感特性)
|
||||||
|
const string configFileName = "appsettings.json";
|
||||||
|
|
||||||
// MVC控制器
|
// 步骤1:获取多维度的候选基础路径(覆盖不同部署场景)
|
||||||
services.AddControllers();
|
var candidateBasePaths = new List<string>
|
||||||
|
{
|
||||||
|
// 候选1:程序集所在目录(优先,解决工作目录不一致问题)
|
||||||
|
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
|
||||||
|
// 候选2:当前工作目录(兜底)
|
||||||
|
Directory.GetCurrentDirectory(),
|
||||||
|
// 候选3:应用根目录(针对单文件发布/容器部署场景)
|
||||||
|
AppContext.BaseDirectory,
|
||||||
|
// 候选4:自定义环境变量指定的配置目录(灵活性扩展)
|
||||||
|
Environment.GetEnvironmentVariable("APP_CONFIG_DIR")
|
||||||
|
}
|
||||||
|
// 过滤空值和无效路径
|
||||||
|
.Where(path => !string.IsNullOrEmpty(path) && Directory.Exists(path))
|
||||||
|
.Distinct() // 去重
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// HTTP客户端
|
// 步骤2:遍历候选路径,查找存在的配置文件
|
||||||
|
string configFilePath = null;
|
||||||
|
foreach (var basePath in candidateBasePaths)
|
||||||
|
{
|
||||||
|
var tempPath = Path.Combine(basePath, configFileName);
|
||||||
|
if (File.Exists(tempPath))
|
||||||
|
{
|
||||||
|
configFilePath = tempPath;
|
||||||
|
break; // 找到第一个存在的配置文件即可
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤3:配置加载(增加容错和日志提示)
|
||||||
|
if (!string.IsNullOrEmpty(configFilePath))
|
||||||
|
{
|
||||||
|
// 找到配置文件,正常加载
|
||||||
|
var basePath = Path.GetDirectoryName(configFilePath);
|
||||||
|
config.SetBasePath(basePath)
|
||||||
|
.AddJsonFile(configFileName, optional: false, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
// 可选:输出日志,确认配置文件加载路径(方便排查)
|
||||||
|
//Console.WriteLine($"成功加载配置文件:{configFilePath}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 未找到配置文件,抛出明确异常(或根据需求调整为兜底逻辑)
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
$"未找到配置文件 {configFileName},已检查以下路径:{string.Join("; ", candidateBasePaths)}",
|
||||||
|
configFileName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置依赖注入服务
|
||||||
|
/// </summary>
|
||||||
|
private static void ConfigureServices(IServiceCollection services, IConfiguration config, IWebHostEnvironment environment, string dbPath)
|
||||||
|
{
|
||||||
|
|
||||||
|
//打印logo
|
||||||
|
//PrintApp();
|
||||||
|
|
||||||
|
services.AddSingleton(new Appsettings(config));
|
||||||
|
// 雪花ID生成器
|
||||||
|
services.AddSnowFlakeId(options => options.WorkId = new Random().Next(1, 100));
|
||||||
|
|
||||||
|
// MVC控制器+异常拦截器
|
||||||
|
services.AddControllers().AddGlobalExceptionFilter();
|
||||||
|
|
||||||
|
// HTTP客户端
|
||||||
services.AddHttpClients();
|
services.AddHttpClients();
|
||||||
|
|
||||||
// 数据库
|
// 数据库
|
||||||
services.AddSqlsugar(config);
|
services.AddSqlsugar(dbPath);
|
||||||
|
|
||||||
// 定时任务
|
// 定时任务
|
||||||
services.AddQuartzService(config);
|
services.AddQuartzService(dbPath);
|
||||||
|
|
||||||
// 仓储和服务注册
|
// 仓储和服务注册
|
||||||
services.AddServicesFromNamespace("dy.net.repository")
|
services.AddServicesFromNamespace("dy.net.repository")
|
||||||
.AddServicesFromNamespace("dy.net.service");
|
.AddServicesFromNamespace("dy.net.service");
|
||||||
|
|
||||||
// SPA静态文件支持
|
services.AddScoped<FFmpegHelper>();
|
||||||
|
|
||||||
|
// SPA静态文件支持
|
||||||
services.AddSpaStaticFiles(options => options.RootPath = SpaRootPath);
|
services.AddSpaStaticFiles(options => options.RootPath = SpaRootPath);
|
||||||
|
|
||||||
// 开发环境启用Swagger
|
// 开发环境启用Swagger
|
||||||
if (isDevelopment)
|
//if (environment.IsDevelopment())
|
||||||
{
|
//{
|
||||||
services.AddSwagger();
|
// services.AddSwagger();
|
||||||
}
|
//}
|
||||||
|
|
||||||
// 响应压缩
|
// 响应压缩
|
||||||
services.AddResponseCompression();
|
services.AddResponseCompression();
|
||||||
|
|
||||||
// JWT认证
|
// JWT认证
|
||||||
services.ConfigureJwtAuthentication();
|
services.ConfigureJwtAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置中间件
|
/// 配置中间件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ConfigureMiddleware(WebApplication app, bool isDevelopment)
|
private static void ConfigureMiddleware(WebApplication app, IWebHostEnvironment environment)
|
||||||
{
|
{
|
||||||
// 响应压缩
|
// 响应压缩
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
// 开发环境启用SwaggerUI
|
// 开发环境启用SwaggerUI
|
||||||
if (isDevelopment)
|
//if (environment.IsDevelopment())
|
||||||
{
|
//{
|
||||||
app.UseCustomSwaggerUI(options => options.Title = SwaggerDocTitle);
|
// app.UseCustomSwaggerUI(options => options.Title = SwaggerDocTitle);
|
||||||
}
|
//}
|
||||||
|
|
||||||
// 路由
|
// 路由
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
// 认证授权
|
// 认证授权
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
// 配置文件上传路径
|
// 配置文件上传路径
|
||||||
//ConfigureUploadPath(app, isDevelopment);
|
//ConfigureUploadPath(app, isDevelopment);
|
||||||
|
|
||||||
// API路由映射
|
// API路由映射
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
// 生产环境启用SPA
|
app.UseStaticFiles();
|
||||||
if (!isDevelopment)
|
// 生产环境启用SPA
|
||||||
|
if (!environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseSpaStaticFiles();
|
app.UseSpaStaticFiles();
|
||||||
app.UseSpa(spa => spa.Options.SourcePath = SpaSourcePath);
|
app.UseSpa(spa => spa.Options.SourcePath = SpaSourcePath);
|
||||||
@@ -153,47 +200,71 @@ namespace dy.net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化应用服务数据
|
/// 初始化应用服务数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void InitApplicationServices(WebApplication app)
|
private static async Task InitApplicationServices(WebApplication app, bool isDevelopment, string dbPath)
|
||||||
{
|
{
|
||||||
using var scope = app.Services.CreateScope();
|
using var scope = app.Services.CreateScope();
|
||||||
var services = scope.ServiceProvider;
|
var services = scope.ServiceProvider;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 初始化用户
|
// 初始化用户
|
||||||
var userService = services.GetRequiredService<UserService>();
|
var userService = services.GetRequiredService<AdminUserService>();
|
||||||
userService.InitUser();
|
userService.InitUser("douyin", "douyin2026");
|
||||||
|
|
||||||
// 初始化Cookie
|
var pwdTxt = Path.Combine(AppContext.BaseDirectory, "db", "pwd.txt");
|
||||||
var cookieService = services.GetRequiredService<DyCookieService>();
|
if (!string.IsNullOrWhiteSpace(dbPath))
|
||||||
cookieService.Init();
|
{
|
||||||
|
pwdTxt = Path.Combine(dbPath, "pwd.txt");
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化配置
|
if (File.Exists(pwdTxt))
|
||||||
var commonService = services.GetRequiredService<CommonService>();
|
{
|
||||||
|
var pwd = File.ReadAllText(pwdTxt);
|
||||||
|
var reset = userService.ResetPwd(pwd);
|
||||||
|
File.Delete(pwdTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var commonService = services.GetRequiredService<DouyinCommonService>();
|
||||||
|
|
||||||
|
// 更新视频类型--兼容老版本--不再需要
|
||||||
|
//commonService.UpdateCollectViedoType();
|
||||||
|
// 重置博主作品同步状态为未同步
|
||||||
|
//commonService.UpdateAllCookieSyncedToZero();
|
||||||
|
|
||||||
|
//await commonService.UpdateFollowedSavePathAsync();
|
||||||
|
//await commonService.UpdateImageVideoSavePath();
|
||||||
|
|
||||||
|
// 初始化Cookie
|
||||||
|
var cookieService = services.GetRequiredService<DouyinCookieService>();
|
||||||
|
var deploy = Appsettings.Get("deploy");
|
||||||
|
if (deploy != null && deploy == "docker")//docker环境直接初始化一个默认的配置
|
||||||
|
{
|
||||||
|
cookieService.InitCookie();
|
||||||
|
}
|
||||||
|
// 初始化配置
|
||||||
var config = commonService.InitConfig();
|
var config = commonService.InitConfig();
|
||||||
|
|
||||||
// 更新收藏视频类型--兼容老版本-原来的旧数据没有这个类型字段
|
//Serilog.Log.Debug("isRestart1=" + config.IsFirstRunning);
|
||||||
commonService.UpdateCollectViedoType();
|
//await cookieService.UpdateCookieToSupportOldVersionAsync();
|
||||||
// 重置博主作品同步状态为未同步
|
|
||||||
commonService.UpdateAllCookieSyncedToZero();
|
|
||||||
// 启动定时任务
|
|
||||||
var quartzJobService = services.GetRequiredService<QuartzJobService>();
|
|
||||||
quartzJobService.StartJob(config?.Cron ?? "30");
|
|
||||||
|
|
||||||
Serilog.Log.Debug("系统初始化完成,会默认将-博主作品同步功能-同步全部作品重置为关闭(若要开启,可以到抖音授权页面中修改)");
|
if (!isDevelopment)
|
||||||
Serilog.Log.Debug($"默认设置的每次读取行数为:{config.BatchCount},可前往系统配置页修改");
|
{
|
||||||
|
var cookie = await cookieService.GetOpendCookies();
|
||||||
|
if (cookie != null && cookie.Any())
|
||||||
|
{
|
||||||
|
// 启动定时任务
|
||||||
|
var quartzJobService = services.GetRequiredService<DouyinQuartzJobService>();
|
||||||
|
await quartzJobService.InitOrReStartAllJobs(config?.Cron <= 0 ? "30" : config.Cron.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var logger = services.GetRequiredService<ILogger<Program>>();
|
Serilog.Log.Error(ex, "Failed to initialize services on startup");
|
||||||
logger.LogError(ex, "Failed to initialize services on startup");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
|
||||||
-->
|
|
||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
|
|
||||||
<History>True|2025-11-12T15:06:38.7834109Z||;False|2025-11-12T23:01:02.2269478+08:00||;True|2025-10-22T22:08:11.6423148+08:00||;True|2025-10-22T21:54:24.7180547+08:00||;True|2025-10-22T21:40:03.5194315+08:00||;True|2025-10-22T21:28:28.8962766+08:00||;True|2025-10-22T21:22:47.9631689+08:00||;True|2025-10-22T21:18:24.5274318+08:00||;True|2025-10-22T21:14:51.6386326+08:00||;False|2025-10-22T21:14:07.6282769+08:00||;True|2025-10-22T21:03:48.9892860+08:00||;True|2025-10-22T21:00:56.3617243+08:00||;False|2025-10-22T21:00:30.5472941+08:00||;True|2025-10-22T20:51:29.6155916+08:00||;False|2025-10-22T20:50:47.1882956+08:00||;True|2025-10-22T15:01:04.1668366+08:00||;True|2025-10-22T14:49:43.6340569+08:00||;True|2025-10-22T14:38:39.4685603+08:00||;True|2025-10-21T18:35:28.8392541+08:00||;True|2025-10-20T10:31:05.5865212+08:00||;True|2025-10-20T10:20:41.5717101+08:00||;True|2025-10-19T10:08:33.6669332+08:00||;False|2025-10-19T10:07:22.3337545+08:00||;False|2025-10-19T10:05:22.9484805+08:00||;True|2025-10-10T16:54:27.1472888+08:00||;False|2025-10-10T16:53:44.1700030+08:00||;False|2025-10-10T16:52:48.1740453+08:00||;False|2025-10-10T16:51:21.5067253+08:00||;False|2025-10-10T16:50:19.2140597+08:00||;False|2025-10-10T16:49:21.2213290+08:00||;False|2025-10-10T16:48:47.7229948+08:00||;False|2025-10-10T16:48:15.1258700+08:00||;True|2025-09-30T13:29:42.2354235+08:00||;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||;</History>
|
|
||||||
<LastFailureDetails />
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
|
||||||
-->
|
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DeleteExistingFiles>true</DeleteExistingFiles>
|
<DeleteExistingFiles>true</DeleteExistingFiles>
|
||||||
@@ -17,5 +15,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ProjectGuid>680660ef-acae-43a9-ab6c-b75532e758ad</ProjectGuid>
|
<ProjectGuid>680660ef-acae-43a9-ab6c-b75532e758ad</ProjectGuid>
|
||||||
<SelfContained>false</SelfContained>
|
<SelfContained>false</SelfContained>
|
||||||
|
<!--<DockerArch>x86</DockerArch>-->
|
||||||
|
<!--<DockerArch>arm</DockerArch> -->
|
||||||
|
<!--<DockerCoreVersion>2.2.0</DockerCoreVersion>-->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?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>true</SelfContained>
|
||||||
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
<DebugType>none</DebugType>
|
||||||
|
<DebugSymbols>false</DebugSymbols>
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
<!-- 单文件压缩 -->
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<!-- 编译级优化,减少生成代码体积 -->
|
||||||
|
<TrimUnusedDependencies>true</TrimUnusedDependencies>
|
||||||
|
<!-- 裁剪未使用的NuGet依赖 -->
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"dy.net": {
|
"dy.net": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": true,
|
//"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": "http://localhost:5193"
|
"applicationUrl": "http://localhost:10101"
|
||||||
},
|
},
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
|
|||||||
@@ -1,122 +1,191 @@
|
|||||||
# dysync.net - 抖音同步工具(收藏/喜欢/指定博主,无缝对接Emby/Jellyfin)
|
# dysync.net - 抖音同步工具(抖小云)
|
||||||
|
|
||||||
`dysync.net` 是一款基于 **.NET Core 6.0** 和 **Vue** 开发的工具,用于同步抖音收藏夹、「我喜欢」的视频及指定博主作品,解决个人收藏视频易失效的问题。支持多账号同步,内置视频信息刮削功能,同步后的视频可直接在 Emby 或 Jellyfin 中播放。
|
`dysync.net` 是一款基于 **.NET Core ** 和 **Vue** 开发的工具,用于同步抖音收藏夹、「我喜欢」的视频及指定博主作品,支持多账号同步,内置视频信息刮削功能,同步后的视频可直接在 Emby 或 Jellyfin 中播放。
|
||||||
|
|
||||||
> 🔧 问题反馈:使用中遇到任何问题,欢迎添加 QQ:279225040 咨询
|
> 🔧 **问题反馈**:如使用中遇到任何问题,欢迎进Q群反馈:759876963,不一定及时回复,见谅。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 📺 Emby 播放效果预览
|
## 📺 Emby 效果预览
|
||||||

|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📌 1. 获取抖音关键信息(必做!同步核心凭证)
|
## 📋 目录
|
||||||
Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取,避免遗漏或错误。
|
- [dysync.net - 抖音同步工具(抖小云)](#dysyncnet---抖音同步工具抖小云)
|
||||||
|
- [📺 Emby 播放效果预览](#-emby-播放效果预览)
|
||||||
### 1.1 提取抖音 Cookie
|
- [📋 目录](#-目录)
|
||||||
1. 打开 **抖音网页版** (https://www.douyin.com/) 并登录目标账号;
|
- [1. 获取抖音关键信息(必做!同步核心凭证)](#1-获取抖音关键信息必做同步核心凭证)
|
||||||
2. 进入「我的收藏」页面,确保页面加载完成;
|
- [1.1 提取抖音 Cookie](#11-提取抖音-cookie)
|
||||||
3. 按 `F12` 打开浏览器「开发者工具」,切换到「Network (网络)」标签;
|
- [1.2 提取 `sec_user_id`(个人/指定博主)](#12-提取-sec_user_id个人指定博主)
|
||||||
4. 刷新页面,在搜索框中输入 `v1/web/aweme/listcollection` 筛选请求;
|
- [2. 路径映射规则(核心!错配会导致无法访问/数据丢失)](#2-路径映射规则核心错配会导致无法访问数据丢失)
|
||||||
5. 点击任意一条筛选结果,在右侧「Headers (标头)」中找到 `Cookie` 字段,**完整复制整段内容**(不可删减字符)。
|
- [3. 默认账号密码(首次登录用)](#3-默认账号密码首次登录用)
|
||||||
|
- [4. 运行方式(推荐 Docker Compose)](#4-运行方式推荐-docker-compose)
|
||||||

|
- [镜像版本](#镜像版本)
|
||||||

|
- [构建命令示例](#构建命令示例)
|
||||||
|
- [方式一:Docker 命令行](#方式一docker-命令行)
|
||||||
### 1.2 提取 `sec_user_id`(个人/指定博主)
|
- [方式二:Docker Compose 运行(推荐)](#方式二docker-compose-运行推荐)
|
||||||
- **个人 sec_user_id**(同步自己的收藏/喜欢用):
|
- [🚀 5. 软件截图](#-5-软件截图)
|
||||||
进入自己的抖音主页,浏览器地址栏中 `sec_user_id=` 后的字符串即为个人 ID(如 `https://www.douyin.com/user/sec_user_id=xxx`)。
|
- [🚀 5. 已有功能与计划](#-5-已有功能与计划)
|
||||||
- **博主 sec_user_id**(同步指定博主作品用):
|
|
||||||
1. 进入目标博主主页;
|
|
||||||
2. 方式1:直接复制地址栏中 `user/` 到 `?from_tab_name`中间部分内容即是博主的 `sec_user_id`;
|
|
||||||
3. 方式2:按 `F12` → 「Network」→ 任意请求 → 「Headers」→ 提取 `sec_user_id` 字段值。
|
|
||||||
|
|
||||||
> ⚠️ 风控提示:同步博主作品时,**慎用开启全量同步**(一次性下载过多易被抖音限制访问)。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 2. 路径映射规则(核心!错配会导致无法访问/数据丢失)
|
## 1. 获取抖音关键信息(必做!同步核心凭证)
|
||||||
|
|
||||||
|
Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取,避免遗漏或错误。
|
||||||
|
|
||||||
|
|
||||||
|
### 1.1 提取`Cookie`以及 `sec_uer_id`
|
||||||
|
1. 打开 **抖音网页版** (https://www.douyin.com/) 并登录目标账号;
|
||||||
|
2. 进入个人主页,按下F12进入开发者模式、并切换到 `网络`(也可能叫`network`).
|
||||||
|
3. 在筛选框中输入`/follow`.
|
||||||
|
4. 点击自己头像边上的 `关注` 按钮、会弹出你的关注列表,然后在右侧网络请求里面会出现多个请求,随便选一个.
|
||||||
|
4. 在请求的标签里面切换到`负载`(也可能叫`payload`)
|
||||||
|
5. 找到sec_user_id,复制值即可
|
||||||
|
6. 在请求的标签里面切换到`标头`(也可能叫`Headers`)
|
||||||
|
7. 往下拉,直到出现`Cookie` ,复制完整的值,注意前后不要带换行符,很多人会多复制个换行符出来.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### 1.2 提取 博主的`sec_user_id`以及博主的uid
|
||||||
|
- **对于想下载博主视频,但是又不想关注博主,需要用到**
|
||||||
|
- 1.进入博主主页,按`F12` 点击`Network` 或`网络` 筛选器里面填`/web/aweme/post` 然后切到`预览`或`preview` 展开 json数据结果 找到`aweme_list` 然后随便点开其中一个子项 即可找到`aweme_list[0].author.sec_uid` 这便是博主的uid 。后续在关注列表中,需要手动添加非关注博主同步视频时将会要用到。
|
||||||
|

|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 路径映射规则(核心!错配会导致无法访问/数据丢失)
|
||||||
为实现视频在 Emby/Jellyfin 中正常播放及数据持久化,需正确配置本地路径与容器路径的映射:
|
为实现视频在 Emby/Jellyfin 中正常播放及数据持久化,需正确配置本地路径与容器路径的映射:
|
||||||
|
|
||||||
| 存储类型 | 容器内路径 | 本地路径配置要求 | 用途说明 |
|
| 存储类型 | 容器内路径 | 本地路径配置要求 | 用途说明 |
|
||||||
|------------------|------------------|----------------------------------------------------------------------------------|------------------------------|
|
| ------------ | --------------- | --------------------------------------------------------------------- | -------------------------- |
|
||||||
| 个人收藏视频 | `/app/collect` | 映射到本地目录(如 NAS:`/volume1/抖音/收藏`、电脑:`D:/抖音/收藏`) | 存储同步后的收藏视频 |
|
| 个人收藏视频 | `/app/collect` | 映射到本地目录(如 NAS:`/volume1/抖音/收藏`、电脑:`D:/抖音/收藏`) | 存储同步后的收藏视频 |
|
||||||
| 个人喜欢视频 | `/app/favorite` | 映射到本地目录(建议与收藏视频路径区分,如 `/volume1/抖音/喜欢`) | 存储同步后的「我喜欢」视频 |
|
| 个人喜欢视频 | `/app/favorite` | 映射到本地目录(建议与收藏视频路径区分,如 `/volume1/抖音/喜欢`) | 存储同步后的「我喜欢」视频 |
|
||||||
| 指定博主视频 | `/app/uper` | 映射到本地目录(如 `/volume1/抖音/博主作品`) | 存储同步后的博主视频 |
|
| 图文视频 | `/app/images` | 映射到本地目录(建议与收藏视频路径区分,如 `/volume1/抖音/图文视频`) | 存储同步后的「图文视频」 |
|
||||||
| 数据库文件 | `/app/db` | 映射到本地稳定目录(如 `/volume1/抖音/工具数据库`) | 持久化配置、同步记录(防止容器删除后数据丢失) |
|
| 指定博主视频 | `/app/uper` | 映射到本地目录(如 `/volume1/抖音/博主作品`) | 存储同步后的博主视频 |
|
||||||
| 多账号视频(可选)| `/app/user1`/`/app/user2` 等 | 为每个账号分配独立本地路径(如 `/volume1/抖音/账号A`、`/volume1/抖音/账号B`) | 实现多账号隔离同步 |
|
| 指定博主视频 | `/app/mix` | 映射到本地目录(如 `/volume1/抖音/合集`) | 存储同步后的合集视频 |
|
||||||
|
| 指定博主视频 | `/app/series` | 映射到本地目录(如 `/volume1/抖音/短剧`) | 存储同步后的短剧视频 |
|
||||||
|
| 数据库文件 | `/app/db` | 映射到本地稳定目录(如 `/volume1/抖音/db`) | 持久化配置、同步记录 |
|
||||||
|
| 数据库文件 | `/app/mp3` | 映射到本地稳定目录(如 `/volume1/抖音/mp3`) | 图文视频自定义音频(用于原视频音频带版权的情况) |
|
||||||
|
|
||||||
> ✅ 关键注意:
|
> ✅ **关键注意**:
|
||||||
> 1. 本地路径需与后续后台「抖音授权」页面配置的「文件存储路径」**完全一致**;
|
> 1. 抖音授权时配置的路径为上面 表格里面 容器内路径 如 `/app/collect` 且注意 必须是`/`开头,路径可以自己随便命名,只要保证 授权里面配的路径有在docker-compose里面添加映射即可
|
||||||
> 2. 未配置路径映射时,文件仅存于容器内部,Emby/Jellyfin 无法访问,且容器删除后数据丢失。
|
> 2. 未配置路径映射时,文件仅存于容器内部,且容器删除后数据丢失。
|
||||||
|
|
||||||

|

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

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔑 3. 默认账号密码(首次登录用)
|
## 3. 默认账号密码(首次登录用)
|
||||||
首次访问后台管理页面时,使用以下默认账号密码:
|
首次访问后台管理页面时,使用以下默认账号密码:
|
||||||
- **用户名**:`douyin`
|
- **用户名**:`douyin`
|
||||||
- **密码**:`douyin2025`
|
- **密码**:`douyin2026`
|
||||||
|
|
||||||
> ⚠️ 安全建议:登录后修改密码。
|
> ⚠️ **安全建议**:登录后修改密码。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 4. 运行方式(推荐 Docker Compose,更易维护)
|
## 4. 运行方式(推荐 Docker Compose)
|
||||||
|
|
||||||
|
### 镜像版本
|
||||||
|
|
||||||
|
| 镜像标签 | 架构 |
|
||||||
|
| ----------------- | -------------- |
|
||||||
|
| `beta_2.2.0` | x86_64 (amd64) |
|
||||||
|
| `arm_2.2.0` | ARM64 |
|
||||||
|
| `latest` | x86_64 (amd64) |
|
||||||
|
|
||||||
|
|
||||||
|
### 构建命令示例
|
||||||
|
|
||||||
将下方命令中的「本地路径」替换为你的实际路径,终端执行即可:
|
将下方命令中的「本地路径」替换为你的实际路径,终端执行即可:
|
||||||
```bash
|
|
||||||
|
|
||||||
### Arm设备请使用 registry.cn-hangzhou.aliyuncs.com/jianzhichu/dysync.net:arm_1.0 版本镜像
|
|
||||||
### 方式一:Docker 命令行
|
|
||||||
docker run -d --restart=always \
|
|
||||||
-v /你的/本地/收藏视频路径:/app/collect \
|
|
||||||
-v /你的/本地/喜欢视频路径:/app/favorite \
|
|
||||||
-v /你的/本地/数据库路径:/app/db \
|
|
||||||
-v /你的/本地/博主视频路径:/app/uper \
|
|
||||||
-p 10101:10101 \
|
|
||||||
--name dysync2025 \
|
|
||||||
registry.cn-hangzhou.aliyuncs.com/jianzhichu/dysync.net
|
|
||||||
# 注意:-p 后面的容器端口必须为 10101(源码固定)
|
|
||||||
|
|
||||||
|
|
||||||
### 方式二:Docker Compose 运行(推荐)
|
### 方式一:Docker Compose 运行(推荐)
|
||||||
### Arm设备请使用 registry.cn-hangzhou.aliyuncs.com/jianzhichu/dysync.net:arm_1.0 版本镜像
|
|
||||||
创建 docker-compose.yml 文件,复制以下内容,替换「本地路径」后执行 docker-compose up -d:
|
创建 docker-compose.yml 文件,复制以下内容,替换「本地路径」后执行 docker-compose up -d:
|
||||||
|
|
||||||
version: '3.8'
|
<span style="color: red; font-weight: bold;">需要注意:unraid玩家需要增加 user配置 --user 1000:100</span>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
services:
|
services:
|
||||||
dysync:
|
dysync:
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/jianzhichu/dysync.net
|
image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_2.2.0
|
||||||
container_name: dysync2025
|
container_name: dysync2026 # 容器名称
|
||||||
restart: always
|
restart: unless-stopped # 始终重启容器,除非容器被手动停止或Docker服务停止
|
||||||
ports:
|
ports:
|
||||||
- "10101:10101"
|
- "10101:10101"
|
||||||
volumes:
|
volumes:
|
||||||
- /volume1/docker/dysync/db:/app/db
|
# 基础路径映射
|
||||||
|
- /vol2/1000/media/dysync/db:/app/db # 数据库目录(持久化配置和同步记录)
|
||||||
|
# 默认音频目录(用于图文/动态视频合成,版权受限音频的替代)
|
||||||
|
- /vol2/1000/media/dysync/mp3:/app/mp3
|
||||||
|
# 账号1 - 视频存储目录
|
||||||
|
- /vol2/1000/media/dysync/dy1/coll:/app/collect # 个人收藏视频
|
||||||
|
- /vol2/1000/media/dysync/dy1/fav:/app/favorite # 个人喜欢视频
|
||||||
|
- /vol2/1000/media/dysync/dy1/up:/app/uper # 指定博主视频
|
||||||
|
- /vol2/1000/media/dysync/dy1/mix:/app/mix # 合集(不需要可删除)
|
||||||
|
- /vol2/1000/media/dysync/dy1/series:/app/series # 短剧(不需要可删除)
|
||||||
|
|
||||||
|
# 账号2 - 多账号路径映射示例(可选:授权时可复用账号1路径)
|
||||||
|
- /vol2/1000/media/dysync/dy2/collect:/app/collect2
|
||||||
|
- /vol2/1000/media/dysync/dy2/fav:/app/favorite2
|
||||||
|
- /vol2/1000/media/dysync/dy2/up:/app/uper2
|
||||||
|
|
||||||
- /volume2/mediay/dysync:/app/collect
|
|
||||||
- /volume2/mediay/dysync_fav:/app/favorite
|
|
||||||
- /volume2/mediay/dysync_up:/app/uper
|
|
||||||
# 下面是多账号路径映射示例(可选)
|
|
||||||
- /volume2/mediay/dysync2:/app/yeyeye
|
|
||||||
- /volume2/mediay/dysync2_fav:/app/yeyeye_fav
|
|
||||||
- /volume2/mediay/dysync2_up:/app/yeyeye_up
|
|
||||||
# 配置DNS服务器(解决域名解析失败导致的无法访问外网)
|
|
||||||
network_mode: bridge
|
network_mode: bridge
|
||||||
dns:
|
dns:
|
||||||
- 8.8.8.8 # Google DNS
|
- 223.5.5.5 # 阿里云DNS(备用)
|
||||||
- 114.114.114.114 # 国内DNS
|
- 114.114.114.114 # 国内114 DNS(提升国内访问稳定性)
|
||||||
- 223.5.5.5
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 250m # 限制内存占用≤250MB(图文视频下载场景不建议设置)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
另一个版本去掉了所有视频存储路径设置项,docker只需要映射3个路径就可以,但是!!!
|
||||||
|
不兼容老的版本,如果觉得这种方式好一些可以重新部署。其他没什么变化。
|
||||||
|
所有路径都变成 根目录/cookie名字/视频类型/(如果是合集或者短剧这里还有一层)/合集或短剧/名称/视频文件
|
||||||
|
|
||||||
|
```
|
||||||
|
services:
|
||||||
|
dysync:
|
||||||
|
image: ccr.ccs.tencentyun.com/jianzhichu/dysync:latest
|
||||||
|
container_name: dysync_latest # 容器自定义名称
|
||||||
|
restart: unless-stopped # 容器非手动停止则自动重启
|
||||||
|
ports:
|
||||||
|
- "10110:10101" # 端口映射
|
||||||
|
environment:
|
||||||
|
# 限制GC堆最大内存为100MB(100*1024*1024=104857600,可根据需求调整)
|
||||||
|
- COMPlus_GCHeapHardLimit=154857600
|
||||||
|
# 禁用服务器GC,改用工作站GC(轻量应用更友好)
|
||||||
|
- COMPlus_GCServer=0
|
||||||
|
# 可选:强制GC回收后释放内存给操作系统(Linux专属)
|
||||||
|
- COMPlus_GCConserveMemory=1
|
||||||
|
# 可选:显示GC日志(排查内存问题时用)
|
||||||
|
# - COMPlus_GCLogFile=/app/gc.log
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# 基础路径映射 - 主账号(账号1)
|
||||||
|
- /vol2/1000/media/dy001/db:/app/db # 数据库目录(持久化配置和同步记录)
|
||||||
|
- /vol2/1000/media/dy001/mp3:/app/mp3 # 备用音频目录(版权音频无法下载时使用)
|
||||||
|
- /vol2/1000/media/dy001/:/app/data # 视频存储目录
|
||||||
|
|
||||||
|
|
||||||
|
network_mode: bridge
|
||||||
|
dns:
|
||||||
|
- 223.5.5.5 # 阿里云DNS(备用)
|
||||||
|
- 114.114.114.114 # 国内114 DNS(提升国内访问稳定性)
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 211m # 限制内存占用≤250MB(图文视频下载场景不建议设置)
|
||||||
|
```
|
||||||
|
|
||||||
## 🚀 5. 软件截图
|
## 🚀 5. 软件截图
|
||||||
|
|
||||||

|

|
||||||
@@ -125,8 +194,111 @@ services:
|
|||||||
|
|
||||||

|

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

|

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

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

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

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

|

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

|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/mobile-home.png" width="30%" />
|
||||||
|
<img src="docs/mobile-log.png" width="30%" />
|
||||||
|
<img src="docs/mobile-logdetail.jpg" width="30%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🚀 5. 已有功能与计划
|
||||||
|
|
||||||
|
|
||||||
|
1. ✅ 支持多账号同步(每个账号可单独配置存储路径)
|
||||||
|
|
||||||
|
2. ✅ 收藏的视频
|
||||||
|
|
||||||
|
3. ✅ 喜欢的视频(点赞的视频)
|
||||||
|
|
||||||
|
4. ✅ 图文视频(需要将图片 + mp3 合成视频)
|
||||||
|
|
||||||
|
5. ✅ 指定博主的视频,可配置是否单独存放一个总文件夹,是否直接用视频标题做文件名
|
||||||
|
|
||||||
|
6. ✅ 清除日志(防止容器被日志占用太多空间,可配置保留天数)
|
||||||
|
|
||||||
|
7. ✅ 将项目名称改成 "抖小云" 灵感来源于哪吒电影那坨云
|
||||||
|
|
||||||
|
8. ✅ Cookie 过期提醒,在DY授权页可查看
|
||||||
|
|
||||||
|
9. ✅ 自动根据去重规则进行去重(可设置去重优先级,同一个视频出现再多个分类时适用)
|
||||||
|
|
||||||
|
10. ✅ 在同步记录页面中直接播放视频
|
||||||
|
|
||||||
|
11. ✅ 关注列表同步。
|
||||||
|
|
||||||
|
11. ✅ 选中记录,批量重新同步。以及分享(比较简陋)
|
||||||
|
|
||||||
|
12. ✅ 关注列表支持新增,非关注的博主
|
||||||
|
|
||||||
|
13. ✅ 增加永久删除功能,删除后,以后不会再同步该视频
|
||||||
|
|
||||||
|
14. ✅ 增加配置项及关注列表(手动添加部分)导出导入功能
|
||||||
|
|
||||||
|
15. ✅ 支持移动端
|
||||||
|
|
||||||
|
16. ✅ 增加开关配置是否仅同步最近视频
|
||||||
|
|
||||||
|
17. ✅ 完成飞牛fpk打包:https://github.com/jianzhichu/FnDepot
|
||||||
|
|
||||||
|
18. ✅ 合集、短剧、自定义收藏夹下载。
|
||||||
|
|
||||||
|
19. ✅ 增加一个工具dy.cookie.exe在源码根目录,可以快速重置失效cookie,操作说明(一步一步操作即可)。
|
||||||
|
|
||||||
|
|
||||||
|
## 💖 支持项目
|
||||||
|
|
||||||
|
如果这个项目对你有帮助,欢迎通过以下方式支持:
|
||||||
|
|
||||||
|
### ⭐ Star 项目
|
||||||
|
点击右上角的 ⭐ Star 按钮,让更多人发现这个项目
|
||||||
|
|
||||||
|
### 💰 赞赏支持
|
||||||
|
|
||||||
|
- 扫码请作者喝杯奶茶 ☕
|
||||||
|
|
||||||
|
| 微信支付 | 支付宝 |
|
||||||
|
| :-------------------------------------: | :--------------------------------------: |
|
||||||
|
| <img src="docs/wx.jpg" width="300" /> | <img src="docs/zfb.jpg" width="300" /> |
|
||||||
|
|
||||||
|
### 🎁 其他支持方式
|
||||||
|
- 分享给更多需要的朋友
|
||||||
|
- 提交 Bug 报告和功能建议
|
||||||
|
- 贡献代码和文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<strong>感谢你的支持!❤️</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# 免责声明
|
||||||
|
|
||||||
|
本项目(dysync.net)仅作为技术研究与学习交流的开源项目,其所有代码、功能及相关内容均不代表任何第三方平台的官方服务或立场。使用本项目前,请仔细阅读以下条款:
|
||||||
|
|
||||||
|
## 1. 授权与使用限制
|
||||||
|
- 本项目未获得抖音(Douyin)等任何第三方平台的官方授权、合作或认可,与该等平台无任何关联。
|
||||||
|
- 项目功能仅允许用于个人技术研究和学习目的,严禁用于商业用途、非法活动或违反任何第三方平台规则的行为。
|
||||||
|
|
||||||
|
## 2. 知识产权说明
|
||||||
|
- 项目中涉及的第三方平台(如抖音)的名称、商标、接口、内容等均为其各自所有者的知识产权。
|
||||||
|
- 本项目仅对技术实现方式进行研究,不涉及对第三方知识产权的侵占或滥用。如因使用本项目引发知识产权争议,相关责任由使用者自行承担。
|
||||||
|
|
||||||
|
## 3. 风险提示与责任豁免
|
||||||
|
- 使用本项目可能导致违反第三方平台的用户协议或相关规定,进而导致账号限制、功能封禁等风险,此类风险由使用者自行承担。
|
||||||
|
- 项目开发者不对使用本项目产生的任何直接或间接损失(包括但不限于数据丢失、设备损坏、账号问题等)承担责任。
|
||||||
|
- 使用者应确保自身行为符合《网络安全法》《著作权法》等相关法律法规,不得利用本项目从事侵权、违法或违背公序良俗的活动。
|
||||||
|
|
||||||
|
## 4. 声明的修改与生效
|
||||||
|
- 本免责声明的最终解释权归项目开发者所有,开发者保留随时修改本声明的权利。
|
||||||
|
- 修改后的声明将在项目仓库中更新,建议使用者定期查阅。
|
||||||
|
- 一旦使用本项目的代码、功能或相关资源,即视为您已充分理解并同意本免责声明的全部条款。如不同意,请立即停止使用。
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
VITE_BASE_URL=/
|
VITE_BASE_URL=/
|
||||||
VITE_API_URL=http://localhost
|
VITE_API_URL=/
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
VITE_API_URL=http://localhost:10101
|
VITE_BASE_URL=/
|
||||||
|
VITE_API_URL=http://localhost:10101/
|
||||||
@@ -4,112 +4,140 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/logo.png" />
|
<link rel="icon" type="image/png" href="/logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport"
|
||||||
<title>抖音同步小帮手</title>
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<title>抖小云</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ######### 核心修改:区分 #stepin-app 的两个状态 ######### */
|
||||||
|
/* 1. 加载阶段:#stepin-app 仅包含加载动画(或为空),应用 flex 居中,保留原有样式 */
|
||||||
|
#stepin-app {
|
||||||
|
/* 加载阶段默认样式:flex 居中,保留呼吸圆环效果 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Vue 挂载后:#stepin-app 非空(包含 Vue 组件),覆盖为普通块级元素,解除约束 */
|
||||||
|
#stepin-app:not(:has(.breath-ring)) {
|
||||||
|
/* 核心:取消 flex 布局,改为普通块级元素 */
|
||||||
|
display: block !important;
|
||||||
|
/* 覆盖原有 flex 相关属性,解除居中约束 */
|
||||||
|
align-items: unset !important;
|
||||||
|
justify-content: unset !important;
|
||||||
|
/* 确保页面撑满横向宽度 */
|
||||||
|
width: 100% !important;
|
||||||
|
/* 保留高度,确保 Vue 页面正常占满视口 */
|
||||||
|
height: 100% !important;
|
||||||
|
/* 移除 gap,避免影响 Vue 页面布局 */
|
||||||
|
gap: 0 !important;
|
||||||
|
/* 恢复页面滚动功能,解除 overflow: hidden 的限制 */
|
||||||
|
overflow: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 核心呼吸圆环容器(原有样式保留,无修改) */
|
||||||
|
.breath-ring {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 圆环基础样式(原有样式保留,无修改) */
|
||||||
|
.breath-ring::before,
|
||||||
|
.breath-ring::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内层圆环:细圆环 + 渐变(原有样式保留,无修改) */
|
||||||
|
.breath-ring::before {
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-top-color: #1890ff;
|
||||||
|
border-right-color: #722ed1;
|
||||||
|
animation: ringRotate 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 外层圆环:呼吸扩散动画(原有样式保留,无修改) */
|
||||||
|
.breath-ring::after {
|
||||||
|
border: 2px solid #1890ff;
|
||||||
|
animation: ringBreath 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载文字:极简高级(原有样式保留,无修改) */
|
||||||
|
.loading-text {
|
||||||
|
color: #222222;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-tip {
|
||||||
|
color: #aaaaaa;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 圆环旋转动画(原有样式保留,无修改) */
|
||||||
|
@keyframes ringRotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 圆环呼吸扩散动画(原有样式保留,无修改) */
|
||||||
|
@keyframes ringBreath {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="stepin-app">
|
<div id="stepin-app">
|
||||||
<style>
|
<!-- <div class="loading-text">正在加载资源,请稍候...</div> -->
|
||||||
html {
|
<!-- 呼吸圆环加载组件 -->
|
||||||
overflow: hidden;
|
<div class="breath-ring"></div>
|
||||||
}
|
<!-- <div class="loading-tip">首次加载较慢,请耐心等待...</div> -->
|
||||||
|
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
<script>
|
<script>
|
||||||
if (!global) {
|
if (!global) {
|
||||||
@@ -118,11 +146,4 @@
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</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>
|
</html>
|
||||||
@@ -32,11 +32,13 @@
|
|||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"qs": "^6.10.1",
|
"qs": "^6.10.1",
|
||||||
|
"sortablejs": "^1.15.6",
|
||||||
"stepin": "2.1.42-beta",
|
"stepin": "2.1.42-beta",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-json-editor": "^1.4.3",
|
"vue-json-editor": "^1.4.3",
|
||||||
"vue-request": "^2.0.4",
|
"vue-request": "^2.0.4",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6",
|
||||||
|
"vue-sortablejs": "^1.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/container-queries": "^0.1.0",
|
"@tailwindcss/container-queries": "^0.1.0",
|
||||||
@@ -50,8 +52,9 @@
|
|||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-register": "^6.26.0",
|
"babel-register": "^6.26.0",
|
||||||
|
"baseline-browser-mapping": "^2.9.15",
|
||||||
"body-parser": "^1.20.1",
|
"body-parser": "^1.20.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.2.0",
|
||||||
"gh-pages": "^3.1.0",
|
"gh-pages": "^3.1.0",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-clean": "^0.4.0",
|
"gulp-clean": "^0.4.0",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ThemeProvider is-root v-bind="themeConfig" :apply-style="false">
|
<ThemeProvider is-root v-bind="themeConfig" :apply-style="false">
|
||||||
<stepin-view system-name="抖音小帮手" :class="`${contentClass}`" :user="user" :navMode="navigation" :useTabs="useTabs" :themeList="themeList" v-model:show-setting="showSetting" v-model:theme="theme" @themeSelect="configTheme" logo-src="@/assets/logo.png">
|
<stepin-view system-name="抖小云" :class="`${contentClass}`" :user="user" :navMode="navigation" :useTabs="useTabs" :themeList="themeList" v-model:show-setting="showSetting" v-model:theme="theme" @themeSelect="configTheme" logo-src="@/assets/logo.png" v-model:collapsed="open">
|
||||||
<template #headerActions>
|
<template #headerActions>
|
||||||
<HeaderActions @showSetting="showSetting = true" />
|
<HeaderActions @showSetting="showSetting = true" />
|
||||||
</template>
|
</template>
|
||||||
@@ -16,8 +16,6 @@
|
|||||||
</stepin-view>
|
</stepin-view>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
<my-personal ref="personalRef" />
|
<my-personal ref="personalRef" />
|
||||||
<email-set ref="emailRef" />
|
|
||||||
<!-- <login-modal :unless="['/login']" /> -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -28,7 +26,7 @@ import avatar from '@/assets/avatar.png';
|
|||||||
import { PageFooter, HeaderActions } from '@/components/layout';
|
import { PageFooter, HeaderActions } from '@/components/layout';
|
||||||
import Setting from './components/setting';
|
import Setting from './components/setting';
|
||||||
import { LoginModal } from '@/pages/login';
|
import { LoginModal } from '@/pages/login';
|
||||||
import { MyPersonal, EmailSet } from '@/pages/personal';
|
import { MyPersonal } from '@/pages/personal';
|
||||||
import { configTheme, themeList } from '@/theme';
|
import { configTheme, themeList } from '@/theme';
|
||||||
import { ThemeProvider } from 'stepin';
|
import { ThemeProvider } from 'stepin';
|
||||||
// logout,profile
|
// logout,profile
|
||||||
@@ -36,6 +34,7 @@ const { logout } = useAccountStore();
|
|||||||
const showPersonalDrawer = ref<boolean>(false);
|
const showPersonalDrawer = ref<boolean>(false);
|
||||||
const personalRef = ref(null);
|
const personalRef = ref(null);
|
||||||
const emailRef = ref(null);
|
const emailRef = ref(null);
|
||||||
|
const open = ref(false);
|
||||||
|
|
||||||
const showSetting = ref(false);
|
const showSetting = ref(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -82,19 +81,10 @@ const user = reactive({
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
useApiStore()
|
useApiStore()
|
||||||
.apiCheckInitStatus()
|
.apiUserInfo()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
// useApiStore()
|
user.name = res.data?.userName;
|
||||||
// .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;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -124,7 +114,11 @@ onMounted(() => {
|
|||||||
background: theme('backgroundColor.layout');
|
background: theme('backgroundColor.layout');
|
||||||
}
|
}
|
||||||
.system-name {
|
.system-name {
|
||||||
// color: #16213e !important;
|
color: #722ed1 !important;
|
||||||
|
margin-left: 0em !important;
|
||||||
|
}
|
||||||
|
.ant-tabs-extra-content {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +132,97 @@ body {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 小屏下 Modal / Drawer 全局滚动策略
|
||||||
|
// 挂载到 document.body 的元素 scoped 样式无法命中,此处非 scoped 全局生效
|
||||||
|
// 策略:让 wrap 承担滚动,不强制 body 内滚——这样鼠标在 modal 任意位置滚轮都能滚,避免 Mac overlay scrollbar 手感"滑不动"
|
||||||
|
.ant-modal-wrap {
|
||||||
|
overflow-y: auto !important; // wrap 是 fixed 定位滚动容器;antd 默认就是 auto,此处 !important 兜底
|
||||||
|
-webkit-overflow-scrolling: touch; // 移动端惯性滚动
|
||||||
|
|
||||||
|
// 强制显示滚动条(Mac overlay 默认隐藏,用户误以为"滑不动")
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border-radius: 5px;
|
||||||
|
&:hover { background: rgba(0, 0, 0, 0.55); }
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal {
|
||||||
|
top: 20px !important; // 覆盖 antd 通过 inline style 设的 top: 100px
|
||||||
|
padding-bottom: 20px !important;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// body 承担滚动:max-height 硬约束,内容超出时滚,内容少时自然收缩(modal 不会撑成一大块留白)
|
||||||
|
// 100vh - 150px = 减去 top(20) + margin-bottom(20) + header(约 55) + padding(约 40) + 保险 15
|
||||||
|
.ant-modal .ant-modal-body {
|
||||||
|
max-height: calc(100vh - 150px) !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border-radius: 5px;
|
||||||
|
&:hover { background: rgba(0, 0, 0, 0.55); }
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抖音授权 Cookie 编辑用 full-modal(全屏抽屉式,高度 100vh、footer 常驻底部)
|
||||||
|
.full-modal .ant-modal {
|
||||||
|
top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.full-modal .ant-modal-content {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.full-modal .ant-modal-header,
|
||||||
|
.full-modal .ant-modal-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.full-modal .ant-modal-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: none !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录页整个界面本身就是一个 Modal(LoginModal.vue),必须恢复 antd 默认布局,不能被上面的全局规则改
|
||||||
|
.login-modal .ant-modal {
|
||||||
|
top: 100px !important;
|
||||||
|
padding-bottom: 24px !important;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 忘记密码引导 Modal(LoginBox.vue 里的 forgot-modal)同样保留默认布局
|
||||||
|
.forgot-modal .ant-modal {
|
||||||
|
top: 100px !important;
|
||||||
|
padding-bottom: 24px !important;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawer 组件(抽屉)也保证内容可滚
|
||||||
|
.ant-drawer-body {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
.stepin-img-checkbox {
|
.stepin-img-checkbox {
|
||||||
@apply transition-transform;
|
@apply transition-transform;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="90px" height="90px" viewBox="0 0 90 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>logo-black</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g id="LOGO" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Artboard" transform="translate(-532.000000, -80.000000)">
|
||||||
|
<g id="logo-black" transform="translate(532.000000, 80.000000)">
|
||||||
|
<g id="Group">
|
||||||
|
<circle id="Combined-Shape" fill="#C71D23" cx="44.8544363" cy="44.8544363" r="44.8544363"></circle>
|
||||||
|
<path d="M67.558546,39.8714292 L42.0857966,39.8714292 C40.8627004,39.8720094 39.8710953,40.8633548 39.8701949,42.0864508 L39.8687448,47.623783 C39.867826,48.8471055 40.8592652,49.8390642 42.0825879,49.8393845 C42.0827874,49.8393846 42.0829869,49.8393846 42.0831864,49.8387862 L57.5909484,49.838657 C58.8142711,49.8386283 59.8059783,50.830319 59.8059885,52.0536417 C59.8059885,52.0536479 59.8059885,52.053654 59.8059701,52.0536602 L59.8059701,52.6073539 L59.8059701,52.6073539 L59.8059701,53.161115 C59.8059701,56.8310831 56.8308731,59.80618 53.160905,59.80618 L32.1165505,59.80618 C30.8934034,59.806119 29.9018373,58.8145802 29.9017425,57.5914331 L29.9011625,36.5491188 C29.9008781,32.8791508 32.8758931,29.9039718 36.5458611,29.9038706 C36.5459222,29.9038706 36.5459833,29.9038706 36.5460443,29.9040538 L67.5523638,29.9040538 C68.77515,29.9026795 69.7666266,28.9118177 69.7687593,27.6890325 L69.7721938,22.1516997 C69.774326,20.928378 68.7832423,19.9360642 67.5599198,19.9353054 C67.5594619,19.9353051 67.5590039,19.935305 67.558546,19.9366784 L36.5479677,19.9366784 C27.3730474,19.9366784 19.935305,27.3744208 19.935305,36.549341 L19.935305,67.558546 C19.935305,68.7818687 20.927004,69.7735676 22.1503267,69.7735676 L54.8224984,69.7735676 C63.079746,69.7735676 69.7735676,63.079746 69.7735676,54.8224984 L69.7735676,42.0864509 C69.7735676,40.8631282 68.7818687,39.8714292 67.558546,39.8714292 Z" id="G" fill="#FFFFFF"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
@@ -49,13 +49,13 @@ const navList = [
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ThemeProvider :color="{ middle: { 'bg-base': '#1896ff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true">
|
<ThemeProvider :color="{ middle: { 'bg-base': '#fff' }, primary: { DEFAULT: '#fff' } }" :autoAdapt="true">
|
||||||
<div class="front-view flex flex-col">
|
<div class="front-view flex flex-col">
|
||||||
<div class="text-text flex-1">
|
<div class="text-text flex-1">
|
||||||
<div class="front-header flex items-baseline py-md px-xl">
|
<div class="front-header flex items-baseline py-md px-xl">
|
||||||
<div class="text-xxl text-text hover:text-text">
|
<div class="text-xxl text-text hover:text-text">
|
||||||
<!-- <img src="@/assets/png.svg" /> -->
|
<!-- <img src="@/assets/png.svg" /> -->
|
||||||
<!-- 抖音同步小帮手 -->
|
<!-- 抖小云 -->
|
||||||
</div>
|
</div>
|
||||||
<!-- <div style="width: calc(100% - 430px)" class="front-navigation mx-xl flex overflow-hidden items-center text-lg overflow-ellipsis whitespace-nowrap">
|
<!-- <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">
|
<div :class="`front-nav-item flex items-center cursor-pointer mx-base ${nav.children ? 'with-list' : ''}`" v-for="nav in navList">
|
||||||
|
|||||||
@@ -1,45 +1,35 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { LogoutOutlined } from '@ant-design/icons-vue';
|
import { LogoutOutlined } from '@ant-design/icons-vue';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { ThemeProvider, alert } from 'stepin';
|
import { ThemeProvider, alert } from 'stepin';
|
||||||
import http from '@/store/http';
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ThemeProvider :color="{ middle: { 'bg-base': '#fff','bg-container':'#fff','bg-container-light':'#fff' }, primary: { DEFAULT: '#1896ff' } }" :autoAdapt="true">
|
<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="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;color:#722ed1;"> -->
|
<div class="front-content">
|
||||||
<!-- <img src="/logo.png" /> -->
|
|
||||||
<!-- 抖音同步小帮手 -->
|
|
||||||
<!-- </div> -->
|
|
||||||
<div class="front-content ">
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.front-view {
|
.front-view {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.front-content {
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto; // 防止mobile页面内容溢出
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原样式保留(无冲突)
|
||||||
.front-header {
|
.front-header {
|
||||||
.front-nav-item {
|
.front-nav-item {
|
||||||
&.with-list .front-nav-item-content {
|
&.with-list .front-nav-item-content {
|
||||||
@@ -55,11 +45,5 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.front-content {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
.front-view {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,85 +1,360 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive } from 'vue';
|
// 步骤2:声明父组件传递的 showSetting 事件(关键!)
|
||||||
|
const emits = defineEmits(['showSetting']);
|
||||||
|
import { ref, onMounted } from 'vue'; // 移除 h、VNode 导入
|
||||||
import { StepinHeaderAction } from 'stepin';
|
import { StepinHeaderAction } from 'stepin';
|
||||||
import Notice from '@/components/notice/Notice.vue';
|
|
||||||
import DayNightSwitch from '@/components/switch/DayNightSwitch.vue';
|
import DayNightSwitch from '@/components/switch/DayNightSwitch.vue';
|
||||||
import { BellOutlined } from '@ant-design/icons-vue';
|
import { TagsOutlined, CopyOutlined, GithubOutlined } from '@ant-design/icons-vue';
|
||||||
import Fullscreen from '../fullscreen/Fullscreen.vue';
|
import Fullscreen from '../fullscreen/Fullscreen.vue';
|
||||||
|
import { useApiStore } from '@/store';
|
||||||
|
import { message, Popover, Badge } from 'ant-design-vue'; // 新增:导入 Badge 组件用于气泡提示
|
||||||
|
|
||||||
defineEmits<{
|
// 版本数据和当前版本状态
|
||||||
(e: 'showSetting'): void;
|
const dyVersions = ref<string[]>([]);
|
||||||
}>();
|
const copyLoading = ref<Record<string, boolean>>({});
|
||||||
|
// 版本 popover 显隐控制
|
||||||
|
const versionPopoverVisible = ref<boolean>(false);
|
||||||
|
// 新增:是否有新版本(用于气泡提示)
|
||||||
|
const hasNewVersion = ref<boolean>(false);
|
||||||
|
// 新增:localStorage 缓存 key(统一命名,方便维护)
|
||||||
|
const VERSION_CACHE_KEY = 'dysync_version_cache';
|
||||||
|
|
||||||
const noticeList = reactive([
|
// 仓库地址常量
|
||||||
|
const gitRepos = ref([
|
||||||
{
|
{
|
||||||
title: '消息',
|
name: 'Gitee',
|
||||||
list: [
|
url: 'https://gitee.com/deathvicky/dysync.net',
|
||||||
{
|
color: '#FF6600',
|
||||||
title: '影佑',
|
},
|
||||||
content: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
{
|
||||||
img: 'src/assets/avatar/face-1.jpg',
|
name: 'GitHub',
|
||||||
time: 0,
|
url: 'https://github.com/jianzhichu/dysync.net',
|
||||||
},
|
color: '#6c35de',
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
const gitCopyLoading = ref<Record<string, boolean>>({});
|
||||||
|
const popoverVisible = ref<boolean>(false);
|
||||||
|
|
||||||
|
// 复制版本号方法(保留原有兼容逻辑,无改动)
|
||||||
|
const copyVersion = (version: string) => {
|
||||||
|
const pureVersion = version.replace('(当前版本)', '').replace('(最新版)', '').trim();
|
||||||
|
copyLoading.value[version] = true;
|
||||||
|
|
||||||
|
const doCopy = async () => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||||
|
await navigator.clipboard.writeText(pureVersion);
|
||||||
|
message.success(`已复制版本: ${pureVersion}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.style.position = 'absolute';
|
||||||
|
textarea.style.top = '-9999px';
|
||||||
|
textarea.style.left = '-9999px';
|
||||||
|
textarea.value = pureVersion;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
|
||||||
|
textarea.select();
|
||||||
|
const success = document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
message.success(`已复制版本: ${pureVersion}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('execCommand 复制失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.warning(`复制失败,请手动复制:${pureVersion}`);
|
||||||
|
console.warn('复制版本号失败:', error);
|
||||||
|
} finally {
|
||||||
|
copyLoading.value[version] = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
doCopy();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助方法:判断是否为当前版本(模板中使用)
|
||||||
|
const isCurrentVersion = (version: string) => {
|
||||||
|
return version.includes('(当前版本)');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助方法:判断是否为最新版本(模板中使用)
|
||||||
|
const isLatestVersion = (version: string) => {
|
||||||
|
return version.includes('(最新版)');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制仓库地址方法(保留原有逻辑,无改动)
|
||||||
|
const copyGitUrl = (repo: { name: string; url: string; color: string }) => {
|
||||||
|
gitCopyLoading.value[repo.url] = true;
|
||||||
|
const doCopy = async () => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||||
|
await navigator.clipboard.writeText(repo.url);
|
||||||
|
message.success(`已复制 ${repo.name} 地址`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.style.position = 'absolute';
|
||||||
|
textarea.style.top = '-9999px';
|
||||||
|
textarea.style.left = '-9999px';
|
||||||
|
textarea.value = repo.url;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
const success = document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
message.success(`已复制 ${repo.name} 地址`);
|
||||||
|
} else {
|
||||||
|
throw new Error('execCommand 复制失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.warning(`复制 ${repo.name} 地址失败,请手动复制`);
|
||||||
|
console.warn(`复制 ${repo.name} 地址失败:`, error);
|
||||||
|
} finally {
|
||||||
|
gitCopyLoading.value[repo.url] = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
doCopy();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:统一处理版本数据的方法(提取公共逻辑,避免冗余)
|
||||||
|
const handleVersionData = (rawData: string[]) => {
|
||||||
|
const newVersions = [...rawData];
|
||||||
|
const versionLen = newVersions.length;
|
||||||
|
|
||||||
|
if (versionLen > 0) {
|
||||||
|
if (versionLen === 1) {
|
||||||
|
newVersions[0] = `${newVersions[0]}(最新版)`;
|
||||||
|
hasNewVersion.value = false; // 只有一个版本,无新版
|
||||||
|
} else {
|
||||||
|
newVersions[0] = `${newVersions[0]}(当前版本)`;
|
||||||
|
const lastIndex = versionLen - 1;
|
||||||
|
newVersions[lastIndex] = `${newVersions[lastIndex]}(最新版)`;
|
||||||
|
// 判断是否有新版本:当前版本 !== 最新版本(去除备注后对比)
|
||||||
|
const currentVersion = newVersions[0].replace('(当前版本)', '').trim();
|
||||||
|
const latestVersion = newVersions[lastIndex].replace('(最新版)', '').trim();
|
||||||
|
hasNewVersion.value = currentVersion !== latestVersion;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasNewVersion.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newVersions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:保存版本数据到 localStorage
|
||||||
|
const saveVersionToLocalStorage = (versionData: string[]) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(VERSION_CACHE_KEY, JSON.stringify(versionData));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('版本数据缓存到 localStorage 失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:从 localStorage 获取缓存的版本数据
|
||||||
|
const getVersionFromLocalStorage = (): string[] => {
|
||||||
|
try {
|
||||||
|
const cacheData = localStorage.getItem(VERSION_CACHE_KEY);
|
||||||
|
if (cacheData) {
|
||||||
|
return JSON.parse(cacheData) as string[];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('从 localStorage 读取版本缓存失败:', error);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 改造:版本查看方法(获取数据后更新缓存,控制 popover 显隐)
|
||||||
|
const showVersionNotification = () => {
|
||||||
|
useApiStore()
|
||||||
|
.CheckTag()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
// 处理版本数据(添加备注、判断新版)
|
||||||
|
const processedVersions = handleVersionData(res.data);
|
||||||
|
dyVersions.value = processedVersions;
|
||||||
|
// 点击后更新 localStorage 缓存
|
||||||
|
saveVersionToLocalStorage(processedVersions);
|
||||||
|
// 打开版本 popover
|
||||||
|
versionPopoverVisible.value = true;
|
||||||
|
} else {
|
||||||
|
message.error(res.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
message.error('获取版本列表失败');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:页面加载时自动调用接口获取版本并缓存
|
||||||
|
const fetchVersionOnMount = () => {
|
||||||
|
useApiStore()
|
||||||
|
.CheckTag()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
// 处理版本数据(添加备注、判断新版)
|
||||||
|
const processedVersions = handleVersionData(res.data);
|
||||||
|
dyVersions.value = processedVersions;
|
||||||
|
// 页面加载时缓存到 localStorage
|
||||||
|
saveVersionToLocalStorage(processedVersions);
|
||||||
|
} else {
|
||||||
|
// 接口失败时,尝试读取本地缓存
|
||||||
|
const cacheVersions = getVersionFromLocalStorage();
|
||||||
|
if (cacheVersions.length > 0) {
|
||||||
|
dyVersions.value = handleVersionData(cacheVersions);
|
||||||
|
}
|
||||||
|
message.error(res.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// 接口异常时,尝试读取本地缓存
|
||||||
|
const cacheVersions = getVersionFromLocalStorage();
|
||||||
|
if (cacheVersions.length > 0) {
|
||||||
|
dyVersions.value = handleVersionData(cacheVersions);
|
||||||
|
}
|
||||||
|
message.error('获取版本列表失败,已加载本地缓存');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全局注入加载动画样式
|
||||||
|
onMounted(() => {
|
||||||
|
// 页面加载时自动调用版本接口
|
||||||
|
fetchVersionOnMount();
|
||||||
|
|
||||||
|
if (!document.querySelector('#custom-spin-style')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'custom-spin-style';
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes custom-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Git 弹窗控制方法
|
||||||
|
const showGit = () => {
|
||||||
|
popoverVisible.value = !popoverVisible.value;
|
||||||
|
console.log('打开开源地址弹窗');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- <StepinHeaderAction>
|
|
||||||
<a-input placeholder="开始搜索...">
|
|
||||||
<template #prefix>
|
|
||||||
<search-outlined />
|
|
||||||
</template>
|
|
||||||
</a-input>
|
|
||||||
</StepinHeaderAction> -->
|
|
||||||
<StepinHeaderAction>
|
<StepinHeaderAction>
|
||||||
<DayNightSwitch />
|
<DayNightSwitch />
|
||||||
</StepinHeaderAction>
|
</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> -->
|
<!-- 版本查看:改造为 Badge 包裹图标,实现气泡提示 -->
|
||||||
<!-- <StepinHeaderAction>
|
|
||||||
<a class="action-item" href="http://github.com/stepui/stepin-template" target="_blank">
|
|
||||||
<GithubOutlined />
|
|
||||||
</a>
|
|
||||||
</StepinHeaderAction>
|
|
||||||
<StepinHeaderAction>
|
<StepinHeaderAction>
|
||||||
<a class="action-item" href="http://gitee.com/stepui/stepin-template" target="_blank">
|
<div class="action-item">
|
||||||
<img class="gitee-logo" src="@/assets/gitee.svg" />
|
<a-popover v-model:visible="versionPopoverVisible" placement="bottom" trigger="click" overlay-class="version-popover-overlay" @visible-change="(visible) => versionPopoverVisible = visible">
|
||||||
</a>
|
<template #content>
|
||||||
</StepinHeaderAction> -->
|
<div class="version-popover-content">
|
||||||
<!-- <StepinHeaderAction>
|
<!-- 版本列表:纯模板 v-for 实现,替代 VNode 渲染 -->
|
||||||
<div class="action-item setting" @click="$emit('showSetting')">
|
<div class="custom-version-list" v-if="dyVersions.length > 0">
|
||||||
<SettingOutlined />
|
<div class="custom-version-item" :class="{
|
||||||
|
'custom-current-version': isCurrentVersion(version),
|
||||||
|
'custom-latest-version': isLatestVersion(version)
|
||||||
|
}" v-for="(version, index) in dyVersions" :key="index" :title="isCurrentVersion(version)
|
||||||
|
? '当前使用版本 - 点击右侧按钮复制版本号'
|
||||||
|
: isLatestVersion(version)
|
||||||
|
? '最新版本 - 点击右侧按钮复制版本号'
|
||||||
|
: '点击右侧按钮复制版本号'">
|
||||||
|
<!-- 版本文本 -->
|
||||||
|
<div class="version-text">
|
||||||
|
<span :style="{
|
||||||
|
color: isCurrentVersion(version) ? '#1890ff' : isLatestVersion(version) ? '#2e7d32' : '#1f2937'
|
||||||
|
}">
|
||||||
|
{{ version }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 复制按钮 -->
|
||||||
|
<button class="version-copy-btn" :disabled="copyLoading[version]" @click.stop="copyVersion(version)" :title="copyLoading[version] ? '复制中...' : '复制版本号'">
|
||||||
|
<CopyOutlined :style="{
|
||||||
|
fontSize: '14px',
|
||||||
|
color: copyLoading[version]
|
||||||
|
? '#d1d5db'
|
||||||
|
: isCurrentVersion(version)
|
||||||
|
? '#1890ff'
|
||||||
|
: isLatestVersion(version)
|
||||||
|
? '#2e7d32'
|
||||||
|
: '#9ca3af',
|
||||||
|
animation: copyLoading[version] ? 'custom-spin 1s linear infinite' : 'none'
|
||||||
|
}" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 暂无版本数据 -->
|
||||||
|
<div class="no-version-data" v-else>
|
||||||
|
暂无版本数据
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- <a-tooltip placement="bottom"> -->
|
||||||
|
<template #title>
|
||||||
|
<!-- <span>版本查看</span> -->
|
||||||
|
</template>
|
||||||
|
<!-- 改造:用 Badge 包裹图标,有新版时显示气泡提示 -->
|
||||||
|
<a-badge :dot="hasNewVersion">
|
||||||
|
<TagsOutlined class="action-icon" @click="showVersionNotification" />
|
||||||
|
</a-badge>
|
||||||
|
<!-- </a-tooltip> -->
|
||||||
|
</a-popover>
|
||||||
</div>
|
</div>
|
||||||
</StepinHeaderAction> -->
|
</StepinHeaderAction>
|
||||||
|
|
||||||
<!-- <a-popover placement="bottomRight">
|
<!-- Git 开源地址弹窗(保留原有实现) -->
|
||||||
<StepinHeaderAction>
|
<StepinHeaderAction>
|
||||||
<div class="action-item notice">
|
<div class="action-item">
|
||||||
<BellOutlined />
|
<a-popover v-model:visible="popoverVisible" placement="bottom" trigger="click" overlay-class="git-popover-overlay" @visible-change="(visible) => popoverVisible = visible">
|
||||||
</div>
|
<template #content>
|
||||||
</StepinHeaderAction>
|
<div class="git-popover-content">
|
||||||
<template #content>
|
<div class="git-repo-item" v-for="repo in gitRepos" :key="repo.url">
|
||||||
<Notice :data-source="noticeList" />
|
<div class="git-repo-name" :style="{ color: repo.color }">
|
||||||
</template>
|
<span class="git-repo-tag" :style="{ backgroundColor: repo.color }"></span>
|
||||||
</a-popover> -->
|
{{ repo.name }}
|
||||||
<!-- <StepinHeaderAction>
|
</div>
|
||||||
|
<div class="git-repo-url-wrapper">
|
||||||
|
<a :href="repo.url" target="_blank" class="git-repo-url" :style="{ color: repo.color }" title="点击打开仓库地址">
|
||||||
|
{{ repo.url }}
|
||||||
|
</a>
|
||||||
|
<button class="git-copy-btn" :disabled="gitCopyLoading[repo.url]" @click.stop="copyGitUrl(repo)" title="复制仓库地址">
|
||||||
|
<CopyOutlined :style="{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: gitCopyLoading[repo.url] ? '#d1d5db' : repo.color,
|
||||||
|
animation: gitCopyLoading[repo.url] ? 'custom-spin 1s linear infinite' : 'none'
|
||||||
|
}" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-tooltip placement="bottom">
|
||||||
|
<template #title>
|
||||||
|
<span>项目开源地址</span>
|
||||||
|
</template>
|
||||||
|
<GithubOutlined class="action-icon" @click="showGit" />
|
||||||
|
</a-tooltip>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
</StepinHeaderAction>
|
||||||
|
|
||||||
|
<StepinHeaderAction>
|
||||||
<Fullscreen class="-mx-xs -my-sm h-[56px] px-xs py-sm flex items-center" target=".stepin-layout" />
|
<Fullscreen class="-mx-xs -my-sm h-[56px] px-xs py-sm flex items-center" target=".stepin-layout" />
|
||||||
</StepinHeaderAction> -->
|
</StepinHeaderAction>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.gitee-logo {
|
/* 顶部按钮样式 */
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
.action-item {
|
.action-item {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -88,13 +363,225 @@ const noticeList = reactive([
|
|||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&.setting {
|
&:hover {
|
||||||
font-size: 18px;
|
color: #1890ff;
|
||||||
}
|
|
||||||
|
|
||||||
&.notice {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 版本 Popover 样式(纯模板适配) */
|
||||||
|
:deep(.version-popover-overlay) {
|
||||||
|
.ant-popover-inner {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-arrow-content {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-popover-content {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 版本列表(模板对应样式) */
|
||||||
|
.custom-version-list {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-version-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5fafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当前版本/最新版本高亮 */
|
||||||
|
.custom-current-version {
|
||||||
|
background-color: rgba(24, 144, 255, 0.05) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(24, 144, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-latest-version {
|
||||||
|
background-color: rgba(46, 125, 50, 0.05) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(46, 125, 50, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 版本文本容器 */
|
||||||
|
.version-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: calc(100% - 40px); /* 修正:原 maxwidth 拼写错误 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 版本复制按钮 */
|
||||||
|
.version-copy-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(24, 144, 255, 0.1);
|
||||||
|
color: #1890ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暂无版本数据 */
|
||||||
|
.no-version-data {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 20px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Git Popover 样式 */
|
||||||
|
:deep(.git-popover-overlay) {
|
||||||
|
.ant-popover-inner {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-arrow-content {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-popover-content {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-repo-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-repo-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-repo-tag {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-repo-url-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-repo-url {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-copy-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 根容器样式 */
|
||||||
|
.header-actions-root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,41 +1,137 @@
|
|||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup>
|
||||||
|
import { GithubOutlined, CopyrightOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useApiStore } from '@/store';
|
||||||
|
|
||||||
|
// 响应式变量存储年份文本
|
||||||
|
const yearText = ref('');
|
||||||
|
const currentVersion = ref(); // 已存在的版本号响应式变量
|
||||||
|
onMounted(() => {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const startYear = 2025;
|
||||||
|
|
||||||
|
if (currentYear <= startYear) {
|
||||||
|
yearText.value = startYear.toString();
|
||||||
|
} else {
|
||||||
|
yearText.value = `${startYear}-${currentYear}`;
|
||||||
|
}
|
||||||
|
useApiStore()
|
||||||
|
.getVer()
|
||||||
|
.then((r) => {
|
||||||
|
if (r.code == 0) {
|
||||||
|
currentVersion.value = r.data; // 接口返回后赋值版本号
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-footer">
|
<footer class="page-footer">
|
||||||
<div class="links">
|
<div class="footer-content">
|
||||||
<!-- <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">
|
<div class="copyright">
|
||||||
<GithubOutlined />
|
<CopyrightOutlined class="icon-copyright" />
|
||||||
</a> -->
|
<span>{{ yearText }} 抖小云 dysync.net. </span>
|
||||||
<!-- <a class="link" href="https://www.antdv.com/docs/vue/introduce-cn/" target="_blank"> Ant Design </a> -->
|
|
||||||
|
<!-- 将社交链接移动到这里 -->
|
||||||
|
<!-- <div class="social-links">
|
||||||
|
<a href="https://github.com/jianzhichu/dysync.net" target="_blank" rel="noopener noreferrer" aria-label="访问我们的 GitHub 仓库" class="social-link">
|
||||||
|
<GithubOutlined />
|
||||||
|
</a>
|
||||||
|
<a href="https://gitee.com/deathvicky/dysync.net" target="_blank" rel="noopener noreferrer" aria-label="访问我们的 Gitee 仓库" class="social-link">
|
||||||
|
<img class="gitee-icon" src="@/assets/gitee.svg" alt="Gitee" />
|
||||||
|
</a>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- 新增:显示当前版本号,增加样式分隔提升美观度 -->
|
||||||
|
<span class="version-text" v-if="currentVersion">{{currentVersion.deploy}}_{{ currentVersion.tag }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copyright">
|
</footer>
|
||||||
Copyright
|
|
||||||
<CopyrightOutlined class="icon-copyright" />
|
|
||||||
2023 dy.net
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.page-footer {
|
.page-footer {
|
||||||
text-align: center;
|
// 使用 :root 变量,便于主题切换
|
||||||
@apply text-gray-400;
|
--footer-text-color: var(--text-color-secondary, #999);
|
||||||
|
--footer-hover-color: var(--primary-color, #1890ff);
|
||||||
|
--version-text-color: var(--text-color-tertiary, #666);
|
||||||
|
|
||||||
.links {
|
width: 100%;
|
||||||
|
color: var(--footer-text-color);
|
||||||
|
padding: 24px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 版权信息现在是一个父 flex 容器
|
||||||
|
.copyright {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
// 增大 gap 以分隔文字和社交链接组
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
.version-text {
|
||||||
|
// 核心修改1:白色文字(醒目,与绿底形成强烈对比)
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 2px 6px; // 微调内边距,让绿底区域更饱满,视觉更舒适
|
||||||
|
// 核心修改2:柔和绿色背景(两种可选方案,不刺眼、显高级)
|
||||||
|
// 方案1:推荐!清新柔和绿(适配大多数页面,不张扬,高级感足)
|
||||||
|
background-color: #722ed1;
|
||||||
|
// 方案2:稍浅薄荷绿(更明亮,醒目度稍高,适合活泼风格页面)
|
||||||
|
// background-color: #73d13d;
|
||||||
|
border-radius: 4px; // 适当增大圆角,更显圆润柔和
|
||||||
|
// 核心修改3:调整光效为白色光晕(匹配白字,提升质感,不突兀)
|
||||||
|
// text-shadow: 0 0 3px rgba(255, 255, 255, 0.8), 0 0 6px rgba(255, 255, 255, 0.4);
|
||||||
|
// 可选优化:轻微暗角阴影,让绿底更有层次感,不扁平
|
||||||
|
// box-shadow: 0 1px 2px rgba(82, 196, 26, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
// 社交链接组自身也是一个 flex 容器
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
// 为两个图标之间设置间距
|
||||||
|
gap: 16px;
|
||||||
|
// 移除了原来的 margin-bottom
|
||||||
|
|
||||||
.link {
|
.social-link {
|
||||||
@apply hover:text-gray-400 pl-4 pr-4;
|
color: inherit;
|
||||||
|
transition: color 0.3s ease, transform 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
opacity: 0.8;
|
||||||
|
align-items: center;
|
||||||
|
// 移除了图标和文字的 gap,因为现在没有文字了
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--footer-hover-color);
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg,
|
||||||
|
img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyright {
|
.gitee-icon {
|
||||||
margin-top: 8px;
|
width: 14px !important;
|
||||||
|
height: 14px !important;
|
||||||
.icon-copyright {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<!-- components/VersionDrawer.vue -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, onMounted } from 'vue';
|
||||||
|
import { Drawer } from 'ant-design-vue';
|
||||||
|
import { CloseOutlined, CopyOutlined, CheckCircleOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { useApiStore } from '@/store';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
// ✅ 移除 defineProps/defineEmits 导入,直接使用
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
currentVersion?: string; // 接收当前版本号
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 组件挂载时获取配置
|
||||||
|
onMounted(() => {});
|
||||||
|
|
||||||
|
const getVersions = () => {
|
||||||
|
useApiStore()
|
||||||
|
.CheckTag()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code == 1) {
|
||||||
|
dyVersions.value = res.data;
|
||||||
|
} else {
|
||||||
|
message.error(res.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 版本数据
|
||||||
|
const dyVersions = ref<string[]>([]);
|
||||||
|
|
||||||
|
// 关闭抽屉
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制版本号
|
||||||
|
const copyVersion = (version: string) => {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(version)
|
||||||
|
.then(() => {
|
||||||
|
message.success(`已复制版本: ${version}`);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('复制失败,请手动复制');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调试:监听 visible 变化,确认父组件传递的状态是否正确
|
||||||
|
console.log('抽屉初始状态:', props.visible);
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
console.log('抽屉 visible 变化:', val);
|
||||||
|
if (val) {
|
||||||
|
getVersions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 强制添加样式,确保不被隐藏 -->
|
||||||
|
<a-drawer title="版本列表" placement="right" :visible="props.visible" @close="handleClose" :width="450" :mask="true" :mask-closable="true" :z-index="9999" class="version-drawer">
|
||||||
|
<div class="version-list">
|
||||||
|
<div v-for="(tag, index) in dyVersions" :key="index" class="version-item">
|
||||||
|
<div class="version-info">
|
||||||
|
{{ tag }}
|
||||||
|
<!-- 当前版本标记 -->
|
||||||
|
<span v-if="props.currentVersion === tag" class="current-version-tag">
|
||||||
|
<CheckCircleOutlined class="current-icon" />
|
||||||
|
当前版本
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 复制按钮 -->
|
||||||
|
<a-tooltip title="复制版本号" placement="top">
|
||||||
|
<div class="copy-btn" @click.stop="copyVersion(tag)">
|
||||||
|
<CopyOutlined />
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-if="dyVersions.length === 0" class="empty-tip">暂无版本数据</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="close-btn" @click="handleClose">
|
||||||
|
<CloseOutlined /> 关闭版本列表
|
||||||
|
</div> -->
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
/* 强制显示抽屉容器,避免被全局样式隐藏 */
|
||||||
|
.version-drawer {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-drawer-content-wrapper) {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
z-index: 9999 !important; /* 确保层级最高 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-list {
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-item {
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当前版本标记样式 */
|
||||||
|
.current-version-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #1890ff;
|
||||||
|
background: #e6f7ff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 复制按钮样式 */
|
||||||
|
.copy-btn {
|
||||||
|
color: #999;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
background: #f0f9ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整tooltip样式,避免被遮挡 */
|
||||||
|
:deep(.ant-tooltip) {
|
||||||
|
z-index: 10000 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,7 +9,6 @@ export type Type = 'day' | 'night';
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: { type: String as PropType<Type> },
|
value: { type: String as PropType<Type> },
|
||||||
// nightColor: { type: String, default: '#1D1D1D' },
|
|
||||||
nightColor: { type: String, default: '#1a1a2e' },
|
nightColor: { type: String, default: '#1a1a2e' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -17,37 +16,34 @@ const emit = defineEmits<{
|
|||||||
(e: 'update:value', value: Type): void;
|
(e: 'update:value', value: Type): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'theme-mode';
|
||||||
|
|
||||||
|
const getInitialValue = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const cachedValue = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (cachedValue === 'day' || cachedValue === 'night') {
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props.value || 'day';
|
||||||
|
};
|
||||||
|
|
||||||
const { value: _value } = useModelValue(
|
const { value: _value } = useModelValue(
|
||||||
() => props.value,
|
() => props.value,
|
||||||
(val) => emit('update:value', val),
|
(val) => emit('update:value', val),
|
||||||
'day'
|
getInitialValue()
|
||||||
);
|
);
|
||||||
|
|
||||||
const switcher: { [key in Type]: Type } = {
|
const switcher: { [key in Type]: Type } = {
|
||||||
day: 'night',
|
day: 'night',
|
||||||
night: 'day',
|
night: 'day',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { theme } = storeToRefs(useThemeStore());
|
const { theme } = storeToRefs(useThemeStore());
|
||||||
|
let cachedMiddleColors = {};
|
||||||
|
|
||||||
// 监听主题色变换,更新缓存
|
// --- 修正点 1: 函数定义移到 watch 之前 ---
|
||||||
let cachedMiddleColors = cloneDeep(theme.value.color.middle);
|
const updateHtmlClass = (mode: Type) => {
|
||||||
watch(
|
|
||||||
theme,
|
|
||||||
(val) => {
|
|
||||||
if (val.color.middle['bg-base'] !== props.nightColor) {
|
|
||||||
cachedMiddleColors = cloneDeep(val.color.middle);
|
|
||||||
_value.value = 'day';
|
|
||||||
} else {
|
|
||||||
_value.value = 'night';
|
|
||||||
}
|
|
||||||
console.log('theme', _value.value);
|
|
||||||
updateHtmlClass(_value.value);
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 在你的主题store中添加切换逻辑
|
|
||||||
const updateHtmlClass = (mode) => {
|
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
if (mode === 'night') {
|
if (mode === 'night') {
|
||||||
html.classList.add('dark-mode');
|
html.classList.add('dark-mode');
|
||||||
@@ -55,14 +51,46 @@ const updateHtmlClass = (mode) => {
|
|||||||
html.classList.remove('dark-mode');
|
html.classList.remove('dark-mode');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 主题颜色配置
|
|
||||||
|
// 监听 theme bg-base 变化,用于同步外部修改
|
||||||
|
watch(
|
||||||
|
() => theme.value?.color?.middle?.['bg-base'],
|
||||||
|
(newBgColor) => {
|
||||||
|
if (newBgColor) {
|
||||||
|
if (newBgColor === props.nightColor) {
|
||||||
|
_value.value = 'night';
|
||||||
|
} else {
|
||||||
|
_value.value = 'day';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 修正点 2: watch 现在可以安全地调用 updateHtmlClass ---
|
||||||
|
watch(
|
||||||
|
_value,
|
||||||
|
(newMode) => {
|
||||||
|
// console.log('mode switched to:', newMode);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(STORAGE_KEY, newMode);
|
||||||
|
}
|
||||||
|
updateHtmlClass(newMode);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 修正点 3: 惰性缓存白天模式颜色 ---
|
||||||
const colorCfg = computed(() => {
|
const colorCfg = computed(() => {
|
||||||
if (_value.value === 'day') {
|
if (_value.value === 'day') {
|
||||||
|
if (Object.keys(cachedMiddleColors).length === 0) {
|
||||||
|
cachedMiddleColors = cloneDeep(theme.value?.color?.middle || {});
|
||||||
|
}
|
||||||
return { middle: cachedMiddleColors };
|
return { middle: cachedMiddleColors };
|
||||||
}
|
}
|
||||||
return { middle: { 'bg-base': props.nightColor } };
|
return { middle: { 'bg-base': props.nightColor } };
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ThemeProvider is-root :color="colorCfg">
|
<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 @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">
|
||||||
@@ -72,6 +100,7 @@ const colorCfg = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.day-night-switch {
|
.day-night-switch {
|
||||||
.spot {
|
.spot {
|
||||||
@@ -91,4 +120,4 @@ const colorCfg = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -125,7 +125,7 @@ const loadVideoStream = async (videoId: string) => {
|
|||||||
|
|
||||||
// 构建视频流 URL,浏览器会自动发送 Range 请求
|
// 构建视频流 URL,浏览器会自动发送 Range 请求
|
||||||
// 添加时间戳避免缓存问题
|
// 添加时间戳避免缓存问题
|
||||||
const url = `/api/video/stream/${videoId}?t=${Date.now()}`;
|
const url = `/api/Video/play/${videoId}?t=${Date.now()}`;
|
||||||
console.log('[VideoPlayer] 设置视频流 URL:', url);
|
console.log('[VideoPlayer] 设置视频流 URL:', url);
|
||||||
|
|
||||||
// 直接使用 URL,让浏览器处理 Range 请求
|
// 直接使用 URL,让浏览器处理 Range 请求
|
||||||
@@ -555,11 +555,12 @@ defineExpose({
|
|||||||
.video-element {
|
.video-element {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 70vh;
|
max-height: 45vh;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -578,3 +579,15 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
// 长视频描述作为标题时限 2 行显示,避免撑爆 modal header
|
||||||
|
.video-player-modal .modal-title-text {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,431 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { reactive, ref, watch, UnwrapRef, onMounted } from 'vue';
|
||||||
|
import { FormInstance } from 'ant-design-vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { useApiStore, useAccountStore } from '@/store';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { ExclamationCircleOutlined, SyncOutlined, SaveOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
interface UpSecUserIdItem {
|
||||||
|
uper?: string;
|
||||||
|
uid?: string;
|
||||||
|
syncAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigItem = {
|
||||||
|
id?: string;
|
||||||
|
userName: string; // 改为非可选,强制初始化
|
||||||
|
cookies: string;
|
||||||
|
savePath: string;
|
||||||
|
favSavePath: string;
|
||||||
|
secUserId: string;
|
||||||
|
status: number;
|
||||||
|
upSavePath: string;
|
||||||
|
// imgSavePath: string;
|
||||||
|
useSinglePath: boolean; // 非可选
|
||||||
|
};
|
||||||
|
|
||||||
|
// 强制初始化所有字段为非undefined值
|
||||||
|
const newConfig = (config?: ConfigItem): ConfigItem => {
|
||||||
|
return {
|
||||||
|
id: '0',
|
||||||
|
userName: '', // 强制空字符串
|
||||||
|
cookies: '',
|
||||||
|
savePath: '',
|
||||||
|
favSavePath: '',
|
||||||
|
secUserId: '',
|
||||||
|
status: 0,
|
||||||
|
upSavePath: '',
|
||||||
|
// imgSavePath: '',
|
||||||
|
useSinglePath: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 改用 ref 而非 reactive(解决深层响应式问题)
|
||||||
|
const form = ref<ConfigItem>(newConfig());
|
||||||
|
const formModel = ref<FormInstance>();
|
||||||
|
const formLoading = ref(false);
|
||||||
|
|
||||||
|
// 重置表单:强制重置 + 清空校验状态
|
||||||
|
const resetConfig = () => {
|
||||||
|
form.value = newConfig();
|
||||||
|
formModel.value?.resetFields(); // 必须调用,清空校验提示
|
||||||
|
message.info('表单已重置为默认值');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听统一路径(改用 ref 的取值方式)
|
||||||
|
watch(
|
||||||
|
[() => form.value.savePath, () => form.value.useSinglePath],
|
||||||
|
([newSavePath, useSinglePath]) => {
|
||||||
|
if (useSinglePath && newSavePath) {
|
||||||
|
form.value.favSavePath = newSavePath;
|
||||||
|
form.value.upSavePath = newSavePath;
|
||||||
|
// form.value.imgSavePath = newSavePath;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 校验规则:明确trigger + 不依赖组件自动触发
|
||||||
|
const formRules = reactive({
|
||||||
|
userName: [{ required: true, message: '请输入Cookie名称', trigger: 'change' }],
|
||||||
|
savePath: [{ required: true, message: '请输入收藏存储路径', trigger: 'change' }],
|
||||||
|
secUserId: [{ required: false, message: '请输入我的secUserId', trigger: 'change' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
// 定义rowCount并暴露到模板(之前定义在onMounted里,模板访问不到)
|
||||||
|
const rowCount = ref(6);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const accountStore = useAccountStore(); // 移到内部避免提前引用
|
||||||
|
useApiStore()
|
||||||
|
.AppisInit()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code == 0 && res.data) {
|
||||||
|
if (accountStore.logged) {
|
||||||
|
router.push('/dashboard');
|
||||||
|
} else {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
|
||||||
|
// 提交:手动指定校验字段 + 强制取值
|
||||||
|
const submitConfig = async () => {
|
||||||
|
formLoading.value = true;
|
||||||
|
try {
|
||||||
|
// 手动获取表单实例,防止null
|
||||||
|
const formInstance = formModel.value;
|
||||||
|
if (!formInstance) {
|
||||||
|
message.error('表单实例未初始化');
|
||||||
|
formLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动校验(兜底方案)
|
||||||
|
const checkResult = manualCheckForm();
|
||||||
|
if (!checkResult.pass) {
|
||||||
|
message.error(checkResult.msg);
|
||||||
|
formLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 提交时强制解构,确保传递最新值
|
||||||
|
const res = await useApiStore().DeskInitAsync({ ...form.value });
|
||||||
|
if (res.code === 0) {
|
||||||
|
message.success('配置保存成功,即将跳转登录页...');
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/login');
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
message.error(`保存失败:${res.message || '未知错误'}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('校验失败:', err);
|
||||||
|
const firstErr = err.errorFields?.[0]?.errors?.[0] || '请检查所有必填项';
|
||||||
|
message.error(firstErr);
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手动校验函数(兜底方案)
|
||||||
|
const manualCheckForm = (): { pass: boolean; msg: string } => {
|
||||||
|
if (!form.value.userName.trim()) {
|
||||||
|
return { pass: false, msg: '请输入Cookie名称' };
|
||||||
|
}
|
||||||
|
if (!form.value.savePath.trim()) {
|
||||||
|
return { pass: false, msg: '请输入收藏存储路径' };
|
||||||
|
}
|
||||||
|
return { pass: true, msg: '' };
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="init-config-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="/logo.png" alt="抖小云Logo" class="logo-img" />
|
||||||
|
</div>
|
||||||
|
<h1 class="page-title">抖小云初始化配置信息</h1>
|
||||||
|
<!-- <p class="page-desc">第一次运行,请花两分钟完成初始配置</p> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-card">
|
||||||
|
<a-form ref="formModel" :model="form" :rules="formRules" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" layout="horizontal" class="config-form" @submit.prevent="submitConfig">
|
||||||
|
|
||||||
|
<!-- Cookie名称 -->
|
||||||
|
<a-form-item label="Cookie名" name="userName">
|
||||||
|
<a-input v-model:value="form.userName" placeholder="随便填个名称,作为唯一标识" @input="() => {}" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- Cookie值:现在rows能正常生效了 -->
|
||||||
|
<a-form-item label="Cookie值" name="cookies">
|
||||||
|
<div class="cookie-wrapper">
|
||||||
|
<a-textarea v-model:value="form.cookies" :rows="rowCount" placeholder="粘贴完整Cookie值,也可以先不填,保存配置后,后续可以在“抖音授权”修改" />
|
||||||
|
<div class="cookie-tip">
|
||||||
|
<a href="https://gitee.com/deathvicky/dysync.net" target="_blank" class="cookie-link">
|
||||||
|
查看Cookie以及secUserId获取方式
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- secUserId -->
|
||||||
|
<a-form-item label="我的secUserId" name="secUserId">
|
||||||
|
<div class="input-with-tip">
|
||||||
|
<a-input v-model:value="form.secUserId" placeholder="同步喜欢视频和关注博主视频必填" @input="() => {}" />
|
||||||
|
<a-tooltip title="同步喜欢视频和关注博主视频必填">
|
||||||
|
<ExclamationCircleOutlined style="color: #faad14; margin-left: 8px; font-size: 16px;" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 收藏路径 -->
|
||||||
|
<a-form-item label="收藏存储路径" name="savePath">
|
||||||
|
<div class="input-with-tip">
|
||||||
|
<a-input v-model:value="form.savePath" placeholder="/app/collect" @input="() => {}" />
|
||||||
|
<a-tooltip title="必填">
|
||||||
|
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px; margin-left: 8px; " />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 统一路径开关 -->
|
||||||
|
<a-form-item label="统一存储路径" name="useSinglePath">
|
||||||
|
<div class="switch-with-desc">
|
||||||
|
<a-switch v-model:checked="form.useSinglePath" class="custom-large-switch" @change="() => {}" />
|
||||||
|
<div class="switch-desc">
|
||||||
|
<span class="main-desc">{{ form.useSinglePath ? '开启' : '关闭' }}</span>
|
||||||
|
<span class="sub-desc">{{ form.useSinglePath ? '所有类型视频(收藏的、喜欢的、关注博主的)共用一个路径' : '各类型视频(收藏的、喜欢的、关注博主的)文件路径独立配置' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- 其他路径 -->
|
||||||
|
<a-form-item v-if="!form.useSinglePath" label="喜欢存储路径" name="favSavePath">
|
||||||
|
<a-input v-model:value="form.favSavePath" placeholder="喜欢视频存储路径,不想同步就空着,后续可以在“抖音授权”修改" @input="() => {}" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item v-if="!form.useSinglePath" label="关注存储路径" name="upSavePath">
|
||||||
|
<a-input v-model:value="form.upSavePath" placeholder="关注视频存储路径,不想同步就空着,后续可以在“抖音授权”修改" @input="() => {}" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- <a-form-item v-if="!form.useSinglePath" label="图文存储路径" name="imgSavePath">
|
||||||
|
<a-input v-model:value="form.imgSavePath" placeholder="图文视频存储路径,不想同步就空着,后续可以在“抖音授权”修改" @input="() => {}" />
|
||||||
|
</a-form-item> -->
|
||||||
|
|
||||||
|
<!-- 同步状态开关 -->
|
||||||
|
<a-form-item label="同步状态" name="status">
|
||||||
|
<div class="switch-with-desc">
|
||||||
|
<a-switch v-model:checked="form.status" :checked-value="1" :un-checked-value="0" class="custom-large-switch" />
|
||||||
|
<div class="switch-desc">
|
||||||
|
<span class="main-desc">{{ form.status === 1 ? '开启' : '关闭' }}</span>
|
||||||
|
<span class="sub-desc">建议初始不开启,保存设置后,进入系统配置完成其他设置后再手动开启!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<a-form-item :wrapper-col="{ span: 24 }" class="form-actions">
|
||||||
|
<div class="btn-center-wrapper">
|
||||||
|
<a-button type="primary" html-type="submit" :loading="formLoading" class="save-btn">
|
||||||
|
<template #icon>
|
||||||
|
<SaveOutlined />
|
||||||
|
</template>保存配置
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.init-config-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 10px 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.logo-container {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.logo-img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
.page-desc {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.config-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.config-form {
|
||||||
|
--ant-form-item-margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.input-with-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.switch-with-desc {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.switch-desc {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.switch-desc .main-desc {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.switch-desc .sub-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
:deep(.custom-large-switch) {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 30px !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
.btn-center-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
padding: 0 40px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
.save-btn:hover {
|
||||||
|
background-color: #4096ff;
|
||||||
|
border-color: #4096ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本域恢复默认,让rows生效 */
|
||||||
|
:deep(.ant-input-textarea) {
|
||||||
|
font-size: 15px;
|
||||||
|
height: auto !important; /* 取消固定高度 */
|
||||||
|
min-height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cookie容器和链接样式 */
|
||||||
|
.cookie-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.cookie-tip {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.cookie-link {
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.cookie-link:hover {
|
||||||
|
color: #4096ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.init-config-page {
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
.config-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.logo-img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.switch-with-desc {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.switch-desc {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
:deep(.custom-large-switch) {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
:deep(.ant-input-textarea-input) {
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
|
||||||
|
}
|
||||||
|
:deep(.ant-input-textarea-input)::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
:deep(.ant-input-textarea-input)::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(150, 150, 150, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
:deep(.ant-input-disabled) {
|
||||||
|
background-color: #f9fafb !important;
|
||||||
|
color: #6b7280 !important;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Init from './Init.vue';
|
|
||||||
|
|
||||||
export default Init;
|
|
||||||
@@ -3,54 +3,321 @@
|
|||||||
<login-box class="shadow-lg" @success="onLoginSuccess" @failure="onLoginFail" />
|
<login-box class="shadow-lg" @success="onLoginSuccess" @failure="onLoginFail" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import LoginBox from './LoginBox.vue';
|
import LoginBox from './LoginBox.vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
// import http from '@/store/http';
|
import { message, notification, Modal } from 'ant-design-vue'; // 引入 Modal 用于确认提示
|
||||||
|
import { onMounted, ref, h } from 'vue';
|
||||||
|
import { useApiStore } from '@/store';
|
||||||
|
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
function onLoginSuccess() {
|
// 版本数据存储
|
||||||
router.push('/dashboard');
|
const currentTag = ref<string>('');
|
||||||
}
|
const latestTag = ref<string>('未知版本');
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
|
|
||||||
function onLoginFail(status, res) {
|
// 复制版本号到剪贴板核心逻辑
|
||||||
console.log(res);
|
const copyToClipboard = async (content: string, type: string) => {
|
||||||
if (res.code === 0) {
|
if (!content || content === '未知版本') {
|
||||||
} else {
|
message.warning(`${type}无有效内容可复制`);
|
||||||
message.error(res.erro, 5);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
message.success(`${type}已复制到剪贴板 ✅`);
|
||||||
|
} else {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = content;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.left = '-9999px';
|
||||||
|
textarea.style.top = '-9999px';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
message.success(`${type}已复制到剪贴板 ✅`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
message.error('复制失败,请手动选中复制');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统一的关闭处理:标记缓存 + 关闭通知
|
||||||
|
const handleNoticeClose = (noticeKey: string) => {
|
||||||
|
console.log('版本提醒通知已关闭,后续不再提醒');
|
||||||
|
// 标记为已提醒,存入缓存(确保版本通知和确认弹窗都不再出现)
|
||||||
|
localStorage.setItem('maintain_notice_shown', 'true');
|
||||||
|
// 关闭版本通知
|
||||||
|
notification.close(noticeKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭前的确认提示弹窗
|
||||||
|
const showCloseConfirm = (noticeKey: string) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认关闭',
|
||||||
|
content: '关闭后该提醒将不再弹出,确定要关闭吗?',
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => {
|
||||||
|
// 确认关闭:执行统一处理
|
||||||
|
handleNoticeClose(noticeKey);
|
||||||
|
message.success('提醒已关闭,后续不再弹出');
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
// 取消关闭:不执行任何操作,保留版本通知
|
||||||
|
message.info('已取消关闭');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开右上角 notification 通知
|
||||||
|
const openVersionNotice = () => {
|
||||||
|
const noticeKey = `version_notice_${Date.now()}`;
|
||||||
|
notification.open({
|
||||||
|
message: '温馨提示一下',
|
||||||
|
key: noticeKey,
|
||||||
|
duration: 0,
|
||||||
|
placement: 'topRight',
|
||||||
|
// 通知描述内容
|
||||||
|
description: h('div', { class: 'notice-content' }, [
|
||||||
|
h('p', { class: 'notice-desc' }, '当前您使用的docker镜像为阿里云镜像,已停止维护。'),
|
||||||
|
h('p', { class: 'notice-version-item' }, [
|
||||||
|
h('strong', { class: 'notice-version-label' }, '当前版本:'),
|
||||||
|
h('span', { style: { color: '#ff4d4f', fontWeight: '500' } }, currentTag.value.replace('[不再维护]', '')),
|
||||||
|
h(CopyOutlined, {
|
||||||
|
class: 'notice-copy-icon',
|
||||||
|
title: '复制当前版本',
|
||||||
|
onClick: () => copyToClipboard(currentTag.value.replace('[不再维护]', ''), '当前版本'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
h('p', { class: 'notice-version-item' }, [
|
||||||
|
h('strong', { class: 'notice-version-label' }, '最新版本:'),
|
||||||
|
h('span', { style: { color: '#52c41a', fontWeight: '500' } }, latestTag.value),
|
||||||
|
h(CopyOutlined, {
|
||||||
|
class: 'notice-copy-icon',
|
||||||
|
title: '复制最新版本',
|
||||||
|
onClick: () => copyToClipboard(latestTag.value, '最新版本'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
h('p', { class: 'notice-tip' }, '建议升级到最新版本!'),
|
||||||
|
]),
|
||||||
|
// 「我已知晓」按钮:点击触发确认弹窗
|
||||||
|
btn: () =>
|
||||||
|
h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
class: 'notice-confirm-btn',
|
||||||
|
onClick: () => {
|
||||||
|
// 不直接关闭,先弹出确认提示
|
||||||
|
showCloseConfirm(noticeKey);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'我已知晓'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useApiStore()
|
||||||
|
.AppisInit()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code == 0 && res.data) {
|
||||||
|
} else {
|
||||||
|
router.push('/init');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useApiStore()
|
||||||
|
.CheckTag()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
if (res.data.length > 0) {
|
||||||
|
const tag = res.data[0];
|
||||||
|
if (tag.indexOf('不再维护') !== -1) {
|
||||||
|
const hasShown = localStorage.getItem('maintain_notice_shown');
|
||||||
|
if (!hasShown) {
|
||||||
|
currentTag.value = tag;
|
||||||
|
latestTag.value = res.data.length >= 2 ? res.data[1] : '未知版本';
|
||||||
|
openVersionNotice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(res.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function onLoginSuccess() {
|
||||||
|
if (isMobileBrowser()) router.push('/mobile');
|
||||||
|
else router.push('/dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMobileBrowser = (): boolean => {
|
||||||
|
if (typeof navigator === 'undefined' || typeof window === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const userAgent = navigator.userAgent.toLowerCase();
|
||||||
|
const mobileUA = /android|iphone|ipod|blackberry|windows phone|iemobile|opera mini/i.test(userAgent);
|
||||||
|
const isTablet = /ipad|tablet|playbook|kindle|android 3\.|android 4\.[0-3]/.test(userAgent);
|
||||||
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
const isMobileScreen = window.innerWidth <= 768 && window.innerHeight <= 1024;
|
||||||
|
|
||||||
|
return (mobileUA && !isTablet && isTouchDevice) || (isMobileScreen && isTouchDevice);
|
||||||
|
};
|
||||||
|
|
||||||
|
function onLoginFail(reason: string, fields: any) {
|
||||||
|
console.log('登录失败:', reason, fields);
|
||||||
|
message.error(reason || '登录失败,请重试', 5);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.login {
|
.login {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
// 与深色登录框搭配的渐变背景
|
min-height: -webkit-fill-available;
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
// 可以添加一些装饰性背景元素
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 400px;
|
width: 280px;
|
||||||
height: 400px;
|
height: 280px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(54, 191, 250, 0.1);
|
background: rgba(54, 191, 250, 0.1);
|
||||||
top: 20%;
|
top: 15%;
|
||||||
left: 15%;
|
left: 5%;
|
||||||
filter: blur(80px);
|
filter: blur(60px);
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
top: 20%;
|
||||||
|
left: 15%;
|
||||||
|
filter: blur(80px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 300px;
|
width: 220px;
|
||||||
height: 300px;
|
height: 220px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(54, 191, 250, 0.08);
|
background: rgba(54, 191, 250, 0.08);
|
||||||
bottom: 10%;
|
bottom: 5%;
|
||||||
right: 10%;
|
right: 5%;
|
||||||
filter: blur(60px);
|
filter: blur(40px);
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
bottom: 10%;
|
||||||
|
right: 10%;
|
||||||
|
filter: blur(60px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (bottom: env(safe-area-inset-bottom)) {
|
||||||
|
.login {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.ant-notification {
|
||||||
|
.ant-notification-notice {
|
||||||
|
width: 500px !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content {
|
||||||
|
font-size: 14px !important;
|
||||||
|
line-height: 1.8 !important;
|
||||||
|
color: #334155 !important;
|
||||||
|
width: 480px !important;
|
||||||
|
|
||||||
|
.notice-desc {
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
padding-left: 2px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-version-item {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
margin: 8px 0 !important;
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
background-color: #f8fafc !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
transition: background-color 0.2s ease !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f1f5f9 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-version-label {
|
||||||
|
color: #1e293b !important;
|
||||||
|
width: 70px !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-copy-icon {
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
color: #165dff !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
margin-left: 8px !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #0d47a1 !important;
|
||||||
|
transform: scale(1.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-tip {
|
||||||
|
margin-top: 12px !important;
|
||||||
|
color: #64748b !important;
|
||||||
|
padding-left: 2px !important;
|
||||||
|
font-style: italic !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-confirm-btn {
|
||||||
|
background-color: #165dff !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: background-color 0.2s ease !important;
|
||||||
|
margin-top: 12px !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #0d47a1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,78 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-page min-h-screen flex items-center justify-center p-4 md:p-8">
|
<ThemeProvider :color="{
|
||||||
<ThemeProvider :color="{
|
middle: { 'bg-base': '#fff' },
|
||||||
middle: { 'bg-base': '#f8fafc' },
|
primary: { DEFAULT: '#4f46e5', hover: '#4338ca' }
|
||||||
primary: { DEFAULT: '#4f46e5', hover: '#4338ca' }
|
}">
|
||||||
}">
|
<!-- 登录卡片 - 移动端全屏 + PC端保留原宽度 -->
|
||||||
<!-- 登录卡片 -->
|
<div class="login-box rounded-2xl shadow-lg p-6 md:p-8 lg:p-10 max-w-md w-full border border-gray-200 transition-all duration-500 hover:shadow-xl transform hover:-translate-y-1 mx-auto" style="box-sizing: border-box; max-width: 90vw;">
|
||||||
<div class="login-box rounded-2xl shadow-lg p-8 md:p-10 max-w-md w-full border border-gray-200 transition-all duration-500 hover:shadow-xl transform hover:-translate-y-1">
|
<!-- 顶部Logo区域 - 边框圈精准贴合Logo内容 -->
|
||||||
<!-- 顶部Logo区域 -->
|
<div class="flex flex-col items-center mb-6 md:mb-8">
|
||||||
<div class="flex flex-col items-center mb-8">
|
<div class="w-30 h-30 md:w-36 md:h-36 bg-primary/10 rounded-full flex items-center justify-center mb-4 md:mb-5">
|
||||||
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
<div class="w-24 h-24 md:w-28 h-28 rounded-full flex items-center justify-center">
|
||||||
<img src="/public/logo.png">
|
<img src="/logo.png" alt="抖小云logo" class="w-18 h-18 md:w-20 h-20 object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="third-title text-2xl font-bold text-gray-800 tracking-tight">抖音同步小帮手</h2>
|
</div>
|
||||||
|
<h2 class="third-title text-xl md:text-2xl font-bold text-gray-800 tracking-tight">抖小云</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full text-gray-700">
|
||||||
|
<!-- 用户名输入框 -->
|
||||||
|
<a-form-item :required="true" name="username" class="mb-4 md:mb-5" :validate-status="usernameStatus">
|
||||||
|
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[46px] md:h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" style="padding: 0 12px;">
|
||||||
|
<template #prefix>
|
||||||
|
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="12" cy="7" r="4"></circle>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 密码输入框 -->
|
||||||
|
<a-form-item :required="true" name="password" class="mb-2" :validate-status="passwordStatus">
|
||||||
|
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[46px] md:h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" :type="showPassword ? 'text' : 'password'" style="padding: 0 12px;">
|
||||||
|
<template #prefix>
|
||||||
|
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<!-- 密码显示/隐藏图标 -->
|
||||||
|
<template #suffix>
|
||||||
|
<div class="w-8 h-8 flex items-center justify-center" @click="showPassword = !showPassword">
|
||||||
|
<EyeOutlined v-if="showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="隐藏密码" />
|
||||||
|
<EyeInvisibleOutlined v-else class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="显示密码" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 记住密码 -->
|
||||||
|
<div class="flex items-center justify-between mb-5 md:mb-6 px-1">
|
||||||
|
<a-form-item class="mb-0">
|
||||||
|
<a-checkbox v-model:checked="rememberPassword" class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||||
|
<span class="text-xs md:text-sm">记住密码</span>
|
||||||
|
</a-checkbox>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item class="mb-0">
|
||||||
|
<div class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer" @click="handleForgotPassword">
|
||||||
|
<span class="text-xs md:text-sm">忘记密码</span>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full text-gray-700">
|
<!-- 登录按钮 -->
|
||||||
<!-- 用户名输入框 -->
|
<a-button htmlType="submit" class="h-[48px] md:h-[52px] w-full rounded-lg transition-all duration-300 hover:bg-primary/90 bg-primary border-primary text-white text-base font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5 active:translate-y-0" type="primary" :loading="loading" style="touch-action: manipulation; -webkit-tap-highlight-color: transparent;">
|
||||||
<a-form-item :required="true" name="username" class="mb-5" :validate-status="usernameStatus">
|
<span v-if="!loading">登录</span>
|
||||||
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20">
|
<span v-else>登录中...</span>
|
||||||
<template #prefix>
|
</a-button>
|
||||||
<svg class="w-5 h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
</a-form>
|
||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
</div>
|
||||||
<circle cx="12" cy="7" r="4"></circle>
|
</ThemeProvider>
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
</a-input>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<!-- 密码输入框 -->
|
<!-- 忘记密码弹窗 -->
|
||||||
<a-form-item :required="true" name="password" class="mb-2" :validate-status="passwordStatus">
|
<a-modal v-model:visible="forgotModalVisible" title="密码重置教程" width="50%" :max-width="500" centered :mask-closable="false" :footer="null" class="forgot-modal">
|
||||||
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" :type="showPassword ? 'text' : 'password'">
|
<div class="p-2">
|
||||||
<template #prefix>
|
<!-- 步骤说明 -->
|
||||||
<svg class="w-5 h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<div class="space-y-4 mb-6">
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
<div class="flex items-start">
|
||||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">1</div>
|
||||||
</svg>
|
<div class="flex-1 text-gray-700 text-sm leading-relaxed">在 db 目录下新建一个文本文件,命名为 <span class="px-2 py-0.5 bg-gray-100 text-primary font-medium rounded text-sm">pwd.txt</span></div>
|
||||||
</template>
|
</div>
|
||||||
<!-- 密码显示/隐藏图标 -->
|
<div class="flex items-start">
|
||||||
<template #suffix>
|
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">2</div>
|
||||||
<EyeOutlined v-if="showPassword" @click="showPassword = !showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="隐藏密码" />
|
<div class="flex-1 text-gray-700 text-sm leading-relaxed">打开文件写入你要设置的新密码,留空则自动重置为默认密码:<span class="px-2 py-0.5 bg-gray-100 text-primary font-medium rounded text-sm">douyin2026</span></div>
|
||||||
<EyeInvisibleOutlined v-else @click="showPassword = !showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="显示密码" />
|
</div>
|
||||||
</template>
|
<div class="flex items-start">
|
||||||
</a-input>
|
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">3</div>
|
||||||
</a-form-item>
|
<div class="flex-1 text-gray-700 text-sm leading-relaxed">重启 docker服务或者飞牛安装的抖小云应用 ,使用新密码登录即可</div>
|
||||||
|
</div>
|
||||||
<!-- 记住密码与忘记密码 -->
|
|
||||||
<div class="flex items-center justify-between mb-6 px-1">
|
|
||||||
<a-form-item class="mb-0">
|
|
||||||
<a-checkbox v-model:checked="rememberPassword" class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer">
|
|
||||||
<span class="text-sm">记住密码</span>
|
|
||||||
</a-checkbox>
|
|
||||||
</a-form-item>
|
|
||||||
<!-- <a href="#" class="text-primary hover:text-primary/80 text-sm font-medium transition-colors duration-300" @click.prevent="handleForgotPassword">
|
|
||||||
忘记密码?
|
|
||||||
</a> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
|
||||||
<a-button htmlType="submit" class="h-[52px] w-full rounded-lg transition-all duration-300 hover:bg-primary/90 bg-primary border-primary text-white text-base font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5 active:translate-y-0" type="primary" :loading="loading">
|
|
||||||
<span v-if="!loading">登录</span>
|
|
||||||
<span v-else>登录中...</span>
|
|
||||||
</a-button>
|
|
||||||
|
|
||||||
<!-- 注册入口 -->
|
|
||||||
<!-- <div class="text-center mt-6 text-gray-500 text-sm">
|
|
||||||
还没有账号?
|
|
||||||
<a href="#" class="text-primary hover:text-primary/80 font-medium transition-colors duration-300 ml-1" @click.prevent="handleRegister">
|
|
||||||
立即注册
|
|
||||||
</a>
|
|
||||||
</div> -->
|
|
||||||
</a-form>
|
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
|
||||||
</div>
|
<!-- 知道了按钮 -->
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<a-button type="primary" class="h-10 px-8 rounded-lg" @click="forgotModalVisible = false">
|
||||||
|
我知道了
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -88,8 +113,14 @@ const showPassword = ref(false);
|
|||||||
const rememberPassword = ref(false);
|
const rememberPassword = ref(false);
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
// 忘记密码弹窗显示状态
|
||||||
|
const forgotModalVisible = ref(false);
|
||||||
|
|
||||||
// 表单状态(用于输入框验证反馈)
|
// 表单状态(用于输入框验证反馈)
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
const usernameStatus = computed(() => (form.username ? 'success' : ''));
|
const usernameStatus = computed(() => (form.username ? 'success' : ''));
|
||||||
const passwordStatus = computed(() => (form.password ? 'success' : ''));
|
const passwordStatus = computed(() => (form.password ? 'success' : ''));
|
||||||
|
|
||||||
@@ -98,11 +129,6 @@ export interface LoginFormProps {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = reactive<LoginFormProps>({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'success', fields: LoginFormProps): void;
|
(e: 'success', fields: LoginFormProps): void;
|
||||||
(e: 'failure', reason: string, fields: LoginFormProps): void;
|
(e: 'failure', reason: string, fields: LoginFormProps): void;
|
||||||
@@ -158,19 +184,20 @@ async function login(params: LoginFormProps) {
|
|||||||
try {
|
try {
|
||||||
const res = await accountStore.login(params.username, params.password);
|
const res = await accountStore.login(params.username, params.password);
|
||||||
emit('success', params);
|
emit('success', params);
|
||||||
|
console.log(res);
|
||||||
message.success('登录成功!');
|
message.success('登录成功!');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// emit('failure', e.message, e.data);
|
emit('failure', e.message || '登录失败', params);
|
||||||
message.error(e.data.erro || '登录失败,请重试');
|
message.error(e.data?.erro || '登录失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 忘记密码处理
|
// 忘记密码处理 - 打开弹窗
|
||||||
function handleForgotPassword() {
|
function handleForgotPassword() {
|
||||||
|
forgotModalVisible.value = true;
|
||||||
emit('forgot-password');
|
emit('forgot-password');
|
||||||
message.info('忘记密码功能即将上线');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册处理
|
// 注册处理
|
||||||
@@ -180,15 +207,85 @@ function handleRegister() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 页面基础样式 */
|
/* 全局布局适配 - 替代原外层容器的样式 */
|
||||||
.login-page {
|
:root {
|
||||||
|
-webkit-text-size-adjust: 100%; /* 禁止iOS文本缩放 */
|
||||||
|
-webkit-font-smoothing: antialiased; /* 优化字体渲染 */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
min-height: 100vh;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
/* 全局居中布局 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录卡片核心样式 */
|
||||||
|
.login-box {
|
||||||
|
box-sizing: border-box;
|
||||||
|
touch-action: manipulation; /* 优化触摸性能 */
|
||||||
|
/* 移动端全屏,PC端保留原max-w-md(默认28rem/448px)+ 90vw限制 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配(<768px) */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
max-width: 100vw !important; /* 移动端全屏 */
|
||||||
|
min-height: 100vh;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 20px 16px !important;
|
||||||
|
padding-top: 20vh !important; /* 移动端垂直居中 */
|
||||||
|
}
|
||||||
|
.third-title {
|
||||||
|
font-size: 1.25rem !important;
|
||||||
|
}
|
||||||
|
::v-deep(.ant-input) {
|
||||||
|
font-size: 14px; /* 移动端字体适配 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏手机额外适配 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-box {
|
||||||
|
padding-top: 15vh !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PC端样式(保留原有宽度,仅调小最大宽度) */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
/* 原宽度是 min(448px, 90vw),现调整为更小的数值,比如 380px(可根据需求修改) */
|
||||||
|
max-width: min(340px, 90vw) !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
border: 1px solid #e5e7eb !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1) !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框聚焦动画优化 */
|
/* 输入框聚焦动画优化 */
|
||||||
|
::v-deep(.ant-input) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
::v-deep(.ant-input:focus) {
|
::v-deep(.ant-input:focus) {
|
||||||
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2) !important;
|
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2) !important;
|
||||||
border-color: #4f46e5 !important;
|
border-color: #4f46e5 !important;
|
||||||
|
outline: none; /* 移除默认轮廓 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 复选框样式优化 */
|
/* 复选框样式优化 */
|
||||||
@@ -196,29 +293,26 @@ function handleRegister() {
|
|||||||
background-color: #4f46e5 !important;
|
background-color: #4f46e5 !important;
|
||||||
border-color: #4f46e5 !important;
|
border-color: #4f46e5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.ant-checkbox:hover .ant-checkbox-inner) {
|
::v-deep(.ant-checkbox:hover .ant-checkbox-inner) {
|
||||||
border-color: #4f46e5 !important;
|
border-color: #4f46e5 !important;
|
||||||
}
|
}
|
||||||
|
::v-deep(.ant-checkbox) {
|
||||||
|
transform: scale(0.9); /* 移动端复选框适当缩小 */
|
||||||
|
}
|
||||||
|
|
||||||
/* 按钮样式优化 */
|
/* 按钮样式优化 */
|
||||||
::v-deep(.ant-btn-primary) {
|
::v-deep(.ant-btn-primary) {
|
||||||
background-color: #4f46e5 !important;
|
background-color: #4f46e5 !important;
|
||||||
border-color: #4f46e5 !important;
|
border-color: #4f46e5 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.ant-btn-primary:hover) {
|
::v-deep(.ant-btn-primary:hover) {
|
||||||
background-color: #4338ca !important;
|
background-color: #4338ca !important;
|
||||||
border-color: #4338ca !important;
|
border-color: #4338ca !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.ant-btn-primary:focus) {
|
::v-deep(.ant-btn-primary:focus) {
|
||||||
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.3) !important;
|
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.3) !important;
|
||||||
}
|
outline: none;
|
||||||
|
|
||||||
/* 分割线样式优化 */
|
|
||||||
::v-deep(.ant-divider) {
|
|
||||||
background-color: #e5e7eb !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加载状态动画优化 */
|
/* 加载状态动画优化 */
|
||||||
@@ -226,24 +320,35 @@ function handleRegister() {
|
|||||||
margin-right: 8px !important;
|
margin-right: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* 弹窗样式美化 */
|
||||||
@media (max-width: 375px) {
|
::v-deep(.forgot-modal .ant-modal-header) {
|
||||||
.login-box {
|
border-radius: 12px 12px 0 0;
|
||||||
padding: 60px 20px !important;
|
background: #f9fafb;
|
||||||
}
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
::v-deep(.forgot-modal .ant-modal-title) {
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
::v-deep(.forgot-modal .ant-modal-body) {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
::v-deep(.forgot-modal .ant-modal-content) {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.third-title {
|
/* 防止点击闪烁和触摸高亮 */
|
||||||
font-size: 1.5rem !important;
|
* {
|
||||||
}
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 平滑滚动 */
|
/* 平滑滚动 */
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
overflow-x: hidden; /* 防止移动端横向滚动 */
|
||||||
|
|
||||||
/* 防止点击闪烁 */
|
|
||||||
* {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,79 +1,293 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-form layout="inline" style="margin-top:5px;margin-bottom:5px;">
|
<a-form layout="inline" style="margin-top:5px;margin-bottom:5px;align-items: center;">
|
||||||
|
|
||||||
|
<!-- 日期控制 -->
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-date-picker v-model:value="dateValue" format="YYYYMMDD" :locale="locale" @change="datePickChange" />
|
<div class="date-control-group">
|
||||||
|
<a-button type="text" @click="handleDateMinus" class="date-btn"><left-outlined /></a-button>
|
||||||
|
<span class="current-date-text">{{ currentDateText }}</span>
|
||||||
|
<a-button type="text" @click="handleDatePlus" class="date-btn" :disabled="isToday"><right-outlined /></a-button>
|
||||||
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<!-- debug/error单选按钮 -->
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-radio-group v-model:value="typeValue" button-style="solid" @change="typeChange">
|
<a-radio-group class="log-type-radio-group" v-model:value="typeValue" @change="typeChange" button-style="solid">
|
||||||
<a-radio-button value="debug">debug</a-radio-button>
|
<a-radio-button value="debug">debug</a-radio-button>
|
||||||
<a-radio-button value="error">error</a-radio-button>
|
<a-radio-button value="error">error</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<!-- 复制按钮 - 同一行最右侧 -->
|
||||||
|
<a-form-item style="margin-left: auto;">
|
||||||
|
<a-button type="text" size="small" @click="copyLogs" class="copy-btn">
|
||||||
|
<copy-outlined />
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a-card title="" :bordered="true">
|
<a-card title="" :bordered="true" :class="{ 'log-error': typeValue === 'error' }">
|
||||||
<pre>{{ logs }}</pre>
|
<pre class="card-width-pre">{{ logs }}</pre>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineComponent, reactive, ref, watch, onMounted } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useApiStore } from '@/store';
|
import { useApiStore } from '@/store';
|
||||||
import type { UnwrapRef } from 'vue';
|
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
|
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||||
type RangeValue = [Dayjs, Dayjs];
|
import { message } from 'ant-design-vue';
|
||||||
import 'dayjs/locale/zh-cn';
|
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
const dateValue = ref<Dayjs>(dayjs(Date()));
|
|
||||||
|
const currentDate = ref<Dayjs>(dayjs());
|
||||||
const typeValue = ref<string>('debug');
|
const typeValue = ref<string>('debug');
|
||||||
const iframeUrl = ref<string>('');
|
const iframeUrl = ref<string>('');
|
||||||
const dateValue1 = ref<string>();
|
|
||||||
const logs = ref<string>('');
|
const logs = ref<string>('');
|
||||||
dateValue1.value = dayjs(Date()).format('YYYYMMDD');
|
|
||||||
iframeUrl.value = `type=${typeValue.value}&date=${dateValue1.value}`;
|
const currentDateText = computed(() => {
|
||||||
const datePickChange = (e, dateStr) => {
|
return currentDate.value.format('YYYYMMDD');
|
||||||
dateValue1.value = dateStr;
|
});
|
||||||
iframeUrl.value = `type=${typeValue.value}&date=${dateValue1.value}`;
|
|
||||||
console.log(iframeUrl);
|
const isToday = computed(() => {
|
||||||
|
return currentDate.value.isSame(dayjs(), 'day');
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDateMinus = () => {
|
||||||
|
currentDate.value = currentDate.value.subtract(1, 'day');
|
||||||
|
updateIframeUrlAndLoadLogs();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDatePlus = () => {
|
||||||
|
if (isToday.value) return;
|
||||||
|
currentDate.value = currentDate.value.add(1, 'day');
|
||||||
|
updateIframeUrlAndLoadLogs();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIframeUrlAndLoadLogs = () => {
|
||||||
|
iframeUrl.value = `${typeValue.value}/${currentDateText.value}`;
|
||||||
loadLogs();
|
loadLogs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeChange = (e) => {
|
const typeChange = (e: any) => {
|
||||||
console.log(e.target);
|
typeValue.value = e.target.value;
|
||||||
iframeUrl.value = `type=${e.target.value}&date=${dateValue1.value}`;
|
updateIframeUrlAndLoadLogs();
|
||||||
loadLogs();
|
|
||||||
};
|
};
|
||||||
const mIfrm = ref<any>(null);
|
|
||||||
onMounted(() => {
|
const formatLogTime = (logContent: string) => {
|
||||||
// console.log(mIfrm.value);
|
const timeRegex = /(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+ \+08:00/g;
|
||||||
loadLogs();
|
return logContent.replace(timeRegex, '$1');
|
||||||
});
|
};
|
||||||
|
|
||||||
const loadLogs = () => {
|
const loadLogs = () => {
|
||||||
useApiStore()
|
useApiStore()
|
||||||
.apiGetLogs(iframeUrl.value)
|
.apiGetLogs(iframeUrl.value)
|
||||||
.then((log) => {
|
.then((log) => {
|
||||||
// console.log(log);
|
log = formatLogTime(log);
|
||||||
const lines = log.split('\n'); // 将文本按换行符分割为行数组
|
const lines = log.split('\n');
|
||||||
const reversedLines = lines.reverse(); // 对行数组进行倒序操作
|
const reversedLines = lines.reverse();
|
||||||
const reversedText = reversedLines.join('\n'); // 将行数组重新连接为一个字符串
|
const reversedText = reversedLines.join('\n');
|
||||||
|
|
||||||
logs.value = reversedText;
|
logs.value = reversedText;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('加载日志失败:', err);
|
||||||
|
logs.value = '日志加载失败,请稍后重试';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 复制日志内容的核心函数
|
||||||
|
const copyLogs = async () => {
|
||||||
|
try {
|
||||||
|
// 空内容判断
|
||||||
|
if (!logs.value || logs.value === '日志加载失败,请稍后重试') {
|
||||||
|
message.warning('暂无可复制的日志内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 使用浏览器剪贴板API复制内容
|
||||||
|
await navigator.clipboard.writeText(logs.value);
|
||||||
|
message.success('日志内容复制成功!');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
// 降级方案:兼容不支持Clipboard API的浏览器
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = logs.value;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
// 提示反馈
|
||||||
|
message.success('日志内容复制成功!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateIframeUrlAndLoadLogs();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang='less' scoped>
|
|
||||||
html {
|
<style lang='less' scoped>
|
||||||
|
// 基础样式:仅清理默认margin/padding,不设置全屏
|
||||||
|
html,
|
||||||
|
body {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表单容器:正常宽度,保留少量间距
|
||||||
|
:deep(.ant-form) {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志容器:自适应高度,无固定上限,保留上下留白
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
padding: 0 8px;
|
||||||
// max-height: 400px;
|
margin: 16px 0; // 上下留白,远离屏幕边缘
|
||||||
iframe {
|
box-sizing: border-box;
|
||||||
.word-wrap {
|
overflow: hidden;
|
||||||
color: white !important;
|
}
|
||||||
|
// pre样式:自适应高度,内容过多时内部滚动
|
||||||
|
.card-width-pre {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0 !important;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
// 核心:最小高度保证容器不塌陷,最大高度限制不超视口一半
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: calc(67vh); // 最多占视口一半,绝对不会满屏
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
// 滚动条美化(保留)
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.date-control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.date-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-date-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1f2329;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制按钮样式
|
||||||
|
.copy-btn {
|
||||||
|
// height: 32px; /* 和日期按钮/单选按钮高度一致 */
|
||||||
|
// padding: 0 16px;
|
||||||
|
color: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// card-body样式:恢复默认内边距,作为pre的宽度基准
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 4px; // 恢复合理内边距,作为pre的宽度容器
|
||||||
|
height: 100%; // 全屏继承容器高度
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden; // 新增:隐藏卡片内溢出,确保pre独占滚动区域
|
||||||
|
}
|
||||||
|
|
||||||
|
// error状态下pre文字颜色
|
||||||
|
.log-error {
|
||||||
|
.card-width-pre {
|
||||||
|
color: #f5222d !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// radio-button样式(保留之前的修正版)
|
||||||
|
:deep(.log-type-radio-group) {
|
||||||
|
.ant-radio-button-wrapper:first-child {
|
||||||
|
&.ant-radio-button-wrapper-checked {
|
||||||
|
background-color: #52c41a !important;
|
||||||
|
border-color: #52c41a !important;
|
||||||
|
color: #fff !important;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
background-color: #389e0d !important;
|
||||||
|
border-color: #389e0d !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.ant-radio-button-wrapper-checked):hover {
|
||||||
|
border-color: #73d13d !important;
|
||||||
|
color: #52c41a !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-radio-button-wrapper:last-child {
|
||||||
|
&.ant-radio-button-wrapper-checked {
|
||||||
|
background-color: #f5222d !important;
|
||||||
|
border-color: #f5222d !important;
|
||||||
|
color: #fff !important;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
background-color: #cf1322 !important;
|
||||||
|
border-color: #cf1322 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.ant-radio-button-wrapper-checked):hover {
|
||||||
|
border-color: #ff4d4f !important;
|
||||||
|
color: #f5222d !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-radio-button-wrapper {
|
||||||
|
&:first-child {
|
||||||
|
border-radius: 6px 0 0 6px !important;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 6px 6px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: -1px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-radio-button-wrapper-checked {
|
||||||
|
&:first-child {
|
||||||
|
&::before {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
// 调整form布局,让复制按钮靠右
|
||||||
|
:deep(.ant-form-inline) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,26 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-drawer v-model:visible="visible" class="custom-class" title="个人设置" placement="right" @after-visible-change="afterVisibleChange" :maskClosable="true">
|
<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-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">
|
<a-tab-pane key="2">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<span>
|
<span>
|
||||||
@@ -60,7 +40,6 @@ import { checkPass } from '@/utils/regexHelper';
|
|||||||
import type { Rule } from 'ant-design-vue/es/form';
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
const activeKey = ref<string>('2');
|
const activeKey = ref<string>('2');
|
||||||
const uploadAction = ref<string>('');
|
|
||||||
const visible = ref<boolean>(false);
|
const visible = ref<boolean>(false);
|
||||||
|
|
||||||
function show(vis: boolean) {
|
function show(vis: boolean) {
|
||||||
@@ -122,7 +101,6 @@ const loadUserInfo = () => {
|
|||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
if (res.data.avatar != null && res.data.avatar !== '') imageUrl.value = `/upload/${res.data.avatar}`;
|
if (res.data.avatar != null && res.data.avatar !== '') imageUrl.value = `/upload/${res.data.avatar}`;
|
||||||
passwordFormData.UserId = res.data.id;
|
passwordFormData.UserId = res.data.id;
|
||||||
uploadAction.value = `/api/auth/UpdateUserAvatar?Uid=${res.data.id}`;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -150,7 +128,7 @@ const onFinish = (values: PassFormState) => {
|
|||||||
visible.value = false;
|
visible.value = false;
|
||||||
http.removeAuthorization();
|
http.removeAuthorization();
|
||||||
} else {
|
} else {
|
||||||
message.error(res.erro, 8);
|
message.error(res.message, 8);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import MyPersonal from './MyPersonal.vue';
|
import MyPersonal from './MyPersonal.vue';
|
||||||
import EmailSet from './EmailSet.vue';
|
// import EmailSet from './EmailSet.vue';
|
||||||
|
|
||||||
export { MyPersonal, EmailSet };
|
export { MyPersonal };
|
||||||
|
|||||||
@@ -1,745 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="stats-dashboard">
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<!-- 页面标题 -->
|
|
||||||
|
|
||||||
<!-- 核心统计概览 -->
|
|
||||||
<section class="stats-overview">
|
|
||||||
<div class="stat-card primary-card">
|
|
||||||
<div class="stat-header">
|
|
||||||
<span class="stat-meta">总视频数</span>
|
|
||||||
<div class="stat-icon video-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">{{ totalVideos }}</div>
|
|
||||||
<div class="stat-trend">
|
|
||||||
<!-- <span class="trend-up">较上月 +5.2%</span> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card secondary-card">
|
|
||||||
<div class="stat-header">
|
|
||||||
<span class="stat-meta">占用空间</span>
|
|
||||||
<div class="stat-icon size-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M22 12H2v8h20v-8z" />
|
|
||||||
<path d="M6 18h.01" />
|
|
||||||
<path d="M10 18h.01" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">{{ fileSizeTotal }} G</div>
|
|
||||||
<div class="stat-trend">
|
|
||||||
<!-- <span class="trend-up">较上月 +12.8%</span> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 次级统计卡片组 -->
|
|
||||||
<section class="stats-grid">
|
|
||||||
<!-- 我喜欢的数量卡片 -->
|
|
||||||
<div class="stat-card mini-card">
|
|
||||||
<div class="stat-header">
|
|
||||||
<span class="stat-meta">我喜欢的</span>
|
|
||||||
<div class="stat-icon like-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">{{ favoriteCount }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 我收藏的数量卡片 -->
|
|
||||||
<div class="stat-card mini-card">
|
|
||||||
<div class="stat-header">
|
|
||||||
<span class="stat-meta">我收藏的</span>
|
|
||||||
<div class="stat-icon collect-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">{{ collectCount }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 作者总数卡片 -->
|
|
||||||
<div class="stat-card mini-card">
|
|
||||||
<div class="stat-header">
|
|
||||||
<span class="stat-meta">作者总数</span>
|
|
||||||
<div class="stat-icon author-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
|
||||||
<circle cx="12" cy="7" r="4"></circle>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">{{ totalAuthors }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分类总数卡片 -->
|
|
||||||
<div class="stat-card mini-card">
|
|
||||||
<div class="stat-header">
|
|
||||||
<span class="stat-meta">分类总数</span>
|
|
||||||
<div class="stat-icon cate-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="2" y="5" width="20" height="14" rx="2" />
|
|
||||||
<path d="M17 5v4" />
|
|
||||||
<path d="M7 5v4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-value">{{ categoryTotal }}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 详细分类统计 -->
|
|
||||||
<section class="detailed-stats">
|
|
||||||
<div class="stats-header">
|
|
||||||
<h2>详细统计</h2>
|
|
||||||
<div class="tab-controls">
|
|
||||||
<button class="tab-btn" :class="{ active: currentTab === 'author' }" @click="currentTab = 'author'">
|
|
||||||
视频作者
|
|
||||||
</button>
|
|
||||||
<button class="tab-btn" :class="{ active: currentTab === 'type' }" @click="currentTab = 'type'">
|
|
||||||
视频分类
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition name="stats-fade" mode="out-in">
|
|
||||||
<!-- 作者统计 -->
|
|
||||||
<div v-if="currentTab === 'author'" key="author-view" class="stats-content">
|
|
||||||
<div class="authors-grid">
|
|
||||||
<div class="author-card" v-for="(author, index) in authors" :key="index">
|
|
||||||
<div class="author-avatar">
|
|
||||||
<img :src="author.icon" alt="作者头像" />
|
|
||||||
</div>
|
|
||||||
<div class="author-info">
|
|
||||||
<h3 class="author-name">{{ author.name }}</h3>
|
|
||||||
<p class="author-stats">作品数: {{ author.count }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="author-progress">
|
|
||||||
<div class="progress-bar" :style="{ width: `${(author.count / totalVideos) * 100}%` }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分类统计 -->
|
|
||||||
<div v-else key="category-view" class="stats-content">
|
|
||||||
<div class="categories-grid">
|
|
||||||
<div class="category-card" v-for="(category, index) in categories" :key="index" :style="{ '--category-color': category.color }">
|
|
||||||
<div class="category-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 12 12" fill="white">
|
|
||||||
<use :xlink:href="`#${category.icon}`"></use>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="category-info">
|
|
||||||
<h3 class="category-name">{{ category.name }}</h3>
|
|
||||||
<p class="category-stats">作品数: {{ category.count }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="category-percentage">
|
|
||||||
{{ Math.round((category.count / totalVideos) * 100) }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SVG图标定义 -->
|
|
||||||
<svg style="display: none;">
|
|
||||||
<!-- 所有图标定义保持不变 -->
|
|
||||||
<symbol id="cup" viewBox="0 0 12 12">
|
|
||||||
<path d="M18 4h2v16h-2zM4 4h14v2H4zM4 8h10v2H4zM4 12h10v2H4zM4 16h6v2H4zM4 20h6v2H4z" />
|
|
||||||
</symbol>
|
|
||||||
<!-- 其他图标省略,保持原样 -->
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useApiStore } from '@/store';
|
|
||||||
|
|
||||||
// 类型接口
|
|
||||||
interface Author {
|
|
||||||
name: string;
|
|
||||||
count: number;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Category {
|
|
||||||
name: string;
|
|
||||||
count: number;
|
|
||||||
color: string;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态管理
|
|
||||||
const totalVideos = ref<number>(0);
|
|
||||||
const totalAuthors = ref<number>(0);
|
|
||||||
const categoryTotal = ref<number>(0);
|
|
||||||
const fileSizeTotal = ref<number>(0);
|
|
||||||
const favoriteCount = ref<number>(0);
|
|
||||||
const collectCount = ref<number>(0);
|
|
||||||
const categories = ref<Category[]>([]);
|
|
||||||
const authors = ref<Author[]>([]);
|
|
||||||
const currentTab = ref<string>('author');
|
|
||||||
|
|
||||||
// 组件名称
|
|
||||||
defineOptions({
|
|
||||||
name: 'StatsDashboard',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载数据
|
|
||||||
onMounted(() => {
|
|
||||||
loadDashboardData();
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadDashboardData = async () => {
|
|
||||||
try {
|
|
||||||
const res = await useApiStore().VideoStatics();
|
|
||||||
totalVideos.value = res.data.videoCount;
|
|
||||||
totalAuthors.value = res.data.authorCount;
|
|
||||||
categoryTotal.value = res.data.categoryCount;
|
|
||||||
favoriteCount.value = res.data.favoriteCount;
|
|
||||||
collectCount.value = res.data.collectCount;
|
|
||||||
categories.value = res.data.categories;
|
|
||||||
fileSizeTotal.value = res.data.viedoSizeTotal; // 修复拼写错误
|
|
||||||
authors.value = res.data.authors;
|
|
||||||
|
|
||||||
// 分类图标和颜色随机分配
|
|
||||||
const cates = getRandomElements(categoriessss.value, categories.value.length);
|
|
||||||
const colors = getRandomElements(colorArray, categories.value.length);
|
|
||||||
categories.value.forEach((item, index) => {
|
|
||||||
item.icon = cates[index];
|
|
||||||
item.color = colors[index];
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('加载仪表盘数据失败:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 随机获取数组元素方法
|
|
||||||
const getRandomElements = (arr: any[], n: number) => {
|
|
||||||
if (n <= 0) return [];
|
|
||||||
if (n >= arr.length) return [...arr];
|
|
||||||
return [...arr].sort(() => Math.random() - 0.5).slice(0, n);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 图标列表和颜色数组
|
|
||||||
const categoriessss = ref<any[]>([
|
|
||||||
'cup',
|
|
||||||
'spoon',
|
|
||||||
'fork',
|
|
||||||
'knife',
|
|
||||||
'plate',
|
|
||||||
'bottle',
|
|
||||||
'toothbrush',
|
|
||||||
'comb',
|
|
||||||
'mirror',
|
|
||||||
'soap',
|
|
||||||
'mobile',
|
|
||||||
'tablet',
|
|
||||||
'camera',
|
|
||||||
'headphones',
|
|
||||||
'speaker',
|
|
||||||
'tv',
|
|
||||||
'watch',
|
|
||||||
'charger',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const colorArray = [
|
|
||||||
'#3A7CA5',
|
|
||||||
'#D9A566',
|
|
||||||
'#6B4226',
|
|
||||||
'#829399',
|
|
||||||
'#A63446',
|
|
||||||
'#4F6367',
|
|
||||||
'#FE5F55',
|
|
||||||
'#C7EFCF',
|
|
||||||
'#78C0E0',
|
|
||||||
'#49BEAA',
|
|
||||||
'#49DCB1',
|
|
||||||
'#FF9999',
|
|
||||||
'#66B2FF',
|
|
||||||
'#99FF99',
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 白天模式默认样式(无dark-mode类时生效) */
|
|
||||||
.stats-dashboard {
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: #ffffff; /* 白天背景:白色 */
|
|
||||||
color: #333333; /* 白天文本:深灰 */
|
|
||||||
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加宽整体宽度并减少两侧留白 */
|
|
||||||
.dashboard-container {
|
|
||||||
max-width: 1400px; /* 从1200px加宽至1600px */
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 15px; /* 两侧留白从20px减至15px */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式适配:避免宽屏溢出 */
|
|
||||||
@media (max-width: 1700px) {
|
|
||||||
.dashboard-container {
|
|
||||||
max-width: 95%; /* 中等屏幕用百分比控制宽度 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 头部样式 */
|
|
||||||
.dashboard-header {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 1px solid #e0e0e0; /* 白天边框:浅灰 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
color: #333333; /* 白天标题:深灰 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-subtitle {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #666666; /* 白天副标题:中灰 */
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 统计卡片样式(增强白天阴影) */
|
|
||||||
.stat-card {
|
|
||||||
background: #f5f5f5; /* 白天卡片背景:浅灰 */
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 增强阴影 */
|
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); /* 增强hover阴影 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-meta {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666666; /* 白天元数据:中灰 */
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-icon {
|
|
||||||
background-color: rgba(76, 175, 80, 0.2);
|
|
||||||
color: #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.size-icon {
|
|
||||||
background-color: rgba(33, 150, 243, 0.2);
|
|
||||||
color: #2196f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.like-icon {
|
|
||||||
background-color: rgba(233, 30, 99, 0.2);
|
|
||||||
color: #e91e63;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collect-icon {
|
|
||||||
background-color: rgba(255, 152, 0, 0.2);
|
|
||||||
color: #ff9800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-icon {
|
|
||||||
background-color: rgba(156, 39, 176, 0.2);
|
|
||||||
color: #9c27b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cate-icon {
|
|
||||||
background-color: rgba(0, 188, 212, 0.2);
|
|
||||||
color: #00bcd4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
color: #333333;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-trend {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-up {
|
|
||||||
color: #4caf50;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-up::before {
|
|
||||||
content: '↑';
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 概览区域布局 */
|
|
||||||
.stats-overview {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.stats-overview {
|
|
||||||
grid-template-columns: 2fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-card {
|
|
||||||
border-top: 4px solid #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-card {
|
|
||||||
border-top: 4px solid #2196f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 次级统计网格布局 */
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.stats-grid {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mini-card .stat-value {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 详细统计区域 */
|
|
||||||
.detailed-stats {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 25px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #666666;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn.active {
|
|
||||||
background: rgba(76, 175, 80, 0.2);
|
|
||||||
color: #4caf50;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn:hover:not(.active) {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 统计内容区域 */
|
|
||||||
.stats-content {
|
|
||||||
animation: fadeIn 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authors-grid,
|
|
||||||
.categories-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
|
||||||
.authors-grid,
|
|
||||||
.categories-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.authors-grid,
|
|
||||||
.categories-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 作者卡片(增强白天阴影) */
|
|
||||||
.author-card {
|
|
||||||
background: #eeeeee;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); /* 新增阴影 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-card:hover {
|
|
||||||
transform: translateX(5px);
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12); /* 增强hover阴影 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-avatar {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-avatar img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-name {
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-stats {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-progress {
|
|
||||||
height: 6px;
|
|
||||||
background: #e0e0e0;
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
height: 100%;
|
|
||||||
background: #4caf50;
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: width 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 分类卡片(增强白天阴影) */
|
|
||||||
.category-card {
|
|
||||||
background: #eeeeee;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); /* 新增阴影 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-card:hover {
|
|
||||||
transform: translateX(5px);
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12); /* 增强hover阴影 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--category-color);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-name {
|
|
||||||
margin: 0 0 3px 0;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-stats {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-percentage {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--category-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动画效果 */
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-fade-enter-from,
|
|
||||||
.stats-fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-fade-enter-active,
|
|
||||||
.stats-fade-leave-active {
|
|
||||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 晚上模式样式 */
|
|
||||||
html.dark-mode .stats-dashboard {
|
|
||||||
background-color: #1a1a2e;
|
|
||||||
color: #eaeaea;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .dashboard-header {
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .dashboard-header h1 {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .header-subtitle {
|
|
||||||
color: #b0b0c3;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .stat-card {
|
|
||||||
background: rgba(30, 30, 50, 0.8);
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .stat-card:hover {
|
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .stat-meta {
|
|
||||||
color: #b0b0c3;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .stat-value {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .detailed-stats {
|
|
||||||
background: rgba(30, 30, 50, 0.8);
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .stats-header h2 {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .tab-btn {
|
|
||||||
color: #b0b0c3;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .tab-btn:hover:not(.active) {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .author-card {
|
|
||||||
background: rgba(40, 40, 65, 0.6);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .author-name {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .author-stats {
|
|
||||||
color: #b0b0c3;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .author-progress {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .category-card {
|
|
||||||
background: rgba(40, 40, 65, 0.6);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .category-name {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .category-stats {
|
|
||||||
color: #b0b0c3;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -13,19 +13,93 @@ interface NaviGuard {
|
|||||||
after?: NavigationHookAfter;
|
after?: NavigationHookAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginGuard: NavigationGuard = function (to, from) {
|
/**
|
||||||
// console.log('Authorization', http.checkAuthorization())
|
* 检测是否为移动端设备(UA + 屏幕宽度双检测)
|
||||||
const account = useAccountStore();
|
*/
|
||||||
if (!http.checkAuthorization() && !/^\/(login|home|init)?$/.test(to.fullPath)) {
|
const isMobile = (): boolean => {
|
||||||
account.setLogged(false)
|
const userAgent = navigator.userAgent.toLowerCase();
|
||||||
return '/login';
|
const mobileUaReg = /iphone|android|ipad|ipod|mobile|wap|symbian|windows ce|blackberry|webos|ucbrowser/i;
|
||||||
} else {
|
const isSmallScreen = window.innerWidth < 768;
|
||||||
}
|
return mobileUaReg.test(userAgent) || isSmallScreen;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dynamicinitRoute =
|
// 标记是否已跳转到移动端路由,防止无限循环
|
||||||
{
|
let hasRedirectedToMobile = false;
|
||||||
|
// 新增:标记是否已从/mobile跳转到/dashboard,防止无限循环
|
||||||
|
let hasRedirectedToDashboard = false;
|
||||||
|
// 优化后的移动端跳转守卫(登录优先)
|
||||||
|
const MobileRedirectGuard: NavigationGuard = function (to, from, next) {
|
||||||
|
// 1. 排除移动端路由和登录页,避免逻辑干扰
|
||||||
|
const isMobileRoute = to.path === '/mobile';
|
||||||
|
const isLoginRoute = to.path === '/login';
|
||||||
|
// 新增:检测当前是否为非移动端设备
|
||||||
|
const isNonMobile = !isMobile();
|
||||||
|
|
||||||
|
// 【新增核心逻辑】:当前路由是/mobile,但不是移动端设备,跳转到/dashboard
|
||||||
|
if (isMobileRoute && isNonMobile) {
|
||||||
|
if (!hasRedirectedToDashboard) {
|
||||||
|
hasRedirectedToDashboard = true;
|
||||||
|
// 重置移动端跳转标记,避免后续干扰
|
||||||
|
hasRedirectedToMobile = false;
|
||||||
|
next({ path: '/dashboard' });
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobileRoute) {
|
||||||
|
hasRedirectedToMobile = true;
|
||||||
|
// 重置dashboard跳转标记,避免后续干扰
|
||||||
|
hasRedirectedToDashboard = false;
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoginRoute) {
|
||||||
|
// 登录页无需移动端跳转,直接放行
|
||||||
|
hasRedirectedToMobile = false; // 重置标记,登录后可正常跳转移动端
|
||||||
|
hasRedirectedToDashboard = false; // 重置dashboard跳转标记
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 移动端检测
|
||||||
|
if (isMobile()) {
|
||||||
|
const isAuthorized = http.checkAuthorization();
|
||||||
|
if (!isAuthorized) {
|
||||||
|
// 未登录:优先跳登录(保持原有优先级)
|
||||||
|
next('/login');
|
||||||
|
} else {
|
||||||
|
// 已登录:跳移动端(防止重复跳转)
|
||||||
|
if (!hasRedirectedToMobile) {
|
||||||
|
hasRedirectedToMobile = true;
|
||||||
|
hasRedirectedToDashboard = false; // 重置dashboard跳转标记
|
||||||
|
next({ path: '/mobile' });
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非移动端:重置所有标记,不影响后续操作
|
||||||
|
hasRedirectedToMobile = false;
|
||||||
|
hasRedirectedToDashboard = false;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const loginGuard: NavigationGuard = function (to, from, next) {
|
||||||
|
if (!http.checkAuthorization() && !/^\/(init|login|home|mobile)?$/.test(to.fullPath)) {
|
||||||
|
console.log(to.fullPath)
|
||||||
|
const account = useAccountStore();
|
||||||
|
account.setLogged(false);
|
||||||
|
// 重置dashboard跳转标记
|
||||||
|
hasRedirectedToDashboard = false;
|
||||||
|
next('/login');
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dynamicinitRoute = {
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
redirect: '/login',
|
redirect: '/login',
|
||||||
@@ -38,31 +112,22 @@ const dynamicinitRoute =
|
|||||||
component: () => import('@/pages/login'),
|
component: () => import('@/pages/login'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const InitGuard: NavigationGuard = function (to, from, next) {
|
||||||
const InitGuard: NavigationGuard = function (to, from) {
|
|
||||||
|
|
||||||
if (to.fullPath != '/login') {
|
if (to.fullPath != '/login') {
|
||||||
useApiStore()
|
if (!router.hasRoute('login')) {
|
||||||
.apiCheckInitStatus()
|
router.addRoute(dynamicinitRoute);
|
||||||
.then((res) => {
|
}
|
||||||
// console.log(to.fullPath)
|
next('/login');
|
||||||
if (res.code === 0) {
|
} else {
|
||||||
} else {
|
next();
|
||||||
if (!router.hasRoute('login')) {
|
|
||||||
router.addRoute(dynamicinitRoute)
|
|
||||||
}
|
|
||||||
router.push('/login')
|
|
||||||
// return '/init'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 进度条
|
// 进度条
|
||||||
const ProgressGuard: NaviGuard = {
|
const ProgressGuard: NaviGuard = {
|
||||||
before(to, from) {
|
before(to, from, next) {
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
|
next();
|
||||||
},
|
},
|
||||||
after(to, from) {
|
after(to, from) {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
@@ -70,16 +135,18 @@ const ProgressGuard: NaviGuard = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AuthGuard: NaviGuard = {
|
const AuthGuard: NaviGuard = {
|
||||||
before(to, from) {
|
before(to, from, next) {
|
||||||
const { hasAuthority } = useAuthStore();
|
const { hasAuthority } = useAuthStore();
|
||||||
if (to.meta?.permission && !hasAuthority(to.meta?.permission)) {
|
if (to.meta?.permission && !hasAuthority(to.meta?.permission)) {
|
||||||
return { name: '403', query: { permission: to.meta.permission, path: to.fullPath } };
|
next({ name: '403', query: { permission: to.meta.permission, path: to.fullPath } });
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ForbiddenGuard: NaviGuard = {
|
const ForbiddenGuard: NaviGuard = {
|
||||||
before(to) {
|
before(to, from, next) {
|
||||||
if (to.name === '403' && (to.query.permission || to.query.path)) {
|
if (to.name === '403' && (to.query.permission || to.query.path)) {
|
||||||
to.fullPath = to.fullPath
|
to.fullPath = to.fullPath
|
||||||
.replace(/permission=[^&=]*&?/, '')
|
.replace(/permission=[^&=]*&?/, '')
|
||||||
@@ -90,21 +157,59 @@ const ForbiddenGuard: NaviGuard = {
|
|||||||
delete to.query.permission;
|
delete to.query.permission;
|
||||||
delete to.query.path;
|
delete to.query.path;
|
||||||
}
|
}
|
||||||
|
next();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 404 not found
|
// 404 not found
|
||||||
const NotFoundGuard: NaviGuard = {
|
const NotFoundGuard: NaviGuard = {
|
||||||
before(to, from) {
|
before(to, from, next) {
|
||||||
const { loading } = useMenuStore();
|
const { loading } = useMenuStore();
|
||||||
if (to.meta._is404Page && loading) {
|
if (to.meta._is404Page && loading) {
|
||||||
to.params.loading = true as any;
|
to.params.loading = true as any;
|
||||||
}
|
}
|
||||||
|
next();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 优化后的页面刷新移动端检测(登录优先)
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
const isNonMobile = !isMobile();
|
||||||
|
|
||||||
|
// 【新增】:刷新后如果是/mobile路由且非移动端,跳转到/dashboard
|
||||||
|
if (currentPath === '/mobile' && isNonMobile) {
|
||||||
|
router.push('/dashboard').catch(err => {
|
||||||
|
if (!err.message.includes('NavigationDuplicated')) {
|
||||||
|
console.error('刷新时从/mobile跳转到/dashboard失败:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile() && currentPath !== '/mobile') {
|
||||||
|
const isAuthorized = http.checkAuthorization();
|
||||||
|
if (!isAuthorized) {
|
||||||
|
// 未登录:跳登录(避免重复跳转)
|
||||||
|
if (currentPath !== '/login') {
|
||||||
|
router.push('/login').catch(err => {
|
||||||
|
if (!err.message.includes('NavigationDuplicated')) {
|
||||||
|
console.error('刷新时跳转登录页失败:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 已登录:跳移动端
|
||||||
|
router.push('/mobile').catch(err => {
|
||||||
|
if (!err.message.includes('NavigationDuplicated')) {
|
||||||
|
console.error('刷新时跳转移动端路由失败:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// before: [ProgressGuard.before, InitGuard, loginGuard, AuthGuard.before, ForbiddenGuard.before, NotFoundGuard.before],
|
before: [ProgressGuard.before, MobileRedirectGuard, loginGuard, AuthGuard.before, ForbiddenGuard.before, NotFoundGuard.before],
|
||||||
before: [ProgressGuard.before, loginGuard, AuthGuard.before, ForbiddenGuard.before, NotFoundGuard.before],
|
|
||||||
after: [ProgressGuard.after],
|
after: [ProgressGuard.after],
|
||||||
};
|
};
|
||||||
@@ -9,7 +9,7 @@ const router = createRouter(
|
|||||||
routes,
|
routes,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
console.log(router)
|
// console.log(router)
|
||||||
// 注册导航守卫
|
// 注册导航守卫
|
||||||
guards.before.forEach(router.beforeEach);
|
guards.before.forEach(router.beforeEach);
|
||||||
guards.after.forEach(router.afterEach);
|
guards.after.forEach(router.afterEach);
|
||||||
|
|||||||
@@ -14,6 +14,31 @@ const routes: RouteRecordRaw[] = [
|
|||||||
children: null,
|
children: null,
|
||||||
component: () => import('@/pages/login'),
|
component: () => import('@/pages/login'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'mobile',
|
||||||
|
redirect: '/mobile',
|
||||||
|
meta: {
|
||||||
|
title: '移动端首页',
|
||||||
|
renderMenu: false,
|
||||||
|
icon: 'CreditCardOutlined',
|
||||||
|
},
|
||||||
|
children: null,
|
||||||
|
component: () => import('@/pages/mobile/MobileDashboard.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'init',
|
||||||
|
redirect: '/init',
|
||||||
|
meta: {
|
||||||
|
title: '初始化',
|
||||||
|
renderMenu: false,
|
||||||
|
icon: 'CreditCardOutlined',
|
||||||
|
},
|
||||||
|
children: null,
|
||||||
|
component: () => import('@/pages/desk/index.vue'),
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// path: '/',
|
// path: '/',
|
||||||
// name: 'init',
|
// name: 'init',
|
||||||
@@ -26,6 +51,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
// children: null,
|
// children: null,
|
||||||
// component: () => import('@/pages/init'),
|
// component: () => import('@/pages/init'),
|
||||||
// },
|
// },
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/front',
|
path: '/front',
|
||||||
name: '前端',
|
name: '前端',
|
||||||
@@ -45,7 +71,30 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
component: () => import('@/pages/login'),
|
component: () => import('@/pages/login'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/mobile',
|
||||||
|
name: 'mobile',
|
||||||
|
meta: {
|
||||||
|
icon: 'LoginOutlined',
|
||||||
|
view: 'blank',
|
||||||
|
target: '_blank',
|
||||||
|
cacheable: false,
|
||||||
|
},
|
||||||
|
children: null,
|
||||||
|
component: () => import('@/pages/mobile/MobileDashboard.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/init',
|
||||||
|
name: 'init',
|
||||||
|
meta: {
|
||||||
|
icon: 'LoginOutlined',
|
||||||
|
view: 'blank',
|
||||||
|
target: '_blank',
|
||||||
|
cacheable: false,
|
||||||
|
},
|
||||||
|
children: null,
|
||||||
|
component: () => import('@/pages/desk/index.vue'),
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// path: '/init',
|
// path: '/init',
|
||||||
// name: '初始化',
|
// name: '初始化',
|
||||||
@@ -107,6 +156,18 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
component: () => import('@/pages/workplace/Workplace.vue'),
|
component: () => import('@/pages/workplace/Workplace.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/follow',
|
||||||
|
name: '关注列表',
|
||||||
|
meta: {
|
||||||
|
icon: 'HeartOutlined',
|
||||||
|
view: 'self',
|
||||||
|
target: '_self',
|
||||||
|
renderMenu: true,
|
||||||
|
cacheable: false,
|
||||||
|
},
|
||||||
|
component: () => import('@/pages/followd/index.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/cok',
|
path: '/cok',
|
||||||
name: '抖音授权',
|
name: '抖音授权',
|
||||||
@@ -119,6 +180,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
component: () => import('@/pages/cok/Table.vue'),
|
component: () => import('@/pages/cok/Table.vue'),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/set',
|
path: '/set',
|
||||||
name: '系统配置',
|
name: '系统配置',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const useAccountStore = defineStore('account', {
|
|||||||
if (response.code === 200 && response.data.code === 0) {
|
if (response.code === 200 && response.data.code === 0) {
|
||||||
this.logged = true;
|
this.logged = true;
|
||||||
this.logged2 = true
|
this.logged2 = true
|
||||||
console.log(response)
|
// console.log(response)
|
||||||
http.setAuthorization(`Bearer ${response.data.token}`, response.data.expires);
|
http.setAuthorization(`Bearer ${response.data.token}`, response.data.expires);
|
||||||
// await useMenuStore().getMenuList();
|
// await useMenuStore().getMenuList();
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defineStore, storeToRefs } from 'pinia';
|
|||||||
import http from './http';
|
import http from './http';
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { Response } from '@/types';
|
import { Response } from '@/types';
|
||||||
|
|
||||||
// import { RouteOption } from '@/router/interface';
|
// import { RouteOption } from '@/router/interface';
|
||||||
// import { addRoutes, removeRoute } from '@/router/dynamicRoutes';
|
// import { addRoutes, removeRoute } from '@/router/dynamicRoutes';
|
||||||
// import { useSettingStore } from './setting';
|
// import { useSettingStore } from './setting';
|
||||||
@@ -28,62 +29,34 @@ import { Response } from '@/types';
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
export const useApiStore = defineStore('coreapi', () => {
|
export const useApiStore = defineStore('coreapi', () => {
|
||||||
//检查是否已经初始化过了
|
|
||||||
async function apiCheckInitStatus() {
|
|
||||||
|
|
||||||
return { code: 0 }
|
|
||||||
// const initStatus = localStorage.getItem('ddns-init');
|
|
||||||
// if (initStatus && initStatus === '1') {
|
|
||||||
// return { code: 0 }
|
|
||||||
// } else {
|
|
||||||
// return http
|
|
||||||
// .request<any, Response<any>>('/api/init/Check', 'GET')
|
|
||||||
// .then((res) => {
|
|
||||||
// // console.log(res)
|
|
||||||
// if (res.data.code === 0) {
|
|
||||||
// // localStorage.setItem('ddns-init', '1')
|
|
||||||
// }
|
|
||||||
// return res.data;
|
|
||||||
// })
|
|
||||||
// .finally(() => {
|
|
||||||
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
//初始化系统配置
|
|
||||||
async function apiInit(request: object) {
|
|
||||||
console.log(request)
|
|
||||||
return http
|
|
||||||
.request<any, Response<any>>('/api/init/Init', 'post_json', request)
|
|
||||||
.then((res) => {
|
|
||||||
// console.log(res)
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//获取配置
|
|
||||||
async function apiGetConfig() {
|
async function apiGetConfig() {
|
||||||
return http
|
return http.request<any, Response<any>>('/api/config/GetConfig', 'get').then(r => {
|
||||||
.request<any, Response<any>>('/api/config/GetConfig', 'GET')
|
return r;
|
||||||
.then((res) => {
|
}).finally(() => {
|
||||||
console.log(res)
|
|
||||||
return res.data;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// //获取配置
|
||||||
|
// async function apiGetConfig() {
|
||||||
|
// return http
|
||||||
|
// .request<any, Response<any>>('/api/config/GetConfig', 'GET')
|
||||||
|
// .then((res) => {
|
||||||
|
// return res;
|
||||||
|
// })
|
||||||
|
// .finally(() => {
|
||||||
|
|
||||||
|
// });
|
||||||
|
// }
|
||||||
//修改配置
|
//修改配置
|
||||||
async function apiUpdateConfig(request: object) {
|
async function apiUpdateConfig(request: object) {
|
||||||
return http
|
return http
|
||||||
.request<any, Response<any>>('/api/config/UpdateConfig', 'post_json', request)
|
.request<any, Response<any>>('/api/config/UpdateConfig', 'post_json', request)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log(res)
|
console.log(res)
|
||||||
return res.data;
|
return res;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
||||||
@@ -91,7 +64,7 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
}
|
}
|
||||||
//后台日志
|
//后台日志
|
||||||
async function apiGetLogs(param: string) {
|
async function apiGetLogs(param: string) {
|
||||||
return http.request<any, Response<any>>('/api/logs/GetLog?' + param, 'get').then(r => {
|
return http.request<any, Response<any>>('/api/logs/GetLog/' + param, 'get').then(r => {
|
||||||
// console.log(r)
|
// console.log(r)
|
||||||
return r.data;
|
return r.data;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@@ -101,7 +74,7 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
//用户信息-头像
|
//用户信息-头像
|
||||||
async function apiUserInfo() {
|
async function apiUserInfo() {
|
||||||
return http.request<any, Response<any>>('/api/auth/GetUserAvatar', 'get').then(r => {
|
return http.request<any, Response<any>>('/api/auth/GetUserAvatar', 'get').then(r => {
|
||||||
return r.data;
|
return r;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -109,7 +82,7 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
//密码修改
|
//密码修改
|
||||||
async function apiChangePwd(param: object) {
|
async function apiChangePwd(param: object) {
|
||||||
return http.request<any, Response<any>>('/api/auth/UpdatePwd', 'post_json', param).then(r => {
|
return http.request<any, Response<any>>('/api/auth/UpdatePwd', 'post_json', param).then(r => {
|
||||||
return r.data;
|
return r;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -117,14 +90,23 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
//StartJobNow
|
//StartJobNow
|
||||||
async function StartJobNow() {
|
async function StartJobNow() {
|
||||||
return http.request<any, Response<any>>('/api/config/ExecuteJobNow', 'get').then(r => {
|
return http.request<any, Response<any>>('/api/config/ExecuteJobNow', 'get').then(r => {
|
||||||
return r.data;
|
return r;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
//视频统计
|
||||||
async function VideoStatics() {
|
async function VideoStatics() {
|
||||||
return http.request<any, Response<any>>('/api/video/statics', 'get').then(r => {
|
return http.request<any, Response<any>>('/api/video/statics', 'get').then(r => {
|
||||||
return r.data;
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//视频曲线
|
||||||
|
async function VideoChart(day: number) {
|
||||||
|
return http.request<any, Response<any>>(`/api/video/chart/${day}`, 'get').then(r => {
|
||||||
|
return r;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -133,7 +115,7 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
//视频查询
|
//视频查询
|
||||||
async function VideoPageList(param: object) {
|
async function VideoPageList(param: object) {
|
||||||
return http.request<any, Response<any>>('/api/video/paged', 'post_json', param).then(r => {
|
return http.request<any, Response<any>>('/api/video/paged', 'post_json', param).then(r => {
|
||||||
return r.data;
|
return r;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -169,7 +151,14 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
//cookies
|
//cookies
|
||||||
async function CookiePageList(param: object) {
|
async function CookiePageList(param: object) {
|
||||||
return http.request<any, Response<any>>('/api/config/paged', 'post_json', param).then(r => {
|
return http.request<any, Response<any>>('/api/config/paged', 'post_json', param).then(r => {
|
||||||
return r.data;
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function CookieList() {
|
||||||
|
return http.request<any, Response<any>>('/api/config/list', 'get').then(r => {
|
||||||
|
return r;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -177,19 +166,252 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
|
|
||||||
async function UpdateConfig(param: object) {
|
async function UpdateConfig(param: object) {
|
||||||
return http.request<any, Response<any>>('/api/config/update', 'post_json', param).then(r => {
|
return http.request<any, Response<any>>('/api/config/update', 'post_json', param).then(r => {
|
||||||
return r.data;
|
return r;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async function DeskInitAsync(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/config/deskinit', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function AppisInit() {
|
||||||
|
return http.request<any, Response<any>>('/api/config/isInit', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function deleteCookie(id: string) {
|
async function deleteCookie(id: string) {
|
||||||
return http.request<any, Response<any>>('/api/config/delete?id=' + id, 'get').then(r => {
|
return http.request<any, Response<any>>('/api/config/delete?id=' + id, 'get').then(r => {
|
||||||
return r.data;
|
return r;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
//follows
|
||||||
|
async function FollowList(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/follow/paged', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//同步关注列表
|
||||||
|
async function SyncFollow() {
|
||||||
|
return http.request<any, Response<any>>('/api/follow/sync', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//更新同步关注者状态
|
||||||
|
async function OpenOrCloseSync(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/follow/openOrCloseSync', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//更新同步关注者状态
|
||||||
|
async function OpenOrCloseFullSync(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/follow/openOrCloseFullSync', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//重新下载
|
||||||
|
async function ReDownViedos(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/video/redown', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//批量删除
|
||||||
|
async function BathRealDelete(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/video/vdelete/batch', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//删除
|
||||||
|
async function DeleteVideo(param: string) {
|
||||||
|
return http.request<any, Response<any>>('/api/video/vdelete/' + param, 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//查询已删除
|
||||||
|
async function GetDeleteViedos() {
|
||||||
|
return http.request<any, Response<any>>('/api/video/vdelete/get', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//删除博主全部视频
|
||||||
|
async function DeleteByAuthor(param: string) {
|
||||||
|
return http.request<any, Response<any>>('/api/video/vdelete/byauthor/' + param, 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//检查版本
|
||||||
|
async function getVer() {
|
||||||
|
return http.request<any, Response<any>>('/api/config/mytag', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//检查版本
|
||||||
|
async function CheckTag() {
|
||||||
|
return http.request<any, Response<any>>('/api/config/checktag', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mp3List() {
|
||||||
|
return http.request<any, Response<any>>('/api/config/mp3List', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//快速停止或启动cookie配置
|
||||||
|
async function SwitchCookieStatus(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/config/switch', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//添加非关注的博主
|
||||||
|
async function AddFollow(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/follow/add', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//删除非关注的博主
|
||||||
|
async function DelFollow(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/follow/delete', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//导出配置
|
||||||
|
async function ExportConf() {
|
||||||
|
return http.request<any, Response<any>>('/api/config/exportConf', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//导入配置
|
||||||
|
async function ImportConf(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/config/importConf', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//移动端获取日志列表
|
||||||
|
async function MobileLogs() {
|
||||||
|
return http.request<any, Response<any>>('/api/logs/list', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//移动端获取日志详情
|
||||||
|
async function LogDetail(type: string, date: string) {
|
||||||
|
return http.request<any, Response<any>>('/api/logs/content?type=' + type + "&Date=" + date, 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//TOP
|
||||||
|
async function TopVideo(param: number) {
|
||||||
|
return http.request<any, Response<any>>('/api/Video/top' + param, 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Renfo
|
||||||
|
async function Renfo() {
|
||||||
|
return http.request<any, Response<any>>('/api/Video/renfo', 'get').then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//合集、自定义收藏夹、短剧列表
|
||||||
|
async function CatePageList(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/cate/paged', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//批量修改 合集、自定义收藏夹、短剧
|
||||||
|
async function BatchSaveCate(param: object) {
|
||||||
|
return http.request<any, Response<any>>('/api/cate/BatchSave', 'post_json', param).then(r => {
|
||||||
|
return r;
|
||||||
|
}).finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// // 音频文件上传接口
|
||||||
|
// async function apiUploadAudio(formData: FormData, options?: { onUploadProgress?: (progressEvent: ProgressEvent) => void }) {
|
||||||
|
// return http
|
||||||
|
// .request<any, Response<any>>(
|
||||||
|
// '/api/config/UploadAudio', // 请求地址
|
||||||
|
// 'post_form', // 使用新增的 post_form 类型
|
||||||
|
// formData, // FormData 参数(文件+其他参数)
|
||||||
|
// {
|
||||||
|
// onUploadProgress: options?.onUploadProgress, // 上传进度回调(原生 ProgressEvent)
|
||||||
|
// timeout: 120000 // 上传文件超时时间设为2分钟(可选)
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// .then((res) => {
|
||||||
|
// // console.log('音频上传结果:', res);
|
||||||
|
// // 适配你的响应格式(如果响应是包裹层,取 data)
|
||||||
|
// return res;
|
||||||
|
// })
|
||||||
|
// .catch((err) => {
|
||||||
|
// console.error('音频上传失败:', err);
|
||||||
|
// throw err; // 抛出错误让前端捕获
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
// 删除指定作者的所有视频数据
|
// 删除指定作者的所有视频数据
|
||||||
async function DeleteAuthorVideos(authorName: string) {
|
async function DeleteAuthorVideos(authorName: string) {
|
||||||
@@ -200,11 +422,51 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取抖音扫码登录二维码
|
||||||
|
async function QrCodeGet() {
|
||||||
|
return http.request<any, Response<any>>('/api/config/qrcode', 'get').then(r => {
|
||||||
|
return r.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询扫码状态
|
||||||
|
async function QrCodeStatus(token: string) {
|
||||||
|
return http.request<any, Response<any>>('/api/config/qrstatus?token=' + token, 'get').then(r => {
|
||||||
|
return r.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫码成功后绑定到账号
|
||||||
|
async function QrCodeBind(token: string, id: string) {
|
||||||
|
return http.request<any, Response<any>>(`/api/config/qrbind?token=${token}&id=${id}`, 'post_json').then(r => {
|
||||||
|
return r.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
VideoChart,
|
||||||
|
BatchSaveCate,
|
||||||
|
CatePageList,
|
||||||
|
getVer,
|
||||||
|
mp3List,
|
||||||
|
BathRealDelete,
|
||||||
|
DeleteByAuthor,
|
||||||
|
Renfo,
|
||||||
|
// apiUploadAudio,
|
||||||
|
AppisInit,
|
||||||
|
DeskInitAsync,
|
||||||
|
SwitchCookieStatus,
|
||||||
|
TopVideo,
|
||||||
|
LogDetail,
|
||||||
|
MobileLogs,
|
||||||
|
ExportConf,
|
||||||
|
ImportConf,
|
||||||
|
GetDeleteViedos,
|
||||||
|
DelFollow,
|
||||||
|
AddFollow,
|
||||||
|
CheckTag,
|
||||||
deleteCookie,
|
deleteCookie,
|
||||||
UpdateConfig,
|
UpdateConfig,
|
||||||
apiCheckInitStatus,
|
|
||||||
apiInit,
|
|
||||||
apiGetConfig,
|
apiGetConfig,
|
||||||
apiUpdateConfig,
|
apiUpdateConfig,
|
||||||
apiGetLogs,
|
apiGetLogs,
|
||||||
@@ -213,9 +475,13 @@ export const useApiStore = defineStore('coreapi', () => {
|
|||||||
StartJobNow,
|
StartJobNow,
|
||||||
VideoStatics,
|
VideoStatics,
|
||||||
VideoPageList,
|
VideoPageList,
|
||||||
VideoCategories,
|
|
||||||
VideoCleanup,
|
|
||||||
CookiePageList,
|
CookiePageList,
|
||||||
DeleteAuthorVideos
|
CookieList,
|
||||||
|
FollowList,
|
||||||
|
SyncFollow,
|
||||||
|
OpenOrCloseSync,
|
||||||
|
OpenOrCloseFullSync,
|
||||||
|
ReDownViedos,
|
||||||
|
DeleteVideo
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import createHttp from '@/utils/axiosHttp';
|
|||||||
import { isResponse } from '@/types';
|
import { isResponse } from '@/types';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import { useAccountStore } from '@/store';
|
import { useAccountStore } from '@/store';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import router from '@/router';
|
||||||
|
|
||||||
const http = createHttp({
|
const http = createHttp({
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
@@ -16,7 +18,10 @@ const isAxiosResponse = (obj: any): obj is AxiosResponse => {
|
|||||||
return typeof obj === 'object' && obj.status && obj.statusText && obj.headers && obj.config;
|
return typeof obj === 'object' && obj.status && obj.statusText && obj.headers && obj.config;
|
||||||
};
|
};
|
||||||
|
|
||||||
// progress 进度条 -- 开启
|
// 仅新增这一行:跳转锁
|
||||||
|
let isRedirecting = false;
|
||||||
|
|
||||||
|
// progress 进度条 -- 开启(和你原本一致)
|
||||||
http.interceptors.request.use((req: AxiosRequestConfig) => {
|
http.interceptors.request.use((req: AxiosRequestConfig) => {
|
||||||
if (!NProgress.isStarted()) {
|
if (!NProgress.isStarted()) {
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
@@ -24,31 +29,44 @@ http.interceptors.request.use((req: AxiosRequestConfig) => {
|
|||||||
return req;
|
return req;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 解析响应结果
|
// 解析响应结果(完全和你原本一致,一字未改)
|
||||||
http.interceptors.response.use(
|
http.interceptors.response.use(
|
||||||
(rep: AxiosResponse<String>) => {
|
(rep: AxiosResponse<String>) => {
|
||||||
const { data } = rep;
|
const { data } = rep;
|
||||||
// if (data.code != null)
|
|
||||||
// console.log(data)
|
|
||||||
// if (data.code === 401) {
|
|
||||||
// console.log(data)
|
|
||||||
// }
|
|
||||||
if (isResponse(data)) {
|
if (isResponse(data)) {
|
||||||
return data.code === 0 ? data : Promise.reject(data);
|
return data.code === 0 ? data : Promise.reject(data);
|
||||||
}
|
}
|
||||||
return Promise.reject({ message: rep.statusText, code: rep.status, data });
|
return Promise.reject({ message: rep.statusText, code: rep.status, data });
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// console.log(error)
|
if (error.response?.status === 401) {
|
||||||
// debugger
|
// 仅新增:加锁判断(这是唯一改动)
|
||||||
if (error.response && error.response.status === 401) {
|
if (!isRedirecting) {
|
||||||
useAccountStore().setLogged(false);
|
isRedirecting = true;
|
||||||
return Promise.reject({
|
|
||||||
message: '未授权,请重新登录',
|
const accountStore = useAccountStore();
|
||||||
code: 401,
|
accountStore.setLogged(false);
|
||||||
error: 'Unauthorized',
|
message.warning('登录状态已过期,请重新登录');
|
||||||
data: error.response.data,
|
setTimeout(() => {
|
||||||
});
|
|
||||||
|
const redirectPath = router.currentRoute.value.fullPath;
|
||||||
|
if (redirectPath !== '/login') {
|
||||||
|
router.push({
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: redirectPath }
|
||||||
|
}).then(() => {
|
||||||
|
console.log('跳转登录页成功');
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('跳转登录页失败:', err);
|
||||||
|
}).finally(() => {
|
||||||
|
isRedirecting = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isRedirecting = false;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
// 新增结束
|
||||||
} else {
|
} else {
|
||||||
if (error.response && isAxiosResponse(error.response)) {
|
if (error.response && isAxiosResponse(error.response)) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
@@ -64,7 +82,7 @@ http.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// progress 进度条 -- 关闭
|
// progress 进度条 -- 关闭(改回你原本的逻辑,不碰返回值)
|
||||||
http.interceptors.response.use(
|
http.interceptors.response.use(
|
||||||
(rep) => {
|
(rep) => {
|
||||||
if (NProgress.isStarted()) {
|
if (NProgress.isStarted()) {
|
||||||
@@ -76,8 +94,9 @@ http.interceptors.response.use(
|
|||||||
if (NProgress.isStarted()) {
|
if (NProgress.isStarted()) {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
}
|
}
|
||||||
|
// 改回你原本的返回值:return error(之前改这个导致登录异常)
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default http;
|
export default http;
|
||||||
@@ -4,7 +4,7 @@ import { ref } from 'vue';
|
|||||||
export type Navigation = 'side' | 'head' | 'mix';
|
export type Navigation = 'side' | 'head' | 'mix';
|
||||||
|
|
||||||
export const useSettingStore = defineStore('setting', () => {
|
export const useSettingStore = defineStore('setting', () => {
|
||||||
const navigation = ref<Navigation>('head');
|
const navigation = ref<Navigation>('side');
|
||||||
const useTabs = ref<boolean>(true);
|
const useTabs = ref<boolean>(true);
|
||||||
const theme = ref('header-night');
|
const theme = ref('header-night');
|
||||||
const contentClass = ref('common');
|
const contentClass = ref('common');
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ export function configTheme(key: string) {
|
|||||||
setBgSeriesColors({ 'bg-base': '#1a1a2e' });
|
setBgSeriesColors({ 'bg-base': '#1a1a2e' });
|
||||||
break;
|
break;
|
||||||
case 'header-dark':
|
case 'header-dark':
|
||||||
setNavigation('head');
|
// setNavigation('head');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
setNavigation('side');
|
// setNavigation('side');
|
||||||
}
|
}
|
||||||
if (key.indexOf('header') !== -1) {
|
if (key.indexOf('header') !== -1) {
|
||||||
setNavigation('head');
|
// setNavigation('head');
|
||||||
setTheme(key.split('-')[1])
|
setTheme(key.split('-')[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, Method as _Method, AxiosResponse } from 'axios';
|
import axios, { AxiosInstance, AxiosRequestConfig, Method as _Method, AxiosResponse } from 'axios';
|
||||||
|
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import Cookie from 'js-cookie';
|
import Cookie from 'js-cookie';
|
||||||
|
|
||||||
@@ -9,13 +8,13 @@ declare interface _AxiosExtend {
|
|||||||
* @param url 请求地址
|
* @param url 请求地址
|
||||||
* @param method 请求方法
|
* @param method 请求方法
|
||||||
* @param params 请求参数
|
* @param params 请求参数
|
||||||
* @param config 请求配置
|
* @param config 请求配置(新增上传进度回调)
|
||||||
*/
|
*/
|
||||||
request<T = any, R = AxiosResponse<T>>(
|
request<T = any, R = AxiosResponse<T>>(
|
||||||
url: string,
|
url: string,
|
||||||
method: Method,
|
method: Method,
|
||||||
params?: Record<string | number, any>,
|
params?: Record<string | number, any> | FormData,
|
||||||
config?: AxiosRequestConfig
|
config?: AxiosRequestConfig & { onUploadProgress?: (progressEvent: ProgressEvent) => void }
|
||||||
): Promise<R>;
|
): Promise<R>;
|
||||||
/**
|
/**
|
||||||
* 设置token
|
* 设置token
|
||||||
@@ -41,7 +40,7 @@ declare interface _AxiosExtend {
|
|||||||
|
|
||||||
export interface AxiosHttp extends Omit<AxiosInstance, 'request'>, _AxiosExtend { }
|
export interface AxiosHttp extends Omit<AxiosInstance, 'request'>, _AxiosExtend { }
|
||||||
|
|
||||||
export type Method = _Method | 'POST_JSON' | 'post_json' | 'PUT_JSON' | 'put_json';
|
export type Method = _Method | 'POST_JSON' | 'post_json' | 'PUT_JSON' | 'put_json' | 'POST_FORM' | 'post_form';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转表单格式
|
* 转表单格式
|
||||||
@@ -83,20 +82,10 @@ function toUrlencoded(params?: Record<string | number, any>) {
|
|||||||
function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
||||||
const _axios = axios.create(config);
|
const _axios = axios.create(config);
|
||||||
|
|
||||||
// 添加请求拦截器,自动添加Authorization header
|
// 移除 401 处理,仅保留基础的响应拦截器(可选,也可直接删除)
|
||||||
_axios.interceptors.request.use(
|
_axios.interceptors.response.use(
|
||||||
(config) => {
|
response => response,
|
||||||
// 从Cookie中读取Authorization token
|
error => Promise.reject(error)
|
||||||
const token = Cookie.get(config.xsrfCookieName || 'Authorization');
|
|
||||||
if (token && config.headers) {
|
|
||||||
// 将token添加到请求头
|
|
||||||
config.headers[config.xsrfHeaderName || 'Authorization'] = token;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const http: AxiosHttp = {
|
const http: AxiosHttp = {
|
||||||
@@ -104,10 +93,15 @@ function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
|||||||
request<T = any, R = AxiosResponse<T>>(
|
request<T = any, R = AxiosResponse<T>>(
|
||||||
url: string,
|
url: string,
|
||||||
method: Method,
|
method: Method,
|
||||||
params?: Record<string | number, any>,
|
params?: Record<string | number, any> | FormData,
|
||||||
config?: AxiosRequestConfig
|
config?: AxiosRequestConfig & { onUploadProgress?: (progressEvent: ProgressEvent) => void }
|
||||||
): Promise<R> {
|
): Promise<R> {
|
||||||
const _method = method.toUpperCase();
|
const _method = method.toUpperCase();
|
||||||
|
const requestConfig: AxiosRequestConfig = {
|
||||||
|
...config,
|
||||||
|
onUploadProgress: config?.onUploadProgress,
|
||||||
|
};
|
||||||
|
|
||||||
switch (_method) {
|
switch (_method) {
|
||||||
case 'GET':
|
case 'GET':
|
||||||
return _axios.get(url, {
|
return _axios.get(url, {
|
||||||
@@ -115,24 +109,35 @@ function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
|||||||
paramsSerializer: (data) => {
|
paramsSerializer: (data) => {
|
||||||
return qs.stringify(data, { indices: false, skipNulls: true });
|
return qs.stringify(data, { indices: false, skipNulls: true });
|
||||||
},
|
},
|
||||||
...config,
|
...requestConfig,
|
||||||
});
|
});
|
||||||
case 'POST':
|
case 'POST':
|
||||||
return _axios.post(url, toUrlencoded(params), config);
|
return _axios.post(url, toUrlencoded(params as Record<string | number, any>), requestConfig);
|
||||||
case 'POST_JSON':
|
case 'POST_JSON':
|
||||||
return _axios.post(url, params, config);
|
return _axios.post(url, params, {
|
||||||
|
...requestConfig,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...requestConfig.headers },
|
||||||
|
});
|
||||||
|
case 'POST_FORM':
|
||||||
|
return _axios.post(url, params, {
|
||||||
|
...requestConfig,
|
||||||
|
headers: { ...requestConfig.headers },
|
||||||
|
});
|
||||||
case 'PUT':
|
case 'PUT':
|
||||||
return _axios.put(url, toFormData(params), config);
|
return _axios.put(url, toFormData(params as Record<string | number, any>), requestConfig);
|
||||||
case 'PUT_JSON':
|
case 'PUT_JSON':
|
||||||
return _axios.put(url, params, config);
|
return _axios.put(url, params, {
|
||||||
|
...requestConfig,
|
||||||
|
headers: { 'Content-Type': 'application/json', ...requestConfig.headers },
|
||||||
|
});
|
||||||
case 'DELETE':
|
case 'DELETE':
|
||||||
return _axios.delete(url, { data: toFormData(params), ...config });
|
return _axios.delete(url, { data: toFormData(params as Record<string | number, any>), ...requestConfig });
|
||||||
case 'HEAD':
|
case 'HEAD':
|
||||||
return _axios.head(url, { params, ...config });
|
return _axios.head(url, { params, ...requestConfig });
|
||||||
case 'OPTIONS':
|
case 'OPTIONS':
|
||||||
return _axios.options(url, { params, ...config });
|
return _axios.options(url, { params, ...requestConfig });
|
||||||
case 'PATCH':
|
case 'PATCH':
|
||||||
return _axios.patch(url, { params, ...config });
|
return _axios.patch(url, { params, ...requestConfig });
|
||||||
case 'PURGE':
|
case 'PURGE':
|
||||||
case 'LINK':
|
case 'LINK':
|
||||||
case 'UNLINK':
|
case 'UNLINK':
|
||||||
@@ -174,4 +179,4 @@ function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
|||||||
return http;
|
return http;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createAxiosHttp;
|
export default createAxiosHttp;
|
||||||
@@ -22,7 +22,9 @@ const prodRollupOptions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// vite 配置
|
// vite 配置
|
||||||
|
// net stop winnat
|
||||||
|
// net start winnat
|
||||||
export default ({ command, mode }) => {
|
export default ({ command, mode }) => {
|
||||||
// 获取环境变量
|
// 获取环境变量
|
||||||
const env = loadEnv(mode, process.cwd());
|
const env = loadEnv(mode, process.cwd());
|
||||||
@@ -32,6 +34,9 @@ export default ({ command, mode }) => {
|
|||||||
cors: true,
|
cors: true,
|
||||||
port: 8001,
|
port: 8001,
|
||||||
open: false, //自动打开
|
open: false, //自动打开
|
||||||
|
watch: {
|
||||||
|
usePolling: true, // 绕过 fsevents 架构不兼容问题(arm64 Mac)
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: env.VITE_API_URL || 'http://localhost:10101',
|
target: env.VITE_API_URL || 'http://localhost:10101',
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"dbconn": "",
|
|
||||||
"dbtype": "Sqlite"
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 895 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 877 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 708 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 127 KiB |
@@ -1,126 +0,0 @@
|
|||||||
namespace dy.net.dto
|
|
||||||
{
|
|
||||||
public class VideoNFOInfo
|
|
||||||
{
|
|
||||||
|
|
||||||
// 基本信息
|
|
||||||
/// <summary>
|
|
||||||
/// 视频标题(通常是本地化标题)
|
|
||||||
/// </summary>
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 原始标题(通常是影片的原名,如外语片的原名)
|
|
||||||
/// </summary>
|
|
||||||
public string OriginalTitle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 排序标题,用于媒体库排序时使用
|
|
||||||
/// </summary>
|
|
||||||
public string SortTitle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 发行年份
|
|
||||||
/// </summary>
|
|
||||||
public int Year { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 详细剧情简介
|
|
||||||
/// </summary>
|
|
||||||
public string Plot { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 剧情大纲(比Plot更简短的描述)
|
|
||||||
/// </summary>
|
|
||||||
public string Outline { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 宣传语、标语(影片的简短宣传句子)
|
|
||||||
/// </summary>
|
|
||||||
public string Tagline { get; set; }
|
|
||||||
|
|
||||||
// 人员信息
|
|
||||||
/// <summary>
|
|
||||||
/// 导演姓名
|
|
||||||
/// </summary>
|
|
||||||
public string Director { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 演员列表
|
|
||||||
/// </summary>
|
|
||||||
public List<string> Actors { get; set; } = new List<string>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 编剧列表
|
|
||||||
/// </summary>
|
|
||||||
public List<string> Writers { get; set; } = new List<string>();
|
|
||||||
|
|
||||||
// 媒体信息
|
|
||||||
/// <summary>
|
|
||||||
/// 类型(如动作、喜剧、科幻等,多个类型用逗号分隔)
|
|
||||||
/// </summary>
|
|
||||||
public string Genre { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 评分(通常是10分制)
|
|
||||||
/// </summary>
|
|
||||||
public double Rating { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 评分人数
|
|
||||||
/// </summary>
|
|
||||||
public int Votes { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 制作公司
|
|
||||||
/// </summary>
|
|
||||||
public string Studio { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 首映日期
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? Premiered { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 片长(通常以分钟为单位,如"120分钟")
|
|
||||||
/// </summary>
|
|
||||||
public string Runtime { get; set; }
|
|
||||||
|
|
||||||
// 文件信息
|
|
||||||
/// <summary>
|
|
||||||
/// 文件名及路径
|
|
||||||
/// </summary>
|
|
||||||
public string FileName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 文件大小(以字节为单位)
|
|
||||||
/// </summary>
|
|
||||||
public long FileSize { get; set; }
|
|
||||||
|
|
||||||
// 可根据需要添加更多字段
|
|
||||||
/// <summary>
|
|
||||||
/// 国家/地区(制作国家或地区)
|
|
||||||
/// </summary>
|
|
||||||
public string Country { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 语言(影片使用的语言)
|
|
||||||
/// </summary>
|
|
||||||
public string Language { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 视频编码格式(如H.264, H.265等)
|
|
||||||
/// </summary>
|
|
||||||
public string VideoCodec { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 音频编码格式(如AC3, DTS等)
|
|
||||||
/// </summary>
|
|
||||||
public string AudioCodec { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 分辨率(如1920x1080, 3840x2160等)
|
|
||||||
/// </summary>
|
|
||||||
public string Resolution { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace dy.net.dto
|
|
||||||
{
|
|
||||||
public class VideoPageRequestDTO: PageRequestDto
|
|
||||||
{
|
|
||||||
// 3. 引用类型(string)如果允许为null,显式声明为 string?
|
|
||||||
public string? Tag { get; set; }
|
|
||||||
|
|
||||||
// 支持多个标签筛选
|
|
||||||
public List<string>? Tags { get; set; }
|
|
||||||
|
|
||||||
public string? Author { get; set; }
|
|
||||||
|
|
||||||
public string? ViedoType { get; set; }
|
|
||||||
|
|
||||||
// 4. 泛型集合(List<string>)允许为null,声明为 List<string>?
|
|
||||||
public List<string>? Dates { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 删除状态筛选:null=全部,true=已删除(videoSavePath为空),false=未删除(videoSavePath不为空)
|
|
||||||
/// </summary>
|
|
||||||
public bool? IsDeleted { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 排序字段:syncTime-同步时间, viedoType-同步类型, viedoCate-视频类型, author-作者, videoTitle-视频名称, dyUser-用户
|
|
||||||
/// </summary>
|
|
||||||
public string? SortField { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 排序方向:asc-升序, desc-降序
|
|
||||||
/// </summary>
|
|
||||||
public string? SortOrder { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class PageRequestDto
|
|
||||||
{
|
|
||||||
|
|
||||||
public int PageIndex { get; set; }
|
|
||||||
|
|
||||||
public int PageSize { get; set; } = 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,8 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<SpaRoot>app\</SpaRoot>
|
<SpaRoot>app\</SpaRoot>
|
||||||
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
|
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
|
||||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
|
|
||||||
|
|
||||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||||
<RetainVMGarbageCollection>true</RetainVMGarbageCollection>
|
<RetainVMGarbageCollection>true</RetainVMGarbageCollection>
|
||||||
@@ -19,65 +17,102 @@
|
|||||||
<TieredCompilation>true</TieredCompilation>
|
<TieredCompilation>true</TieredCompilation>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
<DockerfileContext>.</DockerfileContext>
|
<DockerfileContext>.</DockerfileContext>
|
||||||
|
<!-- 2. 强制禁用默认编译项(旧版VS关键配置) -->
|
||||||
|
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||||
|
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="app\**" />
|
<Compile Remove="app\**" />
|
||||||
|
<Compile Remove="db\**" />
|
||||||
|
<Compile Remove="expand\**" />
|
||||||
<Compile Remove="logs\**" />
|
<Compile Remove="logs\**" />
|
||||||
<Content Remove="app\**" />
|
<Content Remove="app\**" />
|
||||||
|
<Content Remove="db\**" />
|
||||||
|
<Content Remove="expand\**" />
|
||||||
<Content Remove="logs\**" />
|
<Content Remove="logs\**" />
|
||||||
<EmbeddedResource Remove="app\**" />
|
<EmbeddedResource Remove="app\**" />
|
||||||
|
<EmbeddedResource Remove="db\**" />
|
||||||
|
<EmbeddedResource Remove="expand\**" />
|
||||||
<EmbeddedResource Remove="logs\**" />
|
<EmbeddedResource Remove="logs\**" />
|
||||||
<None Remove="app\**" />
|
<None Remove="app\**" />
|
||||||
|
<None Remove="db\**" />
|
||||||
|
<None Remove="expand\**" />
|
||||||
<None Remove="logs\**" />
|
<None Remove="logs\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="**/*.cs" Exclude="app\**;db\**;expand\**;logs\**;obj\**;bin\**" />
|
||||||
|
<Content Include="**/*.json;**/*.xml;**/*.config" Exclude="app\**;db\**;expand\**;logs\**;obj\**;bin\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Content Remove="wwwroot\index.html" />
|
||||||
|
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
<PackageReference Include="ClockSnowFlake" Version="1.1.0" />
|
<PackageReference Include="ClockSnowFlake" Version="1.1.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.16" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.16" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.10" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
|
||||||
<PackageReference Include="Quartz" Version="3.8.0" />
|
|
||||||
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
|
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.8.0" />
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.8.0" />
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||||
<PackageReference Include="SqlSugarCore" Version="5.1.4.128" />
|
<PackageReference Include="SqlSugarCore" Version="5.1.4.128" />
|
||||||
<PackageReference Include="SqlSugarCoreNoDrive" Version="5.1.4.124" />
|
<!--<PackageReference Include="SqlSugarCoreNoDrive" Version="5.1.4.124" />-->
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.6.3" />
|
<!--<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.6.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.3.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.3.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.6.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.6.3" />-->
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />
|
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||||
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />
|
<!--<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />-->
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="db\" />
|
|
||||||
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="dy.net.sln" />
|
<None Include="dy.net.sln" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="db\dy.sqlite">
|
<None Update="Dockerfile">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
<None Update="Dockerfile-arm">
|
<None Update="Dockerfile-arm">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
<None Update="Dockerfile">
|
<None Update="mp3\silent_10.mp3">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="mp3_0\silent_10.mp3">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="db\" />
|
||||||
|
<Folder Include="wwwroot\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="jzc.http.lib">
|
||||||
|
<HintPath>lib\jzc.http.lib.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
||||||
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||||
@@ -96,5 +131,11 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<!-- 核心配置:仅用核心版本号2.2.0,自动拼接前缀 -->
|
||||||
|
<!--<Target Name="DockerBuildAndPushAfterPublish" AfterTargets="Publish">
|
||||||
|
<Exec Command="docker build -t jianzhichu/dysync.net:beta_$(DockerCoreVersion) . && docker tag jianzhichu/dysync.net:beta_$(DockerCoreVersion) ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_$(DockerCoreVersion) && docker push ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_$(DockerCoreVersion)"
|
||||||
|
Condition="'$(DockerArch)' == 'x86'" />
|
||||||
|
</Target>-->
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ActiveDebugProfile>tiny.ddns</ActiveDebugProfile>
|
<ActiveDebugProfile>dy.net</ActiveDebugProfile>
|
||||||
<NameOfLastUsedPublishProfile>E:\code\dysync\Properties\PublishProfiles\FolderProfile.pubxml</NameOfLastUsedPublishProfile>
|
<NameOfLastUsedPublishProfile>E:\code\dysync\Properties\PublishProfiles\docker.pubxml</NameOfLastUsedPublishProfile>
|
||||||
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||||
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
|
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 18.0.11010.61 d18.0
|
VisualStudioVersion = 18.0.11010.61
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dy.net", "dy.net.csproj", "{680660EF-ACAE-43A9-AB6C-B75532E758AD}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dy.net", "dy.net.csproj", "{680660EF-ACAE-43A9-AB6C-B75532E758AD}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace dy.extension
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||