11 KiB
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替换为Vditorapp/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: 写失败检查(确认旧编辑器仍被引用)
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
{
"dependencies": {
"vditor": "^3.10.0"
}
}
并移除:
{
"dependencies": {
"md-editor-v3": "..."
}
}
- Step 4: 安装依赖并校验 lockfile 变更
Run: bun install
Expected: vditor 安装成功,依赖树无冲突错误
- Step 5: 提交
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 双向同步)
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: 写最小实现(客户端初始化 + 销毁 + 双向同步)
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: 提交
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 策略)
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: 增加断点配置逻辑
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: 提交
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,失败提示)
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: 实现上传适配
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: 提交
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: 写失败检查(页面仍依赖统一编辑器组件)
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: 提交
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: 写失败检查(仍存在旧编辑器运行时引用)
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: 提交
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
- Spec coverage: 已覆盖范围边界(仅可编辑输入)、PC=A 与移动端=B 策略、上传链路、双向同步、生命周期、验收标准。
- Placeholder scan: 无 TBD/TODO/“后续补充”占位词;每个任务均给出命令与预期结果。
- Type consistency: 全程统一使用
PostBodyMarkdownEditor、modelValue/update:modelValue、/api/file/upload命名,前后一致。