diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 252c70422..817751cdf 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -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 diff --git a/app/Models/ProjectTask.php b/app/Models/ProjectTask.php index d06b771e4..13a8b4406 100644 --- a/app/Models/ProjectTask.php +++ b/app/Models/ProjectTask.php @@ -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; diff --git a/resources/assets/js/pages/manage/components/TaskMove.vue b/resources/assets/js/pages/manage/components/TaskMove.vue index e2d9e86e6..bd44ad96c 100644 --- a/resources/assets/js/pages/manage/components/TaskMove.vue +++ b/resources/assets/js/pages/manage/components/TaskMove.vue @@ -12,7 +12,7 @@
-
{{ $L('移动前') }}
+
{{ beforeTitle }}
{{$L('状态')}}:
@@ -43,7 +43,7 @@
-
{{ $L('移动后') }}
+
{{ afterTitle }}
{{$L('状态')}}:
- +
@@ -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--; diff --git a/resources/assets/js/pages/manage/components/TaskOperation.vue b/resources/assets/js/pages/manage/components/TaskOperation.vue index 50528bddc..878224a0e 100644 --- a/resources/assets/js/pages/manage/components/TaskOperation.vue +++ b/resources/assets/js/pages/manage/components/TaskOperation.vue @@ -47,7 +47,7 @@