From e1a9cc738e6130372989152e628326fb651044fb Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 11 Mar 2026 17:01:24 +0800 Subject: [PATCH] =?UTF-8?q?[=E9=87=8D=E6=9E=84]=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=94=B9=E9=80=A0v5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- atsf_server/router/web-router.go | 31 ++ atsf_server/web/app/(dashboard)/page.tsx | 22 +- atsf_server/web/app/config-version/page.tsx | 5 + atsf_server/web/app/file/page.tsx | 5 + atsf_server/web/app/node/page.tsx | 5 + atsf_server/web/app/proxy-route/page.tsx | 5 + atsf_server/web/app/setting/page.tsx | 5 + atsf_server/web/app/user/add/page.tsx | 5 + atsf_server/web/app/user/edit/page.tsx | 5 + atsf_server/web/app/user/page.tsx | 5 + .../components/layout/dashboard-sidebar.tsx | 10 +- .../components/dashboard-overview.tsx | 36 +- .../settings/components/settings-page.tsx | 119 +++--- .../features/users/components/users-page.tsx | 54 ++- atsf_server/web/next-env.d.ts | 1 - atsf_server/web/src/App.js | 240 ----------- atsf_server/web/src/components/FilesTable.js | 303 ------------- atsf_server/web/src/components/Footer.js | 38 -- atsf_server/web/src/components/GitHubOAuth.js | 57 --- atsf_server/web/src/components/Header.js | 230 ---------- atsf_server/web/src/components/Loading.js | 14 - atsf_server/web/src/components/LoginForm.js | 202 --------- .../web/src/components/OperationSetting.js | 270 ------------ .../web/src/components/OtherSetting.js | 202 --------- .../src/components/PasswordResetConfirm.js | 76 ---- .../web/src/components/PasswordResetForm.js | 96 ----- .../web/src/components/PersonalSetting.js | 217 ---------- .../web/src/components/PrivateRoute.js | 13 - .../web/src/components/RegisterForm.js | 193 --------- .../web/src/components/SystemSetting.js | 399 ------------------ atsf_server/web/src/components/UsersTable.js | 317 -------------- .../web/src/constants/common.constant.js | 1 - atsf_server/web/src/constants/index.js | 3 - .../web/src/constants/toast.constants.js | 7 - .../web/src/constants/user.constants.js | 19 - atsf_server/web/src/context/Status/index.js | 19 - atsf_server/web/src/context/Status/reducer.js | 20 - atsf_server/web/src/context/User/index.js | 19 - atsf_server/web/src/context/User/reducer.js | 21 - atsf_server/web/src/helpers/api.js | 13 - atsf_server/web/src/helpers/auth-header.js | 10 - atsf_server/web/src/helpers/history.js | 3 - atsf_server/web/src/helpers/index.js | 4 - atsf_server/web/src/helpers/utils.js | 181 -------- atsf_server/web/src/index.css | 74 ---- atsf_server/web/src/index.js | 31 -- atsf_server/web/src/pages/About/index.js | 51 --- atsf_server/web/src/pages/ApplyLog/index.js | 74 ---- .../web/src/pages/ConfigVersion/index.js | 213 ---------- atsf_server/web/src/pages/File/index.js | 14 - atsf_server/web/src/pages/Home/index.js | 110 ----- .../web/src/pages/ManagedDomain/index.js | 198 --------- atsf_server/web/src/pages/Node/index.js | 291 ------------- atsf_server/web/src/pages/NotFound/index.js | 20 - atsf_server/web/src/pages/ProxyRoute/index.js | 398 ----------------- atsf_server/web/src/pages/Setting/index.js | 55 --- .../web/src/pages/TLSCertificate/index.js | 221 ---------- atsf_server/web/src/pages/User/AddUser.js | 77 ---- atsf_server/web/src/pages/User/EditUser.js | 132 ------ atsf_server/web/src/pages/User/index.js | 14 - atsf_server/web/tsconfig.json | 4 +- docs/frontend-revamp-plan.md | 17 - 62 files changed, 212 insertions(+), 5282 deletions(-) create mode 100644 atsf_server/web/app/config-version/page.tsx create mode 100644 atsf_server/web/app/file/page.tsx create mode 100644 atsf_server/web/app/node/page.tsx create mode 100644 atsf_server/web/app/proxy-route/page.tsx create mode 100644 atsf_server/web/app/setting/page.tsx create mode 100644 atsf_server/web/app/user/add/page.tsx create mode 100644 atsf_server/web/app/user/edit/page.tsx create mode 100644 atsf_server/web/app/user/page.tsx delete mode 100644 atsf_server/web/src/App.js delete mode 100644 atsf_server/web/src/components/FilesTable.js delete mode 100644 atsf_server/web/src/components/Footer.js delete mode 100644 atsf_server/web/src/components/GitHubOAuth.js delete mode 100644 atsf_server/web/src/components/Header.js delete mode 100644 atsf_server/web/src/components/Loading.js delete mode 100644 atsf_server/web/src/components/LoginForm.js delete mode 100644 atsf_server/web/src/components/OperationSetting.js delete mode 100644 atsf_server/web/src/components/OtherSetting.js delete mode 100644 atsf_server/web/src/components/PasswordResetConfirm.js delete mode 100644 atsf_server/web/src/components/PasswordResetForm.js delete mode 100644 atsf_server/web/src/components/PersonalSetting.js delete mode 100644 atsf_server/web/src/components/PrivateRoute.js delete mode 100644 atsf_server/web/src/components/RegisterForm.js delete mode 100644 atsf_server/web/src/components/SystemSetting.js delete mode 100644 atsf_server/web/src/components/UsersTable.js delete mode 100644 atsf_server/web/src/constants/common.constant.js delete mode 100644 atsf_server/web/src/constants/index.js delete mode 100644 atsf_server/web/src/constants/toast.constants.js delete mode 100644 atsf_server/web/src/constants/user.constants.js delete mode 100644 atsf_server/web/src/context/Status/index.js delete mode 100644 atsf_server/web/src/context/Status/reducer.js delete mode 100644 atsf_server/web/src/context/User/index.js delete mode 100644 atsf_server/web/src/context/User/reducer.js delete mode 100644 atsf_server/web/src/helpers/api.js delete mode 100644 atsf_server/web/src/helpers/auth-header.js delete mode 100644 atsf_server/web/src/helpers/history.js delete mode 100644 atsf_server/web/src/helpers/index.js delete mode 100644 atsf_server/web/src/helpers/utils.js delete mode 100644 atsf_server/web/src/index.css delete mode 100644 atsf_server/web/src/index.js delete mode 100644 atsf_server/web/src/pages/About/index.js delete mode 100644 atsf_server/web/src/pages/ApplyLog/index.js delete mode 100644 atsf_server/web/src/pages/ConfigVersion/index.js delete mode 100644 atsf_server/web/src/pages/File/index.js delete mode 100644 atsf_server/web/src/pages/Home/index.js delete mode 100644 atsf_server/web/src/pages/ManagedDomain/index.js delete mode 100644 atsf_server/web/src/pages/Node/index.js delete mode 100644 atsf_server/web/src/pages/NotFound/index.js delete mode 100644 atsf_server/web/src/pages/ProxyRoute/index.js delete mode 100644 atsf_server/web/src/pages/Setting/index.js delete mode 100644 atsf_server/web/src/pages/TLSCertificate/index.js delete mode 100644 atsf_server/web/src/pages/User/AddUser.js delete mode 100644 atsf_server/web/src/pages/User/EditUser.js delete mode 100644 atsf_server/web/src/pages/User/index.js diff --git a/atsf_server/router/web-router.go b/atsf_server/router/web-router.go index 1be2f194..8b485add 100644 --- a/atsf_server/router/web-router.go +++ b/atsf_server/router/web-router.go @@ -5,6 +5,7 @@ import ( "atsflare/controller" "atsflare/middleware" "embed" + "fmt" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" "io/fs" @@ -22,6 +23,7 @@ func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { router.Use(middleware.GlobalWebRateLimit()) fileDownloadRoute := router.Group("/") fileDownloadRoute.GET("/upload/:file", middleware.DownloadRateLimit(), controller.DownloadFile) + registerLegacyWebRedirects(router) router.Use(middleware.Cache()) router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) router.NoRoute(func(c *gin.Context) { @@ -38,6 +40,35 @@ func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { }) } +func registerLegacyWebRedirects(router *gin.Engine) { + legacyRoutes := map[string]string{ + "/proxy-route": "/proxy-routes", + "/config-version": "/config-versions", + "/node": "/nodes", + "/apply-log": "/apply-logs", + "/managed-domain": "/managed-domains", + "/tls-certificate": "/tls-certificates", + "/file": "/files", + "/user": "/users", + "/user/add": "/users?mode=create", + "/user/edit": "/users", + "/setting": "/settings", + } + + for source, target := range legacyRoutes { + router.GET(source, func(target string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Redirect(http.StatusFound, target) + } + }(target)) + } + + router.GET("/user/edit/:id", func(c *gin.Context) { + target := fmt.Sprintf("/users?edit=%s", c.Param("id")) + c.Redirect(http.StatusFound, target) + }) +} + func serveExportedPage(c *gin.Context, buildFS fs.FS) bool { requestPath := strings.Trim(c.Request.URL.Path, "/") diff --git a/atsf_server/web/app/(dashboard)/page.tsx b/atsf_server/web/app/(dashboard)/page.tsx index e23f6a89..c020e83b 100644 --- a/atsf_server/web/app/(dashboard)/page.tsx +++ b/atsf_server/web/app/(dashboard)/page.tsx @@ -1,14 +1,14 @@ import { DashboardOverview } from '@/features/dashboard/components/dashboard-overview'; import { PageHeader } from '@/components/layout/page-header'; -export default function DashboardPage() { - return ( -
- - -
- ); -} +export default function DashboardPage() { + return ( +
+ + +
+ ); +} diff --git a/atsf_server/web/app/config-version/page.tsx b/atsf_server/web/app/config-version/page.tsx new file mode 100644 index 00000000..786ffb1d --- /dev/null +++ b/atsf_server/web/app/config-version/page.tsx @@ -0,0 +1,5 @@ +import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect'; + +export default function LegacyConfigVersionPage() { + return ; +} diff --git a/atsf_server/web/app/file/page.tsx b/atsf_server/web/app/file/page.tsx new file mode 100644 index 00000000..b7e741a5 --- /dev/null +++ b/atsf_server/web/app/file/page.tsx @@ -0,0 +1,5 @@ +import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect'; + +export default function LegacyFilePage() { + return ; +} diff --git a/atsf_server/web/app/node/page.tsx b/atsf_server/web/app/node/page.tsx new file mode 100644 index 00000000..a2743058 --- /dev/null +++ b/atsf_server/web/app/node/page.tsx @@ -0,0 +1,5 @@ +import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect'; + +export default function LegacyNodePage() { + return ; +} diff --git a/atsf_server/web/app/proxy-route/page.tsx b/atsf_server/web/app/proxy-route/page.tsx new file mode 100644 index 00000000..6293ea5a --- /dev/null +++ b/atsf_server/web/app/proxy-route/page.tsx @@ -0,0 +1,5 @@ +import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect'; + +export default function LegacyProxyRoutePage() { + return ; +} diff --git a/atsf_server/web/app/setting/page.tsx b/atsf_server/web/app/setting/page.tsx new file mode 100644 index 00000000..e4ada263 --- /dev/null +++ b/atsf_server/web/app/setting/page.tsx @@ -0,0 +1,5 @@ +import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect'; + +export default function LegacySettingPage() { + return ; +} diff --git a/atsf_server/web/app/user/add/page.tsx b/atsf_server/web/app/user/add/page.tsx new file mode 100644 index 00000000..fccf7ae8 --- /dev/null +++ b/atsf_server/web/app/user/add/page.tsx @@ -0,0 +1,5 @@ +import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect'; + +export default function LegacyUserAddPage() { + return ; +} diff --git a/atsf_server/web/app/user/edit/page.tsx b/atsf_server/web/app/user/edit/page.tsx new file mode 100644 index 00000000..3d9c9762 --- /dev/null +++ b/atsf_server/web/app/user/edit/page.tsx @@ -0,0 +1,5 @@ +import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect'; + +export default function LegacyUserEditPage() { + return ; +} diff --git a/atsf_server/web/app/user/page.tsx b/atsf_server/web/app/user/page.tsx new file mode 100644 index 00000000..c04350c3 --- /dev/null +++ b/atsf_server/web/app/user/page.tsx @@ -0,0 +1,5 @@ +import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect'; + +export default function LegacyUserPage() { + return ; +} diff --git a/atsf_server/web/components/layout/dashboard-sidebar.tsx b/atsf_server/web/components/layout/dashboard-sidebar.tsx index 31da3041..f1247da8 100644 --- a/atsf_server/web/components/layout/dashboard-sidebar.tsx +++ b/atsf_server/web/components/layout/dashboard-sidebar.tsx @@ -64,11 +64,11 @@ export function DashboardSidebar() { })} - {!isSidebarCollapsed ? ( -
- 阶段 4 已接入设置、用户、文件等边缘模块,旧前端仅保留少量兼容兜底价值。 -
- ) : null} + {!isSidebarCollapsed ? ( +
+ 新版管理端已覆盖认证、核心链路和边缘模块,旧版前端代码已完成移除。 +
+ ) : null} ); diff --git a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx index 9a0c08b6..dfa85a0c 100644 --- a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx +++ b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx @@ -19,13 +19,13 @@ const readinessItems = [ }, ]; -export function DashboardOverview() { - return ( -
- } +export function DashboardOverview() { + return ( +
+ } >
{readinessItems.map((item) => ( @@ -60,14 +60,14 @@ export function DashboardOverview() {
- -
    -
  1. 1. 对照旧前端完成设置、用户、文件与关于页的联调回归。
  2. -
  3. 2. 补齐关键页面的测试覆盖与构建验收。
  4. -
  5. 3. 评估默认入口切换与旧前端回滚预案。
  6. -
-
-
-
- ); -} + +
    +
  1. 1. 对照后端接口完成设置、节点、证书和用户链路联调。
  2. +
  3. 2. 补齐关键页面的测试覆盖与构建验收。
  4. +
  5. 3. 清理迁移期文档和发布说明中的旧前端表述。
  6. +
+
+ + + ); +} diff --git a/atsf_server/web/features/settings/components/settings-page.tsx b/atsf_server/web/features/settings/components/settings-page.tsx index 7d43c414..37adec8a 100644 --- a/atsf_server/web/features/settings/components/settings-page.tsx +++ b/atsf_server/web/features/settings/components/settings-page.tsx @@ -4,11 +4,12 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { marked } from 'marked'; import { useEffect, useMemo, useState } from 'react'; -import { EmptyState } from '@/components/feedback/empty-state'; -import { ErrorState } from '@/components/feedback/error-state'; -import { InlineMessage } from '@/components/feedback/inline-message'; -import { LoadingState } from '@/components/feedback/loading-state'; -import { useAuth } from '@/components/providers/auth-provider'; +import { EmptyState } from '@/components/feedback/empty-state'; +import { ErrorState } from '@/components/feedback/error-state'; +import { InlineMessage } from '@/components/feedback/inline-message'; +import { LoadingState } from '@/components/feedback/loading-state'; +import { TurnstileWidget } from '@/components/forms/turnstile-widget'; +import { useAuth } from '@/components/providers/auth-provider'; import { PageHeader } from '@/components/layout/page-header'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; @@ -156,10 +157,11 @@ export function SettingsPage() { const [operationFields, setOperationFields] = useState(defaultOperationFields); const [otherFields, setOtherFields] = useState(defaultOtherFields); const [accessToken, setAccessToken] = useState(''); - const [wechatCode, setWeChatCode] = useState(''); - const [emailAddress, setEmailAddress] = useState(''); - const [emailCode, setEmailCode] = useState(''); - const [latestRelease, setLatestRelease] = useState(null); + const [wechatCode, setWeChatCode] = useState(''); + const [emailAddress, setEmailAddress] = useState(''); + const [emailCode, setEmailCode] = useState(''); + const [emailTurnstileToken, setEmailTurnstileToken] = useState(''); + const [latestRelease, setLatestRelease] = useState(null); const isRoot = (user?.role ?? 0) >= 100; @@ -355,17 +357,22 @@ export function SettingsPage() { }); }; - const handleEmailVerification = () => { - if (!emailAddress.trim()) { - setFeedback({ tone: 'danger', message: '请输入要绑定的邮箱地址。' }); - return; - } - - void runBusyAction('email-send', async () => { - await sendEmailVerification(emailAddress.trim()); - setFeedback({ tone: 'success', message: '验证码已发送,请检查邮箱。' }); - }); - }; + const handleEmailVerification = () => { + if (!emailAddress.trim()) { + setFeedback({ tone: 'danger', message: '请输入要绑定的邮箱地址。' }); + return; + } + + if (publicStatusQuery.data?.turnstile_check && !emailTurnstileToken) { + setFeedback({ tone: 'info', message: '请先完成人机验证。' }); + return; + } + + void runBusyAction('email-send', async () => { + await sendEmailVerification(emailAddress.trim(), emailTurnstileToken || undefined); + setFeedback({ tone: 'success', message: '验证码已发送,请检查邮箱。' }); + }); + }; const handleBindEmail = () => { if (!emailAddress.trim() || !emailCode.trim()) { @@ -591,38 +598,46 @@ export function SettingsPage() { 当前状态:{profile.email ? `已绑定 ${profile.email}` : '未绑定'}

- {publicStatus.turnstile_check ? ( - - ) : ( -
- - setEmailAddress(event.target.value)} - placeholder='请输入邮箱地址' - /> - - - setEmailCode(event.target.value)} - placeholder='请输入邮箱验证码' - /> - -
- - {busyKey === 'email-send' ? '发送中...' : '发送验证码'} - - - {busyKey === 'email-bind' ? '绑定中...' : '绑定邮箱'} - -
-
- )} - +
+ + setEmailAddress(event.target.value)} + placeholder='请输入邮箱地址' + /> + + + setEmailCode(event.target.value)} + placeholder='请输入邮箱验证码' + /> + + {publicStatus.turnstile_check ? ( + publicStatus.turnstile_site_key ? ( + setEmailTurnstileToken(token)} + onExpire={() => setEmailTurnstileToken('')} + onError={() => setEmailTurnstileToken('')} + /> + ) : ( + + ) + ) : null} +
+ + {busyKey === 'email-send' ? '发送中...' : '发送验证码'} + + + {busyKey === 'email-bind' ? '绑定中...' : '绑定邮箱'} + +
+
+ diff --git a/atsf_server/web/features/users/components/users-page.tsx b/atsf_server/web/features/users/components/users-page.tsx index b19ccaac..79577cbd 100644 --- a/atsf_server/web/features/users/components/users-page.tsx +++ b/atsf_server/web/features/users/components/users-page.tsx @@ -1,10 +1,11 @@ -'use client'; - -import { zodResolver } from '@hookform/resolvers/zod'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useEffect, useMemo, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useSearchParams } from 'next/navigation'; +import { useEffect, useMemo, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; @@ -92,10 +93,11 @@ function toPayload(values: UserFormValues): UserMutationPayload { }; } -export function UsersPage() { - const queryClient = useQueryClient(); - const { user: currentUser } = useAuth(); - const [page, setPage] = useState(0); +export function UsersPage() { + const queryClient = useQueryClient(); + const searchParams = useSearchParams(); + const { user: currentUser } = useAuth(); + const [page, setPage] = useState(0); const [searchInput, setSearchInput] = useState(''); const [searchKeyword, setSearchKeyword] = useState(''); const [editingUserId, setEditingUserId] = useState(null); @@ -127,15 +129,35 @@ export function UsersPage() { enabled: isAdmin && editingUserId !== null, }); - useEffect(() => { - if (editingUserQuery.data) { - form.reset({ + useEffect(() => { + if (editingUserQuery.data) { + form.reset({ username: editingUserQuery.data.username, display_name: editingUserQuery.data.display_name || '', password: '', }); - } - }, [editingUserQuery.data, form]); + } + }, [editingUserQuery.data, form]); + + useEffect(() => { + const editParam = searchParams?.get('edit'); + const modeParam = searchParams?.get('mode'); + + if (editParam) { + const parsedUserId = Number.parseInt(editParam, 10); + if (!Number.isNaN(parsedUserId) && parsedUserId > 0) { + setFeedback(null); + setEditingUserId(parsedUserId); + } + return; + } + + if (modeParam === 'create') { + setFeedback(null); + setEditingUserId(null); + form.reset(defaultValues); + } + }, [form, searchParams]); const saveMutation = useMutation({ mutationFn: async (values: UserFormValues) => { diff --git a/atsf_server/web/next-env.d.ts b/atsf_server/web/next-env.d.ts index dab0f084..76faf1f3 100644 --- a/atsf_server/web/next-env.d.ts +++ b/atsf_server/web/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/atsf_server/web/src/App.js b/atsf_server/web/src/App.js deleted file mode 100644 index 88ba6272..00000000 --- a/atsf_server/web/src/App.js +++ /dev/null @@ -1,240 +0,0 @@ -import React, { lazy, Suspense, useContext, useEffect } from 'react'; -import { Route, Routes } from 'react-router-dom'; -import Loading from './components/Loading'; -import User from './pages/User'; -import { PrivateRoute } from './components/PrivateRoute'; -import RegisterForm from './components/RegisterForm'; -import LoginForm from './components/LoginForm'; -import NotFound from './pages/NotFound'; -import Setting from './pages/Setting'; -import EditUser from './pages/User/EditUser'; -import AddUser from './pages/User/AddUser'; -import { - API, - getBuildVersion, - hasBuildVersion, - normalizeVersion, - showError, - showNotice, -} from './helpers'; -import PasswordResetForm from './components/PasswordResetForm'; -import GitHubOAuth from './components/GitHubOAuth'; -import PasswordResetConfirm from './components/PasswordResetConfirm'; -import { UserContext } from './context/User'; -import { StatusContext } from './context/Status'; -import File from './pages/File'; -import ProxyRoute from './pages/ProxyRoute'; -import ConfigVersion from './pages/ConfigVersion'; -import Node from './pages/Node'; -import ApplyLog from './pages/ApplyLog'; -import TLSCertificate from './pages/TLSCertificate'; -import ManagedDomain from './pages/ManagedDomain'; - -const Home = lazy(() => import('./pages/Home')); -const About = lazy(() => import('./pages/About')); - -function App() { - const [userState, userDispatch] = useContext(UserContext); - const [statusState, statusDispatch] = useContext(StatusContext); - - const loadUser = () => { - let user = localStorage.getItem('user'); - if (user) { - let data = JSON.parse(user); - userDispatch({ type: 'login', payload: data }); - } - }; - const loadStatus = async () => { - const res = await API.get('/api/status'); - const { success, data } = res.data; - if (success) { - console.log(`GitHub 仓库地址:https://github.com/Rain-kl/ATSFlare`); - localStorage.setItem('status', JSON.stringify(data)); - statusDispatch({ type: 'set', payload: data }); - localStorage.setItem('system_name', data.system_name); - localStorage.setItem('footer_html', data.footer_html); - localStorage.setItem('home_page_link', data.home_page_link); - const currentBuildVersion = getBuildVersion(); - const latestServerVersion = normalizeVersion(data.version); - if ( - hasBuildVersion() && - latestServerVersion !== '' && - latestServerVersion !== '0.0.0' && - latestServerVersion !== currentBuildVersion - ) { - showNotice( - `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面` - ); - } - } else { - showError('无法正常连接至服务器!'); - } - }; - - useEffect(() => { - loadUser(); - loadStatus().then(); - }, []); - - return ( - - }> - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - - }> - - - - } - /> - }> - - - } - /> - } /> - - ); -} - -export default App; diff --git a/atsf_server/web/src/components/FilesTable.js b/atsf_server/web/src/components/FilesTable.js deleted file mode 100644 index 79b290bb..00000000 --- a/atsf_server/web/src/components/FilesTable.js +++ /dev/null @@ -1,303 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - Button, - Form, - Header, - Icon, - Pagination, - Popup, - Progress, - Segment, - Table, -} from 'semantic-ui-react'; -import { API, copy, showError, showSuccess } from '../helpers'; -import { useDropzone } from 'react-dropzone'; -import { ITEMS_PER_PAGE } from '../constants'; - -const FilesTable = () => { - const [files, setFiles] = useState([]); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searching, setSearching] = useState(false); - const { acceptedFiles, getRootProps, getInputProps } = useDropzone(); - const [uploading, setUploading] = useState(false); - const [uploadProgress, setUploadProgress] = useState('0'); - - const loadFiles = async (startIdx) => { - const res = await API.get(`/api/file/?p=${startIdx}`); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setFiles(data); - } else { - let newFiles = files; - newFiles.push(...data); - setFiles(newFiles); - } - } else { - showError(message); - } - setLoading(false); - }; - - const onPaginationChange = (e, { activePage }) => { - (async () => { - if (activePage === Math.ceil(files.length / ITEMS_PER_PAGE) + 1) { - // In this case we have to load more data and then append them. - await loadFiles(activePage - 1); - } - setActivePage(activePage); - })(); - }; - - useEffect(() => { - loadFiles(0) - .then() - .catch((reason) => { - showError(reason); - }); - }, []); - - const downloadFile = (link, filename) => { - let linkElement = document.createElement('a'); - linkElement.download = filename; - linkElement.href = '/upload/' + link; - linkElement.click(); - }; - - const copyLink = (link) => { - let url = window.location.origin + '/upload/' + link; - copy(url).then(); - showSuccess('链接已复制到剪贴板'); - }; - - const deleteFile = async (id, idx) => { - const res = await API.delete(`/api/file/${id}`); - const { success, message } = res.data; - if (success) { - let newFiles = [...files]; - let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; - newFiles[realIdx].deleted = true; - // newFiles.splice(idx, 1); - setFiles(newFiles); - showSuccess('文件已删除!'); - } else { - showError(message); - } - }; - - const searchFiles = async () => { - if (searchKeyword === '') { - // if keyword is blank, load files instead. - await loadFiles(0); - setActivePage(1); - return; - } - setSearching(true); - const res = await API.get(`/api/file/search?keyword=${searchKeyword}`); - const { success, message, data } = res.data; - if (success) { - setFiles(data); - setActivePage(1); - } else { - showError(message); - } - setSearching(false); - }; - - const handleKeywordChange = async (e, { value }) => { - setSearchKeyword(value.trim()); - }; - - const sortFile = (key) => { - if (files.length === 0) return; - setLoading(true); - let sortedUsers = [...files]; - sortedUsers.sort((a, b) => { - return ('' + a[key]).localeCompare(b[key]); - }); - if (sortedUsers[0].id === files[0].id) { - sortedUsers.reverse(); - } - setFiles(sortedUsers); - setLoading(false); - }; - - const uploadFiles = async () => { - if (acceptedFiles.length === 0) return; - setUploading(true); - let formData = new FormData(); - for (let i = 0; i < acceptedFiles.length; i++) { - formData.append('file', acceptedFiles[i]); - } - const res = await API.post(`/api/file`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - onUploadProgress: (e) => { - let uploadProgress = ((e.loaded / e.total) * 100).toFixed(2); - setUploadProgress(uploadProgress); - }, - }); - const { success, message } = res.data; - if (success) { - showSuccess(`${acceptedFiles.length} 个文件上传成功!`); - } else { - showError(message); - } - setUploading(false); - setUploadProgress('0'); - setSearchKeyword(''); - loadFiles(0).then(); - setActivePage(1); - }; - - useEffect(() => { - uploadFiles().then(); - }, [acceptedFiles]); - - return ( - <> - -
- - 拖拽上传或点击上传 - -
-
- {uploading ? ( - - ) : ( - <> - )} -
- - - - - - - { - sortFile('filename'); - }} - > - 文件名 - - { - sortFile('uploader_id'); - }} - > - 上传者 - - { - sortFile('email'); - }} - > - 上传时间 - - 操作 - - - - - {files - .slice( - (activePage - 1) * ITEMS_PER_PAGE, - activePage * ITEMS_PER_PAGE - ) - .map((file, idx) => { - if (file.deleted) return <>; - return ( - - - - {file.filename} - - - {file.uploader}} - /> - {file.upload_time} - -
- - - -
-
-
- ); - })} -
- - - - - - - - -
- - ); -}; - -export default FilesTable; diff --git a/atsf_server/web/src/components/Footer.js b/atsf_server/web/src/components/Footer.js deleted file mode 100644 index a744947a..00000000 --- a/atsf_server/web/src/components/Footer.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import { Container, Segment } from 'semantic-ui-react'; -import { getFooterHTML, getSystemName } from '../helpers'; - -const Footer = () => { - const systemName = getSystemName(); - const footer = getFooterHTML(); - - return ( - - - {footer ? ( -
- ) : ( - - )} -
-
- ); -}; - -export default Footer; diff --git a/atsf_server/web/src/components/GitHubOAuth.js b/atsf_server/web/src/components/GitHubOAuth.js deleted file mode 100644 index 147d4d30..00000000 --- a/atsf_server/web/src/components/GitHubOAuth.js +++ /dev/null @@ -1,57 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { Dimmer, Loader, Segment } from 'semantic-ui-react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { API, showError, showSuccess } from '../helpers'; -import { UserContext } from '../context/User'; - -const GitHubOAuth = () => { - const [searchParams, setSearchParams] = useSearchParams(); - - const [userState, userDispatch] = useContext(UserContext); - const [prompt, setPrompt] = useState('处理中...'); - const [processing, setProcessing] = useState(true); - - let navigate = useNavigate(); - - const sendCode = async (code, count) => { - const res = await API.get(`/api/oauth/github?code=${code}`); - const { success, message, data } = res.data; - if (success) { - if (message === 'bind') { - showSuccess('绑定成功!'); - navigate('/setting'); - } else { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - showSuccess('登录成功!'); - navigate('/'); - } - } else { - showError(message); - if (count === 0) { - setPrompt(`操作失败,重定向至登录界面中...`); - navigate('/setting'); // in case this is failed to bind GitHub - return; - } - count++; - setPrompt(`出现错误,第 ${count} 次重试中...`); - await new Promise((resolve) => setTimeout(resolve, count * 2000)); - await sendCode(code, count); - } - }; - - useEffect(() => { - let code = searchParams.get('code'); - sendCode(code, 0).then(); - }, []); - - return ( - - - {prompt} - - - ); -}; - -export default GitHubOAuth; diff --git a/atsf_server/web/src/components/Header.js b/atsf_server/web/src/components/Header.js deleted file mode 100644 index dabe34a2..00000000 --- a/atsf_server/web/src/components/Header.js +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { UserContext } from '../context/User'; - -import { - Button, - Container, - Dropdown, - Icon, - Menu, - Segment, -} from 'semantic-ui-react'; -import { API, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers'; -import '../index.css'; - -// Header Buttons -const headerButtons = [ - { - name: '首页', - to: '/', - icon: 'home', - }, - { - name: '规则', - to: '/proxy-route', - icon: 'sitemap', - admin: true, - }, - { - name: '版本', - to: '/config-version', - icon: 'history', - admin: true, - }, - { - name: '节点', - to: '/node', - icon: 'server', - admin: true, - }, - { - name: '记录', - to: '/apply-log', - icon: 'tasks', - admin: true, - }, - { - name: '证书', - to: '/tls-certificate', - icon: 'lock', - admin: true, - }, - { - name: '域名', - to: '/managed-domain', - icon: 'globe', - admin: true, - }, - { - name: '用户', - to: '/user', - icon: 'user', - admin: true, - }, - { - name: '设置', - to: '/setting', - icon: 'setting', - }, - { - name: '关于', - to: '/about', - icon: 'info circle', - }, -]; - -const Header = () => { - const [userState, userDispatch] = useContext(UserContext); - let navigate = useNavigate(); - - const [showSidebar, setShowSidebar] = useState(false); - const systemName = getSystemName(); - - async function logout() { - setShowSidebar(false); - await API.get('/api/user/logout'); - showSuccess('注销成功!'); - userDispatch({ type: 'logout' }); - localStorage.removeItem('user'); - navigate('/login'); - } - - const toggleSidebar = () => { - setShowSidebar(!showSidebar); - }; - - const renderButtons = (isMobile) => { - return headerButtons.map((button) => { - if (button.admin && !isAdmin()) return <>; - if (isMobile) { - return ( - { - navigate(button.to); - setShowSidebar(false); - }} - > - {button.name} - - ); - } - return ( - - - {button.name} - - ); - }); - }; - - if (isMobile()) { - return ( - <> - - - - logo -
- {systemName} -
-
- - - - - -
-
- {showSidebar ? ( - - - {renderButtons(true)} - - {userState.user ? ( - - ) : ( - <> - - - - )} - - - - ) : ( - <> - )} - - ); - } - - return ( - <> - - - - logo -
- {systemName} -
-
- {renderButtons(false)} - - {userState.user ? ( - - - 注销 - - - ) : ( - - )} - -
-
- - ); -}; - -export default Header; diff --git a/atsf_server/web/src/components/Loading.js b/atsf_server/web/src/components/Loading.js deleted file mode 100644 index 1210a56f..00000000 --- a/atsf_server/web/src/components/Loading.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { Segment, Dimmer, Loader } from 'semantic-ui-react'; - -const Loading = ({ prompt: name = 'page' }) => { - return ( - - - 加载{name}中... - - - ); -}; - -export default Loading; diff --git a/atsf_server/web/src/components/LoginForm.js b/atsf_server/web/src/components/LoginForm.js deleted file mode 100644 index 27351d60..00000000 --- a/atsf_server/web/src/components/LoginForm.js +++ /dev/null @@ -1,202 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { - Button, - Divider, - Form, - Grid, - Header, - Image, - Message, - Modal, - Segment, -} from 'semantic-ui-react'; -import { Link, useNavigate, useSearchParams } from 'react-router-dom'; -import { UserContext } from '../context/User'; -import { API, showError, showSuccess } from '../helpers'; - -const LoginForm = () => { - const [inputs, setInputs] = useState({ - username: '', - password: '', - wechat_verification_code: '', - }); - const [searchParams, setSearchParams] = useSearchParams(); - const [submitted, setSubmitted] = useState(false); - const { username, password } = inputs; - const [userState, userDispatch] = useContext(UserContext); - let navigate = useNavigate(); - - const [status, setStatus] = useState({}); - - useEffect(() => { - if (searchParams.get("expired")) { - showError('未登录或登录已过期,请重新登录!'); - } - let status = localStorage.getItem('status'); - if (status) { - status = JSON.parse(status); - setStatus(status); - } - }, []); - - const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); - - const onGitHubOAuthClicked = () => { - window.open( - `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` - ); - }; - - const onWeChatLoginClicked = () => { - setShowWeChatLoginModal(true); - }; - - const onSubmitWeChatVerificationCode = async () => { - const res = await API.get( - `/api/oauth/wechat?code=${inputs.wechat_verification_code}` - ); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - navigate('/'); - showSuccess('登录成功!'); - setShowWeChatLoginModal(false); - } else { - showError(message); - } - }; - - function handleChange(e) { - const { name, value } = e.target; - setInputs((inputs) => ({ ...inputs, [name]: value })); - } - - async function handleSubmit(e) { - setSubmitted(true); - if (username && password) { - const res = await API.post('/api/user/login', { - username, - password, - }); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - navigate('/'); - showSuccess('登录成功!'); - } else { - showError(message); - } - } - } - - return ( - - -
- 用户登录 -
-
- - - - - -
- - 忘记密码? - - 点击重置 - - ; 没有账户? - - 点击注册 - - - {status.github_oauth || status.wechat_login ? ( - <> - Or - {status.github_oauth ? ( - - - - - -
-
- ); -}; - -export default LoginForm; diff --git a/atsf_server/web/src/components/OperationSetting.js b/atsf_server/web/src/components/OperationSetting.js deleted file mode 100644 index 1fa061c4..00000000 --- a/atsf_server/web/src/components/OperationSetting.js +++ /dev/null @@ -1,270 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - Button, - Divider, - Form, - Grid, - Header, - Icon, - Label, - Message, - Segment, -} from 'semantic-ui-react'; -import { API, copy, showError, showSuccess } from '../helpers'; - -const OperationSetting = () => { - const [inputs, setInputs] = useState({ - AgentHeartbeatInterval: '30000', - AgentSyncInterval: '30000', - NodeOfflineThreshold: '120000', - AgentUpdateRepo: 'Rain-kl/ATSFlare', - ServerAddress: '', - AgentDiscoveryToken: '', - }); - const [loading, setLoading] = useState(false); - - const getOptions = async () => { - const res = await API.get('/api/option/'); - const { success, message, data } = res.data; - if (success) { - let newInputs = {}; - data.forEach((item) => { - if (item.key in inputs) { - newInputs[item.key] = item.value; - } - }); - setInputs((prev) => ({ ...prev, ...newInputs })); - } else { - showError(message); - } - }; - - useEffect(() => { - getOptions(); - getDiscoveryToken(); - }, []); - - const getDiscoveryToken = async () => { - try { - const res = await API.get('/api/nodes/bootstrap-token'); - const { success, data } = res.data; - if (success && data) { - setInputs((prev) => ({ - ...prev, - AgentDiscoveryToken: data.discovery_token, - })); - } - } catch (e) { - // ignore - } - }; - - const updateOption = async (key, value) => { - setLoading(true); - const res = await API.put('/api/option', { key, value }); - const { success, message } = res.data; - if (success) { - showSuccess('设置已保存'); - setInputs((prev) => ({ ...prev, [key]: value })); - } else { - showError(message); - } - setLoading(false); - }; - - const handleInputChange = (e, { name, value }) => { - setInputs((prev) => ({ ...prev, [name]: value })); - }; - - const submitAgentIntervals = async () => { - const hb = parseInt(inputs.AgentHeartbeatInterval, 10); - const sync = parseInt(inputs.AgentSyncInterval, 10); - const offline = parseInt(inputs.NodeOfflineThreshold, 10); - if (isNaN(hb) || hb < 5000) { - showError('心跳间隔不能小于 5000 毫秒'); - return; - } - if (isNaN(sync) || sync < 5000) { - showError('同步间隔不能小于 5000 毫秒'); - return; - } - if (isNaN(offline) || offline < 10000) { - showError('离线阈值不能小于 10000 毫秒'); - return; - } - await updateOption('AgentHeartbeatInterval', String(hb)); - await updateOption('AgentSyncInterval', String(sync)); - await updateOption('NodeOfflineThreshold', String(offline)); - }; - - const submitUpdateRepo = async () => { - await updateOption('AgentUpdateRepo', inputs.AgentUpdateRepo); - }; - - const rotateDiscoveryToken = async () => { - setLoading(true); - try { - const res = await API.post('/api/nodes/bootstrap-token/rotate'); - const { success, message, data } = res.data; - if (success) { - showSuccess('Discovery Token 已重新生成'); - setInputs((prev) => ({ - ...prev, - AgentDiscoveryToken: data.discovery_token, - })); - } else { - showError(message); - } - } catch (e) { - showError('操作失败'); - } - setLoading(false); - }; - - const formatMs = (ms) => { - const val = parseInt(ms, 10); - if (isNaN(val)) return ms; - if (val >= 60000) return `${val / 60000} 分钟`; - return `${val / 1000} 秒`; - }; - - const serverAddr = inputs.ServerAddress || window.location.origin; - const curlCommand = inputs.AgentDiscoveryToken - ? `curl -fsSL https://raw.githubusercontent.com/Rain-kl/ATSFlare/main/scripts/install-agent.sh | bash -s -- --server-url ${serverAddr} --discovery-token ${inputs.AgentDiscoveryToken}` - : ''; - const openSwaggerDoc = () => { - window.open('/swagger/index.html', '_blank', 'noopener,noreferrer'); - }; - - return ( - - -
-
接口文档
- -

通过 Swagger UI 查看当前 Server API,并直接在浏览器中调试接口。

-
- - - -
Agent 运行参数
- -

这些参数通过心跳响应下发到所有 Agent,修改后下次心跳即生效。

-
- - - - - - - 保存运行参数 - - - -
Agent 更新源
- -

- 自动更新开关和手动更新由节点页单独控制,这里只配置 Agent - 更新仓库。 -

-
- - - - - 保存更新设置 - - - -
节点接入
- - - - - - - - - - - - {curlCommand && ( - <> -
Agent 一键部署命令
- -
-                  {curlCommand}
-                
-
- - - )} - -
-
- ); -}; - -export default OperationSetting; diff --git a/atsf_server/web/src/components/OtherSetting.js b/atsf_server/web/src/components/OtherSetting.js deleted file mode 100644 index 3ccf3994..00000000 --- a/atsf_server/web/src/components/OtherSetting.js +++ /dev/null @@ -1,202 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Divider, Form, Grid, Header, Modal } from 'semantic-ui-react'; -import { - API, - getBuildVersion, - hasBuildVersion, - normalizeVersion, - showError, - showSuccess, -} from '../helpers'; -import { marked } from 'marked'; - -const OtherSetting = () => { - let [inputs, setInputs] = useState({ - Footer: '', - Notice: '', - About: '', - SystemName: '', - HomePageLink: '', - }); - let [loading, setLoading] = useState(false); - const [showUpdateModal, setShowUpdateModal] = useState(false); - const [updateData, setUpdateData] = useState({ - tag_name: '', - content: '', - }); - - const getOptions = async () => { - const res = await API.get('/api/option/'); - const { success, message, data } = res.data; - if (success) { - let newInputs = {}; - data.forEach((item) => { - if (item.key in inputs) { - newInputs[item.key] = item.value; - } - }); - setInputs(newInputs); - } else { - showError(message); - } - }; - - useEffect(() => { - getOptions().then(); - }, []); - - const updateOption = async (key, value) => { - setLoading(true); - const res = await API.put('/api/option', { - key, - value, - }); - const { success, message } = res.data; - if (success) { - setInputs((inputs) => ({ ...inputs, [key]: value })); - } else { - showError(message); - } - setLoading(false); - }; - - const handleInputChange = async (e, { name, value }) => { - setInputs((inputs) => ({ ...inputs, [name]: value })); - }; - - const submitNotice = async () => { - await updateOption('Notice', inputs.Notice); - }; - - const submitFooter = async () => { - await updateOption('Footer', inputs.Footer); - }; - - const submitSystemName = async () => { - await updateOption('SystemName', inputs.SystemName); - }; - - const submitHomePageLink = async () => { - await updateOption('HomePageLink', inputs.HomePageLink); - }; - - const submitAbout = async () => { - await updateOption('About', inputs.About); - }; - - const openGitHubRelease = () => { - window.location = 'https://github.com/Rain-kl/ATSFlare/releases/latest'; - }; - - const checkUpdate = async () => { - const res = await API.get('/api/update/latest-release'); - const { success, message, data } = res.data; - if (!success) { - showError(message); - return; - } - const { tag_name, body } = data; - if ( - hasBuildVersion() && - normalizeVersion(tag_name) === getBuildVersion() - ) { - showSuccess(`已是最新版本:${tag_name}`); - } else { - setUpdateData({ - tag_name: tag_name, - content: marked.parse(body), - }); - setShowUpdateModal(true); - } - }; - - return ( - - -
-
通用设置
- 检查更新 - - - - 保存公告 - -
个性化设置
- - - - 设置系统名称 - - - - 设置首页链接 - - - - 保存关于 - - - - 设置页脚 - -
- setShowUpdateModal(false)} - onOpen={() => setShowUpdateModal(true)} - open={showUpdateModal} - > - 新版本:{updateData.tag_name} - - -
-
-
- - - - - - -
- ); -}; - -export default PasswordResetConfirm; diff --git a/atsf_server/web/src/components/PasswordResetForm.js b/atsf_server/web/src/components/PasswordResetForm.js deleted file mode 100644 index 8de95d6c..00000000 --- a/atsf_server/web/src/components/PasswordResetForm.js +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react'; -import { API, showError, showInfo, showSuccess } from '../helpers'; -import Turnstile from 'react-turnstile'; - -const PasswordResetForm = () => { - const [inputs, setInputs] = useState({ - email: '', - }); - const { email } = inputs; - - const [loading, setLoading] = useState(false); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); - - useEffect(() => { - let status = localStorage.getItem('status'); - if (status) { - status = JSON.parse(status); - if (status.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } - } - }, []); - - function handleChange(e) { - const { name, value } = e.target; - setInputs((inputs) => ({ ...inputs, [name]: value })); - } - - async function handleSubmit(e) { - if (!email) return; - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setLoading(true); - const res = await API.get( - `/api/reset_password?email=${email}&turnstile=${turnstileToken}` - ); - const { success, message } = res.data; - if (success) { - showSuccess('重置邮件发送成功,请检查邮箱!'); - setInputs({ ...inputs, email: '' }); - } else { - showError(message); - } - setLoading(false); - } - - return ( - - -
- 密码重置 -
-
- - - {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} - - -
-
-
- ); -}; - -export default PasswordResetForm; diff --git a/atsf_server/web/src/components/PersonalSetting.js b/atsf_server/web/src/components/PersonalSetting.js deleted file mode 100644 index 76c0cdcc..00000000 --- a/atsf_server/web/src/components/PersonalSetting.js +++ /dev/null @@ -1,217 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Divider, Form, Header, Image, Modal } from 'semantic-ui-react'; -import { Link } from 'react-router-dom'; -import { API, copy, showError, showInfo, showSuccess } from '../helpers'; -import Turnstile from 'react-turnstile'; - -const PersonalSetting = () => { - const [inputs, setInputs] = useState({ - wechat_verification_code: '', - email_verification_code: '', - email: '', - }); - const [status, setStatus] = useState({}); - const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); - const [showEmailBindModal, setShowEmailBindModal] = useState(false); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); - const [loading, setLoading] = useState(false); - - useEffect(() => { - let status = localStorage.getItem('status'); - if (status) { - status = JSON.parse(status); - setStatus(status); - if (status.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } - } - }, []); - - const handleInputChange = (e, { name, value }) => { - setInputs((inputs) => ({ ...inputs, [name]: value })); - }; - - const generateToken = async () => { - const res = await API.get('/api/user/token'); - const { success, message, data } = res.data; - if (success) { - await copy(data); - showSuccess(`令牌已重置并已复制到剪贴板:${data}`); - } else { - showError(message); - } - }; - - const bindWeChat = async () => { - if (inputs.wechat_verification_code === '') return; - const res = await API.get( - `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}` - ); - const { success, message } = res.data; - if (success) { - showSuccess('微信账户绑定成功!'); - setShowWeChatBindModal(false); - } else { - showError(message); - } - }; - - const openGitHubOAuth = () => { - window.open( - `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email` - ); - }; - - const sendVerificationCode = async () => { - if (inputs.email === '') return; - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setLoading(true); - const res = await API.get( - `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` - ); - const { success, message } = res.data; - if (success) { - showSuccess('验证码发送成功,请检查邮箱!'); - } else { - showError(message); - } - setLoading(false); - }; - - const bindEmail = async () => { - if (inputs.email_verification_code === '') return; - setLoading(true); - const res = await API.get( - `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}` - ); - const { success, message } = res.data; - if (success) { - showSuccess('邮箱账户绑定成功!'); - setShowEmailBindModal(false); - } else { - showError(message); - } - setLoading(false); - }; - - return ( -
-
通用设置
- - - -
账号绑定
- {status.wechat_login && ( - - )} - setShowWeChatBindModal(false)} - onOpen={() => setShowWeChatBindModal(true)} - open={showWeChatBindModal} - size={'mini'} - > - - - -
-

