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