mirror of
https://github.com/kuaifan/dootask.git
synced 2026-02-06 13:15:35 +00:00
feat: 新增任务重复周期
This commit is contained in:
parent
d4d3f1245b
commit
e63e025bda
@ -1467,6 +1467,7 @@ class ProjectController extends AbstractController
|
|||||||
* @apiParam {Number} task_id 任务ID
|
* @apiParam {Number} task_id 任务ID
|
||||||
* @apiParam {String} [name] 任务描述
|
* @apiParam {String} [name] 任务描述
|
||||||
* @apiParam {Array} [times] 计划时间(格式:开始时间,结束时间;如:2020-01-01 00:00,2020-01-01 23:59)
|
* @apiParam {Array} [times] 计划时间(格式:开始时间,结束时间;如:2020-01-01 00:00,2020-01-01 23:59)
|
||||||
|
* @apiParam {String} [loop] 重复周期,数字代表天数(子任务不支持)
|
||||||
* @apiParam {Array} [owner] 修改负责人
|
* @apiParam {Array} [owner] 修改负责人
|
||||||
* @apiParam {String} [content] 任务详情(子任务不支持)
|
* @apiParam {String} [content] 任务详情(子任务不支持)
|
||||||
* @apiParam {String} [color] 背景色(子任务不支持)
|
* @apiParam {String} [color] 背景色(子任务不支持)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ use App\Module\RandomColor;
|
|||||||
use App\Tasks\AutoArchivedTask;
|
use App\Tasks\AutoArchivedTask;
|
||||||
use App\Tasks\DeleteTmpTask;
|
use App\Tasks\DeleteTmpTask;
|
||||||
use App\Tasks\EmailNoticeTask;
|
use App\Tasks\EmailNoticeTask;
|
||||||
|
use App\Tasks\LoopTask;
|
||||||
use Arr;
|
use Arr;
|
||||||
use Cache;
|
use Cache;
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
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('wg_tmp_msgs', 1));
|
||||||
Task::deliver(new DeleteTmpTask('tmp', 24));
|
Task::deliver(new DeleteTmpTask('tmp', 24));
|
||||||
|
// 周期任务
|
||||||
|
Task::deliver(new LoopTask());
|
||||||
|
|
||||||
return "success";
|
return "success";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,8 @@ use Request;
|
|||||||
* @property string|null $p_name 优先级名称
|
* @property string|null $p_name 优先级名称
|
||||||
* @property string|null $p_color 优先级颜色
|
* @property string|null $p_color 优先级颜色
|
||||||
* @property int|null $sort 排序(ASC)
|
* @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 $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property \Illuminate\Support\Carbon|null $deleted_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 whereFlowItemId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemName($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 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 whereName($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePColor($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePColor($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePLevel($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePLevel($value)
|
||||||
@ -760,11 +764,21 @@ class ProjectTask extends AbstractModel
|
|||||||
'change' => [$oldStringAt, $newStringAt]
|
'change' => [$oldStringAt, $newStringAt]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//修改计划时间需要重置任务邮件提醒日志
|
// 修改计划时间需要重置任务邮件提醒日志
|
||||||
ProjectTaskMailLog::whereTaskId($this->id)->delete();
|
ProjectTaskMailLog::whereTaskId($this->id)->delete();
|
||||||
}
|
}
|
||||||
// 以下紧顶级任务可修改
|
// 以下紧顶级任务可修改
|
||||||
if ($this->parent_id === 0) {
|
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')) {
|
if (Arr::exists($data, 'assist')) {
|
||||||
$array = [];
|
$array = [];
|
||||||
@ -858,6 +872,105 @@ class ProjectTask extends AbstractModel
|
|||||||
return true;
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步项目成员至聊天室
|
* 同步项目成员至聊天室
|
||||||
*/
|
*/
|
||||||
|
|||||||
48
app/Tasks/LoopTask.php
Normal file
48
app/Tasks/LoopTask.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tasks;
|
||||||
|
|
||||||
|
use App\Models\ProjectTask;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务重复周期
|
||||||
|
*/
|
||||||
|
class LoopTask extends AbstractTask
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function start()
|
||||||
|
{
|
||||||
|
ProjectTask::whereBetween('loop_at', [
|
||||||
|
Carbon::now()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddProjectTasksLoopLoopAt extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('project_tasks', function (Blueprint $table) {
|
||||||
|
if (!Schema::hasColumn('project_tasks', 'loop')) {
|
||||||
|
$table->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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -275,6 +275,29 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem v-if="(taskDetail.loop && taskDetail.loop != 'never') || loopForce">
|
||||||
|
<div class="item-label" slot="label">
|
||||||
|
<i class="taskfont"></i>{{$L('重复周期')}}
|
||||||
|
</div>
|
||||||
|
<ul class="item-content">
|
||||||
|
<li>
|
||||||
|
<EDropdown
|
||||||
|
ref="loop"
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom"
|
||||||
|
@command="updateData('loop', $event)">
|
||||||
|
<ETooltip :disabled="windowSmall || !taskDetail.loop_at" :content="`${$L('下个周期')}: ${taskDetail.loop_at}`" placement="right">
|
||||||
|
<span>{{$L(loopLabel(taskDetail.loop))}}</span>
|
||||||
|
</ETooltip>
|
||||||
|
<EDropdownMenu slot="dropdown" class="task-detail-loop">
|
||||||
|
<EDropdownItem v-for="item in loops" :key="item.key" :command="item.key">
|
||||||
|
{{$L(item.label)}}
|
||||||
|
</EDropdownItem>
|
||||||
|
</EDropdownMenu>
|
||||||
|
</EDropdown>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</FormItem>
|
||||||
<FormItem v-if="fileList.length > 0">
|
<FormItem v-if="fileList.length > 0">
|
||||||
<div class="item-label" slot="label">
|
<div class="item-label" slot="label">
|
||||||
<i class="taskfont"></i>{{$L('附件')}}
|
<i class="taskfont"></i>{{$L('附件')}}
|
||||||
@ -490,6 +513,8 @@ export default {
|
|||||||
timeValue: [],
|
timeValue: [],
|
||||||
timeOptions: {shortcuts:$A.timeOptionShortcuts()},
|
timeOptions: {shortcuts:$A.timeOptionShortcuts()},
|
||||||
|
|
||||||
|
loopForce: false,
|
||||||
|
|
||||||
nowTime: $A.Time(),
|
nowTime: $A.Time(),
|
||||||
nowInterval: null,
|
nowInterval: null,
|
||||||
|
|
||||||
@ -527,6 +552,17 @@ export default {
|
|||||||
dialogDrag: false,
|
dialogDrag: false,
|
||||||
imageAttachment: true,
|
imageAttachment: true,
|
||||||
receiveTaskSubscribe: null,
|
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: '截止时间',
|
name: '截止时间',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!taskDetail.loop || taskDetail.loop == 'never') {
|
||||||
|
list.push({
|
||||||
|
command: 'loop',
|
||||||
|
icon: '',
|
||||||
|
name: '重复周期',
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.fileList.length == 0) {
|
if (this.fileList.length == 0) {
|
||||||
list.push({
|
list.push({
|
||||||
command: 'file',
|
command: 'file',
|
||||||
@ -751,6 +794,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.timeOpen = false;
|
this.timeOpen = false;
|
||||||
this.timeForce = false;
|
this.timeForce = false;
|
||||||
|
this.loopForce = false;
|
||||||
this.assistForce = false;
|
this.assistForce = false;
|
||||||
this.addsubForce = false;
|
this.addsubForce = false;
|
||||||
this.receiveShow = false;
|
this.receiveShow = false;
|
||||||
@ -784,6 +828,14 @@ export default {
|
|||||||
return $A.Date(taskDetail.end_at, true) < this.nowTime;
|
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) {
|
onNameKeydown(e) {
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
if (!e.shiftKey) {
|
if (!e.shiftKey) {
|
||||||
@ -850,6 +902,14 @@ export default {
|
|||||||
this.$set(this.taskDetail, 'times', [params.start_at, params.end_at])
|
this.$set(this.taskDetail, 'times', [params.start_at, params.end_at])
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'loop':
|
||||||
|
if (params === 'custom') {
|
||||||
|
this.customLoop()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$set(this.taskDetail, 'loop', params)
|
||||||
|
break;
|
||||||
|
|
||||||
case 'content':
|
case 'content':
|
||||||
const content = this.$refs.desc.getContent();
|
const content = this.$refs.desc.getContent();
|
||||||
if (content == this.taskContent) {
|
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() {
|
openOwner() {
|
||||||
const list = this.getOwner.map(({userid}) => userid)
|
const list = this.getOwner.map(({userid}) => userid)
|
||||||
this.$set(this.taskDetail, 'owner_userid', list)
|
this.$set(this.taskDetail, 'owner_userid', list)
|
||||||
@ -1070,6 +1175,13 @@ export default {
|
|||||||
})
|
})
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'loop':
|
||||||
|
this.loopForce = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.loop.show();
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
|
||||||
case 'file':
|
case 'file':
|
||||||
this.onUploadClick(true)
|
this.onUploadClick(true)
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -825,6 +825,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-detail-loop {
|
||||||
|
> li {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.task-detail {
|
.task-detail {
|
||||||
.task-info {
|
.task-info {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user