diff --git a/src/locales/de-DE/install.json b/src/locales/de-DE/install.json index f22259290..62d65807d 100644 --- a/src/locales/de-DE/install.json +++ b/src/locales/de-DE/install.json @@ -43,6 +43,7 @@ "code_copy": "Code kopieren", "code_collapse": "Einklappen", "code_expand": "Ausklappen", + "code_fullscreen": "Code im Vollbild anzeigen", "loading_title": "Skript wird geladen", "loading_desc": "Skriptinhalt wird von der Quelle heruntergeladen und ausgewertet", "loading_title_update": "Update-Details werden vorbereitet", diff --git a/src/locales/en-US/install.json b/src/locales/en-US/install.json index 64c24de4f..cdd6f1a0f 100644 --- a/src/locales/en-US/install.json +++ b/src/locales/en-US/install.json @@ -43,6 +43,7 @@ "code_copy": "Copy code", "code_collapse": "Collapse", "code_expand": "Expand", + "code_fullscreen": "View code full screen", "loading_title": "Loading script", "loading_desc": "Downloading and parsing the script content from the source", "loading_title_update": "Preparing update details", diff --git a/src/locales/ja-JP/install.json b/src/locales/ja-JP/install.json index 0abd1017c..1f1fe6a6c 100644 --- a/src/locales/ja-JP/install.json +++ b/src/locales/ja-JP/install.json @@ -43,6 +43,7 @@ "code_copy": "コードをコピー", "code_collapse": "折りたたむ", "code_expand": "展開", + "code_fullscreen": "コードを全画面で表示", "loading_title": "スクリプトを読み込み中", "loading_desc": "ソースからスクリプトの内容をダウンロードして解析しています", "loading_title_update": "更新の詳細を準備中", diff --git a/src/locales/ko-KR/install.json b/src/locales/ko-KR/install.json index a06879c34..ab16e9395 100644 --- a/src/locales/ko-KR/install.json +++ b/src/locales/ko-KR/install.json @@ -43,6 +43,7 @@ "code_copy": "코드 복사", "code_collapse": "접기", "code_expand": "펼치기", + "code_fullscreen": "코드를 전체 화면으로 보기", "loading_title": "스크립트 불러오는 중", "loading_desc": "소스에서 스크립트 내용을 다운로드하고 분석하고 있습니다", "loading_title_update": "업데이트 세부 정보를 준비하는 중", diff --git a/src/locales/pt-BR/install.json b/src/locales/pt-BR/install.json index 93047d702..f43b03ced 100644 --- a/src/locales/pt-BR/install.json +++ b/src/locales/pt-BR/install.json @@ -43,6 +43,7 @@ "code_copy": "Copiar código", "code_collapse": "Recolher", "code_expand": "Expandir", + "code_fullscreen": "Ver código em tela cheia", "loading_title": "Carregando script", "loading_desc": "Baixando e analisando o conteúdo da origem do script", "loading_title_update": "Preparando os detalhes da atualização", diff --git a/src/locales/ru-RU/install.json b/src/locales/ru-RU/install.json index f452106b2..478cba003 100644 --- a/src/locales/ru-RU/install.json +++ b/src/locales/ru-RU/install.json @@ -43,6 +43,7 @@ "code_copy": "Копировать код", "code_collapse": "Свернуть", "code_expand": "Развернуть", + "code_fullscreen": "Открыть код на весь экран", "loading_title": "Загрузка скрипта", "loading_desc": "Загрузка и разбор содержимого скрипта из источника", "loading_title_update": "Подготовка сведений об обновлении", diff --git a/src/locales/tr-TR/install.json b/src/locales/tr-TR/install.json index 0cb0a6752..6de4ce731 100644 --- a/src/locales/tr-TR/install.json +++ b/src/locales/tr-TR/install.json @@ -43,6 +43,7 @@ "code_copy": "Kodu kopyala", "code_collapse": "Daralt", "code_expand": "Genişlet", + "code_fullscreen": "Kodu tam ekran görüntüle", "loading_title": "Betik yükleniyor", "loading_desc": "Betik içeriği kaynaktan indiriliyor ve ayrıştırılıyor", "loading_title_update": "Güncelleme ayrıntıları hazırlanıyor", diff --git a/src/locales/vi-VN/install.json b/src/locales/vi-VN/install.json index 6f99c54d7..45ed235ce 100644 --- a/src/locales/vi-VN/install.json +++ b/src/locales/vi-VN/install.json @@ -43,6 +43,7 @@ "code_copy": "Sao chép mã", "code_collapse": "Thu gọn", "code_expand": "Mở rộng", + "code_fullscreen": "Xem mã ở chế độ toàn màn hình", "loading_title": "Đang tải script", "loading_desc": "Đang tải xuống và phân tích nội dung script từ nguồn", "loading_title_update": "Đang chuẩn bị chi tiết cập nhật", diff --git a/src/locales/zh-CN/install.json b/src/locales/zh-CN/install.json index 3947d3f7a..1d25ea2df 100644 --- a/src/locales/zh-CN/install.json +++ b/src/locales/zh-CN/install.json @@ -43,6 +43,7 @@ "code_copy": "复制代码", "code_collapse": "折叠", "code_expand": "展开", + "code_fullscreen": "全屏查看代码", "loading_title": "正在加载脚本", "loading_desc": "正在从来源下载并解析脚本内容", "loading_title_update": "正在准备更新详情", diff --git a/src/locales/zh-TW/install.json b/src/locales/zh-TW/install.json index 6998fe976..b86ebdf8c 100644 --- a/src/locales/zh-TW/install.json +++ b/src/locales/zh-TW/install.json @@ -43,6 +43,7 @@ "code_copy": "複製程式碼", "code_collapse": "收合", "code_expand": "展開", + "code_fullscreen": "全螢幕檢視程式碼", "loading_title": "正在載入腳本", "loading_desc": "正在從來源下載並解析腳本內容", "loading_title_update": "正在準備更新詳情", diff --git a/src/pages/components/CodeEditor/index.test.tsx b/src/pages/components/CodeEditor/index.test.tsx index 8661091b4..1d0fb8fc9 100644 --- a/src/pages/components/CodeEditor/index.test.tsx +++ b/src/pages/components/CodeEditor/index.test.tsx @@ -15,10 +15,16 @@ const h = vi.hoisted(() => { removeDecorations: vi.fn(), createDecorationsCollection: vi.fn(), }); + const diffModifiedEditor = makeEditor(); + const diffEditor = { + ...makeEditor(), + getModifiedEditor: vi.fn(() => diffModifiedEditor), + }; return { resolvedTheme: "light" as string, setTheme: vi.fn(), - createDiffEditor: vi.fn((_container?: unknown, _options?: any) => makeEditor()), + diffModifiedEditor, + createDiffEditor: vi.fn((_container?: unknown, _options?: any) => diffEditor), create: vi.fn((_container?: unknown, _options?: any) => makeEditor()), createModel: vi.fn(() => ({ dispose: vi.fn(), @@ -125,4 +131,16 @@ describe("CodeEditor 可访问性与主题", () => { }); expect(h.setTheme).toHaveBeenCalledWith("dark"); }); + + it("diff 编辑器创建后也应回调 onEditorMount", async () => { + const onEditorMount = vi.fn(); + + await act(async () => { + render( + + ); + }); + + await waitFor(() => expect(onEditorMount).toHaveBeenCalledWith(h.diffModifiedEditor)); + }); }); diff --git a/src/pages/components/CodeEditor/index.tsx b/src/pages/components/CodeEditor/index.tsx index d0465e68a..cd1fc1d9c 100644 --- a/src/pages/components/CodeEditor/index.tsx +++ b/src/pages/components/CodeEditor/index.tsx @@ -162,7 +162,7 @@ function CodeEditor({ id, className, code, diffCode, editable, onChange, onEdito const initialTheme = resolveMonacoTheme(resolvedTheme); if (diffCode) { - edit = editor.createDiffEditor(container, { + const diffEditor = editor.createDiffEditor(container, { hideUnchangedRegions: { enabled: true }, enableSplitViewResizing: false, renderSideBySide: false, @@ -171,6 +171,7 @@ function CodeEditor({ id, className, code, diffCode, editable, onChange, onEdito theme: initialTheme, ...commonEditorOptions, }); + edit = diffEditor; // standalone model 不随 editor.dispose 自动清理,需手动跟踪并在 cleanup 释放 originalModel = editor.createModel(diffCode, "javascript"); modifiedModel = editor.createModel(code, "javascript"); @@ -180,6 +181,7 @@ function CodeEditor({ id, className, code, diffCode, editable, onChange, onEdito }); editorInstanceRef.current = edit; editorReadyRef.current = true; + onEditorMountRef.current?.(diffEditor.getModifiedEditor()); } else { const standaloneEdit = editor.create(container, { language: "javascript", diff --git a/src/pages/components/ui/dialog.tsx b/src/pages/components/ui/dialog.tsx index 26b8ea78e..db8d01f8e 100644 --- a/src/pages/components/ui/dialog.tsx +++ b/src/pages/components/ui/dialog.tsx @@ -11,6 +11,10 @@ function DialogPortal({ ...props }: React.ComponentProps; } +function DialogTrigger({ ...props }: React.ComponentProps) { + return ; +} + function DialogOverlay({ className, ...props }: React.ComponentProps) { return ( ) { +function DialogContent({ + className, + children, + closeLabel = "Close", + closeClassName, + ...props +}: React.ComponentProps & { closeLabel?: string; closeClassName?: string }) { return ( @@ -37,9 +47,14 @@ function DialogContent({ className, children, ...props }: React.ComponentProps {children} - + - {"Close"} + {closeLabel} @@ -86,4 +101,4 @@ function DialogDescription({ className, ...props }: React.ComponentProps { expect(writeText).toHaveBeenCalledWith(code); }); + it("点击全页面查看后在占满视口的对话框中查看代码,并可退出", () => { + render(); + + const fullscreenButton = screen.getByTestId("code-fullscreen"); + expect(fullscreenButton).toHaveAttribute("aria-label", "全屏查看代码"); + fireEvent.click(fullscreenButton); + + const dialog = screen.getByTestId("code-fullscreen-dialog"); + expect(dialog).toHaveClass("left-0", "top-0", "h-dvh", "w-dvw", "max-w-none"); + expect(within(dialog).getByRole("button", { name: "关闭" })).toHaveClass( + "rounded-md", + "flex", + "items-center", + "justify-center", + "text-muted-foreground", + "opacity-100", + "hover:bg-muted", + "hover:text-foreground", + "focus:ring-0", + "focus:ring-offset-0", + "focus-visible:outline-none", + "focus-visible:ring-2", + "focus-visible:ring-ring/50" + ); + expect(within(dialog).getByTestId("code-body")).toHaveAttribute("data-code", code); + + fireEvent.click(screen.getByRole("button", { name: "关闭" })); + expect(screen.queryByTestId("code-fullscreen-dialog")).not.toBeInTheDocument(); + expect(screen.getByTestId("code-body")).toHaveAttribute("data-id", "install-code-preview"); + }); + + it("更新态可在全屏预览中查看代码并隐藏加载骨架", () => { + const oldCode = "// old\nconst a = 0;"; + render(); + + fireEvent.click(screen.getByTestId("code-fullscreen")); + + const dialog = screen.getByTestId("code-fullscreen-dialog"); + expect(within(dialog).getByTestId("code-body")).toHaveAttribute("data-diff", oldCode); + expect(within(dialog).queryByTestId("code-skeleton")).not.toBeInTheDocument(); + }); + + it("按 Escape 关闭全屏并将焦点还给触发按钮", async () => { + render(); + + const fullscreenButton = screen.getByTestId("code-fullscreen"); + fullscreenButton.focus(); + fireEvent.click(fullscreenButton); + expect(screen.getByRole("dialog")).toHaveAttribute("aria-labelledby"); + + fireEvent.keyDown(document, { key: "Escape" }); + + await waitFor(() => { + expect(screen.queryByTestId("code-fullscreen-dialog")).not.toBeInTheDocument(); + expect(document.activeElement).toBe(fullscreenButton); + }); + }); + + it("默认折叠时仍可从全屏按钮打开代码", () => { + render(); + + expect(screen.queryByTestId("code-body")).not.toBeInTheDocument(); + fireEvent.click(screen.getByTestId("code-fullscreen")); + + expect(within(screen.getByTestId("code-fullscreen-dialog")).getByTestId("code-body")).toHaveAttribute( + "data-code", + code + ); + }); + it("提供 diff 统计时渲染 +N 与 −N", () => { render(); expect(screen.getByText("+42")).toBeInTheDocument(); diff --git a/src/pages/install/components/CodePreview.tsx b/src/pages/install/components/CodePreview.tsx index ad69f74ad..2f4d94934 100644 --- a/src/pages/install/components/CodePreview.tsx +++ b/src/pages/install/components/CodePreview.tsx @@ -1,7 +1,8 @@ import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { CodeXml, Copy, Check, ChevronDown, ChevronRight } from "lucide-react"; +import { CodeXml, Copy, Check, ChevronDown, ChevronRight, Maximize2 } from "lucide-react"; import CodeEditor from "@App/pages/components/CodeEditor"; +import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@App/pages/components/ui/dialog"; import { Skeleton } from "@App/pages/components/ui/skeleton"; import { cn } from "@App/pkg/utils/cn"; @@ -36,6 +37,7 @@ export function CodePreview({ const [collapsed, setCollapsed] = useState(defaultCollapsed); const [copied, setCopied] = useState(false); const [editorReady, setEditorReady] = useState(false); + const [fullscreen, setFullscreen] = useState(false); const lineCount = useMemo(() => code.split("\n").length, [code]); // diffCode 语义:""=无 diff(普通只读预览),有值=内联 diff;切勿传 undefined(表示不加载) @@ -47,67 +49,127 @@ export function CodePreview({ setTimeout(() => setCopied(false), 1500); }; + const setFullscreenMode = (open: boolean) => { + setFullscreen(open); + setEditorReady(false); + }; + + const codeEditor = (id: string) => ( + setEditorReady(true)} + className="h-full w-full" + /> + ); + + const editorSkeleton = ( +
+ {CODE_SKELETON_LINES.map((line, i) => ( + + ))} +
+ ); + return ( -
-
- - {t("editor:code")} - {language} - {t("install:code_lines", { count: lineCount })} - {diffStat && ( - - {`+${diffStat.added}`} - {`−${diffStat.removed}`} + +
+
+ + {t("editor:code")} + + {language} - )} -
- - -
-
- {!collapsed && ( -
- {!editorReady && ( - // 编辑器实例要等偏好设置读出来才创建,这段时间这里本来是一块纯空白,看着像加载失败 -
- {CODE_SKELETON_LINES.map((line, i) => ( - - ))} -
+ {t("install:code_lines", { count: lineCount })} + {diffStat && ( + + {`+${diffStat.added}`} + {`−${diffStat.removed}`} + )} - setEditorReady(true)} - className="h-full w-full" - /> +
+ + + + + +
- )} -
+ {!collapsed && !fullscreen && ( +
+ {!editorReady && editorSkeleton} + {codeEditor("install-code-preview")} +
+ )} + +
+
+
+ {!editorReady && editorSkeleton} + {codeEditor("install-code-preview-fullscreen")} +
+
+
+ ); }