mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +08:00
[优化] 界面优化
This commit is contained in:
@@ -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<FeedbackState | null>(null);
|
||||
const [editingRouteId, setEditingRouteId] = useState<number | null>(null);
|
||||
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
||||
const [matchResult, setMatchResult] = useState<ManagedDomainMatchResult | null>(null);
|
||||
const [isMatching, setIsMatching] = useState(false);
|
||||
export function ProxyRoutesPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [feedback, setFeedback] = useState<FeedbackState | null>(null);
|
||||
const [editingRouteId, setEditingRouteId] = useState<number | null>(null);
|
||||
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
||||
const [matchResult, setMatchResult] = useState<ManagedDomainMatchResult | null>(null);
|
||||
const [isMatching, setIsMatching] = useState(false);
|
||||
|
||||
const form = useForm<ProxyRouteFormValues>({
|
||||
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 (
|
||||
<>
|
||||
<div className='space-y-6'>
|
||||
<PageHeader
|
||||
title='反代规则'
|
||||
description='维护域名到源站的映射、HTTPS 证书绑定与自定义请求头,并可直接触发配置发布。'
|
||||
action={
|
||||
<>
|
||||
<PrimaryButton type='button' onClick={() => publishMutation.mutate()} disabled={publishMutation.isPending}>
|
||||
{publishMutation.isPending ? '发布中...' : '发布当前规则'}
|
||||
</PrimaryButton>
|
||||
<SecondaryButton type='button' onClick={handleCreate}>
|
||||
新增规则
|
||||
</SecondaryButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{feedback ? <InlineMessage tone={feedback.tone} message={feedback.message} /> : null}
|
||||
|
||||
<AppCard title='规则列表' description='支持查看启用状态、HTTPS 配置、自定义请求头数量及最近更新时间。'>
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-6'>
|
||||
<PageHeader
|
||||
title='反代规则'
|
||||
description='维护域名到源站的映射、HTTPS 证书绑定与自定义请求头,并可直接触发配置发布。'
|
||||
action={
|
||||
<>
|
||||
<PrimaryButton type='button' onClick={() => publishMutation.mutate()} disabled={publishMutation.isPending}>
|
||||
{publishMutation.isPending ? '发布中...' : '发布当前规则'}
|
||||
</PrimaryButton>
|
||||
<SecondaryButton type='button' onClick={handleCreate}>
|
||||
新增规则
|
||||
</SecondaryButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{feedback ? <InlineMessage tone={feedback.tone} message={feedback.message} /> : null}
|
||||
|
||||
<AppCard title='规则列表'>
|
||||
{routesQuery.isLoading ? (
|
||||
<LoadingState />
|
||||
) : routesQuery.isError ? (
|
||||
@@ -494,184 +494,184 @@ export function ProxyRoutesPage() {
|
||||
</div>
|
||||
)}
|
||||
</AppCard>
|
||||
</div>
|
||||
<AppModal
|
||||
isOpen={isEditorOpen}
|
||||
onClose={handleReset}
|
||||
title={editingRouteId ? '编辑规则' : '新增规则'}
|
||||
description='新增或修改反代规则后,可直接回到列表页继续发布。'
|
||||
size='xl'
|
||||
footer={
|
||||
<div className='flex flex-wrap justify-end gap-3'>
|
||||
<SecondaryButton type='button' onClick={handleReset} disabled={saveMutation.isPending}>
|
||||
取消
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type='submit' form='proxy-route-editor-form' disabled={saveMutation.isPending}>
|
||||
{saveMutation.isPending ? '保存中...' : editingRouteId ? '保存修改' : '新增规则'}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form id='proxy-route-editor-form' className='space-y-5' onSubmit={handleSubmit}>
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<ResourceField
|
||||
label='域名'
|
||||
hint='示例:example.com'
|
||||
error={form.formState.errors.domain?.message}
|
||||
>
|
||||
<ResourceInput placeholder='example.com' {...form.register('domain')} />
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label='源站地址'
|
||||
hint='示例:https://origin.internal'
|
||||
error={form.formState.errors.origin_url?.message}
|
||||
>
|
||||
<ResourceInput placeholder='https://origin.internal' {...form.register('origin_url')} />
|
||||
</ResourceField>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 lg:grid-cols-2'>
|
||||
<ToggleField
|
||||
label='启用规则'
|
||||
description='关闭后该规则不会参与配置渲染与发布。'
|
||||
checked={watchedEnabled}
|
||||
onChange={(checked) => form.setValue('enabled', checked, { shouldDirty: true })}
|
||||
/>
|
||||
<ToggleField
|
||||
label='启用 HTTPS'
|
||||
description='启用后必须关联 TLS 证书,可选择是否将 HTTP 自动重定向到 HTTPS。'
|
||||
checked={watchedEnableHttps}
|
||||
onChange={(checked) => {
|
||||
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 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 lg:grid-cols-[1.2fr_0.8fr]'>
|
||||
<ResourceField
|
||||
label='TLS 证书'
|
||||
hint='启用 HTTPS 后可自动推荐匹配证书,也支持手动选择。'
|
||||
error={form.formState.errors.cert_id?.message}
|
||||
>
|
||||
<ResourceSelect
|
||||
value={watchedCertId}
|
||||
disabled={!watchedEnableHttps || certificatesQuery.isLoading}
|
||||
onChange={(event) =>
|
||||
form.setValue('cert_id', event.target.value, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value=''>请选择证书</option>
|
||||
{certificates.map((certificate) => (
|
||||
<option key={certificate.id} value={certificate.id}>
|
||||
{buildCertificateLabel(certificate)}
|
||||
</option>
|
||||
))}
|
||||
</ResourceSelect>
|
||||
</ResourceField>
|
||||
<ToggleField
|
||||
label='HTTP 跳转 HTTPS'
|
||||
description='仅在启用 HTTPS 后可开启。开启后会将 HTTP 请求重定向到 HTTPS。'
|
||||
checked={watchedRedirectHttp}
|
||||
disabled={!watchedEnableHttps}
|
||||
onChange={(checked) =>
|
||||
form.setValue('redirect_http', checked, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||||
<div>
|
||||
<p className='text-sm font-semibold text-[var(--foreground-primary)]'>自定义请求头</p>
|
||||
<p className='mt-1 text-xs leading-5 text-[var(--foreground-secondary)]'>
|
||||
可为空。若填写,请保证 Header 名称合法且不包含换行。
|
||||
</p>
|
||||
</div>
|
||||
<SecondaryButton
|
||||
type='button'
|
||||
onClick={() => append({ key: '', value: '' })}
|
||||
className='px-3 py-2 text-xs'
|
||||
>
|
||||
添加请求头
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 space-y-4'>
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className='grid gap-3 md:grid-cols-[1fr_1fr_auto]'>
|
||||
<ResourceField
|
||||
label={index === 0 ? 'Header 名称' : `Header 名称 ${index + 1}`}
|
||||
error={form.formState.errors.custom_headers?.[index]?.key?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
placeholder='X-Trace-Id'
|
||||
{...form.register(`custom_headers.${index}.key`)}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label={index === 0 ? 'Header 值' : `Header 值 ${index + 1}`}
|
||||
error={form.formState.errors.custom_headers?.[index]?.value?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
placeholder='$request_id'
|
||||
{...form.register(`custom_headers.${index}.value`)}
|
||||
/>
|
||||
</ResourceField>
|
||||
<div className='flex items-end'>
|
||||
<DangerButton
|
||||
type='button'
|
||||
onClick={() => handleRemoveHeader(index)}
|
||||
className='w-full px-3 py-3 text-xs md:w-auto'
|
||||
>
|
||||
删除
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppCard title='证书匹配提示' description='根据域名自动匹配托管证书,优先使用精确匹配规则。'>
|
||||
<div className='space-y-3'>
|
||||
<p className='text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
{getMatchMessage(matchResult, isMatching, watchedDomain, watchedEnableHttps)}
|
||||
</p>
|
||||
{matchResult?.matched && matchResult.candidates.length > 1 ? (
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{matchResult.candidates.map((candidate) => (
|
||||
<StatusBadge
|
||||
key={`${candidate.managed_domain_id}-${candidate.certificate_id}`}
|
||||
label={`${candidate.domain} → ${candidate.certificate_name}`}
|
||||
variant={candidate.match_type === 'exact' ? 'success' : 'info'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className='rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4 text-sm text-[var(--foreground-secondary)]'>
|
||||
当前可选证书:{certificatesQuery.isLoading ? '加载中...' : `${certificates.length} 张`}
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<ResourceField
|
||||
label='备注'
|
||||
hint='可选,便于标记用途、责任人或特殊说明。'
|
||||
error={form.formState.errors.remark?.message}
|
||||
>
|
||||
<ResourceTextarea placeholder='例如:主站生产流量入口' {...form.register('remark')} />
|
||||
</ResourceField>
|
||||
</form>
|
||||
</AppModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
<AppModal
|
||||
isOpen={isEditorOpen}
|
||||
onClose={handleReset}
|
||||
title={editingRouteId ? '编辑规则' : '新增规则'}
|
||||
description='新增或修改反代规则后,可直接回到列表页继续发布。'
|
||||
size='xl'
|
||||
footer={
|
||||
<div className='flex flex-wrap justify-end gap-3'>
|
||||
<SecondaryButton type='button' onClick={handleReset} disabled={saveMutation.isPending}>
|
||||
取消
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type='submit' form='proxy-route-editor-form' disabled={saveMutation.isPending}>
|
||||
{saveMutation.isPending ? '保存中...' : editingRouteId ? '保存修改' : '新增规则'}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form id='proxy-route-editor-form' className='space-y-5' onSubmit={handleSubmit}>
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<ResourceField
|
||||
label='域名'
|
||||
hint='示例:example.com'
|
||||
error={form.formState.errors.domain?.message}
|
||||
>
|
||||
<ResourceInput placeholder='example.com' {...form.register('domain')} />
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label='源站地址'
|
||||
hint='示例:https://origin.internal'
|
||||
error={form.formState.errors.origin_url?.message}
|
||||
>
|
||||
<ResourceInput placeholder='https://origin.internal' {...form.register('origin_url')} />
|
||||
</ResourceField>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 lg:grid-cols-2'>
|
||||
<ToggleField
|
||||
label='启用规则'
|
||||
description='关闭后该规则不会参与配置渲染与发布。'
|
||||
checked={watchedEnabled}
|
||||
onChange={(checked) => form.setValue('enabled', checked, { shouldDirty: true })}
|
||||
/>
|
||||
<ToggleField
|
||||
label='启用 HTTPS'
|
||||
description='启用后必须关联 TLS 证书,可选择是否将 HTTP 自动重定向到 HTTPS。'
|
||||
checked={watchedEnableHttps}
|
||||
onChange={(checked) => {
|
||||
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 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 lg:grid-cols-[1.2fr_0.8fr]'>
|
||||
<ResourceField
|
||||
label='TLS 证书'
|
||||
hint='启用 HTTPS 后可自动推荐匹配证书,也支持手动选择。'
|
||||
error={form.formState.errors.cert_id?.message}
|
||||
>
|
||||
<ResourceSelect
|
||||
value={watchedCertId}
|
||||
disabled={!watchedEnableHttps || certificatesQuery.isLoading}
|
||||
onChange={(event) =>
|
||||
form.setValue('cert_id', event.target.value, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value=''>请选择证书</option>
|
||||
{certificates.map((certificate) => (
|
||||
<option key={certificate.id} value={certificate.id}>
|
||||
{buildCertificateLabel(certificate)}
|
||||
</option>
|
||||
))}
|
||||
</ResourceSelect>
|
||||
</ResourceField>
|
||||
<ToggleField
|
||||
label='HTTP 跳转 HTTPS'
|
||||
description='仅在启用 HTTPS 后可开启。开启后会将 HTTP 请求重定向到 HTTPS。'
|
||||
checked={watchedRedirectHttp}
|
||||
disabled={!watchedEnableHttps}
|
||||
onChange={(checked) =>
|
||||
form.setValue('redirect_http', checked, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||||
<div>
|
||||
<p className='text-sm font-semibold text-[var(--foreground-primary)]'>自定义请求头</p>
|
||||
<p className='mt-1 text-xs leading-5 text-[var(--foreground-secondary)]'>
|
||||
可为空。若填写,请保证 Header 名称合法且不包含换行。
|
||||
</p>
|
||||
</div>
|
||||
<SecondaryButton
|
||||
type='button'
|
||||
onClick={() => append({ key: '', value: '' })}
|
||||
className='px-3 py-2 text-xs'
|
||||
>
|
||||
添加请求头
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 space-y-4'>
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className='grid gap-3 md:grid-cols-[1fr_1fr_auto]'>
|
||||
<ResourceField
|
||||
label={index === 0 ? 'Header 名称' : `Header 名称 ${index + 1}`}
|
||||
error={form.formState.errors.custom_headers?.[index]?.key?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
placeholder='X-Trace-Id'
|
||||
{...form.register(`custom_headers.${index}.key`)}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label={index === 0 ? 'Header 值' : `Header 值 ${index + 1}`}
|
||||
error={form.formState.errors.custom_headers?.[index]?.value?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
placeholder='$request_id'
|
||||
{...form.register(`custom_headers.${index}.value`)}
|
||||
/>
|
||||
</ResourceField>
|
||||
<div className='flex items-end'>
|
||||
<DangerButton
|
||||
type='button'
|
||||
onClick={() => handleRemoveHeader(index)}
|
||||
className='w-full px-3 py-3 text-xs md:w-auto'
|
||||
>
|
||||
删除
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppCard title='证书匹配提示' description='根据域名自动匹配托管证书,优先使用精确匹配规则。'>
|
||||
<div className='space-y-3'>
|
||||
<p className='text-sm leading-6 text-[var(--foreground-secondary)]'>
|
||||
{getMatchMessage(matchResult, isMatching, watchedDomain, watchedEnableHttps)}
|
||||
</p>
|
||||
{matchResult?.matched && matchResult.candidates.length > 1 ? (
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{matchResult.candidates.map((candidate) => (
|
||||
<StatusBadge
|
||||
key={`${candidate.managed_domain_id}-${candidate.certificate_id}`}
|
||||
label={`${candidate.domain} → ${candidate.certificate_name}`}
|
||||
variant={candidate.match_type === 'exact' ? 'success' : 'info'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className='rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4 text-sm text-[var(--foreground-secondary)]'>
|
||||
当前可选证书:{certificatesQuery.isLoading ? '加载中...' : `${certificates.length} 张`}
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<ResourceField
|
||||
label='备注'
|
||||
hint='可选,便于标记用途、责任人或特殊说明。'
|
||||
error={form.formState.errors.remark?.message}
|
||||
>
|
||||
<ResourceTextarea placeholder='例如:主站生产流量入口' {...form.register('remark')} />
|
||||
</ResourceField>
|
||||
</form>
|
||||
</AppModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user