diff --git a/docs/superpowers/plans/2026-09-05-crm-contract-generate-word.md b/docs/superpowers/plans/2026-09-05-crm-contract-generate-word.md
new file mode 100644
index 000000000..3c252a1ed
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-05-crm-contract-generate-word.md
@@ -0,0 +1,565 @@
+# 合同管理打印弹窗「生成 Word」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:** 在合同管理打印弹窗增加「生成 Word」,将当前富文本导出为可编辑 `.docx`,便于改完后发给客户。
+
+**Architecture:** 与「生成 PDF」同源:取打印弹窗当前 HTML(占位符刷新 + 红章已由弹窗开关处理),前端预处理水印/Logo/绝对 URL 后,用 `@turbodocx/html-to-docx` 转 DOCX 并 `file-saver` 下载。公共逻辑集中在 `contract-html-to-docx.ts`,两处弹窗只接线。
+
+**Tech Stack:** Vue 3 + TypeScript;`@turbodocx/html-to-docx`;`file-saver`;`node:test` + `pnpm exec tsx --test`
+
+**Spec:** `docs/superpowers/specs/2026-09-05-crm-contract-generate-word-design.md`
+
+**注意:** 工作区可能有无关未提交改动。只改本计划列出的文件,提交时勿夹带无关 diff。
+
+---
+
+## 文件结构
+
+| 操作 | 文件 | 职责 |
+|------|------|------|
+| Modify | `web-admin/package.json`(及 lockfile) | 增加 `@turbodocx/html-to-docx` |
+| Create | `web-admin/src/utils/crm/contract-html-to-docx.ts` | 供方编码、去 Logo、绝对 URL、SX 水印、文件名、生成 Blob |
+| Create | `web-admin/src/utils/crm/contract-html-to-docx.test.ts` | 纯函数单测 |
+| Modify | `web-admin/src/views/crm/contract/components/ContractDetailDialog.vue` | 底栏按钮 + handler |
+| Modify | `web-admin/src/views/crm/contract/detail.vue` | 底栏按钮 + handler |
+
+后端:不改。报价单等其它模块:不做。
+
+---
+
+### Task 1: 安装依赖
+
+**Files:**
+- Modify: `web-admin/package.json`
+- Modify: `web-admin/pnpm-lock.yaml`(由 pnpm 生成)
+
+- [ ] **Step 1: 安装包**
+
+```bash
+cd web-admin && pnpm add @turbodocx/html-to-docx
+```
+
+Expected: `package.json` dependencies 出现 `@turbodocx/html-to-docx`;安装成功无 error。
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add web-admin/package.json web-admin/pnpm-lock.yaml
+git commit -m "$(cat <<'EOF'
+chore(web-admin): add @turbodocx/html-to-docx for contract Word export
+
+EOF
+)"
+```
+
+---
+
+### Task 2: HTML 预处理纯函数(TDD)
+
+**Files:**
+- Create: `web-admin/src/utils/crm/contract-html-to-docx.test.ts`
+- Create: `web-admin/src/utils/crm/contract-html-to-docx.ts`
+
+- [ ] **Step 1: 写失败单测**
+
+创建 `web-admin/src/utils/crm/contract-html-to-docx.test.ts`:
+
+```typescript
+import assert from 'node:assert/strict'
+import { describe, it } from 'node:test'
+import {
+ buildContractDocxFileName,
+ injectSxWatermarkHtml,
+ parseContractSupplierCode,
+ prepareContractHtmlForDocx,
+ stripContractLogoHtml,
+ toAbsoluteAssetUrls
+} from './contract-html-to-docx'
+
+describe('parseContractSupplierCode', () => {
+ it('parses supplier code from contractNo', () => {
+ assert.equal(parseContractSupplierCode('SX/X-JT20250926002'), 'SX')
+ assert.equal(parseContractSupplierCode('YD/X-JT20250926001'), 'YD')
+ })
+
+ it('defaults to SX when missing or unmatched', () => {
+ assert.equal(parseContractSupplierCode(undefined), 'SX')
+ assert.equal(parseContractSupplierCode(''), 'SX')
+ assert.equal(parseContractSupplierCode('NO-MATCH'), 'SX')
+ })
+})
+
+describe('stripContractLogoHtml', () => {
+ it('removes logo img and logo-only wrapper div', () => {
+ const html =
+ '

正文
'
+ const out = stripContractLogoHtml(html)
+ assert.equal(out.includes('logo.gif'), false)
+ assert.equal(out.includes('正文'), true)
+ assert.equal(out.includes('watermask.jpg'), true)
+ })
+})
+
+describe('toAbsoluteAssetUrls', () => {
+ it('prefixes root-relative src with origin', () => {
+ const out = toAbsoluteAssetUrls(
+ '
',
+ 'http://localhost:3006'
+ )
+ assert.equal(out.includes('src="http://localhost:3006/seals/a.png"'), true)
+ assert.equal(out.includes('src="https://cdn.example/x.png"'), true)
+ })
+})
+
+describe('injectSxWatermarkHtml', () => {
+ it('prepends watermark image with absolute url', () => {
+ const out = injectSxWatermarkHtml('合同
', 'http://localhost:3006/watermask.jpg')
+ assert.equal(out.includes('http://localhost:3006/watermask.jpg'), true)
+ assert.equal(out.includes('alt="水印"'), true)
+ assert.equal(out.includes('合同
'), true)
+ })
+})
+
+describe('prepareContractHtmlForDocx', () => {
+ it('adds SX watermark and keeps logo', () => {
+ const out = prepareContractHtmlForDocx({
+ html: '正文
',
+ contractNo: 'SX/X-JT20250926002',
+ origin: 'http://localhost:3006'
+ })
+ assert.equal(out.includes('watermask.jpg'), true)
+ assert.equal(out.includes('logo.gif'), true)
+ assert.equal(out.includes('http://localhost:3006/logo.gif'), true)
+ })
+
+ it('strips logo and skips watermark for non-SX', () => {
+ const out = prepareContractHtmlForDocx({
+ html: '正文
',
+ contractNo: 'YD/X-JT20250926001',
+ origin: 'http://localhost:3006'
+ })
+ assert.equal(out.includes('watermask'), false)
+ assert.equal(out.includes('logo.gif'), false)
+ assert.equal(out.includes('正文'), true)
+ })
+})
+
+describe('buildContractDocxFileName', () => {
+ it('uses contract name and sanitizes illegal chars', () => {
+ assert.equal(buildContractDocxFileName('甲/乙合同', 'SX/X-JT1'), '甲-乙合同合同.docx')
+ })
+
+ it('falls back to contractNo', () => {
+ assert.equal(buildContractDocxFileName('', 'SX/X-JT1'), 'SX-X-JT1合同.docx')
+ })
+})
+```
+
+- [ ] **Step 2: 跑测确认失败**
+
+```bash
+cd web-admin && pnpm exec tsx --test src/utils/crm/contract-html-to-docx.test.ts
+```
+
+Expected: FAIL(模块不存在 / 导出未定义)
+
+- [ ] **Step 3: 实现纯函数**
+
+创建 `web-admin/src/utils/crm/contract-html-to-docx.ts`(本步先实现测试覆盖的导出;`generateContractDocxBlob` 可先占位或放 Task 3):
+
+```typescript
+/** 合同打印弹窗:HTML → DOCX 预处理与生成 */
+
+export function parseContractSupplierCode(contractNo?: string | null): string {
+ if (!contractNo) return 'SX'
+ const match = String(contractNo).match(/^([A-Z]+)\/X-JT/)
+ return match?.[1] || 'SX'
+}
+
+export function stripContractLogoHtml(html: string): string {
+ let result = html.replace(/]*>\s*
![]()
]*(?:logo\.gif|logo)[^>]*>[\s\S]*?<\/div>/gi, (match) => {
+ if (match.includes('logo.gif') || match.includes('/logo') || (match.includes('logo') && !match.includes('watermask'))) {
+ return ''
+ }
+ return match
+ })
+ result = result.replace(/
![]()
]*(?:logo\.gif|logo)[^>]*(?![^<]*watermask)[^>]*>/gi, '')
+ return result
+}
+
+export function toAbsoluteAssetUrls(html: string, origin: string): string {
+ const base = origin.replace(/\/$/, '')
+ return html.replace(/\bsrc=(["'])(\/[^"']+)\1/gi, (_m, quote: string, path: string) => {
+ return `src=${quote}${base}${path}${quote}`
+ })
+}
+
+export function injectSxWatermarkHtml(html: string, watermarkUrl: string): string {
+ const watermarkBlock = `
+

+
`
+ return `${watermarkBlock}${html}`
+}
+
+export function prepareContractHtmlForDocx(options: {
+ html: string
+ contractNo?: string | null
+ origin: string
+}): string {
+ const origin = options.origin.replace(/\/$/, '')
+ const supplierCode = parseContractSupplierCode(options.contractNo)
+ const isSX = supplierCode === 'SX'
+
+ let html = options.html || ''
+ if (!isSX) {
+ html = stripContractLogoHtml(html)
+ }
+
+ html = toAbsoluteAssetUrls(html, origin)
+
+ if (isSX) {
+ html = injectSxWatermarkHtml(html, `${origin}/watermask.jpg`)
+ }
+
+ return html
+}
+
+export function buildContractDocxFileName(
+ contractName?: string | null,
+ contractNo?: string | null
+): string {
+ const raw = (contractName && String(contractName).trim()) || (contractNo && String(contractNo).trim()) || '合同'
+ const safe = raw.replace(/[/\\:*?"<>|]/g, '-')
+ return `${safe}合同.docx`
+}
+```
+
+- [ ] **Step 4: 跑测确认通过**
+
+```bash
+cd web-admin && pnpm exec tsx --test src/utils/crm/contract-html-to-docx.test.ts
+```
+
+Expected: PASS(全部用例)
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add web-admin/src/utils/crm/contract-html-to-docx.ts web-admin/src/utils/crm/contract-html-to-docx.test.ts
+git commit -m "$(cat <<'EOF'
+feat(web-admin): add contract HTML prep helpers for Word export
+
+EOF
+)"
+```
+
+---
+
+### Task 3: 生成 DOCX Blob
+
+**Files:**
+- Modify: `web-admin/src/utils/crm/contract-html-to-docx.ts`
+
+- [ ] **Step 1: 追加 `generateContractDocxBlob`**
+
+在 `contract-html-to-docx.ts` 末尾增加:
+
+```typescript
+import HTMLtoDOCX from '@turbodocx/html-to-docx'
+
+const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+
+/**
+ * 将已预处理的合同 HTML 转为 DOCX Blob(浏览器下载用)。
+ * 调用方须先执行 prepareContractHtmlForDocx。
+ */
+export async function generateContractDocxBlob(
+ preparedHtml: string,
+ title?: string
+): Promise
{
+ const fullHtml = `${preparedHtml}`
+ const result = await HTMLtoDOCX(fullHtml, null, {
+ orientation: 'portrait',
+ title: title || '合同',
+ margins: {
+ top: 720,
+ right: 720,
+ bottom: 720,
+ left: 720
+ }
+ })
+
+ if (result instanceof Blob) return result
+ return new Blob([result as ArrayBuffer], { type: DOCX_MIME })
+}
+```
+
+注意:若项目 eslint 要求 import 置顶,把 `import HTMLtoDOCX` 移到文件顶部。
+
+- [ ] **Step 2: 本地烟测(可选但推荐)**
+
+在 Node 下快速验证函数可调用(不要求生成完整业务 HTML):
+
+```bash
+cd web-admin && pnpm exec tsx -e "
+import { prepareContractHtmlForDocx, generateContractDocxBlob } from './src/utils/crm/contract-html-to-docx.ts'
+const html = prepareContractHtmlForDocx({ html: '测试合同
', contractNo: 'SX/X-JT1', origin: 'http://localhost:3006' })
+const blob = await generateContractDocxBlob(html, '测试')
+console.log(blob?.constructor?.name || typeof blob, blob?.size || (blob instanceof ArrayBuffer ? blob.byteLength : 'n/a'))
+"
+```
+
+Expected: 输出类似 `Blob <非零 size>` 或在 Node 下为 ArrayBuffer 再被包成 Blob;无 throw。若库在 Node 缺 polyfill 失败,可跳过本步,改到浏览器 UI 验证(Task 4/5)。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add web-admin/src/utils/crm/contract-html-to-docx.ts
+git commit -m "$(cat <<'EOF'
+feat(web-admin): generate contract DOCX blob from prepared HTML
+
+EOF
+)"
+```
+
+---
+
+### Task 4: 接线 ContractDetailDialog
+
+**Files:**
+- Modify: `web-admin/src/views/crm/contract/components/ContractDetailDialog.vue`
+
+- [ ] **Step 1: 底栏增加「生成 Word」按钮**
+
+在「生成 PDF」按钮旁(打印弹窗 footer,约 884–886 行)增加描边按钮(保持工具栏仅 PDF 一个 `type="primary"` / `btn-main`):
+
+```vue
+
+ 生成 Word
+
+
+ 生成 PDF
+
+```
+
+`Document` 已在该文件 icons 导入列表中,无需新增。
+
+- [ ] **Step 2: 增加 import、状态与 handler**
+
+在 script 中增加:
+
+```typescript
+import FileSaver from 'file-saver'
+import {
+ buildContractDocxFileName,
+ generateContractDocxBlob,
+ prepareContractHtmlForDocx
+} from '@/utils/crm/contract-html-to-docx'
+```
+
+在 `pdfGenerating` 旁:
+
+```typescript
+const wordGenerating = ref(false)
+```
+
+在 `handleGeneratePdfFromDialog` 附近新增(复用 PDF 的占位符刷新思路;红章已在 `contractContent`):
+
+```typescript
+/** 从打印弹窗生成 Word(前端 HTML→DOCX) */
+const handleGenerateWordFromDialog = async () => {
+ if (!detailContract.value?.id) {
+ $modal.msgError('合同信息不存在')
+ return
+ }
+
+ wordGenerating.value = true
+ try {
+ $modal.loading('正在生成 Word...')
+
+ let htmlContent = contractContent.value
+ const res = await getContractDetail(detailContract.value.id)
+ if (res?.data) {
+ htmlContent = replacePlaceholders(htmlContent, res.data)
+ contractContent.value = htmlContent
+ }
+
+ const prepared = prepareContractHtmlForDocx({
+ html: htmlContent,
+ contractNo: detailContract.value.contractNo,
+ origin: window.location.origin
+ })
+
+ const blob = await generateContractDocxBlob(
+ prepared,
+ detailContract.value.contractName || detailContract.value.contractNo
+ )
+ const fileName = buildContractDocxFileName(
+ detailContract.value.contractName,
+ detailContract.value.contractNo
+ )
+ FileSaver.saveAs(blob, fileName)
+
+ $modal.closeLoading()
+ $modal.msgSuccess('Word 生成成功')
+ } catch (error) {
+ console.error('生成 Word 失败:', error)
+ $modal.closeLoading()
+ $modal.msgError('生成 Word 失败,请重试')
+ } finally {
+ wordGenerating.value = false
+ }
+}
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add web-admin/src/views/crm/contract/components/ContractDetailDialog.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): add Generate Word on contract print dialog
+
+EOF
+)"
+```
+
+---
+
+### Task 5: 接线 detail.vue
+
+**Files:**
+- Modify: `web-admin/src/views/crm/contract/detail.vue`
+
+- [ ] **Step 1: 底栏增加按钮**
+
+在「生成PDF」旁(约 1114 行)增加:
+
+```vue
+
+ 生成 Word
+
+ 生成PDF
+```
+
+(`Document` 已导入;本页 footer 风格与 Dialog 略有差异,保持本页现有按钮写法即可。)
+
+- [ ] **Step 2: import、状态与 handler**
+
+```typescript
+import FileSaver from 'file-saver'
+import {
+ buildContractDocxFileName,
+ generateContractDocxBlob,
+ prepareContractHtmlForDocx
+} from '@/utils/crm/contract-html-to-docx'
+```
+
+```typescript
+const wordGenerating = ref(false)
+```
+
+对齐 `handleGeneratePdfFile` 的占位符刷新(使用本文件已有的 `getContract` / `replacePlaceholders` / `contract`):
+
+```typescript
+/** 从打印弹窗生成 Word */
+const handleGenerateWordFile = async () => {
+ if (!contract.value?.id) {
+ $modal.msgError('合同信息不存在')
+ return
+ }
+
+ wordGenerating.value = true
+ try {
+ $modal.loading('正在生成 Word...')
+
+ let htmlContent = contractContent.value
+ const res = await getContract(contract.value.id)
+ if (res?.data) {
+ htmlContent = replacePlaceholders(htmlContent, res.data)
+ contractContent.value = htmlContent
+ }
+
+ const prepared = prepareContractHtmlForDocx({
+ html: htmlContent,
+ contractNo: contract.value.contractNo,
+ origin: window.location.origin
+ })
+
+ const blob = await generateContractDocxBlob(
+ prepared,
+ contract.value.contractName || contract.value.contractNo
+ )
+ FileSaver.saveAs(
+ blob,
+ buildContractDocxFileName(contract.value.contractName, contract.value.contractNo)
+ )
+
+ $modal.closeLoading()
+ $modal.msgSuccess('Word 生成成功')
+ } catch (error) {
+ console.error('生成 Word 失败:', error)
+ $modal.closeLoading()
+ $modal.msgError('生成 Word 失败,请重试')
+ } finally {
+ wordGenerating.value = false
+ }
+}
+```
+
+若本页获取详情 API 名不是 `getContract`,以文件顶部实际 import 为准(已有 `getContract`)。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add web-admin/src/views/crm/contract/detail.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): add Generate Word on contract detail print dialog
+
+EOF
+)"
+```
+
+---
+
+### Task 6: 人工验收
+
+**Files:** 无代码改动
+
+- [ ] **Step 1: 跑单测回归**
+
+```bash
+cd web-admin && pnpm exec tsx --test src/utils/crm/contract-html-to-docx.test.ts
+```
+
+Expected: PASS
+
+- [ ] **Step 2: 浏览器验收清单**
+
+在 `http://localhost:3006/#/crm/contract`(或门户进合同管理):
+
+| # | 操作 | 期望 |
+|---|------|------|
+| 1 | 打开合同 → 打印弹窗 | 底栏有「生成 Word」 |
+| 2 | 显示红章 → 生成 Word | `.docx` 可打开;含印章图;正文可编辑 |
+| 3 | 隐藏红章 → 再生成 | 无印章图 |
+| 4 | SX 合同号 | 含水印图 |
+| 5 | 非 SX 合同号 | 无水印;无 Logo |
+| 6 | 生成 PDF / 打印 | 行为与改前一致 |
+| 7 | (若有)详情页打印弹窗 | 同样有「生成 Word」且可用 |
+
+- [ ] **Step 3: 无需额外 commit**(若验收中修 bug,单独小提交并写清原因)
+
+---
+
+## Spec 覆盖自检
+
+| Spec 项 | 对应 Task |
+|---------|-----------|
+| 打印弹窗「生成 Word」、两处入口 | Task 4、5 |
+| 前端 HTML→DOCX、`@turbodocx/html-to-docx` | Task 1、3 |
+| 红章跟弹窗开关 | Task 4/5 使用当前 `contractContent` |
+| SX 水印 / 非 SX 去 Logo | Task 2 `prepareContractHtmlForDocx` |
+| 文件名规则 | Task 2 `buildContractDocxFileName` |
+| 失败提示、不影响 PDF | Task 4/5 catch + 独立 `wordGenerating` |
+| 非目标:后端/其它模块 | 未列入任务 |