From a8d1c8367fe534d86abc22f72befcadfae06bf55 Mon Sep 17 00:00:00 2001
From: MT-Mint <798521692@qq.com>
Date: Tue, 17 Mar 2026 15:26:59 +0800
Subject: [PATCH] =?UTF-8?q?feat(artifact):=20=E6=B7=BB=E5=8A=A0=E5=86=85?=
=?UTF-8?q?=E5=AE=B9=E9=A2=84=E8=A7=88=E7=BC=A9=E6=94=BE=E9=80=89=E6=8B=A9?=
=?UTF-8?q?=E5=99=A8=E5=92=8C=E5=85=A8=E5=B1=80format=E5=91=BD=E4=BB=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/docs/artifact-zoom-feature.md | 149 ++++++++++++++++++
frontend/package.json | 2 +
.../artifacts/artifact-file-detail.tsx | 119 +++++++++++++-
.../src/components/workspace/code-editor.tsx | 5 +-
frontend/src/styles/globals.css | 31 +++-
5 files changed, 293 insertions(+), 13 deletions(-)
create mode 100644 frontend/docs/artifact-zoom-feature.md
diff --git a/frontend/docs/artifact-zoom-feature.md b/frontend/docs/artifact-zoom-feature.md
new file mode 100644
index 00000000..0c5393c8
--- /dev/null
+++ b/frontend/docs/artifact-zoom-feature.md
@@ -0,0 +1,149 @@
+# Artifact 缩放功能文档
+
+## 功能概述
+
+Artifact 缩放功能允许用户在预览 Markdown 和 HTML 内容时调整缩放比例(50% - 200%),提供更灵活的阅读体验。
+
+## 文件结构
+
+```
+frontend/src/
+├── components/
+│ └── workspace/
+│ └── artifacts/
+│ └── artifact-file-detail.tsx # 缩放组件定义与使用
+└── styles/
+ └── globals.css # 缩放相关 CSS 变量
+```
+
+## 核心实现
+
+### 1. 缩放选择器组件 (`ArtifactZoomSelector`)
+
+**位置**: `artifact-file-detail.tsx`
+
+```tsx
+// 缩放比例选项
+const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200];
+
+export const ArtifactZoomSelector = ({
+ value = 100,
+ onChange,
+}: ArtifactZoomSelectorProps) => {
+ // 放大/缩小逻辑
+ // 返回: [ZoomOut图标] [百分比文本] [ZoomIn图标]
+};
+```
+
+**UI 设计**:
+- 白色圆角容器,带轻微阴影和模糊效果
+- 水平三元素布局:缩小按钮 | 百分比显示 | 放大按钮
+- 支持深色模式
+- 边界值时按钮自动禁用
+
+### 2. CSS 变量缩放 (`globals.css`)
+
+```css
+/* 缩放变量,默认为 1 (100%) */
+:root {
+ --zoom-scale: 1;
+}
+
+/* 字体大小使用 calc() 计算 */
+p {
+ font-size: calc(14px * var(--zoom-scale));
+}
+
+[data-streamdown="heading-1"] {
+ font-size: calc(20px * var(--zoom-scale));
+}
+
+[data-streamdown="heading-2"],
+[data-streamdown="heading-3"] {
+ font-size: calc(16px * var(--zoom-scale));
+}
+```
+
+### 3. 预览组件集成 (`ArtifactFilePreview`)
+
+```tsx
+export function ArtifactFilePreview({
+ content,
+ language,
+ zoom = 100,
+}: {
+ content: string;
+ language: string;
+ zoom?: number;
+}) {
+ const zoomScale = zoom / 100;
+
+ // Markdown: 使用 CSS 变量
+ if (language === "markdown") {
+ return (
+
+ ...
+
+ );
+ }
+
+ // HTML: 使用 CSS zoom 属性
+ if (language === "html") {
+ return ;
+ }
+}
+```
+
+## 使用方式
+
+### 在 ArtifactHeader 中使用
+
+```tsx
+function ArtifactFileDetail() {
+ const [zoom, setZoom] = useState(100);
+
+ return (
+
+
+
+ {/* 中间标题 */}
+ {/* 右侧操作按钮 */}
+
+
+
+
+
+
+ );
+}
+```
+
+## 缩放级别
+
+| 级别 | 比例 |
+|------|------|
+| 最小 | 50% |
+| | 60%, 70%, 80%, 90% |
+| 默认 | 100% |
+| | 110%, 120%, 130%, 150%, 175% |
+| 最大 | 200% |
+
+## 技术要点
+
+1. **CSS 变量方式** - 适用于 Markdown 内容,通过 `--zoom-scale` 变量控制所有字体和间距
+2. **CSS zoom 属性** - 适用于 HTML iframe,直接缩放整个内容
+3. **状态提升** - `zoom` 状态在父组件管理,通过 props 传递给预览组件
+4. **响应式** - 缩放变化时所有相关样式自动重新计算
+
+## 扩展建议
+
+- 可添加快捷键支持(如 `Ctrl++` / `Ctrl+-`)
+- 可支持双击重置为 100%
+- 可将缩放状态持久化到 localStorage
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 46ca46a1..6b8690d2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -8,6 +8,8 @@
"build": "next build",
"check": "next lint && tsc --noEmit",
"dev": "next dev --turbo",
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css,json}\"",
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css,json}\"",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"preview": "next build && next start",
diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx
index 658ed51a..9de3d7c5 100644
--- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx
+++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx
@@ -7,8 +7,17 @@ import {
PackageIcon,
SquareArrowOutUpRightIcon,
XIcon,
+ type LucideIcon,
+ ZoomIn,
+ ZoomOut,
} from "lucide-react";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+ type HTMLAttributes,
+} from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";
@@ -94,6 +103,7 @@ export function ArtifactFileDetail({
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
const [isInstalling, setIsInstalling] = useState(false);
+ const [zoom, setZoom] = useState(100);
const { isMock } = useThread();
useEffect(() => {
if (isSupportPreview) {
@@ -163,8 +173,11 @@ export function ArtifactFileDetail({
+ {/* 放大缩小选择器 */}
+
- {!isWriteFile && filepath.endsWith(".skill") && (
+ {/* 新界面打开的按钮 */}
+ {/* {!isWriteFile && filepath.endsWith(".skill") && (
- )}
+ )} */}
{!isWriteFile && (
-
+ {/* 主内容区 */}
{isSupportPreview &&
viewMode === "preview" &&
@@ -233,12 +246,14 @@ export function ArtifactFileDetail({
)}
{isCodeFile && viewMode === "code" && (
)}
@@ -256,13 +271,20 @@ export function ArtifactFileDetail({
export function ArtifactFilePreview({
content,
language,
+ zoom = 100,
}: {
content: string;
language: string;
+ zoom?: number;
}) {
+ const zoomScale = zoom / 100;
+
if (language === "markdown") {
return (
-
+
);
}
return null;
}
+
+// 缩放比例选项
+const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200];
+
+export type ArtifactZoomSelectorProps = Omit<
+ HTMLAttributes
,
+ "onChange"
+> & {
+ value?: number;
+ onChange?: (value: number) => void;
+};
+
+export const ArtifactZoomSelector = ({
+ value = 100,
+ onChange,
+ className,
+ ...props
+}: ArtifactZoomSelectorProps) => {
+ const handleZoomIn = () => {
+ const currentIndex = ZOOM_LEVELS.indexOf(value);
+ const nextValue = ZOOM_LEVELS[currentIndex + 1];
+ if (currentIndex < ZOOM_LEVELS.length - 1 && nextValue !== undefined) {
+ onChange?.(nextValue);
+ }
+ };
+
+ const handleZoomOut = () => {
+ const currentIndex = ZOOM_LEVELS.indexOf(value);
+ const prevValue = ZOOM_LEVELS[currentIndex - 1];
+ if (currentIndex > 0 && prevValue !== undefined) {
+ onChange?.(prevValue);
+ }
+ };
+
+ const canZoomIn = ZOOM_LEVELS.indexOf(value) < ZOOM_LEVELS.length - 1;
+ const canZoomOut = ZOOM_LEVELS.indexOf(value) > 0;
+
+ return (
+
+
+
+ {value}%
+
+
+
+ );
+};
diff --git a/frontend/src/components/workspace/code-editor.tsx b/frontend/src/components/workspace/code-editor.tsx
index 84c558c4..842a3e45 100644
--- a/frontend/src/components/workspace/code-editor.tsx
+++ b/frontend/src/components/workspace/code-editor.tsx
@@ -42,6 +42,7 @@ export function CodeEditor({
disabled,
autoFocus,
settings,
+ zoom = 100,
}: {
className?: string;
placeholder?: string;
@@ -50,6 +51,7 @@ export function CodeEditor({
disabled?: boolean;
autoFocus?: boolean;
settings?: unknown;
+ zoom?: number;
}) {
const {
thread: { isLoading },
@@ -69,13 +71,14 @@ export function CodeEditor({
python(),
];
}, []);
-
+ const zoomScale = zoom / 100;
return (
{isLoading ? (