在富文本编辑器中封装实用的 AI 写作助手功能

将 AI 智能写作能力无缝集成到富文本编辑器中,是提升内容生产效率的关键。本文聚焦于 Vue 3 + wangEditor 技术栈,为您提供一套架构清晰、代码可即用 的实现方案。我们将重点展示如何通过分层解耦 的设计思想,构建一个功能完整、用户体验优秀的 AI 助手,并保留所有核心实现代码

一、架构核心:职责分离与事件驱动

为了确保系统的高可维护性和可扩展性,我们采用了清晰的分层架构

  1. 视图层 (NoteEditor.vue):编辑器宿主,负责 AI 弹窗的显示/隐藏、快捷键监听以及最终的 AI 服务调用与结果插入。
  2. 功能层 (AIWritingPopup.vue):弹窗组件,专注于用户输入、AI 操作类型选择和结果展示。
  3. 管理层 (AIToolManager):管理 AI 工具的生命周期和编辑器快捷键绑定。

核心交互模式是事件驱动 :编辑器操作(如快捷键触发)或弹窗操作(如点击"润色")最终都派发事件,由 NoteEditor.vue 统一响应和处理。

二、视图层:NoteEditor.vue 的集成与事件处理

NoteEditor.vue 是所有功能的汇聚点。它管理着编辑器实例,处理复杂的生命周期,以及响应来自不同来源的 AI 触发事件。

1. 核心状态与生命周期

使用 shallowRef 记录编辑器实例,并在组件销毁时进行彻底清理,防止内存泄漏。同时,在创建时初始化 AIToolManager

javascript 复制代码
// ai_multimodal_web/src/components/NoteEditor.vue (核心JS/Setup 部分)
import { shallowRef, ref, onMounted, onBeforeUnmount } from 'vue'
import { AIToolManager } from '@/utils/definedMenu'

const editorRef = shallowRef(null) // 编辑器实例引用
const aiPopupVisible = ref(false) // AI弹窗显示状态
const aiPopupPosition = ref({ x: 0, y: 0 }) // AI弹窗位置

// AI工具管理器
const aiToolManager = new AIToolManager()

// 编辑器创建完成
const handleCreated = (editor) => {
    editorRef.value = editor // 记录 editor 实例
    aiToolManager.init(editor)
}

// 组件销毁时,清理所有资源
onBeforeUnmount(() => {
    // 移除 AI 事件监听
    document.removeEventListener('askAiClick', handleAskAiClick)
    document.removeEventListener('keydown', handleGlobalKeydown) // 移除快捷键监听
    
    const editor = editorRef.value
    if (editor != null) editor.destroy()

    aiToolManager.destroy()
})

2. 智能快捷键系统

设计 Ctrl + Alt 组合键作为全局触发器。在全局监听中,通过 editor.isFocused() 检查焦点状态,并精确计算弹窗位置。

javascript 复制代码
// ai_multimodal_web/src/components/NoteEditor.vue (快捷键监听)

// 动态弹窗定位算法
const calculatePopupPosition = (selection) => {
    const range = selection.getRangeAt(0)
    const rect = range.getBoundingClientRect()
    // 计算弹窗位置(光标下方)
    let x = rect.left
    let y = rect.bottom + 10
    
    // 简单的边界检测和调整
    const popupWidth = 300 
    const viewportWidth = window.innerWidth 
    if (x + popupWidth > viewportWidth) {
        x = viewportWidth - popupWidth - 10
    }
    
    return { x: Math.max(10, x), y: Math.max(10, y) }
}

// 全局快捷键监听设计 (Ctrl + Alt)
const handleGlobalKeydown = (event) => {
    if (event.ctrlKey && event.altKey) {
        event.preventDefault()
        const editor = editorRef.value
        
        // 智能检测焦点
        if (editor && editor.isFocused()) {
            const selection = document.getSelection()
            if (selection.rangeCount > 0) {
                aiPopupPosition.value = calculatePopupPosition(selection)
                aiPopupVisible.value = true
            }
        }
    }
}

