mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-06 23:50:41 +00:00
feat(task): 实现消息合并转发功能,支持批量选择和转发消息
This commit is contained in:
parent
4b32472d64
commit
984b98e4fc
@ -2311,6 +2311,7 @@ class DialogController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$msg_ids = Request::input('msg_ids');
|
||||
$msg_id = intval(Request::input("msg_id"));
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
@ -2321,6 +2322,30 @@ class DialogController extends AbstractController
|
||||
return Base::retError("请选择对话或成员");
|
||||
}
|
||||
//
|
||||
// 支持批量逐条转发
|
||||
if (!empty($msg_ids) && is_array($msg_ids)) {
|
||||
if (count($msg_ids) > 100) {
|
||||
return Base::retError("最多转发100条消息");
|
||||
}
|
||||
$allMsgs = [];
|
||||
$msgs = WebSocketDialogMsg::whereIn('id', $msg_ids)->orderBy('created_at')->get();
|
||||
if ($msgs->isEmpty()) {
|
||||
return Base::retError("消息不存在或已被删除");
|
||||
}
|
||||
WebSocketDialog::checkDialog($msgs->first()->dialog_id);
|
||||
foreach ($msgs as $msg) {
|
||||
$res = $msg->forwardMsg($dialogids, $userids, $user, $show_source, $leave_message);
|
||||
if (Base::isSuccess($res)) {
|
||||
$allMsgs = array_merge($allMsgs, $res['data']['msgs']);
|
||||
}
|
||||
// 留言只在第一条时发送,后续不再重复
|
||||
$leave_message = '';
|
||||
}
|
||||
return Base::retSuccess('转发成功', [
|
||||
'msgs' => $allMsgs
|
||||
]);
|
||||
}
|
||||
//
|
||||
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
|
||||
if (empty($msg)) {
|
||||
return Base::retError("消息不存在或已被删除");
|
||||
@ -2330,6 +2355,47 @@ class DialogController extends AbstractController
|
||||
return $msg->forwardMsg($dialogids, $userids, $user, $show_source, $leave_message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/merge-forward 合并转发消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName msg__merge_forward
|
||||
*
|
||||
* @apiParam {Array} msg_ids 消息ID数组(最多100条)
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {Number} show_source 是否显示原发送者信息
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function msg__merge_forward()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$msg_ids = Request::input('msg_ids');
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
$show_source = intval(Request::input("show_source"));
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (empty($dialogids) && empty($userids)) {
|
||||
return Base::retError("请选择对话或成员");
|
||||
}
|
||||
if (empty($msg_ids) || !is_array($msg_ids)) {
|
||||
return Base::retError("请选择要转发的消息");
|
||||
}
|
||||
if (count($msg_ids) > 100) {
|
||||
return Base::retError("最多转发100条消息");
|
||||
}
|
||||
//
|
||||
return WebSocketDialogMsg::mergeForwardMsg($msg_ids, $dialogids, $userids, $user, $show_source, $leave_message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/emoji emoji回复
|
||||
*
|
||||
|
||||
@ -492,6 +492,47 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
* @param string $leaveMessage 转发留言
|
||||
* @return mixed
|
||||
*/
|
||||
/**
|
||||
* 收集目标对话
|
||||
* @param array|int $userids 转发给的成员ID
|
||||
* @param array|int $dialogids 转发给的对话ID
|
||||
* @param User $user 当前用户
|
||||
* @return array
|
||||
*/
|
||||
private static function collectTargetDialogs($userids, $dialogids, $user)
|
||||
{
|
||||
$dialogs = [];
|
||||
if ($userids) {
|
||||
if (!is_array($userids)) {
|
||||
$userids = [$userids];
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!User::whereUserid($userid)->exists()) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog) {
|
||||
$dialogs[$dialog->id] = $dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
if (isset($dialogs[$dialogid])) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::find($dialogid);
|
||||
if ($dialog) {
|
||||
$dialogs[$dialog->id] = $dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $dialogs;
|
||||
}
|
||||
|
||||
public function forwardMsg($dialogids, $userids, $user, $showSource = 1, $leaveMessage = '')
|
||||
{
|
||||
return AbstractModel::transaction(function () use ($dialogids, $user, $userids, $showSource, $leaveMessage) {
|
||||
@ -513,35 +554,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
'leave' => $leaveMessage ? 1 : 0, // 是否留言(用于判断是否发给AI)
|
||||
];
|
||||
$msgs = [];
|
||||
$dialogs = [];
|
||||
if ($userids) {
|
||||
if (!is_array($userids)) {
|
||||
$userids = [$userids];
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!User::whereUserid($userid)->exists()) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog) {
|
||||
$dialogs[$dialog->id] = $dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
if (isset($dialogs[$dialogid])) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::find($dialogid);
|
||||
if ($dialog) {
|
||||
$dialogs[$dialog->id] = $dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
$dialogs = self::collectTargetDialogs($userids, $dialogids, $user);
|
||||
foreach ($dialogs as $dialog) {
|
||||
$res = self::sendMsg('forward-' . $forwardId, $dialog->id, $this->type, $msgData, $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
@ -564,6 +577,81 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并转发消息
|
||||
* @param array $msgIds 消息ID数组
|
||||
* @param array|int $dialogids 转发给的对话ID
|
||||
* @param array|int $userids 转发给的成员ID
|
||||
* @param User $user 当前用户
|
||||
* @param int $showSource 是否显示原发送者信息
|
||||
* @param string $leaveMessage 转发留言
|
||||
* @return array
|
||||
*/
|
||||
public static function mergeForwardMsg($msgIds, $dialogids, $userids, $user, $showSource = 1, $leaveMessage = '')
|
||||
{
|
||||
return AbstractModel::transaction(function () use ($msgIds, $dialogids, $userids, $user, $showSource, $leaveMessage) {
|
||||
// 查询并验证所有消息
|
||||
$msgs = self::whereIn('id', $msgIds)->orderBy('created_at')->get();
|
||||
if ($msgs->isEmpty()) {
|
||||
throw new ApiException('消息不存在或已被删除');
|
||||
}
|
||||
// 验证所有消息属于同一对话
|
||||
$dialogId = $msgs->first()->dialog_id;
|
||||
if ($msgs->pluck('dialog_id')->unique()->count() > 1) {
|
||||
throw new ApiException('只能合并转发同一对话的消息');
|
||||
}
|
||||
WebSocketDialog::checkDialog($dialogId);
|
||||
// 收集发送者生成标题
|
||||
$senderIds = $msgs->pluck('userid')->unique()->values()->toArray();
|
||||
$senderNames = User::whereIn('userid', array_slice($senderIds, 0, 2))
|
||||
->pluck('nickname')
|
||||
->toArray();
|
||||
$title = implode(Doo::translate('和'), $senderNames);
|
||||
if (count($senderIds) > 2) {
|
||||
$title .= Doo::translate('等人');
|
||||
}
|
||||
$title .= Doo::translate('的聊天记录');
|
||||
// 组装消息列表
|
||||
$list = [];
|
||||
foreach ($msgs as $msg) {
|
||||
$list[] = [
|
||||
'userid' => $msg->userid,
|
||||
'type' => $msg->type,
|
||||
'msg' => Base::json2array($msg->getRawOriginal('msg')),
|
||||
'created_at' => $msg->created_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
// 构建合并转发消息体
|
||||
$msgData = [
|
||||
'title' => $title,
|
||||
'list' => $list,
|
||||
'count' => count($list),
|
||||
'forward_data' => [
|
||||
'show' => $showSource,
|
||||
'leave' => $leaveMessage ? 1 : 0,
|
||||
],
|
||||
];
|
||||
$dialogs = self::collectTargetDialogs($userids, $dialogids, $user);
|
||||
// 发送到每个目标对话
|
||||
$result = [];
|
||||
foreach ($dialogs as $dialog) {
|
||||
$res = self::sendMsg(null, $dialog->id, 'merge-forward', $msgData, $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$result[] = $res['data'];
|
||||
}
|
||||
if ($leaveMessage) {
|
||||
$res = self::sendMsg(null, $dialog->id, 'text', ['text' => $leaveMessage], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$result[] = $res['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('转发成功', [
|
||||
'msgs' => $result
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
* @param array|int $ids
|
||||
@ -695,6 +783,10 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
case 'template':
|
||||
return self::previewTemplateMsg($data['msg']);
|
||||
|
||||
case 'merge-forward':
|
||||
$action = Doo::translate("聊天记录");
|
||||
return "[{$action}] " . Base::cutStr($data['msg']['title'] ?? '', 50);
|
||||
|
||||
case 'preview':
|
||||
return $data['msg']['preview'];
|
||||
|
||||
@ -1262,6 +1354,9 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$msg['height'] = $imageSize[1];
|
||||
}
|
||||
}
|
||||
if ($type === 'merge-forward') {
|
||||
$mtype = 'merge-forward';
|
||||
}
|
||||
if ($push_silence === null) {
|
||||
$push_silence = !in_array($type, ["text", "file", "record", "meeting"]);
|
||||
}
|
||||
|
||||
2
resources/assets/js/functions/web.js
vendored
2
resources/assets/js/functions/web.js
vendored
@ -461,6 +461,8 @@ import {convertLocalResourcePath} from "../components/Replace/utils";
|
||||
case 'notice':
|
||||
const notice = data.msg.source === 'api' ? data.msg.notice : $A.L(data.msg.notice);
|
||||
return $A.cutString(notice, 50)
|
||||
case 'merge-forward':
|
||||
return `[${$A.L('聊天记录')}] ${$A.cutString(data.msg.title || '', 50)}`
|
||||
case 'template':
|
||||
return $A.templateMsgSimpleDesc(data.msg)
|
||||
case 'preview':
|
||||
|
||||
@ -40,6 +40,9 @@
|
||||
{{source.msg.source === 'api' ? source.msg.notice : $L(source.msg.notice)}}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="multiSelectMode && isSelectableMsg" class="dialog-multi-check" @click.stop="onMultiSelectToggle">
|
||||
<Icon :type="isSelected ? 'ios-checkmark-circle' : 'ios-radio-button-off'" :class="{checked: isSelected}"/>
|
||||
</div>
|
||||
<div
|
||||
class="dialog-avatar"
|
||||
@pointerdown="handleOperation">
|
||||
@ -132,6 +135,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
multiSelectMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedMsgIdsSet: {
|
||||
type: Set,
|
||||
default: () => new Set()
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
@ -165,12 +176,22 @@ export default {
|
||||
return this.simpleView || this.msgId > 0
|
||||
},
|
||||
|
||||
isSelected() {
|
||||
return this.multiSelectMode && this.selectedMsgIdsSet.has(this.source.id);
|
||||
},
|
||||
|
||||
isSelectableMsg() {
|
||||
return !['tag', 'top', 'todo', 'notice'].includes(this.source.type);
|
||||
},
|
||||
|
||||
classArray() {
|
||||
return {
|
||||
'dialog-item': true,
|
||||
'reply-item': this.isReply,
|
||||
'unread-start': this.isUnreadStart,
|
||||
'self': this.isRightMsg,
|
||||
'multi-select-mode': this.multiSelectMode,
|
||||
'multi-selected': this.isSelected,
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -262,6 +283,10 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
onMultiSelectToggle() {
|
||||
this.dispatch("on-multi-select-toggle", this.source.id)
|
||||
},
|
||||
|
||||
onViewReply(data) {
|
||||
this.dispatch("on-view-reply", data)
|
||||
},
|
||||
|
||||
@ -43,6 +43,8 @@
|
||||
<WordChainMsg v-else-if="msgData.type === 'word-chain'" :msg="msgData.msg" :msgId="msgData.id" :unfoldWordChainData="unfoldWordChainData" @unfoldWordChain="unfoldWordChain(msgData)" @onWordChain="onWordChain"/>
|
||||
<!--投票-->
|
||||
<VoteMsg v-else-if="msgData.type === 'vote'" :msg="msgData.msg" :voteData="voteData" @onVote="onVote($event, msgData)"/>
|
||||
<!--合并转发-->
|
||||
<MergeForwardMsg v-else-if="msgData.type === 'merge-forward'" :msg="msgData.msg"/>
|
||||
<!--模板-->
|
||||
<TemplateMsg v-else-if="msgData.type === 'template'" :msg="msgData.msg" @viewText="viewText"/>
|
||||
<!--等待-->
|
||||
@ -190,6 +192,7 @@ import MeetingMsg from "./meet.vue";
|
||||
import WordChainMsg from "./word-chain.vue";
|
||||
import VoteMsg from "./vote.vue";
|
||||
import TemplateMsg from "./template";
|
||||
import MergeForwardMsg from "./merge-forward.vue";
|
||||
import LoadMsg from "./load.vue";
|
||||
import UnknownMsg from "./unknown.vue";
|
||||
import emitter from "../../../../store/events";
|
||||
@ -208,6 +211,7 @@ export default {
|
||||
components: {
|
||||
UnknownMsg,
|
||||
LoadMsg,
|
||||
MergeForwardMsg,
|
||||
TemplateMsg,
|
||||
VoteMsg,
|
||||
WordChainMsg,
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="content-merge-forward" @click="openDetail">
|
||||
<div class="merge-title">{{ msg.title }}</div>
|
||||
<div class="merge-list">
|
||||
<div v-for="(item, index) in displayList" :key="index" class="merge-item">
|
||||
<UserAvatar :userid="item.userid" :show-icon="false" :show-name="true" :size="14"/>
|
||||
<span class="item-colon">:</span>
|
||||
<span class="item-desc" v-html="$A.getMsgSimpleDesc(item)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="merge-footer">{{ $L('共') }} {{ msg.count || msg.list.length }} {{ $L('条消息') }}</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<Modal
|
||||
v-model="detailShow"
|
||||
:title="msg.title"
|
||||
class-name="merge-forward-detail-modal"
|
||||
:mask-closable="true"
|
||||
width="500"
|
||||
@click.native.stop>
|
||||
<Scrollbar class-name="merge-detail-scroller" :style="{maxHeight: '60vh'}">
|
||||
<div class="merge-detail-list">
|
||||
<div v-for="(item, index) in msg.list" :key="index" class="merge-detail-item">
|
||||
<UserAvatar :userid="item.userid" :size="28" :show-name="false"/>
|
||||
<div class="detail-content">
|
||||
<div class="detail-header">
|
||||
<UserAvatar :userid="item.userid" :show-icon="false" :show-name="true" :size="0"/>
|
||||
<span class="detail-time">{{ item.created_at }}</span>
|
||||
</div>
|
||||
<div class="detail-body">
|
||||
<template v-if="item.type === 'text'">
|
||||
<pre v-html="$A.formatTextMsg(item.msg.text)"></pre>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'file'">
|
||||
<span>[{{ $L('文件') }}] {{ item.msg.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'merge-forward'">
|
||||
<span>[{{ $L('聊天记录') }}] {{ item.msg.title }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-html="$A.getMsgSimpleDesc(item)"></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scrollbar>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MergeForwardMsg",
|
||||
props: {
|
||||
msg: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
detailShow: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayList() {
|
||||
if (!this.msg || !this.msg.list) return [];
|
||||
return this.msg.list.slice(0, 4);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openDetail() {
|
||||
this.detailShow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -201,7 +201,7 @@
|
||||
:data-sources="allMsgs"
|
||||
:data-component="msgItem"
|
||||
|
||||
:extra-props="{dialogData, operateVisible, operateItem, pointerMouse, isMyDialog, msgId, unreadOne, scrollIng, readEnabled}"
|
||||
:extra-props="{dialogData, operateVisible, operateItem, pointerMouse, isMyDialog, msgId, unreadOne, scrollIng, readEnabled, multiSelectMode, selectedMsgIdsSet}"
|
||||
:estimate-size="dialogData.type=='group' ? 105 : 77"
|
||||
:keeps="dialogMsgKeep"
|
||||
:disabled="scrollDisabled"
|
||||
@ -219,7 +219,8 @@
|
||||
@on-error="onError"
|
||||
@on-emoji="onEmoji"
|
||||
@on-other="onOther"
|
||||
@on-show-emoji-user="onShowEmojiUser">
|
||||
@on-show-emoji-user="onShowEmojiUser"
|
||||
@on-multi-select-toggle="onMultiSelectToggle">
|
||||
<template #header v-if="!isChildComponent">
|
||||
<div class="dialog-item head-box">
|
||||
<div v-if="loadIng > 0 || prevId > 0" class="loading" :class="{filled: allMsgs.length === 0}">
|
||||
@ -231,8 +232,20 @@
|
||||
</VirtualList>
|
||||
</div>
|
||||
|
||||
<!--多选操作栏-->
|
||||
<div v-if="multiSelectMode" class="dialog-multi-select-bar">
|
||||
<div class="multi-select-info">
|
||||
<span>{{ $L('已选') }} {{ selectedMsgIds.length }} {{ $L('条') }}</span>
|
||||
<span v-if="selectedMsgIds.length >= 100" class="multi-select-max">{{ $L('(最多100条)') }}</span>
|
||||
</div>
|
||||
<div class="multi-select-actions">
|
||||
<Button type="primary" size="small" :disabled="selectedMsgIds.length === 0" @click="onMultiForward">{{ $L('转发') }}</Button>
|
||||
<Button size="small" @click="onMultiSelectCancel">{{ $L('取消') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--底部输入-->
|
||||
<div ref="footer" class="dialog-footer" @click="onClickFooter">
|
||||
<div v-show="!multiSelectMode" ref="footer" class="dialog-footer" @click="onClickFooter">
|
||||
<!--滚动到底部-->
|
||||
<div
|
||||
v-if="scrollTail > 500 || (msgNew > 0 && allMsgs.length > 0)"
|
||||
@ -355,6 +368,10 @@
|
||||
<i class="taskfont"></i>
|
||||
<span>{{ $L('转发') }}</span>
|
||||
</li>
|
||||
<li v-if="actionPermission(operateItem, 'forward')" @click="onOperate('multiSelect')">
|
||||
<i class="taskfont"></i>
|
||||
<span>{{ $L('多选') }}</span>
|
||||
</li>
|
||||
<li v-if="operateItem.userid == userId" @click="onOperate('withdraw')">
|
||||
<i class="taskfont"></i>
|
||||
<span>{{ $L('撤回') }}</span>
|
||||
@ -503,7 +520,9 @@
|
||||
:title="$L('转发')"
|
||||
:confirm-title="$L('确认转发')"
|
||||
:multiple-max="50"
|
||||
:msg-detail="operateItem"
|
||||
:msg-detail="multiSelectMode ? null : operateItem"
|
||||
:msg-ids="multiSelectMode ? selectedMsgIds : []"
|
||||
:msg-list="multiSelectMsgList"
|
||||
:before-submit="onForward"/>
|
||||
|
||||
<!-- 设置待办 -->
|
||||
@ -801,6 +820,9 @@ export default {
|
||||
operateStyles: {},
|
||||
operateItem: {},
|
||||
|
||||
multiSelectMode: false,
|
||||
selectedMsgIds: [],
|
||||
|
||||
recordState: '',
|
||||
pointerMouse: false,
|
||||
|
||||
@ -946,6 +968,15 @@ export default {
|
||||
return this.dialogData.group_type === 'user'
|
||||
},
|
||||
|
||||
selectedMsgIdsSet() {
|
||||
return new Set(this.selectedMsgIds);
|
||||
},
|
||||
|
||||
multiSelectMsgList() {
|
||||
if (!this.multiSelectMode || this.selectedMsgIds.length === 0) return [];
|
||||
return this.allMsgs.filter(m => this.selectedMsgIdsSet.has(m.id));
|
||||
},
|
||||
|
||||
dialogList() {
|
||||
return this.cacheDialogs.filter(dialog => {
|
||||
return !(dialog.name === undefined || dialog.dialog_delete === 1);
|
||||
@ -1254,6 +1285,7 @@ export default {
|
||||
window.localStorage.removeItem('__cache:vote__')
|
||||
window.localStorage.removeItem('__cache:unfoldWordChain__')
|
||||
//
|
||||
this.onMultiSelectCancel()
|
||||
this.handlerMsgTransfer()
|
||||
},
|
||||
immediate: true
|
||||
@ -2993,20 +3025,27 @@ export default {
|
||||
},
|
||||
|
||||
onForward(forwardData) {
|
||||
const isMulti = forwardData.msg_ids && forwardData.msg_ids.length > 0;
|
||||
const url = isMulti
|
||||
? (forwardData.forward_mode === 'merge' ? 'dialog/msg/merge-forward' : 'dialog/msg/forward')
|
||||
: 'dialog/msg/forward';
|
||||
const data = {
|
||||
dialogids: forwardData.dialogids,
|
||||
userids: forwardData.userids,
|
||||
show_source: forwardData.sender ? 1 : 0,
|
||||
leave_message: forwardData.message
|
||||
};
|
||||
if (isMulti) {
|
||||
data.msg_ids = forwardData.msg_ids;
|
||||
} else {
|
||||
data.msg_id = forwardData.msg_id;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.$store.dispatch("call", {
|
||||
url: 'dialog/msg/forward',
|
||||
data: {
|
||||
dialogids: forwardData.dialogids,
|
||||
userids: forwardData.userids,
|
||||
msg_id: forwardData.msg_id,
|
||||
show_source: forwardData.sender ? 1 : 0,
|
||||
leave_message: forwardData.message
|
||||
}
|
||||
}).then(({data, msg}) => {
|
||||
this.$store.dispatch("call", {url, data}).then(({data, msg}) => {
|
||||
this.$store.dispatch("saveDialogMsg", data.msgs);
|
||||
this.$store.dispatch("updateDialogLastMsg", data.msgs);
|
||||
$A.messageSuccess(msg);
|
||||
if (isMulti) this.onMultiSelectCancel();
|
||||
resolve()
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg);
|
||||
@ -3015,6 +3054,27 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
onMultiSelectToggle(msgId) {
|
||||
const index = this.selectedMsgIds.indexOf(msgId);
|
||||
if (index > -1) {
|
||||
this.selectedMsgIds.splice(index, 1);
|
||||
} else if (this.selectedMsgIds.length < 100) {
|
||||
this.selectedMsgIds.push(msgId);
|
||||
} else {
|
||||
$A.messageWarning(this.$L('最多选择100条消息'));
|
||||
}
|
||||
},
|
||||
|
||||
onMultiForward() {
|
||||
if (this.selectedMsgIds.length === 0) return;
|
||||
this.$refs.forwarder.onSelection();
|
||||
},
|
||||
|
||||
onMultiSelectCancel() {
|
||||
this.multiSelectMode = false;
|
||||
this.selectedMsgIds = [];
|
||||
},
|
||||
|
||||
onActivity(activity) {
|
||||
if (this.msgActivity === false) {
|
||||
if (activity) {
|
||||
@ -3135,6 +3195,10 @@ export default {
|
||||
|
||||
// 长按触发消息操作
|
||||
case "operateMsg":
|
||||
if (this.multiSelectMode && $A.isJson(data) && data.id) {
|
||||
this.onMultiSelectToggle(data.id);
|
||||
return;
|
||||
}
|
||||
this.operateVisible = $A.isJson(data) && this.operateItem.id === data.id;
|
||||
this.operateItem = $A.isJson(data) ? data : {};
|
||||
this.operateCopys = []
|
||||
@ -3330,6 +3394,11 @@ export default {
|
||||
this.$refs.forwarder.onSelection()
|
||||
break;
|
||||
|
||||
case "multiSelect":
|
||||
this.multiSelectMode = true;
|
||||
this.selectedMsgIds = [this.operateItem.id];
|
||||
break;
|
||||
|
||||
case "withdraw":
|
||||
this.onWithdraw()
|
||||
break;
|
||||
|
||||
@ -28,7 +28,42 @@
|
||||
</div>
|
||||
<div class="twice-affirm-body-extend">
|
||||
<div class="forwarder-wrapper-body">
|
||||
<div v-if="msgDetail" class="dialog-wrapper inde-list">
|
||||
<!--多选转发方式-->
|
||||
<div v-if="isMultiMode" class="forward-mode-select">
|
||||
<RadioGroup v-model="forwardMode" size="small">
|
||||
<Radio label="one-by-one">{{ $L('逐条转发') }}</Radio>
|
||||
<Radio label="merge">{{ $L('合并转发') }}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<!--多选消息预览-->
|
||||
<div v-if="isMultiMode" class="dialog-wrapper inde-list">
|
||||
<Scrollbar class-name="dialog-scroller">
|
||||
<template v-if="forwardMode === 'merge'">
|
||||
<div class="merge-forward-preview">
|
||||
<div class="merge-preview-title">{{ $L('聊天记录') }}</div>
|
||||
<div v-for="(item, index) in previewMsgList" :key="item.id" class="merge-preview-item">
|
||||
<UserAvatar :userid="item.userid" :show-icon="false" :show-name="true" :size="16"/>
|
||||
<span class="preview-desc" v-html="$A.getMsgSimpleDesc(item)"></span>
|
||||
</div>
|
||||
<div class="merge-preview-count">{{ $L('共') }} {{ msgIds.length }} {{ $L('条消息') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DialogItem
|
||||
v-for="item in previewMsgList"
|
||||
:key="item.id"
|
||||
:source="item"
|
||||
@on-view-text="onViewText"
|
||||
@on-view-file="onViewFile"
|
||||
@on-down-file="onDownFile"
|
||||
@on-emoji="onEmoji"
|
||||
@on-other="onOther"
|
||||
simpleView/>
|
||||
</template>
|
||||
</Scrollbar>
|
||||
</div>
|
||||
<!--单条消息预览-->
|
||||
<div v-else-if="msgDetail" class="dialog-wrapper inde-list">
|
||||
<Scrollbar class-name="dialog-scroller">
|
||||
<DialogItem
|
||||
:source="msgDetail"
|
||||
@ -127,6 +162,16 @@ export default {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
// 多选消息ID数组
|
||||
msgIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 多选消息详情列表
|
||||
msgList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -135,6 +180,7 @@ export default {
|
||||
loading: false,
|
||||
|
||||
message: '', // 留言
|
||||
forwardMode: 'one-by-one', // 转发方式: one-by-one | merge
|
||||
|
||||
ainew: $A.getStorageBoolean('forwarder.ainew', true), // 是否AI开启新会话
|
||||
sender: $A.getStorageBoolean('forwarder.sender', true), // 是否隐藏原发送者信息
|
||||
@ -144,6 +190,15 @@ export default {
|
||||
computed: {
|
||||
...mapState(['cacheUserBasic']),
|
||||
|
||||
isMultiMode() {
|
||||
return this.msgIds && this.msgIds.length > 0;
|
||||
},
|
||||
|
||||
previewMsgList() {
|
||||
if (!this.isMultiMode) return [];
|
||||
return this.msgList.slice(0, this.forwardMode === 'merge' ? 4 : 10);
|
||||
},
|
||||
|
||||
aiUser({forwardTo, cacheUserBasic}) {
|
||||
const users = forwardTo.filter(item => item.type !== 'group');
|
||||
return users.filter(user => {
|
||||
@ -214,6 +269,9 @@ export default {
|
||||
const data = {
|
||||
message: this.message,
|
||||
}
|
||||
if (this.isMultiMode) {
|
||||
data.forward_mode = this.forwardMode;
|
||||
}
|
||||
if (!this.senderHidden) {
|
||||
data.sender = this.sender
|
||||
}
|
||||
|
||||
@ -22,7 +22,9 @@
|
||||
|
||||
:dialog-id="forwardDialogId"
|
||||
:forward-to="forwardTo"
|
||||
:msg-detail="msgDetail"/>
|
||||
:msg-detail="msgDetail"
|
||||
:msg-ids="msgIds"
|
||||
:msg-list="msgList"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -72,6 +74,16 @@ export default {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
// 多选消息ID数组
|
||||
msgIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 多选消息详情列表
|
||||
msgList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -117,7 +129,9 @@ export default {
|
||||
//
|
||||
data.dialogids = selects.filter(value => $A.leftExists(value, 'd:')).map(value => value.replace('d:', ''));
|
||||
data.userids = selects.filter(value => !$A.leftExists(value, 'd:'));
|
||||
if (this.msgDetail) {
|
||||
if (this.msgIds && this.msgIds.length > 0) {
|
||||
data.msg_ids = this.msgIds;
|
||||
} else if (this.msgDetail) {
|
||||
data.msg_id = this.msgDetail.id;
|
||||
}
|
||||
//
|
||||
|
||||
@ -651,6 +651,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-multi-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.ivu-icon {
|
||||
font-size: 22px;
|
||||
color: #c5c5c5;
|
||||
transition: color 0.2s;
|
||||
|
||||
&.checked {
|
||||
color: #2d8cf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.multi-selected {
|
||||
background-color: rgba(45, 140, 240, 0.06);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dialog-avatar {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
@ -954,6 +979,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
.content-merge-forward {
|
||||
background: #f7f7f7;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.merge-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.merge-list {
|
||||
.merge-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 22px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.item-colon {
|
||||
margin: 0 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.merge-footer {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.content-file {
|
||||
position: relative;
|
||||
|
||||
@ -1977,6 +2054,31 @@
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.dialog-multi-select-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 24px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
background-color: #fafafa;
|
||||
|
||||
.multi-select-info {
|
||||
font-size: 14px;
|
||||
color: #515a6e;
|
||||
|
||||
.multi-select-max {
|
||||
color: #ed4014;
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-select-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
@ -2814,3 +2916,56 @@ body.window-portrait {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.merge-forward-detail-modal {
|
||||
.merge-detail-scroller {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.merge-detail-list {
|
||||
.merge-detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
& + .merge-detail-item {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.detail-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +79,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
.forward-mode-select {
|
||||
padding-bottom: 12px;
|
||||
|
||||
.ivu-radio-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.merge-forward-preview {
|
||||
background: #f7f7f7;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.merge-preview-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.merge-preview-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 22px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.preview-desc {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.merge-preview-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.leave-message {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user