feat: 添加任务复制功能

This commit is contained in:
kuaifan 2025-09-24 23:46:21 +08:00
parent 7c5a966944
commit a03dec91c5
4 changed files with 185 additions and 15 deletions

View File

@ -2601,7 +2601,133 @@ class ProjectController extends AbstractController
}
/**
* @api {post} api/project/task/ai_generate 40. 使用 AI 助手生成任务
* @api {post} api/project/task/copy 40. 复制任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__copy
*
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} project_id 目标项目ID
* @apiParam {Number} column_id 目标列表ID
* @apiParam {Number} flow_item_id 工作流id
* @apiParam {Array} owner 负责人
* @apiParam {Array} assist 协助人
* @apiParam {String} [completed] 是否已完成(仅在没有工作流时生效)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__copy()
{
User::auth();
//
$task_id = intval(Request::input('task_id'));
$project_id = intval(Request::input('project_id'));
$column_id = intval(Request::input('column_id'));
$flow_item_id = intval(Request::input('flow_item_id'));
$owner = Request::input('owner', []);
$assist = Request::input('assist', []);
$completed = Request::exists('completed') ? (bool)Request::input('completed') : null;
//
$task = ProjectTask::userTask($task_id);
//
$sourceProject = Project::userProject($task->project_id);
ProjectPermission::userTaskPermission($sourceProject, ProjectPermission::TASK_MOVE, $task);
//
$project = Project::userProject($project_id);
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_ADD);
//
$column = ProjectColumn::whereProjectId($project->id)->whereId($column_id)->first();
if (empty($column)) {
return Base::retError('列表不存在');
}
if (ProjectTask::whereProjectId($project->id)
->whereNull('project_tasks.complete_at')
->whereNull('project_tasks.archived_at')
->count() > 2000) {
return Base::retError('项目内未完成任务最多不能超过2000个');
}
if (ProjectTask::whereColumnId($column->id)
->whereNull('project_tasks.complete_at')
->whereNull('project_tasks.archived_at')
->count() > 500) {
return Base::retError('单个列表未完成任务最多不能超过500个');
}
$flowItem = null;
if ($flow_item_id) {
$flowItem = ProjectFlowItem::whereProjectId($project->id)->whereId($flow_item_id)->first();
if (empty($flowItem)) {
return Base::retError('任务状态不存在');
}
} else {
if (ProjectFlowItem::whereProjectId($project->id)->count() > 0) {
return Base::retError('请选择移动后状态', [], 102);
}
}
//
$projectUserIds = ProjectUser::whereProjectId($project->id)->pluck('userid')->toArray();
$owner = array_values(array_filter(array_unique(array_map('intval', Arr::wrap($owner)))));
$assist = array_values(array_filter(array_unique(array_map('intval', Arr::wrap($assist)))));
$owner = array_values(array_intersect($owner, $projectUserIds));
$assist = array_values(array_diff(array_intersect($assist, $projectUserIds), $owner));
//
$newTask = AbstractModel::transaction(function () use ($task, $project, $column, $flowItem, $owner, $assist, $completed) {
/** @var ProjectTask $task */
$copy = $task->copyTask();
$copy->project_id = $project->id;
$copy->column_id = $column->id;
$copy->sort = intval(ProjectTask::whereColumnId($column->id)->orderByDesc('sort')->value('sort')) + 1;
$copy->flow_item_id = 0;
$copy->flow_item_name = '';
$copy->save();
$copy->load(['content', 'taskFile', 'taskTag', 'taskUser']);
if ($copy->content) {
$copy->content->project_id = $project->id;
$copy->content->save();
}
foreach ($copy->taskFile as $taskFile) {
$taskFile->project_id = $project->id;
$taskFile->save();
}
foreach ($copy->taskTag as $taskTag) {
$taskTag->project_id = $project->id;
$taskTag->save();
}
ProjectTaskUser::whereTaskId($copy->id)->delete();
$copy->setRelation('taskUser', collect());
$copy->setRelation('project', $project);
$updateData = [
'task_id' => $copy->id,
'owner' => $owner,
];
if ($copy->parent_id === 0) {
$updateData['assist'] = $assist;
}
if ($flowItem) {
$updateData['flow_item_id'] = $flowItem->id;
} elseif ($completed !== null) {
$updateData['complete_at'] = $completed ? Carbon::now()->toDateTimeString() : false;
}
$updateMarking = [];
$copy->updateTask($updateData, $updateMarking);
$copy->addLog('复制{任务}', [
'copy_from' => $task->id,
]);
return $copy;
});
//
$data = ProjectTask::oneTask($newTask->id)->toArray();
$data['column_name'] = $column->name;
$data['project_name'] = $project->name;
//
return Base::retSuccess('复制成功', $data);
}
/**
* @api {post} api/project/task/ai_generate 41. 使用 AI 助手生成任务
*
* @apiDescription 需要token身份使用AI根据用户输入和上下文信息生成任务标题和详细描述
* @apiVersion 1.0.0

View File

@ -1143,9 +1143,14 @@ class ProjectTask extends AbstractModel
*/
public function copyTask()
{
return AbstractModel::transaction(function() {
// 复制任务
$task = $this->replicate();
$source = $this->fresh(['content', 'taskFile', 'taskUser']);
if (!$source) {
throw new ApiException('任务不存在');
}
return AbstractModel::transaction(function () use ($source) {
// 复制任务(使用最新数据,避免复制临时字段)
$task = $source->replicate();
$task->dialog_id = 0;
$task->archived_at = null;
$task->archived_userid = 0;
@ -1154,21 +1159,21 @@ class ProjectTask extends AbstractModel
$task->created_at = Carbon::now();
$task->save();
// 复制任务内容
if ($this->content) {
$tmp = $this->content->replicate();
if ($source->content) {
$tmp = $source->content->replicate();
$tmp->task_id = $task->id;
$tmp->created_at = Carbon::now();
$tmp->save();
}
// 复制任务附件
foreach ($this->taskFile as $taskFile) {
foreach ($source->taskFile as $taskFile) {
$tmp = $taskFile->replicate();
$tmp->task_id = $task->id;
$tmp->created_at = Carbon::now();
$tmp->save();
}
// 复制任务成员
foreach ($this->taskUser as $taskUser) {
foreach ($source->taskUser as $taskUser) {
$tmp = $taskUser->replicate();
$tmp->task_id = $task->id;
$tmp->task_pid = $task->id;

View File

@ -12,7 +12,7 @@
<div class="task-move-content">
<div class="task-move-content-old">
<div class="task-move-title">{{ $L('移动前') }}</div>
<div class="task-move-title">{{ beforeTitle }}</div>
<div class="task-move-row">
<span class="label">{{$L('状态')}}:</span>
<div class="flow">
@ -43,7 +43,7 @@
</div>
</div>
<div class="task-move-content-new">
<div class="task-move-title">{{ $L('移动后') }}</div>
<div class="task-move-title">{{ afterTitle }}</div>
<div class="task-move-row">
<span class="label">{{$L('状态')}}:</span>
<TaskMenu
@ -91,7 +91,7 @@
<div class="ivu-modal-footer">
<div class="adaption">
<Button type="default" @click="close">{{$L('取消')}}</Button>
<Button type="primary" :loading="loadIng > 0" @click="onConfirm">{{$L('确定')}}</Button>
<Button type="primary" :loading="loadIng > 0" @click="onConfirm">{{confirmText}}</Button>
</div>
</div>
</div>
@ -117,6 +117,11 @@ export default {
type: Object,
default: false
},
type: {
type: String,
default: "move",
validator: value => ["move", "copy"].includes(value)
},
},
data() {
@ -148,6 +153,18 @@ export default {
computed: {
...mapState(['cacheProjects', 'cacheColumns']),
isCopy() {
return this.type === "copy";
},
beforeTitle() {
return this.$L(this.isCopy ? '复制前' : '移动前');
},
afterTitle() {
return this.$L(this.isCopy ? '复制后' : '移动后');
},
confirmText() {
return this.$L(this.isCopy ? '复制' : '确定');
},
},
watch: {
@ -251,8 +268,8 @@ export default {
},
async onConfirm() {
if (this.task.project_id == this.cascader[0] && this.task.column_id == this.cascader[1]) {
$A.messageError("未变更移动项");
if (!this.isCopy && this.task.project_id == this.cascader[0] && this.task.column_id == this.cascader[1]) {
$A.messageError(this.$L('未变更移动项'));
return;
}
this.loadIng++;
@ -269,7 +286,7 @@ export default {
callData.completed = this.updateData.flow.complete_at ? 1 : 0;
}
this.$store.dispatch("call", {
url: "project/task/move",
url: this.isCopy ? "project/task/copy" : "project/task/move",
data: callData
}).then(({data, msg}) => {
this.loadIng--;

View File

@ -47,7 +47,7 @@
<template v-if="task.parent_id === 0">
<template v-if="operationShow">
<EDropdownItem command="favorite" :divided="turns.length > 0">
<EDropdownItem command="favorite" divided>
<div class="item" :class="{favorited: isFavorited}">
<i class="taskfont movefont">&#xe683;</i>{{$L(isFavorited ? '取消收藏' : '收藏')}}
</div>
@ -67,6 +67,11 @@
<i class="taskfont movefont">&#xe7fc;</i>{{$L('移动')}}
</div>
</EDropdownItem>
<EDropdownItem command="copy">
<div class="item">
<Icon type="ios-copy" />{{$L('复制')}}
</div>
</EDropdownItem>
<EDropdownItem command="remove">
<div class="item hover-del">
<Icon type="md-trash" />{{$L('删除')}}
@ -104,6 +109,19 @@
<TaskMove ref="addTask" v-model="moveTaskShow" :task="task"/>
</Modal>
<!--复制任务-->
<Modal
v-model="copyTaskShow"
:title="$L('复制任务')"
:mask-closable="false"
:styles="{
width: '90%',
maxWidth: '540px'
}"
footer-hide>
<TaskMove v-model="copyTaskShow" :task="task" type="copy"/>
</Modal>
<!-- 发送任务 -->
<Forwarder
ref="forwarder"
@ -145,6 +163,7 @@ export default {
styles: {},
moveTaskShow: false,
copyTaskShow: false,
isFavorited: false,
}
},
@ -331,6 +350,9 @@ export default {
case 'move':
this.moveTaskShow = true;
break;
case 'copy':
this.copyTaskShow = true;
break;
}
},