feat(dialog): 重构合并转发功能

- 合并转发消息体改为存储 msg_ids + preview,不再存储完整消息列表
- 新增 mergedetail API 按需加载合并转发详情
- 详情展示从 Modal 改为 DrawerOverlay,支持完整消息渲染
- 统一不可转发消息类型过滤(tag/top/todo/notice/word-chain/vote/template)
- 合并转发标题改为前端国际化拼接
- DialogWrapper 支持 staticMsgs 静态模式用于详情渲染
- 优化多选操作栏和转发确认界面样式
This commit is contained in:
kuaifan 2026-04-05 09:31:41 +00:00
parent 00a2ea3d2f
commit 6a71964592
13 changed files with 277 additions and 152 deletions

View File

@ -2334,6 +2334,9 @@ class DialogController extends AbstractController
} }
WebSocketDialog::checkDialog($msgs->first()->dialog_id); WebSocketDialog::checkDialog($msgs->first()->dialog_id);
foreach ($msgs as $msg) { foreach ($msgs as $msg) {
if (in_array($msg->type, WebSocketDialogMsg::$unforwardableTypes)) {
continue;
}
$res = $msg->forwardMsg($dialogids, $userids, $user, $show_source, $leave_message); $res = $msg->forwardMsg($dialogids, $userids, $user, $show_source, $leave_message);
if (Base::isSuccess($res)) { if (Base::isSuccess($res)) {
$allMsgs = array_merge($allMsgs, $res['data']['msgs']); $allMsgs = array_merge($allMsgs, $res['data']['msgs']);
@ -2356,12 +2359,12 @@ class DialogController extends AbstractController
} }
/** /**
* @api {get} api/dialog/msg/merge-forward 合并转发消息 * @api {get} api/dialog/msg/mergeforward 合并转发消息
* *
* @apiDescription 需要token身份 * @apiDescription 需要token身份
* @apiVersion 1.0.0 * @apiVersion 1.0.0
* @apiGroup dialog * @apiGroup dialog
* @apiName msg__merge_forward * @apiName msg__mergeforward
* *
* @apiParam {Array} msg_ids 消息ID数组最多100条 * @apiParam {Array} msg_ids 消息ID数组最多100条
* @apiParam {Array} dialogids 转发给的对话ID * @apiParam {Array} dialogids 转发给的对话ID
@ -2373,7 +2376,7 @@ class DialogController extends AbstractController
* @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据 * @apiSuccess {Object} data 返回数据
*/ */
public function msg__merge_forward() public function msg__mergeforward()
{ {
$user = User::auth(); $user = User::auth();
// //
@ -2396,6 +2399,57 @@ class DialogController extends AbstractController
return WebSocketDialogMsg::mergeForwardMsg($msg_ids, $dialogids, $userids, $user, $show_source, $leave_message); return WebSocketDialogMsg::mergeForwardMsg($msg_ids, $dialogids, $userids, $user, $show_source, $leave_message);
} }
/**
* @api {get} api/dialog/msg/mergedetail 合并转发消息详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__mergedetail
*
* @apiParam {Number} msg_id 合并转发消息ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__mergedetail()
{
User::auth();
//
$msg_id = intval(Request::input('msg_id'));
if ($msg_id <= 0) {
return Base::retError('参数错误');
}
$dialogMsg = WebSocketDialogMsg::find($msg_id);
if (!$dialogMsg || $dialogMsg->type !== 'merge-forward') {
return Base::retError('消息不存在或已被删除');
}
WebSocketDialog::checkDialog($dialogMsg->dialog_id);
//
$msgData = Base::json2array($dialogMsg->getRawOriginal('msg'));
$msgIds = $msgData['msg_ids'] ?? [];
if (empty($msgIds)) {
return Base::retError('消息不存在或已被删除');
}
$msgs = WebSocketDialogMsg::withTrashed()
->whereIn('id', $msgIds)
->orderBy('created_at')
->get()
->map(function ($msg) {
return [
'id' => $msg->id,
'userid' => $msg->userid,
'type' => $msg->type,
'msg' => $msg->msg,
'created_at' => $msg->created_at->toDateTimeString(),
];
});
return Base::retSuccess('success', [
'msgs' => $msgs,
]);
}
/** /**
* @api {get} api/dialog/msg/emoji emoji回复 * @api {get} api/dialog/msg/emoji emoji回复
* *

View File

@ -500,7 +500,7 @@ class Setting extends AbstractModel
} }
$limitTime = Carbon::parse($dialogMsg->created_at)->addMinutes($limitNum); $limitTime = Carbon::parse($dialogMsg->created_at)->addMinutes($limitNum);
if ($limitTime->lt(Carbon::now())) { if ($limitTime->lt(Carbon::now())) {
throw new ApiException('已超过' . Doo::translate(Base::forumMinuteDay($limitNum)) . '' . $error); throw new ApiException('已超过' . Base::forumMinuteDay($limitNum) . '' . $error);
} }
} }
} }

View File

@ -533,9 +533,17 @@ class WebSocketDialogMsg extends AbstractModel
return $dialogs; return $dialogs;
} }
/**
* 不支持转发的消息类型
*/
public static $unforwardableTypes = ['tag', 'top', 'todo', 'notice', 'word-chain', 'vote', 'template'];
public function forwardMsg($dialogids, $userids, $user, $showSource = 1, $leaveMessage = '') public function forwardMsg($dialogids, $userids, $user, $showSource = 1, $leaveMessage = '')
{ {
return AbstractModel::transaction(function () use ($dialogids, $user, $userids, $showSource, $leaveMessage) { return AbstractModel::transaction(function () use ($dialogids, $user, $userids, $showSource, $leaveMessage) {
if (in_array($this->type, self::$unforwardableTypes)) {
throw new ApiException('此类型消息不支持转发');
}
$msgData = Base::json2array($this->getRawOriginal('msg')); $msgData = Base::json2array($this->getRawOriginal('msg'));
$forwardData = is_array($msgData['forward_data']) ? $msgData['forward_data'] : []; $forwardData = is_array($msgData['forward_data']) ? $msgData['forward_data'] : [];
$forwardId = $forwardData['id'] ?: $this->id; $forwardId = $forwardData['id'] ?: $this->id;
@ -601,31 +609,35 @@ class WebSocketDialogMsg extends AbstractModel
throw new ApiException('只能合并转发同一对话的消息'); throw new ApiException('只能合并转发同一对话的消息');
} }
WebSocketDialog::checkDialog($dialogId); WebSocketDialog::checkDialog($dialogId);
// 收集发送者生成标题 // 过滤不支持转发的消息类型
$msgs = $msgs->filter(function ($msg) {
return !in_array($msg->type, self::$unforwardableTypes);
});
if ($msgs->isEmpty()) {
throw new ApiException('所选消息均不支持转发');
}
// 收集发送者信息
$senderIds = $msgs->pluck('userid')->unique()->values()->toArray(); $senderIds = $msgs->pluck('userid')->unique()->values()->toArray();
$senderNames = User::whereIn('userid', array_slice($senderIds, 0, 2)) $senderNames = User::whereIn('userid', array_slice($senderIds, 0, 2))
->pluck('nickname') ->pluck('nickname')
->toArray(); ->toArray();
$title = implode(Doo::translate('和'), $senderNames); // 组装预览列表前4条精简字段
if (count($senderIds) > 2) { $msgIds = $msgs->pluck('id')->toArray();
$title .= Doo::translate('等人'); $preview = [];
} foreach ($msgs->take(4) as $msg) {
$title .= Doo::translate('的聊天记录'); $preview[] = [
// 组装消息列表
$list = [];
foreach ($msgs as $msg) {
$list[] = [
'userid' => $msg->userid, 'userid' => $msg->userid,
'type' => $msg->type, 'type' => $msg->type,
'msg' => Base::json2array($msg->getRawOriginal('msg')), 'msg' => self::buildPreviewMsg($msg->type, Base::json2array($msg->getRawOriginal('msg'))),
'created_at' => $msg->created_at->toDateTimeString(),
]; ];
} }
// 构建合并转发消息体 // 构建合并转发消息体
$msgData = [ $msgData = [
'title' => $title, 'sender_names' => $senderNames,
'list' => $list, 'sender_total' => count($senderIds),
'count' => count($list), 'msg_ids' => $msgIds,
'preview' => $preview,
'count' => count($msgIds),
'forward_data' => [ 'forward_data' => [
'show' => $showSource, 'show' => $showSource,
'leave' => $leaveMessage ? 1 : 0, 'leave' => $leaveMessage ? 1 : 0,
@ -652,6 +664,26 @@ class WebSocketDialogMsg extends AbstractModel
}); });
} }
/**
* 构建预览消息(精简字段)
* @param string $type
* @param array $msg
* @return array
*/
private static function buildPreviewMsg($type, $msg)
{
switch ($type) {
case 'text':
return ['text' => $msg['text'] ?? ''];
case 'file':
return ['name' => $msg['name'] ?? '', 'ext' => $msg['ext'] ?? ''];
case 'location':
return ['title' => $msg['title'] ?? ''];
default:
return [];
}
}
/** /**
* 删除消息 * 删除消息
* @param array|int $ids * @param array|int $ids
@ -784,8 +816,7 @@ class WebSocketDialogMsg extends AbstractModel
return self::previewTemplateMsg($data['msg']); return self::previewTemplateMsg($data['msg']);
case 'merge-forward': case 'merge-forward':
$action = Doo::translate("聊天记录"); return "[" . Doo::translate("聊天记录") . "]";
return "[{$action}] " . Base::cutStr($data['msg']['title'] ?? '', 50);
case 'preview': case 'preview':
return $data['msg']['preview']; return $data['msg']['preview'];

View File

@ -963,3 +963,13 @@ AI建议采纳(*)建议
消息内容格式错误 消息内容格式错误
AI 调用失败 AI 调用失败
AI 返回内容为空 AI 返回内容为空
修改AI自动分析
关联不存在
只能合并转发同一对话的消息
所选消息均不支持转发
无法创建任务对话
最多转发(*)条消息
此类型消息不支持转发
没有权限操作此任务
请选择要转发的消息

View File

@ -2329,3 +2329,21 @@ AI 消息助手
指派 指派
关联 关联
采纳 采纳
逐条转发
合并转发
AI任务分析
关闭后所有项目将不再自动分析任务。
关闭后本项目将不再自动分析任务。
新建任务后AI自动分析并给出建议。
(最多(*)条)
最多选择(*)条消息
系统已关闭AI任务分析功能。
聊天记录
确定要解除与任务 #(*) 的关联吗?
共(*)条消息
已选(*)条
(*)的聊天记录
(*)和(*)的聊天记录
(*)和(*)等人的聊天记录

View File

@ -431,6 +431,20 @@ import {convertLocalResourcePath} from "../components/Replace/utils";
* @param imgClassName * @param imgClassName
* @returns {string|*} * @returns {string|*}
*/ */
getMergeForwardTitle(msg) {
const names = msg.sender_names || [];
if (names.length === 0) {
return $A.L('聊天记录');
}
if (names.length === 1) {
return $A.L('(*)的聊天记录', names[0]);
}
if (msg.sender_total > 2) {
return $A.L('(*)和(*)等人的聊天记录', names[0], names[1]);
}
return $A.L('(*)和(*)的聊天记录', names[0], names[1]);
},
getMsgSimpleDesc(data, imgClassName = null) { getMsgSimpleDesc(data, imgClassName = null) {
if (!$A.isJson(data)) { if (!$A.isJson(data)) {
return ''; return '';
@ -462,7 +476,7 @@ import {convertLocalResourcePath} from "../components/Replace/utils";
const notice = data.msg.source === 'api' ? data.msg.notice : $A.L(data.msg.notice); const notice = data.msg.source === 'api' ? data.msg.notice : $A.L(data.msg.notice);
return $A.cutString(notice, 50) return $A.cutString(notice, 50)
case 'merge-forward': case 'merge-forward':
return `[${$A.L('聊天记录')}] ${$A.cutString(data.msg.title || '', 50)}` return `[${$A.L('聊天记录')}] ${$A.cutString($A.getMergeForwardTitle(data.msg), 50)}`
case 'template': case 'template':
return $A.templateMsgSimpleDesc(data.msg) return $A.templateMsgSimpleDesc(data.msg)
case 'preview': case 'preview':

View File

@ -72,7 +72,8 @@
@on-error="onError" @on-error="onError"
@on-emoji="onEmoji" @on-emoji="onEmoji"
@on-other="onOther" @on-other="onOther"
@on-show-emoji-user="onShowEmojiUser"/> @on-show-emoji-user="onShowEmojiUser"
@on-merge-forward-detail="onMergeForwardDetail"/>
</template> </template>
</div> </div>
</template> </template>
@ -181,7 +182,7 @@ export default {
}, },
isSelectableMsg() { isSelectableMsg() {
return !['tag', 'top', 'todo', 'notice'].includes(this.source.type); return !['tag', 'top', 'todo', 'notice', 'word-chain', 'vote', 'template'].includes(this.source.type);
}, },
classArray() { classArray() {
@ -323,6 +324,10 @@ export default {
this.dispatch("on-show-emoji-user", data) this.dispatch("on-show-emoji-user", data)
}, },
onMergeForwardDetail(data) {
this.dispatch("on-merge-forward-detail", data)
},
dispatch(event, ...arg) { dispatch(event, ...arg) {
if (this.isReply) { if (this.isReply) {
this.$emit(event, ...arg) this.$emit(event, ...arg)

View File

@ -44,7 +44,7 @@
<!--投票--> <!--投票-->
<VoteMsg v-else-if="msgData.type === 'vote'" :msg="msgData.msg" :voteData="voteData" @onVote="onVote($event, msgData)"/> <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"/> <MergeForwardMsg v-else-if="msgData.type === 'merge-forward'" :msg="msgData.msg" @on-view-detail="onMergeForwardDetail"/>
<!--模板--> <!--模板-->
<TemplateMsg v-else-if="msgData.type === 'template'" :msg="msgData.msg" @viewText="viewText"/> <TemplateMsg v-else-if="msgData.type === 'template'" :msg="msgData.msg" @viewText="viewText"/>
<!--等待--> <!--等待-->
@ -592,6 +592,10 @@ export default {
this.$emit("on-show-emoji-user", item) this.$emit("on-show-emoji-user", item)
}, },
onMergeForwardDetail(msg) {
this.$emit("on-merge-forward-detail", {msgId: this.msgData.id, msgData: msg})
},
sortEmojiUser(useris) { sortEmojiUser(useris) {
const myList = useris.filter(item => item == this.userId); const myList = useris.filter(item => item == this.userId);
const otherList = useris.filter(item => item != this.userId); const otherList = useris.filter(item => item != this.userId);

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="content-merge-forward" @click="openDetail"> <div class="content-merge-forward" @click="openDetail">
<div class="merge-title">{{ msg.title }}</div> <div class="merge-title">{{ mergeTitle }}</div>
<div class="merge-list"> <div class="merge-list">
<div v-for="(item, index) in displayList" :key="index" class="merge-item"> <div v-for="(item, index) in displayList" :key="index" class="merge-item">
<UserAvatar :userid="item.userid" :show-icon="false" :show-name="true" :size="14"/> <UserAvatar :userid="item.userid" :show-icon="false" :show-name="true" :size="14"/>
@ -8,44 +8,7 @@
<span class="item-desc" v-html="$A.getMsgSimpleDesc(item)"></span> <span class="item-desc" v-html="$A.getMsgSimpleDesc(item)"></span>
</div> </div>
</div> </div>
<div class="merge-footer">{{ $L('共') }} {{ msg.count || msg.list.length }} {{ $L('条消息') }}</div> <div class="merge-footer">{{ $L('共(*)条消息', msg.count || 0) }}</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> </div>
</template> </template>
@ -58,20 +21,17 @@ export default {
default: () => ({}) default: () => ({})
} }
}, },
data() {
return {
detailShow: false
}
},
computed: { computed: {
displayList() { displayList() {
if (!this.msg || !this.msg.list) return []; return this.msg?.preview || [];
return this.msg.list.slice(0, 4); },
mergeTitle() {
return $A.getMergeForwardTitle(this.msg);
} }
}, },
methods: { methods: {
openDetail() { openDetail() {
this.detailShow = true; this.$emit("on-view-detail", this.msg);
} }
} }
} }

View File

@ -220,6 +220,7 @@
@on-emoji="onEmoji" @on-emoji="onEmoji"
@on-other="onOther" @on-other="onOther"
@on-show-emoji-user="onShowEmojiUser" @on-show-emoji-user="onShowEmojiUser"
@on-merge-forward-detail="onMergeForwardDetail"
@on-multi-select-toggle="onMultiSelectToggle"> @on-multi-select-toggle="onMultiSelectToggle">
<template #header v-if="!isChildComponent"> <template #header v-if="!isChildComponent">
<div class="dialog-item head-box"> <div class="dialog-item head-box">
@ -233,9 +234,9 @@
</div> </div>
<!--多选操作栏--> <!--多选操作栏-->
<div v-if="multiSelectMode" class="dialog-multi-select-bar"> <div v-if="!isStaticMode && multiSelectMode" class="dialog-multi-select-bar">
<div class="multi-select-info"> <div class="multi-select-info">
<span>{{ $L('已选') }} {{ selectedMsgIds.length }} {{ $L('条') }}</span> <span>{{ $L('已选(*)条', selectedMsgIds.length) }}</span>
<span v-if="selectedMsgIds.length >= 100" class="multi-select-max">{{ $L('(最多100条)') }}</span> <span v-if="selectedMsgIds.length >= 100" class="multi-select-max">{{ $L('(最多100条)') }}</span>
</div> </div>
<div class="multi-select-actions"> <div class="multi-select-actions">
@ -245,7 +246,7 @@
</div> </div>
<!--底部输入--> <!--底部输入-->
<div v-show="!multiSelectMode" ref="footer" class="dialog-footer" @click="onClickFooter"> <div v-if="!isStaticMode" v-show="!multiSelectMode" ref="footer" class="dialog-footer" @click="onClickFooter">
<!--滚动到底部--> <!--滚动到底部-->
<div <div
v-if="scrollTail > 500 || (msgNew > 0 && allMsgs.length > 0)" v-if="scrollTail > 500 || (msgNew > 0 && allMsgs.length > 0)"
@ -673,6 +674,26 @@
</div> </div>
</DrawerOverlay> </DrawerOverlay>
<!--合并转发详情-->
<DrawerOverlay
v-model="mergeForwardShow"
placement="right"
class-name="dialog-wrapper-list"
:size="500">
<template v-if="mergeForwardShow">
<div v-if="mergeForwardLoading" style="display:flex;align-items:center;justify-content:center;height:100%">
<Spin size="large"/>
</div>
<DialogWrapper
v-else
:staticMsgs="mergeForwardMsgs"
isChildComponent
class="inde-list">
<div slot="head" class="drawer-title">{{ mergeForwardTitle }}</div>
</DialogWrapper>
</template>
</DrawerOverlay>
<!-- 群接龙 --> <!-- 群接龙 -->
<DialogGroupWordChain/> <DialogGroupWordChain/>
@ -757,6 +778,11 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
// store API
staticMsgs: {
type: Array,
default: null
},
beforeBack: Function beforeBack: Function
}, },
@ -858,6 +884,11 @@ export default {
todoViewMid: 0, todoViewMid: 0,
todoViewId: 0, todoViewId: 0,
mergeForwardShow: false,
mergeForwardData: {},
mergeForwardMsgs: [],
mergeForwardLoading: false,
scrollDisabled: false, scrollDisabled: false,
scrollDirection: null, scrollDirection: null,
scrollAction: 0, scrollAction: 0,
@ -891,18 +922,22 @@ export default {
}, },
mounted() { mounted() {
if (!this.isStaticMode) {
emitter.on('websocketMsg', this.onWebsocketMsg); emitter.on('websocketMsg', this.onWebsocketMsg);
emitter.on('streamMsgData', this.onMsgChange); emitter.on('streamMsgData', this.onMsgChange);
this.keepInterval = setInterval(this.keepIntoInput, 1000) this.keepInterval = setInterval(this.keepIntoInput, 1000)
this.windowTouch && document.addEventListener('selectionchange', this.onSelectionchange); this.windowTouch && document.addEventListener('selectionchange', this.onSelectionchange);
}
}, },
beforeDestroy() { beforeDestroy() {
if (!this.isStaticMode) {
this.windowTouch && document.removeEventListener('selectionchange', this.onSelectionchange); this.windowTouch && document.removeEventListener('selectionchange', this.onSelectionchange);
clearInterval(this.keepInterval); clearInterval(this.keepInterval);
emitter.off('streamMsgData', this.onMsgChange); emitter.off('streamMsgData', this.onMsgChange);
emitter.off('websocketMsg', this.onWebsocketMsg); emitter.off('websocketMsg', this.onWebsocketMsg);
this.generateUnreadData(this.dialogId) this.generateUnreadData(this.dialogId)
}
// //
if (!this.isChildComponent) { if (!this.isChildComponent) {
this.$store.dispatch('forgetInDialog', {uid: this._uid}) this.$store.dispatch('forgetInDialog', {uid: this._uid})
@ -947,7 +982,16 @@ export default {
...mapGetters(['isLoad', 'isMessengerPage', 'getDialogQuote']), ...mapGetters(['isLoad', 'isMessengerPage', 'getDialogQuote']),
isStaticMode() {
return this.staticMsgs !== null
},
mergeForwardTitle() {
return $A.getMergeForwardTitle(this.mergeForwardData);
},
isReady() { isReady() {
if (this.isStaticMode) return true
return this.dialogId > 0 && this.dialogData.id > 0 return this.dialogId > 0 && this.dialogData.id > 0
}, },
@ -1006,6 +1050,9 @@ export default {
}, },
allMsgList() { allMsgList() {
if (this.isStaticMode) {
return this.staticMsgs || []
}
const array = []; const array = [];
array.push(...this.dialogMsgList.filter(item => this.msgFilter(item))); array.push(...this.dialogMsgList.filter(item => this.msgFilter(item)));
if (this.msgId > 0) { if (this.msgId > 0) {
@ -1276,6 +1323,13 @@ export default {
watch: { watch: {
dialogId: { dialogId: {
handler(dialog_id, old_id) { handler(dialog_id, old_id) {
if (this.isStaticMode) {
this.allMsgs = (this.staticMsgs || []).map((item, index) => {
if (!item.id) item.id = index + 1
return item
})
return
}
this.getDialogBase(dialog_id) this.getDialogBase(dialog_id)
this.generateUnreadData(old_id) this.generateUnreadData(old_id)
// //
@ -3027,7 +3081,7 @@ export default {
onForward(forwardData) { onForward(forwardData) {
const isMulti = forwardData.msg_ids && forwardData.msg_ids.length > 0; const isMulti = forwardData.msg_ids && forwardData.msg_ids.length > 0;
const url = isMulti const url = isMulti
? (forwardData.forward_mode === 'merge' ? 'dialog/msg/merge-forward' : 'dialog/msg/forward') ? (forwardData.forward_mode === 'merge' ? 'dialog/msg/mergeforward' : 'dialog/msg/forward')
: 'dialog/msg/forward'; : 'dialog/msg/forward';
const data = { const data = {
dialogids: forwardData.dialogids, dialogids: forwardData.dialogids,
@ -4017,6 +4071,26 @@ export default {
this.respondShow = true this.respondShow = true
}, },
onMergeForwardDetail({msgId, msgData}) {
if (this.operateVisible) {
return
}
this.mergeForwardData = msgData
this.mergeForwardMsgs = []
this.mergeForwardLoading = true
this.mergeForwardShow = true
this.$store.dispatch("call", {
url: 'dialog/msg/mergedetail',
data: { msg_id: msgId },
}).then(({data}) => {
this.mergeForwardMsgs = data.msgs || []
}).catch(_ => {
this.mergeForwardShow = false
}).finally(() => {
this.mergeForwardLoading = false
})
},
onOther({event, data}) { onOther({event, data}) {
if (this.operateVisible) { if (this.operateVisible) {
return return
@ -4263,8 +4337,8 @@ export default {
actionPermission(item, permission) { actionPermission(item, permission) {
switch (permission) { switch (permission) {
case 'forward': case 'forward':
if (['word-chain', 'vote', 'template'].includes(item.type)) { if (['tag', 'top', 'todo', 'notice', 'word-chain', 'vote', 'template'].includes(item.type)) {
return false // return false //
} }
break; break;

View File

@ -45,7 +45,7 @@
<UserAvatar :userid="item.userid" :show-icon="false" :show-name="true" :size="16"/> <UserAvatar :userid="item.userid" :show-icon="false" :show-name="true" :size="16"/>
<span class="preview-desc" v-html="$A.getMsgSimpleDesc(item)"></span> <span class="preview-desc" v-html="$A.getMsgSimpleDesc(item)"></span>
</div> </div>
<div class="merge-preview-count">{{ $L('共') }} {{ msgIds.length }} {{ $L('条消息') }}</div> <div class="merge-preview-count">{{ $L('共(*)条消息', msgIds.length) }}</div>
</div> </div>
</template> </template>
<template v-else> <template v-else>
@ -101,7 +101,7 @@
<Icon @click="onAinew" class="radio-icon" :type="ainew ? 'ios-checkmark-circle' : 'ios-radio-button-off'"/> <Icon @click="onAinew" class="radio-icon" :type="ainew ? 'ios-checkmark-circle' : 'ios-radio-button-off'"/>
<span @click="onAinew" class="radio-label">{{ $L('AI开启新会话') }}</span> <span @click="onAinew" class="radio-label">{{ $L('AI开启新会话') }}</span>
</li> </li>
<li v-if="!senderHidden" :class="{selected: !sender}"> <li v-if="!senderHidden && forwardMode !== 'merge'" :class="{selected: !sender}">
<Icon @click="onSender" class="radio-icon" :type="sender ? 'ios-radio-button-off' : 'ios-checkmark-circle'"/> <Icon @click="onSender" class="radio-icon" :type="sender ? 'ios-radio-button-off' : 'ios-checkmark-circle'"/>
<span @click="onSender" class="radio-label">{{ $L('不显示原发送者信息') }}</span> <span @click="onSender" class="radio-label">{{ $L('不显示原发送者信息') }}</span>
</li> </li>

View File

@ -656,7 +656,9 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 28px; width: 28px;
height: 28px;
flex-shrink: 0; flex-shrink: 0;
margin-top: 1px;
margin-right: 4px; margin-right: 4px;
cursor: pointer; cursor: pointer;
@ -666,16 +668,11 @@
transition: color 0.2s; transition: color 0.2s;
&.checked { &.checked {
color: #2d8cf0; color: $primary-color;
} }
} }
} }
&.multi-selected {
background-color: rgba(45, 140, 240, 0.06);
border-radius: 8px;
}
.dialog-avatar { .dialog-avatar {
position: relative; position: relative;
margin-bottom: 20px; margin-bottom: 20px;
@ -2055,12 +2052,13 @@
} }
.dialog-multi-select-bar { .dialog-multi-select-bar {
height: 52px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px 24px; padding: 0 24px;
border-top: 1px solid #e8e8e8; border-top: 1px solid #f4f5f5;
background-color: #fafafa; background-color: #F4F5F7;
.multi-select-info { .multi-select-info {
font-size: 14px; font-size: 14px;
@ -2076,6 +2074,10 @@
.multi-select-actions { .multi-select-actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
> button {
height: 28px;
padding: 0 9px;
}
} }
} }
@ -2916,56 +2918,3 @@ body.window-portrait {
opacity: 1; 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

@ -76,6 +76,11 @@
} }
} }
.reply-item {
border-bottom: 0;
margin-bottom: 0;
}
} }
} }
@ -84,7 +89,8 @@
.ivu-radio-group { .ivu-radio-group {
display: flex; display: flex;
gap: 16px; flex-wrap: wrap;
gap: 4px;
} }
} }