perf: 优化输入框工具栏

This commit is contained in:
kuaifan 2025-08-19 11:59:35 +08:00
parent 63a792d169
commit 8f3e250073
3 changed files with 249 additions and 41 deletions

View File

@ -24,6 +24,29 @@
</EPopover>
</div>
<!-- 工具栏 -->
<div class="chat-input-toolbar">
<EPopover
ref="toolbarRef"
v-model="selectedText"
:visibleArrow="false"
transition=""
placement="top-start"
popperClass="chat-input-toolbar-popover">
<div slot="reference"></div>
<ul class="chat-input-toolbar-menu">
<li
v-for="(item, index) in tools"
:key="index"
:data-label="item.label"
:data-type="item.type"
v-touchmouse="onMenu">
<i class="taskfont" v-html="item.icon"></i>
</li>
</ul>
</EPopover>
</div>
<div ref="inputWrapper" class="chat-input-wrapper">
<!-- 回复修改 -->
<div v-if="quoteData" class="chat-quote">
@ -275,16 +298,18 @@
footer-hide
fullscreen>
<div class="chat-input-box" :style="chatInputBoxStyle">
<!-- 输入区域 -->
<div class="chat-input-wrapper">
<div ref="editorFull" class="no-dark-content"></div>
</div>
<ul class="chat-input-menu" :class="{activation: fullSelection.length > 0}">
<!-- 工具栏 -->
<ul class="chat-input-menu" :class="{activation: fullSelected}">
<li
v-for="(item, index) in fullTools"
v-for="(item, index) in tools"
:key="index"
@touchstart.prevent=""
@touchend.prevent="onFullMenu(item.label, item.type)"
@click="onFullMenu(item.label, item.type)">
:data-label="item.label"
:data-type="item.type"
v-touchmouse="onMenu">
<i class="taskfont" v-html="item.icon"></i>
</li>
</ul>
@ -299,6 +324,7 @@ import {mapGetters, mapState} from "vuex";
import Quill from 'quill-hi';
import {Delta} from "quill-hi/core";
import "quill-mention-hi";
import "./selection-plugin";
import ChatEmoji from "./emoji";
import touchmouse from "../../../../directives/touchmouse";
import touchclick from "../../../../directives/touchclick";
@ -442,11 +468,14 @@ export default {
moreTimer: null,
selectTimer: null,
selectRange: null,
selectedText: false,
fullInput: false,
fullQuill: null,
fullSelection: {index: 0, length: 0},
fullTools: [
fullSelected: false,
fullSelection: null,
tools: [
{
label: 'bold',
type: '',
@ -937,7 +966,7 @@ export default {
readOnly: false,
placeholder: this.placeholder,
modules: {
toolbar: this.$isEEUIApp || this.windowTouch ? false : this.toolbar,
toolbar: false,
keyboard: this.simpleMode ? {} : {
bindings: {
'short enter': {
@ -975,6 +1004,17 @@ export default {
}
}
},
selectionPlugin: {
onTextSelected: (selectedText) => {
if (this.$isEEUIApp || this.windowTouch) {
return
}
this.selectedText = !!selectedText.trim()
},
onSelectionCleared: () => {
this.selectedText = false
}
},
mention: this.quillMention()
}
}, this.options)
@ -1786,6 +1826,14 @@ export default {
placeholder: this.placeholder,
modules: {
toolbar: false,
selectionPlugin: {
onTextSelected: (selectedText) => {
this.fullSelected = !!selectedText.trim()
},
onSelectionCleared: () => {
this.fullSelected = false
}
},
mention: this.quillMention()
}
}, this.options))
@ -1800,9 +1848,6 @@ export default {
}, 100)
}
})
this.fullQuill.on('text-change', _ => {
this.fullSelection = this.fullQuill.getSelection()
})
this.fullQuill.enable(true)
this.$refs.editorFull.firstChild.innerHTML = this.$refs.editor.firstChild.innerHTML
this.$nextTick(_ => {
@ -1822,31 +1867,36 @@ export default {
})
},
onFullMenu(action, type) {
const {length} = this.fullQuill.getSelection(true);
onMenu(action, _, el) {
if (action !== 'up') {
return;
}
const quill = this.getEditor();
const {length} = quill.getSelection(true);
if (length === 0) {
$A.messageWarning("请选择文字后再操作")
return
}
switch (action) {
const label = el.getAttribute('data-label');
switch (label) {
case 'bold':
this.fullQuill.format('bold', !this.fullQuill.getFormat().bold);
quill.format('bold', !quill.getFormat().bold);
break;
case 'strike':
this.fullQuill.format('strike', !this.fullQuill.getFormat().strike);
quill.format('strike', !quill.getFormat().strike);
break;
case 'italic':
this.fullQuill.format('italic', !this.fullQuill.getFormat().italic);
quill.format('italic', !quill.getFormat().italic);
break;
case 'underline':
this.fullQuill.format('underline', !this.fullQuill.getFormat().underline);
quill.format('underline', !quill.getFormat().underline);
break;
case 'blockquote':
this.fullQuill.format('blockquote', !this.fullQuill.getFormat().blockquote);
quill.format('blockquote', !quill.getFormat().blockquote);
break;
case 'link':
if (this.fullQuill.getFormat().link) {
this.fullQuill.format('link', false);
if (quill.getFormat().link) {
quill.format('link', false);
return
}
$A.modalInput({
@ -1856,12 +1906,13 @@ export default {
if (!link) {
return false;
}
this.fullQuill.format('link', link);
quill.format('link', link);
}
})
break;
case 'list':
this.fullQuill.format('list', this.fullQuill.getFormat().list === type ? false : type);
const type = el.getAttribute('data-type') || '';
quill.format('list', quill.getFormat().list === type ? false : type);
break;
}
},

View File

@ -0,0 +1,128 @@
// selection-plugin.js - 独立的 Quill 插件文件
import Quill from 'quill-hi';
import Emitter from "quill-hi/core/emitter";
// 通过 Quill.import 导入需要的核心模块
const Module = Quill.import('core/module');
class SelectionPlugin extends Module {
static DEFAULTS = {
immediate: true,
minLength: 1,
onTextSelected: null,
onSelectionCleared: null,
onSelectionChange: null
};
constructor(quill, options = {}) {
super(quill, { ...SelectionPlugin.DEFAULTS, ...options });
this.lastRange = null;
this.debounceTimer = null;
this.setupEventListeners();
}
setupEventListeners() {
// 监听选择变化事件
this.quill.on(Emitter.events.SELECTION_CHANGE, (range, oldRange, source) => {
this.handleSelectionChange(range, oldRange, source);
});
// 监听文本变化事件,处理输入替换选中文本的情况
this.quill.on(Emitter.events.TEXT_CHANGE, (delta, oldDelta, source) => {
// 延迟检查,确保选择状态已更新
setTimeout(() => {
const currentRange = this.quill.getSelection();
this.handleSelectionChange(currentRange, this.lastRange, source);
}, 0);
});
}
handleSelectionChange(range, oldRange, source) {
// 防抖处理
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.processSelectionChange(range, oldRange, source);
this.lastRange = range;
this.debounceTimer = null;
}, this.options.immediate ? 0 : 100);
}
processSelectionChange(range, oldRange, source) {
// 通用选择变化回调
if (this.options.onSelectionChange) {
this.options.onSelectionChange(range, oldRange, source);
}
// 处理文本被选中的情况
if (range && range.length >= this.options.minLength) {
const selectedText = this.quill.getText(range.index, range.length);
if (this.options.onTextSelected) {
this.options.onTextSelected(selectedText, range, source);
}
// 发射自定义事件
this.quill.emitter.emit('text-selected', {
text: selectedText,
range: range,
source: source
});
}
// 处理选择被清除的情况
else if ((!range || range.length === 0) && oldRange && oldRange.length > 0) {
if (this.options.onSelectionCleared) {
this.options.onSelectionCleared(oldRange, source);
}
// 发射自定义事件
this.quill.emitter.emit('selection-cleared', {
previousRange: oldRange,
source: source
});
}
}
// 公共 API 方法
getSelectedText() {
const range = this.quill.getSelection();
if (range && range.length > 0) {
return this.quill.getText(range.index, range.length);
}
return null;
}
hasSelection() {
const range = this.quill.getSelection();
return !!(range && range.length > 0);
}
selectText(index, length) {
this.quill.setSelection(index, length, Emitter.sources.API);
}
clearSelection() {
const range = this.quill.getSelection();
if (range) {
this.quill.setSelection(range.index, 0, Emitter.sources.API);
}
}
// 清理资源
destroy() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
}
}
// 注册插件
Quill.register('modules/selectionPlugin', SelectionPlugin);
// 导出给模块系统使用(可选)
if (typeof module !== 'undefined' && module.exports) {
module.exports = SelectionPlugin;
}

