From 822bc3ea69ae1ae7e138212ceeeb943e141d45f9 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Wed, 22 Jun 2022 18:31:07 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=94=AF=E6=8C=81@=E7=BE=A4=E8=81=8A?= =?UTF-8?q?=E4=BB=A5=E5=A4=96=E6=88=90=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/DialogController.php | 37 +- app/Models/ProjectTask.php | 38 +- app/Models/WebSocketDialog.php | 18 +- app/Tasks/WebSocketDialogMsgTask.php | 24 +- .../manage/components/ChatInput/index.vue | 332 +++++++++++------- resources/assets/js/store/actions.js | 8 +- .../sass/pages/components/chat-input.scss | 6 + .../sass/pages/components/dialog-wrapper.scss | 9 +- 8 files changed, 287 insertions(+), 185 deletions(-) diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 814203e30..9ffe4cf0e 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -268,36 +268,6 @@ class DialogController extends AbstractController 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. 获取未读消息数量 * @@ -882,7 +852,8 @@ class DialogController extends AbstractController return Base::retError('创建群组失败'); } $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); } @@ -956,8 +927,8 @@ class DialogController extends AbstractController $dialog = WebSocketDialog::checkDialog($dialog_id, "auto"); // $dialog->checkGroup(); - $dialog->joinGroup($userids); - $dialog->pushMsg("groupJoin", $dialog->formatData($user->userid), $userids); + $dialog->joinGroup($userids, $user->userid); + $dialog->pushMsg("groupJoin", null, $userids); return Base::retSuccess('添加成功'); } diff --git a/app/Models/ProjectTask.php b/app/Models/ProjectTask.php index 372e96858..cc5ab4541 100644 --- a/app/Models/ProjectTask.php +++ b/app/Models/ProjectTask.php @@ -928,11 +928,18 @@ class ProjectTask extends AbstractModel /** * 权限版本 - * @param int $level 1-负责人,2-协助人/负责人,3-创建人/协助人/负责人 + * @param int $level + * 1:负责人 + * 2:协助人/负责人 + * 3:创建人/协助人/负责人 + * 4:任务群聊成员/3 * @return bool */ public function permission($level = 1) { + if ($level >= 4) { + return $this->permission(3) || $this->existDialogUser(); + } if ($level >= 3 && $this->isCreater()) { return true; } @@ -942,6 +949,15 @@ class ProjectTask extends AbstractModel return $this->isOwner(); } + /** + * 判断是否在任务对话里 + * @return bool + */ + public function existDialogUser() + { + return $this->dialog_id && WebSocketDialogUser::whereDialogId($this->dialog_id)->whereUserid(User::userid())->exists(); + } + /** * 判断是否创建者 * @return bool @@ -1218,7 +1234,10 @@ class ProjectTask extends AbstractModel * @param int $task_id * @param bool $archived 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 * @return self */ @@ -1245,19 +1264,20 @@ class ProjectTask extends AbstractModel try { $project = Project::userProject($task->project_id); } catch (\Throwable $e) { - if ($task->owner === null) { + if ($task->owner !== null || (!$permission && $task->permission(4))) { + $project = Project::find($task->project_id); + if (empty($project)) { + throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002); + } + } else { throw new ApiException($e->getMessage(), [ 'task_id' => $task_id ], -4002); } - $project = Project::find($task->project_id); - if (empty($project)) { - throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002); - } } // - if ($permission === 2) { + if ($permission >= 2) { $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('仅限项目负责人、任务负责人、协助人员或任务创建者操作'); } // diff --git a/app/Models/WebSocketDialog.php b/app/Models/WebSocketDialog.php index f9c4ca439..0d9bb0292 100644 --- a/app/Models/WebSocketDialog.php +++ b/app/Models/WebSocketDialog.php @@ -124,22 +124,27 @@ class WebSocketDialog extends AbstractModel /** * 加入聊天室 * @param int|array $userid 加入的会员ID或会员ID组 + * @param int $inviter 邀请人 * @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) { if ($value > 0) { WebSocketDialogUser::updateInsert([ 'dialog_id' => $this->id, 'userid' => $value, ], [ - 'inviter' => User::userid(), + 'inviter' => $inviter, ]); } } }); + $this->pushMsg("groupUpdate", [ + 'id' => $this->id, + 'people' => WebSocketDialogUser::whereDialogId($this->id)->count() + ]); 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 array $data 发送内容,默认为[id=>项目ID] + * @param array $data 发送内容,默认为[id=>会话ID] * @param array $userid 指定会员,默认为群组所有成员 * @return void */ diff --git a/app/Tasks/WebSocketDialogMsgTask.php b/app/Tasks/WebSocketDialogMsgTask.php index 1f78b28dd..9358099f8 100644 --- a/app/Tasks/WebSocketDialogMsgTask.php +++ b/app/Tasks/WebSocketDialogMsgTask.php @@ -13,7 +13,7 @@ use Request; /** - * 推送回话消息 + * 推送会话消息 * Class WebSocketDialogMsgTask * @package App\Tasks */ @@ -38,6 +38,7 @@ class WebSocketDialogMsgTask extends AbstractTask $_A = [ '__fill_url_remote_url' => true, ]; + // $msg = WebSocketDialogMsg::find($this->id); if (empty($msg)) { @@ -48,14 +49,31 @@ class WebSocketDialogMsgTask extends AbstractTask return; } + // 提及会员 + $mentions = []; + if ($msg->type === 'text') { + preg_match_all("//", $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 = []; - $userids = $dialog->dialogUser->pluck('userid')->toArray(); foreach ($userids AS $userid) { if ($userid == $msg->userid) { $array[$userid] = false; } else { - $mention = preg_match("//", $msg->type === 'text' ? $msg->msg['text'] : ''); + $mention = array_intersect([0, $userid], $mentions) ? 1 : 0; WebSocketDialogMsgRead::createInstance([ 'dialog_id' => $msg->dialog_id, 'msg_id' => $msg->id, diff --git a/resources/assets/js/pages/manage/components/ChatInput/index.vue b/resources/assets/js/pages/manage/components/ChatInput/index.vue index 12bf260ed..2f169ff4b 100755 --- a/resources/assets/js/pages/manage/components/ChatInput/index.vue +++ b/resources/assets/js/pages/manage/components/ChatInput/index.vue @@ -202,6 +202,7 @@ export default { mentionMode: '', userList: null, + userCache: null, taskList: null, showMore: false, @@ -279,7 +280,7 @@ export default { } }, computed: { - ...mapState(['dialogInputCache', 'cacheProjects', 'cacheTasks', 'cacheUserBasic', 'dialogMsgs']), + ...mapState(['dialogInputCache', 'cacheProjects', 'cacheTasks', 'cacheUserBasic', 'dialogMsgs', 'cacheDialogs']), isEnterSend() { if (typeof this.enterSend === "boolean") { @@ -350,6 +351,10 @@ export default { return `${minute}:${seconds}″${millisecond}` }, + dialogData() { + return this.dialogId > 0 ? (this.cacheDialogs.find(({id}) => id == this.dialogId) || {}) : {}; + }, + replyData() { const {replyId} = this; if (replyId > 0) { @@ -382,11 +387,13 @@ export default { // Reset lists dialogId() { this.userList = null; + this.userCache = null; this.taskList = null; this.$emit('input', this.getInputCache()) }, taskId() { this.userList = null; + this.userCache = null; this.taskList = null; this.$emit('input', this.getInputCache()) }, @@ -499,7 +506,7 @@ export default { return `
${data.value}
`; } if (data.id === 0) { - return `
@
${data.value}
${this.$L('提示所有成员')}
`; + return `
@
${data.value}
${data.tip}
`; } if (data.avatar) { return `
${data.value}
`; @@ -518,14 +525,14 @@ export default { containers[i].classList.add(mentionName); scrollPreventThrough(containers[i]); } - this.getSource(mentionChar).then(array => { - let values = []; + this.getMentionSource(mentionChar, searchTerm, array => { + const values = []; array.some(item => { let list = item.list; - if (searchTerm && !item.ignoreSearch) { + if (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) list.length > 0 && values.push(...list) } @@ -908,149 +915,210 @@ export default { return 0; }, - getSource(mentionChar) { - return new Promise(resolve => { - switch (mentionChar) { - case "@": // @成员 - this.mentionMode = "user-mention"; - if (this.userList !== null) { - resolve(this.userList) - return; - } - const atCallback = (list) => { - if (list.length > 2) { - this.userList = [{ - ignoreSearch: true, - label: null, - list: [{id: 0, value: this.$L('所有人')}] - }, { - ignoreSearch: false, + getMentionSource(mentionChar, searchTerm, resultCallback) { + switch (mentionChar) { + case "@": // @成员 + this.mentionMode = "user-mention"; + 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) { + this.userCache.push({ + label: null, + list: [{id: 0, value: this.$L('所有人'), tip: this.$L('仅提示会话内成员')}] + }) + } + this.userCache.push(...[{ label: [{id: 0, value: this.$L('会话内成员'), disabled: true}], list, - }] + }, { + label: [{id: 0, value: this.$L('会话以外成员'), disabled: true}], + list: moreUser, + }]) } else { - this.userList = [{ - ignoreSearch: false, - label: null, - list - }] + if (list.length > 2) { + 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, + list + }) + } } - resolve(this.userList) - } - let array = []; - if (this.dialogId > 0) { - // 根据会话ID获取成员 - this.$store.dispatch("call", { - url: 'dialog/user', - data: { - dialog_id: this.dialogId, - getuser: 1 - } - }).then(({data}) => { - if (data.length > 0) { - array.push(...data.map(item => { - return { - id: item.userid, - value: item.nickname, - avatar: item.userimg, - online: item.online, - } - })) - } - atCallback(array) - }).catch(_ => { - atCallback(array) - }); - } else if (this.taskId > 0) { - // 根据任务ID获取成员 - const task = this.cacheTasks.find(({id}) => id == this.taskId) - if (task && $A.isArray(task.task_user)) { - task.task_user.some(tmp => { - let item = this.cacheUserBasic.find(({userid}) => userid == tmp.userid); - if (item) { - array.push({ - id: item.userid, - value: item.nickname, - avatar: item.userimg, - online: item.online, - }) - } + 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) { + // 根据会话ID获取成员 + this.$store.dispatch("call", { + url: 'dialog/user', + data: { + dialog_id: this.dialogId, + getuser: 1 + } + }).then(({data}) => { + if (this.cacheDialogs.find(({id}) => id == this.dialogId)) { + this.$store.dispatch("saveDialog", { + id: this.dialogId, + people: data.length }) } + if (data.length > 0) { + array.push(...data.map(item => { + return { + id: item.userid, + value: item.nickname, + avatar: item.userimg, + online: item.online, + } + })) + } atCallback(array) + }).catch(_ => { + atCallback(array) + }); + } else if (this.taskId > 0) { + // 根据任务ID获取成员 + const task = this.cacheTasks.find(({id}) => id == this.taskId) + if (task && $A.isArray(task.task_user)) { + task.task_user.some(tmp => { + const item = this.cacheUserBasic.find(({userid}) => userid == tmp.userid); + if (item) { + array.push({ + id: item.userid, + value: item.nickname, + avatar: item.userimg, + online: item.online, + }) + } + }) } - break; + atCallback(array) + } + break; - case "#": // #任务 - this.mentionMode = "task-mention"; - if (this.taskList !== null) { - resolve(this.taskList) - return; + case "#": // #任务 + this.mentionMode = "task-mention"; + if (this.taskList !== null) { + resultCallback(this.taskList) + return; + } + const taskCallback = (list) => { + this.taskList = []; + // 项目任务 + if (list.length > 0) { + list = list.map(item => { + return { + id: item.id, + value: item.name + } + }) + this.taskList.push({ + label: [{id: 0, value: this.$L('项目未完成任务'), disabled: true}], + list, + }) } - const taskCallback = (list) => { - this.taskList = []; - // 项目任务 - if (list.length > 0) { - list = list.map(item => { + // 待完成任务 + let data = this.$store.getters.transforTasks(this.$store.getters.dashboardTask['all']); + if (data.length > 0) { + data = data.sort((a, b) => { + 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({ + label: [{id: 0, value: this.$L('我的待完成任务'), disabled: true}], + list: data.map(item => { return { id: item.id, value: item.name } - }) - this.taskList.push({ - ignoreSearch: false, - label: [{id: 0, value: this.$L('项目未完成任务'), disabled: true}], - list, - }) - } - // 待完成任务 - let data = this.$store.getters.transforTasks(this.$store.getters.dashboardTask['all']); - if (data.length > 0) { - data = data.sort((a, b) => { - 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({ - ignoreSearch: false, - label: [{id: 0, value: this.$L('我的待完成任务'), disabled: true}], - list: data.map(item => { - return { - id: item.id, - value: item.name - } - }), - }) - } - resolve(this.taskList) - } - // - const projectId = this.getProjectId(); - if (projectId > 0) { - this.$store.dispatch("getTaskForProject", projectId).then(_ => { - let tasks = this.cacheTasks.filter(task => { - if (task.archived_at) { - return false; - } - return task.project_id == projectId - && task.parent_id === 0 - && !task.archived_at - && !task.complete_at - }) - if (tasks.length > 0) { - taskCallback(tasks); - } else { - taskCallback([]) - } - }).catch(_ => { - taskCallback([]) + }), }) - return; } - taskCallback([]) - break; + resultCallback(this.taskList) + } + // + const projectId = this.getProjectId(); + if (projectId > 0) { + this.$store.dispatch("getTaskForProject", projectId).then(_ => { + const tasks = this.cacheTasks.filter(task => { + if (task.archived_at) { + return false; + } + return task.project_id == projectId + && task.parent_id === 0 + && !task.archived_at + && !task.complete_at + }) + if (tasks.length > 0) { + taskCallback(tasks); + } else { + taskCallback([]) + } + }).catch(_ => { + taskCallback([]) + }) + return; + } + taskCallback([]) + break; - default: - resolve([]) - break; + default: + resultCallback([]) + 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([]) } }) }, diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index 53434b0b2..ec6dc7756 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -2510,7 +2510,13 @@ export default { case 'groupAdd': case 'groupJoin': // 群组添加、加入 - dispatch("saveDialog", data) + dispatch("getDialogOne", data.id).catch(() => {}) + break; + case 'groupUpdate': + // 群组更新 + if (state.cacheDialogs.find(({id}) => id == data.id)) { + dispatch("saveDialog", data) + } break; case 'groupExit': case 'groupDelete': diff --git a/resources/assets/sass/pages/components/chat-input.scss b/resources/assets/sass/pages/components/chat-input.scss index d574dac44..528142c09 100755 --- a/resources/assets/sass/pages/components/chat-input.scss +++ b/resources/assets/sass/pages/components/chat-input.scss @@ -557,6 +557,7 @@ } .mention-item-at { + flex-shrink: 0; width: 28px; height: 28px; line-height: 28px; @@ -568,6 +569,7 @@ } .mention-item-img { + flex-shrink: 0; position: relative; display: flex; align-items: center; @@ -601,6 +603,7 @@ } .mention-item-name { + flex-shrink: 0; padding: 0 8px; font-size: 14px; overflow: hidden; @@ -609,6 +612,8 @@ } .mention-item-tip { + flex: 1; + text-align: right; color: #8f8f8e; font-size: 12px; font-style: normal; @@ -618,6 +623,7 @@ } .mention-item-disabled { + flex-shrink: 0; color: #aaa; font-size: 12px; padding: 0 4px; diff --git a/resources/assets/sass/pages/components/dialog-wrapper.scss b/resources/assets/sass/pages/components/dialog-wrapper.scss index 452dbebff..838133dd6 100644 --- a/resources/assets/sass/pages/components/dialog-wrapper.scss +++ b/resources/assets/sass/pages/components/dialog-wrapper.scss @@ -44,7 +44,7 @@ pointer-events: none; position: absolute; top: 50%; - right: 22px; + right: 52px; transform: translateY(-50%); font-size: 40px; color: #19be6b; @@ -151,7 +151,9 @@ line-height: 20px; padding-top: 2px; color: #aaaaaa; - white-space: normal; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; &.pointer { cursor: pointer; @@ -1093,7 +1095,8 @@ &.completed { &:after { - right: 14px; + font-size: 36px; + right: 40px; } .dialog-title { padding-right: 0;