mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 02:12:53 +00:00
feat: 添加标签排序功能
- 在 ProjectController 中新增 tag__sort 方法,支持项目标签的排序 - 更新 ProjectTag 模型,添加排序字段 - 新增数据库迁移以添加标签排序字段 - 更新前端组件,支持拖拽调整标签顺序 - 优化样式以提升用户体验
This commit is contained in:
parent
c6bee25264
commit
03860a6dce
@ -3244,11 +3244,65 @@ class ProjectController extends AbstractController
|
||||
'color' => $color
|
||||
]
|
||||
]);
|
||||
$maxSort = ProjectTag::where('project_id', $projectId)->max('sort');
|
||||
$data['sort'] = is_numeric($maxSort) ? intval($maxSort) + 1 : 0;
|
||||
$tag = ProjectTag::create($data);
|
||||
}
|
||||
return Base::retSuccess('保存成功', $tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/project/tag/sort 52.1 标签排序
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目负责人)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName tag__sort
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {Array} list 标签ID列表,按新顺序排列
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function tag__sort()
|
||||
{
|
||||
User::auth();
|
||||
$projectId = intval(Request::input('project_id'));
|
||||
$list = Base::json2array(Request::input('list'));
|
||||
if ($projectId <= 0 || !is_array($list)) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
$project = Project::userProject($projectId, true, true);
|
||||
$index = 0;
|
||||
$handled = [];
|
||||
foreach ($list as $tagId) {
|
||||
$tagId = intval($tagId);
|
||||
if ($tagId <= 0) continue;
|
||||
$updated = ProjectTag::where('project_id', $projectId)
|
||||
->where('id', $tagId)
|
||||
->update(['sort' => $index]);
|
||||
if ($updated) {
|
||||
$handled[] = $tagId;
|
||||
$index++;
|
||||
}
|
||||
}
|
||||
$others = ProjectTag::where('project_id', $projectId)
|
||||
->when(!empty($handled), function ($query) use ($handled) {
|
||||
$query->whereNotIn('id', $handled);
|
||||
})
|
||||
->orderBy('sort')
|
||||
->orderByDesc('id')
|
||||
->pluck('id');
|
||||
foreach ($others as $tagId) {
|
||||
ProjectTag::where('id', $tagId)->update(['sort' => $index]);
|
||||
$index++;
|
||||
}
|
||||
$project->addLog("调整标签排序");
|
||||
return Base::retSuccess('排序已保存');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/tag/delete 52. 删除标签
|
||||
*
|
||||
@ -3329,6 +3383,7 @@ class ProjectController extends AbstractController
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
$tags = ProjectTag::where('project_id', $projectId)
|
||||
->orderBy('sort')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
return Base::retSuccess('success', $tags);
|
||||
|
||||
@ -10,6 +10,7 @@ namespace App\Models;
|
||||
* @property string $name 标签名称
|
||||
* @property string|null $desc 标签描述
|
||||
* @property string|null $color 颜色
|
||||
* @property int $sort 排序
|
||||
* @property int $userid 创建人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
@ -49,6 +50,7 @@ class ProjectTag extends AbstractModel
|
||||
'name',
|
||||
'desc',
|
||||
'color',
|
||||
'sort',
|
||||
'userid'
|
||||
];
|
||||
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddSortToProjectTagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$added = false;
|
||||
Schema::table('project_tags', function (Blueprint $table) use (&$added) {
|
||||
if (!Schema::hasColumn('project_tags', 'sort')) {
|
||||
$table->unsignedInteger('sort')->default(0)->after('color')->comment('排序');
|
||||
$added = true;
|
||||
}
|
||||
});
|
||||
|
||||
if ($added) {
|
||||
\App\Models\ProjectTag::query()
|
||||
->select('project_id')
|
||||
->distinct()
|
||||
->orderBy('project_id')
|
||||
->chunk(100, function ($projectIds) {
|
||||
foreach ($projectIds as $project) {
|
||||
$tags = \App\Models\ProjectTag::query()
|
||||
->where('project_id', $project->project_id)
|
||||
->orderByDesc('id')
|
||||
->get(['id']);
|
||||
$index = 0;
|
||||
foreach ($tags as $tag) {
|
||||
\App\Models\ProjectTag::where('id', $tag->id)->update(['sort' => $index++]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('project_tags', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('project_tags', 'sort')) {
|
||||
$table->dropColumn('sort');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,14 @@
|
||||
<template v-else-if="tags.length > 0">({{tags.length}})</template>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<Button
|
||||
v-if="canSortTags && tags.length"
|
||||
:type="sortMode ? 'primary' : 'default'"
|
||||
:loading="sortLoading"
|
||||
icon="md-move"
|
||||
@click="toggleSortMode">
|
||||
{{$L(sortMode ? '完成排序' : '调整排序')}}
|
||||
</Button>
|
||||
<Button type="primary" icon="md-add" @click="handleAdd(null)">
|
||||
{{$L('新建标签')}}
|
||||
</Button>
|
||||
@ -19,8 +27,27 @@
|
||||
<div class="empty-text">{{$L('当前项目暂无任务标签')}}</div>
|
||||
<Button type="primary" icon="md-add" @click="handleAdd(null)">{{$L('新建标签')}}</Button>
|
||||
</div>
|
||||
<div v-else class="template-list">
|
||||
<div v-for="item in tags" :key="item.id" class="tag-item">
|
||||
<Draggable
|
||||
v-else
|
||||
class="template-list"
|
||||
tag="div"
|
||||
:list="tags"
|
||||
:animation="150"
|
||||
:disabled="!canSortTags || !sortMode || sortLoading"
|
||||
item-key="id"
|
||||
handle=".tag-drag-handle"
|
||||
@end="handleSortEnd">
|
||||
<div
|
||||
v-for="item in tags"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{'is-sorting': sortMode && canSortTags}">
|
||||
<div
|
||||
v-if="sortMode && canSortTags"
|
||||
class="tag-drag-handle"
|
||||
:title="$L('拖拽调整排序')">
|
||||
<Icon type="md-menu" />
|
||||
</div>
|
||||
<div class="tag-contents">
|
||||
<div class="tag-title">
|
||||
<Tags :tags="item"/>
|
||||
@ -28,11 +55,13 @@
|
||||
<div v-if="item.desc" class="tag-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<div class="tag-actions">
|
||||
<div v-if="item.userid === userId || projectData.owner_userid === userId" class="tag-actions-btns">
|
||||
<Button @click="handleAdd(item)" type="primary">
|
||||
<div
|
||||
v-if="item.userid === userId || isProjectOwner"
|
||||
class="tag-actions-btns">
|
||||
<Button :disabled="sortMode" @click="handleAdd(item)" type="primary">
|
||||
{{$L('编辑')}}
|
||||
</Button>
|
||||
<Button @click="handleDelete(item)" type="error">
|
||||
<Button :disabled="sortMode" @click="handleDelete(item)" type="error">
|
||||
{{$L('删除')}}
|
||||
</Button>
|
||||
</div>
|
||||
@ -42,7 +71,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</div>
|
||||
|
||||
<!-- 标签添加/编辑 -->
|
||||
@ -51,7 +80,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapGetters} from 'vuex'
|
||||
import {mapState, mapGetters} from 'vuex';
|
||||
import Draggable from 'vuedraggable';
|
||||
import Tags from "./tags.vue";
|
||||
import TaskTagAdd from "./add.vue";
|
||||
|
||||
@ -59,7 +89,8 @@ export default {
|
||||
name: 'ProjectTaskTag',
|
||||
components: {
|
||||
TaskTagAdd,
|
||||
Tags
|
||||
Tags,
|
||||
Draggable
|
||||
},
|
||||
props: {
|
||||
projectId: {
|
||||
@ -71,11 +102,18 @@ export default {
|
||||
return {
|
||||
loadIng: 0,
|
||||
tags: [],
|
||||
sortMode: false,
|
||||
sortLoading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['projectData']),
|
||||
...mapState(['formOptions'])
|
||||
isProjectOwner() {
|
||||
return this.projectData && this.projectData.owner_userid === this.userId
|
||||
},
|
||||
canSortTags() {
|
||||
return this.isProjectOwner
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getTagData()
|
||||
@ -85,7 +123,7 @@ export default {
|
||||
async getTagData() {
|
||||
this.loadIng++
|
||||
try {
|
||||
const {data} = await this.$store.dispatch("call", {
|
||||
const {data} = await this.$store.dispatch('call', {
|
||||
url: 'project/tag/list',
|
||||
data: {
|
||||
project_id: this.projectId
|
||||
@ -100,6 +138,43 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
toggleSortMode() {
|
||||
if (!this.canSortTags || this.sortLoading) return
|
||||
this.sortMode = !this.sortMode
|
||||
},
|
||||
|
||||
async handleSortEnd(event) {
|
||||
if (!this.sortMode || !this.canSortTags) {
|
||||
return
|
||||
}
|
||||
if (event && event.oldIndex === event.newIndex) {
|
||||
return
|
||||
}
|
||||
const list = this.tags.map(tag => tag.id)
|
||||
if (!list.length) {
|
||||
return
|
||||
}
|
||||
this.sortLoading = true
|
||||
try {
|
||||
const {msg} = await this.$store.dispatch('call', {
|
||||
url: 'project/tag/sort',
|
||||
method: 'post',
|
||||
data: {
|
||||
project_id: this.projectId,
|
||||
list
|
||||
},
|
||||
spinner: 2000
|
||||
})
|
||||
$A.messageSuccess(msg || '排序已保存')
|
||||
await this.getTagData()
|
||||
} catch ({msg}) {
|
||||
$A.messageError(msg || '排序保存失败')
|
||||
await this.getTagData()
|
||||
} finally {
|
||||
this.sortLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 新建、编辑标签
|
||||
handleAdd(tag) {
|
||||
this.$refs.addTag.onOpen(tag)
|
||||
@ -113,7 +188,7 @@ export default {
|
||||
onOk: async () => {
|
||||
this.loadIng++
|
||||
try {
|
||||
const {msg} = await this.$store.dispatch("call", {
|
||||
const {msg} = await this.$store.dispatch('call', {
|
||||
url: 'project/tag/delete',
|
||||
data: {
|
||||
id: tag.id
|
||||
|
||||
@ -31,7 +31,18 @@
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 1px;
|
||||
|
||||
@media (width < 768px) {
|
||||
> button {
|
||||
> span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,6 +120,7 @@
|
||||
}
|
||||
}
|
||||
.tag-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@ -116,6 +128,26 @@
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid #F4F4F5;
|
||||
|
||||
&.is-sorting {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.tag-drag-handle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #b0b3bd;
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
|
||||
.ivu-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-contents {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@ -154,6 +186,10 @@
|
||||
> i {
|
||||
margin: 0 -2px;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,6 +203,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.sortable-drag {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user