View File

@ -81,6 +81,16 @@
z-index: -1;
}
.chat-input-toolbar {
position: absolute;
top: 4px;
left: 24px;
width: 0;
height: 0;
visibility: hidden;
z-index: -1;
}
.chat-input-wrapper {
position: relative;
display: inline-block;
@ -253,23 +263,6 @@
}
}
.ql-bubble {
.ql-tooltip {
z-index: 1;
button {
&.ql-active {
position: relative;
background: #3d3d3d;
border-radius: 6px;
}
}
.ql-formats {
display: flex;
align-items: center;
}
}
}
.chat-space {
float: right;
width: 170px;
@ -726,7 +719,37 @@
}
}
}
.chat-input-toolbar-popover {
border: 0;
padding: 0;
overflow: hidden;
box-shadow: none;
background: rgba(255, 255, 255, 0.9);
}
.chat-input-toolbar-menu {
display: flex;
align-items: center;
justify-content: center;
> li {
flex-shrink: 0;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
> i {
color: $primary-color;
}
}
> i {
font-size: 14px;
color: #555;
transition: color 0.3s ease;
}
}
}
.chat-input-more-popover {
min-width: 100px;
padding: 8px;
@ -1045,6 +1068,11 @@
> li {
opacity: 1;
cursor: pointer;
&:hover {
> i {
color: $primary-color;
}
}
&:active {
background-color: #eee;
}
@ -1065,6 +1093,7 @@
> i {
color: #555;
font-size: 16px;
transition: color 0.3s ease;
}
@media screen and (max-width: 320px) {
height: 52px;