feat: 添加个性标签管理功能

This commit is contained in:
kuaifan 2025-10-12 23:02:34 +00:00
parent 49701fcd09
commit 6d97bf1e88
9 changed files with 1088 additions and 7 deletions

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;
@ -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
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

@ -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

@ -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>

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

@ -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>

View File

@ -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 {