diff --git a/docs/README.md b/docs/README.md
index 9f4736622..33948359c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -30,7 +30,7 @@ Expo React Native 移动壳和 Tauri 桌面壳的工程结构、同源 WebView
`/editor/agent` 浏览器内 AI Web 工程编辑器的静态 SPA 沙箱预览 MVP,采用“平台编辑器壳 + api-server 控制面 + 独立 runner worker + 独立预览域”四层结构;技术方案、威胁模型和验收清单见 [【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md](./technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md)、[【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md](./technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md) 和 [【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md](./technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md)。P1 先用确定性 mock Agent 生成结构化 patch、真实打通项目 / 快照 / 构建 / artifact / 预览闭环,落地拆分见 [【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md](./technical/【技术方案】EditorAgentMockAgentP1落地计划-2026-06-15.md)。
-`/editor/canvas` 图片画布编辑器的画布素材 ZIP 导出能力,入口放在右上角标题栏下载图标内,第一版采用前端 JSZip 打包画布中有效图层引用的上传图、生成图和修改结果,方案见 [【前端架构】图片画布素材导出方案-2026-06-15.md](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)。
+`/editor/canvas` 图片画布编辑器的画布素材 ZIP 导出能力,入口放在右上角标题栏下载图标内,采用前端 JSZip 打包画布中有效图层引用的上传图、生成图、修改结果和角色动作序列帧;动作图层右键“导出为”提供序列帧 ZIP(含前端生成的 `preview.gif`)与 Spine JSON ZIP 两个二级选项,方案见 [【前端架构】图片画布素材导出方案-2026-06-15.md](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)。
图片画布生成类面板的模型泥点默认 JSON、运行时 override、后台“模型定价”页面和主站动态下发口径见 [【编辑器】模型定价配置管理方案-2026-06-22.md](./%E3%80%90%E7%BC%96%E8%BE%91%E5%99%A8%E3%80%91%E6%A8%A1%E5%9E%8B%E5%AE%9A%E4%BB%B7%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88-2026-06-22.md)。
diff --git a/docs/technical/【前端架构】图片画布素材导出方案-2026-06-15.md b/docs/technical/【前端架构】图片画布素材导出方案-2026-06-15.md
index 66a94e92b..ba0fb6d88 100644
--- a/docs/technical/【前端架构】图片画布素材导出方案-2026-06-15.md
+++ b/docs/technical/【前端架构】图片画布素材导出方案-2026-06-15.md
@@ -64,7 +64,24 @@
└─ manifest.txt
```
-角色动作单图层导出时,直接生成独立 ZIP:
+角色动作单图层右键“导出为”展开二级菜单:
+
+- `序列帧导出(zip)` 生成普通序列帧 ZIP,保留完整 `frames/`,并由前端把成功读取的帧拼成 `preview.gif`。
+- `Spine 导出(zip)` 生成 Spine JSON ZIP,保留 `frames/`,并附带 `skeleton.json`。
+
+普通序列帧 ZIP 结构:
+
+```text
+角色动作-Sequence.zip
+├─ frames/
+│ ├─ frame-01.png
+│ └─ frame-02.png
+├─ preview.gif
+├─ metadata.json
+└─ manifest.txt
+```
+
+Spine JSON ZIP 结构:
```text
角色动作-SpineJSON.zip
@@ -151,7 +168,8 @@ assetObjectId > objectKey > sourceAssetId > src
- 同源或可访问 URL 使用 `fetch` 拉取 Blob。
- 私有 generated / OSS 素材必须走同源 `/api/assets/read-bytes` 读取字节;不要在导出流程里直接 `fetch` OSS 签名 URL,避免浏览器 CORS 拦截。
- `mediaType="image-sequence"` 逐帧读取 `imageSequenceFrames`,写入 `sequences/<编号-标题>/frames/`。
- - `mediaType="image-sequence"` 同步写入 `skeleton.json`,供 Spine Editor 或 runtime 以 slot attachment timeline 方式播放序列帧。
+ - 画布素材 ZIP 内的 `mediaType="image-sequence"` 同步写入 `skeleton.json`,供 Spine Editor 或 runtime 以 slot attachment timeline 方式播放序列帧。
+ - 单图层普通序列帧导出写入 `preview.gif`,由前端基于成功读取的帧生成动画预览,不依赖压缩包读取时再临时播放 PNG。
- 拉取失败时记录失败项,不中断整个导出。
5. 使用 `JSZip` 写入 `images/`、`metadata.json` 和 `manifest.txt`。
6. `zip.generateAsync({ type: 'blob' })` 生成文件。
@@ -176,6 +194,8 @@ assetObjectId > objectKey > sourceAssetId > src
- 图层的锁定、翻转、分组状态写入元数据。
- `data:image` 图片不经过网络请求即可导出。
- URL 图片 fetch 失败时不中断其他素材导出。
+- 动作图层右键菜单把 `导出为` 作为一级入口,二级菜单提供 `序列帧导出(zip)` 和 `Spine 导出(zip)`。
+- 单图层普通序列帧 ZIP 包含 `frames/`、`preview.gif`、`metadata.json` 和 `manifest.txt`,且不包含 `skeleton.json`。
- 序列帧导出的 `skeleton.json` 可被独立验证器解析并预览。
## Spine JSON 验证器
diff --git a/src/components/image-editor/ImageCanvasContextMenusView.test.tsx b/src/components/image-editor/ImageCanvasContextMenusView.test.tsx
index 928c676e0..3819db426 100644
--- a/src/components/image-editor/ImageCanvasContextMenusView.test.tsx
+++ b/src/components/image-editor/ImageCanvasContextMenusView.test.tsx
@@ -109,8 +109,17 @@ describe('ImageCanvasContextMenusView', () => {
fireEvent.click(screen.getByRole('menuitem', { name: '显示' }));
fireEvent.click(screen.getByRole('menuitem', { name: '解锁' }));
fireEvent.click(screen.getByRole('menuitem', { name: '水平翻转' }));
- fireEvent.click(screen.getByRole('menuitem', { name: 'Spine 导出(zip)' }));
- fireEvent.click(screen.getByRole('menuitem', { name: '序列帧导出(zip)' }));
+ expect(
+ screen
+ .getByRole('menuitem', { name: '导出为' })
+ .getAttribute('aria-haspopup'),
+ ).toBe('menu');
+ fireEvent.click(
+ screen.getByRole('menuitem', { name: '序列帧导出(zip)' }),
+ );
+ fireEvent.click(
+ screen.getByRole('menuitem', { name: 'Spine 导出(zip)' }),
+ );
fireEvent.click(screen.getByRole('menuitem', { name: '生成动画' }));
fireEvent.click(screen.getByRole('menuitem', { name: '删除' }));
@@ -123,11 +132,11 @@ describe('ImageCanvasContextMenusView', () => {
expect(props.onFlipContextLayers).toHaveBeenCalledWith('x');
expect(props.onExportContextLayer).toHaveBeenNthCalledWith(
1,
- expect.objectContaining({ mode: 'spine-json' }),
+ expect.objectContaining({ mode: 'sequence-with-preview' }),
);
expect(props.onExportContextLayer).toHaveBeenNthCalledWith(
2,
- expect.objectContaining({ mode: 'sequence-with-preview' }),
+ expect.objectContaining({ mode: 'spine-json' }),
);
expect(props.onOpenCharacterAnimationPanel).toHaveBeenCalledWith(layer);
expect(props.onCloseContextMenu).toHaveBeenCalledTimes(1);
diff --git a/src/components/image-editor/ImageCanvasContextMenusView.tsx b/src/components/image-editor/ImageCanvasContextMenusView.tsx
index 5853ccfe3..99c3c5182 100644
--- a/src/components/image-editor/ImageCanvasContextMenusView.tsx
+++ b/src/components/image-editor/ImageCanvasContextMenusView.tsx
@@ -1,4 +1,7 @@
-import { PlatformFloatingMenu, PlatformFloatingMenuItem } from '../common/PlatformFloatingMenu';
+import {
+ PlatformFloatingMenu,
+ PlatformFloatingMenuItem,
+} from '../common/PlatformFloatingMenu';
import type {
CanvasClipboard,
CanvasContextMenuState,
@@ -188,7 +191,11 @@ export function ImageCanvasContextMenusView({
移动至底层
-