feat(protocol): Add template preview functionality with localization support

This commit is contained in:
web
2025-08-14 09:55:13 -07:00
committed by speakeloudest
parent e4979a5de8
commit d17d77353f
38 changed files with 522 additions and 92 deletions
@@ -56,8 +56,8 @@ import { useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
import { TemplatePreview } from './template-preview';
// 表单验证规则 - 基于 API.CreateSubscribeApplicationRequest
const createClientFormSchema = (t: any) =>
z.object({
name: z.string().min(1, t('form.validation.nameRequired')),
@@ -83,6 +83,8 @@ export function ProtocolForm() {
const t = useTranslations('subscribe');
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewApplicationId, setPreviewApplicationId] = useState<number | null>(null);
const [editingClient, setEditingClient] = useState<API.SubscribeApplication | null>(null);
const tableRef = useRef<ProTableActions>(null);
@@ -136,6 +138,7 @@ export function ProtocolForm() {
onCheckedChange={async (checked) => {
await updateSubscribeApplication({
...row.original,
proxy_template: '',
is_default: checked,
});
tableRef.current?.refresh();
@@ -294,7 +297,6 @@ export function ProtocolForm() {
} else {
await createSubscribeApplication({
...data,
proxy_template: '',
is_default: false,
});
toast.success(t('actions.createSuccess'));
@@ -310,6 +312,11 @@ export function ProtocolForm() {
}
};
const handlePreview = (application: API.SubscribeApplication) => {
setPreviewApplicationId(application.id);
setPreviewOpen(true);
};
return (
<>
<ProTable<API.SubscribeApplication, Record<string, unknown>>
@@ -322,6 +329,11 @@ export function ProtocolForm() {
}}
actions={{
render: (row) => [
<TemplatePreview
key='preview'
applicationId={row.id}
output_format={row.output_format}
/>,
<Button
key='edit'
onClick={() => handleEdit(row as unknown as API.SubscribeApplication)}
@@ -606,7 +618,7 @@ export function ProtocolForm() {
{t('form.descriptions.template.subscribeName')}
</li>
<li>
<code className='rounded px-1'>.Nodes</code> -{' '}
<code className='rounded px-1'>.Proxies</code> -{' '}
{t('form.descriptions.template.nodes')}
</li>
<li>
@@ -623,7 +635,7 @@ export function ProtocolForm() {
<ul className='ml-2 list-disc space-y-1 text-xs'>
<li>
<code className='rounded px-1'>
{'{{range .Nodes}}...{{end}}'}
{'{{range .Proxies}}...{{end}}'}
</code>{' '}
- {t('form.descriptions.template.range')}
</li>
@@ -646,10 +658,11 @@ export function ProtocolForm() {
</FormLabel>
<FormControl>
<GoTemplateEditor
showLineNumbers
schema={{
SiteName: { type: 'string', description: 'Site name' },
SubscribeName: { type: 'string', description: 'Subscribe name' },
Nodes: {
Proxies: {
type: 'array',
description: 'Array of proxy nodes',
items: {
@@ -0,0 +1,89 @@
'use client';
import { previewSubscribeTemplate } from '@/services/admin/application';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@workspace/ui/components/sheet';
import { Icon } from '@workspace/ui/custom-components/icon';
import { Markdown } from '@workspace/ui/custom-components/markdown';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
interface TemplatePreviewProps {
applicationId: number;
output_format?: string;
}
export function TemplatePreview({ applicationId, output_format }: TemplatePreviewProps) {
const t = useTranslations('subscribe.templatePreview');
const [isOpen, setIsOpen] = useState(false);
const { data, isLoading, error } = useQuery({
queryKey: ['previewSubscribeTemplate', applicationId],
queryFn: () => previewSubscribeTemplate({ id: applicationId }, { skipErrorHandler: true }),
enabled: isOpen && !!applicationId,
retry: false,
});
const originalContent = data?.data?.data?.template || '';
const errorMessage = (error as any)?.data?.msg || error?.message || t('failed');
const displayContent = originalContent || (error ? errorMessage : '');
const getDecodedContent = () => {
if (output_format === 'base64' && originalContent) {
try {
return atob(originalContent);
} catch {
return t('base64.decodeError');
}
}
return '';
};
const getDisplayContent = () => {
switch (output_format) {
case 'base64':
return `\`\`\`base64\n# ${t('base64.originalContent')}\n${displayContent}\n\n# ${t('base64.decodedContent')}\n${getDecodedContent()}\n\`\`\``;
case 'yaml':
return `\`\`\`yaml\n${displayContent}\n\`\`\``;
case 'json':
return `\`\`\`json\n${displayContent}\n\`\`\``;
case 'conf':
return `\`\`\`ini\n${displayContent}\n\`\`\``;
case 'plain':
return `\`\`\`text\n${displayContent}\n\`\`\``;
default:
return displayContent;
}
};
const handleOpenChange = (newOpen: boolean) => {
setIsOpen(newOpen);
};
return (
<>
<Button variant='ghost' size='sm' onClick={() => setIsOpen(true)}>
<Icon icon='mdi:eye' className='mr-2 h-4 w-4' />
{t('preview')}
</Button>
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
<SheetContent className='w-[800px] max-w-[90vw] md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('title')}</SheetTitle>
</SheetHeader>
{isLoading ? (
<div className='flex items-center justify-center'>
<Icon icon='mdi:loading' className='h-6 w-6 animate-spin' />
<span className='ml-2'>{t('loading')}</span>
</div>
) : (
<div className='*:text-sm [&_pre>div>div+div]:max-h-[calc(100dvh-48px-36px-36px)] [&_pre>div>div+div]:overflow-auto'>
<Markdown>{getDisplayContent()}</Markdown>
</div>
)}
</SheetContent>
</Sheet>
</>
);
}