mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-10 18:02:55 +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\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;
|
||||
@ -355,6 +357,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);
|
||||
}
|
||||
|
||||
@ -773,8 +778,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');
|
||||
@ -792,6 +801,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;
|
||||
}
|
||||
@ -1456,6 +1468,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 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>
|
||||
{{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') || '-'}}
|
||||
@ -102,6 +119,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<UserTagsModal
|
||||
v-if="userData.userid"
|
||||
v-model="tagModalVisible"
|
||||
:userid="userData.userid"
|
||||
@updated="onTagsUpdated"/>
|
||||
</ModalAlive>
|
||||
</template>
|
||||
|
||||
@ -109,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: {
|
||||
@ -121,6 +146,8 @@ export default {
|
||||
|
||||
showModal: false,
|
||||
|
||||
tagModalVisible: false,
|
||||
|
||||
commonDialog: {
|
||||
userid: null,
|
||||
total: null,
|
||||
@ -160,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: {
|
||||
@ -172,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(_ => {
|
||||
@ -181,7 +220,8 @@ export default {
|
||||
|
||||
onHide() {
|
||||
this.commonDialogShow = false;
|
||||
this.showModal = false
|
||||
this.showModal = false;
|
||||
this.tagModalVisible = false;
|
||||
},
|
||||
|
||||
onOpenAvatar() {
|
||||
@ -210,6 +250,27 @@ export default {
|
||||
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;
|
||||
@ -309,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>
|
||||
|
||||
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
|
||||
v-model="formData.introduction"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
: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,
|
||||
@ -84,6 +108,10 @@ export default {
|
||||
{type: 'string', min: 2, message: this.$L('昵称长度至少2位!'), trigger: 'change'}
|
||||
]
|
||||
},
|
||||
|
||||
tagModalVisible: false,
|
||||
personalTags: [],
|
||||
personalTagTotal: 0,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -91,6 +119,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(['userInfo', 'formOptions']),
|
||||
|
||||
displayTags() {
|
||||
return this.personalTags;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
userInfo() {
|
||||
@ -108,6 +140,15 @@ export default {
|
||||
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() {
|
||||
@ -133,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>
|
||||
|
||||
@ -153,10 +153,12 @@
|
||||
.setting-item {
|
||||
.ivu-input,
|
||||
.ivu-select-default,
|
||||
.ivu-date-picker {
|
||||
.ivu-date-picker,
|
||||
.user-tags-preview {
|
||||
max-width: 460px;
|
||||
}
|
||||
.ivu-date-picker {
|
||||
.ivu-date-picker,
|
||||
.user-tags-preview {
|
||||
width: 100%;
|
||||
}
|
||||
.ivu-form {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user