onMounted(() => {
    document.addEventListener('keydown', handleGlobalKeydown)
    // ... 其他监听
})

3. 结果插入与状态同步

响应弹窗发出的 insert-text 事件,确保 AI 结果能可靠地被插入到编辑器中。

javascript 复制代码
// ai_multimodal_web/src/components/NoteEditor.vue (事件处理器)

// 处理 AI 弹窗关闭事件
const handleAiPopupClose = () => {
    aiPopupVisible.value = false
}

// 智能文本插入算法
const handleAIInsertText = (text) => {
    const editor = editorRef.value
    if (editor && text) {
        try {
            editor.focus() // 强制聚焦
            // 异步插入处理,确保时序正确
            setTimeout(() => {
                editor.insertText(text)
                // 插入后,关闭弹窗
                aiPopupVisible.value = false
            }, 50)
        } catch (error) {
            console.error('插入文本失败:', error)
        }
    }
}

三、功能层:AIWritingPopup.vue 弹窗实现

AIWritingPopup.vue 是一个模态组件,负责用户在选择操作和输入文本时的所有交互细节。

vue 复制代码
<template>
  <div v-show="visible" class="ai-writing-popup" :style="popupStyle">
    <div class="popup-content">
      
      <div class="dropdown-menu" v-show="showDropdown">
        <div 
          v-for="action in quickActions" 
          :key="action.value" 
          class="dropdown-item"
          @click="handleActionSelect(action.value)"
        >
          {{ action.label }}
        </div>
      </div>

      <div class="input-section" v-show="!showDropdown || aiResult">
        <input 
          ref="inputRef"
          type="text" 
          v-model="inputText" 
          placeholder="请输入内容或指令..."
          @keyup.enter="handleSubmit"
        />
        <button @click="handleSubmit" :disabled="isLoading">
          {{ isLoading ? '处理中...' : '发送' }}
        </button>
      </div>

      <div v-if="aiResult" class="result-section">
        <p>{{ aiResult }}</p>
        <button @click="handleInsert">插入</button>
        <button @click="handleClose">关闭</button>
      </div>

    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, nextTick } from 'vue'

const props = defineProps({
  visible: Boolean,
  position: Object,
  editor: Object // 接收编辑器实例,用于上下文获取
})

const emit = defineEmits(['close', 'insert-text', 'ai-action']) // insert-text 由 NoteEditor 响应

const inputRef = ref(null)
const inputText = ref('')
const isLoading = ref(false)
const aiResult = ref('')
const selectedAction = ref('continue') // 默认操作
const showDropdown = ref(true) // 是否显示操作菜单

// AI操作配置(可扩展)
const quickActions = [
    { label: 'AI 续写', value: 'continue' },
    { label: 'AI 润色', value: 'polish' },
    { label: 'AI 总结', value: 'summary' },
    { label: 'AI 翻译', value: 'translate' }
]

// 弹窗样式计算
const popupStyle = computed(() => ({
  left: `${props.position.x}px`,
  top: `${props.position.y}px`
}))

// 监听弹窗显示,自动聚焦输入框
watch(() => props.visible, (newVal) => {
  if (newVal) {
    // 重置状态
    inputText.value = props.editor?.getSelectionText() || '' // 自动带入选中内容
    aiResult.value = ''
    showDropdown.value = true
    nextTick(() => {
        inputRef.value?.focus()
    })
  }
})

// 处理操作选择
const handleActionSelect = (action) => {
    selectedAction.value = action
    showDropdown.value = false // 隐藏菜单
    nextTick(() => inputRef.value?.focus())
}

// 提交 AI 请求
const handleSubmit = async () => {
    if (!inputText.value.trim() || isLoading.value) return

    isLoading.value = true
    showDropdown.value = false
    aiResult.value = ''

    // 1. 触发 AI 动作事件 (由 NoteEditor 处理服务调用)
    emit('ai-action', {
        action: selectedAction.value,
        text: inputText.value,
    })
    
    // 2. 模拟/实际 API 调用 (此处为模拟,实际应调用真实的 AI API)
    await new Promise(resolve => setTimeout(resolve, 1500))
    
    // 3. 假设从 API 获得结果
    aiResult.value = `[AI ${selectedAction.value} 结果] 这是根据您的 "${inputText.value}" 生成的新内容。`

    isLoading.value = false
}

