perf: 支持@群聊以外成员

This commit is contained in:
kuaifan 2022-06-22 18:31:07 +08:00
parent 3aefe99bd9
commit 822bc3ea69
8 changed files with 287 additions and 185 deletions

View File

@ -268,36 +268,6 @@ class DialogController extends AbstractController
return Base::retSuccess('success', $data); return Base::retSuccess('success', $data);
} }
/**
* @api {get} api/dialog/msg/one 06. 获取单个消息
*
* @apiDescription 主要用于获取回复消息的详情需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__one
*
* @apiParam {Number} msg_id 消息ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__one()
{
User::auth();
//
$msg_id = intval(Request::input('msg_id'));
//
$dialogMsg = WebSocketDialogMsg::find($msg_id);
if (empty($dialogMsg)) {
return Base::retError('消息不存在或已被删除');
}
//
WebSocketDialog::checkDialog($dialogMsg->dialog_id);
//
return Base::retSuccess('success', $dialogMsg);
}
/** /**
* @api {get} api/dialog/msg/unread 07. 获取未读消息数量 * @api {get} api/dialog/msg/unread 07. 获取未读消息数量
* *
@ -882,7 +852,8 @@ class DialogController extends AbstractController
return Base::retError('创建群组失败'); return Base::retError('创建群组失败');
} }
$data = WebSocketDialog::find($dialog->id)?->formatData($user->userid); $data = WebSocketDialog::find($dialog->id)?->formatData($user->userid);
$dialog->pushMsg("groupAdd", $data, $userids); $userids = array_values(array_diff($userids, [$user->userid]));
$dialog->pushMsg("groupAdd", null, $userids);
return Base::retSuccess('创建成功', $data); return Base::retSuccess('创建成功', $data);
} }
@ -956,8 +927,8 @@ class DialogController extends AbstractController
$dialog = WebSocketDialog::checkDialog($dialog_id, "auto"); $dialog = WebSocketDialog::checkDialog($dialog_id, "auto");
// //
$dialog->checkGroup(); $dialog->checkGroup();
$dialog->joinGroup($userids); $dialog->joinGroup($userids, $user->userid);
$dialog->pushMsg("groupJoin", $dialog->formatData($user->userid), $userids); $dialog->pushMsg("groupJoin", null, $userids);
return Base::retSuccess('添加成功'); return Base::retSuccess('添加成功');
} }

View File

@ -928,11 +928,18 @@ class ProjectTask extends AbstractModel
/** /**
* 权限版本 * 权限版本
* @param int $level 1-负责人2-协助人/负责人3-创建人/协助人/负责人 * @param int $level
* 1:负责人
* 2:协助人/负责人
* 3:创建人/协助人/负责人
* 4:任务群聊成员/3
* @return bool * @return bool
*/ */
public function permission($level = 1) public function permission($level = 1)
{ {
if ($level >= 4) {
return $this->permission(3) || $this->existDialogUser();
}
if ($level >= 3 && $this->isCreater()) { if ($level >= 3 && $this->isCreater()) {
return true; return true;
} }
@ -942,6 +949,15 @@ class ProjectTask extends AbstractModel
return $this->isOwner(); return $this->isOwner();
} }
/**
* 判断是否在任务对话里
* @return bool
*/
public function existDialogUser()
{
return $this->dialog_id && WebSocketDialogUser::whereDialogId($this->dialog_id)->whereUserid(User::userid())->exists();
}
/** /**
* 判断是否创建者 * 判断是否创建者
* @return bool * @return bool
@ -1218,7 +1234,10 @@ class ProjectTask extends AbstractModel
* @param int $task_id * @param int $task_id
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制 * @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制 * @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制
* @param int|bool $permission 0|false:不限制, 1|true:限制项目负责人、任务负责人、协助人员及任务创建者, 2:已有负责人才限制true (子任务时如果是主任务负责人也可以) * @param int|bool $permission
* - 0|false 限制:项目成员、任务成员、任务群聊成员(任务成员 = 任务创建人+任务协助人+任务负责人)
* - 1|true 限制:项目负责人、任务成员
* - 2 已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param array $with * @param array $with
* @return self * @return self
*/ */
@ -1245,19 +1264,20 @@ class ProjectTask extends AbstractModel
try { try {
$project = Project::userProject($task->project_id); $project = Project::userProject($task->project_id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
if ($task->owner === null) { if ($task->owner !== null || (!$permission && $task->permission(4))) {
throw new ApiException($e->getMessage(), [ 'task_id' => $task_id ], -4002);
}
$project = Project::find($task->project_id); $project = Project::find($task->project_id);
if (empty($project)) { if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002); throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
} }
} else {
throw new ApiException($e->getMessage(), [ 'task_id' => $task_id ], -4002);
}
} }
// //
if ($permission === 2) { if ($permission >= 2) {
$permission = $task->hasOwner() ? 1 : 0; $permission = $task->hasOwner() ? 1 : 0;
} }
if (($permission === 1 || $permission === true) && !$project->owner && !$task->permission(3)) { if ($permission && !$project->owner && !$task->permission(3)) {
throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作'); throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作');
} }
// //

