Browse Source

docs(plan): add Vditor unification implementation plan

Record the task-by-task implementation plan for migrating editable markdown inputs to Vditor with desktop/mobile behavior and verification steps.

Made-with: Cursor
main
npmrun 2 weeks ago
parent
commit
770ac577d7
  1. 375
      docs/superpowers/plans/2026-04-24-vditor-unification-implementation-plan.md

375
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<Vditor | null>(null);
const hostEl = ref<HTMLElement | null>(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 "<PostBodyMarkdownEditor" app/pages/me/posts/new.vue app/pages/me/posts/[id].vue
```
Expected: 两个页面都命中,确认页面接入点统一。
- [ ] **Step 2: 启动开发环境并验证基础流程**
Run: `bun run dev`
Expected:
- 新建文章页可输入 Markdown、可保存
- 编辑文章页可回填并保存
- [ ] **Step 3: 移动端手测(浏览器设备模拟)**
Run: `bun run dev` + 浏览器 DevTools 移动设备模式
Expected:
- 默认编辑优先
- 工具栏为精简模式
- 可进入预览视图
- 软键盘/视口变化下输入区可持续编辑
- [ ] **Step 4: 执行相关自动化测试**
Run: `bun test app/components/PostBodyMarkdownEditor.vditor.test.ts`
Expected: PASS
- [ ] **Step 5: 提交**
```bash
git add app/pages/me/posts/new.vue app/pages/me/posts/[id].vue app/components/PostBodyMarkdownEditor.vditor.test.ts
git commit -m "test(editor): verify vditor behavior on post create/edit flows"
```
---
### Task 6: 清理旧编辑器引用并最终验收
**Files:**
- Modify: `app/components/PostBodyMarkdownEditor.vue`
- Modify: `package.json`
- [ ] **Step 1: 写失败检查(仍存在旧编辑器运行时引用)**
```bash
rg "md-editor-v3|MdEditor" app package.json
```
Expected: 若仍有命中则说明清理未完成。
- [ ] **Step 2: 清理所有旧编辑器运行时引用**
Run: `rg "md-editor-v3|MdEditor" app package.json`
Expected: 无运行时代码命中(仅历史文档可保留)
- [ ] **Step 3: 执行最小验收命令**
Run: `bun test app/components/PostBodyMarkdownEditor.vditor.test.ts`
Expected: PASS
- [ ] **Step 4: 手工闭环验收**
Run: `bun run dev`
Expected:
- PC 端:完整工具栏 + 可预览
- 移动端:编辑优先 + 精简工具栏 + 次级预览
- 图片上传成功插入,失败有提示
- 保存/回填链路正常
- [ ] **Step 5: 提交**
```bash
git add app/components/PostBodyMarkdownEditor.vue package.json bun.lock app/components/PostBodyMarkdownEditor.vditor.test.ts
git commit -m "refactor(markdown): fully unify editable markdown inputs on vditor"
```
---
## Self-Review
1. **Spec coverage:** 已覆盖范围边界(仅可编辑输入)、PC=A 与移动端=B 策略、上传链路、双向同步、生命周期、验收标准。
2. **Placeholder scan:** 无 TBD/TODO/“后续补充”占位词;每个任务均给出命令与预期结果。
3. **Type consistency:** 全程统一使用 `PostBodyMarkdownEditor`、`modelValue/update:modelValue`、`/api/file/upload` 命名,前后一致。
Loading…
Cancel
Save