- 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) -

-
-
- - - -
-
-
- {status.github_oauth && ( - - )} - - setShowEmailBindModal(false)} - onOpen={() => setShowEmailBindModal(true)} - open={showEmailBindModal} - size={'tiny'} - style={{ maxWidth: '450px' }} - > - 绑定邮箱地址 - - -
- - 获取验证码 - - } - /> - - {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} - - -
-
-
-
- ); -}; - -export default PersonalSetting; diff --git a/atsf_server/web/src/components/PrivateRoute.js b/atsf_server/web/src/components/PrivateRoute.js deleted file mode 100644 index f7cc7248..00000000 --- a/atsf_server/web/src/components/PrivateRoute.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Navigate } from 'react-router-dom'; - -import { history } from '../helpers'; - - -function PrivateRoute({ children }) { - if (!localStorage.getItem('user')) { - return ; - } - return children; -} - -export { PrivateRoute }; \ No newline at end of file diff --git a/atsf_server/web/src/components/RegisterForm.js b/atsf_server/web/src/components/RegisterForm.js deleted file mode 100644 index b94ea20b..00000000 --- a/atsf_server/web/src/components/RegisterForm.js +++ /dev/null @@ -1,193 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - Button, - Form, - Grid, - Header, - Image, - Message, - Segment, -} from 'semantic-ui-react'; -import { Link, useNavigate } from 'react-router-dom'; -import { API, showError, showInfo, showSuccess } from '../helpers'; -import Turnstile from 'react-turnstile'; - -const RegisterForm = () => { - const [inputs, setInputs] = useState({ - username: '', - password: '', - password2: '', - email: '', - verification_code: '', - }); - const { username, password, password2 } = inputs; - const [showEmailVerification, setShowEmailVerification] = useState(false); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); - const [loading, setLoading] = useState(false); - - useEffect(() => { - let status = localStorage.getItem('status'); - if (status) { - status = JSON.parse(status); - setShowEmailVerification(status.email_verification); - if (status.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } - } - }); - - let navigate = useNavigate(); - - function handleChange(e) { - const { name, value } = e.target; - console.log(name, value); - setInputs((inputs) => ({ ...inputs, [name]: value })); - } - - async function handleSubmit(e) { - if (password.length < 8) { - showInfo('密码长度不得小于 8 位!'); - return; - } - if (password !== password2) { - showInfo('两次输入的密码不一致'); - return; - } - if (username && password) { - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setLoading(true); - const res = await API.post( - `/api/user/register?turnstile=${turnstileToken}`, - inputs - ); - const { success, message } = res.data; - if (success) { - navigate('/login'); - showSuccess('注册成功!'); - } else { - showError(message); - } - setLoading(false); - } - } - - const sendVerificationCode = async () => { - if (inputs.email === '') return; - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setLoading(true); - const res = await API.get( - `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` - ); - const { success, message } = res.data; - if (success) { - showSuccess('验证码发送成功,请检查你的邮箱!'); - } else { - showError(message); - } - setLoading(false); - }; - - return ( - - -
- 新用户注册 -
-
- - - - - {showEmailVerification ? ( - <> - - 获取验证码 - - } - /> - - - ) : ( - <> - )} - {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} - - -
- - 已有账户? - - 点击登录 - - -
-
- ); -}; - -export default RegisterForm; diff --git a/atsf_server/web/src/components/SystemSetting.js b/atsf_server/web/src/components/SystemSetting.js deleted file mode 100644 index 45a98063..00000000 --- a/atsf_server/web/src/components/SystemSetting.js +++ /dev/null @@ -1,399 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react'; -import { API, removeTrailingSlash, showError } from '../helpers'; - -const SystemSetting = () => { - let [inputs, setInputs] = useState({ - PasswordLoginEnabled: '', - PasswordRegisterEnabled: '', - EmailVerificationEnabled: '', - GitHubOAuthEnabled: '', - GitHubClientId: '', - GitHubClientSecret: '', - Notice: '', - SMTPServer: '', - SMTPPort: '', - SMTPAccount: '', - SMTPToken: '', - ServerAddress: '', - Footer: '', - WeChatAuthEnabled: '', - WeChatServerAddress: '', - WeChatServerToken: '', - WeChatAccountQRCodeImageURL: '', - TurnstileCheckEnabled: '', - TurnstileSiteKey: '', - TurnstileSecretKey: '', - RegisterEnabled: '', - }); - const [originInputs, setOriginInputs] = useState({}); - let [loading, setLoading] = useState(false); - - const getOptions = async () => { - const res = await API.get('/api/option/'); - const { success, message, data } = res.data; - if (success) { - let newInputs = {}; - data.forEach((item) => { - newInputs[item.key] = item.value; - }); - setInputs(newInputs); - setOriginInputs(newInputs); - } else { - showError(message); - } - }; - - useEffect(() => { - getOptions().then(); - }, []); - - const updateOption = async (key, value) => { - setLoading(true); - switch (key) { - case 'PasswordLoginEnabled': - case 'PasswordRegisterEnabled': - case 'EmailVerificationEnabled': - case 'GitHubOAuthEnabled': - case 'WeChatAuthEnabled': - case 'TurnstileCheckEnabled': - case 'RegisterEnabled': - value = inputs[key] === 'true' ? 'false' : 'true'; - break; - default: - break; - } - const res = await API.put('/api/option/', { - key, - value, - }); - const { success, message } = res.data; - if (success) { - setInputs((inputs) => ({ ...inputs, [key]: value })); - } else { - showError(message); - } - setLoading(false); - }; - - const handleInputChange = async (e, { name, value }) => { - if ( - name === 'Notice' || - name.startsWith('SMTP') || - name === 'ServerAddress' || - name === 'GitHubClientId' || - name === 'GitHubClientSecret' || - name === 'WeChatServerAddress' || - name === 'WeChatServerToken' || - name === 'WeChatAccountQRCodeImageURL' || - name === 'TurnstileSiteKey' || - name === 'TurnstileSecretKey' - ) { - setInputs((inputs) => ({ ...inputs, [name]: value })); - } else { - await updateOption(name, value); - } - }; - - const submitServerAddress = async () => { - let ServerAddress = removeTrailingSlash(inputs.ServerAddress); - await updateOption('ServerAddress', ServerAddress); - }; - - const submitSMTP = async () => { - if (originInputs['SMTPServer'] !== inputs.SMTPServer) { - await updateOption('SMTPServer', inputs.SMTPServer); - } - if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) { - await updateOption('SMTPAccount', inputs.SMTPAccount); - } - if ( - originInputs['SMTPPort'] !== inputs.SMTPPort && - inputs.SMTPPort !== '' - ) { - await updateOption('SMTPPort', inputs.SMTPPort); - } - if ( - originInputs['SMTPToken'] !== inputs.SMTPToken && - inputs.SMTPToken !== '' - ) { - await updateOption('SMTPToken', inputs.SMTPToken); - } - }; - - const submitWeChat = async () => { - if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { - await updateOption( - 'WeChatServerAddress', - removeTrailingSlash(inputs.WeChatServerAddress) - ); - } - if ( - originInputs['WeChatAccountQRCodeImageURL'] !== - inputs.WeChatAccountQRCodeImageURL - ) { - await updateOption( - 'WeChatAccountQRCodeImageURL', - inputs.WeChatAccountQRCodeImageURL - ); - } - if ( - originInputs['WeChatServerToken'] !== inputs.WeChatServerToken && - inputs.WeChatServerToken !== '' - ) { - await updateOption('WeChatServerToken', inputs.WeChatServerToken); - } - }; - - const submitGitHubOAuth = async () => { - if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { - await updateOption('GitHubClientId', inputs.GitHubClientId); - } - if ( - originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret && - inputs.GitHubClientSecret !== '' - ) { - await updateOption('GitHubClientSecret', inputs.GitHubClientSecret); - } - }; - - const submitTurnstile = async () => { - if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { - await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); - } - if ( - originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && - inputs.TurnstileSecretKey !== '' - ) { - await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey); - } - }; - - return ( - - -
-
通用设置
- - - - - 更新服务器地址 - - -
配置登录注册
- - - - - - - - - - - - -
- 配置 SMTP - 用以支持系统的邮件发送 -
- - - - - - - 保存 SMTP 设置 - -
- 配置 GitHub OAuth App - - 用以支持通过 GitHub 进行登录注册, - - 点击此处 - - 管理你的 GitHub OAuth App - -
- - Homepage URL 填 {inputs.ServerAddress} - ,Authorization callback URL 填{' '} - {`${inputs.ServerAddress}/oauth/github`} - - - - - - - 保存 GitHub OAuth 设置 - - -
- 配置 WeChat Server - - 用以支持通过微信进行登录注册, - - 点击此处 - - 了解 WeChat Server - -
- - - - - - - 保存 WeChat Server 设置 - - -
- 配置 Turnstile - - 用以支持用户校验, - - 点击此处 - - 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type - -
- - - - - - 保存 Turnstile 设置 - - -
-
- ); -}; - -export default SystemSetting; diff --git a/atsf_server/web/src/components/UsersTable.js b/atsf_server/web/src/components/UsersTable.js deleted file mode 100644 index 67cb787c..00000000 --- a/atsf_server/web/src/components/UsersTable.js +++ /dev/null @@ -1,317 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - Button, - Form, - Label, - Pagination, - Popup, - Table, -} from 'semantic-ui-react'; -import { Link } from 'react-router-dom'; -import { API, showError, showSuccess } from '../helpers'; - -import { ITEMS_PER_PAGE } from '../constants'; - -function renderRole(role) { - switch (role) { - case 1: - return ; - case 10: - return ; - case 100: - return ; - default: - return ; - } -} - -const UsersTable = () => { - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searching, setSearching] = useState(false); - - const loadUsers = async (startIdx) => { - const res = await API.get(`/api/user/?p=${startIdx}`); - const { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setUsers(data); - } else { - let newUsers = users; - newUsers.push(...data); - setUsers(newUsers); - } - } else { - showError(message); - } - setLoading(false); - }; - - const onPaginationChange = (e, { activePage }) => { - (async () => { - if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { - // In this case we have to load more data and then append them. - await loadUsers(activePage - 1); - } - setActivePage(activePage); - })(); - }; - - useEffect(() => { - loadUsers(0) - .then() - .catch((reason) => { - showError(reason); - }); - }, []); - - const manageUser = (username, action, idx) => { - (async () => { - const res = await API.post('/api/user/manage', { - username, - action, - }); - const { success, message } = res.data; - if (success) { - showSuccess('操作成功完成!'); - let user = res.data.data; - let newUsers = [...users]; - let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; - if (action === 'delete') { - newUsers[realIdx].deleted = true; - } else { - newUsers[realIdx].status = user.status; - newUsers[realIdx].role = user.role; - } - setUsers(newUsers); - } else { - showError(message); - } - })(); - }; - - const renderStatus = (status) => { - switch (status) { - case 1: - return ; - case 2: - return ( - - ); - default: - return ( - - ); - } - }; - - const searchUsers = async () => { - if (searchKeyword === '') { - // if keyword is blank, load files instead. - await loadUsers(0); - setActivePage(1); - return; - } - setSearching(true); - const res = await API.get(`/api/user/search?keyword=${searchKeyword}`); - const { success, message, data } = res.data; - if (success) { - setUsers(data); - setActivePage(1); - } else { - showError(message); - } - setSearching(false); - }; - - const handleKeywordChange = async (e, { value }) => { - setSearchKeyword(value.trim()); - }; - - const sortUser = (key) => { - if (users.length === 0) return; - setLoading(true); - let sortedUsers = [...users]; - sortedUsers.sort((a, b) => { - return ('' + a[key]).localeCompare(b[key]); - }); - if (sortedUsers[0].id === users[0].id) { - sortedUsers.reverse(); - } - setUsers(sortedUsers); - setLoading(false); - }; - - return ( - <> -
- - - - - - - { - sortUser('username'); - }} - > - 用户名 - - { - sortUser('display_name'); - }} - > - 显示名称 - - { - sortUser('email'); - }} - > - 邮箱地址 - - { - sortUser('role'); - }} - > - 用户角色 - - { - sortUser('status'); - }} - > - 状态 - - 操作 - - - - - {users - .slice( - (activePage - 1) * ITEMS_PER_PAGE, - activePage * ITEMS_PER_PAGE - ) - .map((user, idx) => { - if (user.deleted) return <>; - return ( - - {user.username} - {user.display_name} - {user.email ? user.email : '无'} - {renderRole(user.role)} - {renderStatus(user.status)} - -
- - - - 删除 - - } - on='click' - flowing - hoverable - > - - - - -
-
-
- ); - })} -
- - - - - - - - - -
- - ); -}; - -export default UsersTable; diff --git a/atsf_server/web/src/constants/common.constant.js b/atsf_server/web/src/constants/common.constant.js deleted file mode 100644 index 1a37d5f6..00000000 --- a/atsf_server/web/src/constants/common.constant.js +++ /dev/null @@ -1 +0,0 @@ -export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend! diff --git a/atsf_server/web/src/constants/index.js b/atsf_server/web/src/constants/index.js deleted file mode 100644 index 8f9ba808..00000000 --- a/atsf_server/web/src/constants/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './toast.constants'; -export * from './user.constants'; -export * from './common.constant'; \ No newline at end of file diff --git a/atsf_server/web/src/constants/toast.constants.js b/atsf_server/web/src/constants/toast.constants.js deleted file mode 100644 index 8b212350..00000000 --- a/atsf_server/web/src/constants/toast.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -export const toastConstants = { - SUCCESS_TIMEOUT: 500, - INFO_TIMEOUT: 3000, - ERROR_TIMEOUT: 5000, - WARNING_TIMEOUT: 10000, - NOTICE_TIMEOUT: 20000 -}; diff --git a/atsf_server/web/src/constants/user.constants.js b/atsf_server/web/src/constants/user.constants.js deleted file mode 100644 index 2680d8ef..00000000 --- a/atsf_server/web/src/constants/user.constants.js +++ /dev/null @@ -1,19 +0,0 @@ -export const userConstants = { - REGISTER_REQUEST: 'USERS_REGISTER_REQUEST', - REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS', - REGISTER_FAILURE: 'USERS_REGISTER_FAILURE', - - LOGIN_REQUEST: 'USERS_LOGIN_REQUEST', - LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS', - LOGIN_FAILURE: 'USERS_LOGIN_FAILURE', - - LOGOUT: 'USERS_LOGOUT', - - GETALL_REQUEST: 'USERS_GETALL_REQUEST', - GETALL_SUCCESS: 'USERS_GETALL_SUCCESS', - GETALL_FAILURE: 'USERS_GETALL_FAILURE', - - DELETE_REQUEST: 'USERS_DELETE_REQUEST', - DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', - DELETE_FAILURE: 'USERS_DELETE_FAILURE' -}; diff --git a/atsf_server/web/src/context/Status/index.js b/atsf_server/web/src/context/Status/index.js deleted file mode 100644 index 71f0682b..00000000 --- a/atsf_server/web/src/context/Status/index.js +++ /dev/null @@ -1,19 +0,0 @@ -// contexts/User/index.jsx - -import React from 'react'; -import { initialState, reducer } from './reducer'; - -export const StatusContext = React.createContext({ - state: initialState, - dispatch: () => null, -}); - -export const StatusProvider = ({ children }) => { - const [state, dispatch] = React.useReducer(reducer, initialState); - - return ( - - {children} - - ); -}; \ No newline at end of file diff --git a/atsf_server/web/src/context/Status/reducer.js b/atsf_server/web/src/context/Status/reducer.js deleted file mode 100644 index ec9ac6ae..00000000 --- a/atsf_server/web/src/context/Status/reducer.js +++ /dev/null @@ -1,20 +0,0 @@ -export const reducer = (state, action) => { - switch (action.type) { - case 'set': - return { - ...state, - status: action.payload, - }; - case 'unset': - return { - ...state, - status: undefined, - }; - default: - return state; - } -}; - -export const initialState = { - status: undefined, -}; diff --git a/atsf_server/web/src/context/User/index.js b/atsf_server/web/src/context/User/index.js deleted file mode 100644 index c6671591..00000000 --- a/atsf_server/web/src/context/User/index.js +++ /dev/null @@ -1,19 +0,0 @@ -// contexts/User/index.jsx - -import React from "react" -import { reducer, initialState } from "./reducer" - -export const UserContext = React.createContext({ - state: initialState, - dispatch: () => null -}) - -export const UserProvider = ({ children }) => { - const [state, dispatch] = React.useReducer(reducer, initialState) - - return ( - - { children } - - ) -} \ No newline at end of file diff --git a/atsf_server/web/src/context/User/reducer.js b/atsf_server/web/src/context/User/reducer.js deleted file mode 100644 index 9ed1d809..00000000 --- a/atsf_server/web/src/context/User/reducer.js +++ /dev/null @@ -1,21 +0,0 @@ -export const reducer = (state, action) => { - switch (action.type) { - case 'login': - return { - ...state, - user: action.payload - }; - case 'logout': - return { - ...state, - user: undefined - }; - - default: - return state; - } -}; - -export const initialState = { - user: undefined -}; \ No newline at end of file diff --git a/atsf_server/web/src/helpers/api.js b/atsf_server/web/src/helpers/api.js deleted file mode 100644 index 35fdb1e9..00000000 --- a/atsf_server/web/src/helpers/api.js +++ /dev/null @@ -1,13 +0,0 @@ -import { showError } from './utils'; -import axios from 'axios'; - -export const API = axios.create({ - baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '', -}); - -API.interceptors.response.use( - (response) => response, - (error) => { - showError(error); - } -); diff --git a/atsf_server/web/src/helpers/auth-header.js b/atsf_server/web/src/helpers/auth-header.js deleted file mode 100644 index a8fe5f5a..00000000 --- a/atsf_server/web/src/helpers/auth-header.js +++ /dev/null @@ -1,10 +0,0 @@ -export function authHeader() { - // return authorization header with jwt token - let user = JSON.parse(localStorage.getItem('user')); - - if (user && user.token) { - return { 'Authorization': 'Bearer ' + user.token }; - } else { - return {}; - } -} \ No newline at end of file diff --git a/atsf_server/web/src/helpers/history.js b/atsf_server/web/src/helpers/history.js deleted file mode 100644 index 629039e5..00000000 --- a/atsf_server/web/src/helpers/history.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createBrowserHistory } from 'history'; - -export const history = createBrowserHistory(); \ No newline at end of file diff --git a/atsf_server/web/src/helpers/index.js b/atsf_server/web/src/helpers/index.js deleted file mode 100644 index 505a8cf9..00000000 --- a/atsf_server/web/src/helpers/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from './history'; -export * from './auth-header'; -export * from './utils'; -export * from './api'; \ No newline at end of file diff --git a/atsf_server/web/src/helpers/utils.js b/atsf_server/web/src/helpers/utils.js deleted file mode 100644 index 11ba15a2..00000000 --- a/atsf_server/web/src/helpers/utils.js +++ /dev/null @@ -1,181 +0,0 @@ -import { toast } from 'react-toastify'; -import { toastConstants } from '../constants'; - -export function isAdmin() { - let user = localStorage.getItem('user'); - if (!user) return false; - user = JSON.parse(user); - return user.role >= 10; -} - -export function isRoot() { - let user = localStorage.getItem('user'); - if (!user) return false; - user = JSON.parse(user); - return user.role >= 100; -} - -export function getSystemName() { - let system_name = localStorage.getItem('system_name'); - if (!system_name) return 'ATSFlare'; - return system_name; -} - -export function getFooterHTML() { - return localStorage.getItem('footer_html'); -} - -export function normalizeVersion(value) { - if (!value) return ''; - return String(value).trim().replace(/^v/i, ''); -} - -export function getBuildVersion() { - return normalizeVersion(process.env.REACT_APP_VERSION); -} - -export function hasBuildVersion() { - return getBuildVersion() !== ''; -} - -export async function copy(text, label) { - let okay = true; - try { - await navigator.clipboard.writeText(text); - } catch (e) { - okay = false; - console.error(e); - } - if (okay && label) { - showSuccess(`${label} 已复制`); - } - return okay; -} - -export function isMobile() { - return window.innerWidth <= 600; -} - -let showErrorOptions = { autoClose: toastConstants.ERROR_TIMEOUT }; -let showWarningOptions = { autoClose: toastConstants.WARNING_TIMEOUT }; -let showSuccessOptions = { autoClose: toastConstants.SUCCESS_TIMEOUT }; -let showInfoOptions = { autoClose: toastConstants.INFO_TIMEOUT }; -let showNoticeOptions = { autoClose: false }; - -if (isMobile()) { - showErrorOptions.position = 'top-center'; - // showErrorOptions.transition = 'flip'; - - showSuccessOptions.position = 'top-center'; - // showSuccessOptions.transition = 'flip'; - - showInfoOptions.position = 'top-center'; - // showInfoOptions.transition = 'flip'; - - showNoticeOptions.position = 'top-center'; - // showNoticeOptions.transition = 'flip'; -} - -export function showError(error) { - console.error(error); - if (error.message) { - if (error.name === 'AxiosError') { - switch (error.response.status) { - case 401: - // toast.error('错误:未登录或登录已过期,请重新登录!', showErrorOptions); - window.location.href = '/login?expired=true'; - break; - case 429: - toast.error('错误:请求次数过多,请稍后再试!', showErrorOptions); - break; - case 500: - toast.error('错误:服务器内部错误,请联系管理员!', showErrorOptions); - break; - case 405: - toast.info('本站仅作演示之用,无服务端!'); - break; - default: - toast.error('错误:' + error.message, showErrorOptions); - } - return; - } - toast.error('错误:' + error.message, showErrorOptions); - } else { - toast.error('错误:' + error, showErrorOptions); - } -} - -export function showWarning(message) { - toast.warn(message, showWarningOptions); -} - -export function showSuccess(message) { - toast.success(message, showSuccessOptions); -} - -export function showInfo(message) { - toast.info(message, showInfoOptions); -} - -export function showNotice(message) { - toast.info(message, showNoticeOptions); -} - -export function openPage(url) { - window.open(url); -} - -export function removeTrailingSlash(url) { - if (url.endsWith('/')) { - return url.slice(0, -1); - } else { - return url; - } -} - -export function timestamp2string(timestamp) { - let date = new Date(timestamp * 1000); - let year = date.getFullYear().toString(); - let month = (date.getMonth() + 1).toString(); - let day = date.getDate().toString(); - let hour = date.getHours().toString(); - let minute = date.getMinutes().toString(); - let second = date.getSeconds().toString(); - if (month.length === 1) { - month = '0' + month; - } - if (day.length === 1) { - day = '0' + day; - } - if (hour.length === 1) { - hour = '0' + hour; - } - if (minute.length === 1) { - minute = '0' + minute; - } - if (second.length === 1) { - second = '0' + second; - } - return ( - year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second - ); -} - -export function formatDateTime(value) { - if (!value) return '无'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - return date.toLocaleString('zh-CN', { hour12: false }); -} - -export function timeAgo(value) { - if (!value) return '暂无'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - const diff = Math.floor((Date.now() - date.getTime()) / 1000); - if (diff < 0) return '刚刚'; - if (diff < 60) return `${diff} 秒前`; - if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`; - if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`; - return `${Math.floor(diff / 86400)} 天前`; -} diff --git a/atsf_server/web/src/index.css b/atsf_server/web/src/index.css deleted file mode 100644 index 713b9594..00000000 --- a/atsf_server/web/src/index.css +++ /dev/null @@ -1,74 +0,0 @@ -body { - margin: 0; - padding-top: 55px; - overflow-y: scroll; - font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - scrollbar-width: none; -} - -body::-webkit-scrollbar { - display: none; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; -} - -.main-content { - padding: 4px; -} - -.small-icon .icon { - font-size: 1em !important; -} - -.custom-footer { - font-size: 1.1em; -} - -.page-toolbar { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; - margin-bottom: 20px; -} - -.page-subtitle { - margin-top: 6px; - color: rgba(0, 0, 0, 0.65); -} - -.atsf-table td { - vertical-align: top !important; -} - -.atsf-pre { - white-space: pre-wrap; - word-break: break-word; - background: #f7f8fa; - border: 1px solid #e3e6ea; - border-radius: 8px; - padding: 12px; - font-size: 12px; - line-height: 1.5; -} - -.table-meta { - margin-top: 6px; - color: rgba(0, 0, 0, 0.58); - font-size: 12px; -} - -@media only screen and (max-width: 600px) { - .hide-on-mobile { - display: none !important; - } - - .page-toolbar { - flex-direction: column; - align-items: stretch; - } -} diff --git a/atsf_server/web/src/index.js b/atsf_server/web/src/index.js deleted file mode 100644 index eca5c3c0..00000000 --- a/atsf_server/web/src/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; -import { Container } from 'semantic-ui-react'; -import App from './App'; -import Header from './components/Header'; -import Footer from './components/Footer'; -import 'semantic-ui-css/semantic.min.css'; -import './index.css'; -import { UserProvider } from './context/User'; -import { ToastContainer } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; -import { StatusProvider } from './context/Status'; - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render( - - - - -
- - - - -