mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-13 03:52:50 +00:00
perf: 优化输入框工具栏
This commit is contained in:
parent
63a792d169
commit
8f3e250073
@ -24,6 +24,29 @@
|
|||||||
</EPopover>
|
</EPopover>
|
||||||
</div>
|
</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 ref="inputWrapper" class="chat-input-wrapper">
|
||||||
<!-- 回复、修改 -->
|
<!-- 回复、修改 -->
|
||||||
<div v-if="quoteData" class="chat-quote">
|
<div v-if="quoteData" class="chat-quote">
|
||||||
@ -275,16 +298,18 @@
|
|||||||
footer-hide
|
footer-hide
|
||||||
fullscreen>
|
fullscreen>
|
||||||
<div class="chat-input-box" :style="chatInputBoxStyle">
|
<div class="chat-input-box" :style="chatInputBoxStyle">
|
||||||
|
<!-- 输入区域 -->
|
||||||
<div class="chat-input-wrapper">
|
<div class="chat-input-wrapper">
|
||||||
<div ref="editorFull" class="no-dark-content"></div>
|
<div ref="editorFull" class="no-dark-content"></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="chat-input-menu" :class="{activation: fullSelection.length > 0}">
|
<!-- 工具栏 -->
|
||||||
|
<ul class="chat-input-menu" :class="{activation: fullSelected}">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in fullTools"
|
v-for="(item, index) in tools"
|
||||||
:key="index"
|
:key="index"
|
||||||
@touchstart.prevent=""
|
:data-label="item.label"
|
||||||
@touchend.prevent="onFullMenu(item.label, item.type)"
|
:data-type="item.type"
|
||||||
@click="onFullMenu(item.label, item.type)">
|
v-touchmouse="onMenu">
|
||||||
<i class="taskfont" v-html="item.icon"></i>
|
<i class="taskfont" v-html="item.icon"></i>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -299,6 +324,7 @@ import {mapGetters, mapState} from "vuex";
|
|||||||
import Quill from 'quill-hi';
|
import Quill from 'quill-hi';
|
||||||
import {Delta} from "quill-hi/core";
|
import {Delta} from "quill-hi/core";
|
||||||
import "quill-mention-hi";
|
import "quill-mention-hi";
|
||||||
|
import "./selection-plugin";
|
||||||
import ChatEmoji from "./emoji";
|
import ChatEmoji from "./emoji";
|
||||||
import touchmouse from "../../../../directives/touchmouse";
|
import touchmouse from "../../../../directives/touchmouse";
|
||||||
import touchclick from "../../../../directives/touchclick";
|
import touchclick from "../../../../directives/touchclick";
|
||||||
@ -442,11 +468,14 @@ export default {
|
|||||||
moreTimer: null,
|
moreTimer: null,
|
||||||
selectTimer: null,
|
selectTimer: null,
|
||||||
selectRange: null,
|
selectRange: null,
|
||||||
|
selectedText: false,
|
||||||
|
|
||||||
fullInput: false,
|
fullInput: false,
|
||||||
fullQuill: null,
|
fullQuill: null,
|
||||||
fullSelection: {index: 0, length: 0},
|
fullSelected: false,
|
||||||
fullTools: [
|
fullSelection: null,
|
||||||
|
|
||||||
|
tools: [
|
||||||
{
|
{
|
||||||
label: 'bold',
|
label: 'bold',
|
||||||
type: '',
|
type: '',
|
||||||
@ -937,7 +966,7 @@ export default {
|
|||||||
readOnly: false,
|
readOnly: false,
|
||||||
placeholder: this.placeholder,
|
placeholder: this.placeholder,
|
||||||
modules: {
|
modules: {
|
||||||
toolbar: this.$isEEUIApp || this.windowTouch ? false : this.toolbar,
|
toolbar: false,
|
||||||
keyboard: this.simpleMode ? {} : {
|
keyboard: this.simpleMode ? {} : {
|
||||||
bindings: {
|
bindings: {
|
||||||
'short enter': {
|
'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()
|
mention: this.quillMention()
|
||||||
}
|
}
|
||||||
}, this.options)
|
}, this.options)
|
||||||
@ -1786,6 +1826,14 @@ export default {
|
|||||||
placeholder: this.placeholder,
|
placeholder: this.placeholder,
|
||||||
modules: {
|
modules: {
|
||||||
toolbar: false,
|
toolbar: false,
|
||||||
|
selectionPlugin: {
|
||||||
|
onTextSelected: (selectedText) => {
|
||||||
|
this.fullSelected = !!selectedText.trim()
|
||||||
|
},
|
||||||
|
onSelectionCleared: () => {
|
||||||
|
this.fullSelected = false
|
||||||
|
}
|
||||||
|
},
|
||||||
mention: this.quillMention()
|
mention: this.quillMention()
|
||||||
}
|
}
|
||||||
}, this.options))
|
}, this.options))
|
||||||
@ -1800,9 +1848,6 @@ export default {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.fullQuill.on('text-change', _ => {
|
|
||||||
this.fullSelection = this.fullQuill.getSelection()
|
|
||||||
})
|
|
||||||
this.fullQuill.enable(true)
|
this.fullQuill.enable(true)
|
||||||
this.$refs.editorFull.firstChild.innerHTML = this.$refs.editor.firstChild.innerHTML
|
this.$refs.editorFull.firstChild.innerHTML = this.$refs.editor.firstChild.innerHTML
|
||||||
this.$nextTick(_ => {
|
this.$nextTick(_ => {
|
||||||
@ -1822,31 +1867,36 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
onFullMenu(action, type) {
|
onMenu(action, _, el) {
|
||||||
const {length} = this.fullQuill.getSelection(true);
|
if (action !== 'up') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const quill = this.getEditor();
|
||||||
|
const {length} = quill.getSelection(true);
|
||||||
if (length === 0) {
|
if (length === 0) {
|
||||||
$A.messageWarning("请选择文字后再操作")
|
$A.messageWarning("请选择文字后再操作")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch (action) {
|
const label = el.getAttribute('data-label');
|
||||||
|
switch (label) {
|
||||||
case 'bold':
|
case 'bold':
|
||||||
this.fullQuill.format('bold', !this.fullQuill.getFormat().bold);
|
quill.format('bold', !quill.getFormat().bold);
|
||||||
break;
|
break;
|
||||||
case 'strike':
|
case 'strike':
|
||||||
this.fullQuill.format('strike', !this.fullQuill.getFormat().strike);
|
quill.format('strike', !quill.getFormat().strike);
|
||||||
break;
|
break;
|
||||||
case 'italic':
|
case 'italic':
|
||||||
this.fullQuill.format('italic', !this.fullQuill.getFormat().italic);
|
quill.format('italic', !quill.getFormat().italic);
|
||||||
break;
|
break;
|
||||||
case 'underline':
|
case 'underline':
|
||||||
this.fullQuill.format('underline', !this.fullQuill.getFormat().underline);
|
quill.format('underline', !quill.getFormat().underline);
|
||||||
break;
|
break;
|
||||||
case 'blockquote':
|
case 'blockquote':
|
||||||
this.fullQuill.format('blockquote', !this.fullQuill.getFormat().blockquote);
|
quill.format('blockquote', !quill.getFormat().blockquote);
|
||||||
break;
|
break;
|
||||||
case 'link':
|
case 'link':
|
||||||
if (this.fullQuill.getFormat().link) {
|
if (quill.getFormat().link) {
|
||||||
this.fullQuill.format('link', false);
|
quill.format('link', false);
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$A.modalInput({
|
$A.modalInput({
|
||||||
@ -1856,12 +1906,13 @@ export default {
|
|||||||
if (!link) {
|
if (!link) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.fullQuill.format('link', link);
|
quill.format('link', link);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break;
|
break;
|
||||||
case 'list':
|
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;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
128
resources/assets/js/pages/manage/components/ChatInput/selection-plugin.js
vendored
Normal file
128
resources/assets/js/pages/manage/components/ChatInput/selection-plugin.js
vendored
Normal 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;
|
||||||
|
}
|
||||||
@ -81,6 +81,16 @@
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-input-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 24px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-input-wrapper {
|
.chat-input-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
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 {
|
.chat-space {
|
||||||
float: right;
|
float: right;
|
||||||
width: 170px;
|
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 {
|
.chat-input-more-popover {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -1045,6 +1068,11 @@
|
|||||||
> li {
|
> li {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
> i {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
&:active {
|
&:active {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
@ -1065,6 +1093,7 @@
|
|||||||
> i {
|
> i {
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 320px) {
|
@media screen and (max-width: 320px) {
|
||||||
height: 52px;
|
height: 52px;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user