diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index ce0b3f792..2e711360f 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -1467,6 +1467,7 @@ class ProjectController extends AbstractController * @apiParam {Number} task_id 任务ID * @apiParam {String} [name] 任务描述 * @apiParam {Array} [times] 计划时间(格式:开始时间,结束时间;如:2020-01-01 00:00,2020-01-01 23:59) + * @apiParam {String} [loop] 重复周期,数字代表天数(子任务不支持) * @apiParam {Array} [owner] 修改负责人 * @apiParam {String} [content] 任务详情(子任务不支持) * @apiParam {String} [color] 背景色(子任务不支持) diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php index c516f2070..a9689a1e6 100755 --- a/app/Http/Controllers/IndexController.php +++ b/app/Http/Controllers/IndexController.php @@ -9,6 +9,7 @@ use App\Module\RandomColor; use App\Tasks\AutoArchivedTask; use App\Tasks\DeleteTmpTask; use App\Tasks\EmailNoticeTask; +use App\Tasks\LoopTask; use Arr; use Cache; use Hhxsv5\LaravelS\Swoole\Task\Task; @@ -191,6 +192,8 @@ class IndexController extends InvokeController // 删除过期的临时表数据 Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1)); Task::deliver(new DeleteTmpTask('tmp', 24)); + // 周期任务 + Task::deliver(new LoopTask()); return "success"; } diff --git a/app/Models/ProjectTask.php b/app/Models/ProjectTask.php index 592e4d7f4..987ad0936 100644 --- a/app/Models/ProjectTask.php +++ b/app/Models/ProjectTask.php @@ -36,6 +36,8 @@ use Request; * @property string|null $p_name 优先级名称 * @property string|null $p_color 优先级颜色 * @property int|null $sort 排序(ASC) + * @property string|null $loop 重复周期 + * @property string|null $loop_at 下一次重复时间 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $deleted_at @@ -78,6 +80,8 @@ use Request; * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemId($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemName($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoop($value) + * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoopAt($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereName($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePColor($value) * @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePLevel($value) @@ -760,11 +764,21 @@ class ProjectTask extends AbstractModel 'change' => [$oldStringAt, $newStringAt] ]); - //修改计划时间需要重置任务邮件提醒日志 + // 修改计划时间需要重置任务邮件提醒日志 ProjectTaskMailLog::whereTaskId($this->id)->delete(); } // 以下紧顶级任务可修改 if ($this->parent_id === 0) { + // 重复周期 + if (Arr::exists($data, 'loop')) { + $this->loop = $data['loop']; + if (!$this->refreshLoop()) { + throw new ApiException('重复周期选择错误'); + } + } elseif (Arr::exists($data, 'times')) { + // 更新任务时间也要更新重复周期 + $this->refreshLoop(); + } // 协助人员 if (Arr::exists($data, 'assist')) { $array = []; @@ -858,6 +872,105 @@ class ProjectTask extends AbstractModel return true; } + /** + * 刷新重复周期时间 + * @param bool $save 是否执行保存 + * @return bool + */ + public function refreshLoop($save = false) + { + if (!$this->start_at) { + return false; + } + // + $success = true; + $start = Carbon::parse($this->start_at); + if ($start->lt(Carbon::today())) { + // 如果任务开始时间小于今天则重复周期开始时间为今天 + $start = Carbon::parse(date("Y-m-d {$start->toTimeString()}")); + } + switch ($this->loop) { + case "day": + $this->loop_at = $start->addDay(); + break; + case "weekdays": + $this->loop_at = $start->addWeekday(); + break; + case "week": + $this->loop_at = $start->addWeek(); + break; + case "twoweeks": + $this->loop_at = $start->addWeeks(2); + break; + case "month": + $this->loop_at = $start->addMonth(); + break; + case "year": + $this->loop_at = $start->addYear(); + break; + case "never": + $this->loop_at = null; + break; + default: + if (Base::isNumber($this->loop)) { + $this->loop_at = $start->addDays($this->loop); + } else { + $success = false; + } + break; + } + if ($success && $save) { + $this->save(); + } + return $success; + } + + /** + * 复制任务 + * @return self + */ + public function copyTask() + { + if ($this->parent_id > 0) { + throw new ApiException('子任务禁止复制'); + } + return AbstractModel::transaction(function() { + // 复制任务 + $task = $this->replicate(); + $task->dialog_id = 0; + $task->archived_at = null; + $task->archived_userid = 0; + $task->archived_follow = 0; + $task->complete_at = null; + $task->created_at = Carbon::now(); + $task->save(); + // 复制任务内容 + if ($this->content) { + $tmp = $this->content->replicate(); + $tmp->task_id = $task->id; + $tmp->created_at = Carbon::now(); + $tmp->save(); + } + // 复制任务附件 + foreach ($this->taskFile as $taskFile) { + $tmp = $taskFile->replicate(); + $tmp->task_id = $task->id; + $tmp->created_at = Carbon::now(); + $tmp->save(); + } + // 复制任务成员 + foreach ($this->taskUser as $taskUser) { + $tmp = $taskUser->replicate(); + $tmp->task_id = $task->id; + $tmp->task_pid = $task->id; + $tmp->created_at = Carbon::now(); + $tmp->save(); + } + // + return $task; + }); + } + /** * 同步项目成员至聊天室 */ diff --git a/app/Tasks/LoopTask.php b/app/Tasks/LoopTask.php new file mode 100644 index 000000000..4c55f5eb6 --- /dev/null +++ b/app/Tasks/LoopTask.php @@ -0,0 +1,48 @@ +subMinutes(10), + Carbon::now() + ])->chunkById(100, function ($list) { + /** @var ProjectTask $item */ + foreach ($list as $item) { + try { + $task = $item->copyTask(); + if ($item->start_at) { + $diffSecond = Carbon::parse($item->start_at)->diffInSeconds(Carbon::parse($item->end_at), true); + $task->start_at = Carbon::parse($item->loop_at); + $task->end_at = $task->start_at->addSeconds($diffSecond); + } + $task->refreshLoop(true); + $task->addLog("创建任务来自周期任务ID:" . $item->id, [], $item->userid); + // + $item->loop = ''; + $item->loop_at = null; + $item->save(); + } catch (\Throwable $e) { + $item->addLog("生成重复任务失败:" . $e->getMessage(), [], $item->userid); + } + } + }); + } +} diff --git a/database/migrations/2022_06_24_115034_add_project_tasks_loop_loop_at.php b/database/migrations/2022_06_24_115034_add_project_tasks_loop_loop_at.php new file mode 100644 index 000000000..a04cae338 --- /dev/null +++ b/database/migrations/2022_06_24_115034_add_project_tasks_loop_loop_at.php @@ -0,0 +1,36 @@ +timestamp('loop_at')->nullable()->after('sort')->comment('下一次重复时间'); + $table->string('loop', 20)->nullable()->default('')->after('sort')->comment('重复周期'); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('project_tasks', function (Blueprint $table) { + $table->dropColumn("loop_at"); + $table->dropColumn("loop"); + }); + } +} diff --git a/public/css/fonts/taskfont.ttf b/public/css/fonts/taskfont.ttf index e17bde5e0..c29b5f9cc 100644 Binary files a/public/css/fonts/taskfont.ttf and b/public/css/fonts/taskfont.ttf differ diff --git a/public/css/fonts/taskfont.woff b/public/css/fonts/taskfont.woff index 8f621aceb..8719ce888 100644 Binary files a/public/css/fonts/taskfont.woff and b/public/css/fonts/taskfont.woff differ diff --git a/public/css/fonts/taskfont.woff2 b/public/css/fonts/taskfont.woff2 index 028678d6e..7c2672fb9 100644 Binary files a/public/css/fonts/taskfont.woff2 and b/public/css/fonts/taskfont.woff2 differ diff --git a/resources/assets/js/pages/manage/components/TaskDetail.vue b/resources/assets/js/pages/manage/components/TaskDetail.vue index 7e4a2fe5a..6cacfbb2f 100644 --- a/resources/assets/js/pages/manage/components/TaskDetail.vue +++ b/resources/assets/js/pages/manage/components/TaskDetail.vue @@ -275,6 +275,29 @@ + +
+ {{$L('重复周期')}} +
+ +
{{$L('附件')}} @@ -490,6 +513,8 @@ export default { timeValue: [], timeOptions: {shortcuts:$A.timeOptionShortcuts()}, + loopForce: false, + nowTime: $A.Time(), nowInterval: null, @@ -527,6 +552,17 @@ export default { dialogDrag: false, imageAttachment: true, receiveTaskSubscribe: null, + + loops: [ + {key: 'never', label: '从不'}, + {key: 'day', label: '每天'}, + {key: 'weekdays', label: '每个工作日'}, + {key: 'week', label: '每周'}, + {key: 'twoweeks', label: '每两周'}, + {key: 'month', label: '每月'}, + {key: 'year', label: '每年'}, + {key: 'custom', label: '自定义'}, + ] } }, @@ -711,6 +747,13 @@ export default { name: '截止时间', }); } + if (!taskDetail.loop || taskDetail.loop == 'never') { + list.push({ + command: 'loop', + icon: '', + name: '重复周期', + }); + } if (this.fileList.length == 0) { list.push({ command: 'file', @@ -751,6 +794,7 @@ export default { } this.timeOpen = false; this.timeForce = false; + this.loopForce = false; this.assistForce = false; this.addsubForce = false; this.receiveShow = false; @@ -784,6 +828,14 @@ export default { return $A.Date(taskDetail.end_at, true) < this.nowTime; }, + loopLabel(loop) { + const item = this.loops.find(item => item.key === loop) + if (item) { + return item.label + } + return loop ? `每${loop}天` : '从不' + }, + onNameKeydown(e) { if (e.keyCode === 13) { if (!e.shiftKey) { @@ -850,6 +902,14 @@ export default { this.$set(this.taskDetail, 'times', [params.start_at, params.end_at]) break; + case 'loop': + if (params === 'custom') { + this.customLoop() + return; + } + this.$set(this.taskDetail, 'loop', params) + break; + case 'content': const content = this.$refs.desc.getContent(); if (content == this.taskContent) { @@ -883,6 +943,51 @@ export default { }) }, + customLoop() { + let value = this.taskDetail.loop || 1 + $A.Modal.confirm({ + render: (h) => { + return h('div', [ + h('div', { + style: { + fontSize: '16px', + fontWeight: '500', + marginBottom: '20px', + } + }, this.$L('重复周期')), + h('Input', { + style: { + width: '160px', + margin: '0 auto', + }, + props: { + type: 'number', + value, + maxlength: 3 + }, + on: { + input: (val) => { + value = $.runNum(val) + } + } + }, [ + h('span', {slot: 'prepend'}, this.$L('每')), + h('span', {slot: 'append'}, this.$L('天')) + ]) + ]) + }, + onOk: _ => { + this.$Modal.remove() + if (value > 0) { + this.updateData('loop', value) + } + }, + loading: true, + okText: this.$L('确定'), + cancelText: this.$L('取消'), + }); + }, + openOwner() { const list = this.getOwner.map(({userid}) => userid) this.$set(this.taskDetail, 'owner_userid', list) @@ -1070,6 +1175,13 @@ export default { }) break; + case 'loop': + this.loopForce = true; + this.$nextTick(() => { + this.$refs.loop.show(); + }) + break; + case 'file': this.onUploadClick(true) break; diff --git a/resources/assets/sass/pages/components/task-detail.scss b/resources/assets/sass/pages/components/task-detail.scss index e265a5110..b31825f7a 100644 --- a/resources/assets/sass/pages/components/task-detail.scss +++ b/resources/assets/sass/pages/components/task-detail.scss @@ -825,6 +825,12 @@ } } +.task-detail-loop { + > li { + text-align: center; + } +} + @media (max-width: 768px) { .task-detail { .task-info { diff --git a/resources/assets/statics/public/css/fonts/taskfont.ttf b/resources/assets/statics/public/css/fonts/taskfont.ttf index e17bde5e0..c29b5f9cc 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.ttf and b/resources/assets/statics/public/css/fonts/taskfont.ttf differ diff --git a/resources/assets/statics/public/css/fonts/taskfont.woff b/resources/assets/statics/public/css/fonts/taskfont.woff index 8f621aceb..8719ce888 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.woff and b/resources/assets/statics/public/css/fonts/taskfont.woff differ diff --git a/resources/assets/statics/public/css/fonts/taskfont.woff2 b/resources/assets/statics/public/css/fonts/taskfont.woff2 index 028678d6e..7c2672fb9 100644 Binary files a/resources/assets/statics/public/css/fonts/taskfont.woff2 and b/resources/assets/statics/public/css/fonts/taskfont.woff2 differ