diff --git a/atsf_server/web/features/config-versions/components/config-versions-page.tsx b/atsf_server/web/features/config-versions/components/config-versions-page.tsx index cfc7b3e3..2fef4983 100644 --- a/atsf_server/web/features/config-versions/components/config-versions-page.tsx +++ b/atsf_server/web/features/config-versions/components/config-versions-page.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useState } from 'react'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; @@ -28,7 +28,7 @@ import { PrimaryButton, SecondaryButton, } from '@/features/shared/components/resource-primitives'; -import { formatDateTime } from '@/lib/utils/date'; +import { formatDateTime } from '@/lib/utils/date'; const versionsQueryKey = ['config-versions']; @@ -249,7 +249,7 @@ export function ConfigVersionsPage() { }, }); - const handleOpenPublishPreview = async () => { + const handleOpenPublishPreview = async () => { setFeedback(null); setIsPreviewLoading(true); @@ -280,20 +280,20 @@ export function ConfigVersionsPage() { return (
- - {isPreviewLoading ? '加载预览中...' : '预览并发布'} - - } - /> + + {isPreviewLoading ? '加载预览中...' : '预览并发布'} + + } + /> {feedback ? : null} -
- {publishPreview ? ( +
+ {publishPreview ? ( publishMutation.mutate()} onCancel={() => setPublishPreview(null)} /> - ) : ( - -
-

1. 使用“预览并发布”查看新增、删除和修改的域名清单。

-

2. 确认渲染出的 Nginx 配置与支持文件内容符合预期。

-

3. 若需要回滚,可在下方历史版本列表中重新激活旧版本。

-
-
- )} + ) : null}
- + {versionsQuery.isLoading ? ( ) : versionsQuery.isError ? ( diff --git a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx index a6980c8a..40aa1ddf 100644 --- a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx +++ b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx @@ -219,36 +219,6 @@ export function DashboardOverview() { return (
- } - > -
-
-

信息架构

-

首页负责总览

-

- 模块页保留列表、表单和具体操作,摘要统一回收到仪表盘。 -

-
-
-

导航效率

-

顶栏与侧栏已精简

-

- 顶栏只保留版本、主题切换和用户入口,侧栏宽度与按钮高度同步压缩。 -

-
-
-

响应式

-

小屏可正常展开侧栏

-

- 小于 1000px 时切换为抽屉侧栏,不再出现按钮可点但导航不可见的问题。 -

-
-
-
-
{dashboardCards.map((card) => ( diff --git a/atsf_server/web/features/managed-domains/components/managed-domains-page.tsx b/atsf_server/web/features/managed-domains/components/managed-domains-page.tsx index 57e006e8..f66ebebe 100644 --- a/atsf_server/web/features/managed-domains/components/managed-domains-page.tsx +++ b/atsf_server/web/features/managed-domains/components/managed-domains-page.tsx @@ -228,7 +228,6 @@ export function ManagedDomainsPage() { (null); - const [editingNodeId, setEditingNodeId] = useState(null); - const [isEditorOpen, setIsEditorOpen] = useState(false); - const [selectedNode, setSelectedNode] = useState(null); - const [serverUrl, setServerUrl] = useState(''); - const [, setRefreshTick] = useState(0); +export function NodesPage() { + const queryClient = useQueryClient(); + const [feedback, setFeedback] = useState(null); + const [editingNodeId, setEditingNodeId] = useState(null); + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [selectedNode, setSelectedNode] = useState(null); + const [serverUrl, setServerUrl] = useState(''); + const [, setRefreshTick] = useState(0); const form = useForm({ resolver: zodResolver(nodeSchema), @@ -209,15 +209,15 @@ export function NodesPage() { return editingNodeId ? updateNode(editingNodeId, payload) : createNode(payload); }, onSuccess: async (node) => { - setFeedback({ - tone: 'success', - message: editingNodeId ? '节点已更新。' : '节点已创建。', - }); - setEditingNodeId(null); - setIsEditorOpen(false); - setSelectedNode(node); - form.reset(defaultValues); - await Promise.all([ + setFeedback({ + tone: 'success', + message: editingNodeId ? '节点已更新。' : '节点已创建。', + }); + setEditingNodeId(null); + setIsEditorOpen(false); + setSelectedNode(node); + form.reset(defaultValues); + await Promise.all([ queryClient.invalidateQueries({ queryKey: nodesQueryKey }), queryClient.invalidateQueries({ queryKey: bootstrapQueryKey }), ]); @@ -276,30 +276,30 @@ export function NodesPage() { return nodes.find((item) => item.id === selectedNode.id) ?? selectedNode; }, [nodes, selectedNode]); - const handleReset = () => { - setFeedback(null); - setEditingNodeId(null); - setIsEditorOpen(false); - form.reset(defaultValues); - }; - - const handleCreate = () => { - setFeedback(null); - setEditingNodeId(null); - form.reset(defaultValues); - setIsEditorOpen(true); - }; - - const handleEdit = (node: NodeItem) => { - setFeedback(null); - setEditingNodeId(node.id); - setSelectedNode(node); - form.reset({ - name: node.name, - auto_update_enabled: node.auto_update_enabled, - }); - setIsEditorOpen(true); - }; + const handleReset = () => { + setFeedback(null); + setEditingNodeId(null); + setIsEditorOpen(false); + form.reset(defaultValues); + }; + + const handleCreate = () => { + setFeedback(null); + setEditingNodeId(null); + form.reset(defaultValues); + setIsEditorOpen(true); + }; + + const handleEdit = (node: NodeItem) => { + setFeedback(null); + setEditingNodeId(node.id); + setSelectedNode(node); + form.reset({ + name: node.name, + auto_update_enabled: node.auto_update_enabled, + }); + setIsEditorOpen(true); + }; const handleDelete = (node: NodeItem) => { if ( @@ -345,76 +345,30 @@ export function NodesPage() { ? buildNodeInstallCommand(normalizedServerUrl, selectedNodeFromList.agent_token) : ''; - return ( - <> -
- - - 新增节点 - - - 应用记录 - - - } - /> - - {feedback ? : null} - -
- - - {rotateMutation.isPending ? '生成中...' : '重新生成 Token'} - - {discoveryCommand ? ( - void handleCopy(discoveryCommand, 'Discovery 部署命令已复制。')} - > - 复制命令 - - ) : null} -
- } - > - {bootstrapQuery.isLoading ? ( - - ) : bootstrapQuery.isError ? ( - - ) : ( -
- - setServerUrl(event.target.value)} /> - + return ( + <> +
+ + + 新增节点 + + + 应用记录 + + + } + /> -
-
-

Discovery Token

-

- {bootstrapToken || '暂无'} -

-
-
- 创建节点后也可以使用节点专属 Token 安装,这种方式适合预分配节点名与更新策略。 -
-
- - {discoveryCommand ? {discoveryCommand} : null} -
- )} - + {feedback ? : null} +
- void queryClient.invalidateQueries({ queryKey: nodesQueryKey })} - > - 刷新列表 - - } - > + void queryClient.invalidateQueries({ queryKey: nodesQueryKey })} + > + 刷新列表 + + } + > {nodesQuery.isLoading ? ( ) : nodesQuery.isError ? ( @@ -594,41 +548,41 @@ export function NodesPage() {
)} -
- - - 取消 - - - {saveMutation.isPending ? '保存中...' : editingNodeId ? '保存修改' : '新增节点'} - -
- } - > -
- - - - - - form.setValue('auto_update_enabled', checked, { - shouldDirty: true, - shouldValidate: true, - }) - } - /> - - - - ); -} +
+ + + 取消 + + + {saveMutation.isPending ? '保存中...' : editingNodeId ? '保存修改' : '新增节点'} + +
+ } + > +
+ + + + + + form.setValue('auto_update_enabled', checked, { + shouldDirty: true, + shouldValidate: true, + }) + } + /> + + + + ); +} diff --git a/atsf_server/web/features/proxy-routes/components/proxy-routes-page.tsx b/atsf_server/web/features/proxy-routes/components/proxy-routes-page.tsx index e7fdd936..b2fc08e6 100644 --- a/atsf_server/web/features/proxy-routes/components/proxy-routes-page.tsx +++ b/atsf_server/web/features/proxy-routes/components/proxy-routes-page.tsx @@ -7,13 +7,13 @@ import { useFieldArray, useForm, useWatch } from 'react-hook-form'; import { z } from 'zod'; 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 { PageHeader } from '@/components/layout/page-header'; -import { AppModal } from '@/components/ui/app-modal'; -import { AppCard } from '@/components/ui/app-card'; -import { StatusBadge } from '@/components/ui/status-badge'; +import { ErrorState } from '@/components/feedback/error-state'; +import { InlineMessage } from '@/components/feedback/inline-message'; +import { LoadingState } from '@/components/feedback/loading-state'; +import { PageHeader } from '@/components/layout/page-header'; +import { AppModal } from '@/components/ui/app-modal'; +import { AppCard } from '@/components/ui/app-card'; +import { StatusBadge } from '@/components/ui/status-badge'; import { publishConfigVersion } from '@/features/config-versions/api/config-versions'; import { createProxyRoute, @@ -212,13 +212,13 @@ function getMatchMessage( return '未找到匹配证书,可继续手动选择。'; } -export function ProxyRoutesPage() { - const queryClient = useQueryClient(); - const [feedback, setFeedback] = useState(null); - const [editingRouteId, setEditingRouteId] = useState(null); - const [isEditorOpen, setIsEditorOpen] = useState(false); - const [matchResult, setMatchResult] = useState(null); - const [isMatching, setIsMatching] = useState(false); +export function ProxyRoutesPage() { + const queryClient = useQueryClient(); + const [feedback, setFeedback] = useState(null); + const [editingRouteId, setEditingRouteId] = useState(null); + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [matchResult, setMatchResult] = useState(null); + const [isMatching, setIsMatching] = useState(false); const form = useForm({ resolver: zodResolver(proxyRouteSchema), @@ -253,13 +253,13 @@ export function ProxyRoutesPage() { ? updateProxyRoute(editingRouteId, payload) : createProxyRoute(payload); }, - onSuccess: async () => { - setFeedback({ tone: 'success', message: editingRouteId ? '规则已更新。' : '规则已创建。' }); - setEditingRouteId(null); - setIsEditorOpen(false); - setMatchResult(null); - form.reset(defaultValues); - await queryClient.invalidateQueries({ queryKey: routesQueryKey }); + onSuccess: async () => { + setFeedback({ tone: 'success', message: editingRouteId ? '规则已更新。' : '规则已创建。' }); + setEditingRouteId(null); + setIsEditorOpen(false); + setMatchResult(null); + form.reset(defaultValues); + await queryClient.invalidateQueries({ queryKey: routesQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); @@ -339,34 +339,34 @@ export function ProxyRoutesPage() { [certificates], ); - const handleReset = () => { - setFeedback(null); - setEditingRouteId(null); - setIsEditorOpen(false); - setMatchResult(null); - form.reset(defaultValues); - }; - - const handleCreate = () => { - setFeedback(null); - setEditingRouteId(null); - setMatchResult(null); - form.reset(defaultValues); - setIsEditorOpen(true); - }; + const handleReset = () => { + setFeedback(null); + setEditingRouteId(null); + setIsEditorOpen(false); + setMatchResult(null); + form.reset(defaultValues); + }; + + const handleCreate = () => { + setFeedback(null); + setEditingRouteId(null); + setMatchResult(null); + form.reset(defaultValues); + setIsEditorOpen(true); + }; const handleSubmit = form.handleSubmit((values) => { setFeedback(null); saveMutation.mutate(values); }); - const handleEdit = (route: ProxyRouteItem) => { - setFeedback(null); - setEditingRouteId(route.id); - setMatchResult(null); - form.reset(toFormValues(route)); - setIsEditorOpen(true); - }; + const handleEdit = (route: ProxyRouteItem) => { + setFeedback(null); + setEditingRouteId(route.id); + setMatchResult(null); + form.reset(toFormValues(route)); + setIsEditorOpen(true); + }; const handleDelete = (route: ProxyRouteItem) => { if (!window.confirm(`确认删除规则 ${route.domain} 吗?`)) { @@ -388,27 +388,27 @@ export function ProxyRoutesPage() { const routes = routesQuery.data || []; - return ( - <> -
- - publishMutation.mutate()} disabled={publishMutation.isPending}> - {publishMutation.isPending ? '发布中...' : '发布当前规则'} - - - 新增规则 - - - } - /> - - {feedback ? : null} - - + return ( + <> +
+ + publishMutation.mutate()} disabled={publishMutation.isPending}> + {publishMutation.isPending ? '发布中...' : '发布当前规则'} + + + 新增规则 + + + } + /> + + {feedback ? : null} + + {routesQuery.isLoading ? ( ) : routesQuery.isError ? ( @@ -494,184 +494,184 @@ export function ProxyRoutesPage() {
)}
-
- - - 取消 - - - {saveMutation.isPending ? '保存中...' : editingRouteId ? '保存修改' : '新增规则'} - -
- } - > -
-
- - - - - - -
- -
- form.setValue('enabled', checked, { shouldDirty: true })} - /> - { - form.setValue('enable_https', checked, { shouldDirty: true, shouldValidate: true }); - if (!checked) { - form.setValue('cert_id', '', { shouldDirty: true, shouldValidate: true }); - form.setValue('redirect_http', false, { shouldDirty: true, shouldValidate: true }); - } - }} - /> -
- -
- - - form.setValue('cert_id', event.target.value, { - shouldDirty: true, - shouldValidate: true, - }) - } - > - - {certificates.map((certificate) => ( - - ))} - - - - form.setValue('redirect_http', checked, { - shouldDirty: true, - shouldValidate: true, - }) - } - /> -
- -
-
-
-

自定义请求头

-

- 可为空。若填写,请保证 Header 名称合法且不包含换行。 -

-
- append({ key: '', value: '' })} - className='px-3 py-2 text-xs' - > - 添加请求头 - -
- -
- {fields.map((field, index) => ( -
- - - - - - -
- handleRemoveHeader(index)} - className='w-full px-3 py-3 text-xs md:w-auto' - > - 删除 - -
-
- ))} -
-
- - -
-

- {getMatchMessage(matchResult, isMatching, watchedDomain, watchedEnableHttps)} -

- {matchResult?.matched && matchResult.candidates.length > 1 ? ( -
- {matchResult.candidates.map((candidate) => ( - - ))} -
- ) : null} -
- 当前可选证书:{certificatesQuery.isLoading ? '加载中...' : `${certificates.length} 张`} -
-
-
- - - - -
- - - ); -} + + + + 取消 + + + {saveMutation.isPending ? '保存中...' : editingRouteId ? '保存修改' : '新增规则'} + + + } + > +
+
+ + + + + + +
+ +
+ form.setValue('enabled', checked, { shouldDirty: true })} + /> + { + form.setValue('enable_https', checked, { shouldDirty: true, shouldValidate: true }); + if (!checked) { + form.setValue('cert_id', '', { shouldDirty: true, shouldValidate: true }); + form.setValue('redirect_http', false, { shouldDirty: true, shouldValidate: true }); + } + }} + /> +
+ +
+ + + form.setValue('cert_id', event.target.value, { + shouldDirty: true, + shouldValidate: true, + }) + } + > + + {certificates.map((certificate) => ( + + ))} + + + + form.setValue('redirect_http', checked, { + shouldDirty: true, + shouldValidate: true, + }) + } + /> +
+ +
+
+
+

