feat(frontend): 控制台 next-intl 中英双语

接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
This commit is contained in:
ryan
2026-08-16 16:49:16 +08:00
parent 64fbaa7ef1
commit 5a8722ff07
219 changed files with 21437 additions and 5212 deletions
@@ -1,6 +1,8 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import { useMemo } from 'react';
import { useForm, type UseFormReturn } from 'react-hook-form';
import { z } from 'zod';
@@ -9,46 +11,50 @@ import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { type PagesProject } from '@/lib/services/openflare';
export const pagesProjectSchema = z
.object({
name: z.string().trim().min(1, '请输入项目名称').max(255),
slug: z.string().trim().max(255).optional().or(z.literal('')),
spa_fallback_enabled: z.boolean(),
spa_fallback_path: z.string().trim(),
api_proxy_enabled: z.boolean(),
api_proxy_path: z.string().trim(),
api_proxy_pass: z.string().trim(),
api_proxy_rewrite: z.string().trim(),
root_dir: z.string().trim().max(512).optional().or(z.literal('')),
entry_file: z.string().trim().min(1, '请输入入口文件').max(512),
})
.superRefine((data, ctx) => {
if (data.spa_fallback_enabled && !data.spa_fallback_path.startsWith('/')) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['spa_fallback_path'],
message: '回退路径必须以 / 开头',
});
}
if (data.api_proxy_enabled) {
if (!data.api_proxy_path.startsWith('/')) {
function createPagesProjectSchema(t: (key: string) => string) {
return z
.object({
name: z.string().trim().min(1, t('form.nameRequired')).max(255),
slug: z.string().trim().max(255).optional().or(z.literal('')),
spa_fallback_enabled: z.boolean(),
spa_fallback_path: z.string().trim(),
api_proxy_enabled: z.boolean(),
api_proxy_path: z.string().trim(),
api_proxy_pass: z.string().trim(),
api_proxy_rewrite: z.string().trim(),
root_dir: z.string().trim().max(512).optional().or(z.literal('')),
entry_file: z.string().trim().min(1, t('form.entryRequired')).max(512),
})
.superRefine((data, ctx) => {
if (data.spa_fallback_enabled && !data.spa_fallback_path.startsWith('/')) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['api_proxy_path'],
message: '匹配路径必须以 / 开头',
path: ['spa_fallback_path'],
message: t('form.fallbackPathPrefix'),
});
}
if (!/^https?:\/\//i.test(data.api_proxy_pass)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['api_proxy_pass'],
message: '后端地址必须以 http:// 或 https:// 开头',
});
if (data.api_proxy_enabled) {
if (!data.api_proxy_path.startsWith('/')) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['api_proxy_path'],
message: t('form.matchPathPrefix'),
});
}
if (!/^https?:\/\//i.test(data.api_proxy_pass)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['api_proxy_pass'],
message: t('form.backendPrefix'),
});
}
}
}
});
});
}
export type PagesProjectFormValues = z.infer<typeof pagesProjectSchema>;
export type PagesProjectFormValues = z.infer<
ReturnType<typeof createPagesProjectSchema>
>;
export function toFormValues(
project?: PagesProject | null,
@@ -110,8 +116,10 @@ export function buildProjectPayload(
}
export function usePagesProjectForm(project?: PagesProject | null) {
const t = useTranslations('pages');
const schema = useMemo(() => createPagesProjectSchema((key) => t(key)), [t]);
return useForm<PagesProjectFormValues>({
resolver: zodResolver(pagesProjectSchema),
resolver: zodResolver(schema),
defaultValues: toFormValues(project),
});
}
@@ -125,6 +133,7 @@ export function ProjectFormFields({
form,
idPrefix = '',
}: ProjectFormFieldsProps) {
const t = useTranslations('pages');
const spaEnabled = form.watch('spa_fallback_enabled');
const apiEnabled = form.watch('api_proxy_enabled');
const fieldId = (name: string) => (idPrefix ? `${idPrefix}-${name}` : name);
@@ -133,7 +142,7 @@ export function ProjectFormFields({
<div className='space-y-4'>
<div className='grid gap-4 md:grid-cols-2'>
<div className='space-y-1.5'>
<Label htmlFor={fieldId('name')}>项目名称</Label>
<Label htmlFor={fieldId('name')}>{t('form.name')}</Label>
<Input id={fieldId('name')} {...form.register('name')} />
{form.formState.errors.name ? (
<p className='text-xs text-destructive'>
@@ -142,10 +151,10 @@ export function ProjectFormFields({
) : null}
</div>
<div className='space-y-1.5'>
<Label htmlFor={fieldId('slug')}>项目标识</Label>
<Label htmlFor={fieldId('slug')}>{t('form.slug')}</Label>
<Input
id={fieldId('slug')}
placeholder='留空自动生成'
placeholder={t('form.slugPlaceholder')}
{...form.register('slug')}
/>
</div>
@@ -153,14 +162,14 @@ export function ProjectFormFields({
<div className='grid gap-4 md:grid-cols-2'>
<div className='space-y-1.5'>
<Label htmlFor={fieldId('entry_file')}>入口文件</Label>
<Label htmlFor={fieldId('entry_file')}>{t('form.entryFile')}</Label>
<Input id={fieldId('entry_file')} {...form.register('entry_file')} />
</div>
<div className='space-y-1.5'>
<Label htmlFor={fieldId('root_dir')}>根目录</Label>
<Label htmlFor={fieldId('root_dir')}>{t('form.rootDir')}</Label>
<Input
id={fieldId('root_dir')}
placeholder='可选'
placeholder={t('form.optional')}
{...form.register('root_dir')}
/>
</div>
@@ -168,9 +177,9 @@ export function ProjectFormFields({
<div className='flex items-center justify-between rounded-lg border border-dashed px-4 py-3'>
<div>
<p className='text-sm font-medium'>SPA fallback</p>
<p className='text-sm font-medium'>{t('form.spaFallback')}</p>
<p className='text-xs text-muted-foreground'>
未命中静态文件时回退到指定路径
{t('form.spaFallbackDesc')}
</p>
</div>
<Switch
@@ -184,7 +193,9 @@ export function ProjectFormFields({
</div>
{spaEnabled ? (
<div className='space-y-1.5'>
<Label htmlFor={fieldId('spa_fallback_path')}>回退路径</Label>
<Label htmlFor={fieldId('spa_fallback_path')}>
{t('form.fallbackPath')}
</Label>
<Input
id={fieldId('spa_fallback_path')}
{...form.register('spa_fallback_path')}
@@ -194,9 +205,9 @@ export function ProjectFormFields({
<div className='flex items-center justify-between rounded-lg border border-dashed px-4 py-3'>
<div>
<p className='text-sm font-medium'>API 反向代理</p>
<p className='text-sm font-medium'>{t('form.apiProxy')}</p>
<p className='text-xs text-muted-foreground'>
为静态站点附加 API 反代规则
{t('form.apiProxyDesc')}
</p>
</div>
<Switch
@@ -209,21 +220,27 @@ export function ProjectFormFields({
{apiEnabled ? (
<div className='grid gap-4 md:grid-cols-2'>
<div className='space-y-1.5'>
<Label htmlFor={fieldId('api_proxy_path')}>匹配路径</Label>
<Label htmlFor={fieldId('api_proxy_path')}>
{t('form.matchPath')}
</Label>
<Input
id={fieldId('api_proxy_path')}
{...form.register('api_proxy_path')}
/>
</div>
<div className='space-y-1.5'>
<Label htmlFor={fieldId('api_proxy_pass')}>后端地址</Label>
<Label htmlFor={fieldId('api_proxy_pass')}>
{t('form.backend')}
</Label>
<Input
id={fieldId('api_proxy_pass')}
{...form.register('api_proxy_pass')}
/>
</div>
<div className='space-y-1.5 md:col-span-2'>
<Label htmlFor={fieldId('api_proxy_rewrite')}>重写规则</Label>
<Label htmlFor={fieldId('api_proxy_rewrite')}>
{t('form.rewrite')}
</Label>
<Input
id={fieldId('api_proxy_rewrite')}
{...form.register('api_proxy_rewrite')}