View File

@ -124,22 +124,27 @@ class WebSocketDialog extends AbstractModel
/** /**
* 加入聊天室 * 加入聊天室
* @param int|array $userid 加入的会员ID或会员ID组 * @param int|array $userid 加入的会员ID或会员ID组
* @param int $inviter 邀请人
* @return bool * @return bool
*/ */
public function joinGroup($userid) public function joinGroup($userid, $inviter)
{ {
AbstractModel::transaction(function () use ($userid) { AbstractModel::transaction(function () use ($inviter, $userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) { foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) { if ($value > 0) {
WebSocketDialogUser::updateInsert([ WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id, 'dialog_id' => $this->id,
'userid' => $value, 'userid' => $value,
], [ ], [
'inviter' => User::userid(), 'inviter' => $inviter,
]); ]);
} }
} }
}); });
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
return true; return true;
} }
@ -174,6 +179,11 @@ class WebSocketDialog extends AbstractModel
} }
}); });
}); });
//
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
} }
/** /**
@ -244,7 +254,7 @@ class WebSocketDialog extends AbstractModel
/** /**
* 推送消息 * 推送消息
* @param $action * @param $action
* @param array $data 发送内容,默认为[id=>项目ID] * @param array $data 发送内容,默认为[id=>会话ID]
* @param array $userid 指定会员,默认为群组所有成员 * @param array $userid 指定会员,默认为群组所有成员
* @return void * @return void
*/ */

View File