// 插入结果到编辑器
const handleInsert = () => {
    if (aiResult.value) {
        emit('insert-text', aiResult.value)
    }
    handleClose()
}

// 关闭弹窗
const handleClose = () => {
    emit('close')
    // 重置内部状态
    isLoading.value = false
    aiResult.value = ''
    showDropdown.value = true
}
</script>

<style scoped>
/* 样式设计(仅保留核心) */
.ai-writing-popup {
    position: fixed;
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    min-width: 300px;
    z-index: 9999;
    padding: 12px;
}
.dropdown-menu {
    border: 1px solid #e0e0e0;
    border-radius: 4px;
    margin-bottom: 8px;
}
.dropdown-item {
    padding: 8px 12px;
    cursor: pointer;
    transition: background-color 0.2s;
}
.dropdown-item:hover {
    background-color: #f0f0f0;
}
.input-section {
    display: flex;
    gap: 8px;
    margin-bottom: 8px;
}
.input-section input {
    flex-grow: 1;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
}
.result-section {
    border-top: 1px solid #eee;
    padding-top: 8px;
}
</style>

四、管理层:AIToolManager 与菜单注册

为了在 wangEditor 工具栏中添加一个持久化的 AI 按钮(作为备用触发方式),我们需要一个专门的管理器来处理菜单的注册和生命周期。

javascript 复制代码
// ai_multimodal_web/src/utils/definedMenu.js (AI 工具菜单定义与管理器)
import { Boot } from '@wangeditor/editor'

// 1. 定义自定义菜单栏的 class
class MyselectAiBar {
    constructor() {
        this.title = 'AI 工具'
        this.tag = 'button'
        this.showDropPanel = true
    }
    getValue() { return '' }
    isActive() { return false }
    isDisabled() { return false }
    exec() {
        // do nothing - 仅展示下拉面板
    }
    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' },
        ]

        items.forEach((item) => {
            const li = document.createElement('li')
            li.textContent = item.label
            // 关键:点击菜单项时,派发统一事件
            li.addEventListener('click', () => {
                const event = new CustomEvent('askAiClick', {
                    detail: { value: item.value, type: 'toolbar' },
                })
                document.dispatchEvent(event)
            })
            ul.appendChild(li)
        })
        return ul
    }
}

const myselectAiConf = {
    key: 'myselectAiBar',
    factory() {
        return new MyselectAiBar()
    },
}

// 2. 幂等性注册函数(只注册一次,防止 HMR 导致重复)
function registerMenusOnce() {
    if (globalThis.__aiMenusRegistered) return
    const module = { menus: [myselectAiConf] }
    Boot.registerModule(module)
    globalThis.__aiMenusRegistered = true
}

// 3. AIToolManager 封装
export class AIToolManager {
    constructor() {
        this.editor = null
    }

    // 初始化:注册菜单并记录 editor
    init(editor) {
        registerMenusOnce()
        this.editor = editor
        // 可以在这里绑定其他快捷键或编辑器相关事件
    }

    // 销毁:清理引用
    destroy() {
        this.editor = null
    }
}

五、业务调用:NoteEditor.vue 中的服务调度

最后,在 NoteEditor.vue 中,我们需要监听 AI 弹窗发出的 ai-action 事件,并进行真实的 AI 服务调用。

javascript 复制代码
// ai_multimodal_web/src/components/NoteEditor.vue (新增 ai-action 事件处理器)

// 假设有一个封装好的 AI 服务
import aiService from '@/api/aiService' 

