mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-06 03:30:25 -05:00
✨ feat(protocol): Add template preview functionality with localization support
This commit is contained in:
parent
f190c68eb0
commit
0448d213a3
@ -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: {
|
||||
|
||||
89
apps/admin/app/dashboard/subscribe/template-preview.tsx
Normal file
89
apps/admin/app/dashboard/subscribe/template-preview.tsx
Normal file
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Formát výstupu",
|
||||
"supportedPlatforms": "Podporované platformy"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Dekódování selhalo: Obsah není platný formát Base64",
|
||||
"decodedContent": "Dekódovaný obsah",
|
||||
"originalContent": "Původní obsah (Base64)"
|
||||
},
|
||||
"failed": "Náhled selhal",
|
||||
"loading": "Načítání...",
|
||||
"preview": "Náhled",
|
||||
"title": "Náhled šablony"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Ausgabeformat",
|
||||
"supportedPlatforms": "Unterstützte Plattformen"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Dekodierung fehlgeschlagen: Inhalt ist kein gültiges Base64-Format",
|
||||
"decodedContent": "Dekodierter Inhalt",
|
||||
"originalContent": "Ursprünglicher Inhalt (Base64)"
|
||||
},
|
||||
"failed": "Vorschau fehlgeschlagen",
|
||||
"loading": "Wird geladen...",
|
||||
"preview": "Vorschau",
|
||||
"title": "Vorlagenvorschau"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Output Format",
|
||||
"supportedPlatforms": "Supported Platforms"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"title": "Template Preview",
|
||||
"preview": "Preview",
|
||||
"loading": "Loading...",
|
||||
"failed": "Preview failed",
|
||||
"base64": {
|
||||
"originalContent": "Original Content (Base64)",
|
||||
"decodedContent": "Decoded Content",
|
||||
"decodeError": "Decode failed: Content is not valid Base64 format"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Formato de Salida",
|
||||
"supportedPlatforms": "Plataformas Soportadas"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Error de decodificación: El contenido no es un formato Base64 válido",
|
||||
"decodedContent": "Contenido decodificado",
|
||||
"originalContent": "Contenido original (Base64)"
|
||||
},
|
||||
"failed": "La vista previa ha fallado",
|
||||
"loading": "Cargando...",
|
||||
"preview": "Vista previa",
|
||||
"title": "Vista previa de la plantilla"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Formato de Salida",
|
||||
"supportedPlatforms": "Plataformas Soportadas"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Error de decodificación: El contenido no es un formato Base64 válido",
|
||||
"decodedContent": "Contenido decodificado",
|
||||
"originalContent": "Contenido original (Base64)"
|
||||
},
|
||||
"failed": "La vista previa falló",
|
||||
"loading": "Cargando...",
|
||||
"preview": "Vista previa",
|
||||
"title": "Vista previa de la plantilla"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "فرمت خروجی",
|
||||
"supportedPlatforms": "پلتفرمهای پشتیبانی شده"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "خطا در رمزگشایی: محتوا فرمت Base64 معتبر نیست",
|
||||
"decodedContent": "محتوای رمزگشایی شده",
|
||||
"originalContent": "محتوای اصلی (Base64)"
|
||||
},
|
||||
"failed": "پیشنمایش ناموفق بود",
|
||||
"loading": "در حال بارگذاری...",
|
||||
"preview": "پیشنمایش",
|
||||
"title": "پیشنمایش الگو"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Tulostusmuoto",
|
||||
"supportedPlatforms": "Tuetut alustat"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Dekoodaus epäonnistui: Sisältö ei ole voimassa olevaa Base64-muotoa",
|
||||
"decodedContent": "Dekoodattu sisältö",
|
||||
"originalContent": "Alkuperäinen sisältö (Base64)"
|
||||
},
|
||||
"failed": "Esikatselu epäonnistui",
|
||||
"loading": "Ladataan...",
|
||||
"preview": "Esikatselu",
|
||||
"title": "Mallin esikatselu"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Format de sortie",
|
||||
"supportedPlatforms": "Plateformes prises en charge"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Échec du décodage : le contenu n'est pas au format Base64 valide",
|
||||
"decodedContent": "Contenu décodé",
|
||||
"originalContent": "Contenu original (Base64)"
|
||||
},
|
||||
"failed": "Échec de l'aperçu",
|
||||
"loading": "Chargement...",
|
||||
"preview": "Aperçu",
|
||||
"title": "Aperçu du modèle"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "आउटपुट प्रारूप",
|
||||
"supportedPlatforms": "समर्थित प्लेटफ़ॉर्म"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "डिकोड विफल: सामग्री मान्य Base64 प्रारूप नहीं है",
|
||||
"decodedContent": "डिकोड की गई सामग्री",
|
||||
"originalContent": "मूल सामग्री (Base64)"
|
||||
},
|
||||
"failed": "पूर्वावलोकन विफल",
|
||||
"loading": "लोड हो रहा है...",
|
||||
"preview": "पूर्वावलोकन",
|
||||
"title": "टेम्पलेट पूर्वावलोकन"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Kimeneti Formátum",
|
||||
"supportedPlatforms": "Támogatott Platformok"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Dekódolás sikertelen: A tartalom nem érvényes Base64 formátum",
|
||||
"decodedContent": "Dekódolt Tartalom",
|
||||
"originalContent": "Eredeti Tartalom (Base64)"
|
||||
},
|
||||
"failed": "Az előnézet nem sikerült",
|
||||
"loading": "Betöltés...",
|
||||
"preview": "Előnézet",
|
||||
"title": "Sablon Előnézet"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "出力形式",
|
||||
"supportedPlatforms": "サポートされているプラットフォーム"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "デコードに失敗しました:コンテンツが有効なBase64形式ではありません",
|
||||
"decodedContent": "デコードされたコンテンツ",
|
||||
"originalContent": "オリジナルコンテンツ(Base64)"
|
||||
},
|
||||
"failed": "プレビューに失敗しました",
|
||||
"loading": "読み込み中...",
|
||||
"preview": "プレビュー",
|
||||
"title": "テンプレートプレビュー"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "출력 형식",
|
||||
"supportedPlatforms": "지원되는 플랫폼"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "디코드 실패: 콘텐츠가 유효한 Base64 형식이 아닙니다",
|
||||
"decodedContent": "디코딩된 콘텐츠",
|
||||
"originalContent": "원본 콘텐츠 (Base64)"
|
||||
},
|
||||
"failed": "미리보기가 실패했습니다",
|
||||
"loading": "로딩 중...",
|
||||
"preview": "미리보기",
|
||||
"title": "템플릿 미리보기"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Utdataformat",
|
||||
"supportedPlatforms": "Støttede plattformer"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Dekoding feilet: Innholdet er ikke i gyldig Base64-format",
|
||||
"decodedContent": "Dekodet innhold",
|
||||
"originalContent": "Opprinnelig innhold (Base64)"
|
||||
},
|
||||
"failed": "Forhåndsvisning feilet",
|
||||
"loading": "Laster...",
|
||||
"preview": "Forhåndsvisning",
|
||||
"title": "Malemal"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Format wyjściowy",
|
||||
"supportedPlatforms": "Obsługiwane platformy"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Dekodowanie nie powiodło się: Zawartość nie jest w prawidłowym formacie Base64",
|
||||
"decodedContent": "Zdekodowana zawartość",
|
||||
"originalContent": "Oryginalna zawartość (Base64)"
|
||||
},
|
||||
"failed": "Nie udało się załadować podglądu",
|
||||
"loading": "Ładowanie...",
|
||||
"preview": "Podgląd",
|
||||
"title": "Podgląd szablonu"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Formato de Saída",
|
||||
"supportedPlatforms": "Plataformas Suportadas"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Falha na decodificação: O conteúdo não é um formato Base64 válido",
|
||||
"decodedContent": "Conteúdo Decodificado",
|
||||
"originalContent": "Conteúdo Original (Base64)"
|
||||
},
|
||||
"failed": "Falha na prévia",
|
||||
"loading": "Carregando...",
|
||||
"preview": "Prévia",
|
||||
"title": "Prévia do Modelo"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Format de Ieșire",
|
||||
"supportedPlatforms": "Platforme Suportate"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Decodarea a eșuat: Conținutul nu este un format Base64 valid",
|
||||
"decodedContent": "Conținut Decodat",
|
||||
"originalContent": "Conținut Original (Base64)"
|
||||
},
|
||||
"failed": "Previzualizarea a eșuat",
|
||||
"loading": "Se încarcă...",
|
||||
"preview": "Previzualizare",
|
||||
"title": "Previzualizare Șablon"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Формат вывода",
|
||||
"supportedPlatforms": "Поддерживаемые платформы"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Ошибка декодирования: Содержимое не является допустимым форматом Base64",
|
||||
"decodedContent": "Декодированное содержимое",
|
||||
"originalContent": "Исходное содержимое (Base64)"
|
||||
},
|
||||
"failed": "Не удалось загрузить предварительный просмотр",
|
||||
"loading": "Загрузка...",
|
||||
"preview": "Предварительный просмотр",
|
||||
"title": "Предварительный просмотр шаблона"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "รูปแบบผลลัพธ์",
|
||||
"supportedPlatforms": "แพลตฟอร์มที่รองรับ"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "การถอดรหัสล้มเหลว: เนื้อหาไม่ใช่รูปแบบ Base64 ที่ถูกต้อง",
|
||||
"decodedContent": "เนื้อหาที่ถอดรหัสแล้ว",
|
||||
"originalContent": "เนื้อหาต้นฉบับ (Base64)"
|
||||
},
|
||||
"failed": "การดูตัวอย่างล้มเหลว",
|
||||
"loading": "กำลังโหลด...",
|
||||
"preview": "ดูตัวอย่าง",
|
||||
"title": "ตัวอย่างแม่แบบ"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Çıktı Formatı",
|
||||
"supportedPlatforms": "Desteklenen Platformlar"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Çözme başarısız oldu: İçerik geçerli bir Base64 formatında değil",
|
||||
"decodedContent": "Çözülmüş İçerik",
|
||||
"originalContent": "Orijinal İçerik (Base64)"
|
||||
},
|
||||
"failed": "Önizleme başarısız oldu",
|
||||
"loading": "Yükleniyor...",
|
||||
"preview": "Önizleme",
|
||||
"title": "Şablon Önizlemesi"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Формат виходу",
|
||||
"supportedPlatforms": "Підтримувані платформи"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Не вдалося декодувати: вміст не є дійсним форматом Base64",
|
||||
"decodedContent": "Декодований вміст",
|
||||
"originalContent": "Оригінальний вміст (Base64)"
|
||||
},
|
||||
"failed": "Не вдалося відобразити попередній перегляд",
|
||||
"loading": "Завантаження...",
|
||||
"preview": "Перегляд",
|
||||
"title": "Попередній перегляд шаблону"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "Định dạng Đầu ra",
|
||||
"supportedPlatforms": "Nền tảng Hỗ trợ"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "Giải mã không thành công: Nội dung không phải định dạng Base64 hợp lệ",
|
||||
"decodedContent": "Nội dung đã giải mã",
|
||||
"originalContent": "Nội dung gốc (Base64)"
|
||||
},
|
||||
"failed": "Xem trước không thành công",
|
||||
"loading": "Đang tải...",
|
||||
"preview": "Xem trước",
|
||||
"title": "Xem trước mẫu"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "输出格式",
|
||||
"supportedPlatforms": "支持的平台"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"title": "模板预览",
|
||||
"preview": "预览",
|
||||
"loading": "加载中...",
|
||||
"failed": "预览失败",
|
||||
"base64": {
|
||||
"originalContent": "原始内容 (Base64)",
|
||||
"decodedContent": "解码后内容",
|
||||
"decodeError": "解码失败:内容不是有效的Base64格式"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,5 +117,16 @@
|
||||
"outputFormat": "輸出格式",
|
||||
"supportedPlatforms": "支持的平台"
|
||||
}
|
||||
},
|
||||
"templatePreview": {
|
||||
"base64": {
|
||||
"decodeError": "解碼失敗:內容不是有效的 Base64 格式",
|
||||
"decodedContent": "解碼內容",
|
||||
"originalContent": "原始內容(Base64)"
|
||||
},
|
||||
"failed": "預覽失敗",
|
||||
"loading": "加載中...",
|
||||
"preview": "預覽",
|
||||
"title": "範本預覽"
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,24 @@ export async function createSubscribeApplication(
|
||||
});
|
||||
}
|
||||
|
||||
/** Preview Template GET /v1/admin/application/preview */
|
||||
export async function previewSubscribeTemplate(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.PreviewSubscribeTemplateParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.Response & { data?: API.PreviewSubscribeTemplateResponse }>(
|
||||
'/v1/admin/application/preview',
|
||||
{
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Update subscribe application PUT /v1/admin/application/subscribe_application */
|
||||
export async function updateSubscribeApplication(
|
||||
body: API.UpdateSubscribeApplicationRequest,
|
||||
|
||||
15
apps/admin/services/admin/typings.d.ts
vendored
15
apps/admin/services/admin/typings.d.ts
vendored
@ -342,7 +342,6 @@ declare namespace API {
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
template: string;
|
||||
output_format: string;
|
||||
download_link: DownloadLink;
|
||||
@ -1213,6 +1212,19 @@ declare namespace API {
|
||||
orderNo: string;
|
||||
};
|
||||
|
||||
type PreviewSubscribeTemplateParams = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
type PreviewSubscribeTemplateRequest = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
type PreviewSubscribeTemplateResponse = {
|
||||
/** 预览的模板内容 */
|
||||
template: string;
|
||||
};
|
||||
|
||||
type PrivacyPolicyConfig = {
|
||||
privacy_policy: string;
|
||||
};
|
||||
@ -1505,7 +1517,6 @@ declare namespace API {
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
template: string;
|
||||
output_format: string;
|
||||
download_link?: DownloadLink;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// @ts-ignore
|
||||
|
||||
/* eslint-disable */
|
||||
import request from '@/utils/request';
|
||||
|
||||
/** Get Ads GET /v1/common/ads */
|
||||
@ -45,13 +45,10 @@ export async function checkVerificationCode(
|
||||
|
||||
/** Get Client GET /v1/common/client */
|
||||
export async function getClient(options?: { [key: string]: any }) {
|
||||
return request<API.Response & { data?: API.GetSubscribeApplicationListResponse }>(
|
||||
'/v1/common/client',
|
||||
{
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
return request<API.Response & { data?: API.GetSubscribeClientResponse }>('/v1/common/client', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Get verification code POST /v1/common/send_code */
|
||||
|
||||
12
apps/admin/services/common/typings.d.ts
vendored
12
apps/admin/services/common/typings.d.ts
vendored
@ -293,9 +293,9 @@ declare namespace API {
|
||||
protocol: string[];
|
||||
};
|
||||
|
||||
type GetSubscribeApplicationListResponse = {
|
||||
type GetSubscribeClientResponse = {
|
||||
total: number;
|
||||
list: SubscribeApplication[];
|
||||
list: SubscribeClient[];
|
||||
};
|
||||
|
||||
type GetTosResponse = {
|
||||
@ -779,20 +779,14 @@ declare namespace API {
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeApplication = {
|
||||
type SubscribeClient = {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
template: string;
|
||||
output_format: string;
|
||||
download_link?: DownloadLink;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeConfig = {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// @ts-ignore
|
||||
|
||||
/* eslint-disable */
|
||||
import request from '@/utils/request';
|
||||
|
||||
/** Get Ads GET /v1/common/ads */
|
||||
@ -45,13 +45,10 @@ export async function checkVerificationCode(
|
||||
|
||||
/** Get Client GET /v1/common/client */
|
||||
export async function getClient(options?: { [key: string]: any }) {
|
||||
return request<API.Response & { data?: API.GetSubscribeApplicationListResponse }>(
|
||||
'/v1/common/client',
|
||||
{
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
return request<API.Response & { data?: API.GetSubscribeClientResponse }>('/v1/common/client', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Get verification code POST /v1/common/send_code */
|
||||
|
||||
12
apps/user/services/common/typings.d.ts
vendored
12
apps/user/services/common/typings.d.ts
vendored
@ -293,9 +293,9 @@ declare namespace API {
|
||||
protocol: string[];
|
||||
};
|
||||
|
||||
type GetSubscribeApplicationListResponse = {
|
||||
type GetSubscribeClientResponse = {
|
||||
total: number;
|
||||
list: SubscribeApplication[];
|
||||
list: SubscribeClient[];
|
||||
};
|
||||
|
||||
type GetTosResponse = {
|
||||
@ -779,20 +779,14 @@ declare namespace API {
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeApplication = {
|
||||
type SubscribeClient = {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
template: string;
|
||||
output_format: string;
|
||||
download_link?: DownloadLink;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeConfig = {
|
||||
|
||||
21
apps/user/services/user/typings.d.ts
vendored
21
apps/user/services/user/typings.d.ts
vendored
@ -275,11 +275,6 @@ declare namespace API {
|
||||
methods: UserAuthMethod[];
|
||||
};
|
||||
|
||||
type GetSubscribeApplicationListResponse = {
|
||||
total: number;
|
||||
list: SubscribeApplication[];
|
||||
};
|
||||
|
||||
type GetSubscribeLogParams = {
|
||||
page: number;
|
||||
size: number;
|
||||
@ -877,22 +872,6 @@ declare namespace API {
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeApplication = {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
scheme?: string;
|
||||
user_agent: string;
|
||||
is_default: boolean;
|
||||
proxy_template: string;
|
||||
template: string;
|
||||
output_format: string;
|
||||
download_link?: DownloadLink;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
type SubscribeConfig = {
|
||||
single_model: boolean;
|
||||
subscribe_path: string;
|
||||
|
||||
@ -66,6 +66,7 @@ const REGEX_PATTERNS = {
|
||||
FIELD_PATH: /^(\.\w+(?:\.\w+)*)$/,
|
||||
NESTED_DOT: /(\.\w+(?:\.\w+)*)\./,
|
||||
NESTED_VAR: /(\$\w+\.\w+(?:\.\w+)*)\.$/,
|
||||
VAR_DOT: /(\$\w+)\.$/,
|
||||
NESTED_GENERAL: /([\w.]+)\.$/,
|
||||
WORD_WITH_SPACES: /(\s*)(\S*)$/,
|
||||
LEADING_SPACES: /^(\s+)/,
|
||||
@ -694,6 +695,33 @@ export function GoTemplateEditor({ schema, enableSprig = true, ...props }: GoTem
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
isSchemaProperty(currentSchema) &&
|
||||
currentSchema.type === 'array' &&
|
||||
currentSchema.items &&
|
||||
currentSchema.items.type === 'object' &&
|
||||
currentSchema.items.properties
|
||||
) {
|
||||
Object.keys(currentSchema.items.properties).forEach((key) => {
|
||||
const prop = currentSchema.items!.properties![key];
|
||||
if (isSchemaProperty(prop)) {
|
||||
items.push({
|
||||
label: key,
|
||||
kind: COMPLETION_KINDS.PROPERTY,
|
||||
insertText: key,
|
||||
documentation: `${prop.description || key} (${prop.type}) - from array item`,
|
||||
sortText: `${SORT_PREFIXES.NESTED}${key}`,
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: key,
|
||||
kind: COMPLETION_KINDS.PROPERTY,
|
||||
insertText: key,
|
||||
documentation: `Field: ${key} - from array item`,
|
||||
sortText: `${SORT_PREFIXES.NESTED}${key}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
@ -848,24 +876,67 @@ export function GoTemplateEditor({ schema, enableSprig = true, ...props }: GoTem
|
||||
Math.max(templateStartNormal, templateStartTrim) +
|
||||
(templateStartTrim > templateStartNormal ? 3 : 2);
|
||||
const actualStart = Math.max(wordStart, templateStart);
|
||||
const currentWord = textUntilPosition.substring(actualStart).trim();
|
||||
const currentWord = textUntilPosition.substring(actualStart);
|
||||
const currentWordTrimmed = currentWord.trim();
|
||||
|
||||
let dotMatches = currentWord.match(REGEX_PATTERNS.NESTED_DOT);
|
||||
let dotMatches = currentWordTrimmed.match(REGEX_PATTERNS.NESTED_DOT);
|
||||
if (!dotMatches) {
|
||||
const beforeCursor = textUntilPosition.substring(Math.max(templateStart, 0));
|
||||
dotMatches =
|
||||
beforeCursor.match(REGEX_PATTERNS.NESTED_DOT) ||
|
||||
beforeCursor.match(REGEX_PATTERNS.NESTED_VAR) ||
|
||||
beforeCursor.match(REGEX_PATTERNS.VAR_DOT) ||
|
||||
beforeCursor.match(REGEX_PATTERNS.NESTED_GENERAL);
|
||||
}
|
||||
const isNestedField = dotMatches && textUntilPosition.endsWith('.') && schema;
|
||||
|
||||
const justTypedDot = currentWord.endsWith('.') || textUntilPosition.endsWith('.');
|
||||
const varDotMatch = textUntilPosition.match(REGEX_PATTERNS.VAR_DOT);
|
||||
const isVarDot =
|
||||
varDotMatch &&
|
||||
textUntilPosition.endsWith('.') &&
|
||||
schema &&
|
||||
activeRangeField &&
|
||||
rangeVariable;
|
||||
|
||||
const isNestedField =
|
||||
(dotMatches && textUntilPosition.endsWith('.') && schema) || isVarDot;
|
||||
|
||||
const justTypedDot =
|
||||
currentWordTrimmed.endsWith('.') || textUntilPosition.endsWith('.');
|
||||
const justTypedSpace = textUntilPosition.endsWith(' ');
|
||||
const wordForFiltering = justTypedDot ? currentWord.slice(0, -1) : currentWord;
|
||||
let wordForFiltering = currentWordTrimmed;
|
||||
if (justTypedDot) {
|
||||
wordForFiltering = currentWordTrimmed.slice(0, -1);
|
||||
}
|
||||
|
||||
if ((justTypedDot && activeRangeField) || isVarDot) {
|
||||
console.log('Go Template Debug:', {
|
||||
justTypedDot,
|
||||
isVarDot,
|
||||
varDotMatch,
|
||||
activeRangeField,
|
||||
rangeVariable,
|
||||
currentWord,
|
||||
currentWordTrimmed,
|
||||
wordForFiltering,
|
||||
textUntilPosition,
|
||||
allCompletionsCount: allCompletions.length,
|
||||
});
|
||||
}
|
||||
|
||||
if (isNestedField && schema) {
|
||||
let fieldPath = '';
|
||||
|
||||
if (isVarDot && varDotMatch) {
|
||||
// 处理变量后跟点的情况 ($n.)
|
||||
const variableName = varDotMatch[1];
|
||||
if (variableName === rangeVariable && activeRangeField) {
|
||||
// 使用activeRangeField作为字段路径
|
||||
fieldPath = activeRangeField;
|
||||
}
|
||||
} else if (dotMatches && dotMatches[1]) {
|
||||
fieldPath = dotMatches[1];
|
||||
}
|
||||
|
||||
if (isNestedField && schema && dotMatches) {
|
||||
const fieldPath = dotMatches[1];
|
||||
if (fieldPath) {
|
||||
const nestedCompletions = getNestedFieldCompletions(
|
||||
schema,
|
||||
@ -889,26 +960,45 @@ export function GoTemplateEditor({ schema, enableSprig = true, ...props }: GoTem
|
||||
);
|
||||
};
|
||||
|
||||
const isRangeContextItem = (item: CompletionItem): boolean => {
|
||||
return (
|
||||
isVariableOrFieldItem(item) ||
|
||||
item.sortText?.startsWith(SORT_PREFIXES.ROOT_IN_RANGE)
|
||||
);
|
||||
};
|
||||
|
||||
const filteredCompletions = allCompletions.filter((item) => {
|
||||
if (isNestedField) {
|
||||
return item.sortText?.startsWith(SORT_PREFIXES.NESTED);
|
||||
}
|
||||
|
||||
if (justTypedDot && activeRangeField) {
|
||||
return (
|
||||
item.label.startsWith('.') ||
|
||||
item.label.startsWith('$') ||
|
||||
item.sortText?.startsWith(SORT_PREFIXES.RANGE_VAR) ||
|
||||
item.sortText?.startsWith(SORT_PREFIXES.VAR_FIELD) ||
|
||||
item.sortText?.startsWith(SORT_PREFIXES.CURRENT) ||
|
||||
item.sortText?.startsWith(SORT_PREFIXES.ROOT_FIELD) ||
|
||||
item.sortText?.startsWith(SORT_PREFIXES.ROOT_IN_RANGE)
|
||||
);
|
||||
}
|
||||
|
||||
if (justTypedDot) {
|
||||
return activeRangeField ? isRangeContextItem(item) : isVariableOrFieldItem(item);
|
||||
return isVariableOrFieldItem(item);
|
||||
}
|
||||
|
||||
if (justTypedSpace) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
activeRangeField &&
|
||||
(item.sortText?.startsWith(SORT_PREFIXES.RANGE_VAR) ||
|
||||
item.sortText?.startsWith(SORT_PREFIXES.VAR_FIELD) ||
|
||||
item.sortText?.startsWith(SORT_PREFIXES.CURRENT))
|
||||
) {
|
||||
if (!wordForFiltering) {
|
||||
return true;
|
||||
}
|
||||
const label = item.label.toLowerCase();
|
||||
const word = wordForFiltering.toLowerCase();
|
||||
return label.includes(word) || label.startsWith(word) || word === '';
|
||||
}
|
||||
|
||||
if (!wordForFiltering) {
|
||||
return true;
|
||||
}
|
||||
@ -1006,18 +1096,11 @@ export function GoTemplateEditor({ schema, enableSprig = true, ...props }: GoTem
|
||||
const wordMatch = templateContent.match(REGEX_PATTERNS.WORD_WITH_SPACES);
|
||||
if (wordMatch) {
|
||||
const [, leadingSpaces, currentWordInTemplate] = wordMatch;
|
||||
if (leadingSpaces && currentWordInTemplate) {
|
||||
if (currentWordInTemplate) {
|
||||
startColumn =
|
||||
templateStart + templateContent.length - currentWordInTemplate.length;
|
||||
} else {
|
||||
const spaceMatch = templateContent.match(REGEX_PATTERNS.LEADING_SPACES);
|
||||
if (
|
||||
spaceMatch &&
|
||||
spaceMatch[1] &&
|
||||
templateContent.trim() === currentWordInTemplate
|
||||
) {
|
||||
startColumn = templateStart + spaceMatch[1].length;
|
||||
}
|
||||
} else if (leadingSpaces) {
|
||||
startColumn = position.column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ export interface MonacoEditorProps {
|
||||
beforeMount?: (monaco: Monaco) => void;
|
||||
language?: string;
|
||||
className?: string;
|
||||
showLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -43,6 +44,7 @@ export function MonacoEditor({
|
||||
beforeMount,
|
||||
language = 'markdown',
|
||||
className,
|
||||
showLineNumbers = false,
|
||||
}: MonacoEditorProps) {
|
||||
const [internalValue, setInternalValue] = useState<string | undefined>(propValue);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
@ -133,7 +135,7 @@ export function MonacoEditor({
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
glyphMargin: false,
|
||||
lineNumbers: 'off',
|
||||
lineNumbers: showLineNumbers ? 'on' : 'off',
|
||||
minimap: { enabled: false },
|
||||
overviewRulerLanes: 0,
|
||||
renderLineHighlight: 'none',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user