diff --git a/docs/superpowers/plans/2026-09-23-attachment-image-preview-rotate-zoom.md b/docs/superpowers/plans/2026-09-23-attachment-image-preview-rotate-zoom.md
new file mode 100644
index 000000000..67885af50
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-23-attachment-image-preview-rotate-zoom.md
@@ -0,0 +1,679 @@
+# 附件图片预览旋转/缩放/拖拽 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在 `AttachmentUploadField` 图片预览弹窗中支持左转/右转、完整可见适配、按钮与滚轮缩放、拖拽平移(仅预览态,不写回文件)。
+
+**Architecture:** 将 `fitBaseScale` / `clampZoom` / `buildPreviewTransform` 抽成纯函数工具,便于 Node 断言;组件内用 refs 管理 angle/zoom/offset,预览区用 `translate + rotate + scale` 渲染。视频/PDF 分支不变。
+
+**Tech Stack:** Vue 3、Element Plus、`@element-plus/icons-vue`、SCSS;无 vitest,纯函数用 Node `assert` 脚本验证。
+
+**Spec:** `docs/superpowers/specs/2026-09-23-attachment-image-preview-rotate-zoom-design.md`
+**Demo:** `docs/superpowers/demos/2026-09-23-attachment-image-preview-rotate-demo.html`
+
+---
+
+## File map
+
+| File | Responsibility |
+|------|----------------|
+| Create: `web-admin/src/utils/image-preview-transform.ts` | 纯函数:fitBase、clampZoom、compose transform CSS |
+| Create: `web-admin/scripts/test-image-preview-transform.mjs` | Node 断言(编译/或直接测逻辑等价 JS) |
+| Modify: `web-admin/src/components/business/attachment-upload-field/index.vue` | 工具条、stage 事件、状态、样式 |
+| Optional: `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md` | 附件预览补一句(非门禁) |
+
+> **Note on test script:** `web-admin` 无 vitest;为避免为单测引入构建链,`test-image-preview-transform.mjs` 内联与 TS 工具**相同算法**的纯 JS 副本做断言,TS 文件为正式源。实现时先写断言脚本(红),再写 `image-preview-transform.ts`,再让脚本 `import` 该逻辑——若 Node 无法直接 import `.ts`,则脚本保持内联副本,并在 Task 1 用注释声明「须与 TS 保持同步」;**推荐**:工具写成 `.mjs` 导出供脚本与组件共用——但组件侧习惯 `.ts`。折中:**TS 工具为唯一实现**;断言脚本用动态 `node --experimental-strip-types`(Node 22+)或复制算法。本仓库 Node 版本未知,采用:**工具写 TS + 断言脚本 import 编译前内联相同函数体**(在脚本顶部注释 `SYNC WITH image-preview-transform.ts`),实现 Task 时以 TS 为准、脚本复制粘贴同一函数。
+
+更干净的做法(本计划采用):
+
+| File | Responsibility |
+|------|----------------|
+| Create: `web-admin/src/utils/image-preview-transform.ts` | 正式实现 |
+| Create: `web-admin/scripts/test-image-preview-transform.mjs` | 从 TS 源**手工同步**的同名函数 + assert(与 bxInvoiceThumb 计划同模式:若可 ESM import JS,则改用 `.js` 工具) |
+
+**最终采用:** 创建 `web-admin/src/utils/image-preview-transform.js`(纯 ESM,无类型依赖,组件可直接 import),断言脚本 import 同一文件。类型用 JSDoc。避免 strip-types。
+
+---
+
+### Task 1: 纯函数工具 + 断言脚本
+
+**Files:**
+- Create: `web-admin/src/utils/image-preview-transform.js`
+- Create: `web-admin/scripts/test-image-preview-rotate.mjs`
+
+- [ ] **Step 1: 写失败的断言脚本(先 import 不存在的模块)**
+
+创建 `web-admin/scripts/test-image-preview-rotate.mjs`:
+
+```js
+import assert from 'node:assert/strict'
+import {
+ IMAGE_PREVIEW_ZOOM_MIN,
+ IMAGE_PREVIEW_ZOOM_MAX,
+ IMAGE_PREVIEW_ZOOM_STEP,
+ clampPreviewZoom,
+ computeFitBaseScale,
+ buildPreviewTransformCss
+} from '../src/utils/image-preview-transform.js'
+
+assert.equal(IMAGE_PREVIEW_ZOOM_MIN, 0.5)
+assert.equal(IMAGE_PREVIEW_ZOOM_MAX, 5)
+assert.equal(IMAGE_PREVIEW_ZOOM_STEP, 0.15)
+
+assert.equal(clampPreviewZoom(0.1), 0.5)
+assert.equal(clampPreviewZoom(9), 5)
+assert.equal(clampPreviewZoom(1.234), 1.23)
+
+// 未旋转:640x400 装入 320x200 → scale = 0.5
+assert.ok(
+ Math.abs(computeFitBaseScale(640, 400, 0, 320, 200) - 0.5) < 1e-9
+)
+
+// 旋转 90°:外接 400x640 装入 320x200 → scale = min(320/400, 200/640) = 0.3125
+assert.ok(
+ Math.abs(computeFitBaseScale(640, 400, 90, 320, 200) - 0.3125) < 1e-9
+)
+
+assert.equal(
+ buildPreviewTransformCss(10, -20, 90, 0.5),
+ 'translate(10px, -20px) rotate(90deg) scale(0.5)'
+)
+
+console.log('image-preview-transform ok')
+```
+
+- [ ] **Step 2: 运行确认失败**
+
+Run:
+
+```bash
+cd web-admin && node scripts/test-image-preview-rotate.mjs
+```
+
+Expected: `ERR_MODULE_NOT_FOUND` for `image-preview-transform.js`
+
+- [ ] **Step 3: 实现工具模块**
+
+Create `web-admin/src/utils/image-preview-transform.js`:
+
+```js
+/** @typedef {{ x: number, y: number }} PreviewOffset */
+
+export const IMAGE_PREVIEW_ZOOM_MIN = 0.5
+export const IMAGE_PREVIEW_ZOOM_MAX = 5
+export const IMAGE_PREVIEW_ZOOM_STEP = 0.15
+
+/**
+ * @param {number} value
+ * @returns {number}
+ */
+export function clampPreviewZoom(value) {
+ const n = Number(value)
+ if (!Number.isFinite(n)) return 1
+ const clamped = Math.min(IMAGE_PREVIEW_ZOOM_MAX, Math.max(IMAGE_PREVIEW_ZOOM_MIN, n))
+ return Math.round(clamped * 100) / 100
+}
+
+/**
+ * 旋转后外接矩形完整装入 stage 的基础缩放(zoom=1 时使用)。
+ * @param {number} naturalWidth
+ * @param {number} naturalHeight
+ * @param {number} angleDeg
+ * @param {number} stageWidth
+ * @param {number} stageHeight
+ * @returns {number}
+ */
+export function computeFitBaseScale(
+ naturalWidth,
+ naturalHeight,
+ angleDeg,
+ stageWidth,
+ stageHeight
+) {
+ const natW = Math.max(Number(naturalWidth) || 1, 1)
+ const natH = Math.max(Number(naturalHeight) || 1, 1)
+ const stageW = Math.max(Number(stageWidth) || 1, 1)
+ const stageH = Math.max(Number(stageHeight) || 1, 1)
+ const rad = ((Number(angleDeg) || 0) * Math.PI) / 180
+ const cos = Math.abs(Math.cos(rad))
+ const sin = Math.abs(Math.sin(rad))
+ const boundW = natW * cos + natH * sin
+ const boundH = natW * sin + natH * cos
+ return Math.min(stageW / boundW, stageH / boundH)
+}
+
+/**
+ * @param {number} offsetX
+ * @param {number} offsetY
+ * @param {number} angleDeg
+ * @param {number} scale
+ * @returns {string}
+ */
+export function buildPreviewTransformCss(offsetX, offsetY, angleDeg, scale) {
+ return (
+ 'translate(' +
+ offsetX +
+ 'px, ' +
+ offsetY +
+ 'px) rotate(' +
+ angleDeg +
+ 'deg) scale(' +
+ scale +
+ ')'
+ )
+}
+```
+
+- [ ] **Step 4: 再跑断言,确认通过**
+
+Run:
+
+```bash
+cd web-admin && node scripts/test-image-preview-rotate.mjs
+```
+
+Expected: `image-preview-transform ok`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add web-admin/src/utils/image-preview-transform.js web-admin/scripts/test-image-preview-rotate.mjs
+git commit -m "$(cat <<'EOF'
+feat(web-admin): add image preview transform helpers for fit/zoom.
+
+Pure functions for AttachmentUploadField image preview rotate/zoom math, with a Node assert script.
+EOF
+)"
+```
+
+---
+
+### Task 2: 预览弹窗模板 — 工具条 + stage 结构
+
+**Files:**
+- Modify: `web-admin/src/components/business/attachment-upload-field/index.vue`(template 中 `el-dialog` 段,约 119–156 行)
+
+- [ ] **Step 1: 扩展 icon import**
+
+在现有 `@element-plus/icons-vue` import 中追加:
+
+```ts
+RefreshLeft,
+RefreshRight,
+ZoomIn,
+ZoomOut,
+Refresh
+```
+
+(`Refresh` 用于「重置」;若项目更偏好其它图标,保持文案「重置」即可。)
+
+- [ ] **Step 2: 替换 dialog body 中图片分支结构**
+
+将原先单独的 `` 改为(视频/PDF 分支保持不动):
+
+```vue
+