feat(task): 实现消息合并转发功能,支持批量选择和转发消息

This commit is contained in:
kuaifan 2026-04-04 07:43:26 +08:00
parent 4b32472d64
commit 984b98e4fc
11 changed files with 661 additions and 46 deletions

View File

@ -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回复
*

View File

@ -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"]);
}

View File

@ -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':

View File

@ -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)
},

View File

@ -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,

View File

@ -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>

View File

@ -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">&#xe638;</i>
<span>{{ $L('转发') }}</span>
</li>
<li v-if="actionPermission(operateItem, 'forward')" @click="onOperate('multiSelect')">
<i class="taskfont">&#xe7b7;</i>
<span>{{ $L('多选') }}</span>
</li>
<li v-if="operateItem.userid == userId" @click="onOperate('withdraw')">
<i class="taskfont">&#xe637;</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;

View File

@ -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
}

View File

@ -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;
}
//

View File

@ -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;
}
}
}
}
}
}

View File

@ -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;