From 8f3e2500732841528049e2ee192bb6e2d297f3b9 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Tue, 19 Aug 2025 11:59:35 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=A1=86=E5=B7=A5=E5=85=B7=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manage/components/ChatInput/index.vue | 97 +++++++++---- .../components/ChatInput/selection-plugin.js | 128 ++++++++++++++++++ .../sass/pages/components/chat-input.scss | 65 ++++++--- 3 files changed, 249 insertions(+), 41 deletions(-) create mode 100644 resources/assets/js/pages/manage/components/ChatInput/selection-plugin.js diff --git a/resources/assets/js/pages/manage/components/ChatInput/index.vue b/resources/assets/js/pages/manage/components/ChatInput/index.vue index 133b66d3e..547585bb2 100755 --- a/resources/assets/js/pages/manage/components/ChatInput/index.vue +++ b/resources/assets/js/pages/manage/components/ChatInput/index.vue @@ -24,6 +24,29 @@ + +
+ +
+
    +
  • + +
  • +
+
+
+
@@ -275,16 +298,18 @@ footer-hide fullscreen>
+
-
    + +
    • + :data-label="item.label" + :data-type="item.type" + v-touchmouse="onMenu">
    @@ -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; } }, diff --git a/resources/assets/js/pages/manage/components/ChatInput/selection-plugin.js b/resources/assets/js/pages/manage/components/ChatInput/selection-plugin.js new file mode 100644 index 000000000..9225fed35 --- /dev/null +++ b/resources/assets/js/pages/manage/components/ChatInput/selection-plugin.js @@ -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; +} diff --git a/resources/assets/sass/pages/components/chat-input.scss b/resources/assets/sass/pages/components/chat-input.scss index d33e09556..d1634221b 100755 --- a/resources/assets/sass/pages/components/chat-input.scss +++ b/resources/assets/sass/pages/components/chat-input.scss @@ -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;