96SEO 2026-04-19 23:49 1
作为一名前端开发者,你肯定接到过不少需求,要求复刻类似豆包、ChatGPT或者Claude那样的对话界面。说实话,这事儿kan着简单,真动起手来全是坑。尤其是那个kan似平平无奇的输入框,它不仅要支持多行文本,还得Neng塞进去各种“智Neng组件”——比如下拉选项、可填空的标签等等。

Zui近我就接到了这么个活儿:用Vue3实现一个豆包风格的模版输入框。起初我以为随便找个富文本库就Neng搞定,结果发现市面上大部分现成的轮子要么太重,要么定制起来极其痛苦。经过一番折腾,我终于摸索出了一套基于 slate-vue3 的解决方案。今天就把这套心路历程和代码逻辑分享给大家,希望Neng帮各位少走点弯路。
在开始敲代码之前,咱们得先聊聊选型。Ru果你去搜“Vue 富文本编辑器”,大概率会kan到 Quill、TinyMCE 或者 wangEditor 这些老牌劲旅。它们确实不错,但面对豆包这种“文本中夹杂交互组件”的需求时就显得力不从心了。你想啊,在一个 contenteditable 的 div 里塞一个原生的 标签,光标位置、选区管理、回车换行,每一个细节douNeng让你怀疑人生。
这时候,Slate.js 就成了救星。虽然它是 React 生态的“亲儿子”,但好在有社区大佬封装了 slate-vue3,让我们 Vue 开发者也Neng享受到这波红利。Slate 的核心理念是把一切内容doukan作数据结构,而不是 DOM 节点。这意味着我们Ke以像操作 JSON 一样操作输入框里的内容,这对于实现复杂的模版逻辑来说简直是降维打击。
废话不多说咱们先把地基打好。这里我选择 Vite 作为构建工具,配合 TypeScript,毕竟类型安全Neng让我们在后续的复杂逻辑中保持清醒。
初始化项目:
pnpm create vite@latest doubao-template-input
按照提示选择 Vue 和 TypeScript。进到目录里我们需要安装几个关键依赖。除了核心的 slate-vue3,为了快速开发样式,我还引入了 UnoCSS。
pnpm i slate-vue3 sass
# Ru果你也想用 UnoCSS,顺带装一下这些
pnpm i -D unocss @unocss/preset-uno @unocss/eslint-config @unocss/preset-icons @iconify-json/mdi @iconify/vue @iconify-json/ion @iconify/utils
配置 UnoCSS 的过程我就不展开了大家去翻翻官方文档或者直接kan我的源码仓库douNeng搞定。重点是我们要把项目的基础样式重置一下避免浏览器默认行为捣乱。
三、 构建输入框的“骨架”在深入编辑器逻辑之前,先把 UI 框架搭起来。豆包的输入框结构其实挺清晰的:上面是一个大的编辑区域,下面是一排功Neng按钮。
我们在 components 目录下新建一个 ChatInput/index.vue。先别管逻辑,先把 HTML 结构写出来:
深度思考
搜索资料
这一步Zuo完,你应该Nengkan到一个长得像模像样的空壳子了。接下来就是Zui核心的部分:把 Slate 塞进去。
四、 初始化 Slate 编辑器在 ChatInput/index.vue 中,我们需要引入 Slate 的核心组件。Slate 的设计非常模块化,我们需要组合 createEditorwithHistory和 withDOM。
import { Slate, Editable, type RenderPlaceholderProps, type RenderElementProps, useInheritRef } from "slate-vue3"
import { h, ref, type Component } from "vue";
import { createEditor } from "slate-vue3/core";
import { withDOM } from "slate-vue3/dom";
import { withHistory } from "slate-vue3/history";
// 定义初始值,这里先放一个空的段落
const initialValue = ,
},
]
// 初始化编辑器实例
const editor = withHistory));
editor.children = initialValue;
然后在模板中替换掉那个占位的注释:
这时候你运行项目,应该Yi经Neng输入文字了。但是这还远远不够。豆包的输入框里可是有“料”的。
五、 实现自定义组件:SelectTag豆包的模版里经常会有那种蓝色的、Ke以点击选择的标签,比如“我是”。在 Slate 里这其实就是一个自定义的 Element。
我们需要定义类型。新建一个 type.ts
import type { BaseEditor, BaseElement } from "slate-vue3/core";
import type { DOMEditor } from "slate-vue3/dom";
export type CustomElement = ParagraphElement | InputTagElement | SelectTagElement;
export interface ParagraphElement extends BaseElement {
type: "paragraph";
children: CustomText;
}
export interface InputTagElement extends BaseElement {
type: "input-tag";
label: string;
children: CustomText;
}
export interface SelectTagElement extends BaseElement {
type: "select-tag";
value: string;
options: { label: string; value: string };
children: CustomText;
}
export type CustomNode = CustomElement | CustomText;
export interface CustomText {
text: string;
}
export type CustomEditor = BaseEditor & DOMEditor;
接下来创建 SelectTag.vue 组件。这个组件其实就是一个包裹了原生 select 的壳子,关键在于要处理 contenteditable="false",防止用户在编辑下拉框的时候把 Slate 的光标搞乱。
组件写好了怎么告诉 Slate 在遇到 type: 'select-tag' 的节点时渲染这个组件呢?这就需要用到 renderElement 函数。
import SelectTag from "./components/SelectTag.vue";
const renderElement = => {
const customElement = element as CustomElement;
switch {
case 'select-tag':
return h(SelectTag as unknown as Component, {
...useInheritRef,
element
}, => children);
default:
return h
}
}
别忘了把这个函数传给 组件,并且还要告诉 Slate 这些元素是行内元素,否则它们会独占一行,那就尴尬了。
const withInlines = => {
const { isInline } = editor;
editor.isInline = element =>
.includes.type) || isInline
return editor
}
// 重新初始化 editor
const editor = withHistory)));
现在我们Ke以试着修改一下 initialValue,kankan效果:
const initialValue = ,
value: '本科生',
options:
},
{ text: ',请帮我写论文。' },
],
},
]
六、 进阶:InputTag 与占位符逻辑
除了下拉选择,豆包还有一种“填空”式的标签,比如“请输入”。这种标签在没内容时显示提示文字,有内容时显示用户输入的文字。
实现这个逻辑稍微有点绕。我们需要一个 InputTag.vue,它内部其实是一个 span,通过监听子节点的文本内容来控制占位符的显示与隐藏。这里有个小坑:Ru果文本节点是空的,光标可Neng会消失,所以我们要给空文本节点加一个极小的 padding-left: 0.1px。
为了简化,这里直接展示如何处理 renderLeaf 来解决光标问题,以及如何在 renderElement 中加入 input-tag 的逻辑:
// 解决空文本节点光标消失的问题
const renderLeaf = => h('span', {
...attributes,
style: {
paddingLeft: leaf.text === '' ? '0.1px' : ''
}
}, children)
// 在 renderElement 中添加 case
const renderElement = => {
const customElement = element as CustomElement;
switch {
case 'select-tag':
return h(SelectTag as unknown as Component, {
...useInheritRef,
element
}, => children);
case 'input-tag':
// 这里假设你也有一个 InputTag 组件,逻辑类似 SelectTag
// return h
return h;
default:
return h
}
}
七、 数据流转:从点击技Neng到发送内容
界面搭好了组件也Neng渲染了Zui后一步就是数据的进出。
是“进”。我们在页面顶部放了一排技Neng卡片,点击卡片应该把对应的模版塞进编辑器。这需要我们在父组件中调用子组件的方法。
在 ChatInput/index.vue 中定义一个 setEditValue 方法并通过 defineExpose 暴露出去:
import { Editor, Transforms } from 'slate-vue3/core';
import type { Node } from 'slate-vue3/core';
function setEditValue {
Editor.withoutNormalizing => {
// 清空现有内容
for {
Transforms.removeNodes;
}
// 插入新内容
Transforms.insertNodes;
});
// 光标归位
const startPoint = Editor.start;
Transforms.select(editor, {
anchor: startPoint,
focus: startPoint
});
}
defineExpose
然后是“出”。点击发送按钮时我们需要把 Slate 的 JSON 树转换成纯文本字符串发给后端。这里需要一个序列化函数:
import { Text } from "slate-vue3/core";
const serializeToPlainText = : string => {
return nodes.map(node => {
if ) {
return node.text;
}
const children = serializeToPlainText;
switch {
case 'input-tag':
return children || node.label || '';
case 'select-tag':
return node.value || '';
case 'paragraph':
return children + '
';
default:
return children;
}
}).join;
};
function send {
const content = serializeToPlainText;
console.info;
alert;
}
经过这一通操作,一个具备豆包核心交互Neng力的输入框就诞生了。虽然代码量不算少,但相比从零手搓 contenteditable 的各种诡异 Bug,这套基于 Slate 的方案要稳健得多。
当然这只是一个起点。实际生产环境中,你可Neng还需要处理图片上传、@人功Neng、Markdown 渲染等等geng复杂的需求。但有了这个底子, 起来就不再是难事了。
Zui后我把完整的代码上传到了 GitHub,里面包含了所有细节和样式配置。Ru果你在实现过程中遇到什么问题,欢迎去上面提 Issue 或者直接抄作业。毕竟站在巨人的肩膀上,我们才Nengkan得geng远嘛!
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback