feat: 完成v1.3.0架构对齐与Web端根路径托管全量开发

- 重构架构:抽离AddCloudSyncServer到扩展类,补全中间层文档对齐
- 修复根路径404与鉴权跳转:新增中间件处理浏览器请求302跳转
- 实现种子数据初始化:添加默认管理员账号自动创建逻辑
- 优化前端代理与构建:新增Vite代理配置与自动构建Target
- 补全测试与文档:新增集成测试,更新开发与部署文档
This commit is contained in:
ShaoHua
2026-08-08 07:33:44 +08:00
parent 4fe0b5a963
commit 3213d4c718
32 changed files with 1937 additions and 86 deletions
+87
View File
@@ -4,6 +4,16 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!--
BuildHuaTodoWebWebRoot 相关路径:
HuaTodoWebProjectDir 相对于 Host.csproj(当前文件位于 src/Hua.Todo.Host/ → 上一级是 src/ → src/Hua.Todo.Web)
SPA 构建产物复制到 $(MSBuildProjectDirectory)/wwwroot/spa/(与 admin/ 平级)
WebRoot 默认是 $(MSBuildProjectDirectory)/wwwroot;构建 npm 输出目录由 vite.config.ts build.outDir 指定为 Hua.Todo.Web/dist
-->
<HuaTodoWebProjectDir>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../Hua.Todo.Web'))</HuaTodoWebProjectDir>
<HuaTodoWebOutputDir>$(HuaTodoWebProjectDir)/dist</HuaTodoWebOutputDir>
<HuaTodoWebSrcDir>$(HuaTodoWebProjectDir)/src</HuaTodoWebSrcDir>
<HuaTodoWebPkgJson>$(HuaTodoWebProjectDir)/package.json</HuaTodoWebPkgJson>
</PropertyGroup>
<ItemGroup>
@@ -24,4 +34,81 @@
<ProjectReference Include="..\Hua.Todo.HttpApi.AspNetCore\Hua.Todo.HttpApi.AspNetCore.csproj" />
</ItemGroup>
<!--
Target: BuildHuaTodoWebWebRoot
BeforeTargets="BeforeBuild" → 在每次 dotnet build/publish Hua.Todo.Host 前执行
Inputs/Outputs → 支持增量构建:package.json + src/**/*.{ts,vue,css,html} 不变时跳过 npm build
Node/npm 不存在 → 只 Warning,不阻塞 API 层 build(外部 CI 可先 build 前端)
步骤:
1) Exec 检测 node -v / npm -v 是否可用
2) 若 node_modules 不存在:npm ci(优先,依赖锁定)或 npm install
3) npm run build → Hua.Todo.Web/dist
4) Copy $(HuaTodoWebOutputDir)/** → $(MSBuildProjectDirectory)/wwwroot/spa/ 和 $(OutputPath)/wwwroot/spa/ 保留目录结构
(与 wwwroot/admin/ 平级;本地 dotnet run 用项目 wwwroot/spa,dotnet publish 用 OutputPath/wwwroot/spa)
-->
<Target Name="BuildHuaTodoWebWebRoot"
BeforeTargets="BeforeBuild"
Inputs="$(HuaTodoWebPkgJson);$(HuaTodoWebSrcDir)**/*.ts;$(HuaTodoWebSrcDir)**/*.vue;$(HuaTodoWebSrcDir)**/*.css;$(HuaTodoWebSrcDir)**/*.html"
Outputs="$(MSBuildProjectDirectory)/wwwroot/spa/index.html;$(OutputPath)/wwwroot/spa/index.html"
Condition="'$(SkipBuildHuaTodoWebWebRoot)' != 'true'">
<!-- 1. 检测 Node/npm 是否可用(CI 常缺) -->
<Exec Command="node -v" ConsoleToMsBuild="true" IgnoreExitCode="true" ContinueOnError="WarnAndContinue">
<Output TaskParameter="ExitCode" PropertyName="NodeExitCode" />
</Exec>
<Exec Command="npm -v" ConsoleToMsBuild="true" IgnoreExitCode="true" ContinueOnError="WarnAndContinue">
<Output TaskParameter="ExitCode" PropertyName="NpmExitCode" />
</Exec>
<Warning Condition="'$(NodeExitCode)' != '0' Or '$(NpmExitCode)' != '0'"
Text="[BuildHuaTodoWebWebRoot] 未检测到 Node/npm,跳过前端构建。
Host wwwroot/spa 将不包含 Hua.Todo.Web 静态页面,HTTP GET / 会返回 404。
请安装 Node.js(&gt;= 18)或设置 SkipBuildHuaTodoWebWebRoot=true 抑制本警告。
CI 可在外层先执行:cd src/Hua.Todo.Web &amp;&amp; npm ci &amp;&amp; npm run build。" />
<!-- 2. npm install(仅当 node_modules 不存在时) -->
<Exec Condition="'$(NodeExitCode)' == '0' And '$(NpmExitCode)' == '0' And !Exists('$(HuaTodoWebProjectDir)node_modules')"
Command="npm install"
WorkingDirectory="$(HuaTodoWebProjectDir)"
ConsoleToMsBuild="true"
ContinueOnError="WarnAndContinue">
<Output TaskParameter="ExitCode" PropertyName="NpmInstallExitCode" />
</Exec>
<!-- 3. npm run build(vue-tsc -b &amp;&amp; vite build) -->
<Exec Condition="'$(NodeExitCode)' == '0' And '$(NpmExitCode)' == '0'"
Command="npm run build"
WorkingDirectory="$(HuaTodoWebProjectDir)"
ConsoleToMsBuild="true"
ContinueOnError="WarnAndContinue">
<Output TaskParameter="ExitCode" PropertyName="NpmBuildExitCode" />
</Exec>
<Warning Condition="'$(NodeExitCode)' == '0' And '$(NpmExitCode)' == '0' And '$(NpmBuildExitCode)' != '0'"
Text="[BuildHuaTodoWebWebRoot] npm run build 退出码 $(NpmBuildExitCode)。
请在 src/Hua.Todo.Web 目录下手动 npm run build 排查 TypeScript/Vite 错误。" />
<!-- 4. 展开 dist 目录为 ItemGroup(一次,两步 Copy 共用) -->
<ItemGroup Condition="'$(NodeExitCode)' == '0' And '$(NpmExitCode)' == '0' And Exists('$(HuaTodoWebOutputDir)/index.html')">
<_WebDist Include="$(HuaTodoWebOutputDir)/**/*" />
</ItemGroup>
<!-- 4a. 拷贝到 Host 项目 wwwroot/spa(本地 dotnet run 用) -->
<Copy Condition="'@(_WebDist)' != ''"
SourceFiles="@(_WebDist)"
DestinationFiles="@(_WebDist-&gt;'$(MSBuildProjectDirectory)/wwwroot/spa/%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
<!-- 4b. 拷贝到 $(OutputPath)/wwwroot/spa(dotnet build/publish 打包输出用) -->
<Copy Condition="'@(_WebDist)' != ''"
SourceFiles="@(_WebDist)"
DestinationFiles="@(_WebDist-&gt;'$(OutputPath)/wwwroot/spa/%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
<Message Condition="Exists('$(MSBuildProjectDirectory)/wwwroot/spa/index.html')"
Importance="High"
Text="[BuildHuaTodoWebWebRoot] 前端静态页已复制到 Host wwwroot/spa/(增量构建命中:Inputs 未变化时本 Target 整体跳过)。" />
</Target>
</Project>
+232 -3
View File
@@ -1,3 +1,5 @@
using System.Reflection;
using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Serilog;
@@ -5,7 +7,7 @@ using Hua.Todo.Application.Common;
using Hua.Todo.Application.Models;
using Hua.Todo.Application.Repositories;
using Hua.Todo.HttpApi;
using Hua.Todo.HttpApi.AspNetCore.CloudSync.Services;
using Hua.Todo.HttpApi.AspNetCore.CloudSync;
using Hua.Todo.HttpApi.AspNetCore.DynamicApi;
using Hua.Todo.HttpApi.AspNetCore.DynamicApi.Swagger;
using Hua.Todo.HttpApi.AspNetCore.Mcp;
@@ -16,6 +18,11 @@ Log.Logger = LoggingConfiguration.Create(
Log.Information("[Startup] Serilog 初始化完成");
var builder = WebApplication.CreateBuilder(args);
// 显式指定开发环境监听端口:HTTP 5173(与前端云同步默认服务器地址对齐),HTTPS 7055
// 开发环境默认端口 5000/5001 与前端配置不一致,导致探测失败
builder.WebHost.UseUrls(
"http://0.0.0.0:5173",
"https://0.0.0.0:7055");
builder.Host.UseSerilog();
Log.Information("[Startup] WebApplication.CreateBuilder 完成");
@@ -29,7 +36,10 @@ Log.Information("[Startup] Swagger 注册完成");
builder.Services.AddApplicationServices("Data Source=Hua.Todo.db");
Log.Information("[Startup] AddApplicationServices 完成");
// 注册云同步服务端能力(认证 / RBAC / 二次认证 / 任务同步 / 安全策略)
// Host 双角色统一部署(不区分「模式 A / 模式 B」):
// 角色 1 - Web 端:用户浏览器直接访问 Host → 同域下承载前端静态页、Dynamic API、Swagger、MCP
// 角色 2 - 云同步服务端:MAUI / Avalonia 嵌入式客户端通过 CloudSyncProxy 转发到 Host /api/*
// 因此必须同时注册:AddApplicationServices + AddCloudSyncServer + AddMcpServerServices
builder.Services.AddCloudSyncServer();
Log.Information("[Startup] AddCloudSyncServer 完成");
@@ -60,13 +70,47 @@ using (var scope = app.Services.CreateScope())
Log.Information("[Startup] EF Core Migrate 完成");
}
// === Host 中间件顺序严格对齐 v1.3.0 00-工单总览 §5 硬约束:
// Swagger → MinimalBootstrapPage(仅当 wwwroot/index.html 不存在时生效)
// → UseDefaultFiles/UseStaticFiles → CORS → Authentication
// → Authorization → UseDynamicApi → MapMcpServer → MapFallbackToFile → Run
// 顺序错误会导致 /api/* 或 /swagger/* 被 MapFallbackToFile 覆盖成返回 index.html。
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// 前端构建缺失兜底:若 wwwroot/spa/index.html 不存在,则在 MapFallbackToFile 之前的 404 路径上
// 返回一个最小引导页(中文错误提示 + 构建命令 + 文档链接),不让用户看到空白 404。
// 前端构建成功后此中间件不生效(短路判断:文件存在直接 _next)。
app.UseMiddleware<MinimalBootstrapPageMiddleware>();
// 静态文件(分层,按优先级从高到低):
// 1) UseDefaultFiles + UseStaticFiles(wwwroot 根)→ 服务 admin/ 等非 SPA 静态文件
// 2) UseDefaultFiles + UseStaticFiles(spa provider,映射到 /)→ 服务 SPA 的 assets/favicon 等
// 放在第 2 层是因为 admin 等目录不希望在 SPA provider 下被覆盖
// 3) MapFallbackToFile → SPA 路由(/settings 等)及 admin 路由(/admin/xxx)
// 注意:放在 UseDynamicApi / MapMcpServer 之后,
// 否则会先匹配返回 index.html,/api/* /mcp 永远走不到。
app.UseDefaultFiles();
app.UseStaticFiles();
// SPA 静态资源(wwwroot/spa/ 映射到根 /)
// 浏览器直访 /favicon.svg → 命中 wwwroot/spa/favicon.svg
// 未命中则 fallback 到下面的 MapFallbackToFile
var spaRoot = Path.Combine(app.Environment.WebRootPath, "spa");
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(spaRoot) });
app.UseStaticFiles(new StaticFileOptions { FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(spaRoot) });
// 开发环境跳过 HTTPS 重定向:
// 1) 避免前端 axios 把 307 当错误直接 toast;
// 2) DynamicApi 的匿名端点(/probe、/login、/bootstrap)在未登录场景使用 HTTP 即可;
// 生产部署推荐由反向代理(Nginx/Caddy)统一处理 TLS 重定向,ASP.NET Core 不必二次重定向。
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
@@ -75,4 +119,189 @@ app.UseDynamicApi();
// 映射 MCP Server 端点(Streamable HTTP,路径 /mcp)
app.MapMcpServer();
// Admin 后台 fallback:/admin 或 /admin/xxx 返回 wwwroot/admin/index.html
app.MapFallbackToFile("/admin/{**rest}", "admin/index.html");
// SPA 路由 fallback(放在最后,优先级最低,不覆盖 /api/* /mcp /swagger /admin)
// 注意:若 wwwroot/spa/index.html 不存在,MinimalBootstrapPageMiddleware 已先返回引导页,这里不会触发 404。
app.MapFallbackToFile("spa/index.html");
app.Run();
/// <summary>
/// 前端静态页缺失时的最小引导页中间件。
///
/// 触发条件:Host 启动时,WebRoot 下找不到 index.html(BuildHuaTodoWebWebRoot MSBuild Target 未执行、
/// Node 缺失、手动跳过构建等)。
/// 作用:拦截所有"非 /api/*、非 /swagger、非 mcp"的 GET 请求,如果最终命中到 404(因为
/// UseStaticFiles / MapFallbackToFile 都找不到文件),则返回一个最小的引导 HTML 页面,
/// 让用户一眼看出是"前端没构建"而不是"部署挂了 / 404"。
/// </summary>
internal sealed class MinimalBootstrapPageMiddleware
{
private readonly RequestDelegate _next;
private readonly IWebHostEnvironment _env;
private readonly ILogger<MinimalBootstrapPageMiddleware> _logger;
private readonly string _indexHtmlPath;
private readonly Lazy<bool> _indexHtmlExists;
public MinimalBootstrapPageMiddleware(
RequestDelegate next,
IWebHostEnvironment env,
ILogger<MinimalBootstrapPageMiddleware> logger)
{
_next = next;
_env = env;
_logger = logger;
_indexHtmlPath = Path.Combine(env.WebRootPath, "spa", "index.html");
// 启动时只判断一次文件是否存在,避免每次请求都 IO
_indexHtmlExists = new Lazy<bool>(() =>
{
var exists = File.Exists(_indexHtmlPath);
if (!exists)
{
logger.LogWarning(
"[MinimalBootstrapPage] 未找到前端入口文件 {IndexHtmlPath},将启用最小引导页。" +
"请执行:cd src/Hua.Todo.Web &amp;&amp; npm install &amp;&amp; npm run build ;" +
"或重新 build Host:dotnet build src/Hua.Todo.Host(会自动触发 BuildHuaTodoWebWebRoot Target)。" +
"CI 可设置 -p:SkipBuildHuaTodoWebWebRoot=true 并在外层 build 前端后拷贝 wwwroot/spa。",
_indexHtmlPath);
}
return exists;
});
}
public async Task InvokeAsync(HttpContext context)
{
await _next(context);
// 只处理:status 404 + 非 /api/* + 非 /swagger/* + 非 /mcp + 非 /admin/*
// + GET 或 HEAD + Accept 包含 text/html(浏览器直接访问页面,不是 XHR/Fetch/API)
if (_indexHtmlExists.Value) return;
if (context.Response.StatusCode != StatusCodes.Status404NotFound) return;
var method = context.Request.Method;
if (!HttpMethods.IsGet(method) && !HttpMethods.IsHead(method)) return;
var path = context.Request.Path.Value ?? string.Empty;
if (path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/swagger", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/mcp", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/admin/", StringComparison.OrdinalIgnoreCase))
{
return;
}
var accept = context.Request.Headers["Accept"].ToString();
if (string.IsNullOrEmpty(accept) ||
!accept.Contains("text/html", StringComparison.OrdinalIgnoreCase))
{
return;
}
// 开始写响应——先重置 status/content-type,再写 body
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.ContentType = "text/html; charset=utf-8";
var appVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0";
var html = BuildBootstrapHtml(appVersion, _indexHtmlPath, _env.WebRootPath, _env.EnvironmentName);
await context.Response.WriteAsync(html, Encoding.UTF8);
}
private static string BuildBootstrapHtml(string appVersion, string indexHtmlPath, string webRootPath, string envName)
{
var idx = HtmlEncode(indexHtmlPath);
var wr = HtmlEncode(webRootPath);
var env = HtmlEncode(envName);
var ver = HtmlEncode(appVersion);
const string S =
@"<!doctype html>
<html lang=""zh-CN"">
<head>
<meta charset=""utf-8"" />
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
<title>Hua.Todo - 前端尚未构建</title>
<style>
:root { color-scheme: light dark; }
body { font-family: -apple-system, ""Segoe UI"", Roboto, ""PingFang SC"", ""Microsoft YaHei"", sans-serif;
margin: 0; padding: 40px 20px; line-height: 1.6; }
.wrap { max-width: 860px; margin: 0 auto; }
h1 { font-size: 22px; margin: 0 0 12px; }
.badge { display:inline-block; padding: 2px 10px; border-radius: 4px;
background:#fff4ce; color:#7a5a00; font-size:12px; margin-bottom:16px; }
@media (prefers-color-scheme: dark) {
.badge { background:#3a2f00; color:#ffe58f; }
}
pre { background: #0b0f14; color: #dbeafe; padding: 14px 16px;
border-radius: 8px; overflow-x: auto; font-size: 13px; }
code { font-family: ""JetBrains Mono"", Consolas, Menlo, monospace; }
.section { margin: 24px 0; padding: 16px 20px; border: 1px solid #e5e7eb; border-radius: 8px; }
ul { padding-left: 22px; }
.foot { opacity: .6; font-size: 12px; margin-top: 30px; }
</style>
</head>
<body>
<div class=""wrap"">
<span class=""badge"">提示:前端尚未构建,仅展示引导页(不影响 API/Swagger/MCP/云同步)</span>
<h1>Hua.Todo Host 已启动,但前端入口文件缺失</h1>
<p>
浏览器访问 <code>/</code> 或 <code>/settings</code> 等 SPA 路由时,
需要 <code>wwwroot/index.html</code> 才能加载完整的 Todo 前端界面。
当前 Host 仍在正常工作中:Dynamic API、Swagger、MCP、云同步服务端均已可用,不受影响。
</p>
<div class=""section"">
<h3>检查项</h3>
<ul>
<li>期望路径:<code>__IDX__</code>(当前不存在)</li>
<li>WebRoot:<code>__WR__</code></li>
<li>环境:<code>__ENV__</code> · 版本:<code>__VER__</code></li>
</ul>
</div>
<div class=""section"">
<h3>如何修复(任选一种)</h3>
<h4>方式一:重新 build Host(推荐,会自动触发 <code>BuildHuaTodoWebWebRoot</code> MSBuild Target)</h4>
<pre><code>cd d:/Proj/6.Hua.Todo
dotnet build src/Hua.Todo.Host -c Release</code></pre>
<h4>方式二:手动构建前端再 copy</h4>
<pre><code>cd src/Hua.Todo.Web
npm install
npm run build
# dist/**/* 自动被 Target 拷贝到:
# 1) src/Hua.Todo.Host/wwwroot/ (本地 dotnet run 用)
# 2) bin/Release/net10.0/wwwroot/ (dotnet publish 用)</code></pre>
<h4>方式三:CI 无 Node 环境 → 外层 build 前端再 publish</h4>
<pre><code>cd src/Hua.Todo.Web &amp;&amp; npm ci &amp;&amp; npm run build
cd ../..
dotnet publish src/Hua.Todo.Host -c Release `
-p:SkipBuildHuaTodoWebWebRoot=true
-o ./artifacts/host</code></pre>
</div>
<div class=""section"">
<h3>在等待前端构建期间,你仍可以使用以下入口</h3>
<ul>
<li><a href=""/swagger/index.html"">Swagger UI - 接口调试 <code>/swagger/index.html</code></a></li>
<li><code>POST /api/auth/bootstrap</code>(首次初始化管理员账号)</li>
<li><code>POST /api/auth/login</code>(Web 端登录)</li>
<li><code>GET /api/cloud-sync-proxy-settings</code>(云同步代理设置)</li>
<li><code>GET /mcp</code>(MCP Server 端点,Streamable HTTP)</li>
</ul>
</div>
<div class=""foot"">
Hua.Todo · Host 双角色统一部署 = Web 端前端/后端 + 云同步服务端 + MCP · 版本 __VER__
</div>
</div>
</body>
</html>";
return S.Replace("__IDX__", idx).Replace("__WR__", wr).Replace("__ENV__", env).Replace("__VER__", ver);
}
private static string HtmlEncode(string? value) =>
string.IsNullOrEmpty(value) ? string.Empty : System.Net.WebUtility.HtmlEncode(value);
}
+45 -29
View File
@@ -189,11 +189,16 @@
const creatingUser = ref(false);
const newUser = reactive({ userName: '', password: '', role: 'user' });
// Axios 拦截器:注入 Token
// Axios 拦截器:注入 Token(与 SPA 共用 sessionStorage['Hua.Todo_cloud_sync_session'])
axios.interceptors.request.use(config => {
const token = localStorage.getItem('admin_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
const raw = sessionStorage.getItem('Hua.Todo_cloud_sync_session');
if (raw) {
try {
const session = JSON.parse(raw);
if (session.accessToken) {
config.headers.Authorization = `Bearer ${session.accessToken}`;
}
} catch (e) {}
}
return config;
});
@@ -211,13 +216,16 @@
);
const checkAuth = () => {
const token = localStorage.getItem('admin_token');
const user = localStorage.getItem('admin_user');
if (token && user) {
isLoggedIn.value = true;
currentUser.value = JSON.parse(user);
return true;
}
const raw = sessionStorage.getItem('Hua.Todo_cloud_sync_session');
if (!raw) return false;
try {
const session = JSON.parse(raw);
if (session.accessToken && session.role === 'admin') {
isLoggedIn.value = true;
currentUser.value = { userName: session.userName || '管理员', role: session.role };
return true;
}
} catch (e) {}
return false;
};
@@ -227,18 +235,27 @@
}
loggingIn.value = true;
try {
const res = await axios.post('/auth/login', loginForm);
const data = res.data;
const res = await axios.post('/api/auth/login', loginForm);
// DynamicApi 响应:{ success, data: LoginResponse, message }
const payload = res.data.data;
if (data.role !== 'admin') {
if (!payload || payload.role !== 'admin') {
throw new Error('只有管理员可以访问此后台');
}
localStorage.setItem('admin_token', data.accessToken);
localStorage.setItem('admin_user', JSON.stringify({ userName: loginForm.userName, role: data.role }));
// 与 SPA 共用 sessionStorage['Hua.Todo_cloud_sync_session'] 格式
const session = {
accessToken: payload.accessToken,
expiresAtUtc: payload.expiresAtUtc,
userId: payload.userId,
role: payload.role,
permissions: payload.permissions || [],
userName: loginForm.userName
};
sessionStorage.setItem('Hua.Todo_cloud_sync_session', JSON.stringify(session));
isLoggedIn.value = true;
currentUser.value = { userName: loginForm.userName, role: data.role };
currentUser.value = { userName: loginForm.userName, role: payload.role };
loginForm.password = '';
ElementPlus.ElMessage.success('登录成功');
@@ -251,8 +268,7 @@
};
const handleLogout = () => {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
sessionStorage.removeItem('Hua.Todo_cloud_sync_session');
isLoggedIn.value = false;
currentUser.value = { userName: '', role: '' };
};
@@ -261,8 +277,8 @@
if (!isLoggedIn.value) return;
loadingUsers.value = true;
try {
const res = await axios.get('/admin/users');
users.value = res.data;
const res = await axios.get('/api/admin/users');
users.value = res.data.data || [];
} catch (e) {} finally {
loadingUsers.value = false;
}
@@ -272,8 +288,8 @@
if (!isLoggedIn.value) return;
loadingSessions.value = true;
try {
const res = await axios.get('/admin/sessions');
sessions.value = res.data;
const res = await axios.get('/api/admin/sessions');
sessions.value = res.data.data || [];
} catch (e) {} finally {
loadingSessions.value = false;
}
@@ -283,8 +299,8 @@
if (!isLoggedIn.value) return;
loadingLogs.value = true;
try {
const res = await axios.get('/admin/audit-logs?count=100');
logs.value = res.data;
const res = await axios.get('/api/admin/audit-logs?count=100');
logs.value = res.data.data || [];
} catch (e) {} finally {
loadingLogs.value = false;
}
@@ -303,7 +319,7 @@
}
creatingUser.value = true;
try {
await axios.post('/admin/users', newUser);
await axios.post('/api/admin/users', newUser);
ElementPlus.ElMessage.success('创建用户成功');
showCreateUserDialog.value = false;
newUser.userName = '';
@@ -323,7 +339,7 @@
inputType: 'password'
});
if (newPass) {
await axios.post(`/admin/users/${user.id}/reset-password`, { newPassword: newPass });
await axios.post(`/api/admin/users/${user.id}/reset-password`, { newPassword: newPass });
ElementPlus.ElMessage.success('重置成功');
}
} catch (e) {}
@@ -337,7 +353,7 @@
cancelButtonText: '取消',
type: 'warning'
});
await axios.delete(`/admin/users/${user.id}`);
await axios.delete(`/api/admin/users/${user.id}`);
ElementPlus.ElMessage.success('删除成功');
fetchUsers();
} catch (e) {}
@@ -345,7 +361,7 @@
const handleRevokeSession = async (session) => {
try {
await axios.delete(`/admin/sessions/${session.id}`);
await axios.delete(`/api/admin/sessions/${session.id}`);
ElementPlus.ElMessage.success('已强制下线');
fetchSessions();
} catch (e) {}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hua.Todo-web</title>
<script type="module" crossorigin src="/assets/index-xkexrwRb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-7aro5nrm.css">
</head>
<body>
<div id="app"></div>
</body>