自定义请求头

+

+ 可为空。若填写,请保证 Header 名称合法且不包含换行。 +

+
+ append({ key: '', value: '' })} + className='px-3 py-2 text-xs' + > + 添加请求头 + +
+ +
+ {fields.map((field, index) => ( +
+ + + + + + +
+ handleRemoveHeader(index)} + className='w-full px-3 py-3 text-xs md:w-auto' + > + 删除 + +
+
+ ))} +
+
+ + +
+

+ {getMatchMessage(matchResult, isMatching, watchedDomain, watchedEnableHttps)} +

+ {matchResult?.matched && matchResult.candidates.length > 1 ? ( +
+ {matchResult.candidates.map((candidate) => ( + + ))} +
+ ) : null} +
+ 当前可选证书:{certificatesQuery.isLoading ? '加载中...' : `${certificates.length} 张`} +
+
+
+ + + + +
+
+ + ); +} diff --git a/atsf_server/web/features/tls-certificates/components/tls-certificates-page.tsx b/atsf_server/web/features/tls-certificates/components/tls-certificates-page.tsx index c59fa4e5..b1747bcc 100644 --- a/atsf_server/web/features/tls-certificates/components/tls-certificates-page.tsx +++ b/atsf_server/web/features/tls-certificates/components/tls-certificates-page.tsx @@ -221,7 +221,6 @@ export function TlsCertificatesPage() { (null); - const [isEditorOpen, setIsEditorOpen] = useState(false); - const [feedback, setFeedback] = useState(null); +export function UsersPage() { + const queryClient = useQueryClient(); + const { user: currentUser } = useAuth(); + const [page, setPage] = useState(0); + const [searchInput, setSearchInput] = useState(''); + const [searchKeyword, setSearchKeyword] = useState(''); + const [editingUserId, setEditingUserId] = useState(null); + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [feedback, setFeedback] = useState(null); const form = useForm({ resolver: zodResolver(userSchema), @@ -129,15 +129,15 @@ export function UsersPage() { enabled: isAdmin && editingUserId !== null, }); - useEffect(() => { - if (editingUserQuery.data) { - form.reset({ - username: editingUserQuery.data.username, - display_name: editingUserQuery.data.display_name || '', - password: '', - }); - } - }, [editingUserQuery.data, form]); + useEffect(() => { + if (editingUserQuery.data) { + form.reset({ + username: editingUserQuery.data.username, + display_name: editingUserQuery.data.display_name || '', + password: '', + }); + } + }, [editingUserQuery.data, form]); const saveMutation = useMutation({ mutationFn: async (values: UserFormValues) => { @@ -155,13 +155,13 @@ export function UsersPage() { await createUser(payload); return '用户已创建。'; }, - onSuccess: async (message) => { - setFeedback({ tone: 'success', message }); - setEditingUserId(null); - setIsEditorOpen(false); - form.reset(defaultValues); - await Promise.all([ - queryClient.invalidateQueries({ queryKey: usersListQueryKey }), + onSuccess: async (message) => { + setFeedback({ tone: 'success', message }); + setEditingUserId(null); + setIsEditorOpen(false); + form.reset(defaultValues); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: usersListQueryKey }), queryClient.invalidateQueries({ queryKey: ['users', 'detail'] }), ]); }, @@ -198,7 +198,7 @@ export function UsersPage() { const activeQuery = searchKeyword.length > 0 ? searchQuery : usersQuery; const users = useMemo(() => activeQuery.data ?? [], [activeQuery.data]); - const handleSearchSubmit = () => { + const handleSearchSubmit = () => { setFeedback(null); setPage(0); setSearchKeyword(searchInput.trim()); @@ -211,25 +211,25 @@ export function UsersPage() { setFeedback(null); }; - const handleResetForm = () => { - setEditingUserId(null); - setIsEditorOpen(false); - setFeedback(null); - form.reset(defaultValues); - }; - - const handleCreate = () => { - setEditingUserId(null); - setFeedback(null); - form.reset(defaultValues); - setIsEditorOpen(true); - }; - - const handleEdit = (targetUser: UserItem) => { - setFeedback(null); - setEditingUserId(targetUser.id); - setIsEditorOpen(true); - }; + const handleResetForm = () => { + setEditingUserId(null); + setIsEditorOpen(false); + setFeedback(null); + form.reset(defaultValues); + }; + + const handleCreate = () => { + setEditingUserId(null); + setFeedback(null); + form.reset(defaultValues); + setIsEditorOpen(true); + }; + + const handleEdit = (targetUser: UserItem) => { + setFeedback(null); + setEditingUserId(targetUser.id); + setIsEditorOpen(true); + }; const handleManage = (targetUser: UserItem, action: ManageUserAction) => { const actionText = { @@ -258,10 +258,10 @@ export function UsersPage() { }); if (!isAdmin) { - return ( -
- + -
- - 新建用户 - - } - /> - - {feedback ? : null} - - +
+ + 新建用户 + + } + /> + + {feedback ? : null} + + -
- - - 取消 - - - {saveMutation.isPending ? '保存中...' : editingUserId ? '保存修改' : '创建用户'} - -
- } - > - {editingUserId && editingUserQuery.isLoading ? ( - - ) : editingUserId && editingUserQuery.isError ? ( - - ) : ( -
- - - - - - - - - - - - - {editingUserId && editingUserQuery.data ? ( -
-
-

绑定邮箱

-

- {editingUserQuery.data.email || '未绑定'} -

-
-
-

第三方账号

-

- GitHub:{editingUserQuery.data.github_id || '未绑定'} -

-

- 微信:{editingUserQuery.data.wechat_id || '未绑定'} -

-
-
- ) : null} -
- )} - - - ); -} +
+ + + 取消 + + + {saveMutation.isPending ? '保存中...' : editingUserId ? '保存修改' : '创建用户'} + + + } + > + {editingUserId && editingUserQuery.isLoading ? ( + + ) : editingUserId && editingUserQuery.isError ? ( + + ) : ( +
+ + + + + + + + + + + + + {editingUserId && editingUserQuery.data ? ( +
+
+

绑定邮箱

+

+ {editingUserQuery.data.email || '未绑定'} +

+
+
+

第三方账号

+

+ GitHub:{editingUserQuery.data.github_id || '未绑定'} +

+

+ 微信:{editingUserQuery.data.wechat_id || '未绑定'} +

+
+
+ ) : null} +
+ )} +
+ + ); +} diff --git a/atsf_server/web/lib/constants/navigation.ts b/atsf_server/web/lib/constants/navigation.ts index afa8fa95..4f99d6fc 100644 --- a/atsf_server/web/lib/constants/navigation.ts +++ b/atsf_server/web/lib/constants/navigation.ts @@ -13,7 +13,7 @@ export const dashboardNavigation: NavigationItem[] = [ }, { href: '/proxy-route', - label: '反向代理', + label: '规则', icon: 'proxy', }, {