Merge branch 'dev' into pro

# Conflicts:
#	CHANGELOG.md
#	cmd
#	package.json
#	public/js/build/404.5645cb91.js
#	public/js/build/404.9598cd97.js
#	public/js/build/404.a5736629.js
#	public/js/build/AceEditor.8747edb1.js
#	public/js/build/AceEditor.af35593f.js
#	public/js/build/AceEditor.e7f5b602.js
#	public/js/build/DialogWrapper.0c7cd033.js
#	public/js/build/DialogWrapper.64072671.js
#	public/js/build/DialogWrapper.7fcb5b27.js
#	public/js/build/Drawio.2ca59c31.js
#	public/js/build/Drawio.6691a6ef.js
#	public/js/build/Drawio.e3576e4e.js
#	public/js/build/FileContent.3a899bcc.js
#	public/js/build/FileContent.c311c89c.js
#	public/js/build/FileContent.d8e600e1.js
#	public/js/build/FilePreview.87ca99d9.js
#	public/js/build/FilePreview.f8134ee5.js
#	public/js/build/FilePreview.f9f90ff4.js
#	public/js/build/IFrame.02598edc.js
#	public/js/build/IFrame.2a7489ee.js
#	public/js/build/IFrame.be9780e1.js
#	public/js/build/ImgUpload.29e2d88d.js
#	public/js/build/ImgUpload.a4eff264.js
#	public/js/build/ImgUpload.e96999cf.js
#	public/js/build/Minder.2bce6c16.js
#	public/js/build/Minder.b1d1145f.js
#	public/js/build/Minder.f5bc5aca.js
#	public/js/build/OnlyOffice.31e7af4f.js
#	public/js/build/OnlyOffice.574ad560.js
#	public/js/build/OnlyOffice.9ce921ed.js
#	public/js/build/ReportEdit.5eb3a319.js
#	public/js/build/ReportEdit.9141bb93.js
#	public/js/build/ReportEdit.e3369e09.js
#	public/js/build/SearchButton.906cea81.js
#	public/js/build/SearchButton.cf201525.js
#	public/js/build/SearchButton.d41addb6.js
#	public/js/build/TEditor.7b9a9d91.js
#	public/js/build/TEditor.971af80f.js
#	public/js/build/TEditor.cc94d929.js
#	public/js/build/TaskDetail.38815236.js
#	public/js/build/TaskDetail.d1a9952e.js
#	public/js/build/TaskDetail.dfd78b4a.js
#	public/js/build/add.0cfbdd9e.js
#	public/js/build/add.3673f91c.js
#	public/js/build/add.423bc480.js
#	public/js/build/application.005cc174.js
#	public/js/build/application.5587ac3b.js
#	public/js/build/application.5b8f123b.js
#	public/js/build/apps.4e0bf65b.js
#	public/js/build/apps.b0a3d4f5.js
#	public/js/build/apps.f77a8c4e.js
#	public/js/build/calendar.31470aa0.js
#	public/js/build/calendar.ad5d85d5.js
#	public/js/build/calendar.e08e7575.js
#	public/js/build/checkin.5d4c364e.js
#	public/js/build/checkin.ab08f01e.js
#	public/js/build/checkin.c05284a9.js
#	public/js/build/dashboard.7cced7be.js
#	public/js/build/dashboard.c82415db.js
#	public/js/build/dashboard.f6ed8299.js
#	public/js/build/dayjs.495f600d.js
#	public/js/build/dayjs.71653272.js
#	public/js/build/dayjs.cf033d87.js
#	public/js/build/delete.4072c68f.js
#	public/js/build/delete.5f06c51d.js
#	public/js/build/delete.b26aa3fd.js
#	public/js/build/device.4cff22ad.js
#	public/js/build/device.66a7e05a.js
#	public/js/build/device.a13f3ef0.js
#	public/js/build/dialog.97b951ce.js
#	public/js/build/dialog.e9f6d55f.js
#	public/js/build/dialog.eb7b795a.js
#	public/js/build/editor.18a511b5.js
#	public/js/build/editor.2cca497c.js
#	public/js/build/editor.e034df4e.js
#	public/js/build/email.0643f86b.js
#	public/js/build/email.1d00cb0c.js
#	public/js/build/email.d95a35c0.js
#	public/js/build/file.4fe82c29.js
#	public/js/build/file.684a63df.js
#	public/js/build/file.9dceb82f.js
#	public/js/build/fileMsg.0a0029c2.js
#	public/js/build/fileMsg.1f4ecb0f.js
#	public/js/build/fileMsg.f99b6f61.js
#	public/js/build/fileTask.72914205.js
#	public/js/build/fileTask.bf35fb6b.js
#	public/js/build/fileTask.f4356f14.js
#	public/js/build/index.236af26f.js
#	public/js/build/index.299c9f99.js
#	public/js/build/index.2ffa8f9e.js
#	public/js/build/index.7d6e1bbe.js
#	public/js/build/index.94a5d2da.css
#	public/js/build/index.af34aeb9.js
#	public/js/build/index.b0ae9460.js
#	public/js/build/index.b69b5f25.js
#	public/js/build/index.b71c2859.js
#	public/js/build/index.c3968cad.js
#	public/js/build/index.d1ae44be.js
#	public/js/build/index.e07db7f9.css
#	public/js/build/index.edee4b6e.css
#	public/js/build/index.ef9e1e57.js
#	public/js/build/index.fe32159a.js
#	public/js/build/jquery.0909250e.js
#	public/js/build/jquery.16b446fd.js
#	public/js/build/jquery.27f590f5.js
#	public/js/build/keyboard.3f5b3ac6.js
#	public/js/build/keyboard.5de3dd2c.js
#	public/js/build/keyboard.c3ef7d49.js
#	public/js/build/language.1fadd54c.js
#	public/js/build/language.8bb72294.js
#	public/js/build/language.f3d03ece.js
#	public/js/build/license.21482fde.js
#	public/js/build/license.60871496.js
#	public/js/build/license.add318a7.js
#	public/js/build/localforage.65ac7a2a.js
#	public/js/build/localforage.be4775a0.js
#	public/js/build/localforage.dd58f5ac.js
#	public/js/build/login.7560afa5.js
#	public/js/build/login.75b3978c.js
#	public/js/build/login.aa163163.js
#	public/js/build/meeting.a60d7e8d.js
#	public/js/build/meeting.aa5510c7.js
#	public/js/build/meeting.fdb9793b.js
#	public/js/build/password.267357fd.js
#	public/js/build/password.749ce44d.js
#	public/js/build/password.e6d81eb1.js
#	public/js/build/personal.69279937.js
#	public/js/build/personal.a27cef8e.js
#	public/js/build/personal.c613af3c.js
#	public/js/build/preload.5827bd38.js
#	public/js/build/preload.8ec61a5b.js
#	public/js/build/preload.c6189d87.js
#	public/js/build/preview.29e49902.js
#	public/js/build/preview.7329f0f4.js
#	public/js/build/preview.b452b0ee.js
#	public/js/build/preview.c64402ed.js
#	public/js/build/preview.ec796a92.js
#	public/js/build/preview.ec85a43c.js
#	public/js/build/pro.2128a514.js
#	public/js/build/pro.213d8da6.js
#	public/js/build/pro.9fb60d27.js
#	public/js/build/projectInvite.0b3bf524.js
#	public/js/build/projectInvite.393920f8.js
#	public/js/build/projectInvite.e9cee390.js
#	public/js/build/reportDetail.2db50632.js
#	public/js/build/reportDetail.90aaf973.js
#	public/js/build/reportDetail.d93cc650.js
#	public/js/build/reportEdit.84a81076.js
#	public/js/build/reportEdit.8baf23d4.js
#	public/js/build/reportEdit.d008dd34.js
#	public/js/build/swipe.0c72cce1.js
#	public/js/build/swipe.4567bb5d.js
#	public/js/build/swipe.92aebd0c.js
#	public/js/build/system.67c1b700.js
#	public/js/build/system.c45c70de.js
#	public/js/build/system.f3384133.js
#	public/js/build/task.1b9e0e77.js
#	public/js/build/task.a445c89e.js
#	public/js/build/task.d43091db.js
#	public/js/build/taskContent.20b80714.js
#	public/js/build/taskContent.3ebbd2f9.js
#	public/js/build/taskContent.9dc7a121.js
#	public/js/build/theme.72d103d1.js
#	public/js/build/theme.7f1b2ffd.js
#	public/js/build/theme.df79fe8f.js
#	public/js/build/token.0ecffef5.js
#	public/js/build/token.a7f5ccf5.js
#	public/js/build/token.ece75257.js
#	public/js/build/validEmail.1462dd30.js
#	public/js/build/validEmail.17a3e0d2.js
#	public/js/build/validEmail.ee19c1f3.js
#	public/js/build/version.137935c7.js
#	public/js/build/version.1441c1fd.js
#	public/js/build/version.b0154505.js
#	public/js/build/video.03b62c93.js
#	public/js/build/video.2dc7f3c6.js
#	public/js/build/video.531c68e2.js
#	public/js/build/view.18713f1b.js
#	public/js/build/view.7770155e.js
#	public/js/build/view.8c6a0cc1.js
#	public/manifest.json
This commit is contained in:
kuaifan 2025-11-05 16:55:17 +08:00
commit 5370bee369
22 changed files with 1752 additions and 72 deletions

