mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 18:42:54 +00:00
2435 lines
94 KiB
Vue
Executable File
2435 lines
94 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 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" :searchKey="emojiQuickKey"/>
|
|
|
|
<!-- 录音浮窗 -->
|
|
<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} from "../../../../utils/markdown";
|
|
import emitter from "../../../../store/events";
|
|
|
|
const globalRangeIndexs = {};
|
|
|
|
export default {
|
|
name: 'ChatInput',
|
|
components: {ChatEmoji},
|
|
directives: {touchmouse, touchclick, TransferDom, clickoutside, longpress},
|
|
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,
|
|
_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.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()
|
|
},
|
|
taskId() {
|
|
this.selectRange = null;
|
|
this.userList = null;
|
|
this.userCache = null;
|
|
this.taskList = null;
|
|
this.fileList = {};
|
|
this.reportList = {};
|
|
this.loadInputDraft()
|
|
},
|
|
|
|
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
|
|
}
|
|
} else 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: {
|
|
'short enter': {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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 = languageName === "zh" || languageName === "zh-CHT" ? "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 = ''
|
|
}
|
|
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
|
|
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;
|
|
}
|
|
}
|
|
},
|
|
|
|
onToolbar(action) {
|
|
this.hidePopover();
|
|
switch (action) {
|
|
case 'user':
|
|
this.openMenu("@");
|
|
break;
|
|
|
|
case 'task':
|
|
this.openMenu("#");
|
|
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;
|
|
|
|
}
|
|
},
|
|
|
|
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>
|