[重构] 前端改造v5

This commit is contained in:
ryan
2026-03-11 17:01:24 +08:00
parent 5d6898b633
commit e1a9cc738e
62 changed files with 212 additions and 5282 deletions
@@ -19,13 +19,13 @@ const readinessItems = [
},
];
export function DashboardOverview() {
return (
<div className='space-y-6'>
<AppCard
title='阶段 4 进行中'
description='当前已完成新版管理端基础工程、核心模块和大部分边缘模块迁移,可继续推进联调与回归。'
action={<StatusBadge label='准备联调回归' variant='success' />}
export function DashboardOverview() {
return (
<div className='space-y-6'>
<AppCard
title='前端迁移完成'
description='新版管理端已承接全部页面入口、旧路由兼容和核心交互,当前可继续推进联调与测试补齐。'
action={<StatusBadge label='统一 Next.js 前端' variant='success' />}
>
<div className='grid gap-4 lg:grid-cols-3'>
{readinessItems.map((item) => (
@@ -60,14 +60,14 @@ export function DashboardOverview() {
</div>
</AppCard>
<AppCard title='下一步建议' description='按前端改造计划,当前可以开始阶段 5 的联调、回归与默认入口切换准备。'>
<ol className='space-y-3 text-sm leading-6 text-[var(--foreground-secondary)]'>
<li>1. 对照旧前端完成设置、用户、文件与关于页的联调回归。</li>
<li>2. 补齐关键页面的测试覆盖与构建验收。</li>
<li>3. 评估默认入口切换与旧前端回滚预案。</li>
</ol>
</AppCard>
</div>
</div>
);
}
<AppCard title='下一步建议' description='当前重点转向联调回归、测试覆盖和发布验收。'>
<ol className='space-y-3 text-sm leading-6 text-[var(--foreground-secondary)]'>
<li>1. 对照后端接口完成设置、节点、证书和用户链路联调。</li>
<li>2. 补齐关键页面的测试覆盖与构建验收。</li>
<li>3. 清理迁移期文档和发布说明中的旧前端表述。</li>
</ol>
</AppCard>
</div>
</div>
);
}
@@ -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<LatestReleaseInfo | null>(null);
const [wechatCode, setWeChatCode] = useState('');
const [emailAddress, setEmailAddress] = useState('');
const [emailCode, setEmailCode] = useState('');
const [emailTurnstileToken, setEmailTurnstileToken] = useState('');
const [latestRelease, setLatestRelease] = useState<LatestReleaseInfo | null>(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}` : '未绑定'}
</p>
</div>
{publicStatus.turnstile_check ? (
<EmptyState
title='当前已启用 Turnstile'
description='邮箱绑定在旧版前端依赖人机校验组件。若需要绑定邮箱,请暂时使用旧前端作为兼容兜底。'
/>
) : (
<div className='space-y-4'>
<ResourceField label='邮箱地址'>
<ResourceInput
value={emailAddress}
onChange={(event) => setEmailAddress(event.target.value)}
placeholder='请输入邮箱地址'
/>
</ResourceField>
<ResourceField label='验证码'>
<ResourceInput
value={emailCode}
onChange={(event) => setEmailCode(event.target.value)}
placeholder='请输入邮箱验证码'
/>
</ResourceField>
<div className='flex flex-wrap gap-2'>
<SecondaryButton type='button' onClick={handleEmailVerification} disabled={busyKey === 'email-send'}>
{busyKey === 'email-send' ? '发送中...' : '发送验证码'}
</SecondaryButton>
<PrimaryButton type='button' onClick={handleBindEmail} disabled={busyKey === 'email-bind'}>
{busyKey === 'email-bind' ? '绑定中...' : '绑定邮箱'}
</PrimaryButton>
</div>
</div>
)}
</div>
<div className='space-y-4'>
<ResourceField label='邮箱地址'>
<ResourceInput
value={emailAddress}
onChange={(event) => setEmailAddress(event.target.value)}
placeholder='请输入邮箱地址'
/>
</ResourceField>
<ResourceField label='验证码'>
<ResourceInput
value={emailCode}
onChange={(event) => setEmailCode(event.target.value)}
placeholder='请输入邮箱验证码'
/>
</ResourceField>
{publicStatus.turnstile_check ? (
publicStatus.turnstile_site_key ? (
<TurnstileWidget
siteKey={publicStatus.turnstile_site_key}
onVerify={(token) => setEmailTurnstileToken(token)}
onExpire={() => setEmailTurnstileToken('')}
onError={() => setEmailTurnstileToken('')}
/>
) : (
<EmptyState
title='Turnstile 配置不完整'
description='当前系统已启用 Turnstile,但未配置 Site Key,邮箱绑定暂不可用。'
/>
)
) : null}
<div className='flex flex-wrap gap-2'>
<SecondaryButton type='button' onClick={handleEmailVerification} disabled={busyKey === 'email-send'}>
{busyKey === 'email-send' ? '发送中...' : '发送验证码'}
</SecondaryButton>
<PrimaryButton type='button' onClick={handleBindEmail} disabled={busyKey === 'email-bind'}>
{busyKey === 'email-bind' ? '绑定中...' : '绑定邮箱'}
</PrimaryButton>
</div>
</div>
</div>
</div>
</AppCard>
</div>
@@ -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<number | null>(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) => {