在 Vue 3.5 中优雅地集成 wangEditor,并定制“AI 工具”下拉菜单(总结/润色/翻译)

在内容创作场景中,富文本编辑器与 AI 能力的结合高效且轻量化的组合。这篇文章以 wangEditorVue 3 中的集成为基础,分享一套低耦合、高可扩展性的方案,核心目标是:如何优雅地新增一个包含"AI 总结/润色/翻译"功能的自定义下拉菜单。

本文实现思路清晰,不仅关注代码的实现,更侧重于架构的解耦与设计模式的应用。

一、架构设计:解耦与单向数据流

高质量的代码意味着清晰的边界。为了避免将业务逻辑直接"污染"到 UI 组件(即 wangEditor 菜单)中,我们采用了经典的 事件总线(Event Bus) 模式进行分层解耦:

  1. 视图层 (NoteEditor.vue):负责编辑器的实例化、生命周期管理以及对最终 AI 服务的调用。
  2. 工具层 (definedMenu.js):仅负责定义 UI 菜单的外观和点击事件的派发。
  3. 核心交互 :菜单被点击后,只派发一个统一的 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 事件后:

  1. 获取文本:优先获取用户选中的文本,如果未选中,则获取全文。
  2. 调用服务 :根据 action 调用后端 AI 服务。
  3. 结果回写:将 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 专注于事件响应与业务调度------这正是现代前端工程实践所推崇的解耦之道。

相关推荐
执沐3 小时前
基于HTML 使用星辰拼出爱心,并附带闪烁+流星+点击生成流星
前端·html
atwednesday3 小时前
日志处理
javascript
#做一个清醒的人3 小时前
【electron6】Web Audio + AudioWorklet PCM 实时采集噪音和模拟调试
前端·javascript·electron·pcm
拉不动的猪4 小时前
图文引用打包时的常见情景解析
前端·javascript·后端
浩男孩4 小时前
🍀继分页器组件后,封装了个抽屉组件
前端
Dolphin_海豚4 小时前
@vue/reactivity
前端·vue.js·面试
该用户已不存在4 小时前
程序员的噩梦,祖传代码该怎么下手?
前端·后端
namehu4 小时前
前端性能优化之:图片缩放 🚀
前端·性能优化·微信小程序
rit84324994 小时前
ES6 箭头函数:告别 `this` 的困扰
开发语言·javascript·es6