feat: 支持搜索历史消息

This commit is contained in:
kuaifan 2022-06-20 01:14:39 +08:00
parent 4384a2dd91
commit d167a91a07
8 changed files with 257 additions and 65 deletions

View File

@ -62,6 +62,42 @@ class DialogController extends AbstractController
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/dialog/search 02. 搜索会话
*
* @apiDescription 根据消息关键词搜索相关会话需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName search
*
* @apiParam {String} key 消息关键词
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function search()
{
$user = User::auth();
//
$key = trim(Request::input('key'));
//
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'm.id as search_msg_id'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msgs as m', 'web_socket_dialogs.id', '=', 'm.dialog_id')
->where('u.userid', $user->userid)
->where('m.key', 'LIKE', "%{$key}%")
->orderByDesc('m.id')
->take(20)
->get();
//
$list->transform(function (WebSocketDialog $item) use ($user) {
return $item->formatData($user->userid);
});
//
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/dialog/one 02. 获取单个会话信息
*

View File

@ -58,29 +58,34 @@ class WebSocketDialog extends AbstractModel
*/
public function formatData($userid)
{
// 最后消息
$last_msg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
$this->last_msg = $last_msg;
// 未读信息
$unreadBuilder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $unreadBuilder->count();
$this->mention = 0;
$this->last_umid = 0;
if ($this->unread > 0) {
$this->mention = $unreadBuilder->clone()->whereMention(1)->count();
$this->last_umid = intval($unreadBuilder->clone()->orderByDesc('msg_id')->value('msg_id'));
if (isset($this->search_msg_id)) {
// 最后消息 (搜索预览消息)
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->find($this->search_msg_id);
$this->last_at = $this->last_msg?->created_at;
} else {
// 最后消息
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
// 未读信息
$unreadBuilder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $unreadBuilder->count();
$this->mention = 0;
$this->last_umid = 0;
if ($this->unread > 0) {
$this->mention = $unreadBuilder->clone()->whereMention(1)->count();
$this->last_umid = intval($unreadBuilder->clone()->orderByDesc('msg_id')->value('msg_id'));
}
$this->mark_unread = $this->mark_unread ?? WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->value('mark_unread');
// 对话人数
$builder = WebSocketDialogUser::whereDialogId($this->id);
$this->people = $builder->count();
}
$this->mark_unread = $this->mark_unread ?? WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->value('mark_unread');
// 对话人数
$builder = WebSocketDialogUser::whereDialogId($this->id);
$this->people = $builder->count();
// 对方信息
$this->dialog_user = null;
$this->group_info = null;
$this->top_at = $this->top_at ?? WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->value('top_at');
switch ($this->type) {
case "user":
$dialog_user = $builder->where('userid', '!=', $userid)->first();
$dialog_user = WebSocketDialogUser::whereDialogId($this->id)->where('userid', '!=', $userid)->first();
if ($dialog_user->userid === 0) {
$dialog_user->userid = $userid;
}

View File

@ -20,6 +20,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $type 消息类型
* @property array|mixed $msg 详细消息
* @property array|mixed $emoji emoji回复
* @property string|null $key 搜索关键词
* @property int|null $read 已阅数量
* @property int|null $send 发送数量
* @property int|null $reply_id 回复ID
@ -39,6 +40,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereEmoji($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereKey($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
@ -60,6 +62,7 @@ class WebSocketDialogMsg extends AbstractModel
];
protected $hidden = [
'key',
'updated_at',
];
@ -335,6 +338,19 @@ class WebSocketDialogMsg extends AbstractModel
}
}
/**
* 生成关键词
* @return string
*/
public function generateMsgKey()
{
return match ($this->type) {
'text' => strip_tags($this->msg['text']),
'meeting', 'file' => $this->msg['name'],
default => '',
};
}
/**
* 返回文本预览消息
* @param $text
@ -464,6 +480,7 @@ class WebSocketDialogMsg extends AbstractModel
$dialog->save();
$dialogMsg->send = 1;
$dialogMsg->dialog_type = $dialog->type;
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
});
Task::deliver(new WebSocketDialogMsgTask($dialogMsg->id));

View File

@ -0,0 +1,50 @@
<?php
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogMsgsKey extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'key')) {
$isAdd = true;
$table->text('key')->after('emoji')->nullable()->default('')->comment('搜索关键词');
}
});
if ($isAdd) {
\App\Models\WebSocketDialogMsg::chunkById(100, function ($lists) {
/** @var \App\Models\WebSocketDialogMsg $item */
foreach ($lists as $item) {
$key = $item->generateMsgKey();
if ($key) {
$item->key = $key;
$item->save();
}
}
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropColumn("key");
});
}
}

View File

