diff --git a/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs b/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs index 4c05e1c..4a323dd 100644 --- a/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs +++ b/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs @@ -170,6 +170,25 @@ public sealed class CourseAdjustmentsController( return Ok(new { Task = task, Sessions = sessions, Classrooms = classrooms, Periods = periods }); } + [HttpGet("teacher-options")] + [Authorize(Roles = Applicants)] + public async Task GetTeacherOptions(CancellationToken cancellationToken) + { + return Ok(await db.Teachers.AsNoTracking() + .Where(x => x.Status == TeacherStatus.Active && !x.IsArchived) + .OrderBy(x => x.College!.Name) + .ThenBy(x => x.TeacherNumber) + .Select(x => new + { + x.Id, + x.TeacherNumber, + x.Name, + x.Title, + CollegeName = x.College!.Name + }) + .ToListAsync(cancellationToken)); + } + // ═══════════════ My adjustments ═══════════════ [HttpGet("mine")] @@ -611,7 +630,8 @@ public sealed class CourseAdjustmentsController( return ValidationProblem("请选择代课教师。"); if (!await db.Teachers.AnyAsync(x => x.Id == request.SubstituteTeacherId && - x.Status == TeacherStatus.Active, cancellationToken)) + x.Status == TeacherStatus.Active && + !x.IsArchived, cancellationToken)) return ValidationProblem("代课教师不存在或已离职。"); break; case CourseAdjustmentType.Cancel: diff --git a/src/Jiaowu.Api/Controllers/SsoController.cs b/src/Jiaowu.Api/Controllers/SsoController.cs index df2b646..fb051a3 100644 --- a/src/Jiaowu.Api/Controllers/SsoController.cs +++ b/src/Jiaowu.Api/Controllers/SsoController.cs @@ -56,6 +56,7 @@ public sealed class SsoController( return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound); var safeReturnUrl = NormalizeReturnUrl(returnUrl); + var accountPage = FrontendPagePath(safeReturnUrl, "account", nativeApp); if (string.IsNullOrWhiteSpace(bindingIntent) && (string.IsNullOrWhiteSpace(captchaId) || string.IsNullOrWhiteSpace(captchaCode) || @@ -77,7 +78,7 @@ public sealed class SsoController( BindingIntentCacheKey(bindingIntent), cancellationToken); if (targetUserId is null) - return RedirectToFrontendError("binding_intent_expired", "/account"); + return RedirectToFrontendError("binding_intent_expired", accountPage); properties.Items[BindingIntentProperty] = bindingIntent; } var completeUrl = Url.Action( @@ -97,7 +98,9 @@ public sealed class SsoController( EffectiveCallbackUrl()); return RedirectToFrontendError( "configuration_error", - string.IsNullOrWhiteSpace(bindingIntent) ? "/login" : "/account"); + string.IsNullOrWhiteSpace(bindingIntent) + ? FrontendPagePath(safeReturnUrl, "login", nativeApp) + : accountPage); } } @@ -111,16 +114,23 @@ public sealed class SsoController( if (!_options.Enabled) return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound); + var safeReturnUrl = NormalizeReturnUrl(returnUrl); var authentication = await HttpContext.AuthenticateAsync( SsoAuthSchemes.ExternalCookie); + var nativeApp = authentication.Properties is { } externalProperties && + externalProperties.Items.TryGetValue(NativeAppProperty, out var nativeAppValue) && + bool.TryParse(nativeAppValue, out var isNativeApp) && + isNativeApp; + var loginPage = FrontendPagePath(safeReturnUrl, "login", nativeApp); + var accountPage = FrontendPagePath(safeReturnUrl, "account", nativeApp); if (!authentication.Succeeded || authentication.Principal is null) - return RedirectToFrontendError("authentication_failed"); + return RedirectToFrontendError("authentication_failed", loginPage); var principal = authentication.Principal; var subject = principal.FindFirstValue("sub") ?? principal.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrWhiteSpace(subject)) - return RedirectToFrontendError("missing_subject"); + return RedirectToFrontendError("missing_subject", loginPage); ApplicationUser? user = null; var bindingIntent = @@ -130,10 +140,6 @@ public sealed class SsoController( out var storedBindingIntent) ? storedBindingIntent : null; - var nativeApp = authentication.Properties is { } externalProperties && - externalProperties.Items.TryGetValue(NativeAppProperty, out var nativeAppValue) && - bool.TryParse(nativeAppValue, out var isNativeApp) && - isNativeApp; var nativeState = authentication.Properties is { } nativeProperties && nativeProperties.Items.TryGetValue(NativeAppStateProperty, out var storedNativeState) ? storedNativeState @@ -147,13 +153,13 @@ public sealed class SsoController( ? null : await userManager.FindByIdAsync(targetUserId); if (user is null) - return RedirectToFrontendError("binding_intent_expired", "/account"); + return RedirectToFrontendError("binding_intent_expired", accountPage); if (!user.IsEnabled || await userManager.IsLockedOutAsync(user)) - return RedirectToFrontendError("account_disabled", "/account"); + return RedirectToFrontendError("account_disabled", accountPage); var linkError = await LinkSsoIdentityAsync(user, subject); if (linkError is not null) - return RedirectToFrontendError(linkError, "/account"); + return RedirectToFrontendError(linkError, accountPage); await cache.RemoveAsync( BindingIntentCacheKey(bindingIntent), cancellationToken); @@ -172,7 +178,7 @@ public sealed class SsoController( { var linkError = await LinkSsoIdentityAsync(user, subject); if (linkError is not null) - return RedirectToFrontendError(linkError); + return RedirectToFrontendError(linkError, loginPage); } } } @@ -195,18 +201,20 @@ public sealed class SsoController( cancellationToken); await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie); - var bindingPage = BuildFrontendUrl("/sso/bind", nativeApp) + + var bindingPage = BuildFrontendUrl( + FrontendPagePath(safeReturnUrl, "sso/bind", nativeApp), + nativeApp) + $"?code={Uri.EscapeDataString(bindingCode)}" + - $"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}"; + $"&redirect={Uri.EscapeDataString(safeReturnUrl)}"; return Redirect(bindingPage); } if (!user.IsEnabled || await userManager.IsLockedOutAsync(user)) - return RedirectToFrontendError("account_disabled"); + return RedirectToFrontendError("account_disabled", loginPage); user.LastLoginAt = DateTime.UtcNow; var updateResult = await userManager.UpdateAsync(user); if (!updateResult.Succeeded) - return RedirectToFrontendError("account_update_failed"); + return RedirectToFrontendError("account_update_failed", loginPage); var exchangeCode = WebEncoders.Base64UrlEncode( RandomNumberGenerator.GetBytes(32)); @@ -220,9 +228,11 @@ public sealed class SsoController( cancellationToken); await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie); - var callback = BuildFrontendUrl("/sso/callback", nativeApp) + + var callback = BuildFrontendUrl( + FrontendPagePath(safeReturnUrl, "sso/callback", nativeApp), + nativeApp) + $"?code={Uri.EscapeDataString(exchangeCode)}" + - $"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}"; + $"&redirect={Uri.EscapeDataString(safeReturnUrl)}"; return Redirect(callback); } @@ -386,6 +396,7 @@ public sealed class SsoController( [HttpPost("prepare-binding")] public async Task> PrepareBinding( CancellationToken cancellationToken, + [FromQuery] string? returnUrl = null, [FromQuery] bool nativeApp = false, [FromQuery] string? nativeState = null) { @@ -411,11 +422,14 @@ public sealed class SsoController( AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }, cancellationToken); + var safeReturnUrl = string.IsNullOrWhiteSpace(returnUrl) + ? "/account" + : NormalizeReturnUrl(returnUrl); var loginUrl = Url.Action( nameof(Login), values: new { - returnUrl = "/account", + returnUrl = safeReturnUrl, bindingIntent = intentCode, nativeApp, nativeState, @@ -464,6 +478,19 @@ public sealed class SsoController( ? returnUrl : "/dashboard"; + internal static string FrontendPagePath( + string? returnUrl, + string page, + bool nativeApp = false) + { + var normalizedPage = "/" + page.Trim('/'); + if (nativeApp) return normalizedPage; + var normalizedReturnUrl = NormalizeReturnUrl(returnUrl); + return normalizedReturnUrl.StartsWith("/next/", StringComparison.OrdinalIgnoreCase) + ? "/next" + normalizedPage + : normalizedPage; + } + private string BuildFrontendUrl(string path, bool nativeApp = false) { if (nativeApp) diff --git a/tests/Jiaowu.Api.Tests/CourseAdjustmentsControllerTests.cs b/tests/Jiaowu.Api.Tests/CourseAdjustmentsControllerTests.cs index bd19a33..9df3c0f 100644 --- a/tests/Jiaowu.Api.Tests/CourseAdjustmentsControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/CourseAdjustmentsControllerTests.cs @@ -6,11 +6,46 @@ using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using System.Text.Json; namespace Jiaowu.Api.Tests; public sealed class CourseAdjustmentsControllerTests { + [Fact] + public async Task TeacherOptions_ReturnOnlyActiveNonArchivedTeachers() + { + await using var fixture = await Fixture.CreateAsync(); + var collegeId = await fixture.Db.Courses.Select(x => x.CollegeId).SingleAsync(); + fixture.Db.Teachers.AddRange( + new Teacher + { + TeacherNumber = "T002", + Name = "离职教师", + CollegeId = collegeId, + Status = TeacherStatus.Retired + }, + new Teacher + { + TeacherNumber = "T003", + Name = "归档教师", + CollegeId = collegeId, + IsArchived = true + }); + await fixture.Db.SaveChangesAsync(); + + var controller = new CourseAdjustmentsController( + fixture.Db, + new TestScope(fixture.TeacherUserId, DataScope.Self, SystemRoles.Teacher)); + var result = await controller.GetTeacherOptions(CancellationToken.None); + + var ok = Assert.IsType(result); + var json = JsonSerializer.Serialize(ok.Value); + Assert.Contains("T001", json); + Assert.DoesNotContain("T002", json); + Assert.DoesNotContain("T003", json); + } + [Fact] public async Task Reschedule_MovesOnlyTheSelectedOccurrence() { diff --git a/tests/Jiaowu.Api.Tests/SsoControllerTests.cs b/tests/Jiaowu.Api.Tests/SsoControllerTests.cs index d906214..104d2b4 100644 --- a/tests/Jiaowu.Api.Tests/SsoControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/SsoControllerTests.cs @@ -119,6 +119,23 @@ public sealed class SsoControllerTests Assert.Equal(expected, SsoController.NormalizeReturnUrl(value)); } + [Theory] + [InlineData("/dashboard", "sso/callback", false, "/sso/callback")] + [InlineData("/next/dashboard", "sso/callback", false, "/next/sso/callback")] + [InlineData("/next/account", "account", false, "/next/account")] + [InlineData("/next/dashboard", "sso/callback", true, "/sso/callback")] + [InlineData("https://evil.example/path", "login", false, "/login")] + public void FrontendPagePath_SelectsReactOnlyForNextWebRoutes( + string? returnUrl, + string page, + bool nativeApp, + string expected) + { + Assert.Equal( + expected, + SsoController.FrontendPagePath(returnUrl, page, nativeApp)); + } + private sealed class BindingFixture : IAsyncDisposable { private readonly SqliteConnection _connection; diff --git a/web-react/.env.example b/web-react/.env.example new file mode 100644 index 0000000..69f4c67 --- /dev/null +++ b/web-react/.env.example @@ -0,0 +1,7 @@ +# Development API requests are proxied to http://localhost:5255 by Vite. +# Set this only when the API is hosted elsewhere. +VITE_API_BASE_URL= + +# Links for pages that have not yet migrated. The development default is +# http://localhost:5255; use the deployed Vue origin for shared previews. +VITE_LEGACY_APP_URL=http://localhost:5255 diff --git a/web-react/.gitignore b/web-react/.gitignore new file mode 100644 index 0000000..f4e2c6d --- /dev/null +++ b/web-react/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/web-react/MIGRATION_MATRIX.md b/web-react/MIGRATION_MATRIX.md new file mode 100644 index 0000000..99cd16d --- /dev/null +++ b/web-react/MIGRATION_MATRIX.md @@ -0,0 +1,61 @@ +# React 路由迁移矩阵 + +状态值:`inventory`、`building`、`verification`、`complete`。 + +| 路由 | 页面 | 角色/公开范围 | 关键合同 | 状态 | +| --- | --- | --- | --- | --- | +| `/login` | 登录 | 公开 | 验证码、密码、TOTP、SSO 设置、会话落盘 | building | +| `/dashboard` | 角色化首页 | 已登录用户 | 管理总览;学生课表/消息/考试/预警;教师教学班/成绩登记进度 | verification | +| `/account` | 个人账户 | 已登录用户 | TOTP、恢复码、SSO 绑定;Passkey 浏览器仪式尚未开放 | verification | +| `/users` | 用户与权限 | SuperAdmin | 列表、筛选、分页、创建、启停、重置密码、角色范围、Excel | verification | +| `/notifications` | 通知中心 | 已登录用户 | 收件箱筛选、游标分页、已读回执、权限范围/筛选/指定收件人群发、已发送 | verification | +| `/my-timetable` | 我的课表 | Student、Teacher | 当前学期周视图、考试/实验融合、Excel/PDF/ICS、订阅启停与密钥轮换 | verification | +| `/exams` | 考试/监考安排 | Student、Teacher | 已发布个人安排、考场、座位、监考教师、历史区分 | verification | +| `/grades` | 我的成绩 | Student | 学期筛选、成绩构成、学分绩点、学业指标和排名 | verification | +| `/warnings` | 我的学业预警 | Student | 预警确认、确认备注、帮扶与复核进度 | verification | +| `/my-profile` | 我的个人档案 | Student | 学籍只读信息、身份与联系资料、紧急联系人、特殊支持需求 | verification | +| `/my-attendance` | 我的考勤 | Student | 定位/二维码签到、课程归档、出勤汇总、异常记录申诉 | verification | +| `/course-selections` | 学生选课 | Student | 批次窗口、学分预算、教学班余量、选课/退选、候补顺位 | verification | +| `/my-curriculum` | 我的培养方案 | Student | 发布方案匹配、课程模块、修读状态、完成学分与失败记录 | verification | +| `/academic-overview` | 学业成绩总览 | Student | 全学期指标、班级/专业/学院排名、学期趋势 | verification | +| `/free-classrooms` | 空闲教室 | Student | 正式课表、预约占用、周次/节次/校区/容量筛选 | verification | +| `/academic-calendar` | 学术校历 | Student | 当前学期角色日程、月视图、节假日和考试提醒 | verification | +| `/student-status-changes` | 学籍异动 | Student | 休学/复学/退学申请、三级进度、提交前撤回 | verification | +| `/official-documents` | 电子凭证 | Student | 自助签发、短时去重、状态追踪、PDF 下载 | verification | +| `/graduation-audits` | 毕业审核 | Student | 发布结论、学分/必修完成度、缺失课程与复核说明 | verification | +| `/tuition` | 我的学费 | Student | 四类费用明细、已缴/待缴、校园余额分项支付 | verification | +| `/other-exams` | 其他考试 | Student | 最佳成绩、历次考试、通过/等级/分数制结果 | verification | +| `/makeup-exams` | 补考安排 | Student | 已发布场次、补考原因、考场、座位和成绩 | verification | +| `/evaluations` | 教学评价 | Student | 开放任务、动态评分维度、一次性提交 | verification | +| `/academic-planning` | 学业规划与毕业模拟 | Student | 未来学期排课、下学期建议、先修冲突、负荷和毕业时间模拟 | verification | +| `/physical-fitness` | 我的体质健康 | Student | 已发布学年、单项成绩、标准分、附加分和综合等级 | verification | +| `/degree-awards` | 我的学位授予 | Student | 正式发布结论、绩点门槛、规则或人工复核说明 | verification | +| `/graduation-clearance` | 我的毕业离校 | Student | 跨部门事项、必办标记、办结/豁免状态和整体进度 | verification | +| `/experiments` | 我的实验 | Student | 集中课次、自主场次、容量、预约与取消 | verification | +| `/experiment-grades` | 我的实验成绩 | Student | 正式发布项目、评分维度、参与状态和课程加权总分 | verification | +| `/campus-registrations` | 校园支付与报名 | Student | 开放项目、报名、审核状态、撤销、校园余额支付 | verification | +| `/approvals` | 我的审批申请 | Student | 免修、缓考、课程替代申请与审核结果 | verification | +| `/teacher-roster` | 选课名单 | Teacher | 本人授课教学班、学生联系与特殊支持信息、Excel 导出 | verification | +| `/teacher-attendance` | 教学点名 | Teacher、教务管理、Counselor | 教学班筛选、人工/扫码/定位签到、Excel、提交、风险信号与课程统计 | verification | +| `/teaching-preferences` | 授课科目申报/资格审核 | Teacher、教务管理 | 教师申报与撤回、学院审核、直接分配授课资格 | verification | +| `/course-equivalency-rules` | 等价课程规则 | 教务管理 | 规则建立、适用年级、启停与安全删除 | verification | +| `/course-enrollments` | 修读课程管理 | 教务管理 | 课程/学生双视图、多维筛选、退选递补与全量导出 | verification | +| `/campus-wallets` | 学生余额管理 | 教务管理 | 学生筛选、单笔调整、批量导入、模板与余额导出 | verification | +| `/base-data/organization` | 组织机构 | SuperAdmin、AcademicAdmin | 校区、学院、专业、行政班;筛选、分页、Excel、辅导员关联 | verification | +| `/base-data/terms` | 学年学期 | SuperAdmin、AcademicAdmin | 当前学期切换、手动归档/恢复、预检查、原因和操作历史 | verification | +| `/base-data/facilities` | 教学场所 | SuperAdmin、AcademicAdmin | 教学楼、教室、容量/场地性质筛选、Excel | verification | +| `/base-data/course-categories` | 课程分类 | SuperAdmin、AcademicAdmin | 分类增删改查、启停、排序和 Excel | verification | +| `/courses` | 课程库 | 已登录用户;教务与学院管理员可维护 | 游标分页、数据范围、先修关系、分类/性质、Excel、归档审计 | verification | +| `/curriculum` | 培养方案管理 | SuperAdmin、AcademicAdmin、CollegeAdmin | 版本、模块、课程、课程组、发布替换与已发布受控编辑 | verification | +| `/teachers`、`/students` | 教师/学生档案 | 教务、学院、辅导员;管理员可写 | 服务端分页、数据范围、完整档案、Excel、账号激活与归档审计 | verification | +| `/teaching-tasks` | 教学任务 | SuperAdmin、AcademicAdmin、CollegeAdmin | 游标分页、职责范围、草稿编制、授课资格、状态流转、批量操作与公共课合班生成 | verification | +| `/schedules` | 排课管理 | SuperAdmin、AcademicAdmin | 版本草稿、手工排课、冲突校验、后台自动排课、异步发布、作息、优化策略与课程约束 | verification | +| `/timetable`、`/class-timetable`、`/teacher-timetable/:teacherId` | 公开/管理/教师课表 | 公开;教务管理与领导可查草稿 | 班级/教师/教室资源、版本切换、周/日/总览、考试实验融合、单项与批量 Excel/PDF | verification | +| `/course-adjustments` | 调停课管理 | Teacher、SuperAdmin、AcademicAdmin、CollegeAdmin | 原课次锁定、调课/停课/补课/代课申请、草稿提交、学院范围审核、冲突复核与通知定位 | verification | +| `/classroom-reservations` | 教室借用与预约 | 已登录用户;CollegeAdmin 审核 | 空闲教室检索、课程/考试/已批准预约避让、申请撤销、学院范围审核与冲突复核 | verification | +| `/activate` | 学生账号激活 | 公开 | 学籍选项、身份匹配、强密码和一次性激活 | verification | +| `/verify/:verificationCode?` | 电子凭证验真 | 公开 | 唯一编号、实时状态、签发信息和 PDF 哈希 | verification | +| `/classroom-repairs/report/:classroomId` | 教室扫码报修 | 公开 | 教室设备、故障类别、联系方式和工单提交 | verification | +| `/venue-display/:type/:id` | 教室/楼宇信息屏 | 公开 | 当日课表、空闲状态、公告、报修二维码和自动刷新 | verification | + +后续从 `web/src/router/index.ts` 逐条补全。只有关键合同和各状态验收完整后,状态才能更新为 `complete`。 diff --git a/web-react/README.md b/web-react/README.md new file mode 100644 index 0000000..c4dcd16 --- /dev/null +++ b/web-react/README.md @@ -0,0 +1,73 @@ +# 明序校园 React 迁移应用 + +这是现有 Vue 前端的并行迁移工程。迁移完成并通过验收前,`web/` 仍是正式发布前端,`web-react/` 不覆盖 `src/Jiaowu.Api/wwwroot`。 + +## 本地运行 + +先在仓库根目录启动 API,然后在此目录执行: + +```powershell +npm.cmd install +npm.cmd run dev +``` + +访问 `http://127.0.0.1:5174/next/`。Vite 会把 `/api` 和 `/health` 转发到 `http://localhost:5255`。 + +生产构建检查: + +```powershell +npm.cmd run build +``` + +## 当前范围 + +- React、TypeScript、Vite、React Router 基础工程; +- Ant Design 主题和项目设计令牌; +- 与 Vue 版本兼容的本地会话键、刷新 Token 和 401 处理; +- 账号密码、验证码和 TOTP 登录; +- 权限感知的应用外壳; +- 读取真实 `/api/dashboard` 数据的首个页面; +- 完整迁移 `/account` 的身份信息、TOTP 和统一身份绑定流程; +- 完整迁移 `/users` 的分页筛选、Excel、账号、角色、状态和密码管理流程。 +- 迁移 `/notifications` 的收件箱、已读回执、权限范围/筛选/指定收件人群发和已发送记录; +- 迁移 `/my-timetable` 的周视图、考试与实验融合、Excel/PDF/ICS 导出和日历订阅管理。 +- 迁移 `/exams` 的学生考试与教师监考安排; +- 迁移学生 `/grades` 成绩单、绩点排名,以及 `/warnings` 预警确认和帮扶进度。 +- 迁移学生 `/my-profile` 个人档案、`/my-attendance` 定位/二维码签到与申诉、`/course-selections` 选课/退选和候补流程。 +- 迁移学生 `/my-curriculum` 培养方案进度、`/academic-overview` 学业排名趋势与 `/free-classrooms` 空闲教室查询。 +- 批量迁移学生校历、学籍异动、电子凭证、毕业审核和学费分项缴纳流程。 +- 迁移学生其他考试成绩、补考安排和教学评价提交闭环。 +- 迁移学生学业规划与毕业模拟、体质健康、学位授予和毕业离校进度。 +- 迁移学生实验预约、实验成绩、校园报名缴费及免修/缓考/课程替代申请闭环。 +- 迁移公开账号激活、电子凭证验真、教室扫码报修及场地信息屏。 +- 迁移教师选课名单工作台,支持按学期查看本人教学班、学生支持信息和 Excel 导出。 +- 迁移教师教学点名工作台,覆盖人工/扫码/定位签到、Excel 导入导出、提交及课程出勤统计。 +- 批量迁移教师授课意愿、等价课程规则、修读课程台账和学生余额管理四个教务工作流。 +- 迁移组织机构、学年学期、教学场所与课程分类基础数据,保留 Excel、服务端分页和学期归档审计闭环。 +- 迁移课程库游标台账,保留学院维护范围、先修关系、筛选导出、批量导入与归档审计。 +- 迁移培养方案管理,覆盖版本复制、模块与课程编制、公共课程组、发布替换和已发布方案受控调整。 +- 迁移教师与学生档案名册,覆盖分页筛选、完整个人资料、Excel、教师账号激活和人员归档审计。 +- 迁移教学任务管理,覆盖权限职责范围、草稿编制、授课资格、发布/归档流转、批量操作与公共课合班生成。 +- 迁移排课管理,覆盖版本修订、手工排课、后台自动排课和发布、作息、优化参数及课程约束。 +- 迁移公开、管理和指定教师课表,支持班级/教师/教室查询、版本切换、考试实验融合与批量导出。 +- 迁移调停课申请与审核工作台,覆盖原课次锁定、调课/停课/补课/代课、草稿提交、冲突复核和通知定位。 +- 迁移教室借用与预约,覆盖空闲教室检索、预约申请/撤销和学院审核闭环。 + +## 迁移纪律 + +1. 不因前端迁移修改后端业务合同;确需修改时必须单独评审。 +2. 每个页面迁移前登记路由、角色、API、写操作、查询参数和移动端行为。 +3. 页面只有在正常、空数据、异常、无权限和移动端状态都通过后才可标记完成。 +4. 未完成的页面返回 Vue 稳定版本,不提供半成品写操作。 +5. React 成为默认前端前,必须验证 PWA、Capacitor、SSO、TOTP、Passkey、文件导入导出及发布回滚。 + +## 迁移批次 + +| 批次 | 内容 | 状态 | +| --- | --- | --- | +| 0 | 工程、主题、会话、登录、应用外壳 | 进行中 | +| 1 | Dashboard、Users、Account、Notifications、个人课表、考试、学生档案、成绩、考勤、预警、选课与移动端代表页 | 已实现,进入联调验收 | +| 2 | 标准 CRUD 与基础数据模块 | 未开始 | +| 3 | 学生、教师与审批工作台 | 未开始 | +| 4 | 选课、排课、成绩、实验等复杂模块 | 未开始 | +| 5 | PWA、Capacitor、发布切换与清理 | 未开始 | diff --git a/web-react/index.html b/web-react/index.html new file mode 100644 index 0000000..b518221 --- /dev/null +++ b/web-react/index.html @@ -0,0 +1,14 @@ + + + + + + + + __APP_PRODUCT_NAME__ · React 迁移预览 + + +
+ + + diff --git a/web-react/package-lock.json b/web-react/package-lock.json new file mode 100644 index 0000000..7f9c752 --- /dev/null +++ b/web-react/package-lock.json @@ -0,0 +1,2834 @@ +{ + "name": "jiaowu-react", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jiaowu-react", + "version": "0.1.0", + "dependencies": { + "@ant-design/icons": "6.3.4", + "@capacitor/core": "8.5.0", + "@capacitor/geolocation": "8.2.2", + "@tanstack/react-query": "5.104.0", + "antd": "6.6.5", + "axios": "1.20.0", + "qrcode": "1.5.4", + "react": "19.3.0", + "react-dom": "19.3.0", + "react-router-dom": "7.18.4", + "zustand": "5.0.15" + }, + "devDependencies": { + "@types/node": "26.6.3", + "@types/qrcode": "1.5.6", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "typescript": "~7.0.2", + "vite": "8.3.1" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.3.4", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.6.0", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.6.0", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capacitor/core": { + "version": "8.5.0", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/geolocation": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.2.2.tgz", + "integrity": "sha512-X8ryw6CfL4LspJnnfVL4++DUKKZVfdUIA9JljWVRljEDIQxZQZtz6xf7qstht+ymc+le6oa5EaUChVA6KsCYlA==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.4" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, + "node_modules/@capacitor/synapse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz", + "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", + "license": "ISC" + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.22.0", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.10.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.10.0", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.3.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.8.6", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^6.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.10.0", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.3.1", + "license": "MIT", + "dependencies": { + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/listy": { + "version": "1.2.3", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.1", + "@rc-component/virtual-list": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.12.0", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.3.1", + "@rc-component/menu": "~1.5.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.5.0", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.3", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "2.0.9", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.13.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.12.2", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.13.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.1", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^8.0.0", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/segmented/node_modules/@babel/runtime": { + "version": "8.0.5", + "license": "MIT" + }, + "node_modules/@rc-component/select": { + "version": "1.10.1", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.3", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.11.1", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.13.0", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.5.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.5.2", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.10.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.16.1", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.10.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.10.1", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.3.3", + "@rc-component/portal": "^2.2.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.13.0", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^19.2.7" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.5.2", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^8.0.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list/node_modules/@babel/runtime": { + "version": "8.0.5", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.104.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.104.0", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.104.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/node": { + "version": "26.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "6.6.5", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.2", + "@ant-design/cssinjs-utils": "^2.1.2", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.3.4", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.29.2", + "@rc-component/cascader": "~1.22.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.1", + "@rc-component/dialog": "~1.10.0", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.3", + "@rc-component/form": "~1.8.6", + "@rc-component/image": "~1.10.0", + "@rc-component/input": "~1.3.1", + "@rc-component/input-number": "~1.6.2", + "@rc-component/listy": "~1.2.3", + "@rc-component/mentions": "~1.12.0", + "@rc-component/menu": "~1.5.0", + "@rc-component/motion": "^1.3.3", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~2.0.8", + "@rc-component/pagination": "~1.4.0", + "@rc-component/picker": "~1.12.2", + "@rc-component/progress": "~1.0.3", + "@rc-component/qrcode": "~2.0.0", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/segmented": "~1.4.0", + "@rc-component/select": "~1.10.1", + "@rc-component/slider": "~1.1.1", + "@rc-component/steps": "~1.2.3", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.11.1", + "@rc-component/tabs": "~1.13.0", + "@rc-component/tooltip": "~1.5.2", + "@rc-component/tour": "~2.4.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/tree-select": "~1.16.1", + "@rc-component/trigger": "^3.10.1", + "@rc-component/upload": "~1.1.1", + "@rc-component/util": "^1.13.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.23", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/react": { + "version": "19.3.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "19.3.0", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "7.18.4", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.4", + "license": "MIT", + "dependencies": { + "react-router": "7.18.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rolldown": { + "version": "1.2.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "license": "MIT" + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "7.0.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/zustand": { + "version": "5.0.15", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/web-react/package.json b/web-react/package.json new file mode 100644 index 0000000..9498967 --- /dev/null +++ b/web-react/package.json @@ -0,0 +1,33 @@ +{ + "name": "jiaowu-react", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "6.3.4", + "@capacitor/core": "8.5.0", + "@capacitor/geolocation": "8.2.2", + "@tanstack/react-query": "5.104.0", + "antd": "6.6.5", + "axios": "1.20.0", + "qrcode": "1.5.4", + "react": "19.3.0", + "react-dom": "19.3.0", + "react-router-dom": "7.18.4", + "zustand": "5.0.15" + }, + "devDependencies": { + "@types/node": "26.6.3", + "@types/qrcode": "1.5.6", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "typescript": "~7.0.2", + "vite": "8.3.1" + } +} diff --git a/web-react/src/api/files.ts b/web-react/src/api/files.ts new file mode 100644 index 0000000..4daeec3 --- /dev/null +++ b/web-react/src/api/files.ts @@ -0,0 +1,57 @@ +import type { AxiosRequestConfig } from 'axios' +import http from './http' + +function responseFileName(header: string | undefined, fallback: string) { + if (!header) return fallback + const encoded = header.match(/filename\*=UTF-8''([^;]+)/i)?.[1] + if (encoded) return decodeURIComponent(encoded) + const plain = header.match(/filename="?([^";]+)"?/i)?.[1] + return plain || fallback +} + +export async function downloadApiFile( + path: string, + fallbackName: string, + config?: AxiosRequestConfig, +) { + const response = await http.get(path, { ...config, responseType: 'blob' }) + const url = URL.createObjectURL(response.data) + try { + const anchor = document.createElement('a') + anchor.href = url + anchor.download = responseFileName(response.headers['content-disposition'], fallbackName) + document.body.appendChild(anchor) + anchor.click() + anchor.remove() + } finally { + URL.revokeObjectURL(url) + } +} + +export async function downloadApiPostFile( + path: string, + body: unknown, + fallbackName: string, +) { + const response = await http.post(path, body, { responseType: 'blob', timeout: 60000 }) + const url = URL.createObjectURL(response.data) + try { + const anchor = document.createElement('a') + anchor.href = url + anchor.download = responseFileName(response.headers['content-disposition'], fallbackName) + document.body.appendChild(anchor) + anchor.click() + anchor.remove() + } finally { + URL.revokeObjectURL(url) + } +} + +export async function importExcel(path: string, file: File) { + const form = new FormData() + form.append('file', file) + return http.post(path, form, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 60000, + }) +} diff --git a/web-react/src/api/http.ts b/web-react/src/api/http.ts new file mode 100644 index 0000000..8d6f76d --- /dev/null +++ b/web-react/src/api/http.ts @@ -0,0 +1,91 @@ +import axios from 'axios' +import { + authStorageKeys, + clearAuthSession, + markActivity, + refreshAuthSession, + refreshIfNeeded, +} from '../auth/session' + +declare module 'axios' { + export interface AxiosRequestConfig { + skipAuthSession?: boolean + } +} + +const http = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL ?? '/api', + timeout: 15000, +}) + +function isAuthenticationRequest(url?: string) { + return url?.startsWith('/auth/login') || + url?.endsWith('/auth/refresh') || + url?.endsWith('/auth/logout') || + url?.endsWith('/auth/sso/exchange') || + url?.endsWith('/auth/sso/bind') +} + +function goLogin(returnUrl: string) { + const target = `/next/login?redirect=${encodeURIComponent(returnUrl.replace(/^\/next/, '') || '/dashboard')}` + if (location.pathname + location.search !== target) location.assign(target) +} + +http.interceptors.request.use(async (config) => { + if (!isAuthenticationRequest(config.url) && !config.skipAuthSession) { + const activeToken = await refreshIfNeeded(true) + if (activeToken) markActivity() + } + const token = localStorage.getItem(authStorageKeys.token) + if (token && !config.skipAuthSession) config.headers.Authorization = `Bearer ${token}` + return config +}) + +http.interceptors.response.use( + (response) => response, + async (error) => { + const authenticationRequest = isAuthenticationRequest(error.config?.url) + const retryableConfig = error.config as (typeof error.config & { _jiaowuRetried?: boolean }) | undefined + if (error.response?.status === 401 && !authenticationRequest && !error.config?.skipAuthSession && + !retryableConfig?._jiaowuRetried) { + const token = await refreshAuthSession() + if (token && retryableConfig) { + retryableConfig._jiaowuRetried = true + retryableConfig.headers.Authorization = `Bearer ${token}` + return http.request(retryableConfig) + } + } + if (error.response?.status === 401 && !authenticationRequest && !error.config?.skipAuthSession) { + clearAuthSession() + goLogin(location.pathname + location.search + location.hash) + } + return Promise.reject(error) + }, +) + +export function apiErrorMessage(error: unknown) { + if (!axios.isAxiosError(error)) return '操作失败,请稍后重试。' + const status = error.response?.status + const data = error.response?.data + const message = typeof data?.message === 'string' ? data.message.trim() : '' + if (message) return message + if (data?.errors && typeof data.errors === 'object') { + const validationMessage = Object.values(data.errors) + .flat() + .map((item) => String(item).trim()) + .filter(Boolean) + .join(';') + if (validationMessage) return validationMessage + } + const detail = typeof data?.detail === 'string' ? data.detail.trim() : '' + if (detail) return detail + const title = typeof data?.title === 'string' ? data.title.trim() : '' + if (title) return title + if (status === 403) return '您没有权限执行此操作,请联系管理员。' + if (status === 404) return '请求的资源不存在。' + if (status === 401) return '登录已过期,请重新登录。' + if (status && status >= 500) return '服务器繁忙,请稍后重试。' + return '操作失败,请检查网络连接。' +} + +export default http diff --git a/web-react/src/app/App.tsx b/web-react/src/app/App.tsx new file mode 100644 index 0000000..ef5aa9b --- /dev/null +++ b/web-react/src/app/App.tsx @@ -0,0 +1,192 @@ +import { lazy, Suspense, useEffect } from 'react' +import { App as AntdApp, ConfigProvider, Skeleton } from 'antd' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom' +import zhCN from 'antd/locale/zh_CN' +import { useAuthStore } from '../stores/auth' + +const LoginPage = lazy(() => import('../pages/LoginPage')) +const DashboardPage = lazy(() => import('../pages/DashboardPage')) +const AccountPage = lazy(() => import('../pages/AccountPage')) +const UsersPage = lazy(() => import('../pages/UsersPage')) +const NotificationsPage = lazy(() => import('../pages/NotificationsPage')) +const MyTimetablePage = lazy(() => import('../pages/MyTimetablePage')) +const ExamsPage = lazy(() => import('../pages/ExamsPage')) +const StudentGradesPage = lazy(() => import('../pages/StudentGradesPage')) +const StudentWarningsPage = lazy(() => import('../pages/StudentWarningsPage')) +const StudentProfilePage = lazy(() => import('../pages/StudentProfilePage')) +const StudentAttendancePage = lazy(() => import('../pages/StudentAttendancePage')) +const CourseSelectionPage = lazy(() => import('../pages/CourseSelectionPage')) +const StudentCurriculumPage = lazy(() => import('../pages/StudentCurriculumPage')) +const AcademicOverviewPage = lazy(() => import('../pages/AcademicOverviewPage')) +const FreeClassroomsPage = lazy(() => import('../pages/FreeClassroomsPage')) +const StudentStatusChangesPage = lazy(() => import('../pages/StudentStatusChangesPage')) +const OfficialDocumentsPage = lazy(() => import('../pages/OfficialDocumentsPage')) +const GraduationAuditPage = lazy(() => import('../pages/GraduationAuditPage')) +const TuitionPage = lazy(() => import('../pages/TuitionPage')) +const AcademicCalendarPage = lazy(() => import('../pages/AcademicCalendarPage')) +const OtherExamsPage = lazy(() => import('../pages/OtherExamsPage')) +const MakeupExamsPage = lazy(() => import('../pages/MakeupExamsPage')) +const EvaluationsPage = lazy(() => import('../pages/EvaluationsPage')) +const AcademicPlanningPage = lazy(() => import('../pages/AcademicPlanningPage')) +const PhysicalFitnessPage = lazy(() => import('../pages/PhysicalFitnessPage')) +const DegreeAwardsPage = lazy(() => import('../pages/DegreeAwardsPage')) +const GraduationClearancePage = lazy(() => import('../pages/GraduationClearancePage')) +const StudentExperimentsPage = lazy(() => import('../pages/StudentExperimentsPage')) +const StudentExperimentGradesPage = lazy(() => import('../pages/StudentExperimentGradesPage')) +const CampusRegistrationsPage = lazy(() => import('../pages/CampusRegistrationsPage')) +const StudentApprovalsPage = lazy(() => import('../pages/StudentApprovalsPage')) +const TeacherRosterPage = lazy(() => import('../pages/TeacherRosterPage')) +const TeacherAttendancePage = lazy(() => import('../pages/TeacherAttendancePage')) +const TeachingPreferencesPage = lazy(() => import('../pages/TeachingPreferencesPage')) +const CourseEquivalencyRulesPage = lazy(() => import('../pages/CourseEquivalencyRulesPage')) +const CampusWalletManagementPage = lazy(() => import('../pages/CampusWalletManagementPage')) +const CourseEnrollmentManagementPage = lazy(() => import('../pages/CourseEnrollmentManagementPage')) +const BaseDataPage = lazy(() => import('../pages/BaseDataPage')) +const CourseCataloguePage = lazy(() => import('../pages/CourseCataloguePage')) +const CurriculumManagementPage = lazy(() => import('../pages/CurriculumManagementPage')) +const PersonnelRegistryPage = lazy(() => import('../pages/PersonnelRegistryPage')) +const TeachingTasksPage = lazy(() => import('../pages/TeachingTasksPage')) +const SchedulesPage = lazy(() => import('../pages/SchedulesPage')) +const TimetableQueryPage = lazy(() => import('../pages/TimetableQueryPage')) +const CourseAdjustmentsPage = lazy(() => import('../pages/CourseAdjustmentsPage')) +const ClassroomReservationsPage = lazy(() => import('../pages/ClassroomReservationsPage')) +const AccountActivationPage = lazy(() => import('../pages/AccountActivationPage')) +const OfficialDocumentVerifyPage = lazy(() => import('../pages/OfficialDocumentVerifyPage')) +const ClassroomRepairReportPage = lazy(() => import('../pages/ClassroomRepairReportPage')) +const VenueDisplayPage = lazy(() => import('../pages/VenueDisplayPage')) +const SsoCallbackPage = lazy(() => import('../pages/SsoCallbackPage')) +const SsoBindPage = lazy(() => import('../pages/SsoBindPage')) +const AppShell = lazy(() => import('../layout/AppShell')) + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +function ProtectedRoute() { + const token = useAuthStore((state) => state.token) + const location = useLocation() + if (!token) return + return +} + +function SessionExpiryBridge() { + const navigate = useNavigate() + useEffect(() => { + const onExpired = () => navigate(`/login?redirect=${encodeURIComponent(location.pathname + location.search + location.hash)}`, { replace: true }) + window.addEventListener('mingxu-session-expired', onExpired) + return () => window.removeEventListener('mingxu-session-expired', onExpired) + }, [navigate]) + return null +} + +function AppRoutes() { + return ( + }> + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + + ) +} + +export default function App() { + return ( + + + + + + + + ) +} diff --git a/web-react/src/auth/session.ts b/web-react/src/auth/session.ts new file mode 100644 index 0000000..06d3858 --- /dev/null +++ b/web-react/src/auth/session.ts @@ -0,0 +1,151 @@ +import axios from 'axios' +import { Capacitor } from '@capacitor/core' + +const TOKEN_KEY = 'jiaowu_token' +const REFRESH_TOKEN_KEY = 'jiaowu_refresh_token' +const ACCESS_EXPIRES_KEY = 'jiaowu_access_expires_at' +const SESSION_EXPIRES_KEY = 'jiaowu_session_expires_at' +const USER_KEY = 'jiaowu_user' +const LAST_ACTIVITY_KEY = 'jiaowu_last_activity_at' +const WEB_IDLE_MILLISECONDS = 30 * 60 * 1000 +const REFRESH_AHEAD_MILLISECONDS = 60 * 1000 +const WEB_SLIDING_TOUCH_MILLISECONDS = 60 * 1000 + +export interface AuthSessionPayload { + token: string + accessTokenExpiresAt: string + refreshToken: string + sessionExpiresAt: string + user: unknown +} + +export const isNativeApp = () => Capacitor.isNativePlatform() + +export function saveAuthSession(payload: AuthSessionPayload, recordActivity = true) { + localStorage.setItem(TOKEN_KEY, payload.token) + localStorage.setItem(REFRESH_TOKEN_KEY, payload.refreshToken) + localStorage.setItem(ACCESS_EXPIRES_KEY, payload.accessTokenExpiresAt) + localStorage.setItem(SESSION_EXPIRES_KEY, payload.sessionExpiresAt) + localStorage.setItem(USER_KEY, JSON.stringify(payload.user)) + if (recordActivity) markActivity() + window.dispatchEvent(new Event('mingxu-auth-changed')) +} + +export function clearAuthSession(notifyExpired = false) { + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(REFRESH_TOKEN_KEY) + localStorage.removeItem(ACCESS_EXPIRES_KEY) + localStorage.removeItem(SESSION_EXPIRES_KEY) + localStorage.removeItem(USER_KEY) + localStorage.removeItem(LAST_ACTIVITY_KEY) + window.dispatchEvent(new Event('mingxu-auth-changed')) + if (notifyExpired) window.dispatchEvent(new Event('mingxu-session-expired')) +} + +export function markActivity() { + localStorage.setItem(LAST_ACTIVITY_KEY, String(Date.now())) +} + +export function hasExceededWebIdleTimeout(now = Date.now()) { + if (isNativeApp()) return false + const lastActivity = Number(localStorage.getItem(LAST_ACTIVITY_KEY) ?? 0) + return lastActivity > 0 && now - lastActivity >= WEB_IDLE_MILLISECONDS +} + +export function hasLocallyExpired(now = Date.now()) { + const sessionExpiresAt = Date.parse(localStorage.getItem(SESSION_EXPIRES_KEY) ?? '') + return hasExceededWebIdleTimeout(now) || + (Number.isFinite(sessionExpiresAt) && sessionExpiresAt <= now) +} + +let refreshPromise: Promise | null = null + +export function refreshAuthSession(): Promise { + if (refreshPromise) return refreshPromise + const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY) + if (!refreshToken || hasLocallyExpired()) { + clearAuthSession(true) + return Promise.resolve(null) + } + + refreshPromise = axios.post( + `${import.meta.env.VITE_API_BASE_URL ?? '/api'}/auth/refresh`, + { refreshToken }, + { timeout: 15000 }, + ).then(({ data }) => { + saveAuthSession(data, false) + return data.token + }).catch((error: unknown) => { + const currentRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY) + if (currentRefreshToken && currentRefreshToken !== refreshToken) { + return localStorage.getItem(TOKEN_KEY) + } + if (axios.isAxiosError(error) && error.response && [400, 401, 403].includes(error.response.status)) { + clearAuthSession(true) + return null + } + throw error + }).finally(() => { + refreshPromise = null + }) + return refreshPromise +} + +export async function refreshIfNeeded(isCurrentRequestActivity = false) { + const token = localStorage.getItem(TOKEN_KEY) + if (!token) return null + if (hasLocallyExpired()) { + clearAuthSession(true) + return null + } + const expiresAt = Date.parse(localStorage.getItem(ACCESS_EXPIRES_KEY) ?? '') + const sessionExpiresAt = Date.parse(localStorage.getItem(SESSION_EXPIRES_KEY) ?? '') + const lastActivity = Number(localStorage.getItem(LAST_ACTIVITY_KEY) ?? 0) + const now = Date.now() + const hasRecentWebActivity = !isNativeApp() && + (isCurrentRequestActivity || now - lastActivity <= WEB_SLIDING_TOUCH_MILLISECONDS) + const needsSlidingTouch = hasRecentWebActivity && Number.isFinite(sessionExpiresAt) && + sessionExpiresAt - now <= WEB_IDLE_MILLISECONDS - WEB_SLIDING_TOUCH_MILLISECONDS + if (!Number.isFinite(expiresAt) || expiresAt - now <= REFRESH_AHEAD_MILLISECONDS || needsSlidingTouch) { + return refreshAuthSession() + } + return token +} + +let initialized = false + +export function initializeAuthSession() { + if (initialized) return + initialized = true + if (localStorage.getItem(TOKEN_KEY) && hasLocallyExpired()) clearAuthSession(true) + + let lastActivityWrite = 0 + const recordActivity = () => { + if (!localStorage.getItem(TOKEN_KEY)) return + const now = Date.now() + if (now - lastActivityWrite < 5000) return + lastActivityWrite = now + markActivity() + } + const activityEvents: Array = ['pointerdown', 'keydown', 'touchstart', 'scroll'] + activityEvents.forEach((event) => window.addEventListener(event, recordActivity, { passive: true })) + + window.setInterval(() => { + if (!localStorage.getItem(TOKEN_KEY)) return + if (hasLocallyExpired()) { + clearAuthSession(true) + return + } + if (document.visibilityState === 'visible') void refreshIfNeeded().catch(() => undefined) + }, 30000) + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') void refreshIfNeeded(true).catch(() => undefined) + }) + window.addEventListener('online', () => void refreshIfNeeded(true).catch(() => undefined)) +} + +export const authStorageKeys = { + token: TOKEN_KEY, + refreshToken: REFRESH_TOKEN_KEY, + user: USER_KEY, +} diff --git a/web-react/src/components/BrandMark.tsx b/web-react/src/components/BrandMark.tsx new file mode 100644 index 0000000..760bc70 --- /dev/null +++ b/web-react/src/components/BrandMark.tsx @@ -0,0 +1,11 @@ +interface BrandMarkProps { + large?: boolean +} + +export default function BrandMark({ large = false }: BrandMarkProps) { + return ( +