mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
[重构] 前端改造v5
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user