
在内容创作场景中,富文本编辑器与 AI 能力的结合高效且轻量化的组合。这篇文章以 wangEditor 在 Vue 3 中的集成为基础,分享一套低耦合、高可扩展性的方案,核心目标是:如何优雅地新增一个包含"AI 总结/润色/翻译"功能的自定义下拉菜单。
本文实现思路清晰,不仅关注代码的实现,更侧重于架构的解耦与设计模式的应用。
一、架构设计:解耦与单向数据流
高质量的代码意味着清晰的边界。为了避免将业务逻辑直接"污染"到 UI 组件(即 wangEditor 菜单)中,我们采用了经典的 事件总线(Event Bus) 模式进行分层解耦:
- 视图层 (
NoteEditor.vue
):负责编辑器的实例化、生命周期管理以及对最终 AI 服务的调用。 - 工具层 (
definedMenu.js
):仅负责定义 UI 菜单的外观和点击事件的派发。 - 核心交互 :菜单被点击后,只派发一个统一的
askAiClick
事件,不关心具体业务逻辑。视图层监听该事件,随后执行业务调用(如 AI 服务)。
💡 幂等性注册的必要性: 在 Vue 3 或 Vite 等现代构建工具的 HMR (热模块替换) 环境下,模块代码可能会被多次执行。为防止自定义菜单被重复注册导致异常,我们引入了全局标记,确保注册过程只发生一次。
二、视图层 (NoteEditor.vue
):集成与事件响应
NoteEditor.vue
是编辑器的主战场。这里的关键在于将自定义菜单键 myselectAiBar
优雅地插入到工具栏配置中,并管理好 AI 工具的生命周期。
1. 注入自定义菜单键与初始化 AI 工具
在编辑器初始化阶段,我们需要通过 insertKeys
将我们的自定义菜单键插入到工具栏的指定位置(这里是第一个,index: 0
)。同时,在 handleCreated
钩子中,我们将编辑器实例传递给 aiToolManager
进行初始化。
vue
// 工具栏配置:插入 AI 工具菜单
const toolbarConfig = {
excludeKeys: ['group-video'], // 排除默认菜单
insertKeys: {
index: 0, // 插入到最前面
keys: ['myselectAiBar'] // 自定义菜单键
}
}
// 编辑器创建完成
const handleCreated = (editor) => {
editorRef.value = editor // 记录实例
// 初始化 AI 工具管理器(重要)
aiToolManager.init(editor)
}
2. 监听事件与"即时反馈"占位
这是实现用户体验的关键一步。当用户点击 AI 菜单时,到 AI 服务返回结果之间存在延迟。为了提供"即时反馈",我们先插入一个**"处理中..."的占位文本**,让用户感知到操作已生效。
javascript
// 监听 AI 菜单点击事件,并插入占位提示
const handleAskAiClick = (e) => {
const detail = e?.detail || {}
const action = detail.value || '' // summary | polish | translate
const editor = editorRef.value
if (!editor) return
const actionLabelMap = { summary: '总结', polish: '润色', translate: '翻译' }
const label = actionLabelMap[action] || '处理'
// 立即插入反馈文本
editor.insertText(`【AI${label}处理中...】`)
// 💡 随后的 AI 服务调用逻辑将在下方"最小落点"部分详细说明
}
onMounted(() => {
// 全局监听事件
document.addEventListener('askAiClick', handleAskAiClick)
})
3. 生命周期清理:避免内存泄漏
在组件卸载时,必须及时清理编辑器实例、移除事件监听器以及销毁 AIToolManager
,这是保证应用健壮性的基本要求。
javascript
// 组件销毁前
onBeforeUnmount(() => {
// 移除全局事件监听
document.removeEventListener('askAiClick', handleAskAiClick)
const editor = editorRef.value
if (editor && !editor.destroyed) {
editor.destroy()
}
// 销毁 AI 工具管理器
aiToolManager.destroy()
})
三、工具层 (definedMenu.js
):定义与事件派发
definedMenu.js
的职责是纯粹的:定义 UI 菜单结构,并在点击时触发一个不带业务逻辑的事件。
1. 构造自定义菜单类 MyselectAiBar
我们继承 wangEditor 的菜单规范,实现 getPanelContentElem
方法来构建自定义下拉面板的 DOM 结构。
javascript
class MyselectAiBar {
constructor() {
this.title = 'AI 工具'
this.tag = 'button'
this.showDropPanel = true // 关键:显示下拉面板
}
// ... 其他 required 方法(略)
getPanelContentElem() {
const ul = document.createElement('ul')
ul.className = 'w-e-panel-my-list'
const items = [
{ label: 'AI 总结', value: 'summary' },
{ label: 'AI 润色', value: 'polish' },
{ label: 'AI 翻译', value: 'translate' },
]
// 构建 UI 列表并绑定事件(下一段重点说明)
// ...
return ul
}
}
2. 事件派发:解耦的核心
在列表项的点击事件中,我们不调用任何业务 API ,仅仅是构造一个携带 value
(即具体操作类型)的 CustomEvent
,并派发到全局 document
上。
javascript
// 在 getPanelContentElem 内部
items.forEach((item) => {
const li = document.createElement('li')
li.textContent = item.label
li.addEventListener('click', () => {
// 核心:仅派发事件,不执行业务逻辑
const event = new CustomEvent('askAiClick', {
detail: {
value: item.value, // summary | polish | translate
type: 'toolbar',
},
})
document.dispatchEvent(event)
})
ul.appendChild(li)
})
3. 幂等性注册与管理器封装
为了应对 HMR 和提供更友好的 API,我们将注册过程封装在 registerMenusOnce
函数中,并通过 AIToolManager
类来集中管理注册和编辑器实例的引用。
javascript
// 注册模块(只执行一次)
function registerMenusOnce() {
// 利用全局标记避免 Vite/Webpack HMR 导致的重复注册
if (globalThis.__aiMenusRegistered) return
const module = {
menus: [/* myselectAiConf */],
}
Boot.registerModule(module)
globalThis.__aiMenusRegistered = true
}
export class AIToolManager {
init(editor) {
registerMenusOnce() // 确保注册
this.editor = editor
}
// ...
}
四、UI 细节优化:样式与可发现性
为了提升用户体验,我们对 AI 菜单按钮和下拉面板进行了克制且高效的样式定制 。目标是:高亮、可发现、不突兀。
- 使用
background-color: #FFE0B2
(柔和的橙色系)对 AI 按钮进行高亮,以降低用户的发现成本。 - 通过
:deep
选择器为下拉面板添加圆角、阴影和 Hover 效果,使其看起来更现代、更有质感。
css
/* 高亮 AI 工具按钮 */
:deep(button[data-menu-key="myselectAiBar"]) {
background-color: #FFE0B2 !important; /* 柔和背景 */
/* ... 更多样式,如圆角、边框、字体等 */
}
/* 下拉面板美化 */
:deep(.w-e-panel),
:deep(.w-e-drop-panel) {
border-radius: 8px !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08) !important; /* 现代阴影 */
/* ... */
}
五、最小落点:对接真实 AI 服务(可选扩展)
至此,我们的前端架构已完全解耦。最后一步是实现 AI 服务的真实调用。这发生在 NoteEditor.vue
内部,在接收到 askAiClick
事件后:
- 获取文本:优先获取用户选中的文本,如果未选中,则获取全文。
- 调用服务 :根据
action
调用后端 AI 服务。 - 结果回写:将 AI 返回的结果插入到编辑器中(并替换掉之前插入的"处理中"占位文本)。
javascript
// 增强后的 handleAskAiClick 内部逻辑(示意)
const editor = editorRef.value
const text = editor?.getSelectionText?.() || editor?.getText() || '' // 获取选区或全文
const action = detail.value
const label = { summary: '总结', polish: '润色', translate: '翻译' }[action] || '处理'
// 1. 插入占位提示(已完成)
// editor.insertText(`【AI${label}处理中...】`)
// 2. 调用你的 AI 服务 (推荐封装到独立的 service/api 层)
const data = await aiService.process({ action, text })
// 3. 插入结果 (可以先删除占位符,再插入结果)
editor.insertText(`\n【AI${label}完成】\n${data}\n`)
多模态Ai项目全流程开发中,从需求分析,到Ui设计,前后端开发,部署上线,感兴趣打开链接(带项目功能演示) ,多模态AI项目开发中...
总结: 通过事件驱动和清晰的分层,我们不仅成功地在 Vue 3 集成了 wangEditor 并定制了 AI 功能菜单,更重要的是,我们构建了一个可维护、易扩展 的架构。definedMenu.js
专注于 UI,NoteEditor.vue
专注于事件响应与业务调度------这正是现代前端工程实践所推崇的解耦之道。