@ -285,6 +285,10 @@ export default {
type: Number,
default: 0
},
searchMsgId: {
type: Number,
default: 0
},
autoFocus: {
type: Boolean,
default: false
@ -315,6 +319,7 @@ export default {
userids: [],
},
confirmId: 0,
dialogDrag: false,
groupInfoShow: false,
@ -473,7 +478,10 @@ export default {
this.allMsgs = this.allMsgList;
requestAnimationFrame(this.onToBottom);
}
this.$store.dispatch("getDialogMsgs", id).catch(_ => {});
this.$store.dispatch("getDialogMsgs", id).then(_ => {
this.confirmId = id;
setTimeout(this.onSearchMsgId, 100)
}).catch(_ => {});
//
this.$store.dispatch('saveInDialog', {
uid: this._uid,
@ -488,6 +496,10 @@ export default {
immediate: true
},
searchMsgId() {
this.onSearchMsgId();
},
dialogMsgTransfer: {
handler({time, msgFile, msgRecord, msgText}) {
if (time > $A.Time()) {
@ -675,6 +687,44 @@ export default {
}
},
onSearchMsgId() {
if (this.searchMsgId > 0 && this.confirmId === this.dialogId) {
this.onPositionId(this.searchMsgId)
this.$store.state.searchMsgId = 0
}
},
onPositionId(position_id, msg_id = 0) {
if (position_id === 0) {
return
}
const index = this.allMsgs.findIndex(item => item.id === position_id)
if (index > -1) {
this.onToIndex(index)
} else {
if (msg_id > 0) {
this.$store.dispatch("setLoad", {
key: `msg-${msg_id}`,
delay: 600
})
}
this.preventToBottom = true;
this.$store.dispatch("getDialogMoreMsgs", {
dialog_id: this.dialogId,
position_id
}).finally(_ => {
const index = this.allMsgs.findIndex(item => item.id === position_id)
if (index > -1) {
this.onToIndex(index)
}
if (msg_id > 0) {
this.$store.dispatch("cancelLoad", `msg-${msg_id}`)
}
this.preventToBottom = false;
})
}
},
itemClassAdd(index) {
return index === this.replyActiveIndex ? 'dialog-shake' : '';
},
@ -832,12 +882,13 @@ export default {
}
},
onToIndex(index, addOffset) {
onToIndex(index) {
const scroller = this.$refs.scroller;
if (scroller) {
scroller.scrollToIndex(index, addOffset);
requestAnimationFrame(_ => scroller.scrollToIndex(index, addOffset)) //
scroller.scrollToIndex(index, -100);
requestAnimationFrame(_ => scroller.scrollToIndex(index, -100)) //
}
requestAnimationFrame(_ => this.replyActiveIndex = index)
},
onToOffset(offset) {
@ -1123,31 +1174,7 @@ export default {
if (this.operateVisible) {
return
}
const runToIndex = (index) => {
this.onToIndex(index, -100)
requestAnimationFrame(_ => this.replyActiveIndex = index)
}
const index = this.allMsgs.findIndex(item => item.id === data.reply_id)
if (index > -1) {
runToIndex(index)
} else {
this.$store.dispatch("setLoad", {
key: `msg-${data.msg_id}`,
delay: 600
})
this.preventToBottom = true;
this.$store.dispatch("getDialogMoreMsgs", {
dialog_id: this.dialogId,
position_id: data.reply_id
}).finally(_ => {
const index = this.allMsgs.findIndex(item => item.id === data.reply_id)
if (index > -1) {
runToIndex(index)
}
this.$store.dispatch("cancelLoad", `msg-${data.msg_id}`)
this.preventToBottom = false;
})
}
this.onPositionId(data.reply_id, data.msg_id)
},
onViewText({target}) {

View File

@ -33,22 +33,20 @@
<ul
v-if="tabActive==='dialog'"
class="dialog">
<li v-if="dialogList.length === 0" class="nothing">
{{$L(dialogKey ? `没有任何与"${dialogKey}"相关的会话` : `没有任何会话`)}}
</li>
<template v-if="dialogList.length === 0">
<li v-if="dialogLoad > 0" class="loading"><Loading/></li>
<li v-else class="nothing">
{{$L(dialogKey ? `没有任何与"${dialogKey}"相关的会话` : `没有任何会话`)}}
</li>
</template>
<li
v-else
v-for="(dialog, key) in dialogList"
:ref="`dialog_${dialog.id}`"
:key="key"
:class="{
top: dialog.top_at,
active: dialog.id == dialogId,
operate: operateVisible && dialog.id == operateItem.id,
completed: $A.dialogCompleted(dialog)
}"
:data-id="dialog.id"
@click="openDialog(dialog.id)"
:class="dialogClass(dialog)"
@click="openDialog(dialog.id, dialog.search_msg_id)"
v-longpress="handleLongpress">
<template v-if="dialog.type=='group'">
<i v-if="dialog.group_type=='project'" class="taskfont icon-avatar project">&#xe6f9;</i>
@ -141,7 +139,12 @@
<div class="msg-dialog-bg-icon"><Icon type="ios-chatbubbles" /></div>
<div class="msg-dialog-bg-text">{{$L('选择一个会话开始聊天')}}</div>
</div>
<DialogWrapper v-if="windowLarge && dialogId > 0" :dialogId="dialogId" @on-active="scrollIntoActive" :auto-focus="$A.isDesktop()"/>
<DialogWrapper
v-if="windowLarge && dialogId > 0"
:dialogId="dialogId"
:searchMsgId="dialogSearchMsgId"
@on-active="scrollIntoActive"
:auto-focus="$A.isDesktop()"/>
</div>
</div>
</div>
@ -161,14 +164,17 @@ export default {
return {
tabActive: 'dialog',
dialogLoad: 0,
dialogKey: '',
dialogSearch: [],
dialogActive: '',
dialogType: [
{type: '', name: '全部'},
{type: 'project', name: '项目'},
{type: 'task', name: '任务'},
{type: 'user', name: '个人'},
],
dialogActive: '',
dialogKey: '',
contactsKey: '',
contactsLoad: 0,
@ -199,14 +205,14 @@ export default {
},
computed: {
...mapState(['cacheDialogs', 'loadDialogs', 'dialogId']),
...mapState(['cacheDialogs', 'loadDialogs', 'dialogId', 'dialogSearchMsgId']),
routeName() {
return this.$route.name
},
dialogList() {
const {dialogActive, dialogKey} = this;
const {dialogActive, dialogKey, dialogSearch} = this;
if (dialogActive == '' && dialogKey == '') {
return this.cacheDialogs.filter(dialog => this.filterDialog(dialog)).sort((a, b) => {
if (a.top_at || b.top_at) {
@ -215,7 +221,7 @@ export default {
return $A.Date(b.last_at) - $A.Date(a.last_at);
});
}
return this.cacheDialogs.filter(dialog => {
const list = this.cacheDialogs.filter(dialog => {
if (!this.filterDialog(dialog)) {
return false;
}
@ -243,7 +249,11 @@ export default {
}
}
return true;
}).sort((a, b) => {
})
if (dialogSearch.length > 0) {
list.push(...dialogSearch.map(item => Object.assign(item, {is_search: true})))
}
return list.sort((a, b) => {
if (a.top_at || b.top_at) {
return $A.Date(b.top_at) - $A.Date(a.top_at);
}
@ -263,7 +273,7 @@ export default {
},
contactsList() {
let list = [];
const list = [];
this.contactsFilter.some(user => {
let az = user.az ? user.az.toUpperCase() : "#";
let item = list.find(item => item.az == az);
@ -342,6 +352,18 @@ export default {
$A.reloadUrl();
break;
}
//
this.dialogSearch = [];
if (val == '') {
return;
}
this.dialogLoad++;
setTimeout(() => {
if (this.dialogKey == val) {
this.searchDialog(val);
}
this.dialogLoad--;
}, 600);
},
contactsKey(val) {
@ -412,11 +434,27 @@ export default {
this.dialogActive = type
},
openDialog(dialogId) {
dialogClass(dialog) {
if (this.dialogKey) {
return null
}
return {
top: dialog.top_at,
active: dialog.id == this.dialogId,
operate: this.operateVisible && dialog.id == this.operateItem.id,
completed: $A.dialogCompleted(dialog)
}
},
openDialog(dialogId, searchMsgId = null) {
if (this.operateVisible) {
return
}
this.$store.dispatch("openDialog", dialogId)
this.dialogKey = "";
this.$store.dispatch("openDialog", {
dialog_id: dialogId,
search_msg_id: searchMsgId
})
},
openContacts(user) {
@ -470,6 +508,18 @@ export default {
return true;
},
searchDialog(key) {
this.dialogLoad++;
this.$store.dispatch("call", {
url: 'dialog/search',
data: {key},
}).then(({data}) => {
this.dialogSearch = data;
}).finally(_ => {
this.dialogLoad--;
});
},
getContactsList(page) {
this.contactsLoad++;
this.$store.dispatch("call", {

View File

@ -2026,6 +2026,12 @@ export default {
* @param dialog_id
*/
openDialog({state, dispatch}, dialog_id) {
let search_msg_id;
if ($A.isJson(dialog_id)) {
search_msg_id = dialog_id.search_msg_id;
dialog_id = dialog_id.dialog_id;
}
state.dialogSearchMsgId = /\d+/.test(search_msg_id) ? search_msg_id : 0;
state.dialogId = /\d+/.test(dialog_id) ? dialog_id : 0;
},

View File

@ -71,6 +71,7 @@ const stateData = {
// 会话聊天
dialogId: 0,
dialogSearchMsgId: 0,
dialogIns: [],
dialogMsgs: [],
dialogInputCache: $A.getStorageArray("cacheDialogInput"),