mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-10 18:02:55 +00:00
2568 lines
99 KiB
Vue
Executable File
2568 lines
99 KiB
Vue
Executable File
<template>
|
||
<div class="chat-input-box" :class="boxClass" v-clickoutside="hidePopover">
|
||
<!-- 快速表情 -->
|
||
<div class="chat-input-quick-emoji">
|
||
<EPopover
|
||
ref="emojiQuickRef"
|
||
v-model="emojiQuickShow"
|
||
:visibleArrow="false"
|
||
transition=""
|
||
placement="top-end"
|
||
popperClass="chat-quick-emoji-popover">
|
||
<div slot="reference"></div>
|
||
<Scrollbar
|
||
tag="ul"
|
||
ref="emojiWrapper"
|
||
:enable-x="true"
|
||
:enable-y="false"
|
||
:touch-content-blur="false"
|
||
class-name="chat-quick-emoji-wrapper scrollbar-hidden">
|
||
<li v-for="(item, index) in emojiQuickItems" :key="index" @click="onEmojiQuick(item)">
|
||
<Imgs :title="item.name" :alt="item.name" :src="item.src"/>
|
||
</li>
|
||
</Scrollbar>
|
||
</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">
|
||
<div v-if="quoteUpdate" class="quote-label">{{$L('编辑消息')}}</div>
|
||
<UserAvatar v-else :userid="quoteData.userid" :userResult="onQuoteUserResult" :show-icon="false" :show-name="true"/>
|
||
<div class="quote-desc no-dark-content">{{$A.getMsgSimpleDesc(quoteData)}}</div>
|
||
<i class="taskfont" v-touchclick="onTouchClick" data-action="cancel-quote"></i>
|
||
</div>
|
||
|
||
<!-- 输入框 -->
|
||
<div
|
||
ref="editor"
|
||
class="no-dark-content user-select-auto"
|
||
@click.stop="onClickEditor"
|
||
@paste="handlePaste"></div>
|
||
|
||
<!-- 工具栏占位 -->
|
||
<div class="chat-space">
|
||
<input class="space-input" @focus="onSpaceInputFocus"/>
|
||
</div>
|
||
|
||
<!-- 工具栏 -->
|
||
<ul class="chat-toolbar" @click.stop>
|
||
<!-- 桌面端表情(漂浮) -->
|
||
<li>
|
||
<EPopover
|
||
ref="emoji"
|
||
v-if="!emojiBottom"
|
||
v-model="showEmoji"
|
||
:visibleArrow="false"
|
||
placement="top"
|
||
popperClass="chat-input-emoji-popover">
|
||
<ETooltip slot="reference" ref="emojiTip" :disabled="$isEEUIApp || windowTouch || showEmoji" placement="top" :enterable="false" :content="$L('表情')">
|
||
<i class="taskfont"></i>
|
||
</ETooltip>
|
||
<ChatEmoji v-if="showEmoji" @on-select="onSelectEmoji" :searchKey="emojiQuickKey"/>
|
||
</EPopover>
|
||
<ETooltip v-else ref="emojiTip" :disabled="$isEEUIApp || windowTouch || showEmoji" placement="top" :enterable="false" :content="$L('表情')">
|
||
<i class="taskfont" @click="showEmoji=!showEmoji"></i>
|
||
</ETooltip>
|
||
</li>
|
||
|
||
<!-- @ # -->
|
||
<li>
|
||
<ETooltip placement="top" :disabled="$isEEUIApp || windowTouch" :enterable="false" :content="$L('选择成员')">
|
||
<i class="taskfont" @click="onToolbar('user')"></i>
|
||
</ETooltip>
|
||
</li>
|
||
<li>
|
||
<ETooltip placement="top" :disabled="$isEEUIApp || windowTouch" :enterable="false" :content="$L('选择任务')">
|
||
<i class="taskfont" @click="onToolbar('task')"></i>
|
||
</ETooltip>
|
||
</li>
|
||
|
||
<!-- 加号更多 -->
|
||
<li>
|
||
<EPopover
|
||
ref="more"
|
||
v-model="showMore"
|
||
:visibleArrow="false"
|
||
placement="top"
|
||
popperClass="chat-input-more-popover">
|
||
<ETooltip slot="reference" ref="moreTip" :disabled="$isEEUIApp || windowTouch || showMore" placement="top" :enterable="false" :content="$L('展开')">
|
||
<i class="taskfont"></i>
|
||
</ETooltip>
|
||
<template v-if="!isAiBot">
|
||
<div v-if="maybePhotoShow" class="chat-input-popover-item maybe-photo" @click="onToolbar('maybe-photo')">
|
||
<span :style="{maxWidth: maybePhotoStyle.width}">{{$L('可能要发的照片')}}:</span>
|
||
<div class="photo-preview" :style="maybePhotoStyle"></div>
|
||
</div>
|
||
<div v-if="recordReady" class="chat-input-popover-item" @click="onToolbar('meeting')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('新会议')}}</em>
|
||
</div>
|
||
<div v-if="canCall" class="chat-input-popover-item" @click="onToolbar('call')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('拨打电话')}}</em>
|
||
</div>
|
||
<div class="chat-input-popover-item" @click="onToolbar('image')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('发送图片')}}</em>
|
||
</div>
|
||
<div class="chat-input-popover-item" @click="onToolbar('file')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('上传文件')}}</em>
|
||
</div>
|
||
<div v-if="canAnon" class="chat-input-popover-item" @click="onToolbar('anon')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('匿名消息')}}</em>
|
||
</div>
|
||
<div v-if="dialogData.type == 'group'" class="chat-input-popover-item" @click="onToolbar('word-chain')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('发起接龙')}}</em>
|
||
</div>
|
||
<div v-if="dialogData.type == 'group'" class="chat-input-popover-item" @click="onToolbar('vote')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('发起投票')}}</em>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="chat-input-popover-item" @click="onToolbar('file')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('上传文件')}}</em>
|
||
</div>
|
||
</template>
|
||
<div class="chat-input-popover-item" @click="onToolbar('ai')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('AI 生成')}}</em>
|
||
</div>
|
||
<div ref="moreFull" class="chat-input-popover-item" @click="onToolbar('full')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('全屏输入')}}</em>
|
||
</div>
|
||
</EPopover>
|
||
</li>
|
||
|
||
<!-- 发送按钮 -->
|
||
<li
|
||
ref="chatSend"
|
||
class="chat-send"
|
||
:class="sendClass"
|
||
v-touchmouse="clickSend"
|
||
v-longpress="{callback: onShowMenu, delay: 300}">
|
||
<EPopover
|
||
ref="menu"
|
||
v-model="showMenu"
|
||
:visibleArrow="false"
|
||
trigger="manual"
|
||
placement="top"
|
||
popperClass="chat-input-more-popover">
|
||
<ETooltip slot="reference" ref="sendTip" placement="top" :disabled="$isEEUIApp || windowTouch || showMenu" :enterable="false" :content="$L(sendContent)">
|
||
<div v-if="loading">
|
||
<div class="chat-load">
|
||
<Loading/>
|
||
</div>
|
||
</div>
|
||
<div v-else>
|
||
<transition name="mobile-send">
|
||
<i v-if="sendClass === 'recorder'" class="taskfont"></i>
|
||
</transition>
|
||
<transition name="mobile-send">
|
||
<i v-if="sendClass !== 'recorder'" class="taskfont"></i>
|
||
</transition>
|
||
</div>
|
||
</ETooltip>
|
||
<div class="chat-input-popover-item" @click="onSend('silence')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('无声发送')}}</em>
|
||
</div>
|
||
<div class="chat-input-popover-item" @click="onSend('md')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('MD 格式发送')}}</em>
|
||
</div>
|
||
<div class="chat-input-popover-item" @click="onSend('normal')">
|
||
<i class="taskfont"></i>
|
||
<em>{{$L('普通格式发送')}}</em>
|
||
</div>
|
||
</EPopover>
|
||
</li>
|
||
</ul>
|
||
|
||
<!-- 录音效果 -->
|
||
<div class="chat-record" :class="recordClassName">
|
||
<div @click="stopRecord(false, true)" class="record-convert">
|
||
<i class="taskfont"></i>
|
||
</div>
|
||
<div class="record-recwave">
|
||
<div ref="recwave"></div>
|
||
</div>
|
||
<div @click="stopRecord(true)" class="record-remove">
|
||
<i class="taskfont"></i>
|
||
<i class="taskfont"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 覆盖层 -->
|
||
<div class="chat-cover" @click.stop="onClickCover"></div>
|
||
</div>
|
||
|
||
<!-- 移动端表情(底部) -->
|
||
<ChatEmoji
|
||
v-if="emojiBottom && showEmoji"
|
||
@on-select="onSelectEmoji"
|
||
@on-delete="onEmojiDelete"
|
||
:searchKey="emojiQuickKey"
|
||
showEmojiDelete/>
|
||
|
||
<!-- 录音浮窗 -->
|
||
<transition name="fade">
|
||
<div
|
||
v-if="recordShow"
|
||
v-transfer-dom
|
||
:data-transfer="true"
|
||
class="chat-input-record-transfer"
|
||
:class="recordClassName"
|
||
:style="recordStyle"
|
||
@click="stopRecord">
|
||
<div v-if="recordDuration > 0" class="record-duration">{{recordFormatDuration}}</div>
|
||
<div v-else class="record-loading"><Loading type="pure"/></div>
|
||
<div class="record-cancel" @click.stop="stopRecord(true)">{{$L(recordFormatTip)}}</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 录音转文字 -->
|
||
<transition name="fade">
|
||
<div
|
||
v-if="recordConvertIng"
|
||
v-transfer-dom
|
||
:data-transfer="true"
|
||
class="chat-input-convert-transfer"
|
||
:style="recordConvertStyle">
|
||
<div class="convert-box">
|
||
<div class="convert-body">
|
||
<div class="convert-content">
|
||
<div v-if="recordConvertSetting" class="convert-setting">
|
||
<i class="taskfont" :class="{active: !!cacheTranscriptionLanguage}" @click="convertSetting('transcription', $event)"></i>
|
||
<i class="taskfont" :class="{active: !!recordConvertTranslate}" @click="convertSetting('translate', $event)"></i>
|
||
</div>
|
||
<div class="convert-input">
|
||
<Input
|
||
type="textarea"
|
||
class="convert-result no-dark-content"
|
||
v-model="recordConvertResult"
|
||
:rows="1"
|
||
:autosize="{minRows: 1, maxRows: 5}"
|
||
:placeholder="recordConvertStatus === 0 ? '...' : ''"
|
||
:disabled="recordConvertStatus !== 1"
|
||
@on-focus="recordConvertFocus=true"
|
||
@on-blur="recordConvertFocus=false"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<ul class="convert-footer" :style="recordConvertFooterStyle">
|
||
<li v-touchclick="onTouchClick" data-action="record-convert-cancel">
|
||
<i class="taskfont"></i>
|
||
<span>{{$L('取消')}}</span>
|
||
</li>
|
||
<li v-touchclick="onTouchClick" data-action="record-convert-voice">
|
||
<i class="taskfont voice"></i>
|
||
<span>{{$L('发送原语音')}}</span>
|
||
</li>
|
||
<li v-touchclick="onTouchClick" data-action="record-convert-result">
|
||
<i v-if="recordConvertStatus === 0" class="send"><Loading/></i>
|
||
<i v-else-if="recordConvertStatus === 2" class="taskfont error"></i>
|
||
<i v-else class="taskfont send"></i>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 全屏输入 -->
|
||
<Modal
|
||
v-model="fullInput"
|
||
:mask-closable="false"
|
||
:beforeClose="onFullBeforeClose"
|
||
class-name="chat-input-full-input"
|
||
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: fullSelected}">
|
||
<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>
|
||
</div>
|
||
<i slot="close" class="taskfont"></i>
|
||
</Modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
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";
|
||
import TransferDom from "../../../../directives/transfer-dom";
|
||
import clickoutside from "../../../../directives/clickoutside";
|
||
import longpress from "../../../../directives/longpress";
|
||
import {inputLoadAdd, inputLoadIsLast, inputLoadRemove} from "./one";
|
||
import {languageList, languageName} from "../../../../language";
|
||
import {isMarkdownFormat, MarkdownConver} from "../../../../utils/markdown";
|
||
import emitter from "../../../../store/events";
|
||
import historyMixin from "./history";
|
||
|
||
const globalRangeIndexs = {};
|
||
|
||
export default {
|
||
name: 'ChatInput',
|
||
components: {ChatEmoji},
|
||
directives: {touchmouse, touchclick, TransferDom, clickoutside, longpress},
|
||
mixins: [historyMixin],
|
||
props: {
|
||
value: {
|
||
type: [String, Number],
|
||
default: ''
|
||
},
|
||
dialogId: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
taskId: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
placeholder: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
disabledRecord: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
loading: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
emojiBottom: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
sendMenu: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
simpleMode: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
options: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
toolbar: {
|
||
type: Array,
|
||
default: () => {
|
||
return ['bold', 'strike', 'italic', 'underline', 'blockquote', 'link', {'list': 'ordered'}, {'list': 'bullet'}, {'list': 'check'}]
|
||
},
|
||
},
|
||
maxlength: {
|
||
type: Number
|
||
},
|
||
defaultMenuOrientation: {
|
||
type: String,
|
||
default: "top"
|
||
},
|
||
replyMsgAutoMention: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
quill: null,
|
||
isFocus: false,
|
||
rangeIndex: 0,
|
||
rangeLength: 0,
|
||
_content: '',
|
||
_options: {},
|
||
|
||
mentionMode: '',
|
||
|
||
maybePhotoShow: false,
|
||
maybePhotoData: {},
|
||
maybePhotoStyle: {},
|
||
|
||
userList: null,
|
||
userCache: null,
|
||
taskList: null,
|
||
fileList: {},
|
||
reportList: {},
|
||
|
||
showMenu: false,
|
||
showMore: false,
|
||
showEmoji: false,
|
||
|
||
emojiQuickShow: false,
|
||
emojiQuickKey: '',
|
||
emojiQuickItems: [],
|
||
|
||
recordReady: false,
|
||
recordRec: null,
|
||
recordBlob: null,
|
||
recordWave: null,
|
||
recordInter: null,
|
||
recordState: "stop",
|
||
recordDuration: 0,
|
||
recordIndex: window.modalTransferIndex,
|
||
|
||
recordConvertIng: false,
|
||
recordConvertFocus: false,
|
||
recordConvertSetting: false, // 是否显示转换设置
|
||
recordConvertStatus: 0, // 0: 转换中, 1: 转换成功, 2: 转换失败
|
||
recordConvertResult: '', // 转换结果
|
||
recordConvertTranslate: '', // 转换结果翻译语言
|
||
|
||
touchStart: {},
|
||
touchFocus: false,
|
||
touchLimitX: false,
|
||
touchLimitY: false,
|
||
|
||
pasteClean: true,
|
||
|
||
changeLoad: 0,
|
||
|
||
isSpecVersion: this.checkIOSVersion(),
|
||
|
||
emojiTimer: null,
|
||
scrollTimer: null,
|
||
textTimer: null,
|
||
fileTimer: null,
|
||
reportTimer: null,
|
||
moreTimer: null,
|
||
selectTimer: null,
|
||
selectRange: null,
|
||
selectedText: false,
|
||
|
||
fullInput: false,
|
||
fullQuill: null,
|
||
fullSelected: false,
|
||
fullSelection: null,
|
||
|
||
tools: [
|
||
{
|
||
label: 'bold',
|
||
type: '',
|
||
icon: '',
|
||
},
|
||
{
|
||
label: 'strike',
|
||
type: '',
|
||
icon: '',
|
||
},
|
||
{
|
||
label: 'italic',
|
||
type: '',
|
||
icon: '',
|
||
},
|
||
{
|
||
label: 'underline',
|
||
type: '',
|
||
icon: '',
|
||
},
|
||
{
|
||
label: 'blockquote',
|
||
type: '',
|
||
icon: '',
|
||
},
|
||
{
|
||
label: 'link',
|
||
type: '',
|
||
icon: '',
|
||
},
|
||
{
|
||
label: 'list',
|
||
type: 'ordered',
|
||
icon: '',
|
||
},
|
||
{
|
||
label: 'list',
|
||
type: 'bullet',
|
||
icon: '',
|
||
},
|
||
{
|
||
label: 'list',
|
||
type: 'unchecked',
|
||
icon: '',
|
||
},
|
||
],
|
||
|
||
iOSDevices: $A.isIos(),
|
||
};
|
||
},
|
||
created() {
|
||
inputLoadAdd(this._uid)
|
||
},
|
||
mounted() {
|
||
this.init();
|
||
this.refreshHistoryContext();
|
||
//
|
||
this.recordInter = setInterval(_ => {
|
||
if (this.recordState === 'ing') {
|
||
// 录音中,但录音时长不增加则取消录音
|
||
if (this.__recordDuration && this.__recordDuration === this.recordDuration) {
|
||
this.__recordDuration = null;
|
||
this.stopRecord(true);
|
||
$A.messageWarning("录音失败,请重试")
|
||
} else {
|
||
this.__recordDuration = this.recordDuration;
|
||
}
|
||
}
|
||
}, 1000)
|
||
//
|
||
if (this.$isEEUIApp) {
|
||
window.__onPermissionRequest = (type, result) => {
|
||
if (type === 'recordAudio' && result === false) {
|
||
// Android 录音权限被拒绝了
|
||
this.stopRecord(true);
|
||
}
|
||
}
|
||
}
|
||
//
|
||
$A.loadScript('js/emoticon.all.js')
|
||
},
|
||
beforeDestroy() {
|
||
inputLoadRemove(this._uid)
|
||
if (this.quill) {
|
||
this.quill.getModule("mention")?.hideMentionList();
|
||
this.quill = null
|
||
}
|
||
if (this.recordRec) {
|
||
this.recordRec = null
|
||
}
|
||
if (this.recordConvertIng) {
|
||
this.recordConvertIng = false
|
||
}
|
||
if (this.recordInter) {
|
||
clearInterval(this.recordInter)
|
||
}
|
||
},
|
||
computed: {
|
||
...mapState([
|
||
'cacheProjects',
|
||
'cacheTasks',
|
||
'cacheUserBasic',
|
||
|
||
'cacheDialogs',
|
||
'dialogMsgs',
|
||
|
||
'cacheTranscriptionLanguage',
|
||
'cacheKeyboard',
|
||
'keyboardShow',
|
||
'keyboardHeight',
|
||
'isModKey',
|
||
'safeAreaSize',
|
||
'viewportHeight',
|
||
]),
|
||
|
||
...mapGetters(['getDialogDraft', 'getDialogQuote']),
|
||
|
||
isEnterSend({cacheKeyboard}) {
|
||
if (this.$isEEUIApp) {
|
||
return cacheKeyboard.send_button_app === 'enter';
|
||
} else {
|
||
return cacheKeyboard.send_button_desktop === 'enter';
|
||
}
|
||
},
|
||
|
||
isAiBot({dialogData}) {
|
||
if (!dialogData.bot || dialogData.type !== 'user') {
|
||
return false
|
||
}
|
||
return /^ai-(.*?)@bot\.system/.test(dialogData.email)
|
||
},
|
||
|
||
canCall() {
|
||
return this.dialogData.type === 'user' && !this.dialogData.bot && this.$isEEUIApp
|
||
},
|
||
|
||
canAnon() {
|
||
return this.dialogData.type === 'user' && !this.dialogData.bot
|
||
},
|
||
|
||
recordShow() {
|
||
const {recordState} = this;
|
||
return ['ready', 'ing'].includes(recordState)
|
||
},
|
||
|
||
recordStyle() {
|
||
const {windowScrollY, recordIndex} = this;
|
||
const style = {
|
||
zIndex: recordIndex,
|
||
}
|
||
if (windowScrollY > 0) {
|
||
style.marginTop = (windowScrollY / 2) + 'px'
|
||
}
|
||
return style
|
||
},
|
||
|
||
recordConvertStyle() {
|
||
const {recordIndex} = this;
|
||
return {
|
||
zIndex: recordIndex,
|
||
}
|
||
},
|
||
|
||
recordConvertFooterStyle() {
|
||
const {recordConvertFocus, keyboardShow, keyboardHeight} = this;
|
||
return (recordConvertFocus && keyboardShow && keyboardHeight > 120 && $A.isIos()) ? {
|
||
alignItems: 'flex-start',
|
||
transform: 'translateY(12px)'
|
||
} : {}
|
||
},
|
||
|
||
boxClass() {
|
||
const array = [];
|
||
if (this.recordShow) {
|
||
if (this.recordState === 'ing' && this.recordDuration > 0) {
|
||
array.push('record-progress');
|
||
} else {
|
||
array.push('record-ready');
|
||
}
|
||
}
|
||
if (this.simpleMode) {
|
||
array.push('simple-mode');
|
||
}
|
||
if (this.showMenu) {
|
||
array.push('show-menu');
|
||
}
|
||
if (this.showMore) {
|
||
array.push('show-more');
|
||
}
|
||
if (this.showEmoji) {
|
||
array.push('show-emoji');
|
||
}
|
||
if (this.mentionMode) {
|
||
array.push(this.mentionMode);
|
||
}
|
||
return array
|
||
},
|
||
|
||
sendClass() {
|
||
if ($A.filterInvalidLine(this.value)) {
|
||
return 'sender';
|
||
}
|
||
if (this.recordReady) {
|
||
return 'recorder'
|
||
}
|
||
return ''
|
||
},
|
||
|
||
sendContent() {
|
||
this.tempHiddenSendTip();
|
||
return this.sendClass === 'recorder' ? '长按录音' : '发送'
|
||
},
|
||
|
||
recordFormatDuration() {
|
||
const {recordDuration} = this;
|
||
let minute = Math.floor(recordDuration / 60000),
|
||
seconds = Math.floor(recordDuration / 1000) % 60,
|
||
millisecond = ("00" + recordDuration % 1000).substr(-2)
|
||
if (minute < 10) minute = `0${minute}`
|
||
if (seconds < 10) seconds = `0${seconds}`
|
||
return `${minute}:${seconds}″${millisecond}`
|
||
},
|
||
|
||
recordClassName({touchLimitX, touchLimitY}) {
|
||
if (touchLimitY) {
|
||
return 'cancel'
|
||
} else if (touchLimitX) {
|
||
return 'convert'
|
||
}
|
||
return ''
|
||
},
|
||
|
||
recordFormatTip({touchLimitX, touchLimitY}) {
|
||
if (touchLimitY) {
|
||
return '松开取消'
|
||
} else if (touchLimitX) {
|
||
return '转文字'
|
||
}
|
||
return '向上滑动取消'
|
||
},
|
||
|
||
dialogData() {
|
||
return this.dialogId > 0 ? (this.cacheDialogs.find(({id}) => id == this.dialogId) || {}) : {};
|
||
},
|
||
|
||
draftId() {
|
||
return this.dialogId || `t_${this.taskId}`
|
||
},
|
||
|
||
draftData() {
|
||
return this.getDialogDraft(this.draftId)?.content || ''
|
||
},
|
||
|
||
quoteData() {
|
||
return this.getDialogQuote(this.dialogId)?.content || null
|
||
},
|
||
|
||
quoteUpdate() {
|
||
return this.getDialogQuote(this.dialogId)?.type === 'update'
|
||
},
|
||
|
||
chatInputBoxStyle({iOSDevices, fullInput, keyboardShow, viewportHeight, safeAreaSize}) {
|
||
const style = {}
|
||
if (iOSDevices && fullInput && keyboardShow && viewportHeight > 0 && $A.isIos()) {
|
||
style.height = Math.max(100, viewportHeight - 70 - safeAreaSize.top) + 'px'
|
||
} else {
|
||
style.paddingBottom = `${safeAreaSize.bottom}px`
|
||
}
|
||
return style
|
||
}
|
||
},
|
||
watch: {
|
||
// Watch content change
|
||
value(val) {
|
||
if (this.quill) {
|
||
if (val && val !== this._content) {
|
||
this._content = val
|
||
this.setContent(val)
|
||
} else if(!val) {
|
||
this.quill.setText('')
|
||
}
|
||
}
|
||
if (!this.simpleMode) {
|
||
this.$store.dispatch("saveDialogDraft", {id: this.draftId, content: val})
|
||
}
|
||
},
|
||
|
||
// Watch disabled change
|
||
disabled(val) {
|
||
this.quill?.enable(!val)
|
||
},
|
||
|
||
// Reset lists
|
||
dialogId() {
|
||
this.selectRange = null;
|
||
this.userList = null;
|
||
this.userCache = null;
|
||
this.taskList = null;
|
||
this.fileList = {};
|
||
this.reportList = {};
|
||
this.loadInputDraft()
|
||
this.refreshHistoryContext();
|
||
},
|
||
taskId() {
|
||
this.selectRange = null;
|
||
this.userList = null;
|
||
this.userCache = null;
|
||
this.taskList = null;
|
||
this.fileList = {};
|
||
this.reportList = {};
|
||
this.loadInputDraft()
|
||
this.refreshHistoryContext();
|
||
},
|
||
|
||
draftData() {
|
||
if (this.isFocus) {
|
||
return
|
||
}
|
||
this.loadInputDraft()
|
||
},
|
||
|
||
quoteData() {
|
||
this.quoteChanged = true
|
||
},
|
||
|
||
showMenu(val) {
|
||
if (val) {
|
||
// this.showMenu = false;
|
||
this.showMore = false;
|
||
this.showEmoji = false;
|
||
this.emojiQuickShow = false;
|
||
}
|
||
},
|
||
|
||
showMore(val) {
|
||
this.maybePhotoShow = false
|
||
if (val) {
|
||
this.showMenu = false;
|
||
// this.showMore = false;
|
||
this.showEmoji = false;
|
||
this.emojiQuickShow = false;
|
||
//
|
||
if (this.isAiBot) {
|
||
return
|
||
}
|
||
$A.eeuiAppGetLatestPhoto().then(({thumbnail, original}) => {
|
||
const size = Math.min(120, Math.max(100, this.$refs.moreFull.clientWidth));
|
||
this.maybePhotoStyle = {
|
||
width: size + 'px',
|
||
height: size + 'px',
|
||
backgroundImage: `url(${thumbnail.base64})`,
|
||
}
|
||
this.maybePhotoData = {thumbnail, original}
|
||
this.maybePhotoShow = true
|
||
this.$nextTick(() => {
|
||
this.$refs.more?.updatePopper()
|
||
})
|
||
}).catch(_ => {
|
||
// 获取图片失败
|
||
})
|
||
}
|
||
},
|
||
|
||
showEmoji(val) {
|
||
if (this.emojiBottom) {
|
||
if (val) {
|
||
this.quill.enable(false)
|
||
} else if (!this.disabled) {
|
||
this.quill.enable(true)
|
||
}
|
||
}
|
||
if (val) {
|
||
let text = this.value
|
||
.replace(/ /g," ")
|
||
.replace(/<[^>]+>/g, "")
|
||
if (text
|
||
&& text.indexOf(" ") === -1
|
||
&& text.length >= 1
|
||
&& text.length <= 8) {
|
||
this.emojiQuickKey = text;
|
||
} else {
|
||
this.emojiQuickKey = "";
|
||
}
|
||
//
|
||
this.showMenu = false;
|
||
this.showMore = false;
|
||
// this.showEmoji = false;
|
||
this.emojiQuickShow = false;
|
||
if (this.quill) {
|
||
const range = this.quill.selection.savedRange;
|
||
this.rangeIndex = range ? range.index : 0
|
||
this.rangeLength = range ? range.length : 0
|
||
}
|
||
} else {
|
||
this.rangeLength = 0;
|
||
if (this.rangeIndex > 0) {
|
||
this.quill.setSelection(this.rangeIndex)
|
||
}
|
||
}
|
||
},
|
||
|
||
emojiQuickShow(val) {
|
||
if (val) {
|
||
this.showMenu = false;
|
||
this.showMore = false;
|
||
this.showEmoji = false;
|
||
// this.emojiQuickShow = false;
|
||
}
|
||
},
|
||
|
||
isFocus(val) {
|
||
if (this.scrollTimer) {
|
||
clearInterval(this.scrollTimer);
|
||
}
|
||
if (val) {
|
||
this.$emit('on-focus')
|
||
this.hidePopover()
|
||
if (this.isSpecVersion) {
|
||
// ios11.0-11.3 对scrollTop及scrolIntoView解释有bug
|
||
// 直接执行会导致输入框滚到底部被遮挡
|
||
} else if (this.windowPortrait) {
|
||
this.scrollTimer = setInterval(() => {
|
||
if (this.quill?.hasFocus()) {
|
||
this.windowScrollY > 0 && $A.scrollIntoViewIfNeeded(this.$refs.editor);
|
||
} else {
|
||
clearInterval(this.scrollTimer);
|
||
}
|
||
}, 200);
|
||
}
|
||
} else {
|
||
this.$emit('on-blur')
|
||
}
|
||
},
|
||
|
||
recordState(state) {
|
||
if (state === 'ing') {
|
||
this.recordWave = window.Recorder.FrequencyHistogramView({
|
||
elem: this.$refs.recwave,
|
||
lineCount: 90,
|
||
position: 0,
|
||
minHeight: 1,
|
||
stripeEnable: false
|
||
})
|
||
} else {
|
||
this.recordWave = null
|
||
this.$refs.recwave.innerHTML = ""
|
||
}
|
||
this.$emit('on-record-state', state)
|
||
},
|
||
|
||
recordShow(show) {
|
||
if (show) {
|
||
this.recordIndex = ++window.modalTransferIndex
|
||
}
|
||
},
|
||
|
||
recordConvertIng(show) {
|
||
if (show) {
|
||
this.recordIndex = ++window.modalTransferIndex
|
||
} else {
|
||
this.recordConvertSetting = false
|
||
}
|
||
},
|
||
|
||
fullInput(val) {
|
||
this.quill?.enable(!val)
|
||
},
|
||
|
||
windowScrollY(val) {
|
||
if (this.fullInput && val > 0) {
|
||
window.scrollTo(0, 0)
|
||
}
|
||
},
|
||
|
||
keyboardShow(val) {
|
||
if (!val && this.isFocus) {
|
||
this.isFocus = false
|
||
this.quill?.blur()
|
||
}
|
||
},
|
||
|
||
selectRange(range) {
|
||
if (range?.index) {
|
||
globalRangeIndexs[this.draftId] = range.index
|
||
}
|
||
},
|
||
},
|
||
methods: {
|
||
init() {
|
||
// Options
|
||
this._options = Object.assign({
|
||
theme: 'bubble',
|
||
bubbleTooltipTop: true,
|
||
formats: ['bold', 'strike', 'italic', 'underline', 'blockquote', 'list', 'link', 'image', 'mention'],
|
||
readOnly: false,
|
||
placeholder: this.placeholder,
|
||
modules: {
|
||
toolbar: false,
|
||
keyboard: this.simpleMode ? {} : {
|
||
bindings: {
|
||
'enter-short': {
|
||
key: "Enter",
|
||
shortKey: true,
|
||
handler: _ => {
|
||
if (!this.isEnterSend) {
|
||
this.onSend();
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
},
|
||
'enter': {
|
||
key: "Enter",
|
||
shiftKey: false,
|
||
handler: _ => {
|
||
if (this.isEnterSend) {
|
||
this.onSend();
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
},
|
||
'esc': {
|
||
key: "Escape",
|
||
shiftKey: false,
|
||
handler: _ => {
|
||
if (this.emojiQuickShow) {
|
||
this.emojiQuickShow = false;
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
},
|
||
'history-up': {
|
||
key: 38,
|
||
handler: range => this.navigateHistory('up', range)
|
||
},
|
||
'history-down': {
|
||
key: 40,
|
||
handler: range => this.navigateHistory('down', range)
|
||
}
|
||
}
|
||
},
|
||
selectionPlugin: {
|
||
onTextSelected: (selectedText) => {
|
||
if (this.$isEEUIApp || this.windowTouch) {
|
||
return
|
||
}
|
||
this.selectedText = !!selectedText.trim()
|
||
},
|
||
onSelectionCleared: () => {
|
||
this.selectedText = false
|
||
}
|
||
},
|
||
mention: this.quillMention()
|
||
}
|
||
}, this.options)
|
||
|
||
// Instance
|
||
this.quill = new Quill(this.$refs.editor, this._options)
|
||
this.quill.enable(!this.disabled)
|
||
|
||
// Set editor content
|
||
if (this.value) {
|
||
this.setContent(this.value)
|
||
} else {
|
||
this.loadInputDraft()
|
||
}
|
||
|
||
// Mark model as touched if editor lost focus
|
||
this.quill.on('selection-change', range => {
|
||
if (!this.inputActivated()) {
|
||
return;
|
||
}
|
||
if (range) {
|
||
this.selectRange = range
|
||
} else if (this.selectRange && document.activeElement && /(ql-editor|ql-clipboard)/.test(document.activeElement.className)) {
|
||
// 修复iOS光标会超出的问题
|
||
this.selectTimer && clearTimeout(this.selectTimer)
|
||
this.selectTimer = setTimeout(_ => {
|
||
this.quill.setSelection(this.selectRange.index, this.selectRange.length)
|
||
}, 100)
|
||
return
|
||
}
|
||
this.isFocus = !!range;
|
||
})
|
||
|
||
// Update model if text changes
|
||
this.quill.on('text-change', _ => {
|
||
if (this.isFocus) {
|
||
const {index} = this.quill.getSelection();
|
||
if (this.quill.getText(index - 1, 1) === "\r") {
|
||
this.quill.insertText(index, "\n");
|
||
this.quill.deleteText(index - 1, 1);
|
||
return;
|
||
}
|
||
}
|
||
if (this.textTimer) {
|
||
clearTimeout(this.textTimer)
|
||
} else {
|
||
this.changeLoad++
|
||
}
|
||
this.textTimer = setTimeout(_ => {
|
||
this.textTimer = null
|
||
this.changeLoad--
|
||
if (this.maxlength > 0 && this.quill.getLength() > this.maxlength) {
|
||
this.quill.deleteText(this.maxlength, this.quill.getLength());
|
||
}
|
||
const html = this.$refs.editor.firstChild.innerHTML;
|
||
this.updateEmojiQuick(html)
|
||
this._content = html
|
||
this.$emit('input', this._content)
|
||
}, 100)
|
||
})
|
||
|
||
// Clipboard Matcher (保留图片跟空格,清除其余所以样式)
|
||
this.quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
|
||
if (!this.pasteClean) {
|
||
return delta
|
||
}
|
||
delta.ops = delta.ops.map(op => {
|
||
const obj = {
|
||
insert: op.insert
|
||
};
|
||
if (op.attributes) {
|
||
['bold', 'strike', 'italic', 'underline', 'list', 'blockquote', 'link'].some(item => {
|
||
if (op.attributes[item]) {
|
||
if (typeof obj.attributes === "undefined") {
|
||
obj.attributes = {}
|
||
}
|
||
obj.attributes[item] = op.attributes[item]
|
||
}
|
||
})
|
||
}
|
||
return obj
|
||
})
|
||
return delta
|
||
})
|
||
|
||
// 专门处理mention的matcher - 同时处理span.mention和a.mention
|
||
this.quill.clipboard.addMatcher(['span.mention', 'a.mention'], (node, delta) => {
|
||
if (!this.pasteClean) {
|
||
return delta
|
||
}
|
||
const mention = this.extractMentionData(node)
|
||
if (mention === null) {
|
||
return delta
|
||
}
|
||
|
||
return new Delta([{
|
||
insert: {mention}
|
||
}]);
|
||
})
|
||
|
||
// Link handler
|
||
const toolbar = this.quill.getModule('toolbar')
|
||
if (toolbar?.handlers?.link) {
|
||
toolbar.addHandler('link', (value) => {
|
||
if (value) {
|
||
$A.modalInput({
|
||
title: "插入链接",
|
||
placeholder: "请输入完整的链接地址",
|
||
onOk: (link) => {
|
||
if (!link) {
|
||
return false;
|
||
}
|
||
this.quill.format('link', link);
|
||
}
|
||
})
|
||
} else {
|
||
this.quill.format('link', false);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Set enterkeyhint
|
||
this.$nextTick(_ => {
|
||
if (this.$isEEUIApp && this.cacheKeyboard.send_button_app === 'enter') {
|
||
this.quill.root.setAttribute('enterkeyhint', 'send')
|
||
}
|
||
})
|
||
|
||
// Ready event
|
||
this.$emit('on-ready', this.quill)
|
||
|
||
// Load recorder
|
||
if (!this.disabledRecord) {
|
||
const i18nLang = /^zh/.test(languageName) ? "zh-CN" : "en-US";
|
||
$A.loadScriptS([
|
||
'js/recorder/recorder.mp3.min.js',
|
||
'js/recorder/lib.fft.js',
|
||
'js/recorder/frequency.histogram.view.js',
|
||
`js/recorder/i18n/${i18nLang}.js`,
|
||
]).then(_ => {
|
||
if (typeof window.Recorder !== 'function') {
|
||
return;
|
||
}
|
||
window.Recorder.i18n.lang = i18nLang
|
||
this.recordRec = window.Recorder({
|
||
type: "mp3",
|
||
bitRate: 64,
|
||
sampleRate: 32000,
|
||
audioTrackSet: {
|
||
noiseSuppression: true,
|
||
echoCancellation: true,
|
||
},
|
||
disableEnvInFix: false,
|
||
onProcess: (buffers, powerLevel, duration, sampleRate, newBufferIdx, asyncEnd) => {
|
||
this.recordWave?.input(buffers[buffers.length - 1], powerLevel, sampleRate);
|
||
this.recordDuration = duration;
|
||
if (duration >= 3 * 60 * 1000) {
|
||
// 最长录3分钟
|
||
this.stopRecord(false);
|
||
}
|
||
}
|
||
})
|
||
if (window.Recorder.Support()) {
|
||
this.recordReady = true;
|
||
}
|
||
if (window.systemInfo.debug !== "yes") {
|
||
window.Recorder.CLog = function () { }
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
quillMention() {
|
||
return {
|
||
allowedChars: /^\S*$/,
|
||
mentionDenotationChars: ["@", "#", "~", "%"],
|
||
defaultMenuOrientation: this.defaultMenuOrientation,
|
||
isolateCharacter: true,
|
||
positioningStrategy: 'fixed',
|
||
renderItem: (data) => {
|
||
if (data.disabled === true) {
|
||
return `<div class="mention-item-disabled">${data.value}</div>`;
|
||
}
|
||
const nameHtml = `<div class="mention-item-name" title="${data.value}">${data.value}</div>`;
|
||
const tipHtml = data.tip ? `<div class="mention-item-tip" title="${data.tip}">${data.tip}</div>` : '';
|
||
if (data.id === 0) {
|
||
return `<div class="mention-item-at">@</div>${nameHtml}${tipHtml}`;
|
||
}
|
||
if (data.avatar) {
|
||
const botHtml = data.bot ? `<div class="taskfont mention-item-bot"></div>` : ''
|
||
return `<div class="mention-item-img${data.online ? ' online' : ''}"><img src="${data.avatar}"/><em></em></div>${botHtml}${nameHtml}${tipHtml}`;
|
||
}
|
||
return `${nameHtml}${tipHtml}`;
|
||
},
|
||
renderLoading: () => {
|
||
return "Loading...";
|
||
},
|
||
source: (searchTerm, renderList, mentionChar) => {
|
||
const mentionName = mentionChar == "@" ? 'user-mention' : (mentionChar == "#" ? 'task-mention' : 'file-mention');
|
||
const containers = document.getElementsByClassName("ql-mention-list-container");
|
||
for (let i = 0; i < containers.length; i++) {
|
||
containers[i].classList.remove("user-mention");
|
||
containers[i].classList.remove("task-mention");
|
||
containers[i].classList.remove("file-mention");
|
||
containers[i].classList.add(mentionName);
|
||
}
|
||
let mentionSourceCache = null;
|
||
this.getMentionSource(mentionChar, searchTerm, array => {
|
||
const values = [];
|
||
array.some(item => {
|
||
let list = item.list;
|
||
if (searchTerm) {
|
||
list = list.filter(({id, value, key}) => {
|
||
if (/^\d+$/.test(searchTerm) && id && id == searchTerm) {
|
||
return true;
|
||
}
|
||
return $A.strExists(key || value, searchTerm)
|
||
});
|
||
}
|
||
if (list.length > 0) {
|
||
item.label && values.push(...item.label)
|
||
values.push(...list)
|
||
}
|
||
})
|
||
if ($A.jsonStringify(values.map(({id}) => id)) !== mentionSourceCache) {
|
||
mentionSourceCache = $A.jsonStringify(values.map(({id}) => id))
|
||
renderList(values, searchTerm);
|
||
}
|
||
})
|
||
}
|
||
}
|
||
},
|
||
|
||
extractMentionData(node) {
|
||
let denotationChar = node.getAttribute('data-denotation-char');
|
||
let dataId = node.getAttribute('data-id') || node.getAttribute('href');
|
||
let dataValue = node.getAttribute('data-value');
|
||
|
||
if (!denotationChar || !dataValue) {
|
||
const textContent = node.textContent || node.innerText || '';
|
||
const match = textContent.match(/^([@#~%])(.*)$/);
|
||
if (match) {
|
||
denotationChar = denotationChar || match[1];
|
||
dataValue = dataValue || match[2];
|
||
}
|
||
}
|
||
|
||
if (!denotationChar || !dataId || !dataValue) {
|
||
return null
|
||
}
|
||
|
||
return {
|
||
denotationChar: denotationChar,
|
||
id: dataId,
|
||
value: dataValue
|
||
};
|
||
},
|
||
|
||
updateEmojiQuick(text) {
|
||
if (!this.isFocus || !text) {
|
||
this.emojiQuickShow = false
|
||
return
|
||
}
|
||
this.emojiTimer && clearTimeout(this.emojiTimer)
|
||
this.emojiTimer = setTimeout(_ => {
|
||
this.emojiTimer = null
|
||
if (/<img/i.test(text)) {
|
||
this.emojiQuickShow = false
|
||
return
|
||
}
|
||
text = text
|
||
.replace(/ /g," ")
|
||
.replace(/<[^>]+>/g, "")
|
||
if (text
|
||
&& text.indexOf(" ") === -1
|
||
&& text.length >= 1
|
||
&& text.length <= 8
|
||
&& $A.isArray(window.emoticonData)) {
|
||
// 显示快捷选择表情窗口
|
||
this.emojiQuickItems = [];
|
||
const baseUrl = $A.mainUrl("images/emoticon")
|
||
window.emoticonData.some(data => {
|
||
let j = 0
|
||
data.list.some(item => {
|
||
const arr = [item.name]
|
||
if (item.key) {
|
||
arr.push(...(`${item.key}`).split(" "))
|
||
}
|
||
if (arr.includes(text)) {
|
||
this.emojiQuickItems.push(Object.assign(item, {
|
||
type: `emoticon`,
|
||
asset: `images/emoticon/${data.path}/${item.path}`,
|
||
name: item.name,
|
||
src: `${baseUrl}/${data.path}/${item.path}`
|
||
}))
|
||
if (++j >= 2) {
|
||
return true
|
||
}
|
||
}
|
||
})
|
||
if (this.emojiQuickItems.length >= 20) {
|
||
return true
|
||
}
|
||
});
|
||
if (this.emojiQuickItems.length > 0) {
|
||
this.$refs.emojiWrapper.$el.style.maxWidth = `${Math.min(500, this.$refs.inputWrapper.clientWidth)}px`
|
||
this.$nextTick(_ => {
|
||
this.emojiQuickShow = true
|
||
this.$refs.emojiQuickRef.updatePopper()
|
||
})
|
||
return
|
||
}
|
||
}
|
||
this.emojiQuickShow = false
|
||
}, 100)
|
||
},
|
||
|
||
inputActivated() {
|
||
return !this.fullInput && inputLoadIsLast(this._uid)
|
||
},
|
||
|
||
getEditor() {
|
||
return this.fullInput ? this.fullQuill : this.quill
|
||
},
|
||
|
||
getText() {
|
||
if (this.quill) {
|
||
return `${this.quill.getText()}`.replace(/^\s+|\s+$/g, "")
|
||
}
|
||
return "";
|
||
},
|
||
|
||
insertText(text) {
|
||
if (this.quill) {
|
||
const {index} = this.quill.getSelection(true);
|
||
this.quill.insertText(index, text)
|
||
}
|
||
},
|
||
|
||
setText(value) {
|
||
if (this.quill) {
|
||
this.quill.setText(value)
|
||
}
|
||
},
|
||
|
||
setContent(value) {
|
||
if (this.quill) {
|
||
this.quill.setContents(this.quill.clipboard.convert({html: value}))
|
||
}
|
||
},
|
||
|
||
setPasteMode(bool) {
|
||
this.pasteClean = bool
|
||
},
|
||
|
||
loadInputDraft() {
|
||
if (this.simpleMode || !this.draftData) {
|
||
this.$emit('input', '')
|
||
return
|
||
}
|
||
this.pasteClean = false
|
||
this.$emit('input', this.draftData)
|
||
this.$nextTick(_ => this.pasteClean = true)
|
||
},
|
||
|
||
onClickEditor() {
|
||
this.clearSearchKey()
|
||
this.updateEmojiQuick(this.value)
|
||
!this.isFocus && this.focus()
|
||
inputLoadAdd(this._uid)
|
||
},
|
||
|
||
clearSearchKey() {
|
||
if (this.$parent.$options.name === 'DialogWrapper' && (this.$store.state.messengerSearchKey.dialog != '' || this.$store.state.messengerSearchKey.contacts != '')) {
|
||
setTimeout(_ => {
|
||
this.$parent.onActive();
|
||
}, 10)
|
||
}
|
||
this.$store.state.messengerSearchKey = {dialog: '', contacts: ''}
|
||
},
|
||
|
||
focus() {
|
||
this.$nextTick(() => {
|
||
const quill = this.getEditor();
|
||
if (quill) {
|
||
if (!this.selectRange?.index) {
|
||
const length = quill.getLength();
|
||
quill.setSelection(Math.min(globalRangeIndexs[this.draftId] || length, length));
|
||
}
|
||
quill.focus()
|
||
}
|
||
})
|
||
},
|
||
|
||
blur() {
|
||
this.$nextTick(() => {
|
||
this.getEditor()?.blur()
|
||
})
|
||
},
|
||
|
||
clickSend(action, event) {
|
||
if (this.loading) {
|
||
return;
|
||
}
|
||
switch (action) {
|
||
case 'down':
|
||
this.touchFocus = this.quill?.hasFocus();
|
||
this.touchLimitX = false;
|
||
this.touchLimitY = false;
|
||
this.touchStart = event.type === "touchstart" ? event.touches[0] : event;
|
||
if ((event.button === undefined || event.button === 0) && this.startRecord()) {
|
||
return;
|
||
}
|
||
if (event.button === 2){
|
||
this.onShowMenu()
|
||
}
|
||
break;
|
||
|
||
case 'move':
|
||
const touchMove = event.type === "touchmove" ? event.touches[0] : event;
|
||
this.touchLimitX = (this.touchStart.clientX - touchMove.clientX) / window.innerWidth > 0.1
|
||
this.touchLimitY = (this.touchStart.clientY - touchMove.clientY) / window.innerHeight > 0.1
|
||
break;
|
||
|
||
case 'up':
|
||
if (this.showMenu) {
|
||
return;
|
||
}
|
||
if (this.stopRecord(this.touchLimitY, this.touchLimitX)) {
|
||
return;
|
||
}
|
||
if (this.touchLimitY || this.touchLimitX) {
|
||
return; // 移动了 X、Y 轴
|
||
}
|
||
this.onSend()
|
||
break;
|
||
|
||
case 'click':
|
||
if (this.showMenu) {
|
||
this.tempHiddenSendTip()
|
||
this.showMenu = false;
|
||
}
|
||
if (this.touchFocus) {
|
||
this.quill.blur();
|
||
this.quill.focus();
|
||
}
|
||
break;
|
||
}
|
||
},
|
||
|
||
onShowMenu() {
|
||
if (this.sendClass === 'recorder' || !this.sendMenu) {
|
||
return;
|
||
}
|
||
this.showMenu = true;
|
||
},
|
||
|
||
onSend(type = 'auto') {
|
||
this.emojiTimer && clearTimeout(this.emojiTimer)
|
||
this.emojiQuickShow = false;
|
||
//
|
||
setTimeout(_ => {
|
||
if ($A.filterInvalidLine(this.value) === '') {
|
||
return
|
||
}
|
||
this.hidePopover('send')
|
||
this.rangeIndex = 0
|
||
this.clearSearchKey()
|
||
//
|
||
if (type === 'auto') {
|
||
type = isMarkdownFormat(this.value) ? 'md' : ''
|
||
}
|
||
if (type === 'normal') {
|
||
type = ''
|
||
}
|
||
const content = this.value;
|
||
this.persistInputHistory(content);
|
||
if (type) {
|
||
this.$emit('on-send', null, type)
|
||
} else {
|
||
this.$emit('on-send')
|
||
}
|
||
}, this.changeLoad > 0 ? 100 : 0)
|
||
},
|
||
|
||
startRecord() {
|
||
if (this.sendClass === 'recorder') {
|
||
this.$store.dispatch("audioStop", true)
|
||
this.recordDuration = 0;
|
||
this.recordState = "ready";
|
||
this.$nextTick(_ => {
|
||
this.recordRec.open(_ => {
|
||
if (this.recordState === "ready") {
|
||
this.recordState = "ing"
|
||
this.recordBlob = null
|
||
setTimeout(_ => {
|
||
if (this.recordState == "stop") {
|
||
this.recordRec.close();
|
||
} else {
|
||
this.recordRec.start()
|
||
}
|
||
}, 300)
|
||
} else {
|
||
this.recordRec.close();
|
||
}
|
||
}, (msg) => {
|
||
this.recordState = "stop";
|
||
$A.messageError(msg || '打开录音失败')
|
||
});
|
||
})
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
},
|
||
|
||
stopRecord(isCancel, isConvert = false) {
|
||
switch (this.recordState) {
|
||
case "ing":
|
||
this.recordState = "stop";
|
||
this.recordRec.stop((blob, duration) => {
|
||
this.recordRec.close();
|
||
if (isCancel === true) {
|
||
return;
|
||
}
|
||
if (duration < 600) {
|
||
$A.messageWarning("说话时间太短") // 小于 600ms 不发送
|
||
} else {
|
||
this.recordBlob = blob;
|
||
this.recordDuration = duration;
|
||
if (isConvert === true) {
|
||
this.blur();
|
||
this.convertRecord();
|
||
} else {
|
||
this.uploadRecord();
|
||
}
|
||
}
|
||
}, (msg) => {
|
||
this.recordRec.close();
|
||
$A.messageError(msg || "录音失败");
|
||
});
|
||
return true;
|
||
|
||
case "ready":
|
||
this.recordState = "stop";
|
||
return true;
|
||
|
||
default:
|
||
this.recordState = "stop";
|
||
return false;
|
||
}
|
||
},
|
||
|
||
hidePopover(action) {
|
||
this.showMenu = false;
|
||
this.showMore = false;
|
||
if (action === 'send') {
|
||
return
|
||
}
|
||
this.showEmoji = false;
|
||
this.emojiQuickShow = false;
|
||
},
|
||
|
||
onClickCover() {
|
||
this.hidePopover();
|
||
this.$nextTick(_ => {
|
||
this.quill?.focus()
|
||
})
|
||
},
|
||
|
||
onTouchClick(e, el) {
|
||
let action = el.getAttribute('data-action')
|
||
if (action === "children") {
|
||
action = e.target?.getAttribute('data-action')
|
||
}
|
||
switch (action) {
|
||
case "cancel-quote":
|
||
this.cancelQuote()
|
||
break;
|
||
|
||
case "record-convert-cancel":
|
||
this.recordConvertIng = false
|
||
break;
|
||
|
||
case "record-convert-voice":
|
||
this.convertSend('voice')
|
||
break;
|
||
|
||
case "record-convert-result":
|
||
this.convertSend('result')
|
||
break;
|
||
}
|
||
},
|
||
|
||
convertRecord() {
|
||
if (this.recordBlob === null) {
|
||
this.recordConvertIng = false
|
||
return;
|
||
}
|
||
this.recordConvertResult = ''
|
||
this.recordConvertStatus = 0
|
||
this.recordConvertIng = true
|
||
//
|
||
const reader = new FileReader();
|
||
reader.onloadend = () => {
|
||
this.$store.dispatch("call", {
|
||
url: 'dialog/msg/convertrecord',
|
||
data: {
|
||
dialog_id: this.dialogId,
|
||
base64: reader.result,
|
||
duration: this.recordDuration,
|
||
language: this.cacheTranscriptionLanguage,
|
||
translate: this.recordConvertTranslate
|
||
},
|
||
method: 'post',
|
||
}).then(({data}) => {
|
||
if (data) {
|
||
this.recordConvertStatus = 1
|
||
this.recordConvertResult = data
|
||
this.recordConvertSetting = true
|
||
} else {
|
||
this.recordConvertStatus = 2
|
||
this.recordConvertResult = this.$L('转文字失败')
|
||
}
|
||
}).catch(({msg}) => {
|
||
this.recordConvertStatus = 2
|
||
this.recordConvertResult = msg
|
||
});
|
||
};
|
||
reader.readAsDataURL(this.recordBlob);
|
||
},
|
||
|
||
async convertSetting(type, event) {
|
||
if (this.recordConvertStatus !== 1) {
|
||
$A.messageWarning("请稍后再试...")
|
||
return;
|
||
}
|
||
await this.$nextTick()
|
||
const list = Object.keys(languageList).map(item => ({
|
||
label: languageList[item],
|
||
value: item
|
||
}))
|
||
let active
|
||
if (type === 'transcription') {
|
||
// 语音转文字
|
||
list.unshift(...[
|
||
{label: this.$L('选择识别语言'), value: '', disabled: true},
|
||
{label: this.$L('自动识别'), value: '', divided: true},
|
||
])
|
||
active = this.cacheTranscriptionLanguage
|
||
} else {
|
||
// 翻译
|
||
list.unshift(...[
|
||
{label: this.$L('选择翻译结果'), value: '', disabled: true},
|
||
{label: this.$L('不翻译结果'), value: '', divided: true},
|
||
])
|
||
active = this.recordConvertTranslate
|
||
}
|
||
this.$store.commit('menu/operation', {
|
||
event,
|
||
list,
|
||
active,
|
||
language: false,
|
||
onUpdate: async (language) => {
|
||
if (type === 'transcription') {
|
||
await this.$store.dispatch('setTranscriptionLanguage', language)
|
||
} else {
|
||
this.recordConvertTranslate = language
|
||
}
|
||
this.convertRecord()
|
||
}
|
||
})
|
||
},
|
||
|
||
convertSend(type) {
|
||
if (!this.recordConvertIng) {
|
||
return;
|
||
}
|
||
if (type === 'voice') {
|
||
this.uploadRecord();
|
||
this.recordConvertIng = false
|
||
} else {
|
||
if (this.recordConvertStatus === 1) {
|
||
this.$emit('on-send', this.recordConvertResult)
|
||
this.recordConvertIng = false
|
||
} else if (this.recordConvertStatus === 2) {
|
||
this.convertRecord()
|
||
}
|
||
}
|
||
},
|
||
|
||
uploadRecord() {
|
||
if (this.recordBlob === null) {
|
||
return;
|
||
}
|
||
const reader = new FileReader();
|
||
reader.onloadend = () => {
|
||
this.$emit('on-record', {
|
||
type: this.recordBlob.type,
|
||
base64: reader.result,
|
||
duration: this.recordDuration,
|
||
})
|
||
};
|
||
reader.readAsDataURL(this.recordBlob);
|
||
},
|
||
|
||
onEmojiQuick(item) {
|
||
if (item.type === 'online') {
|
||
this.$emit('on-send', `<img src="${item.src}"/>`)
|
||
} else {
|
||
this.$emit('on-send', `<img class="emoticon" data-asset="${item.asset}" data-name="${item.name}" src="${item.src}"/>`)
|
||
}
|
||
this.$emit('input', "")
|
||
this.emojiQuickShow = false
|
||
this.focus()
|
||
},
|
||
|
||
onSelectEmoji(item) {
|
||
if (!this.quill) {
|
||
return;
|
||
}
|
||
if (item.type === 'emoji') {
|
||
this.quill.insertText(this.rangeIndex, item.text);
|
||
this.rangeIndex += item.text.length
|
||
this.rangeLength = 0;
|
||
this.quill.setSelection(this.rangeIndex, 0, 'silent');
|
||
if (this.windowLandscape && !this.isModKey) {
|
||
this.showEmoji = false;
|
||
}
|
||
} else if (item.type === 'emoticon') {
|
||
this.$emit('on-send', `<img class="emoticon" data-asset="${item.asset}" data-name="${item.name}" src="${item.src}"/>`)
|
||
if (item.asset === "emosearch") {
|
||
this.$emit('input', "")
|
||
}
|
||
if (this.windowLandscape && !this.isModKey) {
|
||
this.showEmoji = false;
|
||
}
|
||
}
|
||
},
|
||
|
||
onEmojiDelete() {
|
||
if (!this.quill) {
|
||
return;
|
||
}
|
||
const savedRange = this.quill.selection?.savedRange || this.quill.getSelection();
|
||
if (savedRange && typeof savedRange.index === 'number') {
|
||
this.rangeIndex = savedRange.index;
|
||
this.rangeLength = savedRange.length || 0;
|
||
}
|
||
if (this.rangeLength > 0) {
|
||
this.quill.deleteText(this.rangeIndex, this.rangeLength);
|
||
this.rangeLength = 0;
|
||
} else if (this.rangeIndex > 0) {
|
||
const deleteLength = this.getPreviousGraphemeLength(this.rangeIndex);
|
||
if (deleteLength > 0) {
|
||
this.quill.deleteText(this.rangeIndex - deleteLength, deleteLength);
|
||
this.rangeIndex -= deleteLength;
|
||
}
|
||
}
|
||
this.quill.setSelection(this.rangeIndex, 0, 'silent');
|
||
},
|
||
|
||
getPreviousGraphemeLength(index) {
|
||
if (!this.quill || index <= 0) {
|
||
return 0;
|
||
}
|
||
const textBeforeCursor = this.quill.getText(0, index);
|
||
if (!textBeforeCursor) {
|
||
return 0;
|
||
}
|
||
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
||
if (!this.graphemeSegmenter) {
|
||
this.graphemeSegmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
|
||
}
|
||
let lastSegment;
|
||
for (const segment of this.graphemeSegmenter.segment(textBeforeCursor)) {
|
||
lastSegment = segment;
|
||
}
|
||
if (lastSegment && lastSegment.segment) {
|
||
return lastSegment.segment.length;
|
||
}
|
||
}
|
||
const fallbackWindow = Math.min(index, 8);
|
||
const fallbackText = this.quill.getText(index - fallbackWindow, fallbackWindow);
|
||
if (!fallbackText) {
|
||
return 0;
|
||
}
|
||
const fallbackGraphemes = Array.from(fallbackText);
|
||
const lastGrapheme = fallbackGraphemes.pop();
|
||
return lastGrapheme ? lastGrapheme.length : 0;
|
||
},
|
||
|
||
onToolbar(action) {
|
||
this.hidePopover();
|
||
switch (action) {
|
||
case 'user':
|
||
this.openMenu("@");
|
||
break;
|
||
|
||
case 'task':
|
||
this.openMenu("#");
|
||
break;
|
||
|
||
case 'ai':
|
||
this.onMessageAI();
|
||
break;
|
||
|
||
case 'maybe-photo':
|
||
this.$emit('on-file', {
|
||
type: 'photo',
|
||
msg: {
|
||
type: 'img',
|
||
filename: this.maybePhotoData.original.name,
|
||
path: this.maybePhotoData.original.path,
|
||
width: this.maybePhotoData.original.width,
|
||
height: this.maybePhotoData.original.height,
|
||
thumb: this.maybePhotoData.thumbnail.base64,
|
||
}
|
||
})
|
||
break;
|
||
|
||
case 'meeting':
|
||
emitter.emit('addMeeting', {
|
||
type: 'create',
|
||
dialog_id: this.dialogId,
|
||
userids: [this.userId],
|
||
});
|
||
break;
|
||
|
||
case 'full':
|
||
this.onFullInput()
|
||
break;
|
||
|
||
case 'image':
|
||
case 'file':
|
||
case 'call':
|
||
case 'anon':
|
||
this.$emit('on-more', action)
|
||
break;
|
||
|
||
case 'word-chain':
|
||
this.$store.state.dialogDroupWordChain = {
|
||
type: 'create',
|
||
dialog_id: this.dialogId
|
||
}
|
||
break;
|
||
|
||
case 'vote':
|
||
this.$store.state.dialogGroupVote = {
|
||
type: 'create',
|
||
dialog_id: this.dialogId
|
||
}
|
||
break;
|
||
|
||
}
|
||
},
|
||
|
||
onMessageAI() {
|
||
if (this.disabled) {
|
||
return;
|
||
}
|
||
if (!this.dialogId) {
|
||
$A.messageWarning('当前未选择会话');
|
||
return;
|
||
}
|
||
emitter.emit('openAIAssistant', {
|
||
placeholder: this.$L('请简要描述消息的主题、语气或要点,AI 将生成完整消息'),
|
||
onBeforeSend: async (sendData) => {
|
||
if (!sendData) {
|
||
return sendData;
|
||
}
|
||
try {
|
||
const {data: extraData} = await this.$store.dispatch('call', {
|
||
url: 'dialog/msg/aiprompt',
|
||
data: {
|
||
dialog_id: this.dialogId,
|
||
content: sendData.text,
|
||
draft: this.value || '',
|
||
quote_id: this.quoteData?.id || 0,
|
||
},
|
||
});
|
||
if ($A.isJson(extraData)) {
|
||
sendData.extra_data = extraData;
|
||
}
|
||
return sendData;
|
||
} catch (error) {
|
||
const msg = error?.msg || 'AI 提示生成失败';
|
||
$A.modalError(msg);
|
||
throw error;
|
||
}
|
||
},
|
||
onOk: ({aiContent}) => {
|
||
if (!aiContent) {
|
||
$A.messageWarning('AI 未生成内容');
|
||
return;
|
||
}
|
||
const html = MarkdownConver(aiContent);
|
||
this.$emit('input', html);
|
||
this.$nextTick(() => this.focus());
|
||
},
|
||
});
|
||
},
|
||
|
||
onFullInput() {
|
||
if (this.disabled) {
|
||
return
|
||
}
|
||
this.fullInput = !this.fullInput;
|
||
//
|
||
if (this.fullInput) {
|
||
this.$nextTick(_ => {
|
||
this.fullQuill = new Quill(this.$refs.editorFull, Object.assign({
|
||
theme: 'bubble',
|
||
readOnly: false,
|
||
placeholder: this.placeholder,
|
||
modules: {
|
||
toolbar: false,
|
||
selectionPlugin: {
|
||
onTextSelected: (selectedText) => {
|
||
this.fullSelected = !!selectedText.trim()
|
||
},
|
||
onSelectionCleared: () => {
|
||
this.fullSelected = false
|
||
}
|
||
},
|
||
mention: this.quillMention()
|
||
}
|
||
}, this.options))
|
||
this.fullQuill.on('selection-change', range => {
|
||
if (range) {
|
||
this.fullSelection = range
|
||
} else if (this.fullSelection && document.activeElement && /(ql-editor|ql-clipboard)/.test(document.activeElement.className)) {
|
||
// 修复iOS光标会超出的问题
|
||
this.selectTimer && clearTimeout(this.selectTimer)
|
||
this.selectTimer = setTimeout(_ => {
|
||
this.fullQuill.setSelection(this.fullSelection.index, this.fullSelection.length)
|
||
}, 100)
|
||
}
|
||
})
|
||
this.fullQuill.enable(true)
|
||
this.$refs.editorFull.firstChild.innerHTML = this.$refs.editor.firstChild.innerHTML
|
||
this.$nextTick(_ => {
|
||
this.fullQuill.setSelection(this.fullQuill.getLength())
|
||
this.fullQuill.focus()
|
||
})
|
||
})
|
||
}
|
||
},
|
||
|
||
onFullBeforeClose() {
|
||
return new Promise(resolve => {
|
||
if (this.$refs.editorFull?.firstChild) {
|
||
this.$refs.editor.firstChild.innerHTML = this.$refs.editorFull.firstChild.innerHTML
|
||
}
|
||
resolve()
|
||
})
|
||
},
|
||
|
||
onMenu(action, _, el) {
|
||
if (action !== 'up') {
|
||
return;
|
||
}
|
||
const quill = this.getEditor();
|
||
const {length} = quill.getSelection(true);
|
||
if (length === 0) {
|
||
$A.messageWarning("请选择文字后再操作")
|
||
return
|
||
}
|
||
const label = el.getAttribute('data-label');
|
||
switch (label) {
|
||
case 'bold':
|
||
quill.format('bold', !quill.getFormat().bold);
|
||
break;
|
||
case 'strike':
|
||
quill.format('strike', !quill.getFormat().strike);
|
||
break;
|
||
case 'italic':
|
||
quill.format('italic', !quill.getFormat().italic);
|
||
break;
|
||
case 'underline':
|
||
quill.format('underline', !quill.getFormat().underline);
|
||
break;
|
||
case 'blockquote':
|
||
quill.format('blockquote', !quill.getFormat().blockquote);
|
||
break;
|
||
case 'link':
|
||
if (quill.getFormat().link) {
|
||
quill.format('link', false);
|
||
return
|
||
}
|
||
$A.modalInput({
|
||
title: "插入链接",
|
||
placeholder: "请输入完整的链接地址",
|
||
onOk: (link) => {
|
||
if (!link) {
|
||
return false;
|
||
}
|
||
quill.format('link', link);
|
||
}
|
||
})
|
||
break;
|
||
case 'list':
|
||
const type = el.getAttribute('data-type') || '';
|
||
quill.format('list', quill.getFormat().list === type ? false : type);
|
||
break;
|
||
}
|
||
},
|
||
|
||
setQuote(id, type = 'reply') {
|
||
if (this.dialogId <= 0) {
|
||
return
|
||
}
|
||
const content = this.dialogMsgs.find(item => item.id == id && item.dialog_id == this.dialogId)
|
||
if (!content) {
|
||
this.$store.dispatch("removeDialogQuote", this.dialogId);
|
||
return
|
||
}
|
||
this.$store.dispatch("saveDialogQuote", {
|
||
id: this.dialogId,
|
||
type: type === 'update' ? 'update' : 'reply',
|
||
content
|
||
});
|
||
},
|
||
|
||
cancelQuote() {
|
||
if (this.quoteUpdate) {
|
||
// 取消修改
|
||
this.$emit('input', '')
|
||
} else if (this.quoteData) {
|
||
// 取消回复
|
||
const {firstChild} = this.$refs.editor;
|
||
if (firstChild && firstChild.querySelectorAll('img').length === 0) {
|
||
const mentions = firstChild.querySelectorAll("span.mention");
|
||
if (mentions.length === 1) {
|
||
const element = mentions[0];
|
||
if (element.getAttribute("data-id") == this.quoteData.userid) {
|
||
const parent = element.parentNode;
|
||
parent.normalize();
|
||
const nodes = Array.from(parent.childNodes).filter(node => {
|
||
return node.nodeType !== Node.TEXT_NODE || !/^\s*$/.test(node.textContent);
|
||
})
|
||
if (nodes.length === 1) {
|
||
element.remove();
|
||
}
|
||
}
|
||
}
|
||
if (!firstChild.innerText.replace(/\s/g, '')) {
|
||
this.$emit('input', '')
|
||
}
|
||
}
|
||
}
|
||
this.setQuote(0)
|
||
},
|
||
|
||
onQuoteUserResult(userData) {
|
||
if (!this.quoteChanged) {
|
||
return
|
||
}
|
||
this.quoteChanged = false
|
||
// 基本判断
|
||
if (
|
||
this.dialogData.type !== 'group' || // 非群聊
|
||
this.quoteUpdate || // 修改消息
|
||
!this.quoteData || // 无引用消息
|
||
!this.replyMsgAutoMention || // 不自动@
|
||
this.userId === userData.userid || // 自己
|
||
this.quoteData.userid !== userData.userid // 不同人
|
||
) {
|
||
return
|
||
}
|
||
// 判断是否已经@过
|
||
if (new RegExp(`<span[^>]+?class="mention"[^>]+?data-id="${userData.userid}"[^>]*?>`).test(this.$refs.editor.firstChild?.innerHTML)) {
|
||
return
|
||
}
|
||
// 添加@
|
||
this.addMention({
|
||
denotationChar: "@",
|
||
id: userData.userid,
|
||
value: userData.nickname,
|
||
})
|
||
},
|
||
|
||
onSpaceInputFocus() {
|
||
if (this.selectRange) {
|
||
// 修复Android光标会超出的问题
|
||
this.quill?.setSelection(this.selectRange.index, this.selectRange.length)
|
||
}
|
||
},
|
||
|
||
openMenu(char) {
|
||
if (!this.quill) {
|
||
return;
|
||
}
|
||
if (this.value.length === 0 || this.value.endsWith("<p><br></p>")) {
|
||
this.quill.getModule("mention").openMenu(char);
|
||
} else {
|
||
let str = this.value.replace(/<[^>]+>/g,"");
|
||
if (str.length === 0 || str.endsWith(" ")) {
|
||
this.quill.getModule("mention").openMenu(char);
|
||
} else {
|
||
this.quill.getModule("mention").openMenu(` ${char}`);
|
||
}
|
||
}
|
||
},
|
||
|
||
addMention(data) {
|
||
if (!this.quill) {
|
||
return;
|
||
}
|
||
if (!this.inputActivated()) {
|
||
return;
|
||
}
|
||
const {index} = this.quill.getSelection(true);
|
||
this.quill.insertEmbed(index, "mention", data, Quill.sources.USER);
|
||
this.quill.insertText(index + 1, " ", Quill.sources.USER);
|
||
this.quill.setSelection(index + 2, Quill.sources.USER);
|
||
},
|
||
|
||
getProjectId() {
|
||
let object = null;
|
||
if (this.dialogId > 0) {
|
||
object = this.cacheProjects.find(({dialog_id}) => dialog_id == this.dialogId);
|
||
if (object) {
|
||
return object.id;
|
||
}
|
||
object = this.cacheTasks.find(({dialog_id}) => dialog_id == this.dialogId);
|
||
if (object) {
|
||
return object.project_id;
|
||
}
|
||
} else if (this.taskId > 0) {
|
||
object = this.cacheTasks.find(({id}) => id == this.taskId);
|
||
if (object) {
|
||
return object.project_id;
|
||
}
|
||
}
|
||
return 0;
|
||
},
|
||
|
||
getMentionSource(mentionChar, searchTerm, resultCallback) {
|
||
switch (mentionChar) {
|
||
case "@": // @成员
|
||
this.mentionMode = "user-mention";
|
||
const atCallback = (list) => {
|
||
this.getMoreUser(searchTerm, list.map(item => item.id)).then(moreUser => {
|
||
// 群外成员 排序 -> 前5名为最近联系的人
|
||
let cacheDialogs = this.cacheDialogs.filter((h, index) => h.type == "user" && h.bot == 0 && h.last_at)
|
||
cacheDialogs.sort((a, b) => a.last_at > b.last_at ? -1 : (a.last_at < b.last_at ? 1 : 0));
|
||
cacheDialogs = cacheDialogs.filter((h, index) => index < 5)
|
||
moreUser.forEach(user => {
|
||
user.last_at = "1990-01-01 00:00:00";
|
||
cacheDialogs.forEach(dialog => {
|
||
if (dialog.dialog_user?.userid == user.id) {
|
||
user.last_at = dialog.last_at;
|
||
}
|
||
})
|
||
})
|
||
moreUser.sort((a, b) => a.last_at > b.last_at ? -1 : (a.last_at < b.last_at ? 1 : 0));
|
||
//
|
||
this.userList = list
|
||
this.userCache = [];
|
||
if (moreUser.length > 0) {
|
||
if (list.length > 2) {
|
||
this.userCache.push({
|
||
label: null,
|
||
list: [{id: 0, value: this.$L('所有人.All'), tip: ''}]
|
||
})
|
||
}
|
||
this.userCache.push(...[{
|
||
label: [{id: 0, value: this.$L('群内成员'), className: "sticky-top", disabled: true}],
|
||
list,
|
||
}, {
|
||
label: [{id: 0, value: this.$L('群外成员'), className: "sticky-top", disabled: true}],
|
||
list: moreUser,
|
||
}])
|
||
} else {
|
||
if (list.length > 2) {
|
||
this.userCache.push(...[{
|
||
label: null,
|
||
list: [{id: 0, value: this.$L('所有人.All'), tip: ''}]
|
||
}, {
|
||
label: [{id: 0, value: this.$L('群成员'), className: "sticky-top", disabled: true}],
|
||
list,
|
||
}])
|
||
} else {
|
||
this.userCache.push({
|
||
label: null,
|
||
list
|
||
})
|
||
}
|
||
}
|
||
resultCallback(this.userCache)
|
||
})
|
||
}
|
||
//
|
||
if (this.dialogData.people && $A.arrayLength(this.userList) !== this.dialogData.people) {
|
||
this.userList = null;
|
||
this.userCache = null;
|
||
}
|
||
if (this.userCache !== null) {
|
||
resultCallback(this.userCache)
|
||
}
|
||
if (this.userList !== null) {
|
||
atCallback(this.userList)
|
||
return;
|
||
}
|
||
//
|
||
const array = [];
|
||
if (this.dialogId > 0) {
|
||
// 根据会话ID获取成员
|
||
this.$store.dispatch("call", {
|
||
url: 'dialog/user',
|
||
data: {
|
||
dialog_id: this.dialogId,
|
||
getuser: 1
|
||
}
|
||
}).then(({data}) => {
|
||
if (this.cacheDialogs.find(({id}) => id == this.dialogId)) {
|
||
this.$store.dispatch("saveDialog", {
|
||
id: this.dialogId,
|
||
people: data.length,
|
||
people_user: data.filter(item => !item.bot).length,
|
||
people_bot: data.filter(item => item.bot).length,
|
||
})
|
||
}
|
||
if (data.length > 0) {
|
||
array.push(...data.map(item => {
|
||
return {
|
||
id: item.userid,
|
||
value: item.nickname,
|
||
avatar: item.userimg,
|
||
online: item.online,
|
||
bot: item.bot,
|
||
key: `${item.nickname} ${item.email} ${item.pinyin}`
|
||
}
|
||
}))
|
||
}
|
||
atCallback(array)
|
||
}).catch(_ => {
|
||
atCallback(array)
|
||
});
|
||
} else if (this.taskId > 0) {
|
||
// 根据任务ID获取成员
|
||
const task = this.cacheTasks.find(({id}) => id == this.taskId)
|
||
if (task && $A.isArray(task.task_user)) {
|
||
task.task_user.some(tmp => {
|
||
const item = this.cacheUserBasic.find(({userid}) => userid == tmp.userid);
|
||
if (item) {
|
||
array.push({
|
||
id: item.userid,
|
||
value: item.nickname,
|
||
avatar: item.userimg,
|
||
online: item.online,
|
||
bot: item.bot,
|
||
key: `${item.nickname} ${item.email} ${item.pinyin}`
|
||
})
|
||
}
|
||
})
|
||
}
|
||
atCallback(array)
|
||
}
|
||
break;
|
||
|
||
case "#": // #任务
|
||
this.mentionMode = "task-mention";
|
||
if (this.taskList !== null) {
|
||
resultCallback(this.taskList)
|
||
return;
|
||
}
|
||
const taskCallback = (list) => {
|
||
this.taskList = [];
|
||
// 项目任务
|
||
if (list.length > 0) {
|
||
list = list.map(item => {
|
||
return {
|
||
id: item.id,
|
||
value: item.name,
|
||
tip: item.complete_at ? this.$L('已完成') : null,
|
||
}
|
||
}).splice(0, 100)
|
||
this.taskList.push({
|
||
label: [{id: 0, value: this.$L('项目任务'), disabled: true}],
|
||
list,
|
||
})
|
||
}
|
||
// 待完成任务
|
||
const { overdue, today, todo } = this.$store.getters.dashboardTask;
|
||
const combinedTasks = [...overdue, ...today, ...todo];
|
||
let allTask = this.$store.getters.transforTasks(combinedTasks);
|
||
if (allTask.length > 0) {
|
||
allTask = allTask.sort((a, b) => {
|
||
return $A.sortDay(a.end_at || "2099-12-31 23:59:59", b.end_at || "2099-12-31 23:59:59");
|
||
}).splice(0, 100)
|
||
this.taskList.push({
|
||
label: [{id: 0, value: this.$L('我的待完成任务'), disabled: true}],
|
||
list: allTask.map(item => {
|
||
return {
|
||
id: item.id,
|
||
value: item.name
|
||
}
|
||
}),
|
||
})
|
||
}
|
||
// 我协助的任务
|
||
let assistTask = this.$store.getters.assistTask;
|
||
if (assistTask.length > 0) {
|
||
assistTask = assistTask.sort((a, b) => {
|
||
return $A.sortDay(a.end_at || "2099-12-31 23:59:59", b.end_at || "2099-12-31 23:59:59");
|
||
}).splice(0, 100)
|
||
this.taskList.push({
|
||
label: [{id: 0, value: this.$L('我协助的任务'), disabled: true}],
|
||
list: assistTask.map(item => {
|
||
return {
|
||
id: item.id,
|
||
value: item.name
|
||
}
|
||
}),
|
||
})
|
||
}
|
||
resultCallback(this.taskList)
|
||
}
|
||
//
|
||
const projectId = this.getProjectId();
|
||
if (projectId > 0) {
|
||
this.$store.dispatch("getTaskForProject", projectId).then(_ => {
|
||
const tasks = this.cacheTasks.filter(task => {
|
||
if (task.archived_at) {
|
||
return false;
|
||
}
|
||
return task.project_id == projectId
|
||
&& task.parent_id === 0
|
||
&& !task.archived_at
|
||
}).sort((a, b) => {
|
||
return $A.sortDay(b.complete_at || "2099-12-31 23:59:59", a.complete_at || "2099-12-31 23:59:59")
|
||
})
|
||
if (tasks.length > 0) {
|
||
taskCallback(tasks)
|
||
} else {
|
||
taskCallback([])
|
||
}
|
||
}).catch(_ => {
|
||
taskCallback([])
|
||
})
|
||
return;
|
||
}
|
||
taskCallback([])
|
||
break;
|
||
|
||
case "~": // ~文件
|
||
this.mentionMode = "file-mention";
|
||
if ($A.isArray(this.fileList[searchTerm])) {
|
||
resultCallback(this.fileList[searchTerm])
|
||
return;
|
||
}
|
||
this.fileTimer && clearTimeout(this.fileTimer)
|
||
this.fileTimer = setTimeout(async _ => {
|
||
const lists = [];
|
||
const data = (await this.$store.dispatch("searchFiles", searchTerm).catch(_ => {}))?.data;
|
||
if (data) {
|
||
lists.push({
|
||
label: [{id: 0, value: this.$L('文件分享查看'), disabled: true}],
|
||
list: data.filter(item => item.type !== "folder").map(item => {
|
||
return {
|
||
id: item.id,
|
||
value: item.ext ? `${item.name}.${item.ext}` : item.name
|
||
}
|
||
})
|
||
})
|
||
this.fileList[searchTerm] = lists
|
||
}
|
||
resultCallback(lists)
|
||
}, 300)
|
||
break;
|
||
|
||
case "%": // %报告
|
||
this.mentionMode = "report-mention";
|
||
if ($A.isArray(this.reportList[searchTerm])) {
|
||
resultCallback(this.reportList[searchTerm])
|
||
return;
|
||
}
|
||
this.reportTimer && clearTimeout(this.reportTimer)
|
||
this.reportTimer = setTimeout(async _ => {
|
||
const lists = [];
|
||
let wait = 2;
|
||
const myData = (await this.$store.dispatch("call", {
|
||
url: 'report/my',
|
||
data: {
|
||
keys: {
|
||
key: searchTerm,
|
||
},
|
||
},
|
||
}).catch(_ => {}))?.data;
|
||
if (myData) {
|
||
lists.push({
|
||
label: [{id: 0, value: this.$L('我的报告'), disabled: true}],
|
||
list: myData.data.map(item => {
|
||
return {
|
||
id: item.id,
|
||
value: item.title
|
||
}
|
||
})
|
||
})
|
||
wait--
|
||
}
|
||
const receiveData = (await this.$store.dispatch("call", {
|
||
url: 'report/receive',
|
||
data: {
|
||
keys: {
|
||
key: searchTerm,
|
||
},
|
||
},
|
||
}).catch(_ => {}))?.data;
|
||
if (receiveData) {
|
||
lists.push({
|
||
label: [{id: 0, value: this.$L('收到的报告'), disabled: true}],
|
||
list: receiveData.data.map(item => {
|
||
return {
|
||
id: item.id,
|
||
value: item.title
|
||
}
|
||
})
|
||
})
|
||
wait--
|
||
}
|
||
if (wait === 0) {
|
||
this.reportList[searchTerm] = lists
|
||
}
|
||
resultCallback(lists)
|
||
}, 300)
|
||
break;
|
||
|
||
default:
|
||
resultCallback([])
|
||
break;
|
||
}
|
||
},
|
||
|
||
getMoreUser(key, existIds) {
|
||
return new Promise(resolve => {
|
||
const {owner_id, type} = this.dialogData
|
||
const permission = type === 'group' && [0, this.userId].includes(owner_id)
|
||
if (this.taskId > 0 || permission) {
|
||
this.moreTimer && clearTimeout(this.moreTimer)
|
||
this.moreTimer = setTimeout(_ => {
|
||
this.$store.dispatch("call", {
|
||
url: 'users/search',
|
||
data: {
|
||
keys: {
|
||
key,
|
||
bot: 2,
|
||
},
|
||
state: 1,
|
||
take: 30
|
||
},
|
||
}).then(({data}) => {
|
||
const moreUser = data.filter(item => !existIds.includes(item.userid))
|
||
resolve(moreUser.map(item => {
|
||
return {
|
||
id: item.userid,
|
||
value: item.nickname,
|
||
avatar: item.userimg,
|
||
online: !!item.online,
|
||
bot: !!item.bot,
|
||
key: `${item.nickname} ${item.email} ${item.pinyin}`
|
||
}
|
||
}))
|
||
}).catch(_ => {
|
||
resolve([])
|
||
});
|
||
}, this.userCache === null ? 0 : 600)
|
||
} else {
|
||
resolve([])
|
||
}
|
||
})
|
||
},
|
||
|
||
checkIOSVersion() {
|
||
let ua = window && window.navigator && window.navigator.userAgent;
|
||
let match = ua.match(/OS ((\d+_?){2,3})\s/i);
|
||
let IOSVersion = match ? match[1].replace(/_/g, ".") : "unknown";
|
||
const iosVsn = IOSVersion.split(".");
|
||
return +iosVsn[0] == 11 && +iosVsn[1] >= 0 && +iosVsn[1] < 3;
|
||
},
|
||
|
||
handlePaste(e) {
|
||
const files = Array.prototype.slice.call(e.clipboardData.files)
|
||
const postFiles = files.filter(file => !$A.leftExists(file.type, 'image/'));
|
||
if (postFiles.length > 0) {
|
||
e.preventDefault()
|
||
this.$emit('on-file', files)
|
||
}
|
||
},
|
||
|
||
updateTools() {
|
||
if (this.showEmoji) {
|
||
this.$refs.emoji?.updatePopper()
|
||
}
|
||
if (this.showMore) {
|
||
this.$refs.more?.updatePopper()
|
||
}
|
||
if (this.showMenu) {
|
||
this.$refs.menu?.updatePopper()
|
||
}
|
||
const mention = this.quill?.getModule("mention")
|
||
if (mention.isOpen) {
|
||
mention.setMentionContainerPosition()
|
||
}
|
||
},
|
||
|
||
tempHiddenSendTip() {
|
||
const {sendTip} = this.$refs
|
||
if (sendTip && sendTip.$refs.popper) {
|
||
sendTip.$refs.popper.style.visibility = 'hidden'
|
||
sendTip.showPopper = false
|
||
setTimeout(_ => {
|
||
if (sendTip.$refs.popper) {
|
||
sendTip.$refs.popper.style.visibility = 'visible'
|
||
}
|
||
}, 300)
|
||
}
|
||
},
|
||
}
|
||
}
|
||
</script>
|