Browse Source
Record the task-by-task implementation plan for migrating editable markdown inputs to Vditor with desktop/mobile behavior and verification steps. Made-with: Cursormain
1 changed files with 375 additions and 0 deletions
@ -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…
Reference in new issue