
将 AI 智能写作能力无缝集成到富文本编辑器中,是提升内容生产效率的关键。本文聚焦于 Vue 3 + wangEditor 技术栈,为您提供一套架构清晰、代码可即用 的实现方案。我们将重点展示如何通过分层解耦 的设计思想,构建一个功能完整、用户体验优秀的 AI 助手,并保留所有核心实现代码。
一、架构核心:职责分离与事件驱动
为了确保系统的高可维护性和可扩展性,我们采用了清晰的分层架构:
- 视图层 (
NoteEditor.vue
):编辑器宿主,负责 AI 弹窗的显示/隐藏、快捷键监听以及最终的 AI 服务调用与结果插入。 - 功能层 (
AIWritingPopup.vue
):弹窗组件,专注于用户输入、AI 操作类型选择和结果展示。 - 管理层 (
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 的深度集成和高可扩展性。其核心技术价值和亮点在于:
-
架构与解耦(高可维护性):
- 职责分离:严格区分 UI 交互(弹窗)、编辑器管理(宿主)和业务逻辑(AI 服务调用)。
- 事件驱动核心:所有操作(快捷键、工具栏)统一派发事件,核心功能与触发方式完全解耦,确保系统的高度灵活和可扩展性。
-
极致的用户体验(高可用性):
- 智能快捷键 :实现
Ctrl + Alt
全局监听,并结合焦点判断,提供最高效的触发机制。 - 动态定位:利用 Selection API 和边界检测,确保 AI 弹窗精确、人道地跟随用户光标出现。
- 智能快捷键 :实现
-
工程实践(高可靠性):
- 生命周期完整:通过 Vue 钩子对全局事件和编辑器实例进行彻底清理,杜绝内存泄漏和重复注册。
- 原子化插入:通过强制聚焦和异步处理,保障 AI 结果文本插入操作的稳定性。
这套方案为构建易于迭代、稳定可靠的智能富文本应用奠定了坚实基础。
好的,基于前面构建的清晰前端架构,封装 AI 模型接口以实现实际的 AI 写作功能是后续的关键一步。我将说明如何设计这个接口层,确保它与前端解耦并具备良好的可扩展性。
后续步骤:封装 AI 模型接口实现 AI 写作功能
在前端架构已经完成解耦的基础上,下一步就是将之前前端代码中的"模拟调用"替换为真实的 AI 模型接口。
多模态Ai项目全流程开发中,从需求分析,到Ui设计,前后端开发,部署上线,感兴趣打开链接(带项目功能演示) ,多模态AI项目开发中...