diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index bbf0da1a4..e150159d4 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -550,4 +550,156 @@ class DialogController extends AbstractController 'mark_unread' => $dialogUser->mark_unread, ]); } + + /** + * @api {get} api/dialog/group/add 15. 新增群聊 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__add + * + * @apiParam {String} chat_name 群名 + * @apiParam {Array} userids 群成员,格式: [userid1, userid2, userid3] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__add() + { + $user = User::auth(); + // + $chatName = trim(Request::input('chat_name')); + $userids = Request::input('userids'); + // + if (!is_array($userids)) { + return Base::retError('请选择群成员'); + } + $userids = array_merge([$user->userid], $userids); + $userids = array_values(array_filter(array_unique($userids))); + if (count($userids) < 2) { + return Base::retError('群成员至少2人'); + } + // + if (empty($chatName)) { + $array = []; + foreach ($userids as $userid) { + $array[] = User::userid2nickname($userid); + if (count($array) >= 8 || strlen(implode(", ", $array)) > 200) { + $array[] = "..."; + break; + } + } + $chatName = implode(", ", $array); + } + $dialog = WebSocketDialog::createGroup($chatName, $userids, 'user', $user->userid); + if (empty($dialog)) { + return Base::retError('创建群聊失败'); + } + return Base::retSuccess('创建成功', $dialog); + } + + /** + * @api {get} api/dialog/group/user 16. 获取群成员 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__user + * + * @apiParam {Number} dialog_id 会话ID + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__user() + { + User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + // + $dialog = WebSocketDialog::checkDialog($dialog_id); + // + return Base::retSuccess('success', $dialog->dialogUser); + } + + /** + * @api {get} api/dialog/group/adduser 16. 添加群成员 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__adduser + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {Array} userids 新增的群成员,格式: [userid1, userid2, userid3] + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__adduser() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $userids = Request::input('userids'); + // + if (!is_array($userids)) { + return Base::retError('请选择群成员'); + } + // + $dialog = WebSocketDialog::checkDialog($dialog_id); + if ($dialog->owner_id != $user->userid) { + return Base::retError('仅限群主操作'); + } + // + $dialog->joinGroup($userids); + return Base::retSuccess('添加成功'); + } + + /** + * @api {get} api/dialog/group/deluser 16. 移出(退出)群成员 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName group__adduser + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {Array} userids 移出的群成员,格式: [userid1, userid2, userid3] + * - 留空表示自己退出 + * - 有值表示移出,仅限群主操作 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function group__deluser() + { + $user = User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $userids = Request::input('userids'); + // + $type = 'remove'; + if (empty($userids)) { + $type = 'exit'; + $userids = [$user->userid]; + } + // + if (!is_array($userids)) { + return Base::retError('请选择群成员'); + } + // + $dialog = WebSocketDialog::checkDialog($dialog_id); + if ($type === 'remove' && $dialog->owner_id != $user->userid) { + return Base::retError('仅限群主操作'); + } + // + $dialog->exitGroup($userids); + return Base::retSuccess($type === 'remove' ? '移出成功' : '退出成功'); + } } diff --git a/app/Models/WebSocketDialog.php b/app/Models/WebSocketDialog.php index cf8d7bc80..522a38f41 100644 --- a/app/Models/WebSocketDialog.php +++ b/app/Models/WebSocketDialog.php @@ -14,6 +14,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property string|null $group_type 聊天室类型 * @property string|null $name 对话名称 * @property string|null $last_at 最后消息时间 + * @property int|null $owner_id 群主用户ID * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at @@ -29,6 +30,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLastAt($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value) * @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value) * @method static \Illuminate\Database\Query\Builder|WebSocketDialog withTrashed() @@ -74,6 +76,55 @@ class WebSocketDialog extends AbstractModel return true; } + /** + * 加入聊天室 + * @param int|array $userid 加入的会员ID或会员ID组 + * @return bool + */ + public function joinGroup($userid) + { + if ($this->type !== 'group') { + return false; + } + AbstractModel::transaction(function () use ($userid) { + foreach (is_array($userid) ? $userid : [$userid] as $value) { + if ($value > 0) { + WebSocketDialogUser::updateInsert([ + 'dialog_id' => $this->id, + 'userid' => $value, + ]); + } + } + }); + return true; + } + + /** + * 退出聊天室 + * @param int|array $userid 加入的会员ID或会员ID组 + * @return bool + */ + public function exitGroup($userid) + { + $builder = WebSocketDialogUser::whereDialogId($this->id); + if (is_array($userid)) { + $builder->whereIn('userid', $userid); + } else { + $builder->whereUserid($userid); + } + $builder->chunkById(100, function($list) { + /** @var WebSocketDialogUser $item */ + foreach ($list as $item) { + if ($item->userid == $this->owner_id) { + // 群主不可退出 + continue; + } + $item->delete(); + } + }); + return true; + } + /** * 获取对话(同时检验对话身份) * @param $dialog_id @@ -148,17 +199,20 @@ class WebSocketDialog extends AbstractModel /** * 创建聊天室 * @param string $name 聊天室名称 - * @param int|array $userid 加入的会员ID或会员ID组 + * @param int|array $userid 加入的会员ID(组) * @param string $group_type 聊天室类型 + * @param int $owner_id 群主会员ID * @return self|null */ - public static function createGroup($name, $userid, $group_type = '') + public static function createGroup($name, $userid, $group_type = '', $owner_id = 0) { - return AbstractModel::transaction(function () use ($userid, $group_type, $name) { + return AbstractModel::transaction(function () use ($owner_id, $userid, $group_type, $name) { $dialog = self::createInstance([ 'type' => 'group', 'name' => $name ?: '', 'group_type' => $group_type, + 'owner_id' => $owner_id, + 'last_at' => $group_type === 'user' ? Carbon::now() : null, ]); $dialog->save(); foreach (is_array($userid) ? $userid : [$userid] as $value) { @@ -173,47 +227,6 @@ class WebSocketDialog extends AbstractModel }); } - /** - * 加入聊天室 - * @param int $dialog_id 会话ID(即 聊天室ID) - * @param int|array $userid 加入的会员ID或会员ID组 - * @return bool - */ - public static function joinGroup($dialog_id, $userid) - { - $dialog = self::whereId($dialog_id)->whereType('group')->first(); - if (empty($dialog)) { - return false; - } - AbstractModel::transaction(function () use ($dialog, $userid) { - foreach (is_array($userid) ? $userid : [$userid] as $value) { - if ($value > 0) { - WebSocketDialogUser::createInstance([ - 'dialog_id' => $dialog->id, - 'userid' => $value, - ])->save(); - } - } - }); - return true; - } - - /** - * 退出聊天室 - * @param int $dialog_id 会话ID(即 聊天室ID) - * @param int|array $userid 加入的会员ID或会员ID组 - * @return bool - */ - public static function exitGroup($dialog_id, $userid) - { - if (is_array($userid)) { - WebSocketDialogUser::whereDialogId($dialog_id)->whereIn('userid', $userid)->delete(); - } else { - WebSocketDialogUser::whereDialogId($dialog_id)->whereUserid($userid)->delete(); - } - return true; - } - /** * 获取会员对话(没有自动创建) * @param int $userid 会员ID diff --git a/database/migrations/2022_04_14_103139_add_web_socket_dialogs_owner_id.php b/database/migrations/2022_04_14_103139_add_web_socket_dialogs_owner_id.php new file mode 100644 index 000000000..35c953e88 --- /dev/null +++ b/database/migrations/2022_04_14_103139_add_web_socket_dialogs_owner_id.php @@ -0,0 +1,34 @@ +bigInteger('owner_id')->nullable()->default(0)->after('last_at')->comment('群主用户ID'); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('web_socket_dialogs', function (Blueprint $table) { + $table->dropColumn("owner_id"); + }); + } +} diff --git a/resources/assets/js/components/QuickEdit.vue b/resources/assets/js/components/QuickEdit.vue index e92d1e1c4..8e1cee5e7 100644 --- a/resources/assets/js/components/QuickEdit.vue +++ b/resources/assets/js/components/QuickEdit.vue @@ -19,7 +19,7 @@ @@ -49,6 +49,10 @@ export default { type: Boolean, default: true }, + disabled: { + type: Boolean, + default: false + }, }, data() { diff --git a/resources/assets/js/components/UserAvatar.vue b/resources/assets/js/components/UserAvatar.vue index 8f13111ce..db47682e8 100755 --- a/resources/assets/js/components/UserAvatar.vue +++ b/resources/assets/js/components/UserAvatar.vue @@ -75,6 +75,11 @@ type: Number, default: 600 }, + userResult: { + type: Function, + default: () => { + } + } }, data() { return { @@ -205,6 +210,7 @@ // } this.user = info; + this.userResult(info); }, onError() { diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index c37aae0ce..7f5b81be7 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -786,7 +786,7 @@ export default { let path = 'project/' + item.id; let openMenu = this.openMenu[item.id]; return { - "active": $A.leftExists(this.routePath, '/manage/' + path), + "active": this.routePath === '/manage/' + path, "open-menu": openMenu === true, "operate": item.id == this.topOperateItem.id && this.topOperateVisible }; diff --git a/resources/assets/js/pages/manage/components/DialogGroupInfo.vue b/resources/assets/js/pages/manage/components/DialogGroupInfo.vue new file mode 100644 index 000000000..db1a216b1 --- /dev/null +++ b/resources/assets/js/pages/manage/components/DialogGroupInfo.vue @@ -0,0 +1,230 @@ + + + diff --git a/resources/assets/js/pages/manage/components/DialogWrapper.vue b/resources/assets/js/pages/manage/components/DialogWrapper.vue index 91fafc3dd..56f5a8820 100644 --- a/resources/assets/js/pages/manage/components/DialogWrapper.vue +++ b/resources/assets/js/pages/manage/components/DialogWrapper.vue @@ -34,6 +34,14 @@ + + + + + + + +
+ + + + + + +
+
+ + +
+
+ + + + + @@ -128,10 +163,13 @@ import {mapState} from "vuex"; import DialogView from "./DialogView"; import DialogUpload from "./DialogUpload"; import {Store} from "le5le-store"; +import UserInput from "../../../components/UserInput"; +import DrawerOverlay from "../../../components/DrawerOverlay"; +import DialogGroupInfo from "./DialogGroupInfo"; export default { name: "DialogWrapper", - components: {DialogUpload, DialogView, ScrollerY, DragInput}, + components: {DialogGroupInfo, DrawerOverlay, UserInput, DialogUpload, DialogView, ScrollerY, DragInput}, props: { dialogId: { type: Number, @@ -159,6 +197,12 @@ export default { pasteShow: false, pasteFile: [], pasteItem: [], + + createGroupShow: false, + createGroupData: {}, + createGroupLoad: 0, + + groupInfoShow: false, } }, @@ -500,6 +544,31 @@ export default { }) } }, + + openCreateGroup() { + this.createGroupData = { + userids: this.dialogData.dialog_user ? [this.userId, this.dialogData.dialog_user.userid] : [this.userId], + uncancelable: [this.userId] + }; + this.createGroupShow = true; + }, + + onCreateGroup() { + this.createGroupLoad++; + this.$store.dispatch("call", { + url: 'dialog/group/add', + data: this.createGroupData + }).then(({data, msg}) => { + $A.messageSuccess(msg); + this.createGroupShow = false; + this.createGroupData = {}; + this.goForward({name: 'manage-messenger', params: {dialogId: data.id}}); + }).catch(({msg}) => { + $A.modalError(msg); + }).finally(_ => { + this.createGroupLoad--; + }); + }, } } diff --git a/resources/assets/js/pages/manage/components/ProjectWorkflow.vue b/resources/assets/js/pages/manage/components/ProjectWorkflow.vue index da53bd1e8..dfa1381a8 100644 --- a/resources/assets/js/pages/manage/components/ProjectWorkflow.vue +++ b/resources/assets/js/pages/manage/components/ProjectWorkflow.vue @@ -159,14 +159,14 @@ {{$L('流转模式')}} {{$L('剔除模式')}} -
{{$L('流转到此状态时改变任务负责人为状态负责人,原本的任务负责人移至协助人员。')}}
-
{{$L('流转到此状态时改变任务负责人为状态负责人(并保留操作状态的人员),原本的任务负责人移至协助人员。')}}
-
{{$L('流转到此状态时添加状态负责人至任务负责人。')}}
+
{{$L(`流转到【${userData.name}】时改变任务负责人为状态负责人,原本的任务负责人移至协助人员。`)}}
+
{{$L(`流转到【${userData.name}】时改变任务负责人为状态负责人(并保留操作状态的人员),原本的任务负责人移至协助人员。`)}}
+
{{$L(`流转到【${userData.name}】时添加状态负责人至任务负责人。`)}}
-
{{$L('在此状态的任务状态负责人、项目管理员可以修改状态。')}}
-
{{$L('在此状态的任务任务负责人、项目管理员可以修改状态。')}}
+
{{$L(`流转到【${userData.name}】时,仅"状态负责人"和"项目管理员"可以修改状态。`)}}
+
{{$L(`流转到【${userData.name}】时,"任务负责人"和"项目管理员"可以修改状态。`)}}
diff --git a/resources/assets/sass/pages/components/_.scss b/resources/assets/sass/pages/components/_.scss index e251cb4ac..55279d23b 100755 --- a/resources/assets/sass/pages/components/_.scss +++ b/resources/assets/sass/pages/components/_.scss @@ -1,3 +1,4 @@ +@import "dialog-group-info"; @import "dialog-wrapper"; @import "file-content"; @import "project-archived"; diff --git a/resources/assets/sass/pages/components/dialog-group-info.scss b/resources/assets/sass/pages/components/dialog-group-info.scss new file mode 100644 index 000000000..d9d53e28c --- /dev/null +++ b/resources/assets/sass/pages/components/dialog-group-info.scss @@ -0,0 +1,118 @@ +.dialog-group-info { + display: flex; + flex-direction: column; + position: absolute; + top: 10px; + left: 0; + right: 0; + bottom: 0; + + .group-info-title { + color: #b7b1b1; + margin: 18px 24px 0; + } + + .group-info-value { + margin: 4px 24px 0; + line-height: 34px; + + .quick-text { + padding: 6px 0; + height: auto; + line-height: 20px; + box-sizing: content-box; + overflow: visible; + white-space: normal; + } + } + + .group-info-search { + margin: 18px 24px 0; + } + + .group-info-button { + display: flex; + align-items: center; + margin: 18px 24px 0; + cursor: pointer; + + > i { + display: flex; + align-items: center; + justify-content: center; + height: 32px; + width: 32px; + font-size: 18px; + margin-right: 8px; + border-radius: 50%; + color: #777; + border: 1px solid #ddd; + } + } + + .group-info-user { + flex: 1; + overflow: auto; + margin-top: 16px; + padding: 0 24px; + + > ul { + > li { + display: flex; + align-items: center; + list-style: none; + padding-bottom: 16px; + + &:hover { + .user-exit { + opacity: 1; + transform: translateX(0); + } + } + + &.no { + justify-content: center; + color: #999; + .common-loading { + width: 16px; + height: 16px; + } + } + + .common-avatar { + width: 0; + flex: 1; + .avatar-name { + padding-left: 8px; + } + } + + .user-exit { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + margin-left: 4px; + width: 20px; + height: 20px; + font-size: 12px; + color: #999999; + border: 1px solid #dddddd; + border-radius: 50%; + opacity: 0; + transform: translateX(50%); + transition: all 0.2s; + } + + .ivu-tag { + margin-left: 4px; + height: 20px; + line-height: 20px; + padding: 0 5px; + transform: scale(0.9); + transform-origin: right center; + } + } + } + } +} diff --git a/resources/assets/sass/pages/components/dialog-wrapper.scss b/resources/assets/sass/pages/components/dialog-wrapper.scss index cb48b05ec..0b027cac1 100644 --- a/resources/assets/sass/pages/components/dialog-wrapper.scss +++ b/resources/assets/sass/pages/components/dialog-wrapper.scss @@ -132,6 +132,7 @@ background-color: #8BCF70; color: #FFFFFF; text-align: center; + white-space: nowrap; } } @@ -151,6 +152,13 @@ } } } + + .dialog-create { + cursor: pointer; + margin-left: 24px; + font-size: 20px; + color: $primary-text-color; + } } .dialog-scroller { diff --git a/resources/assets/sass/pages/components/project-list.scss b/resources/assets/sass/pages/components/project-list.scss index 348abe986..7e19d02ff 100644 --- a/resources/assets/sass/pages/components/project-list.scss +++ b/resources/assets/sass/pages/components/project-list.scss @@ -39,6 +39,7 @@ background-color: #8BCF70; color:#FFFFFF; text-align: center; + white-space: nowrap; } } .project-icons { diff --git a/resources/assets/statics/public/css/fonts/taskfont.ttf b/resources/assets/statics/public/css/fonts/taskfont.ttf index 894a7255b..803b5da7d 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.ttf and b/resources/assets/statics/public/css/fonts/taskfont.ttf differ diff --git a/resources/assets/statics/public/css/fonts/taskfont.woff b/resources/assets/statics/public/css/fonts/taskfont.woff index dd8d3eea2..b05848e77 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.woff and b/resources/assets/statics/public/css/fonts/taskfont.woff differ diff --git a/resources/assets/statics/public/css/fonts/taskfont.woff2 b/resources/assets/statics/public/css/fonts/taskfont.woff2 index f83819f67..7ab404608 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.woff2 and b/resources/assets/statics/public/css/fonts/taskfont.woff2 differ