mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 02:12:53 +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\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. 获取任务详细描述
|
||||
*
|
||||
|
||||
@ -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',
|
||||
|
||||
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\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) {
|
||||
|
||||
@ -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>
|
||||
</ul>
|
||||
</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>
|
||||
<div v-if="menuList.length > 0" class="add">
|
||||
<div class="add-wrap">
|
||||
@ -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
|
||||
},
|
||||
|
||||
41
resources/assets/js/store/actions.js
vendored
41
resources/assets/js/store/actions.js
vendored
@ -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<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
|
||||
@ -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(() => {})
|
||||
|
||||
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) {
|
||||
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: [],
|
||||
taskLogs: [],
|
||||
taskOperation: {},
|
||||
taskRelatedCache: {},
|
||||
taskArchiveView: 0,
|
||||
taskTemplates: [],
|
||||
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 {
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user