mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 18:42:54 +00:00
feat: 添加任务模板排序功能
- 在 ProjectController 中新增 task__template_sort 方法,支持项目任务模板的排序 - 更新前端组件以支持拖拽调整任务模板顺序 - 新增数据库迁移以填充任务模板的排序字段 - 优化样式以提升用户体验
This commit is contained in:
parent
03860a6dce
commit
652dc0953b
@ -2997,6 +2997,7 @@ class ProjectController extends AbstractController
|
|||||||
return Base::retError('参数错误');
|
return Base::retError('参数错误');
|
||||||
}
|
}
|
||||||
$templates = ProjectTaskTemplate::where('project_id', $projectId)
|
$templates = ProjectTaskTemplate::where('project_id', $projectId)
|
||||||
|
->orderBy('sort')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->get();
|
->get();
|
||||||
return Base::retSuccess('success', $templates);
|
return Base::retSuccess('success', $templates);
|
||||||
@ -3060,11 +3061,66 @@ class ProjectController extends AbstractController
|
|||||||
if ($templateCount >= 50) {
|
if ($templateCount >= 50) {
|
||||||
return Base::retError('每个项目最多添加50个模板');
|
return Base::retError('每个项目最多添加50个模板');
|
||||||
}
|
}
|
||||||
$template = ProjectTaskTemplate::create($data);
|
$maxSort = ProjectTaskTemplate::where('project_id', $projectId)->max('sort');
|
||||||
|
$template = ProjectTaskTemplate::create(array_merge($data, [
|
||||||
|
'sort' => is_numeric($maxSort) ? intval($maxSort) + 1 : 0
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
return Base::retSuccess('保存成功', $template);
|
return Base::retSuccess('保存成功', $template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/project/task/template_sort 48.1 排序任务模板
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份(限:项目负责人)
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup project
|
||||||
|
* @apiName task__template_sort
|
||||||
|
*
|
||||||
|
* @apiParam {Number} project_id 项目ID
|
||||||
|
* @apiParam {Array} list 模板ID列表,按新顺序排列
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function task__template_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 $templateId) {
|
||||||
|
$templateId = intval($templateId);
|
||||||
|
if ($templateId <= 0) continue;
|
||||||
|
$updated = ProjectTaskTemplate::where('project_id', $projectId)
|
||||||
|
->where('id', $templateId)
|
||||||
|
->update(['sort' => $index]);
|
||||||
|
if ($updated) {
|
||||||
|
$handled[] = $templateId;
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$others = ProjectTaskTemplate::where('project_id', $projectId)
|
||||||
|
->when(!empty($handled), function ($query) use ($handled) {
|
||||||
|
$query->whereNotIn('id', $handled);
|
||||||
|
})
|
||||||
|
->orderBy('sort')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->pluck('id');
|
||||||
|
foreach ($others as $templateId) {
|
||||||
|
ProjectTaskTemplate::where('id', $templateId)->update(['sort' => $index]);
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
$project->addLog('调整模板排序');
|
||||||
|
return Base::retSuccess('排序已保存');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/project/task/template_delete 49. 删除任务模板
|
* @api {get} api/project/task/template_delete 49. 删除任务模板
|
||||||
*
|
*
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class BackfillSortProjectTaskTemplates extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
if (!Schema::hasTable('project_task_templates') || !Schema::hasColumn('project_task_templates', 'sort')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
\App\Models\ProjectTaskTemplate::query()
|
||||||
|
->select('project_id')
|
||||||
|
->distinct()
|
||||||
|
->orderBy('project_id')
|
||||||
|
->chunk(100, function ($projects) {
|
||||||
|
foreach ($projects as $project) {
|
||||||
|
$templates = \App\Models\ProjectTaskTemplate::query()
|
||||||
|
->where('project_id', $project->project_id)
|
||||||
|
->orderByDesc('id')
|
||||||
|
->get(['id']);
|
||||||
|
$index = 0;
|
||||||
|
foreach ($templates as $template) {
|
||||||
|
\App\Models\ProjectTaskTemplate::where('id', $template->id)->update(['sort' => $index++]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,14 @@
|
|||||||
<Loading v-if="loadIng > 0"/>
|
<Loading v-if="loadIng > 0"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
<Button
|
||||||
|
v-if="templates.length"
|
||||||
|
:type="sortMode ? 'primary' : 'default'"
|
||||||
|
:loading="sortLoading"
|
||||||
|
icon="md-move"
|
||||||
|
@click="toggleSortMode">
|
||||||
|
{{$L(sortMode ? '完成排序' : '调整排序')}}
|
||||||
|
</Button>
|
||||||
<Button type="primary" icon="md-add" @click="handleAdd">
|
<Button type="primary" icon="md-add" @click="handleAdd">
|
||||||
{{$L('新建模板')}}
|
{{$L('新建模板')}}
|
||||||
</Button>
|
</Button>
|
||||||
@ -18,8 +26,29 @@
|
|||||||
<div class="empty-text">{{$L('当前项目暂无任务模板')}}</div>
|
<div class="empty-text">{{$L('当前项目暂无任务模板')}}</div>
|
||||||
<Button type="primary" icon="md-add" @click="handleAdd">{{$L('新建模板')}}</Button>
|
<Button type="primary" icon="md-add" @click="handleAdd">{{$L('新建模板')}}</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="template-list">
|
<Draggable
|
||||||
<div v-for="item in templates" :key="item.id" class="template-item">
|
v-else
|
||||||
|
class="template-list"
|
||||||
|
tag="div"
|
||||||
|
:list="templates"
|
||||||
|
:animation="150"
|
||||||
|
:disabled="!sortMode || sortLoading"
|
||||||
|
item-key="id"
|
||||||
|
handle=".template-drag-handle"
|
||||||
|
@end="handleSortEnd">
|
||||||
|
<div
|
||||||
|
v-for="item in templates"
|
||||||
|
:key="item.id"
|
||||||
|
class="template-item">
|
||||||
|
<div
|
||||||
|
:class="['template-item-inner', {'is-sorting': sortMode}]">
|
||||||
|
<div
|
||||||
|
v-if="sortMode"
|
||||||
|
class="template-drag-handle"
|
||||||
|
:title="$L('拖拽调整排序')">
|
||||||
|
<Icon type="md-menu" />
|
||||||
|
</div>
|
||||||
|
<div class="template-main">
|
||||||
<div class="template-title">
|
<div class="template-title">
|
||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
<span v-if="item.is_default" class="default-tag">{{$L('默认')}}</span>
|
<span v-if="item.is_default" class="default-tag">{{$L('默认')}}</span>
|
||||||
@ -31,19 +60,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="template-actions">
|
<div class="template-actions">
|
||||||
<Button @click="handleSetDefault(item)" type="primary" :icon="item.is_default ? 'md-checkmark' : ''">
|
<Button :disabled="sortMode" @click="handleSetDefault(item)" type="primary" :icon="item.is_default ? 'md-checkmark' : ''">
|
||||||
{{$L(item.is_default ? '取消默认' : '设为默认')}}
|
{{$L(item.is_default ? '取消默认' : '设为默认')}}
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="handleEdit(item)" type="primary">
|
<Button :disabled="sortMode" @click="handleEdit(item)" type="primary">
|
||||||
{{$L('编辑')}}
|
{{$L('编辑')}}
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="handleDelete(item)" type="error">
|
<Button :disabled="sortMode" @click="handleDelete(item)" type="error">
|
||||||
{{$L('删除')}}
|
{{$L('删除')}}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 编辑模板弹窗 -->
|
<!-- 编辑模板弹窗 -->
|
||||||
<Modal
|
<Modal
|
||||||
@ -102,13 +133,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
|
import Draggable from 'vuedraggable';
|
||||||
import VMPreviewNostyle from "../../../../components/VMEditor/nostyle.vue";
|
import VMPreviewNostyle from "../../../../components/VMEditor/nostyle.vue";
|
||||||
import AllTaskTemplates from "./templates";
|
import AllTaskTemplates from "./templates";
|
||||||
import {languageName} from "../../../../language";
|
import {languageName} from "../../../../language";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProjectTaskTemplate',
|
name: 'ProjectTaskTemplate',
|
||||||
components: {VMPreviewNostyle},
|
components: {VMPreviewNostyle, Draggable},
|
||||||
props: {
|
props: {
|
||||||
projectId: {
|
projectId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@ -119,6 +151,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
loadIng: 0,
|
loadIng: 0,
|
||||||
templates: [],
|
templates: [],
|
||||||
|
sortMode: false,
|
||||||
|
sortLoading: false,
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
editingTemplate: this.getEmptyTemplate(),
|
editingTemplate: this.getEmptyTemplate(),
|
||||||
formRules: {
|
formRules: {
|
||||||
@ -161,6 +195,45 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 开启/关闭排序模式
|
||||||
|
toggleSortMode() {
|
||||||
|
if (this.sortLoading) return
|
||||||
|
this.sortMode = !this.sortMode
|
||||||
|
},
|
||||||
|
|
||||||
|
// 拖拽排序完成
|
||||||
|
async handleSortEnd(event) {
|
||||||
|
if (!this.sortMode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event && event.oldIndex === event.newIndex) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const list = this.templates.map(template => template.id)
|
||||||
|
if (!list.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.sortLoading = true
|
||||||
|
try {
|
||||||
|
const {msg} = await this.$store.dispatch('call', {
|
||||||
|
url: 'project/task/template_sort',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
project_id: this.projectId,
|
||||||
|
list
|
||||||
|
},
|
||||||
|
spinner: 2000
|
||||||
|
})
|
||||||
|
$A.messageSuccess(msg || '排序已保存')
|
||||||
|
await this.loadTemplates()
|
||||||
|
} catch ({msg}) {
|
||||||
|
$A.messageError(msg || '排序保存失败')
|
||||||
|
await this.loadTemplates()
|
||||||
|
} finally {
|
||||||
|
this.sortLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 加载模板列表
|
// 加载模板列表
|
||||||
async loadTemplates() {
|
async loadTemplates() {
|
||||||
this.loadIng++
|
this.loadIng++
|
||||||
@ -173,6 +246,9 @@ export default {
|
|||||||
spinner: 3000
|
spinner: 3000
|
||||||
})
|
})
|
||||||
this.templates = data || []
|
this.templates = data || []
|
||||||
|
if (!this.templates.length) {
|
||||||
|
this.sortMode = false
|
||||||
|
}
|
||||||
} catch ({msg}) {
|
} catch ({msg}) {
|
||||||
$A.messageError(msg || '加载模板失败')
|
$A.messageError(msg || '加载模板失败')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -69,8 +69,42 @@
|
|||||||
|
|
||||||
.template-list {
|
.template-list {
|
||||||
.template-item {
|
.template-item {
|
||||||
padding: 16px 0;
|
|
||||||
border-top: 1px solid #F4F4F5;
|
border-top: 1px solid #F4F4F5;
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
.template-item-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
&.is-sorting {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-drag-handle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #9aa5b1;
|
||||||
|
cursor: grab;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ivu-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.template-title {
|
.template-title {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -116,9 +150,17 @@
|
|||||||
> i {
|
> i {
|
||||||
margin: 0 -2px;
|
margin: 0 -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.sortable-drag {
|
||||||
|
border-top-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
.tag-item {
|
.tag-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -206,8 +248,6 @@
|
|||||||
.sortable-drag {
|
.sortable-drag {
|
||||||
border-top-color: transparent;
|
border-top-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-task-template-system {
|
.project-task-template-system {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user