feat: 消息置顶功能 - 50%

This commit is contained in:
weifashi 2023-12-21 17:20:42 +08:00
parent 48cd32742c
commit 59aa854470
12 changed files with 385 additions and 7 deletions

View File

@ -438,6 +438,8 @@ class DialogController extends AbstractController
$builder->where('tag', '>', 0); $builder->where('tag', '>', 0);
} elseif ($msg_type === 'todo') { } elseif ($msg_type === 'todo') {
$builder->where('todo', '>', 0); $builder->where('todo', '>', 0);
} elseif ($msg_type === 'top') {
$builder->whereNotNull('top_at');
} elseif ($msg_type === 'link') { } elseif ($msg_type === 'link') {
$builder->whereLink(1); $builder->whereLink(1);
} elseif (in_array($msg_type, ['text', 'image', 'file', 'record', 'meeting'])) { } elseif (in_array($msg_type, ['text', 'image', 'file', 'record', 'meeting'])) {
@ -507,6 +509,7 @@ class DialogController extends AbstractController
if ($reDialog) { if ($reDialog) {
$data['dialog'] = $dialog->formatData($user->userid, true); $data['dialog'] = $dialog->formatData($user->userid, true);
$data['todo'] = $data['dialog']->todo_num > 0 ? WebSocketDialogMsgTodo::whereDialogId($dialog->id)->whereUserid($user->userid)->whereDoneAt(null)->orderByDesc('id')->take(50)->get() : []; $data['todo'] = $data['dialog']->todo_num > 0 ? WebSocketDialogMsgTodo::whereDialogId($dialog->id)->whereUserid($user->userid)->whereDoneAt(null)->orderByDesc('id')->take(50)->get() : [];
$data['tops'] = WebSocketDialogMsg::whereDialogId($dialog->id)->whereNotNull('top_at')->orderByDesc('top_at')->take(50)->get();
} }
return Base::retSuccess('success', $data); return Base::retSuccess('success', $data);
} }
@ -2169,4 +2172,60 @@ class DialogController extends AbstractController
return Base::retSuccess('发送成功', $result); return Base::retSuccess('发送成功', $result);
} }
/**
* @api {get} api/dialog/msg/top 46. 置顶/取消置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__top
*
* @apiParam {Number} msg_id 消息ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__top()
{
$user = User::auth();
//
$msg_id = intval(Request::input("msg_id"));
//
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
WebSocketDialog::checkDialog($msg->dialog_id);
//
return $msg->toggleTopMsg($user->userid);
}
/**
* @api {get} api/dialog/toplist 47. 获取置顶列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName toplist
*
* @apiParam {Number} dialog_id 会话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function toplist()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
//
WebSocketDialog::checkDialog($dialog_id);
//
$tops = WebSocketDialogMsg::whereDialogId($dialog_id)->whereNotNull('top_at')->orderByDesc('top_at')->take(50)->get();
//
return Base::retSuccess('success', $tops ?: []);
}
} }

View File

@ -380,6 +380,49 @@ class WebSocketDialogMsg extends AbstractModel
return Base::retSuccess($this->todo ? '设置成功' : '取消成功', $data); return Base::retSuccess($this->todo ? '设置成功' : '取消成功', $data);
} }
/**
* 置顶、取消置顶
* @param int $sender 置顶的会员ID
* @return mixed
*/
public function toggleTopMsg($sender)
{
$before = $this->top;
$beforeTopAt = $this->top_at;
$this->top = $before ? 0 : $sender;
$this->top_at = $before ? null : Carbon::now();
$this->save();
$resData = [
'id' => $this->id,
'top' => $this->top,
'top_at' => $this->top_at,
];
//
$data = [
'update' => $resData
];
$res = self::sendMsg(null, $this->dialog_id, 'top', [
'action' => $this->top ? 'add' : 'remove',
'data' => [
'id' => $this->id,
'type' => $this->type,
'msg' => $this->quoteTextMsg(),
]
], $sender);
if (Base::isSuccess($res)) {
$data['add'] = $res['data'];
$dialog = WebSocketDialog::find($this->dialog_id);
$resData['tops'] = WebSocketDialogMsg::whereDialogId($dialog->id)->whereNotNull('top_at')->orderByDesc('top_at')->take(50)->get();
$dialog->pushMsg('update', $resData);
} else {
$this->top = $before;
$this->top_at = $beforeTopAt;
$this->save();
}
//
return Base::retSuccess($this->top ? '置顶成功' : '取消成功', $data);
}
/** /**
* 转发消息 * 转发消息
* @param array|int $dialogids * @param array|int $dialogids
@ -536,6 +579,9 @@ class WebSocketDialogMsg extends AbstractModel
case 'tag': case 'tag':
$action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注'; $action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}"; return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'top':
$action = $data['msg']['action'] === 'remove' ? '取消置顶' : '置顶';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'todo': case 'todo':
$action = $data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办'); $action = $data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办');
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}"; return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";

View File

@ -168,7 +168,7 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
$table->dropIndex(['msg_id']); $table->dropIndex(['msg_id']);
$table->dropIndex(['userid']); $table->dropIndex(['userid']);
}); });
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) { Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
$table->dropIndex(['dialog_id']); $table->dropIndex(['dialog_id']);
}); });

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogMsgsTopAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'top')) {
$table->bigInteger('top')->nullable()->default(0)->after('send')->comment('置顶的会员ID');
$table->timestamp('top_at')->nullable()->after('top')->comment('置顶时间');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropColumn("top");
$table->dropColumn("top_at");
});
}
}

View File

@ -839,6 +839,8 @@
return `[${$A.L('文件')}] ${data.msg.name}` return `[${$A.L('文件')}] ${data.msg.name}`
case 'tag': case 'tag':
return `[${$A.L(data.msg.action === 'remove' ? '取消标注' : '标注')}] ${$A.getMsgSimpleDesc(data.msg.data)}` return `[${$A.L(data.msg.action === 'remove' ? '取消标注' : '标注')}] ${$A.getMsgSimpleDesc(data.msg.data)}`
case 'top':
return `[${$A.L(data.msg.action === 'remove' ? '取消置顶' : '置顶*')}] ${$A.getMsgSimpleDesc(data.msg.data)}`
case 'todo': case 'todo':
return `[${$A.L(data.msg.action === 'remove' ? '取消待办' : (data.msg.action === 'done' ? '完成' : '设待办'))}] ${$A.getMsgSimpleDesc(data.msg.data)}` return `[${$A.L(data.msg.action === 'remove' ? '取消待办' : (data.msg.action === 'done' ? '完成' : '设待办'))}] ${$A.getMsgSimpleDesc(data.msg.data)}`
case 'notice': case 'notice':

View File

@ -8,6 +8,11 @@
{{$L(source.msg.action === 'remove' ? '取消标注' : '标注了')}} {{$L(source.msg.action === 'remove' ? '取消标注' : '标注了')}}
"{{$A.getMsgSimpleDesc(source.msg.data)}}" "{{$A.getMsgSimpleDesc(source.msg.data)}}"
</div> </div>
<div v-if="source.type === 'top'" class="dialog-top" @click="onViewTag">
<div class="tag-user"><UserAvatar :userid="source.userid" :show-name="true" :show-icon="false"/></div>
{{$L(source.msg.action === 'remove' ? '取消置顶' : '置顶了')}}
"{{$A.getMsgSimpleDesc(source.msg.data)}}"
</div>
<div v-else-if="source.type === 'todo'" class="dialog-todo" @click="onViewTodo"> <div v-else-if="source.type === 'todo'" class="dialog-todo" @click="onViewTodo">
<div class="todo-user"><UserAvatar :userid="source.userid" :show-name="true" :show-icon="false"/></div> <div class="todo-user"><UserAvatar :userid="source.userid" :show-name="true" :show-icon="false"/></div>
{{$L(source.msg.action === 'remove' ? '取消待办' : (source.msg.action === 'done' ? '完成' : '设待办'))}} {{$L(source.msg.action === 'remove' ? '取消待办' : (source.msg.action === 'done' ? '完成' : '设待办'))}}

View File

@ -169,6 +169,10 @@
<div v-if="msgData.tag" class="tag"> <div v-if="msgData.tag" class="tag">
<i class="taskfont">&#xe61e;</i> <i class="taskfont">&#xe61e;</i>
</div> </div>
<!--置顶-->
<div v-if="msgData.top" class="top">
<i class="taskfont">&#xe61e;</i>
</div>
<!--待办--> <!--待办-->
<div v-if="msgData.todo" class="todo" @click="openTodo"> <div v-if="msgData.todo" class="todo" @click="openTodo">
<EPopover <EPopover

View File

@ -136,6 +136,17 @@
</slot> </slot>
</div> </div>
<!--置顶消息-->
<div v-if="topMessage" class="dialog-top-message" @click="topViewShow = true">
<div class="dialog-top-message-content">
<p >{{$L('置顶消息')}}</p>
<p>{{$A.getMsgSimpleDesc(topMessage)}}</p>
</div>
<div class="dialog-top-message-font">
<i class="taskfont">&#xe7d4;</i>
</div>
</div>
<!--跳转提示--> <!--跳转提示-->
<div v-if="positionMsg" class="dialog-position" :class="{'down': tagShow}"> <div v-if="positionMsg" class="dialog-position" :class="{'down': tagShow}">
<div class="position-label" @click="onPositionMark"> <div class="position-label" @click="onPositionMark">
@ -287,6 +298,10 @@
<i class="taskfont">&#xe7b7;</i> <i class="taskfont">&#xe7b7;</i>
<span>{{ $L(operateItem.todo ? '取消待办' : '设待办') }}</span> <span>{{ $L(operateItem.todo ? '取消待办' : '设待办') }}</span>
</li> </li>
<li @click="onOperate('top')">
<i class="taskfont" v-html="operateItem.top_at ? '&#xe7e2;' : '&#xe7e4;'"></i>
<span>{{ $L(operateItem.top_at ? '取消置顶' : '置顶') }}</span>
</li>
<li v-if="msgType !== ''" @click="onOperate('pos')"> <li v-if="msgType !== ''" @click="onOperate('pos')">
<i class="taskfont">&#xee15;</i> <i class="taskfont">&#xee15;</i>
<span>{{ $L('完整对话') }}</span> <span>{{ $L('完整对话') }}</span>
@ -538,6 +553,34 @@
<!-- 群投票 --> <!-- 群投票 -->
<DialogGroupVote/> <DialogGroupVote/>
<!--置顶消息列表-->
<DrawerOverlay
v-model="topViewShow"
placement="right"
class-name="dialog-wrapper-drawer-list"
:size="500">
<div class="dialog-wrapper drawer-list">
<div class="dialog-nav">
<div class="drawer-title">{{$L('置顶消息')}}</div>
</div>
<Scrollbar class-name="dialog-scroller">
<div class="dialog-scroller-item" v-for="msg in topList.sort((a, b) => a.top_at - b.top_at)">
<DialogItem
:source="msg"
@on-view-text="onViewText"
@on-view-file="onViewFile"
@on-down-file="onDownFile"
@on-emoji="onEmoji"
simpleView/>
<div class="original-button-warp">
<Button class="original-button" icon="md-exit" type="text" :loading="todoViewPosLoad" @click="onPosTodo">{{ $L("回到原文") }}</Button>
<Button class="original-button" icon="md-exit" type="text" :loading="todoViewPosLoad" @click="onPosTodo">{{ $L("取消置顶") }}</Button>
</div>
</div>
</Scrollbar>
</div>
</DrawerOverlay>
</div> </div>
</template> </template>
@ -701,6 +744,8 @@ export default {
unreadMsgId: 0, // id unreadMsgId: 0, // id
toBottomReGetMsg: false, // toBottomReGetMsg: false, //
selectionRange: false, // selectionRange: false, //
topViewShow: false,
} }
}, },
@ -728,6 +773,7 @@ export default {
'dialogSearchMsgId', 'dialogSearchMsgId',
'dialogMsgs', 'dialogMsgs',
'dialogTodos', 'dialogTodos',
'dialogTops',
'dialogMsgTransfer', 'dialogMsgTransfer',
'cacheDialogs', 'cacheDialogs',
'wsOpenNum', 'wsOpenNum',
@ -1034,7 +1080,17 @@ export default {
return this.systemConfig.file_upload_limit * 1024 return this.systemConfig.file_upload_limit * 1024
} }
return 1024000 return 1024000
} },
topList() {
return this.dialogTops.filter(item => item.top_at && item.dialog_id == this.dialogId).sort((a, b) => {
return b.top_at - a.top_at;
});
},
topMessage() {
return this.topList[0]
},
}, },
watch: { watch: {
@ -2545,6 +2601,10 @@ export default {
this.onEmoji(value) this.onEmoji(value)
} }
break; break;
case "top":
this.onTop()
break;
} }
}) })
}, },
@ -2902,7 +2962,7 @@ export default {
url: 'dialog/msg/tag', url: 'dialog/msg/tag',
data, data,
}).then(({data}) => { }).then(({data}) => {
this.tagOrTodoSuccess(data) this.tagOrTodoOrTopSuccess(data)
}).catch(({msg}) => { }).catch(({msg}) => {
$A.messageError(msg); $A.messageError(msg);
}).finally(_ => { }).finally(_ => {
@ -3001,7 +3061,7 @@ export default {
data, data,
}).then(({data, msg}) => { }).then(({data, msg}) => {
resolve(msg) resolve(msg)
this.tagOrTodoSuccess(data) this.tagOrTodoOrTopSuccess(data)
this.onActive() this.onActive()
}).catch(({msg}) => { }).catch(({msg}) => {
reject(msg); reject(msg);
@ -3011,7 +3071,7 @@ export default {
}) })
}, },
tagOrTodoSuccess(data) { tagOrTodoOrTopSuccess(data) {
this.$store.dispatch("saveDialogMsg", data.update); this.$store.dispatch("saveDialogMsg", data.update);
if (data.add) { if (data.add) {
this.$store.dispatch("saveDialogMsg", data.add); this.$store.dispatch("saveDialogMsg", data.add);
@ -3176,6 +3236,46 @@ export default {
} }
}, },
onTop() {
if (this.operateVisible) {
return
}
if (this.operateItem?.top_at) {
$A.modalConfirm({
content: "你确定取消置顶吗?",
cancelText: '取消',
okText: '确定',
loading: true,
onOk: () => this.onTopSubmit(this.operateItem)
});
} else {
this.onTopSubmit(this.operateItem)
}
},
onTopSubmit(data) {
return new Promise((resolve, reject) => {
this.$store.dispatch("setLoad", {
key: `msg-${data.msg_id}`,
delay: 600
})
this.$store.dispatch("call", {
url: 'dialog/msg/top',
data: {
msg_id: data.id
},
}).then(({ data, msg }) => {
resolve(msg)
this.tagOrTodoOrTopSuccess({ update: data })
this.onActive()
}).catch(({ msg }) => {
reject(msg);
}).finally(_ => {
this.$store.dispatch("cancelLoad", `msg-${data.msg_id}`)
});
})
},
getUserApproveStatus() { getUserApproveStatus() {
this.approvaUserStatus = '' this.approvaUserStatus = ''
if (this.dialogData.type !== 'user' || this.dialogData.bot) { if (this.dialogData.type !== 'user' || this.dialogData.bot) {

View File

@ -2494,6 +2494,26 @@ export default {
}).catch(console.warn); }).catch(console.warn);
}, },
/**
* 获取会话置顶
* @param state
* @param dispatch
* @param dialog_id
*/
getDialogTop({state, dispatch}, dialog_id) {
dispatch("call", {
url: 'dialog/toplist',
data: {
dialog_id,
},
}).then(({data}) => {
if ($A.arrayLength(data) > 0) {
state.dialogTops = state.dialogTops.filter(item => item.dialog_id != dialog_id)
dispatch("saveDialogTop", data)
}
}).catch(console.warn);
},
/** /**
* 打开会话 * 打开会话
* @param state * @param state
@ -2703,6 +2723,44 @@ export default {
} }
}, },
/**
* 保存置顶数据
* @param state
* @param dispatch
* @param data
*/
saveDialogTop({state, dispatch}, data) {
$A.execMainDispatch("saveDialogTop", data)
//
if ($A.isArray(data)) {
data.forEach(item => {
dispatch("saveDialogTop", item)
});
} else if ($A.isJson(data)) {
const index = state.dialogTops.findIndex(item => item.id == data.id);
if (index > -1) {
state.dialogTops.splice(index, 1, Object.assign({}, state.dialogTops[index], data));
} else {
state.dialogTops.push(data);
}
}
},
/**
* 忘记置顶数据
* @param state
* @param dispatch
* @param msg_id
*/
forgetDialogTopForMsgId({state, dispatch}, msg_id) {
$A.execMainDispatch("forgetDialogTopForMsgId", msg_id)
//
const index = state.dialogTops.findIndex(item => item.msg_id == msg_id);
if (index > -1) {
state.dialogTops.splice(index, 1);
}
},
/** /**
* 保存聊天草稿 * 保存聊天草稿
* @param state * @param state
@ -2777,6 +2835,7 @@ export default {
} }
}) })
dispatch("forgetDialogTodoForMsgId", msg_id) dispatch("forgetDialogTodoForMsgId", msg_id)
dispatch("forgetDialogTopForMsgId", msg_id)
}, },
/** /**
@ -2845,6 +2904,10 @@ export default {
state.dialogTodos = state.dialogTodos.filter(item => item.dialog_id != data.dialog_id) state.dialogTodos = state.dialogTodos.filter(item => item.dialog_id != data.dialog_id)
dispatch("saveDialogTodo", resData.todo) dispatch("saveDialogTodo", resData.todo)
} }
if ($A.isArray(resData.tops)) {
state.dialogTops = state.dialogTops.filter(item => item.dialog_id != data.dialog_id)
dispatch("saveDialogTop", resData.tops)
}
// //
dispatch("saveDialogMsg", resData.list) dispatch("saveDialogMsg", resData.list)
resolve(result) resolve(result)
@ -3290,6 +3353,11 @@ export default {
if (typeof data.todo !== "undefined") { if (typeof data.todo !== "undefined") {
dispatch("getDialogTodo", dialog_id) dispatch("getDialogTodo", dialog_id)
} }
// 更新置顶
if ($A.isArray(data.tops)) {
state.dialogTops = data.tops
dispatch("saveDialogTop", data.tops)
}
return; return;
} }
if (count <= 5) { if (count <= 5) {

View File

@ -110,6 +110,7 @@ export default {
dialogIns: [], dialogIns: [],
dialogMsgs: [], dialogMsgs: [],
dialogTodos: [], dialogTodos: [],
dialogTops: [],
dialogHistory: [], dialogHistory: [],
dialogDraftTimer: {}, dialogDraftTimer: {},
dialogMsgTransfer: {time: 0}, dialogMsgTransfer: {time: 0},

View File

@ -174,7 +174,7 @@
} }
&.twice-affirm{ &.twice-affirm{
padding-bottom: 20px; padding-bottom: 16px;
.search-selected{ .search-selected{
max-width: 100%; max-width: 100%;
} }

View File

@ -41,6 +41,19 @@
margin: 0 auto; margin: 0 auto;
box-shadow: none; box-shadow: none;
} }
.dialog-scroller-item{
border-bottom: 1px solid #eeeeee;
margin-bottom: 16px;
.reply-item {
border-bottom: none;
margin-bottom: 0;
}
.original-button-warp{
display: flex;
margin-bottom: 16px;
}
}
} }
.todo-button { .todo-button {
@ -421,6 +434,38 @@
} }
} }
.dialog-top-message {
padding: 10px;
position: relative;
display: flex;
&:before {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background-color: #f4f5f5;
}
.dialog-top-message-content {
flex: 1;
overflow: hidden;
p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.dialog-top-message-font {
width: 40px;
line-height: 42px;
text-align: center;
.taskfont{
font-size: 22px;
}
}
}
.dialog-scroller { .dialog-scroller {
flex: 1; flex: 1;
position: relative; position: relative;
@ -447,6 +492,7 @@
.dialog-tag, .dialog-tag,
.dialog-todo, .dialog-todo,
.dialog-top,
.dialog-notice, .dialog-notice,
.dialog-new { .dialog-new {
font-size: 12px; font-size: 12px;
@ -459,6 +505,7 @@
word-wrap: break-word; word-wrap: break-word;
} }
.dialog-top,
.dialog-tag { .dialog-tag {
cursor: pointer; cursor: pointer;
@ -1635,6 +1682,9 @@
display: none !important; display: none !important;
} }
} }
.dialog-avatar{
display: none;
}
&.self { &.self {
.dialog-head{ .dialog-head{
background-color: #F4F5F7; background-color: #F4F5F7;
@ -1652,7 +1702,14 @@
color: #84C56A !important; color: #84C56A !important;
} }
} }
.dialog-emoji{
.avatar-name{
color: #818181;
}
> li.hasme{
background-color: #e1e1e1;
}
}
} }
} }
} }