From 3cf705512267031ea98c5de96b7881714ed4f804 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Sat, 27 Sep 2025 15:53:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=85=B3=E8=81=94=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Api/ProjectController.php | 107 ++++++++++++++ app/Models/ProjectTask.php | 128 +++++++++-------- app/Models/ProjectTaskRelation.php | 120 ++++++++++++++++ app/Models/WebSocketDialogMsg.php | 3 + ...00_create_project_task_relations_table.php | 33 +++++ .../js/pages/manage/components/TaskDetail.vue | 95 ++++++++++++ resources/assets/js/store/actions.js | 41 +++++- resources/assets/js/store/mutations.js | 21 +++ resources/assets/js/store/state.js | 1 + .../sass/pages/components/task-detail.scss | 135 +++++++++++++++++- 10 files changed, 623 insertions(+), 61 deletions(-) create mode 100644 app/Models/ProjectTaskRelation.php create mode 100644 database/migrations/2025_09_24_203500_create_project_task_relations_table.php diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 817751cdf..ef966c6aa 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -44,6 +44,7 @@ use App\Models\ProjectTaskFlowChange; use App\Models\ProjectTaskVisibilityUser; use App\Models\ProjectTaskTemplate; use App\Models\ProjectTag; +use App\Models\ProjectTaskRelation; /** * @apiDefine project @@ -1752,6 +1753,112 @@ class ProjectController extends AbstractController 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. 获取任务详细描述 * diff --git a/app/Models/ProjectTask.php b/app/Models/ProjectTask.php index 13a8b4406..3e3749dbc 100644 --- a/app/Models/ProjectTask.php +++ b/app/Models/ProjectTask.php @@ -1560,8 +1560,9 @@ class ProjectTask extends AbstractModel * @param string $action * @param array|self $data 发送内容,默认为[id, parent_id, project_id, column_id, dialog_id] * @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) { return; @@ -1573,6 +1574,7 @@ class ProjectTask extends AbstractModel 'project_id' => $this->project_id, 'column_id' => $this->column_id, 'dialog_id' => $this->dialog_id, + 'visibility' => $this->visibility, ]; } elseif ($data instanceof self) { $data = $data->toArray(); @@ -1583,67 +1585,75 @@ class ProjectTask extends AbstractModel } else { $userids = is_array($userid) ? $userid : [$userid]; } - // - $array = []; - if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) { - $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, - ]) - ]; - } + $userids = array_values(array_unique(array_map('intval', $userids))); + if (empty($userids)) { + return; } - // + + 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) { $params = [ - 'ignoreFd' => Request::header('fd'), + 'ignoreFd' => $ignoreSelf ? Request::header('fd') : null, 'userid' => $item['userid'], 'msg' => [ 'type' => 'projectTask', diff --git a/app/Models/ProjectTaskRelation.php b/app/Models/ProjectTaskRelation.php new file mode 100644 index 000000000..a5403a20e --- /dev/null +++ b/app/Models/ProjectTaskRelation.php @@ -0,0 +1,120 @@ +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>/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); + } + } +} diff --git a/app/Models/WebSocketDialogMsg.php b/app/Models/WebSocketDialogMsg.php index 868531d74..716429299 100644 --- a/app/Models/WebSocketDialogMsg.php +++ b/app/Models/WebSocketDialogMsg.php @@ -8,6 +8,7 @@ use App\Module\Base; use App\Module\Doo; use App\Module\Image; use App\Tasks\PushTask; +use App\Models\ProjectTaskRelation; use App\Exceptions\ApiException; use App\Tasks\WebSocketDialogMsgTask; use Hhxsv5\LaravelS\Swoole\Task\Task; @@ -1332,6 +1333,7 @@ class WebSocketDialogMsg extends AbstractModel ]; $dialogMsg->updateInstance($updateData); $dialogMsg->generateKeyAndSave($search_key); + ProjectTaskRelation::recordMentionsFromMessage($dialogMsg); // WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($sender)->whereHide(1)->change([ 'hide' => 0, // 修改消息时,显示会话(仅自己) @@ -1398,6 +1400,7 @@ class WebSocketDialogMsg extends AbstractModel 'updated_at' => Carbon::now()->toDateTimeString('millisecond'), ]); }); + ProjectTaskRelation::recordMentionsFromMessage($dialogMsg); // $task = new WebSocketDialogMsgTask($dialogMsg->id); if ($push_self) { diff --git a/database/migrations/2025_09_24_203500_create_project_task_relations_table.php b/database/migrations/2025_09_24_203500_create_project_task_relations_table.php new file mode 100644 index 000000000..d5a099d20 --- /dev/null +++ b/database/migrations/2025_09_24_203500_create_project_task_relations_table.php @@ -0,0 +1,33 @@ +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'); + } +}; diff --git a/resources/assets/js/pages/manage/components/TaskDetail.vue b/resources/assets/js/pages/manage/components/TaskDetail.vue index 23564375e..271061c51 100755 --- a/resources/assets/js/pages/manage/components/TaskDetail.vue +++ b/resources/assets/js/pages/manage/components/TaskDetail.vue @@ -334,6 +334,51 @@ + +
+ {{$L('关联任务')}} +
+ +
@@ -597,6 +642,9 @@ export default { loopForce: false, + relatedTasks: [], + relatedRequestKey: 0, + keepInterval: null, keepIntoTimer: null, keepUnix: $A.dayjs().unix(), @@ -668,12 +716,14 @@ export default { }, 1000); // emitter.on('receiveTask', this.onReceiveShow); + emitter.on('taskRelationUpdate', this.onTaskRelationUpdate); }, destroyed() { clearInterval(this.keepInterval); // emitter.off('receiveTask', this.onReceiveShow); + emitter.off('taskRelationUpdate', this.onTaskRelationUpdate); }, computed: { @@ -967,6 +1017,7 @@ export default { handler(id) { if (id > 0) { this.ready = true; + this.loadRelatedTasks(); } else { $A.eeuiAppKeyboardHide() this.timeOpen = false; @@ -978,6 +1029,8 @@ export default { this.addsubForce = false; this.receiveShow = false; this.$refs.chatInput?.hidePopover(); + this.relatedRequestKey++; + this.relatedTasks = []; } }, immediate: true @@ -1508,6 +1561,48 @@ export default { 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) { this.logLoadIng = load }, diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index 4cd06d59a..e8dbc2cc3 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -1138,7 +1138,8 @@ export default { 'microAppsMenus', ], json: [ - 'userInfo' + 'userInfo', + 'taskRelatedCache', ] }; @@ -2458,6 +2459,39 @@ export default { }); }, + /** + * 获取任务关联列表 + * @param state + * @param dispatch + * @param commit + * @param taskId + * @returns {Promise} + */ + 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 @@ -4575,6 +4609,11 @@ export default { case 'recovery': // 恢复(归档) dispatch("saveTask", data) break; + case 'relation': + if (data?.id) { + emitter.emit('taskRelationUpdate', data.id) + } + break; case 'dialog': dispatch("saveTask", data) dispatch("getDialogOne", data.dialog_id).catch(() => {}) diff --git a/resources/assets/js/store/mutations.js b/resources/assets/js/store/mutations.js index c2a01c0b1..56a2100c5 100644 --- a/resources/assets/js/store/mutations.js +++ b/resources/assets/js/store/mutations.js @@ -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) { state.cacheDialogs.push(data) diff --git a/resources/assets/js/store/state.js b/resources/assets/js/store/state.js index 0484ebe94..3be407977 100644 --- a/resources/assets/js/store/state.js +++ b/resources/assets/js/store/state.js @@ -172,6 +172,7 @@ export default { taskFiles: [], taskLogs: [], taskOperation: {}, + taskRelatedCache: {}, taskArchiveView: 0, taskTemplates: [], taskLatestId: 0, diff --git a/resources/assets/sass/pages/components/task-detail.scss b/resources/assets/sass/pages/components/task-detail.scss index e0c58d04a..22ab338d8 100644 --- a/resources/assets/sass/pages/components/task-detail.scss +++ b/resources/assets/sass/pages/components/task-detail.scss @@ -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 { @@ -974,7 +1106,8 @@ body.window-portrait { } .items { .ivu-form-item { - &.item-subtask { + &.item-subtask, + &.item-related-task { display: flex; flex-direction: column; .ivu-form-item-content {