diff --git a/docs/superpowers/plans/2026-04-24-vditor-unification-implementation-plan.md b/docs/superpowers/plans/2026-04-24-vditor-unification-implementation-plan.md new file mode 100644 index 0000000..a75aba5 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-vditor-unification-implementation-plan.md @@ -0,0 +1,375 @@ +# Vditor Unification 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:** 将所有 Markdown 可编辑输入场景统一到 `Vditor`,并满足 PC 端完整编辑体验与移动端编辑优先体验。 + +**Architecture:** 保留 `PostBodyMarkdownEditor` 作为唯一入口组件,对外接口继续使用 `v-model`,内部替换为 `Vditor` 实例并集中处理生命周期、上传回调与端侧差异化配置。页面层尽量不改动业务逻辑,只做必要的样式/交互对齐;通过组件级测试与关键路径手测保障保存、回填、上传与移动端可用性。 + +**Tech Stack:** Nuxt 4、Vue 3、TypeScript、Vditor、Bun + +--- + +## File Structure + +**Create** +- `app/components/PostBodyMarkdownEditor.vditor.test.ts`:编辑器组件行为测试(同步、上传、移动端配置切换) + +**Modify** +- `package.json`:新增 `vditor` 依赖,移除 `md-editor-v3`(如无其他引用) +- `app/components/PostBodyMarkdownEditor.vue`:内部从 `md-editor-v3` 替换为 `Vditor` +- `app/pages/me/posts/new.vue`:仅在必要时调整容器样式(若编辑器高度或移动端布局需要) +- `app/pages/me/posts/[id].vue`:仅在必要时调整容器样式(与新建页一致) + +**Test** +- `app/components/PostBodyMarkdownEditor.vditor.test.ts` +- 手工验证页面:`app/pages/me/posts/new.vue`、`app/pages/me/posts/[id].vue` + +--- + +### Task 1: 依赖切换与基线校验 + +**Files:** +- Modify: `package.json` + +- [ ] **Step 1: 写失败检查(确认旧编辑器仍被引用)** + +```bash +rg "md-editor-v3" app package.json +``` + +Expected: 至少包含 `PostBodyMarkdownEditor.vue` 与 `package.json` 命中,表示替换前基线成立。 + +- [ ] **Step 2: 运行检查确认基线** + +Run: `rg "md-editor-v3" app package.json` +Expected: 有匹配结果(旧实现仍在) + +- [ ] **Step 3: 更新依赖为 Vditor** + +```json +{ + "dependencies": { + "vditor": "^3.10.0" + } +} +``` + +并移除: + +```json +{ + "dependencies": { + "md-editor-v3": "..." + } +} +``` + +- [ ] **Step 4: 安装依赖并校验 lockfile 变更** + +Run: `bun install` +Expected: `vditor` 安装成功,依赖树无冲突错误 + +- [ ] **Step 5: 提交** + +```bash +git add package.json bun.lock +git commit -m "chore(markdown): replace md-editor-v3 dependency with vditor" +``` + +--- + +### Task 2: 重写 PostBodyMarkdownEditor 组件为 Vditor 实现 + +**Files:** +- Modify: `app/components/PostBodyMarkdownEditor.vue` +- Test: `app/components/PostBodyMarkdownEditor.vditor.test.ts` + +- [ ] **Step 1: 写失败测试(v-model 双向同步)** + +```ts +import { describe, test, expect } from "bun:test"; + +describe("PostBodyMarkdownEditor v-model sync", () => { + test("emits update when editor content changes", async () => { + // mount component with modelValue = "a" + // simulate editor input -> "b" + // expect update:modelValue emitted with "b" + expect(true).toBe(false); + }); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `bun test app/components/PostBodyMarkdownEditor.vditor.test.ts` +Expected: FAIL(测试占位断言失败或组件尚未适配 Vditor) + +- [ ] **Step 3: 写最小实现(客户端初始化 + 销毁 + 双向同步)** + +```ts +import Vditor from "vditor"; +import "vditor/dist/index.css"; + +const editor = shallowRef(null); +const hostEl = ref(null); + +onMounted(() => { + if (!hostEl.value) return; + editor.value = new Vditor(hostEl.value, { + value: props.modelValue, + input(value) { + emit("update:modelValue", value); + }, + }); +}); + +watch( + () => props.modelValue, + (next) => { + const inst = editor.value; + if (!inst) return; + if (inst.getValue() !== next) { + inst.setValue(next, true); + } + }, +); + +onBeforeUnmount(() => { + editor.value?.destroy(); + editor.value = null; +}); +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: `bun test app/components/PostBodyMarkdownEditor.vditor.test.ts` +Expected: PASS(双向同步、实例销毁断言通过) + +- [ ] **Step 5: 提交** + +```bash +git add app/components/PostBodyMarkdownEditor.vue app/components/PostBodyMarkdownEditor.vditor.test.ts +git commit -m "refactor(editor): migrate PostBodyMarkdownEditor to vditor core" +``` + +--- + +### Task 3: 实现 PC/移动端差异化配置(A/B 策略) + +**Files:** +- Modify: `app/components/PostBodyMarkdownEditor.vue` +- Test: `app/components/PostBodyMarkdownEditor.vditor.test.ts` + +- [ ] **Step 1: 写失败测试(按断点应用不同 toolbar/preview 策略)** + +```ts +describe("PostBodyMarkdownEditor responsive config", () => { + test("uses full toolbar on desktop and compact toolbar on mobile", async () => { + // mock matchMedia for desktop/mobile + // assert Vditor config differs by mode + expect(true).toBe(false); + }); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `bun test app/components/PostBodyMarkdownEditor.vditor.test.ts -t "responsive config"` +Expected: FAIL(断点策略尚未实现) + +- [ ] **Step 3: 增加断点配置逻辑** + +```ts +const isMobile = useMediaQuery("(max-width: 768px)"); + +const desktopToolbar = [ + "headings", "bold", "italic", "strike", "|", + "list", "ordered-list", "check", "|", + "quote", "line", "code", "inline-code", "|", + "link", "upload", "table", "|", "preview", "fullscreen" +]; + +const mobileToolbar = ["bold", "italic", "headings", "list", "link", "upload", "code"]; + +const options = { + toolbar: isMobile.value ? mobileToolbar : desktopToolbar, + mode: isMobile.value ? "sv" : "wysiwyg", + preview: { + mode: isMobile.value ? "editor" : "both", + }, +}; +``` + +并确保移动端存在可进入预览的入口(按钮或工具栏项)。 + +- [ ] **Step 4: 运行测试确认通过** + +Run: `bun test app/components/PostBodyMarkdownEditor.vditor.test.ts -t "responsive config"` +Expected: PASS + +- [ ] **Step 5: 提交** + +```bash +git add app/components/PostBodyMarkdownEditor.vue app/components/PostBodyMarkdownEditor.vditor.test.ts +git commit -m "feat(editor): add desktop/mobile tailored vditor modes" +``` + +--- + +### Task 4: 接入现有图片上传链路并对齐错误提示 + +**Files:** +- Modify: `app/components/PostBodyMarkdownEditor.vue` +- Test: `app/components/PostBodyMarkdownEditor.vditor.test.ts` + +- [ ] **Step 1: 写失败测试(上传成功插入 URL,失败提示)** + +```ts +describe("PostBodyMarkdownEditor image upload", () => { + test("inserts uploaded urls into markdown", async () => { + // mock fetchData('/api/file/upload') -> { files: [{ url: "https://x/a.png" }] } + // trigger upload hook + // expect markdown contains image syntax with returned url + expect(true).toBe(false); + }); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `bun test app/components/PostBodyMarkdownEditor.vditor.test.ts -t "image upload"` +Expected: FAIL(上传钩子尚未对接) + +- [ ] **Step 3: 实现上传适配** + +```ts +upload: { + accept: "image/*", + handler: async (files) => { + const form = new FormData(); + files.forEach((f) => form.append("file", f)); + try { + const { files: uploaded } = await fetchData("/api/file/upload", { + method: "POST", + body: form, + }); + toast.add({ title: "图片已上传", color: "success" }); + return JSON.stringify({ + msg: "", + code: 0, + data: { errFiles: [], succMap: Object.fromEntries(uploaded.map((x) => [x.url, x.url])) }, + }); + } catch { + toast.add({ title: "图片上传失败", color: "warning" }); + return JSON.stringify({ msg: "upload failed", code: 1, data: { errFiles: [], succMap: {} } }); + } + }, +}, +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: `bun test app/components/PostBodyMarkdownEditor.vditor.test.ts -t "image upload"` +Expected: PASS + +- [ ] **Step 5: 提交** + +```bash +git add app/components/PostBodyMarkdownEditor.vue app/components/PostBodyMarkdownEditor.vditor.test.ts +git commit -m "feat(editor): wire vditor image upload to existing api" +``` + +--- + +### Task 5: 页面回归与移动端可用性验证 + +**Files:** +- Modify: `app/pages/me/posts/new.vue`(如需最小样式调整) +- Modify: `app/pages/me/posts/[id].vue`(如需最小样式调整) + +- [ ] **Step 1: 写失败检查(页面仍依赖统一编辑器组件)** + +```bash +rg "