mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-14 21:02:49 +00:00
feat: 添加任务关联功能
This commit is contained in:
parent
aba31eda83
commit
3cf7055122
@ -44,6 +44,7 @@ use App\Models\ProjectTaskFlowChange;
|
|||||||
use App\Models\ProjectTaskVisibilityUser;
|
use App\Models\ProjectTaskVisibilityUser;
|
||||||
use App\Models\ProjectTaskTemplate;
|
use App\Models\ProjectTaskTemplate;
|
||||||
use App\Models\ProjectTag;
|
use App\Models\ProjectTag;
|
||||||
|
use App\Models\ProjectTaskRelation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine project
|
* @apiDefine project
|
||||||
@ -1752,6 +1753,112 @@ class ProjectController extends AbstractController
|
|||||||
return Base::retSuccess('success', $data);
|
return Base::retSuccess('success', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/project/task/related 25.1 获取任务关联任务列表
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup project
|
||||||
|
* @apiName task__related
|
||||||
|
*
|
||||||
|
* @apiParam {Number} task_id 任务ID
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function task__related()
|
||||||
|
{
|
||||||
|
User::auth();
|
||||||
|
$task_id = intval(Request::input('task_id'));
|
||||||
|
if ($task_id <= 0) {
|
||||||
|
return Base::retError('参数错误', ['task_id' => $task_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = ProjectTask::userTask($task_id, null);
|
||||||
|
|
||||||
|
$relations = ProjectTaskRelation::whereTaskId($task->id)
|
||||||
|
->orderByDesc('updated_at')
|
||||||
|
->limit(100)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($relations->isEmpty()) {
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'task_id' => $task->id,
|
||||||
|
'list' => [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$relatedTaskIds = $relations->pluck('related_task_id')->unique()->values();
|
||||||
|
$relatedTasks = [];
|
||||||
|
foreach ($relatedTaskIds as $relatedId) {
|
||||||
|
try {
|
||||||
|
$relatedTask = ProjectTask::userTask($relatedId, null, true, ['project', 'projectColumn']);
|
||||||
|
|
||||||
|
$flowItemParts = explode('|', $relatedTask->flow_item_name ?: '');
|
||||||
|
$flowItemStatus = $flowItemParts[0] ?? '';
|
||||||
|
$flowItemName = $flowItemParts[1] ?? $relatedTask->flow_item_name;
|
||||||
|
$flowItemColor = $flowItemParts[2] ?? '';
|
||||||
|
|
||||||
|
$relatedTask->flow_item_status = $flowItemStatus;
|
||||||
|
$relatedTask->flow_item_name = $flowItemName;
|
||||||
|
$relatedTask->flow_item_color = $flowItemColor;
|
||||||
|
|
||||||
|
$relatedTasks[$relatedTask->id] = $relatedTask;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = [];
|
||||||
|
foreach ($relations as $relation) {
|
||||||
|
$relatedTask = $relatedTasks[$relation->related_task_id] ?? null;
|
||||||
|
if (!$relatedTask) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($list[$relation->related_task_id])) {
|
||||||
|
$list[$relation->related_task_id] = [
|
||||||
|
'task_id' => $relation->task_id,
|
||||||
|
'related_task_id' => $relation->related_task_id,
|
||||||
|
'mention' => false,
|
||||||
|
'mentioned_by' => false,
|
||||||
|
'latest_msg_id' => $relation->msg_id,
|
||||||
|
'latest_at' => $relation->updated_at?->toDateTimeString(),
|
||||||
|
'task' => [
|
||||||
|
'id' => $relatedTask->id,
|
||||||
|
'name' => $relatedTask->name,
|
||||||
|
'project_id' => $relatedTask->project_id,
|
||||||
|
'project_name' => $relatedTask->project?->name,
|
||||||
|
'column_id' => $relatedTask->column_id,
|
||||||
|
'column_name' => $relatedTask->projectColumn?->name,
|
||||||
|
'complete_at' => $relatedTask->complete_at?->toDateTimeString(),
|
||||||
|
'archived_at' => $relatedTask->archived_at?->toDateTimeString(),
|
||||||
|
'flow_item_name' => $relatedTask->flow_item_name,
|
||||||
|
'flow_item_status' => $relatedTask->flow_item_status,
|
||||||
|
'flow_item_color' => $relatedTask->flow_item_color,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($relation->direction === ProjectTaskRelation::DIRECTION_MENTION) {
|
||||||
|
$list[$relation->related_task_id]['mention'] = true;
|
||||||
|
} elseif ($relation->direction === ProjectTaskRelation::DIRECTION_MENTIONED_BY) {
|
||||||
|
$list[$relation->related_task_id]['mentioned_by'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($relation->updated_at && ($list[$relation->related_task_id]['latest_at'] === null || Carbon::parse($list[$relation->related_task_id]['latest_at'])->lt($relation->updated_at))) {
|
||||||
|
$list[$relation->related_task_id]['latest_at'] = $relation->updated_at->toDateTimeString();
|
||||||
|
$list[$relation->related_task_id]['latest_msg_id'] = $relation->msg_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', [
|
||||||
|
'task_id' => $task->id,
|
||||||
|
'list' => array_values($list),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/project/task/content 25. 获取任务详细描述
|
* @api {get} api/project/task/content 25. 获取任务详细描述
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1560,8 +1560,9 @@ class ProjectTask extends AbstractModel
|
|||||||
* @param string $action
|
* @param string $action
|
||||||
* @param array|self $data 发送内容,默认为[id, parent_id, project_id, column_id, dialog_id]
|
* @param array|self $data 发送内容,默认为[id, parent_id, project_id, column_id, dialog_id]
|
||||||
* @param array $userid 指定会员,默认为项目所有成员
|
* @param array $userid 指定会员,默认为项目所有成员
|
||||||
|
* @param bool $ignoreSelf 是否忽略当前连接
|
||||||
*/
|
*/
|
||||||
public function pushMsg($action, $data = null, $userid = null)
|
public function pushMsg($action, $data = null, $userid = null, $ignoreSelf = true)
|
||||||
{
|
{
|
||||||
if (!$this->project) {
|
if (!$this->project) {
|
||||||
return;
|
return;
|
||||||
@ -1573,6 +1574,7 @@ class ProjectTask extends AbstractModel
|
|||||||
'project_id' => $this->project_id,
|
'project_id' => $this->project_id,
|
||||||
'column_id' => $this->column_id,
|
'column_id' => $this->column_id,
|
||||||
'dialog_id' => $this->dialog_id,
|
'dialog_id' => $this->dialog_id,
|
||||||
|
'visibility' => $this->visibility,
|
||||||
];
|
];
|
||||||
} elseif ($data instanceof self) {
|
} elseif ($data instanceof self) {
|
||||||
$data = $data->toArray();
|
$data = $data->toArray();
|
||||||
@ -1583,67 +1585,75 @@ class ProjectTask extends AbstractModel
|
|||||||
} else {
|
} else {
|
||||||
$userids = is_array($userid) ? $userid : [$userid];
|
$userids = is_array($userid) ? $userid : [$userid];
|
||||||
}
|
}
|
||||||
//
|
$userids = array_values(array_unique(array_map('intval', $userids)));
|
||||||
$array = [];
|
if (empty($userids)) {
|
||||||
if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) {
|
return;
|
||||||
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
|
|
||||||
// 负责人
|
|
||||||
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
|
||||||
$owners = array_intersect($userids, $owners);
|
|
||||||
if ($owners) {
|
|
||||||
$array[] = [
|
|
||||||
'userid' => array_values($owners),
|
|
||||||
'data' => array_merge($data, [
|
|
||||||
'owner' => 1,
|
|
||||||
'assist' => 1,
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// 协助人
|
|
||||||
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray();
|
|
||||||
$assists = array_intersect($userids, $assists);
|
|
||||||
if ($assists) {
|
|
||||||
$array[] = [
|
|
||||||
'userid' => array_values($assists),
|
|
||||||
'data' => array_merge($data, [
|
|
||||||
'owner' => 0,
|
|
||||||
'assist' => 1,
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// 其他人
|
|
||||||
switch ($data['visibility']) {
|
|
||||||
case 1:
|
|
||||||
// 项目人员,除了负责人、协助人项目其他人
|
|
||||||
$userids = array_diff($userids, $owners, $assists);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
// 任务人员,除了负责人、协助人
|
|
||||||
$userids = [];
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
// 指定成员
|
|
||||||
$specifys = ProjectTaskVisibilityUser::select(['userid'])->whereTaskId($data['id'])->pluck('userid')->toArray();
|
|
||||||
$userids = array_diff($specifys, $owners, $assists);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$userids = [];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ($userids) {
|
|
||||||
$array[] = [
|
|
||||||
'userid' => array_values($userids),
|
|
||||||
'data' => array_merge($data, [
|
|
||||||
'owner' => 0,
|
|
||||||
'assist' => 0,
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//
|
|
||||||
|
if (!Arr::exists($data, 'visibility')) {
|
||||||
|
$data['visibility'] = $this->visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
$visibility = intval($data['visibility']);
|
||||||
|
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
|
||||||
|
$ownerList = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||||
|
$assistList = $taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||||
|
|
||||||
|
$ownerUsers = array_values(array_intersect($userids, $ownerList));
|
||||||
|
$assistUsers = array_values(array_diff(array_intersect($userids, $assistList), $ownerUsers));
|
||||||
|
|
||||||
|
$array = [];
|
||||||
|
if ($ownerUsers) {
|
||||||
|
$array[] = [
|
||||||
|
'userid' => $ownerUsers,
|
||||||
|
'data' => array_merge($data, [
|
||||||
|
'owner' => 1,
|
||||||
|
'assist' => 1,
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($assistUsers) {
|
||||||
|
$array[] = [
|
||||||
|
'userid' => $assistUsers,
|
||||||
|
'data' => array_merge($data, [
|
||||||
|
'owner' => 0,
|
||||||
|
'assist' => 1,
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$otherUsers = [];
|
||||||
|
switch ($visibility) {
|
||||||
|
case 1:
|
||||||
|
$otherUsers = array_diff($userids, $ownerUsers, $assistUsers);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
$otherUsers = [];
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
$specifys = ProjectTaskVisibilityUser::select(['userid'])->whereTaskId($data['id'])->pluck('userid')->toArray();
|
||||||
|
$otherUsers = array_diff(array_intersect($userids, $specifys), $ownerUsers, $assistUsers);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$otherUsers = array_diff($userids, $ownerUsers, $assistUsers);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($otherUsers) {
|
||||||
|
$array[] = [
|
||||||
|
'userid' => array_values($otherUsers),
|
||||||
|
'data' => $data
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($array)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($array as $item) {
|
foreach ($array as $item) {
|
||||||
$params = [
|
$params = [
|
||||||
'ignoreFd' => Request::header('fd'),
|
'ignoreFd' => $ignoreSelf ? Request::header('fd') : null,
|
||||||
'userid' => $item['userid'],
|
'userid' => $item['userid'],
|
||||||
'msg' => [
|
'msg' => [
|
||||||
'type' => 'projectTask',
|
'type' => 'projectTask',
|
||||||
|
|||||||
120
app/Models/ProjectTaskRelation.php
Normal file
120
app/Models/ProjectTaskRelation.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Module\Base;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ProjectTaskRelation extends AbstractModel
|
||||||
|
{
|
||||||
|
public const DIRECTION_MENTION = 'mention';
|
||||||
|
public const DIRECTION_MENTIONED_BY = 'mentioned_by';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'task_id',
|
||||||
|
'related_task_id',
|
||||||
|
'direction',
|
||||||
|
'dialog_id',
|
||||||
|
'msg_id',
|
||||||
|
'userid',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function task(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProjectTask::class, 'task_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relatedTask(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProjectTask::class, 'related_task_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function recordMentionsFromMessage(WebSocketDialogMsg $msg): void
|
||||||
|
{
|
||||||
|
if ($msg->type !== 'text') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $msg->msg;
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
$payload = Base::json2array($msg->getRawOriginal('msg'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = $payload['text'] ?? '';
|
||||||
|
if (!$text || !preg_match_all('/<span class="mention task" data-id="(\d+)">#?(.*?)<\/span>/i', $text, $matches)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetIds = array_values(array_unique(array_filter(array_map('intval', $matches[1] ?? []))));
|
||||||
|
if (empty($targetIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceTasks = ProjectTask::with('project')->whereDialogId($msg->dialog_id)->get();
|
||||||
|
if ($sourceTasks->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetTasks = ProjectTask::with('project')->whereIn('id', $targetIds)->get()->keyBy('id');
|
||||||
|
if ($targetTasks->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pushTasks = [];
|
||||||
|
foreach ($sourceTasks as $sourceTask) {
|
||||||
|
foreach ($targetIds as $targetId) {
|
||||||
|
if ($targetId === $sourceTask->id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetTask = $targetTasks->get($targetId);
|
||||||
|
if (!$targetTask) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mentionRelation = static::updateOrCreate(
|
||||||
|
[
|
||||||
|
'task_id' => $sourceTask->id,
|
||||||
|
'related_task_id' => $targetTask->id,
|
||||||
|
'direction' => self::DIRECTION_MENTION,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dialog_id' => $msg->dialog_id,
|
||||||
|
'msg_id' => $msg->id,
|
||||||
|
'userid' => $msg->userid,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($mentionRelation->wasRecentlyCreated || $mentionRelation->wasChanged()) {
|
||||||
|
$pushTasks[$sourceTask->id] = $sourceTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reverseRelation = static::updateOrCreate(
|
||||||
|
[
|
||||||
|
'task_id' => $targetTask->id,
|
||||||
|
'related_task_id' => $sourceTask->id,
|
||||||
|
'direction' => self::DIRECTION_MENTIONED_BY,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dialog_id' => $msg->dialog_id,
|
||||||
|
'msg_id' => $msg->id,
|
||||||
|
'userid' => $msg->userid,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($reverseRelation->wasRecentlyCreated || $reverseRelation->wasChanged()) {
|
||||||
|
$pushTasks[$targetTask->id] = $targetTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pushTasks as $task) {
|
||||||
|
$task->loadMissing('project');
|
||||||
|
if (!$task->project) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$task->pushMsg('relation', null, null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ use App\Module\Base;
|
|||||||
use App\Module\Doo;
|
use App\Module\Doo;
|
||||||
use App\Module\Image;
|
use App\Module\Image;
|
||||||
use App\Tasks\PushTask;
|
use App\Tasks\PushTask;
|
||||||
|
use App\Models\ProjectTaskRelation;
|
||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
use App\Tasks\WebSocketDialogMsgTask;
|
use App\Tasks\WebSocketDialogMsgTask;
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||||
@ -1332,6 +1333,7 @@ class WebSocketDialogMsg extends AbstractModel
|
|||||||
];
|
];
|
||||||
$dialogMsg->updateInstance($updateData);
|
$dialogMsg->updateInstance($updateData);
|
||||||
$dialogMsg->generateKeyAndSave($search_key);
|
$dialogMsg->generateKeyAndSave($search_key);
|
||||||
|
ProjectTaskRelation::recordMentionsFromMessage($dialogMsg);
|
||||||
//
|
//
|
||||||
WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($sender)->whereHide(1)->change([
|
WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($sender)->whereHide(1)->change([
|
||||||
'hide' => 0, // 修改消息时,显示会话(仅自己)
|
'hide' => 0, // 修改消息时,显示会话(仅自己)
|
||||||
@ -1398,6 +1400,7 @@ class WebSocketDialogMsg extends AbstractModel
|
|||||||
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
|
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
ProjectTaskRelation::recordMentionsFromMessage($dialogMsg);
|
||||||
//
|
//
|
||||||
$task = new WebSocketDialogMsgTask($dialogMsg->id);
|
$task = new WebSocketDialogMsgTask($dialogMsg->id);
|
||||||
if ($push_self) {
|
if ($push_self) {
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('project_task_relations', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('task_id')->comment('任务ID');
|
||||||
|
$table->unsignedBigInteger('related_task_id')->comment('关联任务ID');
|
||||||
|
$table->string('direction', 32)->default('mention')->comment('关系方向: mention/mentioned_by');
|
||||||
|
$table->unsignedBigInteger('dialog_id')->nullable()->comment('来源会话ID');
|
||||||
|
$table->unsignedBigInteger('msg_id')->nullable()->comment('来源消息ID');
|
||||||
|
$table->unsignedBigInteger('userid')->nullable()->comment('提及人');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['task_id', 'related_task_id', 'direction'], 'project_task_relations_unique');
|
||||||
|
$table->index(['task_id', 'direction']);
|
||||||
|
$table->index('related_task_id');
|
||||||
|
$table->index('dialog_id');
|
||||||
|
$table->index('msg_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('project_task_relations');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -334,6 +334,51 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem v-if="relatedTasks.length > 0" className="item-related-task">
|
||||||
|
<div class="item-label" slot="label">
|
||||||
|
<i class="taskfont"></i>{{$L('关联任务')}}
|
||||||
|
</div>
|
||||||
|
<ul class="item-content related-task">
|
||||||
|
<li
|
||||||
|
v-for="item in relatedTasks"
|
||||||
|
:key="item.related_task_id"
|
||||||
|
class="related-item"
|
||||||
|
@click="openRelatedTask(item)">
|
||||||
|
<span class="related-direction" :class="{
|
||||||
|
inbound: item.mentioned_by,
|
||||||
|
outbound: item.mention,
|
||||||
|
mutual: item.mention && item.mentioned_by
|
||||||
|
}">
|
||||||
|
<Icon v-if="item.mention && item.mentioned_by" type="md-swap"/>
|
||||||
|
<Icon v-else-if="item.mentioned_by" type="md-arrow-round-back"/>
|
||||||
|
<Icon v-else type="md-arrow-round-forward"/>
|
||||||
|
</span>
|
||||||
|
<span class="related-main">
|
||||||
|
<span class="related-id">#{{item.related_task_id}}</span>
|
||||||
|
<span class="related-title">{{item.task.name}}</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="item.task.project_name && item.task.project_id != taskDetail.project_id" class="related-project">{{item.task.project_name}}</span>
|
||||||
|
<span v-if="item.task.column_name" class="related-column">{{item.task.column_name}}</span>
|
||||||
|
<span
|
||||||
|
v-if="item.task.flow_item_name"
|
||||||
|
class="related-status"
|
||||||
|
:class="item.task.flow_item_status"
|
||||||
|
:style="$A.generateColorVarStyle(item.task.flow_item_color, [10], 'flow-item-custom-color')">
|
||||||
|
{{item.task.flow_item_name}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="item.task.complete_at"
|
||||||
|
class="related-status end">
|
||||||
|
{{$L('已完成')}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="item.task.archived_at"
|
||||||
|
class="related-status archived">
|
||||||
|
{{$L('已归档')}}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
<div v-if="menuList.length > 0" class="add">
|
<div v-if="menuList.length > 0" class="add">
|
||||||
<div class="add-wrap">
|
<div class="add-wrap">
|
||||||
@ -597,6 +642,9 @@ export default {
|
|||||||
|
|
||||||
loopForce: false,
|
loopForce: false,
|
||||||
|
|
||||||
|
relatedTasks: [],
|
||||||
|
relatedRequestKey: 0,
|
||||||
|
|
||||||
keepInterval: null,
|
keepInterval: null,
|
||||||
keepIntoTimer: null,
|
keepIntoTimer: null,
|
||||||
keepUnix: $A.dayjs().unix(),
|
keepUnix: $A.dayjs().unix(),
|
||||||
@ -668,12 +716,14 @@ export default {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
//
|
//
|
||||||
emitter.on('receiveTask', this.onReceiveShow);
|
emitter.on('receiveTask', this.onReceiveShow);
|
||||||
|
emitter.on('taskRelationUpdate', this.onTaskRelationUpdate);
|
||||||
},
|
},
|
||||||
|
|
||||||
destroyed() {
|
destroyed() {
|
||||||
clearInterval(this.keepInterval);
|
clearInterval(this.keepInterval);
|
||||||
//
|
//
|
||||||
emitter.off('receiveTask', this.onReceiveShow);
|
emitter.off('receiveTask', this.onReceiveShow);
|
||||||
|
emitter.off('taskRelationUpdate', this.onTaskRelationUpdate);
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@ -967,6 +1017,7 @@ export default {
|
|||||||
handler(id) {
|
handler(id) {
|
||||||
if (id > 0) {
|
if (id > 0) {
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
|
this.loadRelatedTasks();
|
||||||
} else {
|
} else {
|
||||||
$A.eeuiAppKeyboardHide()
|
$A.eeuiAppKeyboardHide()
|
||||||
this.timeOpen = false;
|
this.timeOpen = false;
|
||||||
@ -978,6 +1029,8 @@ export default {
|
|||||||
this.addsubForce = false;
|
this.addsubForce = false;
|
||||||
this.receiveShow = false;
|
this.receiveShow = false;
|
||||||
this.$refs.chatInput?.hidePopover();
|
this.$refs.chatInput?.hidePopover();
|
||||||
|
this.relatedRequestKey++;
|
||||||
|
this.relatedTasks = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
@ -1508,6 +1561,48 @@ export default {
|
|||||||
this.$refs.log.getLists(true);
|
this.$refs.log.getLists(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadRelatedTasks() {
|
||||||
|
if (!this.taskId) {
|
||||||
|
this.relatedTasks = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cacheMap = this.$store.state.taskRelatedCache || {};
|
||||||
|
const cached = cacheMap[this.taskId];
|
||||||
|
if (cached?.list) {
|
||||||
|
this.relatedTasks = cached.list;
|
||||||
|
}
|
||||||
|
const requestKey = ++this.relatedRequestKey;
|
||||||
|
try {
|
||||||
|
const data = await this.$store.dispatch('getTaskRelated', this.taskId);
|
||||||
|
if (requestKey !== this.relatedRequestKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.relatedTasks = data;
|
||||||
|
} catch (e) {
|
||||||
|
if (requestKey === this.relatedRequestKey) {
|
||||||
|
this.relatedTasks = [];
|
||||||
|
}
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openRelatedTask(item) {
|
||||||
|
if (!item || !item.related_task_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.related_task_id === this.taskId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch('openTask', item.related_task_id);
|
||||||
|
},
|
||||||
|
|
||||||
|
onTaskRelationUpdate(taskId) {
|
||||||
|
if (!taskId || taskId !== this.taskId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loadRelatedTasks();
|
||||||
|
},
|
||||||
|
|
||||||
logLoadChange(load) {
|
logLoadChange(load) {
|
||||||
this.logLoadIng = load
|
this.logLoadIng = load
|
||||||
},
|
},
|
||||||
|
|||||||
41
resources/assets/js/store/actions.js
vendored
41
resources/assets/js/store/actions.js
vendored
@ -1138,7 +1138,8 @@ export default {
|
|||||||
'microAppsMenus',
|
'microAppsMenus',
|
||||||
],
|
],
|
||||||
json: [
|
json: [
|
||||||
'userInfo'
|
'userInfo',
|
||||||
|
'taskRelatedCache',
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2458,6 +2459,39 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务关联列表
|
||||||
|
* @param state
|
||||||
|
* @param dispatch
|
||||||
|
* @param commit
|
||||||
|
* @param taskId
|
||||||
|
* @returns {Promise<unknown>}
|
||||||
|
*/
|
||||||
|
getTaskRelated({state, commit, dispatch}, taskId) {
|
||||||
|
taskId = parseInt(taskId, 10);
|
||||||
|
if (!taskId) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
dispatch("call", {
|
||||||
|
url: 'project/task/related',
|
||||||
|
data: {task_id: taskId},
|
||||||
|
}).then(({data}) => {
|
||||||
|
const list = (data.list || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
mention: !!item.mention,
|
||||||
|
mentioned_by: !!item.mentioned_by,
|
||||||
|
}));
|
||||||
|
commit('task/related/save', {
|
||||||
|
taskId,
|
||||||
|
list,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
resolve(list);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加子任务
|
* 添加子任务
|
||||||
* @param dispatch
|
* @param dispatch
|
||||||
@ -4575,6 +4609,11 @@ export default {
|
|||||||
case 'recovery': // 恢复(归档)
|
case 'recovery': // 恢复(归档)
|
||||||
dispatch("saveTask", data)
|
dispatch("saveTask", data)
|
||||||
break;
|
break;
|
||||||
|
case 'relation':
|
||||||
|
if (data?.id) {
|
||||||
|
emitter.emit('taskRelationUpdate', data.id)
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'dialog':
|
case 'dialog':
|
||||||
dispatch("saveTask", data)
|
dispatch("saveTask", data)
|
||||||
dispatch("getDialogOne", data.dialog_id).catch(() => {})
|
dispatch("getDialogOne", data.dialog_id).catch(() => {})
|
||||||
|
|||||||
21
resources/assets/js/store/mutations.js
vendored
21
resources/assets/js/store/mutations.js
vendored
@ -78,6 +78,27 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'task/related/save': function(state, {taskId, list, updatedAt = Date.now()}) {
|
||||||
|
const cache = Object.assign({}, state.taskRelatedCache);
|
||||||
|
cache[taskId] = {
|
||||||
|
list,
|
||||||
|
updated_at: updatedAt,
|
||||||
|
};
|
||||||
|
state.taskRelatedCache = cache;
|
||||||
|
$A.IDBSave("taskRelatedCache", state.taskRelatedCache, 600)
|
||||||
|
},
|
||||||
|
|
||||||
|
'task/related/clear': function(state, taskId) {
|
||||||
|
if (typeof taskId === 'number' || typeof taskId === 'string') {
|
||||||
|
const cache = Object.assign({}, state.taskRelatedCache);
|
||||||
|
delete cache[taskId];
|
||||||
|
state.taskRelatedCache = cache;
|
||||||
|
} else {
|
||||||
|
state.taskRelatedCache = {};
|
||||||
|
}
|
||||||
|
$A.IDBSave("taskRelatedCache", state.taskRelatedCache, 600)
|
||||||
|
},
|
||||||
|
|
||||||
// 对话管理
|
// 对话管理
|
||||||
'dialog/push': function(state, data) {
|
'dialog/push': function(state, data) {
|
||||||
state.cacheDialogs.push(data)
|
state.cacheDialogs.push(data)
|
||||||
|
|||||||
1
resources/assets/js/store/state.js
vendored
1
resources/assets/js/store/state.js
vendored
@ -172,6 +172,7 @@ export default {
|
|||||||
taskFiles: [],
|
taskFiles: [],
|
||||||
taskLogs: [],
|
taskLogs: [],
|
||||||
taskOperation: {},
|
taskOperation: {},
|
||||||
|
taskRelatedCache: {},
|
||||||
taskArchiveView: 0,
|
taskArchiveView: 0,
|
||||||
taskTemplates: [],
|
taskTemplates: [],
|
||||||
taskLatestId: 0,
|
taskLatestId: 0,
|
||||||
|
|||||||
@ -547,6 +547,138 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.related-task {
|
||||||
|
margin-top: 2px;
|
||||||
|
> li {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $primary-text-color;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-title-color;
|
||||||
|
|
||||||
|
.related-title {
|
||||||
|
color: $primary-title-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-direction {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f4f5f5;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
|
||||||
|
.ivu-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.outbound {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inbound {
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mutual {
|
||||||
|
color: #19be6b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
|
||||||
|
.related-id {
|
||||||
|
display: none;
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa0a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-title {
|
||||||
|
color: $primary-text-color;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-project,
|
||||||
|
.related-column {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa0a6;
|
||||||
|
max-width: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-status {
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--flow-item-custom-color-100, $primary-color);
|
||||||
|
border: 1px solid var(--flow-item-custom-color-10, rgba($primary-color, 0.2));
|
||||||
|
background-color: var(--flow-item-custom-color-10, rgba($primary-color, 0.1));
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.start {
|
||||||
|
color: var(--flow-item-custom-color-100, $flow-status-start-color);
|
||||||
|
border-color: var(--flow-item-custom-color-10, rgba($flow-status-start-color, 0.2));
|
||||||
|
background-color: var(--flow-item-custom-color-10, rgba($flow-status-start-color, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.progress {
|
||||||
|
color: var(--flow-item-custom-color-100, $flow-status-progress-color);
|
||||||
|
border-color: var(--flow-item-custom-color-10, rgba($flow-status-progress-color, 0.2));
|
||||||
|
background-color: var(--flow-item-custom-color-10, rgba($flow-status-progress-color, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.test {
|
||||||
|
color: var(--flow-item-custom-color-100, $flow-status-test-color);
|
||||||
|
border-color: var(--flow-item-custom-color-10, rgba($flow-status-test-color, 0.2));
|
||||||
|
background-color: var(--flow-item-custom-color-10, rgba($flow-status-test-color, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.end {
|
||||||
|
color: $primary-color;
|
||||||
|
border-color: rgba($primary-color, 0.2);
|
||||||
|
background-color: rgba($primary-color, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.archived {
|
||||||
|
color: $flow-status-archived-color;
|
||||||
|
border-color: rgba($flow-status-archived-color, 0.2);
|
||||||
|
background-color: rgba($flow-status-archived-color, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ivu-tag {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.visibility-text {
|
.visibility-text {
|
||||||
@ -974,7 +1106,8 @@ body.window-portrait {
|
|||||||
}
|
}
|
||||||
.items {
|
.items {
|
||||||
.ivu-form-item {
|
.ivu-form-item {
|
||||||
&.item-subtask {
|
&.item-subtask,
|
||||||
|
&.item-related-task {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
.ivu-form-item-content {
|
.ivu-form-item-content {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user