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..ac13963
--- /dev/null
+++ b/web-react/MIGRATION_MATRIX.md
@@ -0,0 +1,12 @@
+# React 路由迁移矩阵
+
+状态值:`inventory`、`building`、`verification`、`complete`。
+
+| 路由 | 页面 | 角色/公开范围 | 关键合同 | 状态 |
+| --- | --- | --- | --- | --- |
+| `/login` | 登录 | 公开 | 验证码、密码、TOTP、SSO 设置、会话落盘 | building |
+| `/dashboard` | 教务总览 | 已登录用户 | `GET /api/dashboard`、角色视图、待办和学期 | building |
+| `/account` | 个人账户 | 已登录用户 | TOTP、Passkey、恢复码、SSO 绑定 | inventory |
+| `/users` | 用户与权限 | SuperAdmin | 列表、筛选、分页、创建、重置密码、角色范围 | inventory |
+
+后续从 `web/src/router/index.ts` 逐条补全。只有关键合同和各状态验收完整后,状态才能更新为 `complete`。
diff --git a/web-react/README.md b/web-react/README.md
new file mode 100644
index 0000000..19fdd7c
--- /dev/null
+++ b/web-react/README.md
@@ -0,0 +1,49 @@
+# 明序校园 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` 和 `/users` 迁移占位路由,可安全返回现有 Vue 页面。
+
+## 迁移纪律
+
+1. 不因前端迁移修改后端业务合同;确需修改时必须单独评审。
+2. 每个页面迁移前登记路由、角色、API、写操作、查询参数和移动端行为。
+3. 页面只有在正常、空数据、异常、无权限和移动端状态都通过后才可标记完成。
+4. 未完成的页面返回 Vue 稳定版本,不提供半成品写操作。
+5. React 成为默认前端前,必须验证 PWA、Capacitor、SSO、TOTP、Passkey、文件导入导出及发布回滚。
+
+## 迁移批次
+
+| 批次 | 内容 | 状态 |
+| --- | --- | --- |
+| 0 | 工程、主题、会话、登录、应用外壳 | 进行中 |
+| 1 | Dashboard、Users、Account、移动端代表页 | Dashboard 首版已接入 |
+| 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..ddb1eb5
--- /dev/null
+++ b/web-react/package-lock.json
@@ -0,0 +1,2498 @@
+{
+ "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",
+ "@tanstack/react-query": "5.104.0",
+ "antd": "6.6.5",
+ "axios": "1.20.0",
+ "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/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/@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/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/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/clsx": {
+ "version": "2.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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/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/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/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/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-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-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/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/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/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/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/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-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/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/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..9dd481d
--- /dev/null
+++ b/web-react/package.json
@@ -0,0 +1,30 @@
+{
+ "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",
+ "@tanstack/react-query": "5.104.0",
+ "antd": "6.6.5",
+ "axios": "1.20.0",
+ "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/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/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..3331218
--- /dev/null
+++ b/web-react/src/app/App.tsx
@@ -0,0 +1,87 @@
+import { lazy, Suspense, useEffect } from 'react'
+import { 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 MigrationPlaceholderPage = lazy(() => import('../pages/MigrationPlaceholderPage'))
+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 (
+
+ {Array.from({ length: 9 }, (_, index) => )}
+
+ )
+}
diff --git a/web-react/src/components/LoginCaptcha.tsx b/web-react/src/components/LoginCaptcha.tsx
new file mode 100644
index 0000000..39fd888
--- /dev/null
+++ b/web-react/src/components/LoginCaptcha.tsx
@@ -0,0 +1,107 @@
+import { useCallback, useEffect, useState } from 'react'
+import { Alert, Button, Input, Skeleton } from 'antd'
+import http from '../api/http'
+
+interface CaptchaChoice {
+ id: string
+ label: string
+}
+
+interface CaptchaChallenge {
+ captchaId: string
+ type: 'TextImage' | 'MathImage' | 'Selection' | 'OneClick'
+ prompt?: string
+ choices?: CaptchaChoice[]
+ imageData?: string
+}
+
+interface LoginCaptchaProps {
+ refreshKey: number
+ onChange: (captchaId: string, code: string) => void
+}
+
+export default function LoginCaptcha({ refreshKey, onChange }: LoginCaptchaProps) {
+ const [captcha, setCaptcha] = useState(null)
+ const [code, setCode] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [loadError, setLoadError] = useState(false)
+
+ const refresh = useCallback(async () => {
+ setLoading(true)
+ setLoadError(false)
+ setCode('')
+ onChange('', '')
+ try {
+ const { data } = await http.post('/auth/login/captcha', {}, { skipAuthSession: true })
+ setCaptcha(data)
+ } catch {
+ setCaptcha(null)
+ setLoadError(true)
+ } finally {
+ setLoading(false)
+ }
+ }, [onChange])
+
+ useEffect(() => { void refresh() }, [refresh, refreshKey])
+ useEffect(() => { onChange(captcha?.captchaId ?? '', code) }, [captcha, code, onChange])
+
+ const isImageCaptcha = captcha?.type === 'TextImage' || captcha?.type === 'MathImage'
+
+ return (
+
+
+ 验证码
+
+
+ {loadError ? (
+ void refresh()}>重新加载}
+ />
+ ) : loading && !captcha ? : (
+
+ {isImageCaptcha && (
+ <>
+ {captcha?.imageData && (
+

void refresh()} />
+ )}
+
setCode(event.target.value)}
+ />
+ >
+ )}
+ {captcha && !isImageCaptcha && (
+
+
{captcha.prompt}
+ {captcha.imageData && (
+

void refresh()} />
+ )}
+ {captcha.type === 'Selection' ? (
+
+ {(captcha.choices ?? []).map((choice) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ )}
+
+ )}
+
+ )
+}
diff --git a/web-react/src/config/branding.ts b/web-react/src/config/branding.ts
new file mode 100644
index 0000000..4933ef7
--- /dev/null
+++ b/web-react/src/config/branding.ts
@@ -0,0 +1 @@
+export const branding = __APP_BRANDING__
diff --git a/web-react/src/design/global.css b/web-react/src/design/global.css
new file mode 100644
index 0000000..a7b8f76
--- /dev/null
+++ b/web-react/src/design/global.css
@@ -0,0 +1,155 @@
+* { box-sizing: border-box; }
+html, body, #root { min-width: 320px; min-height: 100%; margin: 0; }
+body { min-height: 100vh; background: var(--canvas); }
+button, input { font: inherit; }
+a { color: inherit; }
+
+:focus-visible {
+ outline: none;
+ box-shadow: var(--focus-ring);
+}
+
+.brand-mark {
+ width: 34px;
+ height: 34px;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 3px;
+ flex: 0 0 auto;
+}
+.brand-mark span { border: 1px solid #9dadd4; }
+.brand-mark span:nth-child(2),
+.brand-mark span:nth-child(5),
+.brand-mark span:nth-child(8) { border-color: var(--teal-bright); background: var(--teal-bright); }
+.brand-mark--large { width: 48px; height: 48px; gap: 4px; }
+
+.login-page {
+ min-height: 100vh;
+ display: grid;
+ grid-template-columns: minmax(420px, 1.05fr) minmax(420px, .95fr);
+ background: var(--paper);
+}
+.login-story {
+ min-height: 100vh;
+ padding: clamp(34px, 6vw, 78px);
+ display: flex;
+ flex-direction: column;
+ color: white;
+ background:
+ radial-gradient(circle at 78% 14%, rgba(84, 207, 189, .19), transparent 28%),
+ linear-gradient(138deg, #101f49 0%, #1d356f 64%, #126d70 132%);
+ overflow: hidden;
+ position: relative;
+}
+.story-brand { display: flex; align-items: center; gap: 16px; }
+.story-brand strong { display: block; font: 600 22px/1.15 var(--font-display); letter-spacing: .14em; }
+.story-brand small { display: block; margin-top: 7px; color: #b7c2df; font-size: 11px; letter-spacing: .08em; }
+.story-copy { max-width: 620px; margin: auto 0; padding: 56px 0; }
+.eyebrow { color: var(--teal-bright); font-size: 11px; font-weight: 700; letter-spacing: .18em; }
+.story-copy h1 { margin: 22px 0 18px; font: 600 clamp(34px, 4.1vw, 58px)/1.28 var(--font-display); letter-spacing: .035em; }
+.story-copy p { max-width: 500px; margin: 0; color: #c8d1e8; font-size: 15px; line-height: 1.8; }
+.term-track { display: grid; grid-template-columns: repeat(20, 1fr); gap: 5px; position: relative; }
+.term-track::before { content: "学期进程"; position: absolute; left: 0; bottom: 18px; color: #8fa0c9; font: 10px var(--font-data); letter-spacing: .16em; }
+.term-track span { height: 9px; border-top: 1px solid rgba(255,255,255,.2); position: relative; }
+.term-track span.is-active { border-color: var(--teal-bright); }
+.term-track span.is-active::after { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--teal-bright); position: absolute; top: -3px; left: 0; box-shadow: 0 0 0 4px rgba(84,207,189,.12); }
+.term-track span.is-now { border-top-width: 2px; border-color: #e9b65d; }
+
+.login-panel { padding: 48px clamp(34px, 7vw, 96px); display: grid; place-items: center; }
+.login-form { width: min(100%, 440px); display: grid; gap: 19px; }
+.form-intro { margin-bottom: 8px; }
+.form-intro > span { color: var(--teal); font-size: 12px; font-weight: 700; letter-spacing: .12em; }
+.form-intro h2 { margin: 10px 0 9px; color: var(--brand-deep); font: 600 30px/1.25 var(--font-display); }
+.form-intro p { margin: 0; color: var(--ink-muted); font-size: 13px; line-height: 1.7; }
+.field-label { display: grid; gap: 8px; color: #263750; font-size: 13px; font-weight: 650; }
+.login-actions { display: grid; gap: 10px; margin-top: 2px; }
+.login-links { display: grid; gap: 8px; justify-items: center; margin-top: 2px; font-size: 12px; }
+.login-links a { color: #4f6380; text-decoration: none; }
+.login-links a:hover { color: var(--teal); }
+.form-error { margin: -4px 0 0; padding: 10px 12px; border-left: 3px solid var(--danger); color: #8c2f27; background: var(--danger-soft); font-size: 12px; line-height: 1.55; }
+
+.captcha { display: grid; gap: 8px; }
+.captcha__heading { display: flex; justify-content: space-between; align-items: center; color: #263750; font-size: 13px; font-weight: 650; }
+.captcha__refresh { padding: 0; border: 0; color: #47729d; background: transparent; cursor: pointer; font-size: 12px; }
+.captcha__body { min-height: 50px; display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 10px; align-items: center; }
+.captcha__image { width: 150px; height: 50px; object-fit: contain; border: 1px solid #d7e2ed; border-radius: var(--radius-sm); cursor: pointer; background: #f7fafc; }
+.captcha__question { grid-column: 1 / -1; display: grid; gap: 9px; padding: 12px; border: 1px solid #d7e2ed; border-radius: var(--radius-sm); background: #f7fafc; }
+.captcha__question p { margin: 0; color: #233b54; font-size: 13px; }
+.captcha__choices { display: flex; flex-wrap: wrap; gap: 8px; }
+
+.app-shell { min-height: 100vh; background: var(--canvas); }
+.app-shell__sider { border-right: 1px solid rgba(255,255,255,.08); background: var(--brand-deep) !important; }
+.shell-brand { height: 76px; padding: 18px 20px; display: flex; align-items: center; gap: 12px; color: white; }
+.shell-brand strong { display: block; font: 600 17px/1.2 var(--font-display); letter-spacing: .1em; }
+.shell-brand small { display: block; margin-top: 4px; color: #9eadd2; font-size: 9px; letter-spacing: .08em; }
+.shell-menu { border-inline-end: 0 !important; background: transparent !important; }
+.shell-header { height: 64px; padding: 0 26px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); background: rgba(255,255,255,.94); backdrop-filter: blur(12px); position: sticky; top: 0; z-index: 10; }
+.shell-context { min-width: 0; }
+.shell-context span { color: var(--teal); font-size: 10px; font-weight: 700; letter-spacing: .13em; }
+.shell-context strong { display: block; margin-top: 3px; overflow: hidden; color: var(--ink-strong); font: 600 16px/1.2 var(--font-display); text-overflow: ellipsis; white-space: nowrap; }
+.shell-user { display: flex; align-items: center; gap: 10px; }
+.shell-user__copy { text-align: right; }
+.shell-user__copy strong { display: block; color: var(--ink-strong); font-size: 12px; }
+.shell-user__copy small { color: var(--ink-muted); font-size: 10px; }
+.shell-content { padding: 28px; }
+.page-container { width: min(100%, 1440px); margin: 0 auto; display: grid; gap: 20px; }
+.page-heading { min-height: 92px; padding: 8px 2px; display: flex; align-items: center; justify-content: space-between; gap: 24px; }
+.page-heading__kicker { color: var(--teal); font-size: 10px; font-weight: 700; letter-spacing: .15em; }
+.page-heading h1 { margin: 8px 0 6px; color: var(--brand-deep); font: 600 28px/1.25 var(--font-display); }
+.page-heading p { margin: 0; color: var(--ink-muted); font-size: 13px; }
+.term-status { min-width: 260px; padding: 13px 15px; border: 1px solid #d5e2e2; border-left: 3px solid var(--teal); background: #f5faf9; }
+.term-status span { display: block; color: var(--teal); font: 700 9px/1.2 var(--font-data); letter-spacing: .11em; }
+.term-status strong { display: block; margin-top: 6px; color: #263750; font-size: 12px; }
+.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
+.metric-card { padding: 20px; border: 1px solid var(--line); background: var(--paper); box-shadow: 0 8px 20px rgba(22,40,88,.04); }
+.metric-card span { color: var(--ink-muted); font-size: 11px; }
+.metric-card strong { display: block; margin: 14px 0 8px; color: var(--brand-deep); font: 700 30px/1 var(--font-data); }
+.metric-card small { color: #7b8798; font-size: 10px; line-height: 1.5; }
+.dashboard-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(290px, .7fr); gap: 18px; }
+.content-panel { border: 1px solid var(--line); background: var(--paper); box-shadow: var(--shadow-panel); }
+.content-panel__header { min-height: 68px; padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); }
+.content-panel__header h2 { margin: 0; color: var(--brand-deep); font: 600 18px/1.2 var(--font-display); }
+.content-panel__body { padding: 20px; }
+.readiness-list { display: grid; gap: 18px; }
+.readiness-row { display: grid; grid-template-columns: 120px 1fr 58px; gap: 14px; align-items: center; }
+.readiness-row span { color: var(--ink); font-size: 12px; }
+.readiness-row strong { color: var(--brand-deep); font: 700 13px var(--font-data); text-align: right; }
+.pending-list { display: grid; gap: 9px; }
+.pending-item { padding: 12px 13px; display: flex; justify-content: space-between; gap: 12px; border-left: 3px solid var(--amber); background: #fffbf4; }
+.pending-item span { color: #5b4930; font-size: 12px; }
+.pending-item strong { color: #8c5b16; font: 700 13px var(--font-data); }
+.migration-banner { padding: 11px 15px; display: flex; align-items: center; gap: 10px; border: 1px solid #d8e1ef; color: #455574; background: #f5f7fc; font-size: 12px; }
+.migration-banner b { color: var(--brand); }
+.placeholder { min-height: 360px; display: grid; place-items: center; padding: 40px; text-align: center; }
+.placeholder h2 { margin: 16px 0 8px; color: var(--brand-deep); font: 600 23px var(--font-display); }
+.placeholder p { max-width: 540px; margin: 0 0 22px; color: var(--ink-muted); line-height: 1.7; }
+
+@media (max-width: 980px) {
+ .login-page { grid-template-columns: 1fr; }
+ .login-story { min-height: 360px; padding: 34px; }
+ .story-copy { margin: 54px 0; padding: 0; }
+ .story-copy h1 { font-size: clamp(30px, 7vw, 44px); }
+ .login-panel { padding: 44px 24px; }
+ .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .dashboard-grid { grid-template-columns: 1fr; }
+}
+
+@media (max-width: 640px) {
+ .login-story { min-height: 310px; }
+ .story-brand small, .story-copy p { display: none; }
+ .story-copy { margin: auto 0; }
+ .term-track { margin-top: 26px; }
+ .shell-header { padding: 0 14px; }
+ .shell-user__copy { display: none; }
+ .shell-content { padding: 16px 12px 24px; }
+ .page-heading { align-items: flex-start; flex-direction: column; }
+ .term-status { width: 100%; min-width: 0; }
+ .metric-grid { grid-template-columns: 1fr; }
+ .readiness-row { grid-template-columns: 96px 1fr 48px; }
+ .captcha__body { grid-template-columns: 128px minmax(0, 1fr); }
+ .captcha__image { width: 128px; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; }
+}
diff --git a/web-react/src/design/tokens.css b/web-react/src/design/tokens.css
new file mode 100644
index 0000000..26f9a27
--- /dev/null
+++ b/web-react/src/design/tokens.css
@@ -0,0 +1,43 @@
+:root {
+ font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif;
+ color: #182033;
+ background: #f2f4f7;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+
+ --ink-strong: #182033;
+ --ink: #344054;
+ --ink-muted: #667085;
+ --brand-deep: #162858;
+ --brand: #284583;
+ --brand-soft: #e9edf8;
+ --teal: #087f73;
+ --teal-bright: #54cfbd;
+ --teal-soft: #e8f5f2;
+ --amber: #b8781d;
+ --amber-soft: #fbf1df;
+ --danger: #b42318;
+ --danger-soft: #fef3f2;
+ --paper: #ffffff;
+ --canvas: #f2f4f7;
+ --line: #dfe4eb;
+ --line-strong: #cbd3df;
+
+ --font-display: "STZhongsong", "Songti SC", "Noto Serif CJK SC", serif;
+ --font-body: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif;
+ --font-data: Consolas, "SFMono-Regular", monospace;
+
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 20px;
+ --space-6: 24px;
+ --space-8: 32px;
+ --space-10: 40px;
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+ --shadow-panel: 0 16px 36px rgba(22, 40, 88, 0.08);
+ --focus-ring: 0 0 0 3px rgba(8, 127, 115, 0.22);
+}
diff --git a/web-react/src/layout/AppShell.tsx b/web-react/src/layout/AppShell.tsx
new file mode 100644
index 0000000..7a3045f
--- /dev/null
+++ b/web-react/src/layout/AppShell.tsx
@@ -0,0 +1,89 @@
+import { useEffect, useMemo, useState } from 'react'
+import {
+ AppstoreOutlined,
+ BellOutlined,
+ IdcardOutlined,
+ MenuFoldOutlined,
+ MenuUnfoldOutlined,
+ SafetyCertificateOutlined,
+} from '@ant-design/icons'
+import { Avatar, Button, Layout, Menu, Tag } from 'antd'
+import { Outlet, useLocation, useNavigate } from 'react-router-dom'
+import { branding } from '../config/branding'
+import { useAuthStore } from '../stores/auth'
+import BrandMark from '../components/BrandMark'
+
+const { Header, Sider, Content } = Layout
+
+export default function AppShell() {
+ const location = useLocation()
+ const navigate = useNavigate()
+ const user = useAuthStore((state) => state.user)
+ const logout = useAuthStore((state) => state.logout)
+ const refreshUser = useAuthStore((state) => state.refreshUser)
+ const [collapsed, setCollapsed] = useState(false)
+
+ useEffect(() => { void refreshUser().catch(() => undefined) }, [refreshUser])
+
+ const items = useMemo(() => [
+ { key: '/dashboard', icon: , label: '教务总览' },
+ { key: '/account', icon: , label: '个人账户' },
+ ...(user?.roles.includes('SuperAdmin')
+ ? [{ key: '/users', icon: , label: '用户与权限' }]
+ : []),
+ ], [user?.roles])
+
+ const pageTitle = items.find((item) => location.pathname.startsWith(item.key))?.label ?? '迁移预览'
+
+ function signOut() {
+ logout()
+ navigate('/login', { replace: true })
+ }
+
+ return (
+
+
+
+
+ {!collapsed &&
{branding.shortName}REACT MIGRATION
}
+
+
+
+
+
+
+
+ )
+}
diff --git a/web-react/src/main.tsx b/web-react/src/main.tsx
new file mode 100644
index 0000000..9b6f531
--- /dev/null
+++ b/web-react/src/main.tsx
@@ -0,0 +1,12 @@
+import { createRoot } from 'react-dom/client'
+import 'antd/dist/reset.css'
+import './design/tokens.css'
+import './design/global.css'
+import { initializeAuthSession } from './auth/session'
+import App from './app/App'
+
+initializeAuthSession()
+
+createRoot(document.getElementById('root')!).render(
+ ,
+)
diff --git a/web-react/src/pages/DashboardPage.tsx b/web-react/src/pages/DashboardPage.tsx
new file mode 100644
index 0000000..0702ff2
--- /dev/null
+++ b/web-react/src/pages/DashboardPage.tsx
@@ -0,0 +1,133 @@
+import { Alert, Button, Progress, Skeleton } from 'antd'
+import { ArrowRightOutlined, ReloadOutlined } from '@ant-design/icons'
+import { useQuery } from '@tanstack/react-query'
+import http from '../api/http'
+
+interface DashboardData {
+ audience: {
+ level: string
+ title: string
+ scopeName: string
+ description: string
+ }
+ currentTerm?: {
+ id: string
+ name: string
+ startDate: string
+ endDate: string
+ }
+ counts: {
+ students: number
+ teachers: number
+ courses: number
+ teachingTasks: number
+ publishedTeachingTasks: number
+ scheduledTeachingTasks: number
+ courseEnrollments: number
+ gradeSheets: number
+ publishedGradeSheets: number
+ submittedGradeSheets: number
+ openCourseSelectionRounds: number
+ }
+ pending: Record
+ greeting: {
+ title: string
+ subtitle: string
+ narrative: string
+ }
+}
+
+const number = new Intl.NumberFormat('zh-CN')
+
+function percent(value: number, total: number) {
+ return total > 0 ? Math.min(100, Math.round(value / total * 100)) : 0
+}
+
+function formatDate(value?: string) {
+ if (!value) return '待设置'
+ const date = new Date(`${value.slice(0, 10)}T00:00:00`)
+ return new Intl.DateTimeFormat('zh-CN', { month: 'long', day: 'numeric' }).format(date)
+}
+
+export default function DashboardPage() {
+ const query = useQuery({
+ queryKey: ['dashboard'],
+ queryFn: async () => (await http.get('/dashboard')).data,
+ })
+
+ if (query.isLoading) return
+ if (query.isError || !query.data) {
+ return (
+
+
} onClick={() => void query.refetch()}>重新加载}
+ />
+
+ )
+ }
+
+ const data = query.data
+ const metrics = [
+ { label: '在籍学生', value: data.counts.students, hint: `${data.audience.scopeName}当前有效学籍` },
+ { label: '教师档案', value: data.counts.teachers, hint: '当前数据范围内的教师' },
+ { label: '本学期教学班', value: data.counts.teachingTasks, hint: `${data.counts.publishedTeachingTasks} 个已发布` },
+ { label: '有效选课', value: data.counts.courseEnrollments, hint: `${data.counts.openCourseSelectionRounds} 个批次开放中` },
+ ]
+ const readiness = [
+ { label: '任务发布', value: data.counts.publishedTeachingTasks, total: data.counts.teachingTasks },
+ { label: '课表覆盖', value: data.counts.scheduledTeachingTasks, total: data.counts.publishedTeachingTasks },
+ { label: '成绩发布', value: data.counts.publishedGradeSheets, total: data.counts.gradeSheets },
+ ]
+ const pending = Object.entries(data.pending).filter(([, value]) => value > 0)
+
+ return (
+
+ React 迁移预览当前页面直接读取现有 `/api/dashboard`,业务数据未使用模拟值。
+
+
+ {metrics.map((metric) => (
+
+ {metric.label}{number.format(metric.value)}{metric.hint}
+
+ ))}
+
+
+
+
+
+ {readiness.map((item) => {
+ const value = percent(item.value, item.total)
+ return (
+
+ )
+ })}
+
+
+
+ 等待处理
{pending.reduce((sum, [, value]) => sum + value, 0)} 项
+
+ {pending.length === 0 ?
: pending.slice(0, 6).map(([key, value]) => (
+
{key}{value}
+ ))}
+
+
+
+
+ )
+}
diff --git a/web-react/src/pages/LoginPage.tsx b/web-react/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..b781be4
--- /dev/null
+++ b/web-react/src/pages/LoginPage.tsx
@@ -0,0 +1,168 @@
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { Button, Input } from 'antd'
+import { Link, useLocation, useNavigate } from 'react-router-dom'
+import { apiErrorMessage } from '../api/http'
+import http from '../api/http'
+import { branding } from '../config/branding'
+import { isNativeApp } from '../auth/session'
+import { useAuthStore } from '../stores/auth'
+import BrandMark from '../components/BrandMark'
+import LoginCaptcha from '../components/LoginCaptcha'
+
+const ssoErrors: Record = {
+ authentication_failed: '统一身份认证未完成,请重新尝试。',
+ missing_subject: '统一身份认证未返回用户唯一标识,请联系管理员检查客户端映射。',
+ account_disabled: '平台账号已停用或锁定,请联系管理员。',
+ account_link_failed: '统一身份账号绑定失败,请联系管理员。',
+ account_update_failed: '登录状态更新失败,请稍后重试。',
+ binding_expired: '账户绑定请求已失效,请重新使用统一身份认证登录。',
+ configuration_error: '统一身份认证回调地址配置不正确,请联系管理员。',
+}
+
+function safeRedirect(value: string | null) {
+ if (!value || !value.startsWith('/') || value.startsWith('//')) return '/dashboard'
+ return value.replace(/^\/next/, '') || '/dashboard'
+}
+
+export default function LoginPage() {
+ const location = useLocation()
+ const navigate = useNavigate()
+ const login = useAuthStore((state) => state.login)
+ const completeTotpLogin = useAuthStore((state) => state.completeTotpLogin)
+ const [userName, setUserName] = useState('')
+ const [password, setPassword] = useState('')
+ const [totpCode, setTotpCode] = useState('')
+ const [twoFactorTicket, setTwoFactorTicket] = useState('')
+ const [captchaId, setCaptchaId] = useState('')
+ const [captchaCode, setCaptchaCode] = useState('')
+ const [captchaRefreshKey, setCaptchaRefreshKey] = useState(0)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState('')
+ const [sso, setSso] = useState({ enabled: false, displayName: '学校统一身份认证' })
+
+ const query = useMemo(() => new URLSearchParams(location.search), [location.search])
+ const redirect = safeRedirect(query.get('redirect'))
+ const handleCaptchaChange = useCallback((id: string, code: string) => {
+ setCaptchaId(id)
+ setCaptchaCode(code)
+ }, [])
+
+ useEffect(() => {
+ setUserName(query.get('userName') ?? '')
+ const ssoError = query.get('ssoError') ?? ''
+ if (ssoError) setError(ssoErrors[ssoError] ?? '统一身份认证失败,请重新尝试。')
+ void http.get('/auth/sso/settings', { skipAuthSession: true })
+ .then(({ data }) => setSso({
+ enabled: Boolean(data.enabled),
+ displayName: String(data.displayName || '学校统一身份认证'),
+ }))
+ .catch(() => setSso((current) => ({ ...current, enabled: false })))
+ }, [query])
+
+ async function submitPassword(event: React.FormEvent) {
+ event.preventDefault()
+ setLoading(true)
+ setError('')
+ try {
+ const result = await login(userName, password, captchaId, captchaCode)
+ if (result.twoFactorTicket) {
+ setTwoFactorTicket(result.twoFactorTicket)
+ return
+ }
+ navigate(redirect, { replace: true })
+ } catch (cause) {
+ setCaptchaRefreshKey((value) => value + 1)
+ setError(apiErrorMessage(cause))
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ async function submitTotp(event: React.FormEvent) {
+ event.preventDefault()
+ setLoading(true)
+ setError('')
+ try {
+ await completeTotpLogin(twoFactorTicket, totpCode)
+ navigate(redirect, { replace: true })
+ } catch (cause) {
+ setError(apiErrorMessage(cause))
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ function startSso() {
+ if (!captchaId || !captchaCode) return
+ const apiBaseUrl = String(import.meta.env.VITE_API_BASE_URL ?? '/api').replace(/\/$/, '')
+ const returnUrl = `/next${redirect}`
+ window.location.assign(
+ `${apiBaseUrl}/auth/sso/login?returnUrl=${encodeURIComponent(returnUrl)}` +
+ `&nativeApp=${isNativeApp()}&captchaId=${encodeURIComponent(captchaId)}` +
+ `&captchaCode=${encodeURIComponent(captchaCode)}`,
+ )
+ }
+
+ return (
+
+
+
+
+
{branding.shortName}{branding.tagline}
+
+
+
{branding.productName}
+
让每一项校园事务,
都有清晰的来处与去向。
+
连接教学运行、学业发展与校园生活,为师生提供一体化服务。
+
+
+ {Array.from({ length: 20 }, (_, index) => (
+
+ ))}
+
+
+
+ {!twoFactorTicket ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/web-react/src/pages/MigrationPlaceholderPage.tsx b/web-react/src/pages/MigrationPlaceholderPage.tsx
new file mode 100644
index 0000000..5c57d3a
--- /dev/null
+++ b/web-react/src/pages/MigrationPlaceholderPage.tsx
@@ -0,0 +1,28 @@
+import { ApartmentOutlined } from '@ant-design/icons'
+import { Button } from 'antd'
+import { useLocation } from 'react-router-dom'
+
+const titles: Record = {
+ '/account': '个人账户',
+ '/users': '用户与权限',
+}
+
+export default function MigrationPlaceholderPage() {
+ const location = useLocation()
+ const title = titles[location.pathname] ?? '该功能'
+ const legacyBase = import.meta.env.VITE_LEGACY_APP_URL ?? 'http://localhost:5255'
+
+ return (
+
+
+
+
+
+
继续使用现有稳定页面
+
迁移期间不提供功能不完整的替代界面。当前操作会返回 Vue 版本,React 页面通过完整验收后再切换。
+
+
+
+
+ )
+}
diff --git a/web-react/src/stores/auth.ts b/web-react/src/stores/auth.ts
new file mode 100644
index 0000000..5fddb57
--- /dev/null
+++ b/web-react/src/stores/auth.ts
@@ -0,0 +1,78 @@
+import { create } from 'zustand'
+import http from '../api/http'
+import {
+ authStorageKeys,
+ clearAuthSession,
+ isNativeApp,
+ saveAuthSession,
+} from '../auth/session'
+
+export interface CurrentUser {
+ id: string
+ userName: string
+ displayName: string
+ roles: string[]
+ collegeId?: string
+ effectiveDataScope: 'Self' | 'Class' | 'College' | 'All'
+}
+
+interface AuthState {
+ token: string
+ user: CurrentUser | null
+ sync: () => void
+ login: (userName: string, password: string, captchaId: string, captchaCode: string) => Promise<{ twoFactorTicket?: string }>
+ completeTotpLogin: (twoFactorTicket: string, code: string) => Promise
+ refreshUser: () => Promise
+ logout: () => void
+}
+
+function readUser(): CurrentUser | null {
+ const value = localStorage.getItem(authStorageKeys.user)
+ if (!value) return null
+ try {
+ return JSON.parse(value) as CurrentUser
+ } catch {
+ return null
+ }
+}
+
+export const useAuthStore = create((set) => ({
+ token: localStorage.getItem(authStorageKeys.token) ?? '',
+ user: readUser(),
+ sync: () => set({
+ token: localStorage.getItem(authStorageKeys.token) ?? '',
+ user: readUser(),
+ }),
+ login: async (userName, password, captchaId, captchaCode) => {
+ const { data } = await http.post('/auth/login', {
+ userName,
+ password,
+ captchaId,
+ captchaCode,
+ isNativeApp: isNativeApp(),
+ })
+ if (data.requiresTotp) return { twoFactorTicket: String(data.twoFactorTicket) }
+ saveAuthSession(data)
+ set({ token: data.token, user: data.user })
+ return {}
+ },
+ completeTotpLogin: async (twoFactorTicket, code) => {
+ const { data } = await http.post('/auth/login/totp', { twoFactorTicket, code })
+ saveAuthSession(data)
+ set({ token: data.token, user: data.user })
+ },
+ refreshUser: async () => {
+ if (!localStorage.getItem(authStorageKeys.token)) return
+ const { data } = await http.get('/auth/me')
+ localStorage.setItem(authStorageKeys.user, JSON.stringify(data))
+ set({ user: data })
+ },
+ logout: () => {
+ const refreshToken = localStorage.getItem(authStorageKeys.refreshToken)
+ if (refreshToken) void http.post('/auth/logout', { refreshToken }).catch(() => undefined)
+ clearAuthSession()
+ set({ token: '', user: null })
+ },
+}))
+
+window.addEventListener('mingxu-auth-changed', () => useAuthStore.getState().sync())
diff --git a/web-react/src/vite-env.d.ts b/web-react/src/vite-env.d.ts
new file mode 100644
index 0000000..15c1a50
--- /dev/null
+++ b/web-react/src/vite-env.d.ts
@@ -0,0 +1,12 @@
+///
+
+interface AppBranding {
+ brandName: string
+ institutionName: string
+ productName: string
+ shortName: string
+ tagline: string
+ description: string
+}
+
+declare const __APP_BRANDING__: Readonly
diff --git a/web-react/tsconfig.app.json b/web-react/tsconfig.app.json
new file mode 100644
index 0000000..e935337
--- /dev/null
+++ b/web-react/tsconfig.app.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["src"]
+}
diff --git a/web-react/tsconfig.json b/web-react/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/web-react/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/web-react/tsconfig.node.json b/web-react/tsconfig.node.json
new file mode 100644
index 0000000..1bec39d
--- /dev/null
+++ b/web-react/tsconfig.node.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
+ "strict": true,
+ "types": ["node"]
+ },
+ "include": ["vite.config.ts", "../web/branding.config.ts"]
+}
diff --git a/web-react/vite.config.ts b/web-react/vite.config.ts
new file mode 100644
index 0000000..a542b71
--- /dev/null
+++ b/web-react/vite.config.ts
@@ -0,0 +1,46 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import { branding } from '../web/branding.config.ts'
+
+function escapeHtml(value: string) {
+ return value
+ .replaceAll('&', '&')
+ .replaceAll('"', '"')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+}
+
+export default defineConfig({
+ base: '/next/',
+ define: {
+ __APP_BRANDING__: JSON.stringify(branding),
+ },
+ plugins: [
+ react(),
+ {
+ name: 'app-branding-html',
+ transformIndexHtml(html) {
+ return html
+ .replaceAll('__APP_PRODUCT_NAME__', escapeHtml(branding.productName))
+ .replaceAll('__APP_DESCRIPTION__', escapeHtml(branding.description))
+ },
+ },
+ ],
+ build: {
+ outDir: 'dist',
+ emptyOutDir: true,
+ },
+ server: {
+ port: 5174,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:5255',
+ changeOrigin: true,
+ },
+ '/health': {
+ target: 'http://localhost:5255',
+ changeOrigin: true,
+ },
+ },
+ },
+})