@ -13,7 +13,7 @@ use Request;
/** /**
* 推送话消息 * 推送话消息
* Class WebSocketDialogMsgTask * Class WebSocketDialogMsgTask
* @package App\Tasks * @package App\Tasks
*/ */
@ -38,6 +38,7 @@ class WebSocketDialogMsgTask extends AbstractTask
$_A = [ $_A = [
'__fill_url_remote_url' => true, '__fill_url_remote_url' => true,
]; ];
// //
$msg = WebSocketDialogMsg::find($this->id); $msg = WebSocketDialogMsg::find($this->id);
if (empty($msg)) { if (empty($msg)) {
@ -48,14 +49,31 @@ class WebSocketDialogMsgTask extends AbstractTask
return; return;
} }
// 提及会员
$mentions = [];
if ($msg->type === 'text') {
preg_match_all("/<span class=\"mention user\" data-id=\"(\d+)\">/", $msg->msg['text'], $matchs);
if ($matchs) {
$mentions = array_values(array_filter(array_unique($matchs[1])));
}
}
// 将会话以外的成员加入会话内
$userids = $dialog->dialogUser->pluck('userid')->toArray();
$diffids = array_values(array_diff($mentions, $userids));
if ($diffids) {
$dialog->joinGroup($diffids, $msg->userid);
$dialog->pushMsg("groupJoin", null, $diffids);
$userids = array_values(array_unique(array_merge($mentions, $userids)));
}
// 推送目标①:会话成员/群成员 // 推送目标①:会话成员/群成员
$array = []; $array = [];
$userids = $dialog->dialogUser->pluck('userid')->toArray();
foreach ($userids AS $userid) { foreach ($userids AS $userid) {
if ($userid == $msg->userid) { if ($userid == $msg->userid) {
$array[$userid] = false; $array[$userid] = false;
} else { } else {
$mention = preg_match("/<span class=\"mention user\" data-id=\"[0|{$userid}]\">/", $msg->type === 'text' ? $msg->msg['text'] : ''); $mention = array_intersect([0, $userid], $mentions) ? 1 : 0;
WebSocketDialogMsgRead::createInstance([ WebSocketDialogMsgRead::createInstance([
'dialog_id' => $msg->dialog_id, 'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id, 'msg_id' => $msg->id,

View File

@ -202,6 +202,7 @@ export default {
mentionMode: '', mentionMode: '',
userList: null, userList: null,
userCache: null,
taskList: null, taskList: null,
showMore: false, showMore: false,
@ -279,7 +280,7 @@ export default {
} }
}, },
computed: { computed: {
...mapState(['dialogInputCache', 'cacheProjects', 'cacheTasks', 'cacheUserBasic', 'dialogMsgs']), ...mapState(['dialogInputCache', 'cacheProjects', 'cacheTasks', 'cacheUserBasic', 'dialogMsgs', 'cacheDialogs']),
isEnterSend() { isEnterSend() {
if (typeof this.enterSend === "boolean") { if (typeof this.enterSend === "boolean") {
@ -350,6 +351,10 @@ export default {
return `${minute}:${seconds}${millisecond}` return `${minute}:${seconds}${millisecond}`
}, },
dialogData() {
return this.dialogId > 0 ? (this.cacheDialogs.find(({id}) => id == this.dialogId) || {}) : {};
},
replyData() { replyData() {
const {replyId} = this; const {replyId} = this;
if (replyId > 0) { if (replyId > 0) {
@ -382,11 +387,13 @@ export default {
// Reset lists // Reset lists
dialogId() { dialogId() {
this.userList = null; this.userList = null;
this.userCache = null;
this.taskList = null; this.taskList = null;
this.$emit('input', this.getInputCache()) this.$emit('input', this.getInputCache())
}, },
taskId() { taskId() {
this.userList = null; this.userList = null;
this.userCache = null;
this.taskList = null; this.taskList = null;
this.$emit('input', this.getInputCache()) this.$emit('input', this.getInputCache())
}, },
@ -499,7 +506,7 @@ export default {
return `<div class="mention-item-disabled">${data.value}</div>`; return `<div class="mention-item-disabled">${data.value}</div>`;
} }
if (data.id === 0) { if (data.id === 0) {
return `<div class="mention-item-at">@</div><div class="mention-item-name">${data.value}</div><div class="mention-item-tip">${this.$L('提示所有成员')}</div>`; return `<div class="mention-item-at">@</div><div class="mention-item-name">${data.value}</div><div class="mention-item-tip">${data.tip}</div>`;
} }
if (data.avatar) { if (data.avatar) {
return `<div class="mention-item-img${data.online ? ' online' : ''}"><img src="${data.avatar}"/><em></em></div><div class="mention-item-name">${data.value}</div>`; return `<div class="mention-item-img${data.online ? ' online' : ''}"><img src="${data.avatar}"/><em></em></div><div class="mention-item-name">${data.value}</div>`;
@ -518,14 +525,14 @@ export default {
containers[i].classList.add(mentionName); containers[i].classList.add(mentionName);
scrollPreventThrough(containers[i]); scrollPreventThrough(containers[i]);
} }
this.getSource(mentionChar).then(array => { this.getMentionSource(mentionChar, searchTerm, array => {
let values = []; const values = [];
array.some(item => { array.some(item => {
let list = item.list; let list = item.list;
if (searchTerm && !item.ignoreSearch) { if (searchTerm) {
list = list.filter(({value}) => $A.strExists(value, searchTerm)); list = list.filter(({value}) => $A.strExists(value, searchTerm));
} }
if (list.length > 0 || item.ignoreSearch) { if (list.length > 0) {
item.label && values.push(...item.label) item.label && values.push(...item.label)
list.length > 0 && values.push(...list) list.length > 0 && values.push(...list)
} }
@ -908,36 +915,61 @@ export default {
return 0; return 0;
}, },
getSource(mentionChar) { getMentionSource(mentionChar, searchTerm, resultCallback) {
return new Promise(resolve => {
switch (mentionChar) { switch (mentionChar) {
case "@": // @ case "@": // @
this.mentionMode = "user-mention"; this.mentionMode = "user-mention";
if (this.userList !== null) {
resolve(this.userList)
return;
}
const atCallback = (list) => { const atCallback = (list) => {
this.getMoreUser(searchTerm, list.map(item => item.id)).then(moreUser => {
this.userList = list
this.userCache = [];
if (moreUser.length > 0) {
if (list.length > 2) { if (list.length > 2) {
this.userList = [{ this.userCache.push({
ignoreSearch: true,
label: null, label: null,
list: [{id: 0, value: this.$L('所有人')}] list: [{id: 0, value: this.$L('所有人'), tip: this.$L('仅提示会话内成员')}]
}, { })
ignoreSearch: false, }
this.userCache.push(...[{
label: [{id: 0, value: this.$L('会话内成员'), disabled: true}], label: [{id: 0, value: this.$L('会话内成员'), disabled: true}],
list, list,
}] }, {
label: [{id: 0, value: this.$L('会话以外成员'), disabled: true}],
list: moreUser,
}])
} else { } else {
this.userList = [{ if (list.length > 2) {
ignoreSearch: false, this.userCache.push(...[{
label: null,
list: [{id: 0, value: this.$L('所有人'), tip: this.$L('提示所有成员')}]
}, {
label: [{id: 0, value: this.$L('会话内成员'), disabled: true}],
list,
}])
} else {
this.userCache.push({
label: null, label: null,
list list
}] })
} }
resolve(this.userList)
} }
let array = []; resultCallback(this.userCache)
})
}
//
if (this.dialogData.people && $A.arrayLength(this.userList) !== this.dialogData.people) {
this.userList = null;
this.userCache = null;
}
if (this.userCache !== null) {
resultCallback(this.userCache)
}
if (this.userList !== null) {
atCallback(this.userList)
return;
}
//
const array = [];
if (this.dialogId > 0) { if (this.dialogId > 0) {
// ID // ID
this.$store.dispatch("call", { this.$store.dispatch("call", {
@ -947,6 +979,12 @@ export default {
getuser: 1 getuser: 1
} }
}).then(({data}) => { }).then(({data}) => {
if (this.cacheDialogs.find(({id}) => id == this.dialogId)) {
this.$store.dispatch("saveDialog", {
id: this.dialogId,
people: data.length
})
}
if (data.length > 0) { if (data.length > 0) {
array.push(...data.map(item => { array.push(...data.map(item => {
return { return {
@ -966,7 +1004,7 @@ export default {
const task = this.cacheTasks.find(({id}) => id == this.taskId) const task = this.cacheTasks.find(({id}) => id == this.taskId)
if (task && $A.isArray(task.task_user)) { if (task && $A.isArray(task.task_user)) {
task.task_user.some(tmp => { task.task_user.some(tmp => {
let item = this.cacheUserBasic.find(({userid}) => userid == tmp.userid); const item = this.cacheUserBasic.find(({userid}) => userid == tmp.userid);
if (item) { if (item) {
array.push({ array.push({
id: item.userid, id: item.userid,
@ -984,7 +1022,7 @@ export default {
case "#": // # case "#": // #
this.mentionMode = "task-mention"; this.mentionMode = "task-mention";
if (this.taskList !== null) { if (this.taskList !== null) {
resolve(this.taskList) resultCallback(this.taskList)
return; return;
} }
const taskCallback = (list) => { const taskCallback = (list) => {
@ -998,7 +1036,6 @@ export default {
} }
}) })
this.taskList.push({ this.taskList.push({
ignoreSearch: false,
label: [{id: 0, value: this.$L('项目未完成任务'), disabled: true}], label: [{id: 0, value: this.$L('项目未完成任务'), disabled: true}],
list, list,
}) })
@ -1010,7 +1047,6 @@ export default {
return $A.Date(a.end_at || "2099-12-31 23:59:59") - $A.Date(b.end_at || "2099-12-31 23:59:59"); return $A.Date(a.end_at || "2099-12-31 23:59:59") - $A.Date(b.end_at || "2099-12-31 23:59:59");
}) })
this.taskList.push({ this.taskList.push({
ignoreSearch: false,
label: [{id: 0, value: this.$L('我的待完成任务'), disabled: true}], label: [{id: 0, value: this.$L('我的待完成任务'), disabled: true}],
list: data.map(item => { list: data.map(item => {
return { return {
@ -1020,13 +1056,13 @@ export default {
}), }),
}) })
} }
resolve(this.taskList) resultCallback(this.taskList)
} }
// //
const projectId = this.getProjectId(); const projectId = this.getProjectId();
if (projectId > 0) { if (projectId > 0) {
this.$store.dispatch("getTaskForProject", projectId).then(_ => { this.$store.dispatch("getTaskForProject", projectId).then(_ => {
let tasks = this.cacheTasks.filter(task => { const tasks = this.cacheTasks.filter(task => {
if (task.archived_at) { if (task.archived_at) {
return false; return false;
} }
@ -1049,9 +1085,41 @@ export default {
break; break;
default: default:
resolve([]) resultCallback([])
break; break;
} }
},
getMoreUser(key, existIds) {
return new Promise(resolve => {
if (this.dialogId > 0 || this.taskId > 0 || this.dialogData.type === 'group') {
this.__getMoreTimer && clearTimeout(this.__getMoreTimer)
this.__getMoreTimer = setTimeout(_ => {
this.$store.dispatch("call", {
url: 'users/search',
data: {
keys: {
key,
},
take: 30
},
}).then(({data}) => {
const moreUser = data.filter(item => !existIds.includes(item.userid))
resolve(moreUser.map(item => {
return {
id: item.userid,
value: item.nickname,
avatar: item.userimg,
online: !!item.online,
}
}))
}).catch(_ => {
resolve([])
});
}, this.userCache === null ? 0 : 600)
} else {
resolve([])
}
}) })
}, },

View File

@ -2510,7 +2510,13 @@ export default {
case 'groupAdd': case 'groupAdd':
case 'groupJoin': case 'groupJoin':
// 群组添加、加入 // 群组添加、加入
dispatch("getDialogOne", data.id).catch(() => {})
break;
case 'groupUpdate':
// 群组更新
if (state.cacheDialogs.find(({id}) => id == data.id)) {
dispatch("saveDialog", data) dispatch("saveDialog", data)
}
break; break;
case 'groupExit': case 'groupExit':
case 'groupDelete': case 'groupDelete':

View File

@ -557,6 +557,7 @@
} }
.mention-item-at { .mention-item-at {
flex-shrink: 0;
width: 28px; width: 28px;
height: 28px; height: 28px;
line-height: 28px; line-height: 28px;
@ -568,6 +569,7 @@
} }
.mention-item-img { .mention-item-img {
flex-shrink: 0;
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
@ -601,6 +603,7 @@
} }
.mention-item-name { .mention-item-name {
flex-shrink: 0;
padding: 0 8px; padding: 0 8px;
font-size: 14px; font-size: 14px;
overflow: hidden; overflow: hidden;
@ -609,6 +612,8 @@
} }
.mention-item-tip { .mention-item-tip {
flex: 1;
text-align: right;
color: #8f8f8e; color: #8f8f8e;
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@ -618,6 +623,7 @@
} }
.mention-item-disabled { .mention-item-disabled {
flex-shrink: 0;
color: #aaa; color: #aaa;
font-size: 12px; font-size: 12px;
padding: 0 4px; padding: 0 4px;

View File

@ -44,7 +44,7 @@
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 22px; right: 52px;
transform: translateY(-50%); transform: translateY(-50%);
font-size: 40px; font-size: 40px;
color: #19be6b; color: #19be6b;
@ -151,7 +151,9 @@
line-height: 20px; line-height: 20px;
padding-top: 2px; padding-top: 2px;
color: #aaaaaa; color: #aaaaaa;
white-space: normal; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.pointer { &.pointer {
cursor: pointer; cursor: pointer;
@ -1093,7 +1095,8 @@
&.completed { &.completed {
&:after { &:after {
right: 14px; font-size: 36px;
right: 40px;
} }
.dialog-title { .dialog-title {
padding-right: 0; padding-right: 0;