const handleAiAction = async (data) => {
    const editor = editorRef.value
    if (!editor) return

    const { action, text } = data
    
    // 1. 插入占位符(可选,但推荐提供即时反馈)
    const actionLabelMap = { summary: '总结', polish: '润色', translate: '翻译' }
    const label = actionLabelMap[action] || '处理'
    editor.insertText(`【AI${label}处理中...】`)

    try {
        // 2. 调用真实的后端服务
        const { resultText } = await aiService.process({ action, text }) 
        
        // 3. 替换占位符并插入结果
        // (复杂的替换逻辑此处省略,可直接插入新内容)
        editor.insertText(`\n【AI${label}完成】\n${resultText}\n`) 

        // 4. 关闭弹窗(如果弹窗未自动关闭)
        aiPopupVisible.value = false

    } catch (error) {
        console.error("AI 服务调用失败:", error)
        editor.insertText(`\n【AI${label}失败】请检查网络或重试。\n`)
    }
}
// ... 确保在 <template> 中将 handleAiAction 绑定到 AIWritingPopup

好的,遵照您的要求,我将总结内容进一步精炼,只保留最核心的技术亮点和价值点。


最终总结:技术价值与核心亮点

该 AI 写作助手方案成功实现了 Vue 3 与 wangEditor 的深度集成和高可扩展性。其核心技术价值和亮点在于:

  1. 架构与解耦(高可维护性)

    • 职责分离:严格区分 UI 交互(弹窗)、编辑器管理(宿主)和业务逻辑(AI 服务调用)。
    • 事件驱动核心:所有操作(快捷键、工具栏)统一派发事件,核心功能与触发方式完全解耦,确保系统的高度灵活和可扩展性。
  2. 极致的用户体验(高可用性)

    • 智能快捷键 :实现 Ctrl + Alt 全局监听,并结合焦点判断,提供最高效的触发机制。
    • 动态定位:利用 Selection API 和边界检测,确保 AI 弹窗精确、人道地跟随用户光标出现。
  3. 工程实践(高可靠性)

    • 生命周期完整:通过 Vue 钩子对全局事件和编辑器实例进行彻底清理,杜绝内存泄漏和重复注册。
    • 原子化插入:通过强制聚焦和异步处理,保障 AI 结果文本插入操作的稳定性。

这套方案为构建易于迭代、稳定可靠的智能富文本应用奠定了坚实基础。

好的,基于前面构建的清晰前端架构,封装 AI 模型接口以实现实际的 AI 写作功能是后续的关键一步。我将说明如何设计这个接口层,确保它与前端解耦并具备良好的可扩展性。


后续步骤:封装 AI 模型接口实现 AI 写作功能

在前端架构已经完成解耦的基础上,下一步就是将之前前端代码中的"模拟调用"替换为真实的 AI 模型接口。

多模态Ai项目全流程开发中,从需求分析,到Ui设计,前后端开发,部署上线,感兴趣打开链接(带项目功能演示)多模态AI项目开发中...

相关推荐
末世灯光3 小时前
时间序列入门第一问:它和普通数据有什么不一样?(附 3 类典型案例)
人工智能·python·机器学习·时序数据
金士顿3 小时前
为什么MainWindow.xaml绑定的datacontext,EtherCATSuiteCtrl.xaml直接用了?
前端
Yann-企业信息化3 小时前
AI 开发工具对比:Dify 与 Coze Studio(开源版)差异对比
人工智能·开源
533_3 小时前
[css] flex布局中的英文字母不换行问题
前端·css
2401_836900333 小时前
YOLOv4:集大成的目标检测王者
人工智能·yolov4
Xi xi xi3 小时前
苏州唯理科技近期也正式发布了国内首款神经腕带产品
大数据·人工智能·经验分享·科技
www.023 小时前
微信克隆人,聊天记录训练专属AI(2.WeClone训练模型)
人工智能·python·微信·聊天克隆人·微信克隆人
浮游本尊3 小时前
React 18.x 学习计划 - 第四天:React Hooks深入
前端·学习·react.js
熊猫钓鱼>_>3 小时前
基于知识图谱的智能会议纪要系统:从语音识别到深度理解
人工智能·语音识别·知识图谱