View File

@ -12,6 +12,7 @@ use App\Module\AI;
use App\Module\Doo;
use App\Models\File;
use App\Models\User;
use App\Models\UserBot;
use App\Module\Base;
use App\Module\Timer;
use App\Models\Setting;
@ -443,6 +444,29 @@ class DialogController extends AbstractController
return Base::retError('打开会话失败');
}
$data = WebSocketDialog::synthesizeData($dialog->id, $user->userid);
if ($userid > 0) {
$botTarget = User::whereUserid($userid)->whereBot(1)->first();
if ($botTarget) {
$userBot = UserBot::whereBotId($botTarget->userid)->first();
if ($userBot) {
$userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_DIALOG_OPEN, [
'dialog_id' => $dialog->id,
'dialog_type' => $dialog->type,
'session_id' => $dialog->session_id,
'dialog_name' => $dialog->getGroupName(),
'user' => [
'userid' => $user->userid,
'email' => $user->email,
'nickname' => $user->nickname,
],
], 10, [
'dialog' => $dialog->id,
'operator' => $user->userid,
]);
}
}
}
return Base::retSuccess('success', $data);
}
@ -1256,6 +1280,9 @@ class DialogController extends AbstractController
if ($model_name) {
$msgData['model_name'] = $model_name;
}
if (User::isBot($user->userid)) {
$msgData['force_webhook'] = true; // 强制使用webhook发送
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key);
}
}

View File

