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

This commit is contained in:
web 2025-08-14 09:55:13 -07:00
parent f190c68eb0
commit 0448d213a3
38 changed files with 522 additions and 92 deletions

View File

@ -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: {

View 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>
</>
);
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "فرمت خروجی",
"supportedPlatforms": "پلتفرم‌های پشتیبانی شده"
}
},
"templatePreview": {
"base64": {
"decodeError": "خطا در رمزگشایی: محتوا فرمت Base64 معتبر نیست",
"decodedContent": "محتوای رمزگشایی شده",
"originalContent": "محتوای اصلی (Base64)"
},
"failed": "پیش‌نمایش ناموفق بود",
"loading": "در حال بارگذاری...",
"preview": "پیش‌نمایش",
"title": "پیش‌نمایش الگو"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "आउटपुट प्रारूप",
"supportedPlatforms": "समर्थित प्लेटफ़ॉर्म"
}
},
"templatePreview": {
"base64": {
"decodeError": "डिकोड विफल: सामग्री मान्य Base64 प्रारूप नहीं है",
"decodedContent": "डिकोड की गई सामग्री",
"originalContent": "मूल सामग्री (Base64)"
},
"failed": "पूर्वावलोकन विफल",
"loading": "लोड हो रहा है...",
"preview": "पूर्वावलोकन",
"title": "टेम्पलेट पूर्वावलोकन"
}
}

View File

@ -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"
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "出力形式",
"supportedPlatforms": "サポートされているプラットフォーム"
}
},
"templatePreview": {
"base64": {
"decodeError": "デコードに失敗しましたコンテンツが有効なBase64形式ではありません",
"decodedContent": "デコードされたコンテンツ",
"originalContent": "オリジナルコンテンツBase64"
},
"failed": "プレビューに失敗しました",
"loading": "読み込み中...",
"preview": "プレビュー",
"title": "テンプレートプレビュー"
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "출력 형식",
"supportedPlatforms": "지원되는 플랫폼"
}
},
"templatePreview": {
"base64": {
"decodeError": "디코드 실패: 콘텐츠가 유효한 Base64 형식이 아닙니다",
"decodedContent": "디코딩된 콘텐츠",
"originalContent": "원본 콘텐츠 (Base64)"
},
"failed": "미리보기가 실패했습니다",
"loading": "로딩 중...",
"preview": "미리보기",
"title": "템플릿 미리보기"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "Формат вывода",
"supportedPlatforms": "Поддерживаемые платформы"
}
},
"templatePreview": {
"base64": {
"decodeError": "Ошибка декодирования: Содержимое не является допустимым форматом Base64",
"decodedContent": "Декодированное содержимое",
"originalContent": "Исходное содержимое (Base64)"
},
"failed": "Не удалось загрузить предварительный просмотр",
"loading": "Загрузка...",
"preview": "Предварительный просмотр",
"title": "Предварительный просмотр шаблона"
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "รูปแบบผลลัพธ์",
"supportedPlatforms": "แพลตฟอร์มที่รองรับ"
}
},
"templatePreview": {
"base64": {
"decodeError": "การถอดรหัสล้มเหลว: เนื้อหาไม่ใช่รูปแบบ Base64 ที่ถูกต้อง",
"decodedContent": "เนื้อหาที่ถอดรหัสแล้ว",
"originalContent": "เนื้อหาต้นฉบับ (Base64)"
},
"failed": "การดูตัวอย่างล้มเหลว",
"loading": "กำลังโหลด...",
"preview": "ดูตัวอย่าง",
"title": "ตัวอย่างแม่แบบ"
}
}

View File

@ -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"
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "Формат виходу",
"supportedPlatforms": "Підтримувані платформи"
}
},
"templatePreview": {
"base64": {
"decodeError": "Не вдалося декодувати: вміст не є дійсним форматом Base64",
"decodedContent": "Декодований вміст",
"originalContent": "Оригінальний вміст (Base64)"
},
"failed": "Не вдалося відобразити попередній перегляд",
"loading": "Завантаження...",
"preview": "Перегляд",
"title": "Попередній перегляд шаблону"
}
}

View File

@ -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"
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "输出格式",
"supportedPlatforms": "支持的平台"
}
},
"templatePreview": {
"title": "模板预览",
"preview": "预览",
"loading": "加载中...",
"failed": "预览失败",
"base64": {
"originalContent": "原始内容 (Base64)",
"decodedContent": "解码后内容",
"decodeError": "解码失败内容不是有效的Base64格式"
}
}
}

View File

@ -117,5 +117,16 @@
"outputFormat": "輸出格式",
"supportedPlatforms": "支持的平台"
}
},
"templatePreview": {
"base64": {
"decodeError": "解碼失敗:內容不是有效的 Base64 格式",
"decodedContent": "解碼內容",
"originalContent": "原始內容Base64"
},
"failed": "預覽失敗",
"loading": "加載中...",
"preview": "預覽",
"title": "範本預覽"
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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',
{
return request<API.Response & { data?: API.GetSubscribeClientResponse }>('/v1/common/client', {
method: 'GET',
...(options || {}),
},
);
});
}
/** Get verification code POST /v1/common/send_code */

View File

@ -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 = {

View File

@ -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',
{
return request<API.Response & { data?: API.GetSubscribeClientResponse }>('/v1/common/client', {
method: 'GET',
...(options || {}),
},
);
});
}
/** Get verification code POST /v1/common/send_code */

View File

@ -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 = {

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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',