mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-12 11:19:56 +00:00
feat: 添加个性标签管理功能
This commit is contained in:
parent
49701fcd09
commit
6d97bf1e88
@ -34,6 +34,8 @@ use App\Models\WebSocketDialogUser;
|
|||||||
use App\Models\UserTaskBrowse;
|
use App\Models\UserTaskBrowse;
|
||||||
use App\Models\UserFavorite;
|
use App\Models\UserFavorite;
|
||||||
use App\Models\UserRecentItem;
|
use App\Models\UserRecentItem;
|
||||||
|
use App\Models\UserTag;
|
||||||
|
use App\Models\UserTagRecognition;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use App\Models\UserEmailVerification;
|
use App\Models\UserEmailVerification;
|
||||||
use App\Module\AgoraIO\AgoraTokenGenerator;
|
use App\Module\AgoraIO\AgoraTokenGenerator;
|
||||||
@ -355,6 +357,9 @@ class UsersController extends AbstractController
|
|||||||
$data['nickname_original'] = $user->getRawOriginal('nickname');
|
$data['nickname_original'] = $user->getRawOriginal('nickname');
|
||||||
$data['department_name'] = $user->getDepartmentName();
|
$data['department_name'] = $user->getDepartmentName();
|
||||||
$data['department_owner'] = UserDepartment::where('parent_id',0)->where('owner_userid', $user->userid)->exists(); // 适用默认部门下第1级负责人才能添加部门OKR
|
$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);
|
return Base::retSuccess('success', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -773,8 +778,12 @@ class UsersController extends AbstractController
|
|||||||
public function basic()
|
public function basic()
|
||||||
{
|
{
|
||||||
$sharekey = Request::header('sharekey');
|
$sharekey = Request::header('sharekey');
|
||||||
if (empty($sharekey) || !Meeting::getShareInfo($sharekey)) {
|
$shareInfo = $sharekey ? Meeting::getShareInfo($sharekey) : null;
|
||||||
User::auth();
|
$viewer = null;
|
||||||
|
if (empty($shareInfo)) {
|
||||||
|
$viewer = User::auth();
|
||||||
|
} elseif (Doo::userId() > 0) {
|
||||||
|
$viewer = User::whereUserid(Doo::userId())->first();
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$userid = Request::input('userid');
|
$userid = Request::input('userid');
|
||||||
@ -792,6 +801,9 @@ class UsersController extends AbstractController
|
|||||||
$basic = UserDelete::userid2basic($id);
|
$basic = UserDelete::userid2basic($id);
|
||||||
}
|
}
|
||||||
if ($basic) {
|
if ($basic) {
|
||||||
|
$tagMeta = UserTag::listWithMeta($basic->userid, $viewer);
|
||||||
|
$basic->personal_tags = $tagMeta['top'];
|
||||||
|
$basic->personal_tags_total = $tagMeta['total'];
|
||||||
//
|
//
|
||||||
$retArray[] = $basic;
|
$retArray[] = $basic;
|
||||||
}
|
}
|
||||||
@ -1456,6 +1468,233 @@ class UsersController extends AbstractController
|
|||||||
return Base::retSuccess('success', $data);
|
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 20. 【会议】获取分享链接
|
* @api {get} api/users/meeting/link 20. 【会议】获取分享链接
|
||||||
*
|
*
|
||||||
|
|||||||
84
app/Models/UserTag.php
Normal file
84
app/Models/UserTag.php
Normal 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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Models/UserTagRecognition.php
Normal file
26
app/Models/UserTagRecognition.php
Normal 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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -43,6 +43,23 @@
|
|||||||
<span>{{$L('个人简介')}}: </span>
|
<span>{{$L('个人简介')}}: </span>
|
||||||
{{userData.introduction || '-'}}
|
{{userData.introduction || '-'}}
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<span>{{$L('最后在线')}}: </span>
|
<span>{{$L('最后在线')}}: </span>
|
||||||
{{$A.newDateString(userData.line_at, 'YYYY-MM-DD HH:mm') || '-'}}
|
{{$A.newDateString(userData.line_at, 'YYYY-MM-DD HH:mm') || '-'}}
|
||||||
@ -102,6 +119,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<UserTagsModal
|
||||||
|
v-if="userData.userid"
|
||||||
|
v-model="tagModalVisible"
|
||||||
|
:userid="userData.userid"
|
||||||
|
@updated="onTagsUpdated"/>
|
||||||
</ModalAlive>
|
</ModalAlive>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -109,10 +131,13 @@
|
|||||||
import emitter from "../../../store/events";
|
import emitter from "../../../store/events";
|
||||||
import transformEmojiToHtml from "../../../utils/emoji";
|
import transformEmojiToHtml from "../../../utils/emoji";
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
|
import UserTagsModal from "./UserTagsModal.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UserDetail',
|
name: 'UserDetail',
|
||||||
|
|
||||||
|
components: {UserTagsModal},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
userData: {
|
userData: {
|
||||||
@ -121,6 +146,8 @@ export default {
|
|||||||
|
|
||||||
showModal: false,
|
showModal: false,
|
||||||
|
|
||||||
|
tagModalVisible: false,
|
||||||
|
|
||||||
commonDialog: {
|
commonDialog: {
|
||||||
userid: null,
|
userid: null,
|
||||||
total: null,
|
total: null,
|
||||||
@ -160,6 +187,17 @@ export default {
|
|||||||
commonDialogList() {
|
commonDialogList() {
|
||||||
return this.commonDialog.list || [];
|
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: {
|
methods: {
|
||||||
@ -172,6 +210,7 @@ export default {
|
|||||||
this.$store.dispatch("showSpinner", 600)
|
this.$store.dispatch("showSpinner", 600)
|
||||||
this.$store.dispatch('getUserData', userid).then(user => {
|
this.$store.dispatch('getUserData', userid).then(user => {
|
||||||
this.userData = user;
|
this.userData = user;
|
||||||
|
this.ensureTagDefaults();
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
this.loadCommonDialogCount()
|
this.loadCommonDialogCount()
|
||||||
}).finally(_ => {
|
}).finally(_ => {
|
||||||
@ -181,7 +220,8 @@ export default {
|
|||||||
|
|
||||||
onHide() {
|
onHide() {
|
||||||
this.commonDialogShow = false;
|
this.commonDialogShow = false;
|
||||||
this.showModal = false
|
this.showModal = false;
|
||||||
|
this.tagModalVisible = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
onOpenAvatar() {
|
onOpenAvatar() {
|
||||||
@ -210,6 +250,27 @@ export default {
|
|||||||
emitter.emit('createGroup', userids);
|
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() {
|
loadCommonDialogCount() {
|
||||||
const target_userid = this.userData.userid;
|
const target_userid = this.userData.userid;
|
||||||
const previousUserId = this.commonDialog.userid;
|
const previousUserId = this.commonDialog.userid;
|
||||||
@ -309,3 +370,52 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
459
resources/assets/js/pages/manage/components/UserTagsModal.vue
Normal file
459
resources/assets/js/pages/manage/components/UserTagsModal.vue
Normal 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>
|
||||||
@ -39,23 +39,47 @@
|
|||||||
<Input
|
<Input
|
||||||
v-model="formData.introduction"
|
v-model="formData.introduction"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
:rows="2"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 8 }"
|
||||||
:maxlength="500"
|
:maxlength="500"
|
||||||
:placeholder="$L('请输入个人简介')"></Input>
|
:placeholder="$L('请输入个人简介')"></Input>
|
||||||
</FormItem>
|
</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>
|
</Form>
|
||||||
<div class="setting-footer">
|
<div class="setting-footer">
|
||||||
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>
|
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{$L('提交')}}</Button>
|
||||||
<Button :loading="loadIng > 0" @click="resetForm" style="margin-left: 8px">{{$L('重置')}}</Button>
|
<Button :loading="loadIng > 0" @click="resetForm" style="margin-left: 8px">{{$L('重置')}}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<UserTagsModal
|
||||||
|
v-if="userInfo.userid"
|
||||||
|
v-model="tagModalVisible"
|
||||||
|
:userid="userInfo.userid"
|
||||||
|
@updated="onTagsUpdated"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ImgUpload from "../../../components/ImgUpload";
|
import ImgUpload from "../../../components/ImgUpload";
|
||||||
|
import UserTagsModal from "../components/UserTagsModal.vue";
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
export default {
|
export default {
|
||||||
components: {ImgUpload},
|
components: {ImgUpload, UserTagsModal},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loadIng: 0,
|
loadIng: 0,
|
||||||
@ -84,6 +108,10 @@ export default {
|
|||||||
{type: 'string', min: 2, message: this.$L('昵称长度至少2位!'), trigger: 'change'}
|
{type: 'string', min: 2, message: this.$L('昵称长度至少2位!'), trigger: 'change'}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
tagModalVisible: false,
|
||||||
|
personalTags: [],
|
||||||
|
personalTagTotal: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -91,6 +119,10 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['userInfo', 'formOptions']),
|
...mapState(['userInfo', 'formOptions']),
|
||||||
|
|
||||||
|
displayTags() {
|
||||||
|
return this.personalTags;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
userInfo() {
|
userInfo() {
|
||||||
@ -108,6 +140,15 @@ export default {
|
|||||||
this.$set(this.formData, 'address', this.userInfo.address || '');
|
this.$set(this.formData, 'address', this.userInfo.address || '');
|
||||||
this.$set(this.formData, 'introduction', this.userInfo.introduction || '');
|
this.$set(this.formData, 'introduction', this.userInfo.introduction || '');
|
||||||
this.formData_bak = $A.cloneJSON(this.formData);
|
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() {
|
submitForm() {
|
||||||
@ -133,7 +174,50 @@ export default {
|
|||||||
|
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.formData = $A.cloneJSON(this.formData_bak);
|
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>
|
</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>
|
||||||
|
|||||||
@ -153,10 +153,12 @@
|
|||||||
.setting-item {
|
.setting-item {
|
||||||
.ivu-input,
|
.ivu-input,
|
||||||
.ivu-select-default,
|
.ivu-select-default,
|
||||||
.ivu-date-picker {
|
.ivu-date-picker,
|
||||||
|
.user-tags-preview {
|
||||||
max-width: 460px;
|
max-width: 460px;
|
||||||
}
|
}
|
||||||
.ivu-date-picker {
|
.ivu-date-picker,
|
||||||
|
.user-tags-preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.ivu-form {
|
.ivu-form {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user