@ -34,6 +34,8 @@ use App\Models\WebSocketDialogUser;
use App\Models\UserTaskBrowse;
use App\Models\UserFavorite;
use App\Models\UserRecentItem;
use App\Models\UserTag;
use App\Models\UserTagRecognition;
use Illuminate\Support\Facades\DB;
use App\Models\UserEmailVerification;
use App\Module\AgoraIO\AgoraTokenGenerator;
@ -386,6 +388,9 @@ class UsersController extends AbstractController
$data['nickname_original'] = $user->getRawOriginal('nickname');
$data['department_name'] = $user->getDepartmentName();
$data['department_owner'] = UserDepartment::where('parent_id',0)->where('owner_userid', $user->userid)->exists(); // 适用默认部门下第1级负责人才能添加部门OKR
$tagMeta = UserTag::listWithMeta($user->userid, $user);
$data['personal_tags'] = $tagMeta['top'];
$data['personal_tags_total'] = $tagMeta['total'];
return Base::retSuccess('success', $data);
}
@ -446,6 +451,9 @@ class UsersController extends AbstractController
* @apiParam {String} [tel] 电话
* @apiParam {String} [nickname] 昵称
* @apiParam {String} [profession] 职位/职称
* @apiParam {String} [birthday] 生日格式YYYY-MM-DD
* @apiParam {String} [address] 地址
* @apiParam {String} [introduction] 个人简介
* @apiParam {String} [lang] 语言比如zh/en
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@ -509,6 +517,40 @@ class UsersController extends AbstractController
$upLdap['employeeType'] = $profession;
}
}
// 生日
if (Arr::exists($data, 'birthday')) {
$birthday = trim((string) Request::input('birthday'));
if ($birthday === '') {
$user->birthday = null;
} else {
try {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthday)) {
$birthdayDate = Carbon::createFromFormat('Y-m-d', $birthday);
} else {
$birthdayDate = Carbon::parse($birthday);
}
} catch (\Exception $e) {
return Base::retError('生日格式错误');
}
$user->birthday = $birthdayDate->format('Y-m-d');
}
}
// 地址
if (Arr::exists($data, 'address')) {
$address = trim((string) Request::input('address'));
if (mb_strlen($address) > 100) {
return Base::retError('地址最多只能设置100个字');
}
$user->address = $address ?: null;
}
// 个人简介
if (Arr::exists($data, 'introduction')) {
$introduction = trim((string) Request::input('introduction'));
if (mb_strlen($introduction) > 500) {
return Base::retError('个人简介最多只能设置500个字');
}
$user->introduction = $introduction ?: null;
}
// 语言
if (Arr::exists($data, 'lang')) {
$lang = trim(Request::input('lang'));
@ -767,8 +809,12 @@ class UsersController extends AbstractController
public function basic()
{
$sharekey = Request::header('sharekey');
if (empty($sharekey) || !Meeting::getShareInfo($sharekey)) {
User::auth();
$shareInfo = $sharekey ? Meeting::getShareInfo($sharekey) : null;
$viewer = null;
if (empty($shareInfo)) {
$viewer = User::auth();
} elseif (Doo::userId() > 0) {
$viewer = User::whereUserid(Doo::userId())->first();
}
//
$userid = Request::input('userid');
@ -786,6 +832,9 @@ class UsersController extends AbstractController
$basic = UserDelete::userid2basic($id);
}
if ($basic) {
$tagMeta = UserTag::listWithMeta($basic->userid, $viewer);
$basic->personal_tags = $tagMeta['top'];
$basic->personal_tags_total = $tagMeta['total'];
//
$retArray[] = $basic;
}
@ -1450,6 +1499,233 @@ class UsersController extends AbstractController
return Base::retSuccess('success', $data);
}
protected function buildUserTagResponse(?User $viewer, int $targetUserId, string $message = 'success')
{
return Base::retSuccess($message, UserTag::listWithMeta($targetUserId, $viewer));
}
/**
* @api {get} api/users/tags/lists 10.1. 获取个性标签列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName tags__lists
*
* @apiParam {Number} [userid] 会员ID不传默认为当前用户
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
* @apiSuccessExample {json} data:
{
"list": [
{
"id": 1,
"name": "认真负责",
"recognition_total": 3,
"recognized": true,
"can_edit": true,
"can_delete": true
}
],
"top": [ ],
"total": 1
}
*/
public function tags__lists()
{
$viewer = User::auth();
$userid = intval(Request::input('userid')) ?: $viewer->userid;
$target = User::whereUserid($userid)->first();
if (empty($target)) {
return Base::retError('会员不存在');
}
return $this->buildUserTagResponse($viewer, $target->userid);
}
/**
* @api {post} api/users/tags/add 10.2. 新增个性标签
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName tags__add
*
* @apiParam {Number} [userid] 会员ID不传默认为当前用户
* @apiParam {String} name 标签名称1-20个字符)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据,同“获取个性标签列表”
*/
public function tags__add()
{
$viewer = User::auth();
$userid = intval(Request::input('userid')) ?: $viewer->userid;
$target = User::whereUserid($userid)->first();
if (empty($target)) {
return Base::retError('会员不存在');
}
$name = trim((string) Request::input('name'));
if ($name === '') {
return Base::retError('请输入个性标签');
}
if (mb_strlen($name) > 20) {
return Base::retError('标签名称最多只能设置20个字');
}
if (UserTag::where('user_id', $userid)->where('name', $name)->exists()) {
return Base::retError('标签已存在');
}
if (UserTag::where('user_id', $userid)->count() >= 100) {
return Base::retError('每位会员最多添加100个标签');
}
$tag = UserTag::create([
'user_id' => $userid,
'name' => $name,
'created_by' => $viewer->userid,
'updated_by' => $viewer->userid,
]);
$tag->save();
return $this->buildUserTagResponse($viewer, $userid, '添加成功');
}
/**
* @api {post} api/users/tags/update 10.3. 修改个性标签
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName tags__update
*
* @apiParam {Number} tag_id 标签ID
* @apiParam {String} name 标签名称1-20个字符)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据,同“获取个性标签列表”
*/
public function tags__update()
{
$viewer = User::auth();
$tagId = intval(Request::input('tag_id'));
$name = trim((string) Request::input('name'));
if ($tagId <= 0) {
return Base::retError('参数错误');
}
if ($name === '') {
return Base::retError('请输入个性标签');
}
if (mb_strlen($name) > 20) {
return Base::retError('标签名称最多只能设置20个字');
}
$tag = UserTag::find($tagId);
if (empty($tag)) {
return Base::retError('标签不存在');
}
if (!$tag->canManage($viewer)) {
return Base::retError('无权操作该标签');
}
if ($name !== $tag->name && UserTag::where('user_id', $tag->user_id)->where('name', $name)->where('id', '!=', $tag->id)->exists()) {
return Base::retError('标签已存在');
}
if ($name !== $tag->name) {
$tag->updateInstance([
'name' => $name,
'updated_by' => $viewer->userid,
]);
} else {
$tag->updateInstance([
'updated_by' => $viewer->userid,
]);
}
$tag->save();
return $this->buildUserTagResponse($viewer, $tag->user_id, '保存成功');
}
/**
* @api {post} api/users/tags/delete 10.4. 删除个性标签
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName tags__delete
*
* @apiParam {Number} tag_id 标签ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据,同“获取个性标签列表”
*/
public function tags__delete()
{
$viewer = User::auth();
$tagId = intval(Request::input('tag_id'));
if ($tagId <= 0) {
return Base::retError('参数错误');
}
$tag = UserTag::find($tagId);
if (empty($tag)) {
return Base::retError('标签不存在');
}
if (!$tag->canManage($viewer)) {
return Base::retError('无权操作该标签');
}
$userId = $tag->user_id;
$tag->delete();
return $this->buildUserTagResponse($viewer, $userId, '删除成功');
}
/**
* @api {post} api/users/tags/recognize 10.5. 认可个性标签
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName tags__recognize
*
* @apiParam {Number} tag_id 标签ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据,同“获取个性标签列表”
*/
public function tags__recognize()
{
$viewer = User::auth();
$tagId = intval(Request::input('tag_id'));
if ($tagId <= 0) {
return Base::retError('参数错误');
}
$tag = UserTag::find($tagId);
if (empty($tag)) {
return Base::retError('标签不存在');
}
$recognition = UserTagRecognition::where('tag_id', $tagId)
->where('user_id', $viewer->userid)
->first();
if ($recognition) {
$recognition->delete();
$message = '已取消认可';
} else {
UserTagRecognition::create([
'tag_id' => $tagId,
'user_id' => $viewer->userid,
]);
$message = '认可成功';
}
return $this->buildUserTagResponse($viewer, $tag->user_id, $message);
}
/**
* @api {get} api/users/meeting/link 【会议】获取分享链接
*
@ -2181,7 +2457,8 @@ class UsersController extends AbstractController
'users.nickname',
'users.userimg',
'user_bots.clear_day',
'user_bots.webhook_url'
'user_bots.webhook_url',
'user_bots.webhook_events'
])
->orderByDesc('id')
->get()
@ -2191,6 +2468,7 @@ class UsersController extends AbstractController
$bot['name'] = $bot['nickname'];
$bot['avatar'] = $bot['userimg'];
$bot['system_name'] = UserBot::systemBotName($bot['name']);
$bot['webhook_events'] = UserBot::normalizeWebhookEvents($bot['webhook_events'] ?? null, empty($bot['webhook_events']));
unset($bot['userid'], $bot['nickname'], $bot['userimg']);
}
@ -2242,11 +2520,13 @@ class UsersController extends AbstractController
'avatar' => $botUser->userimg,
'clear_day' => 0,
'webhook_url' => '',
'webhook_events' => [UserBot::WEBHOOK_EVENT_MESSAGE],
'system_name' => UserBot::systemBotName($botUser->email),
];
if ($userBot) {
$data['clear_day'] = $userBot->clear_day;
$data['webhook_url'] = $userBot->webhook_url;
$data['webhook_events'] = $userBot->webhook_events;
}
return Base::retSuccess('success', $data);
}
@ -2327,6 +2607,9 @@ class UsersController extends AbstractController
if (Arr::exists($data, 'webhook_url')) {
$upBot['webhook_url'] = trim($data['webhook_url']);
}
if (Arr::exists($data, 'webhook_events')) {
$upBot['webhook_events'] = UserBot::normalizeWebhookEvents($data['webhook_events'], false);
}
//
if ($upUser) {
$botUser->updateInstance($upUser);
@ -2343,11 +2626,13 @@ class UsersController extends AbstractController
'avatar' => $botUser->userimg,
'clear_day' => 0,
'webhook_url' => '',
'webhook_events' => [UserBot::WEBHOOK_EVENT_MESSAGE],
'system_name' => UserBot::systemBotName($botUser->email),
];
if ($userBot) {
$data['clear_day'] = $userBot->clear_day;
$data['webhook_url'] = $userBot->webhook_url;
$data['webhook_events'] = $userBot->webhook_events;
}
return Base::retSuccess($botId ? '修改成功' : '添加成功', $data);
}

View File

@ -22,6 +22,9 @@ use Carbon\Carbon;
* @property string|null $tel 联系电话
* @property string $nickname 昵称
* @property string|null $profession 职位/职称
* @property \Illuminate\Support\Carbon|null $birthday 生日
* @property string|null $address 地址
* @property string|null $introduction 个人简介
* @property string $userimg 头像
* @property string|null $encrypt
* @property string|null $password 登录密码
@ -89,7 +92,7 @@ class User extends AbstractModel
public static $defaultAvatarMode = 'auto';
// 基本信息的字段
public static $basicField = ['userid', 'email', 'nickname', 'profession', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at'];
public static $basicField = ['userid', 'email', 'nickname', 'profession', 'birthday', 'address', 'introduction', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at'];
/**
* 昵称

View File

@ -5,10 +5,12 @@ namespace App\Models;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Extranet;
use App\Module\Ihttp;
use App\Module\Timer;
use App\Tasks\JokeSoupTask;
use Cache;
use Carbon\Carbon;
use Throwable;
/**
* App\Models\UserBot
@ -20,6 +22,7 @@ use Carbon\Carbon;
* @property \Illuminate\Support\Carbon|null $clear_at 下一次清理时间
* @property string|null $webhook_url 消息webhook地址
* @property int|null $webhook_num 消息webhook请求次数
* @property array|null $webhook_events Webhook事件配置
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
@ -44,6 +47,131 @@ use Carbon\Carbon;
*/
class UserBot extends AbstractModel
{
public const WEBHOOK_EVENT_MESSAGE = 'message';
public const WEBHOOK_EVENT_DIALOG_OPEN = 'dialog_open';
public const WEBHOOK_EVENT_MEMBER_JOIN = 'member_join';
public const WEBHOOK_EVENT_MEMBER_LEAVE = 'member_leave';
protected $casts = [
'webhook_events' => 'array',
];
/**
* 获取可选的 webhook 事件
*
* @return string[]
*/
public static function webhookEventOptions(): array
{
return [
self::WEBHOOK_EVENT_MESSAGE,
self::WEBHOOK_EVENT_DIALOG_OPEN,
self::WEBHOOK_EVENT_MEMBER_JOIN,
self::WEBHOOK_EVENT_MEMBER_LEAVE,
];
}
/**
* 标准化 webhook 事件配置
*
* @param mixed $events
* @return array
*/
public static function normalizeWebhookEvents(mixed $events, bool $useFallback = true): array
{
if (is_string($events)) {
$events = Base::json2array($events);
}
if ($events === null) {
$events = [];
}
if (!is_array($events)) {
$events = [$events];
}
$events = array_filter(array_map('strval', $events));
$events = array_values(array_intersect($events, self::webhookEventOptions()));
return $events ?: ($useFallback ? [self::WEBHOOK_EVENT_MESSAGE] : []);
}
/**
* 获取 webhook 事件配置
*
* @param mixed $value
* @return array
*/
public function getWebhookEventsAttribute(mixed $value): array
{
if ($value === null || $value === '') {
return self::normalizeWebhookEvents(null, true);
}
return self::normalizeWebhookEvents($value, false);
}
/**
* 设置 webhook 事件配置
*
* @param mixed $value
* @return void
*/
public function setWebhookEventsAttribute(mixed $value): void
{
$useFallback = $value === null;
$this->attributes['webhook_events'] = Base::array2json(self::normalizeWebhookEvents($value, $useFallback));
}
/**
* 判断是否需要触发指定 webhook 事件
*
* @param string $event
* @return bool
*/
public function shouldDispatchWebhook(string $event): bool
{
if (!$this->webhook_url) {
return false;
}
if (!preg_match('/^https?:\/\//', $this->webhook_url)) {
return false;
}
return in_array($event, $this->webhook_events ?? [], true);
}
/**
* 发送 webhook
*
* @param string $event
* @param array $payload
* @param int $timeout
* @param array $context
* @return array|null
*/
public function dispatchWebhook(string $event, array $payload, int $timeout = 30, array $context = []): ?array
{
if (!$this->shouldDispatchWebhook($event)) {
return null;
}
$payload = array_merge([
'event' => $event,
'timestamp' => time(),
'bot_uid' => $this->bot_id,
'owner_uid' => $this->userid,
], $payload);
try {
$result = Ihttp::ihttp_post($this->webhook_url, $payload, $timeout);
$this->increment('webhook_num');
return $result;
} catch (Throwable $th) {
info(Base::array2json(array_merge($context, [
'bot_userid' => $this->bot_id,
'event' => $event,
'webhook_url' => $this->webhook_url,
'error' => $th->getMessage(),
])));
return null;
}
}
/**
* 判断是否系统机器人

84
app/Models/UserTag.php Normal file
View File

@ -0,0 +1,84 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class UserTag extends AbstractModel
{
protected $table = 'user_tags';
protected $fillable = [
'user_id',
'name',
'created_by',
'updated_by',
];
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by', 'userid')
->select(['userid', 'nickname']);
}
public function recognitions(): HasMany
{
return $this->hasMany(UserTagRecognition::class, 'tag_id');
}
public function canManage(User $viewer): bool
{
return $viewer->isAdmin()
|| $viewer->userid === $this->user_id
|| $viewer->userid === $this->created_by;
}
public static function listWithMeta(int $targetUserId, ?User $viewer): array
{
$query = static::query()
->where('user_id', $targetUserId)
->with(['creator'])
->withCount(['recognitions as recognition_total'])
->orderByDesc('recognition_total')
->orderBy('id');
$tags = $query->get();
$viewerId = $viewer?->userid ?? 0;
$viewerIsAdmin = $viewer?->isAdmin() ?? false;
$viewerIsOwner = $viewerId > 0 && $viewerId === $targetUserId;
$recognizedIds = [];
if ($viewerId > 0 && $tags->isNotEmpty()) {
$recognizedIds = UserTagRecognition::query()
->where('user_id', $viewerId)
->whereIn('tag_id', $tags->pluck('id'))
->pluck('tag_id')
->all();
}
$recognizedLookup = array_flip($recognizedIds);
$list = $tags->map(function (self $tag) use ($viewerId, $viewerIsAdmin, $viewerIsOwner, $recognizedLookup) {
$canManage = $viewerIsAdmin || $viewerIsOwner || $viewerId === $tag->created_by;
return [
'id' => $tag->id,
'user_id' => $tag->user_id,
'name' => $tag->name,
'created_by' => $tag->created_by,
'created_by_name' => $tag->creator?->nickname ?: '',
'recognition_total' => (int) $tag->recognition_total,
'recognized' => isset($recognizedLookup[$tag->id]),
'can_edit' => $canManage,
'can_delete' => $canManage,
];
})->values()->toArray();
return [
'list' => $list,
'top' => array_slice($list, 0, 10),
'total' => count($list),
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserTagRecognition extends AbstractModel
{
protected $table = 'user_tag_recognitions';
protected $fillable = [
'tag_id',
'user_id',
];
public function tag(): BelongsTo
{
return $this->belongsTo(UserTag::class, 'tag_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id', 'userid')
->select(['userid', 'nickname']);
}
}

View File

@ -461,7 +461,8 @@ class WebSocketDialog extends AbstractModel
*/
public function joinGroup($userid, $inviter, $important = null)
{
AbstractModel::transaction(function () use ($important, $inviter, $userid) {
$addedUserIds = [];
AbstractModel::transaction(function () use ($important, $inviter, $userid, &$addedUserIds) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
$updateData = [
@ -480,6 +481,7 @@ class WebSocketDialog extends AbstractModel
]);
}, $isInsert);
if ($isInsert) {
$addedUserIds[] = $value;
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => User::userid2nickname($value) . " 已加入群组"
], $inviter, true, true);
@ -490,6 +492,16 @@ class WebSocketDialog extends AbstractModel
$data = WebSocketDialog::generatePeople($this->id);
$data['id'] = $this->id;
$this->pushMsg("groupUpdate", $data);
if ($addedUserIds) {
$meta = ['action' => 'join'];
if ($inviter > 0) {
$actor = $this->getUserSnapshots([$inviter]);
if (!empty($actor)) {
$meta['actor'] = $actor[0];
}
}
$this->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_JOIN, $addedUserIds, $meta);
}
return true;
}
@ -503,14 +515,15 @@ class WebSocketDialog extends AbstractModel
public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true)
{
$typeDesc = $type === 'remove' ? '移出' : '退出';
AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid) {
$removedUserIds = [];
AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid, &$removedUserIds) {
$builder = WebSocketDialogUser::whereDialogId($this->id);
if (is_array($userid)) {
$builder->whereIn('userid', $userid);
} else {
$builder->whereUserid($userid);
}
$builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type) {
$builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type, &$removedUserIds) {
/** @var WebSocketDialogUser $item */
foreach ($list as $item) {
if ($checkDelete) {
@ -531,6 +544,7 @@ class WebSocketDialog extends AbstractModel
}
//
$item->delete();
$removedUserIds[] = $item->userid;
//
if ($pushMsg) {
if ($type === 'remove') {
@ -549,6 +563,87 @@ class WebSocketDialog extends AbstractModel
$data = WebSocketDialog::generatePeople($this->id);
$data['id'] = $this->id;
$this->pushMsg("groupUpdate", $data);
if ($removedUserIds) {
$meta = ['action' => $type];
$operatorId = User::userid();
if ($operatorId > 0) {
$actor = $this->getUserSnapshots([$operatorId]);
if (!empty($actor)) {
$meta['actor'] = $actor[0];
}
}
$this->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_LEAVE, $removedUserIds, $meta);
}
}
/**
* 获取用户快照
* @param array $userIds
* @return array
*/
protected function getUserSnapshots(array $userIds): array
{
$userIds = array_values(array_unique(array_filter($userIds)));
if (empty($userIds)) {
return [];
}
return User::whereIn('userid', $userIds)
->get(['userid', 'nickname', 'email', 'bot'])
->map(function (User $user) {
return [
'userid' => $user->userid,
'nickname' => $user->nickname,
'email' => $user->email,
'is_bot' => (bool)$user->bot,
];
})
->values()
->all();
}
/**
* 推送成员事件到机器人 webhook
* @param string $event
* @param array $memberIds
* @param array $meta
* @return void
*/
protected function dispatchMemberWebhook(string $event, array $memberIds, array $meta = []): void
{
$memberIds = array_values(array_unique(array_filter($memberIds)));
if (empty($memberIds)) {
return;
}
$botIds = $this->dialogUser()->where('bot', 1)->pluck('userid')->toArray();
if (empty($botIds)) {
return;
}
$userBots = UserBot::whereIn('bot_id', $botIds)->get();
if ($userBots->isEmpty()) {
return;
}
$members = $this->getUserSnapshots($memberIds);
if (empty($members)) {
return;
}
$payload = array_merge([
'dialog_id' => $this->id,
'dialog_type' => $this->type,
'group_type' => $this->group_type,
'dialog_name' => $this->getGroupName(),
'members' => $members,
], array_filter($meta, fn ($value) => $value !== null));
foreach ($userBots as $userBot) {
$userBot->dispatchWebhook($event, $payload, 10, [
'dialog' => $this->id,
'event_members' => $memberIds,
]);
}
}
/**

View File

@ -66,7 +66,7 @@ class BotReceiveMsgTask extends AbstractTask
}
// 判断消息是否存在
$msg = WebSocketDialogMsg::with(['user'])->find($this->msgId);
$msg = WebSocketDialogMsg::with(['user', 'webSocketDialog'])->find($this->msgId);
if (empty($msg)) {
return;
}
@ -75,7 +75,11 @@ class BotReceiveMsgTask extends AbstractTask
$msg->readSuccess($botUser->userid);
// 判断消息是否是机器人发送的则不处理,避免循环
if (!$msg->user || $msg->user->bot) {
if ((!$msg->user || $msg->user->bot)) {
$msgData = Base::json2array($msg->msg);
if (Base::val($msgData, 'force_webhook') && $msg->webSocketDialog) {
$this->handleWebhookRequest($msgData['text'], null, $msg, $msg->webSocketDialog, $botUser);
}
return;
}
@ -427,6 +431,7 @@ class BotReceiveMsgTask extends AbstractTask
private function handleWebhookRequest($sendText, $replyText, WebSocketDialogMsg $msg, WebSocketDialog $dialog, User $botUser)
{
$webhookUrl = null;
$userBot = null;
$extras = ['timestamp' => time()];
try {
@ -530,13 +535,11 @@ class BotReceiveMsgTask extends AbstractTask
return;
}
$userBot = UserBot::whereBotId($botUser->userid)->first();
if ($userBot) {
$userBot->webhook_num++;
$userBot->save();
$webhookUrl = $userBot->webhook_url;
if (!$userBot || !$userBot->shouldDispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE)) {
return;
}
}
if (!preg_match("/^https?:\/\//", $webhookUrl)) {
if (!$userBot && !preg_match("/^https?:\/\//", $webhookUrl)) {
return;
}
} catch (\Exception $e) {
@ -547,50 +550,60 @@ class BotReceiveMsgTask extends AbstractTask
return;
}
//
try {
$data = [
'text' => $sendText,
'reply_text' => $replyText,
'token' => User::generateToken($botUser),
'session_id' => $dialog->session_id,
'dialog_id' => $dialog->id,
'dialog_type' => $dialog->type,
'msg_id' => $msg->id,
'msg_uid' => $msg->userid,
'mention' => $this->mention ? 1 : 0,
'bot_uid' => $botUser->userid,
'version' => Base::getVersion(),
'extras' => Base::array2json($extras)
$data = [
'text' => $sendText,
'reply_text' => $replyText,
'token' => User::generateToken($botUser),
'session_id' => $dialog->session_id,
'dialog_id' => $dialog->id,
'dialog_type' => $dialog->type,
'msg_id' => $msg->id,
'msg_uid' => $msg->userid,
'mention' => $this->mention ? 1 : 0,
'bot_uid' => $botUser->userid,
'version' => Base::getVersion(),
'extras' => Base::array2json($extras)
];
// 添加用户信息
$userInfo = User::find($msg->userid);
if ($userInfo) {
$data['msg_user'] = [
'userid' => $userInfo->userid,
'email' => $userInfo->email,
'nickname' => $userInfo->nickname,
'profession' => $userInfo->profession,
'lang' => $userInfo->lang,
'token' => User::generateTokenNoDevice($userInfo, now()->addHour()),
];
// 添加用户信息
$userInfo = User::find($msg->userid);
if ($userInfo) {
$data['msg_user'] = [
'userid' => $userInfo->userid,
'email' => $userInfo->email,
'nickname' => $userInfo->nickname,
'profession' => $userInfo->profession,
'lang' => $userInfo->lang,
'token' => User::generateTokenNoDevice($userInfo, now()->addHour()),
];
}
// 请求Webhook
$result = Ihttp::ihttp_post($webhookUrl, $data, 30);
if ($result['data'] && $data = Base::json2array($result['data'])) {
if ($data['code'] != 200 && $data['message']) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [
'text' => $result['data']['message']
], $botUser->userid, false, false, true);
}
}
} catch (\Throwable $th) {
info(Base::array2json([
'bot_userid' => $botUser->userid,
}
$result = null;
if ($userBot) {
$result = $userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE, $data, 30, [
'dialog' => $dialog->id,
'msg' => $msg->id,
'webhook_url' => $webhookUrl,
'error' => $th->getMessage(),
]));
]);
} else {
try {
$result = Ihttp::ihttp_post($webhookUrl, $data, 30);
} catch (\Throwable $th) {
info(Base::array2json([
'bot_userid' => $botUser->userid,
'dialog' => $dialog->id,
'msg' => $msg->id,
'webhook_url' => $webhookUrl,
'error' => $th->getMessage(),
]));
}
}
if ($result && isset($result['data'])) {
$responseData = Base::json2array($result['data']);
if (($responseData['code'] ?? 0) != 200 && !empty($responseData['message'])) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [
'text' => $responseData['message']
], $botUser->userid, false, false, true);
}
}
}

View File

@ -138,11 +138,11 @@ class WebSocketDialogMsgTask extends AbstractTask
'dot' => $dot,
'updated' => $updated,
];
// 机器人收到消处理
$botUser = User::whereUserid($userid)->whereBot(1)->first();
if ($botUser) {
$this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mentions, $this->client);
}
}
// 机器人收到消处理
$botUser = User::whereUserid($userid)->whereBot(1)->first();
if ($botUser) { // 避免机器人处理自己发送的消息
$this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mentions, $this->client);
}
}
// 更新已发送数量

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('user_bots', function (Blueprint $table) {
if (!Schema::hasColumn('user_bots', 'webhook_events')) {
$table->text('webhook_events')->nullable()->after('webhook_num')->comment('Webhook事件配置');
}
});
DB::table('user_bots')
->where(function ($query) {
$query->whereNull('webhook_events')->orWhere('webhook_events', '');
})
->update(['webhook_events' => json_encode(['message'])]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('user_bots', function (Blueprint $table) {
if (Schema::hasColumn('user_bots', 'webhook_events')) {
$table->dropColumn('webhook_events');
}
});
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->date('birthday')->nullable()->after('profession');
$table->string('address', 255)->nullable()->after('birthday');
$table->text('introduction')->nullable()->after('address');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['birthday', 'address', 'introduction']);
});
}
};

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserTagsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_tags', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id')->index()->comment('被标签用户ID');
$table->string('name', 50)->comment('标签名称');
$table->unsignedBigInteger('created_by')->index()->comment('创建人');
$table->unsignedBigInteger('updated_by')->nullable()->comment('最后更新人');
$table->timestamps();
$table->unique(['user_id', 'name'], 'user_tags_unique_name');
$table->foreign('user_id')->references('userid')->on('users')->onDelete('cascade');
$table->foreign('created_by')->references('userid')->on('users')->onDelete('cascade');
$table->foreign('updated_by')->references('userid')->on('users')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_tags');
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserTagRecognitionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_tag_recognitions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('tag_id')->index()->comment('标签ID');
$table->unsignedBigInteger('user_id')->index()->comment('认可人ID');
$table->timestamps();
$table->unique(['tag_id', 'user_id'], 'user_tag_recognitions_unique');
$table->foreign('tag_id')->references('id')->on('user_tags')->onDelete('cascade');
$table->foreign('user_id')->references('userid')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_tag_recognitions');
}
}

View File

@ -106,6 +106,7 @@
<p><span>ID:</span>{{ item.id }}</p>
<p><span>{{ $L('清理时间') }}:</span>{{ item.clear_day }}</p>
<p><span>Webhook:</span>{{ item.webhook_url || '-' }}</p>
<p><span>{{ $L('Webhook事件') }}:</span>{{ formatWebhookEvents(item.webhook_events) }}</p>
</div>
<div class="modal-item-btns">
<Button icon="md-chatbubbles" @click="applyClick({value: 'mybot-chat'}, item)">{{ $L('开始聊天') }}</Button>
@ -140,6 +141,13 @@
<FormItem prop="webhook_url" label="Webhook">
<Input v-model="mybotModifyData.webhook_url" :maxlength="255" :show-word-limit="0.9" type="textarea" placeholder="Webhook"/>
</FormItem>
<FormItem prop="webhook_events" :label="$L('Webhook事件')">
<CheckboxGroup v-model="mybotModifyData.webhook_events">
<Checkbox v-for="option in webhookEventOptions" :key="option.value" :label="option.value">
{{ $L(option.label) }}
</Checkbox>
</CheckboxGroup>
</FormItem>
</Form>
<div slot="footer" class="adaption">
<Button type="default" @click="mybotModifyShow=false">{{ $L('取消') }}</Button>
@ -377,6 +385,12 @@ export default {
mybotModifyShow: false,
mybotModifyData: {},
mybotModifyLoad: 0,
webhookEventOptions: [
{value: 'message', label: '接收消息'},
{value: 'dialog_open', label: '打开会话'},
{value: 'member_join', label: '成员加入'},
{value: 'member_leave', label: '成员退出'},
],
//
aibotShow: false,
aibotList: AIBotList,
@ -473,6 +487,37 @@ export default {
}
},
methods: {
normalizeWebhookEvents(events = [], useFallback = false) {
if (!Array.isArray(events)) {
events = events ? [events] : [];
}
const allowed = this.webhookEventOptions.map(item => item.value);
const result = events.filter(item => allowed.includes(item));
if (result.length) {
return Array.from(new Set(result));
}
return useFallback ? ['message'] : [];
},
enhanceMybotItem(item = {}) {
const data = $A.cloneJSON(item || {});
let events = data.webhook_events;
if (typeof events === 'undefined' || events === null) {
events = ['message'];
}
events = this.normalizeWebhookEvents(events, false);
if (!events.length) {
events = [];
}
data.webhook_events = events;
return data;
},
formatWebhookEvents(events) {
const values = this.normalizeWebhookEvents(events, false);
const labels = this.webhookEventOptions
.filter(option => values.includes(option.value))
.map(option => this.$L(option.label));
return labels.length ? labels.join('、') : '-';
},
getLogoClass(name) {
name = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
return name
@ -568,7 +613,7 @@ export default {
this.$store.dispatch("call", {
url: 'users/bot/list',
}).then(({data}) => {
this.mybotList = data.list;
this.mybotList = (data.list || []).map(item => this.enhanceMybotItem(item));
}).finally(_ => {
this.mybotLoad--
});
@ -581,7 +626,7 @@ export default {
},
//
addMybot(info) {
this.mybotModifyData = $A.cloneJSON(info)
this.mybotModifyData = this.enhanceMybotItem(info)
this.mybotModifyShow = true;
},
//
@ -620,11 +665,12 @@ export default {
onMybotModify() {
this.mybotModifyLoad++
this.$store.dispatch("editUserBot", this.mybotModifyData).then(({data, msg}) => {
const index = this.mybotList.findIndex(item => item.id === data.id);
const botData = this.enhanceMybotItem(data);
const index = this.mybotList.findIndex(item => item.id === botData.id);
if (index > -1) {
this.mybotList.splice(index, 1, data);
this.mybotList.splice(index, 1, botData);
} else {
this.mybotList.unshift(data);
this.mybotList.unshift(botData);
}
this.mybotModifyShow = false;
this.mybotModifyData = {};

View File

@ -69,9 +69,72 @@ export default {
"| `mention` | {{是否被@到}} | boolean |",
"| `bot_uid` | {{机器人ID}} | string |",
"| `version` | {{系统版本}} | string |",
"### 3. {{打开会话 消息推送}}",
"",
"{{打开机器人会话后会自动POST推送到配置的Webhook地址请求超时为30秒。}}",
"",
"#### {{推送参数}}",
"",
"| {{参数名}} | {{说明}} | {{类型}} |",
"|--------|------|------|",
"| `event` | {{推送事件}} | string |",
"| `timestamp` | {{推送时间戳}} | string |",
"| `dialog_id` | {{对话ID}} | string |",
"| `dialog_type` | {{对话类型}} | string |",
"| `bot_uid` | {{机器人ID}} | string |",
"| `owner_uid` | {{机器人所属用户ID}} | string |",
"| `user` | {{机器人所属用户信息}} | object |",
"| `user.userid` | {{用户ID}} | string |",
"| `user.email` | {{用户邮箱}} | string |",
"| `user.nickname` | {{用户昵称}} | string |",
"### 4. {{成员加入 消息推送}}",
"",
"{{成员加入群组后会自动POST推送到配置的Webhook地址请求超时为30秒。}}",
"",
"#### {{推送参数}}",
"",
"| {{参数名}} | {{说明}} | {{类型}} |",
"|--------|------|------|",
"| `event` | {{推送事件}} | string |",
"| `timestamp` | {{推送时间戳}} | string |",
"| `dialog_id` | {{对话ID}} | string |",
"| `dialog_type` | {{对话类型}} | string |",
"| `bot_uid` | {{机器人ID}} | string |",
"| `owner_uid` | {{机器人所属用户ID}} | string |",
"| `user` | {{机器人所属用户信息}} | object |",
"| `user.userid` | {{用户ID}} | string |",
"| `user.email` | {{用户邮箱}} | string |",
"| `user.nickname` | {{用户昵称}} | string |",
"### 5. {{成员退出 消息推送}}",
"",
"{{成员退出群组后会自动POST推送到配置的Webhook地址请求超时为30秒。}}",
"",
"#### {{推送参数}}",
"",
"| {{参数名}} | {{说明}} | {{类型}} |",
"|--------|------|------|",
"| `event` | {{推送事件}} | string |",
"| `timestamp` | {{推送时间戳}} | string |",
"| `dialog_id` | {{对话ID}} | string |",
"| `dialog_type` | {{对话类型}} | string |",
"| `dialog_name` | {{对话名称}} | string |",
"| `group_type` | {{群组类型}} | string |",
"| `bot_uid` | {{机器人ID}} | string |",
"| `owner_uid` | {{机器人所属用户ID}} | string |",
"| `action` | {{动作}} | string |",
"| `actor` | {{操作人信息}} | object |",
"| `actor.userid` | {{用户ID}} | string |",
"| `actor.email` | {{用户邮箱}} | string |",
"| `actor.nickname` | {{用户昵称}} | string |",
"| `actor.is_bot` | {{是否机器人}} | boolean |",
"| `members` | {{成员信息}} | array |",
"| `members.userid` | {{用户ID}} | string |",
"| `members.email` | {{用户邮箱}} | string |",
"| `members.nickname` | {{用户昵称}} | string |",
"| `members.is_bot` | {{是否机器人}} | boolean |",
].map(item => item.replace(/\{\{([^}]+)\}\}/g, (_, v1) => this.$L(v1))).join("\n");
},
},
methods: {},
}
</script>
</script>

View File

@ -462,6 +462,13 @@
<FormItem v-if="typeof modifyData.webhook_url !== 'undefined'" prop="webhook_url" label="Webhook">
<Input v-model="modifyData.webhook_url" :maxlength="255" />
</FormItem>
<FormItem v-if="typeof modifyData.webhook_events !== 'undefined'" prop="webhook_events" :label="$L('Webhook事件')">
<CheckboxGroup v-model="webhookEvents">
<Checkbox v-for="option in webhookEventOptions" :key="option.value" :label="option.value">
{{$L(option.label)}}
</Checkbox>
</CheckboxGroup>
</FormItem>
</template>
</Form>
<div slot="footer" class="adaption">
@ -768,6 +775,13 @@ export default {
modifyAiShow: false,
modifyData: {},
modifyLoad: 0,
webhookEventOptions: [
{value: 'message', label: '接收消息'},
{value: 'dialog_open', label: '打开会话'},
{value: 'member_join', label: '成员加入'},
{value: 'member_leave', label: '成员退出'},
],
webhookEvents: [],
openId: 0,
errorId: 0,
@ -1449,6 +1463,28 @@ export default {
methods: {
transformEmojiToHtml,
normalizeWebhookEvents(events = [], useFallback = false) {
if (!Array.isArray(events)) {
events = events ? [events] : [];
}
const allowed = this.webhookEventOptions.map(item => item.value);
const result = events.filter(item => allowed.includes(item));
if (result.length) {
return Array.from(new Set(result));
}
return useFallback ? ['message'] : [];
},
prepareWebhookEvents(events, useFallback = false) {
let value = events;
if (typeof value === 'undefined' || value === null) {
value = useFallback ? ['message'] : [];
}
value = this.normalizeWebhookEvents(value, false);
if (!value.length && useFallback) {
return ['message'];
}
return value;
},
/**
* 获取会话基本信息
* @param dialog_id
@ -2727,7 +2763,9 @@ export default {
clear_day: 0,
webhook_url: '',
system_name: '',
webhook_events: [],
})
this.webhookEvents = this.prepareWebhookEvents([], true)
this.modifyLoad++;
this.$store.dispatch("call", {
url: 'users/bot/info',
@ -2738,6 +2776,7 @@ export default {
this.modifyData.clear_day = data.clear_day
this.modifyData.webhook_url = data.webhook_url
this.modifyData.system_name = data.system_name
this.webhookEvents = this.prepareWebhookEvents(data.webhook_events, true)
}).finally(() => {
this.modifyLoad--;
})
@ -2892,6 +2931,7 @@ export default {
name: this.modifyData.name,
clear_day: this.modifyData.clear_day,
webhook_url: this.modifyData.webhook_url,
webhook_events: this.normalizeWebhookEvents(this.webhookEvents),
dialog_id: this.modifyData.dialog_id
}).then(({msg}) => {
$A.messageSuccess(msg);

View File

@ -31,6 +31,35 @@
<span>{{$L('职位/职称')}}: </span>
{{userData.profession || '-'}}
</li>
<li>
<span>{{$L('生日')}}: </span>
{{userData.birthday ? ($A.newDateString(userData.birthday, 'YYYY-MM-DD') || userData.birthday) : '-'}}
</li>
<li>
<span>{{$L('地址')}}: </span>
{{userData.address || '-'}}
</li>
<li>
<span>{{$L('个人简介')}}: </span>
{{userData.introduction || '-'}}
</li>
<li class="user-tags-line">
<span>{{$L('个性标签')}}: </span>
<div class="tags-content" @click="onOpenTagsModal">
<div v-if="displayTags.length" class="tags-list">
<Tag
v-for="tag in displayTags"
:key="tag.id"
:color="tag.recognized ? 'primary' : 'default'"
class="tag-pill">{{tag.name}}</Tag>
</div>
<span v-else class="tags-empty">{{$L('暂无个性标签')}}</span>
<div class="tags-extra">
<span v-if="personalTagTotal > displayTags.length" class="tags-total">{{$L('(*)', personalTagTotal)}}</span>
<Button type="text" size="small" class="manage-button" @click.stop="onOpenTagsModal">{{$L('管理')}}</Button>
</div>
</div>
</li>
<li>
<span>{{$L('最后在线')}}: </span>
{{$A.newDateString(userData.line_at, 'YYYY-MM-DD HH:mm') || '-'}}
@ -43,7 +72,10 @@
</li>
</template>
</ul>
<Button icon="md-chatbubbles" :disabled="!!userData.delete_at" @click="onOpenDialog">{{ $L('开始聊天') }}</Button>
<div class="user-detail-actions">
<Button icon="md-chatbubbles" :disabled="!!userData.delete_at" @click="onOpenDialog">{{ $L('开始聊天') }}</Button>
<Button icon="md-people" :disabled="!!userData.delete_at" @click="onOpenCreateGroup">{{ $L('创建群组') }}</Button>
</div>
</div>
<!-- 共同群组 -->
@ -87,6 +119,11 @@
</div>
</div>
</Modal>
<UserTagsModal
v-if="userData.userid"
v-model="tagModalVisible"
:userid="userData.userid"
@updated="onTagsUpdated"/>
</ModalAlive>
</template>
@ -94,10 +131,13 @@
import emitter from "../../../store/events";
import transformEmojiToHtml from "../../../utils/emoji";
import {mapState} from "vuex";
import UserTagsModal from "./UserTagsModal.vue";
export default {
name: 'UserDetail',
components: {UserTagsModal},
data() {
return {
userData: {
@ -106,6 +146,8 @@ export default {
showModal: false,
tagModalVisible: false,
commonDialog: {
userid: null,
total: null,
@ -145,6 +187,17 @@ export default {
commonDialogList() {
return this.commonDialog.list || [];
},
displayTags() {
return Array.isArray(this.userData.personal_tags) ? this.userData.personal_tags : [];
},
personalTagTotal() {
if (typeof this.userData.personal_tags_total === 'number') {
return this.userData.personal_tags_total;
}
return this.displayTags.length;
}
},
methods: {
@ -157,6 +210,7 @@ export default {
this.$store.dispatch("showSpinner", 600)
this.$store.dispatch('getUserData', userid).then(user => {
this.userData = user;
this.ensureTagDefaults();
this.showModal = true;
this.loadCommonDialogCount()
}).finally(_ => {
@ -166,7 +220,8 @@ export default {
onHide() {
this.commonDialogShow = false;
this.showModal = false
this.showModal = false;
this.tagModalVisible = false;
},
onOpenAvatar() {
@ -181,6 +236,41 @@ export default {
});
},
onOpenCreateGroup() {
const userids = [];
if (this.userId) {
userids.push(this.userId);
}
if (this.userData.userid && this.userData.userid !== this.userId) {
userids.push(this.userData.userid);
}
if (userids.length === 0 && this.userData.userid) {
userids.push(this.userData.userid);
}
emitter.emit('createGroup', userids);
},
ensureTagDefaults() {
if (!Array.isArray(this.userData.personal_tags)) {
this.$set(this.userData, 'personal_tags', []);
}
if (typeof this.userData.personal_tags_total !== 'number') {
this.$set(this.userData, 'personal_tags_total', this.userData.personal_tags.length);
}
},
onOpenTagsModal() {
if (!this.userData.userid) {
return;
}
this.tagModalVisible = true;
},
onTagsUpdated({top, total}) {
this.$set(this.userData, 'personal_tags', Array.isArray(top) ? top : []);
this.$set(this.userData, 'personal_tags_total', typeof total === 'number' ? total : this.userData.personal_tags.length);
},
loadCommonDialogCount() {
const target_userid = this.userData.userid;
const previousUserId = this.commonDialog.userid;
@ -280,3 +370,52 @@ export default {
}
};
</script>
<style lang="scss" scoped>
.user-tags-line {
display: flex;
align-items: flex-start;
gap: 8px;
span:first-child {
flex: 0 0 auto;
}
.tags-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
cursor: pointer;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
.tag-pill {
cursor: pointer;
}
}
.tags-empty {
color: #909399;
}
.tags-extra {
display: flex;
align-items: center;
gap: 8px;
.tags-total {
color: #909399;
font-size: 12px;
}
.manage-button {
padding: 0;
}
}
}
</style>

View File

@ -0,0 +1,459 @@
<template>
<ModalAlive
v-model="visible"
class-name="user-tags-manage-modal"
:mask-closable="false"
:footer-hide="true"
width="520"
:closable="true">
<div class="tag-modal-container">
<div class="tag-modal-header">
<h3>{{$L('个性标签管理')}}</h3>
<p class="tag-modal-meta">
<span>{{$L('当前共(*)个标签', total)}}</span>
</p>
</div>
<div class="tag-modal-form">
<Input
v-model="newTagName"
:maxlength="20"
:disabled="pending.add"
:placeholder="$L('请输入个性标签')"
@on-enter="handleAdd">
<Button
slot="append"
type="primary"
:loading="pending.add"
@click="handleAdd">{{$L('添加')}}</Button>
</Input>
</div>
<div class="tag-modal-body">
<div v-if="loading > 0 && tags.length === 0" class="tag-loading">
<Loading />
</div>
<div v-else-if="tags.length === 0" class="tag-empty">
<Icon type="ios-pricetags-outline" size="32" />
<p>{{$L('还没有个性标签,快来添加吧~')}}</p>
</div>
<ul v-else class="tag-list">
<li
v-for="tag in tags"
:key="tag.id"
class="tag-item"
:class="{'is-editing': editId === tag.id}">
<div class="tag-item-main">
<div class="tag-name" v-if="editId !== tag.id">
<Tag :color="tag.recognized ? 'primary' : 'default'" class="tag-pill">{{tag.name}}</Tag>
</div>
<div class="tag-name edit" v-else>
<Input
ref="editInput"
size="small"
v-model="editName"
:maxlength="20"
:disabled="isPending(tag.id, 'edit')"
@on-enter="confirmEdit(tag)"/>
</div>
<div class="tag-actions">
<Button
type="text"
size="small"
class="recognize-btn"
:loading="isPending(tag.id, 'recognize')"
@click="toggleRecognize(tag)">
<Icon type="md-thumbs-up" />
<span>{{tag.recognition_total}}</span>
<span class="recognize-text">{{$L('认可')}}</span>
</Button>
<template v-if="editId === tag.id">
<Button
type="primary"
size="small"
:loading="isPending(tag.id, 'edit')"
@click="confirmEdit(tag)">{{$L('保存')}}</Button>
<Button
type="text"
size="small"
@click="cancelEdit">{{$L('取消')}}</Button>
</template>
<template v-else>
<Button
v-if="tag.can_edit"
type="text"
size="small"
@click="startEdit(tag)">{{$L('编辑')}}</Button>
<Button
v-if="tag.can_delete"
type="text"
size="small"
:loading="isPending(tag.id, 'delete')"
@click="confirmDelete(tag)">{{$L('删除')}}</Button>
</template>
</div>
</div>
<div class="tag-meta-info" v-if="tag.created_by_name">
<span>{{$L('由(*)创建', tag.created_by_name)}}</span>
</div>
</li>
</ul>
</div>
</div>
</ModalAlive>
</template>
<script>
export default {
name: 'UserTagsModal',
props: {
value: {
type: Boolean,
default: false
},
userid: {
type: Number,
required: true
}
},
data() {
return {
visible: this.value,
loading: 0,
tags: [],
newTagName: '',
editId: null,
editName: '',
pending: {
add: false,
tagId: null,
type: ''
}
};
},
computed: {
userId() {
return this.$store.state.userId;
},
total() {
return this.tags.length;
}
},
watch: {
value(v) {
this.visible = v;
if (v) {
this.openModal();
}
},
visible(v) {
this.$emit('input', v);
if (!v) {
this.resetInlineState();
}
},
userid() {
if (this.visible) {
this.loadTags();
}
}
},
methods: {
openModal() {
this.resetInlineState();
this.loadTags();
},
resetInlineState() {
this.newTagName = '';
this.editId = null;
this.editName = '';
this.pending = {
add: false,
tagId: null,
type: ''
};
},
setPending(type, tagId = null) {
if (type === 'add') {
this.pending.add = true;
} else {
this.pending.tagId = tagId;
this.pending.type = type;
}
},
clearPending(type) {
if (type === 'add') {
this.pending.add = false;
} else if (this.pending.type === type) {
this.pending.tagId = null;
this.pending.type = '';
}
},
isPending(tagId, type) {
return this.pending.tagId === tagId && this.pending.type === type;
},
loadTags() {
if (!this.userid) {
return;
}
this.loading++;
this.$store.dispatch('call', {
url: 'users/tags/lists',
data: {userid: this.userid},
}).then(({data}) => {
this.applyTagData(data);
}).catch(({msg}) => {
$A.modalError(msg || this.$L('加载失败'));
}).finally(() => {
this.loading--;
});
},
applyTagData(data) {
const list = Array.isArray(data?.list) ? data.list : [];
this.tags = list;
const top = Array.isArray(data?.top) ? data.top : list.slice(0, 10);
const total = typeof data?.total === 'number' ? data.total : list.length;
this.emitUpdated({list, top, total});
},
emitUpdated(payload) {
this.$emit('updated', payload);
if (this.userid === this.$store.state.userInfo.userid) {
const info = Object.assign({}, this.$store.state.userInfo, {
personal_tags: payload.top,
personal_tags_total: payload.total
});
this.$store.dispatch('saveUserInfoBase', info);
}
this.$store.dispatch('saveUserBasic', {
userid: this.userid,
personal_tags: payload.top,
personal_tags_total: payload.total
});
},
handleAdd() {
const name = this.newTagName.trim();
if (!name) {
$A.messageError(this.$L('请输入个性标签'));
return;
}
if (name.length > 20) {
$A.messageError(this.$L('标签名称最多只能设置20个字'));
return;
}
if (this.pending.add) {
return;
}
this.setPending('add');
this.$store.dispatch('call', {
url: 'users/tags/add',
method: 'post',
data: {userid: this.userid, name},
}).then(({data, msg}) => {
this.applyTagData(data);
this.newTagName = '';
if (msg) {
$A.messageSuccess(msg);
}
}).catch(({msg}) => {
$A.modalError(msg || this.$L('添加失败'));
}).finally(() => {
this.clearPending('add');
});
},
startEdit(tag) {
this.editId = tag.id;
this.editName = tag.name;
this.$nextTick(() => {
const input = this.$refs.editInput;
if (input && input.focus) {
input.focus();
} else if (Array.isArray(input) && input.length > 0 && input[0].focus) {
input[0].focus();
}
});
},
cancelEdit() {
this.editId = null;
this.editName = '';
},
confirmEdit(tag) {
const name = this.editName.trim();
if (!name) {
$A.messageError(this.$L('请输入个性标签'));
return;
}
if (name.length > 20) {
$A.messageError(this.$L('标签名称最多只能设置20个字'));
return;
}
if (name === tag.name) {
this.cancelEdit();
return;
}
if (this.isPending(tag.id, 'edit')) {
return;
}
this.setPending('edit', tag.id);
this.$store.dispatch('call', {
url: 'users/tags/update',
method: 'post',
data: {tag_id: tag.id, name},
}).then(({data, msg}) => {
this.applyTagData(data);
this.cancelEdit();
if (msg) {
$A.messageSuccess(msg);
}
}).catch(({msg}) => {
$A.modalError(msg || this.$L('保存失败'));
}).finally(() => {
this.clearPending('edit');
});
},
confirmDelete(tag) {
if (this.isPending(tag.id, 'delete')) {
return;
}
$A.modalConfirm({
title: this.$L('删除标签'),
content: this.$L('确定要删除该标签吗?'),
onOk: () => {
this.deleteTag(tag);
}
});
},
deleteTag(tag) {
this.setPending('delete', tag.id);
this.$store.dispatch('call', {
url: 'users/tags/delete',
method: 'post',
data: {tag_id: tag.id},
}).then(({data, msg}) => {
this.applyTagData(data);
if (msg) {
$A.messageSuccess(msg);
}
}).catch(({msg}) => {
$A.modalError(msg || this.$L('删除失败'));
}).finally(() => {
this.clearPending('delete');
});
},
toggleRecognize(tag) {
if (this.isPending(tag.id, 'recognize')) {
return;
}
this.setPending('recognize', tag.id);
this.$store.dispatch('call', {
url: 'users/tags/recognize',
method: 'post',
data: {tag_id: tag.id},
}).then(({data, msg}) => {
this.applyTagData(data);
if (msg) {
$A.messageSuccess(msg);
}
}).catch(({msg}) => {
$A.modalError(msg || this.$L('操作失败'));
}).finally(() => {
this.clearPending('recognize');
});
}
}
};
</script>
<style lang="scss" scoped>
.user-tags-manage-modal {
.tag-modal-container {
padding: 16px 20px 12px;
}
.tag-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.tag-modal-meta {
margin: 0;
color: #909399;
font-size: 12px;
}
}
.tag-modal-form {
margin-bottom: 16px;
}
.tag-modal-body {
max-height: 360px;
overflow-y: auto;
}
.tag-loading {
display: flex;
justify-content: center;
padding: 40px 0;
}
.tag-empty {
text-align: center;
padding: 32px 0;
color: #909399;
p {
margin-top: 8px;
}
}
.tag-list {
list-style: none;
margin: 0;
padding: 0;
.tag-item {
border: 1px solid var(--divider-color, #ebeef5);
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
&.is-editing {
background-color: rgba(64, 158, 255, 0.08);
}
.tag-item-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.tag-name {
flex: 1;
display: flex;
align-items: center;
&.edit {
max-width: 220px;
}
}
.tag-pill {
cursor: default;
}
.tag-actions {
display: flex;
align-items: center;
gap: 4px;
.recognize-btn {
display: inline-flex;
align-items: center;
gap: 4px;
.recognize-text {
font-size: 12px;
color: #606266;
}
}
}
.tag-meta-info {
margin-top: 6px;
font-size: 12px;
color: #a0a3a6;
}
}
}
}
</style>

View File

@ -22,19 +22,64 @@
<FormItem :label="$L('职位/职称')" prop="profession">
<Input v-model="formData.profession" :maxlength="20" :placeholder="$L('请输入职位/职称')"></Input>
</FormItem>
<FormItem :label="$L('生日')" prop="birthday">
<DatePicker
v-model="formData.birthday"
type="date"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
:placeholder="$L('请选择生日')"
confirm
transfer/>
</FormItem>
<FormItem :label="$L('地址')" prop="address">
<Input v-model="formData.address" :maxlength="100" :placeholder="$L('请输入地址')"></Input>
</FormItem>
<FormItem :label="$L('个人简介')" prop="introduction">
<Input
v-model="formData.introduction"
type="textarea"
:rows="2"
:autosize="{ minRows: 2, maxRows: 8 }"
:maxlength="500"
:placeholder="$L('请输入个人简介')"></Input>
</FormItem>
<FormItem :label="$L('个性标签')">
<div class="user-tags-preview" @click="openTagModal">
<template v-if="displayTags.length">
<Tag
v-for="tag in displayTags"
:key="tag.id"
:color="tag.recognized ? 'primary' : 'default'"
class="tag-pill">{{tag.name}}</Tag>
</template>
<span v-else class="tags-empty">{{$L('暂无个性标签')}}</span>
<span v-if="personalTagTotal > displayTags.length" class="tags-total">{{$L('(*)', personalTagTotal)}}</span>
<Button type="text" size="small" class="manage-button" @click.stop="openTagModal">
<Icon type="md-create" />
{{$L('管理')}}
</Button>
</div>
</FormItem>
</Form>
<div class="setting-footer">
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>
<Button :loading="loadIng > 0" @click="resetForm" style="margin-left: 8px">{{$L('重置')}}</Button>
</div>
<UserTagsModal
v-if="userInfo.userid"
v-model="tagModalVisible"
:userid="userInfo.userid"
@updated="onTagsUpdated"/>
</div>
</template>
<script>
import ImgUpload from "../../../components/ImgUpload";
import UserTagsModal from "../components/UserTagsModal.vue";
import {mapState} from "vuex";
export default {
components: {ImgUpload},
components: {ImgUpload, UserTagsModal},
data() {
return {
loadIng: 0,
@ -44,7 +89,10 @@ export default {
email: '',
tel: '',
nickname: '',
profession: ''
profession: '',
birthday: '',
address: '',
introduction: ''
},
ruleData: {
@ -60,6 +108,10 @@ export default {
{type: 'string', min: 2, message: this.$L('昵称长度至少2位'), trigger: 'change'}
]
},
tagModalVisible: false,
personalTags: [],
personalTagTotal: 0,
}
},
mounted() {
@ -67,6 +119,10 @@ export default {
},
computed: {
...mapState(['userInfo', 'formOptions']),
displayTags() {
return this.personalTags;
}
},
watch: {
userInfo() {
@ -80,7 +136,19 @@ export default {
this.$set(this.formData, 'tel', this.userInfo.tel);
this.$set(this.formData, 'nickname', typeof this.userInfo.nickname_original !== "undefined" ? this.userInfo.nickname_original : this.userInfo.nickname);
this.$set(this.formData, 'profession', this.userInfo.profession);
this.$set(this.formData, 'birthday', this.userInfo.birthday || '');
this.$set(this.formData, 'address', this.userInfo.address || '');
this.$set(this.formData, 'introduction', this.userInfo.introduction || '');
this.formData_bak = $A.cloneJSON(this.formData);
this.syncPersonalTags();
},
syncPersonalTags() {
const tags = Array.isArray(this.userInfo.personal_tags) ? this.userInfo.personal_tags : [];
this.personalTags = tags.slice(0, 10);
this.personalTagTotal = typeof this.userInfo.personal_tags_total === 'number'
? this.userInfo.personal_tags_total
: this.personalTags.length;
},
submitForm() {
@ -106,7 +174,50 @@ export default {
resetForm() {
this.formData = $A.cloneJSON(this.formData_bak);
},
openTagModal() {
if (!this.userInfo.userid) {
return;
}
this.tagModalVisible = true;
},
onTagsUpdated({top, total}) {
this.personalTags = Array.isArray(top) ? top : [];
this.personalTagTotal = typeof total === 'number' ? total : this.personalTags.length;
}
}
}
</script>
<style lang="scss" scoped>
.user-tags-preview {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
min-height: 32px;
cursor: pointer;
.tag-pill {
cursor: pointer;
}
.tags-empty {
color: #909399;
}
.tags-total {
color: #909399;
font-size: 12px;
}
.manage-button {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 4px;
}
}
</style>

View File

@ -3470,7 +3470,7 @@ export default {
}
return item.dialog_user.userid === userid
});
if (dialog) {
if (dialog && dialog.bot !== 1) {
return dispatch("openDialog", dialog.id).then(resolve).catch(reject)
}
dispatch("call", {

View File

@ -19,6 +19,11 @@
}
}
.user-detail-actions {
display: flex;
gap: 10px;
}
.ivu-modal-content {
overflow: hidden;

View File

@ -152,9 +152,15 @@
.setting-item {
.ivu-input,
.ivu-select-default {
.ivu-select-default,
.ivu-date-picker,
.user-tags-preview {
max-width: 460px;
}
.ivu-date-picker,
.user-tags-preview {
width: 100%;
}
.ivu-form {
overflow: auto;
}