mirror of
https://github.com/kuaifan/dootask.git
synced 2026-09-08 13:08:54 +00:00
feat(manage): 实现部门负责人视角,支持只读查看部门成员项目与任务
部门负责人/部门管理员可通过系统配置开启,选择管理部门后只读查看 本部门及下级部门成员的全部项目和任务。前端自动根据 department_readonly 标记禁用编辑操作,后端统一注入负责人视角上下文控制数据访问边界。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e0ad8ce6c1
commit
0863e5529a
@ -46,6 +46,7 @@ use App\Models\ProjectTaskTemplate;
|
|||||||
use App\Models\ProjectTag;
|
use App\Models\ProjectTag;
|
||||||
use App\Models\ProjectTaskRelation;
|
use App\Models\ProjectTaskRelation;
|
||||||
use App\Models\ProjectTaskAiEvent;
|
use App\Models\ProjectTaskAiEvent;
|
||||||
|
use App\Models\UserDepartment;
|
||||||
use App\Module\AiTaskSuggestion;
|
use App\Module\AiTaskSuggestion;
|
||||||
use App\Observers\ProjectTaskObserver;
|
use App\Observers\ProjectTaskObserver;
|
||||||
|
|
||||||
@ -128,6 +129,7 @@ class ProjectController extends AbstractController
|
|||||||
public function lists()
|
public function lists()
|
||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
|
$departmentView = UserDepartment::ownerViewContext($user);
|
||||||
//
|
//
|
||||||
$all = Request::input('all');
|
$all = Request::input('all');
|
||||||
$type = Request::input('type', 'all');
|
$type = Request::input('type', 'all');
|
||||||
@ -141,6 +143,9 @@ class ProjectController extends AbstractController
|
|||||||
if ($all) {
|
if ($all) {
|
||||||
$user->identity('admin');
|
$user->identity('admin');
|
||||||
$builder = Project::allData();
|
$builder = Project::allData();
|
||||||
|
} elseif ($departmentView['enabled']) {
|
||||||
|
$projectIds = array_values(array_unique(array_merge($departmentView['own_project_ids'], $departmentView['project_ids'])));
|
||||||
|
$builder = Project::allData()->whereIn('projects.id', $projectIds);
|
||||||
} else {
|
} else {
|
||||||
$builder = Project::authData();
|
$builder = Project::authData();
|
||||||
}
|
}
|
||||||
@ -180,8 +185,9 @@ class ProjectController extends AbstractController
|
|||||||
->orderBy('project_users.sort')
|
->orderBy('project_users.sort')
|
||||||
->orderByDesc('projects.id')
|
->orderByDesc('projects.id')
|
||||||
->paginate(Base::getPaginate(100, 50));
|
->paginate(Base::getPaginate(100, 50));
|
||||||
$list->transform(function (Project $project) use ($getstatistics, $getuserid, $user) {
|
$list->transform(function (Project $project) use ($getstatistics, $getuserid, $user, $departmentView) {
|
||||||
$array = $project->toArray();
|
$array = $project->toArray();
|
||||||
|
$array = UserDepartment::appendDepartmentReadonlyProject($array, $departmentView);
|
||||||
if ($getuserid == 'yes') {
|
if ($getuserid == 'yes') {
|
||||||
$array['userid_list'] = ProjectUser::whereProjectId($project->id)->pluck('userid')->toArray();
|
$array['userid_list'] = ProjectUser::whereProjectId($project->id)->pluck('userid')->toArray();
|
||||||
}
|
}
|
||||||
@ -250,13 +256,15 @@ class ProjectController extends AbstractController
|
|||||||
public function one()
|
public function one()
|
||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
|
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||||
//
|
//
|
||||||
$project_id = intval(Request::input('project_id'));
|
$project_id = intval(Request::input('project_id'));
|
||||||
//
|
//
|
||||||
$project = Project::userProject($project_id);
|
$project = Project::findForDepartmentView($project_id);
|
||||||
$data = array_merge($project->toArray(), $project->getTaskStatistics($user->userid), [
|
$data = array_merge($project->toArray(), $project->getTaskStatistics($user->userid), [
|
||||||
'project_user' => $project->projectUser,
|
'project_user' => $project->projectUser,
|
||||||
]);
|
]);
|
||||||
|
$data = UserDepartment::appendDepartmentReadonlyProject($data, $departmentView);
|
||||||
//
|
//
|
||||||
return Base::retSuccess('success', $data);
|
return Base::retSuccess('success', $data);
|
||||||
}
|
}
|
||||||
@ -999,7 +1007,7 @@ class ProjectController extends AbstractController
|
|||||||
//
|
//
|
||||||
$project_id = intval(Request::input('project_id'));
|
$project_id = intval(Request::input('project_id'));
|
||||||
// 项目
|
// 项目
|
||||||
$project = Project::userProject($project_id);
|
$project = Project::findForDepartmentView($project_id);
|
||||||
//
|
//
|
||||||
$list = ProjectColumn::whereProjectId($project->id)
|
$list = ProjectColumn::whereProjectId($project->id)
|
||||||
->orderBy('sort')
|
->orderBy('sort')
|
||||||
@ -1230,6 +1238,7 @@ class ProjectController extends AbstractController
|
|||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
$userid = $user->userid;
|
$userid = $user->userid;
|
||||||
|
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||||
//
|
//
|
||||||
$parent_id = intval(Request::input('parent_id'));
|
$parent_id = intval(Request::input('parent_id'));
|
||||||
$project_id = intval(Request::input('project_id'));
|
$project_id = intval(Request::input('project_id'));
|
||||||
@ -1294,7 +1303,7 @@ class ProjectController extends AbstractController
|
|||||||
if ($parent_id > 0) {
|
if ($parent_id > 0) {
|
||||||
$isArchived = str_replace(['all', 'yes', 'no'], [null, false, true], $archived);
|
$isArchived = str_replace(['all', 'yes', 'no'], [null, false, true], $archived);
|
||||||
$isDeleted = str_replace(['all', 'yes', 'no'], [null, false, true], $deleted);
|
$isDeleted = str_replace(['all', 'yes', 'no'], [null, false, true], $deleted);
|
||||||
ProjectTask::userTask($parent_id, $isArchived, $isDeleted);
|
ProjectTask::findForDepartmentView($parent_id, $isArchived, $isDeleted);
|
||||||
$scopeAll = true;
|
$scopeAll = true;
|
||||||
$archived = 'all';
|
$archived = 'all';
|
||||||
$builder->where('project_tasks.parent_id', $parent_id);
|
$builder->where('project_tasks.parent_id', $parent_id);
|
||||||
@ -1302,17 +1311,23 @@ class ProjectController extends AbstractController
|
|||||||
$builder->where('project_tasks.parent_id', 0);
|
$builder->where('project_tasks.parent_id', 0);
|
||||||
}
|
}
|
||||||
if ($project_id > 0) {
|
if ($project_id > 0) {
|
||||||
Project::userProject($project_id);
|
if (!UserDepartment::isDepartmentReadonlyProject($departmentView, $project_id)) {
|
||||||
|
Project::userProject($project_id);
|
||||||
|
}
|
||||||
$scopeAll = true;
|
$scopeAll = true;
|
||||||
$builder->where('project_tasks.project_id', $project_id);
|
$builder->where('project_tasks.project_id', $project_id);
|
||||||
}
|
}
|
||||||
if (!$scopeAll && $scope === 'all_project') {
|
if (!$scopeAll && $scope === 'all_project') {
|
||||||
$scopeAll = true;
|
$scopeAll = true;
|
||||||
$builder->whereIn('project_tasks.project_id', function ($query) use ($userid) {
|
if ($departmentView['enabled']) {
|
||||||
$query->select('project_id')
|
$builder->whereIn('project_tasks.project_id', array_values(array_unique(array_merge($departmentView['own_project_ids'], $departmentView['project_ids']))));
|
||||||
->from('project_users')
|
} else {
|
||||||
->where('userid', $userid);
|
$builder->whereIn('project_tasks.project_id', function ($query) use ($userid) {
|
||||||
});
|
$query->select('project_id')
|
||||||
|
->from('project_users')
|
||||||
|
->where('userid', $userid);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($scopeAll) {
|
if ($scopeAll) {
|
||||||
$builder->allData();
|
$builder->allData();
|
||||||
@ -1391,12 +1406,15 @@ class ProjectController extends AbstractController
|
|||||||
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
|
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
|
||||||
$query->where('project_sub_task_visibility_users.userid', $userid);
|
$query->where('project_sub_task_visibility_users.userid', $userid);
|
||||||
});
|
});
|
||||||
$builder->where(function ($query) use ($userid) {
|
$builder->where(function ($query) use ($userid, $departmentView) {
|
||||||
$query->where("project_tasks.visibility", 1);
|
$query->where("project_tasks.visibility", 1);
|
||||||
$query->orWhere("project_users.userid", $userid);
|
$query->orWhere("project_users.userid", $userid);
|
||||||
$query->orWhere("project_task_users.userid", $userid);
|
$query->orWhere("project_task_users.userid", $userid);
|
||||||
$query->orWhere("project_task_visibility_users.userid", $userid);
|
$query->orWhere("project_task_visibility_users.userid", $userid);
|
||||||
$query->orWhere("project_sub_task_visibility_users.userid", $userid);
|
$query->orWhere("project_sub_task_visibility_users.userid", $userid);
|
||||||
|
if ($departmentView['enabled']) {
|
||||||
|
$query->orWhereIn('project_tasks.project_id', $departmentView['project_ids']);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// 优化子查询汇总
|
// 优化子查询汇总
|
||||||
$builder->leftJoinSub(function ($query) {
|
$builder->leftJoinSub(function ($query) {
|
||||||
@ -1439,6 +1457,7 @@ class ProjectController extends AbstractController
|
|||||||
$data = $list->toArray();
|
$data = $list->toArray();
|
||||||
// 还原字段
|
// 还原字段
|
||||||
foreach($data['data'] as &$item){
|
foreach($data['data'] as &$item){
|
||||||
|
$item['department_readonly'] = UserDepartment::isDepartmentReadonlyProject($departmentView, intval($item['project_id']));
|
||||||
$item['file_num'] = $item['_file_num'] ?: 0;
|
$item['file_num'] = $item['_file_num'] ?: 0;
|
||||||
$item['msg_num'] = $item['_msg_num'] ?: 0;
|
$item['msg_num'] = $item['_msg_num'] ?: 0;
|
||||||
$item['sub_num'] = $item['_sub_num'] ?: 0;
|
$item['sub_num'] = $item['_sub_num'] ?: 0;
|
||||||
@ -2018,17 +2037,18 @@ class ProjectController extends AbstractController
|
|||||||
public function task__one()
|
public function task__one()
|
||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
|
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||||
//
|
//
|
||||||
$task_id = intval(Request::input('task_id'));
|
$task_id = intval(Request::input('task_id'));
|
||||||
$archived = Request::input('archived', 'no');
|
$archived = Request::input('archived', 'no');
|
||||||
//
|
//
|
||||||
$isArchived = str_replace(['all', 'yes', 'no'], [null, false, true], $archived);
|
$isArchived = str_replace(['all', 'yes', 'no'], [null, false, true], $archived);
|
||||||
$task = ProjectTask::userTask($task_id, $isArchived, true, ['taskUser', 'taskTag']);
|
$task = ProjectTask::findForDepartmentView($task_id, $isArchived, true, ['taskUser', 'taskTag']);
|
||||||
// 项目可见性
|
// 项目可见性
|
||||||
$projectOwnerids = ProjectUser::whereProjectId($task->project_id)
|
$projectOwnerids = ProjectUser::whereProjectId($task->project_id)
|
||||||
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
|
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
|
||||||
->pluck('userid')->map(fn($v) => (int)$v)->toArray(); // 项目负责人(含项目管理员)
|
->pluck('userid')->map(fn($v) => (int)$v)->toArray(); // 项目负责人(含项目管理员)
|
||||||
if ($task->visibility != 1 && !in_array($user->userid, $projectOwnerids)) {
|
if (!UserDepartment::isDepartmentReadonlyProject($departmentView, intval($task->project_id)) && $task->visibility != 1 && !in_array($user->userid, $projectOwnerids)) {
|
||||||
$taskUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); //任务负责人、协助人
|
$taskUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); //任务负责人、协助人
|
||||||
$subTaskUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); //子任务负责人、协助人
|
$subTaskUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); //子任务负责人、协助人
|
||||||
$visibleUserids = ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid')->toArray(); //可见人
|
$visibleUserids = ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid')->toArray(); //可见人
|
||||||
@ -2039,6 +2059,7 @@ class ProjectController extends AbstractController
|
|||||||
}
|
}
|
||||||
//
|
//
|
||||||
$data = $task->toArray();
|
$data = $task->toArray();
|
||||||
|
$data['department_readonly'] = UserDepartment::isDepartmentReadonlyProject($departmentView, intval($task->project_id));
|
||||||
$data['project_name'] = $task->project?->name;
|
$data['project_name'] = $task->project?->name;
|
||||||
$data['column_name'] = $task->projectColumn?->name;
|
$data['column_name'] = $task->projectColumn?->name;
|
||||||
$data['visibility_appointor'] = $task->visibility == 1 ? [0] : ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid');
|
$data['visibility_appointor'] = $task->visibility == 1 ? [0] : ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid');
|
||||||
@ -2067,7 +2088,7 @@ class ProjectController extends AbstractController
|
|||||||
return Base::retError('参数错误', ['task_id' => $task_id]);
|
return Base::retError('参数错误', ['task_id' => $task_id]);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
$task = ProjectTask::userTask($task_id);
|
$task = ProjectTask::findForDepartmentView($task_id);
|
||||||
//
|
//
|
||||||
return Base::retSuccess('success', [
|
return Base::retSuccess('success', [
|
||||||
'id' => $task->id,
|
'id' => $task->id,
|
||||||
@ -2099,7 +2120,7 @@ class ProjectController extends AbstractController
|
|||||||
return Base::retError('参数错误', ['task_id' => $task_id]);
|
return Base::retError('参数错误', ['task_id' => $task_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$task = ProjectTask::userTask($task_id, null);
|
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||||
|
|
||||||
$relations = ProjectTaskRelation::whereTaskId($task->id)
|
$relations = ProjectTaskRelation::whereTaskId($task->id)
|
||||||
->orderByDesc('updated_at')
|
->orderByDesc('updated_at')
|
||||||
@ -2117,7 +2138,7 @@ class ProjectController extends AbstractController
|
|||||||
$relatedTasks = [];
|
$relatedTasks = [];
|
||||||
foreach ($relatedTaskIds as $relatedId) {
|
foreach ($relatedTaskIds as $relatedId) {
|
||||||
try {
|
try {
|
||||||
$relatedTask = ProjectTask::userTask($relatedId, null, true, ['project', 'projectColumn']);
|
$relatedTask = ProjectTask::findForDepartmentView($relatedId, null, true, ['project', 'projectColumn']);
|
||||||
|
|
||||||
$flowItemParts = explode('|', $relatedTask->flow_item_name ?: '');
|
$flowItemParts = explode('|', $relatedTask->flow_item_name ?: '');
|
||||||
$flowItemStatus = $flowItemParts[0] ?? '';
|
$flowItemStatus = $flowItemParts[0] ?? '';
|
||||||
@ -2243,7 +2264,7 @@ class ProjectController extends AbstractController
|
|||||||
$task_id = intval(Request::input('task_id'));
|
$task_id = intval(Request::input('task_id'));
|
||||||
$history_id = intval(Request::input('history_id'));
|
$history_id = intval(Request::input('history_id'));
|
||||||
//
|
//
|
||||||
$task = ProjectTask::userTask($task_id, null);
|
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||||
//
|
//
|
||||||
if ($history_id > 0) {
|
if ($history_id > 0) {
|
||||||
$taskContent = ProjectTaskContent::whereTaskId($task->id)->whereId($history_id)->first();
|
$taskContent = ProjectTaskContent::whereTaskId($task->id)->whereId($history_id)->first();
|
||||||
@ -2283,7 +2304,7 @@ class ProjectController extends AbstractController
|
|||||||
//
|
//
|
||||||
$task_id = intval(Request::input('task_id'));
|
$task_id = intval(Request::input('task_id'));
|
||||||
//
|
//
|
||||||
$task = ProjectTask::userTask($task_id, null);
|
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||||
//
|
//
|
||||||
$data = ProjectTaskContent::select(['id', 'task_id', 'desc', 'userid', 'created_at'])
|
$data = ProjectTaskContent::select(['id', 'task_id', 'desc', 'userid', 'created_at'])
|
||||||
->whereTaskId($task->id)
|
->whereTaskId($task->id)
|
||||||
@ -2312,7 +2333,7 @@ class ProjectController extends AbstractController
|
|||||||
//
|
//
|
||||||
$task_id = intval(Request::input('task_id'));
|
$task_id = intval(Request::input('task_id'));
|
||||||
//
|
//
|
||||||
$task = ProjectTask::userTask($task_id, null);
|
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||||
//
|
//
|
||||||
return Base::retSuccess('success', $task->taskFile);
|
return Base::retSuccess('success', $task->taskFile);
|
||||||
}
|
}
|
||||||
@ -2401,7 +2422,7 @@ class ProjectController extends AbstractController
|
|||||||
$data = $file->toArray();
|
$data = $file->toArray();
|
||||||
$data['path'] = $file->getRawOriginal('path');
|
$data['path'] = $file->getRawOriginal('path');
|
||||||
//
|
//
|
||||||
ProjectTask::userTask($file->task_id, null);
|
ProjectTask::findForDepartmentView($file->task_id, null);
|
||||||
//
|
//
|
||||||
UserRecentItem::record(
|
UserRecentItem::record(
|
||||||
$user->userid,
|
$user->userid,
|
||||||
@ -2442,7 +2463,7 @@ class ProjectController extends AbstractController
|
|||||||
abort_if(empty($file), 403, "This file not exist.");
|
abort_if(empty($file), 403, "This file not exist.");
|
||||||
//
|
//
|
||||||
try {
|
try {
|
||||||
ProjectTask::userTask($file->task_id, null);
|
ProjectTask::findForDepartmentView($file->task_id, null);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
abort(403, $e->getMessage() ?: "This file not support download.");
|
abort(403, $e->getMessage() ?: "This file not support download.");
|
||||||
}
|
}
|
||||||
@ -3389,7 +3410,7 @@ class ProjectController extends AbstractController
|
|||||||
//
|
//
|
||||||
$project_id = intval(Request::input('project_id'));
|
$project_id = intval(Request::input('project_id'));
|
||||||
//
|
//
|
||||||
$project = Project::userProject($project_id, true);
|
$project = Project::findForDepartmentView($project_id, true);
|
||||||
//
|
//
|
||||||
$list = ProjectFlow::with(['ProjectFlowItem'])->whereProjectId($project->id)->get();
|
$list = ProjectFlow::with(['ProjectFlowItem'])->whereProjectId($project->id)->get();
|
||||||
return Base::retSuccess('success', $list);
|
return Base::retSuccess('success', $list);
|
||||||
@ -3488,10 +3509,10 @@ class ProjectController extends AbstractController
|
|||||||
//
|
//
|
||||||
$builder = ProjectLog::select(["*"]);
|
$builder = ProjectLog::select(["*"]);
|
||||||
if ($task_id > 0) {
|
if ($task_id > 0) {
|
||||||
$task = ProjectTask::userTask($task_id, null);
|
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||||
$builder->whereTaskId($task->id);
|
$builder->whereTaskId($task->id);
|
||||||
} else {
|
} else {
|
||||||
$project = Project::userProject($project_id);
|
$project = Project::findForDepartmentView($project_id);
|
||||||
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
|
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
|
|||||||
@ -94,6 +94,7 @@ class SystemController extends AbstractController
|
|||||||
'unclaimed_task_reminder',
|
'unclaimed_task_reminder',
|
||||||
'unclaimed_task_reminder_time',
|
'unclaimed_task_reminder_time',
|
||||||
'task_ai_auto_analyze',
|
'task_ai_auto_analyze',
|
||||||
|
'department_owner_project_view',
|
||||||
])) {
|
])) {
|
||||||
unset($all[$key]);
|
unset($all[$key]);
|
||||||
}
|
}
|
||||||
@ -148,6 +149,7 @@ class SystemController extends AbstractController
|
|||||||
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
|
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
|
||||||
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
|
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
|
||||||
$setting['task_ai_auto_analyze'] = $setting['task_ai_auto_analyze'] ?: 'open';
|
$setting['task_ai_auto_analyze'] = $setting['task_ai_auto_analyze'] ?: 'open';
|
||||||
|
$setting['department_owner_project_view'] = $setting['department_owner_project_view'] ?: 'close';
|
||||||
$setting['server_timezone'] = config('app.timezone');
|
$setting['server_timezone'] = config('app.timezone');
|
||||||
$setting['server_version'] = Base::getVersion();
|
$setting['server_version'] = Base::getVersion();
|
||||||
//
|
//
|
||||||
|
|||||||
@ -404,9 +404,22 @@ class UsersController extends AbstractController
|
|||||||
$data['nickname_original'] = $user->getRawOriginal('nickname');
|
$data['nickname_original'] = $user->getRawOriginal('nickname');
|
||||||
$data['department_name'] = $user->getDepartmentName();
|
$data['department_name'] = $user->getDepartmentName();
|
||||||
$data['department_owner'] = UserDepartment::where('parent_id',0)->where('owner_userid', $user->userid)->exists(); // 适用默认部门下第1级负责人才能添加部门OKR
|
$data['department_owner'] = UserDepartment::where('parent_id',0)->where('owner_userid', $user->userid)->exists(); // 适用默认部门下第1级负责人才能添加部门OKR
|
||||||
|
$data['managed_departments'] = UserDepartment::getManagedDepartments($user->userid)->toArray();
|
||||||
return Base::retSuccess('success', $data);
|
return Base::retSuccess('success', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/users/info/managed_departments 获取我可切换负责人视角的部门列表
|
||||||
|
*/
|
||||||
|
public function info__managed_departments()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
if (Base::settingFind('system', 'department_owner_project_view', 'close') !== 'open') {
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
|
return Base::retSuccess('success', UserDepartment::getManagedDepartments($user->userid));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/users/info/departments 获取我的部门列表
|
* @api {get} api/users/info/departments 获取我的部门列表
|
||||||
*
|
*
|
||||||
@ -3272,7 +3285,7 @@ class UsersController extends AbstractController
|
|||||||
return Base::retError('参数错误');
|
return Base::retError('参数错误');
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
ProjectTask::userTask($task_id, null, null);
|
ProjectTask::findForDepartmentView($task_id, null, null);
|
||||||
//
|
//
|
||||||
UserTaskBrowse::recordBrowse($user->userid, $task_id);
|
UserTaskBrowse::recordBrowse($user->userid, $task_id);
|
||||||
//
|
//
|
||||||
|
|||||||
@ -723,4 +723,31 @@ class Project extends AbstractModel
|
|||||||
}
|
}
|
||||||
return $project;
|
return $project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目(含部门负责人只读视角兜底)
|
||||||
|
* @param int $project_id
|
||||||
|
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
|
||||||
|
* @param null|bool|string $mustOwner 仅限 null 时尝试部门只读视角
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function findForDepartmentView($project_id, $archived = true, $mustOwner = null)
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||||
|
if (UserDepartment::isDepartmentReadonlyProject($departmentView, intval($project_id)) && $mustOwner === null) {
|
||||||
|
$project = self::allData()->where('projects.id', intval($project_id))->first();
|
||||||
|
if (empty($project)) {
|
||||||
|
throw new ApiException('项目不存在或已被删除', [ 'project_id' => $project_id ], -4001);
|
||||||
|
}
|
||||||
|
if ($archived === true && $project->archived_at != null) {
|
||||||
|
throw new ApiException('项目已归档', [ 'project_id' => $project_id ], -4001);
|
||||||
|
}
|
||||||
|
if ($archived === false && $project->archived_at == null) {
|
||||||
|
throw new ApiException('项目未归档', [ 'project_id' => $project_id ]);
|
||||||
|
}
|
||||||
|
return $project;
|
||||||
|
}
|
||||||
|
return self::userProject($project_id, $archived, $mustOwner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2258,6 +2258,39 @@ class ProjectTask extends AbstractModel
|
|||||||
return $task;
|
return $task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务(含部门负责人只读视角兜底)
|
||||||
|
* @param int $task_id
|
||||||
|
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
|
||||||
|
* @param null|bool $trashed true:仅限未删除, false:仅限已删除, null:不限制
|
||||||
|
* @param array $with
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function findForDepartmentView($task_id, $archived = true, $trashed = true, $with = [])
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||||
|
if ($departmentView['enabled']) {
|
||||||
|
$builder = self::with($with)->allData()->where('project_tasks.id', intval($task_id));
|
||||||
|
if ($trashed === false) {
|
||||||
|
$builder->onlyTrashed();
|
||||||
|
} elseif ($trashed === null) {
|
||||||
|
$builder->withTrashed();
|
||||||
|
}
|
||||||
|
$task = $builder->first();
|
||||||
|
if (!empty($task) && UserDepartment::isDepartmentReadonlyProject($departmentView, intval($task->project_id))) {
|
||||||
|
if ($archived === true && $task->archived_at != null) {
|
||||||
|
throw new ApiException('任务已归档', ['task_id' => $task_id]);
|
||||||
|
}
|
||||||
|
if ($archived === false && $task->archived_at == null) {
|
||||||
|
throw new ApiException('任务未归档', ['task_id' => $task_id]);
|
||||||
|
}
|
||||||
|
return $task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self::userTask($task_id, $archived, $trashed, $with);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建指定周期内的未完成任务查询(用于周报/日报等)
|
* 构建指定周期内的未完成任务查询(用于周报/日报等)
|
||||||
* @param int $userid
|
* @param int $userid
|
||||||
|
|||||||
@ -3,7 +3,9 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Exceptions\ApiException;
|
use App\Exceptions\ApiException;
|
||||||
|
use App\Module\Base;
|
||||||
use Cache;
|
use Cache;
|
||||||
|
use Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\UserDepartment
|
* App\Models\UserDepartment
|
||||||
@ -411,6 +413,93 @@ class UserDepartment extends AbstractModel
|
|||||||
return array_unique($subIds);
|
return array_unique($subIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户可切换负责人视角的部门(正负责人 + 部门管理员)
|
||||||
|
* @param int $userid
|
||||||
|
* @return \Illuminate\Support\Collection
|
||||||
|
*/
|
||||||
|
public static function getManagedDepartments($userid)
|
||||||
|
{
|
||||||
|
$userid = intval($userid);
|
||||||
|
if ($userid <= 0) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
$deputyDepartmentIds = \DB::table('user_department_owners')
|
||||||
|
->where('userid', $userid)
|
||||||
|
->pluck('department_id')
|
||||||
|
->map(fn($v) => intval($v))
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return self::select(['id', 'name', 'parent_id', 'owner_userid'])
|
||||||
|
->where(function ($query) use ($userid, $deputyDepartmentIds) {
|
||||||
|
$query->where('owner_userid', $userid);
|
||||||
|
if ($deputyDepartmentIds) {
|
||||||
|
$query->orWhereIn('id', $deputyDepartmentIds);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户选择的负责人视角部门范围(含所有下级部门)
|
||||||
|
* @param int $userid
|
||||||
|
* @param array|string|null $selectedIds all/空表示全部可管理部门
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getManagedDepartmentScopeIds($userid, $selectedIds = null): array
|
||||||
|
{
|
||||||
|
$managedIds = self::getManagedDepartments($userid)->pluck('id')->map(fn($v) => intval($v))->toArray();
|
||||||
|
if (empty($managedIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if ($selectedIds === 'all' || $selectedIds === null || $selectedIds === '' || $selectedIds === []) {
|
||||||
|
$selected = $managedIds;
|
||||||
|
} else {
|
||||||
|
if (!is_array($selectedIds)) {
|
||||||
|
$selectedIds = explode(',', (string)$selectedIds);
|
||||||
|
}
|
||||||
|
$selected = array_values(array_intersect(
|
||||||
|
array_map('intval', $selectedIds),
|
||||||
|
$managedIds
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (empty($selected)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$scopeIds = [];
|
||||||
|
foreach ($selected as $departmentId) {
|
||||||
|
$scopeIds[] = $departmentId;
|
||||||
|
$scopeIds = array_merge($scopeIds, self::getAllSubDepartmentIds($departmentId));
|
||||||
|
}
|
||||||
|
return array_values(array_unique(array_map('intval', $scopeIds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取负责人视角可管理的成员 userid
|
||||||
|
* @param int $userid
|
||||||
|
* @param array|string|null $selectedIds
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getManagedMemberUserids($userid, $selectedIds = null): array
|
||||||
|
{
|
||||||
|
$departmentIds = self::getManagedDepartmentScopeIds($userid, $selectedIds);
|
||||||
|
if (empty($departmentIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return User::select(['userid'])
|
||||||
|
->where(function ($query) use ($departmentIds) {
|
||||||
|
foreach ($departmentIds as $departmentId) {
|
||||||
|
$query->orWhere('department', 'like', "%,{$departmentId},%");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->pluck('userid')
|
||||||
|
->map(fn($v) => intval($v))
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取部门基本信息(缓存时间1小时)
|
* 获取部门基本信息(缓存时间1小时)
|
||||||
* @param int|array $ids
|
* @param int|array $ids
|
||||||
@ -453,4 +542,70 @@ class UserDepartment extends AbstractModel
|
|||||||
return is_array($ids) ? $result : $result->first();
|
return is_array($ids) ? $result : $result->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门负责人视角上下文(只读)。
|
||||||
|
* $defaultAll=true 用于项目内只读辅助接口兜底:前端漏传部门选择时按全部可管理部门判断。
|
||||||
|
*/
|
||||||
|
public static function ownerViewContext(User $user, bool $defaultAll = false): array
|
||||||
|
{
|
||||||
|
$ids = Request::input('department_owner_ids', Request::input('department_ids'));
|
||||||
|
if (($ids === null || $ids === '') && $defaultAll) {
|
||||||
|
$ids = 'all';
|
||||||
|
}
|
||||||
|
$empty = [
|
||||||
|
'enabled' => false,
|
||||||
|
'member_userids' => [],
|
||||||
|
'project_ids' => [],
|
||||||
|
'project_id_map' => [],
|
||||||
|
'own_project_ids' => [],
|
||||||
|
'own_project_id_map' => [],
|
||||||
|
];
|
||||||
|
if ($ids === null || $ids === '' || Base::settingFind('system', 'department_owner_project_view', 'close') !== 'open') {
|
||||||
|
return $empty;
|
||||||
|
}
|
||||||
|
$memberUserids = self::getManagedMemberUserids($user->userid, $ids);
|
||||||
|
if (empty($memberUserids)) {
|
||||||
|
return $empty;
|
||||||
|
}
|
||||||
|
$projectIds = ProjectUser::whereIn('userid', $memberUserids)
|
||||||
|
->pluck('project_id')
|
||||||
|
->map(fn($v) => intval($v))
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
$ownProjectIds = ProjectUser::whereUserid($user->userid)
|
||||||
|
->pluck('project_id')
|
||||||
|
->map(fn($v) => intval($v))
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
return [
|
||||||
|
'enabled' => !empty($projectIds),
|
||||||
|
'member_userids' => $memberUserids,
|
||||||
|
'project_ids' => $projectIds,
|
||||||
|
'project_id_map' => array_fill_keys($projectIds, true),
|
||||||
|
'own_project_ids' => $ownProjectIds,
|
||||||
|
'own_project_id_map' => array_fill_keys($ownProjectIds, true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断项目是否属于部门只读范围(非本人项目)
|
||||||
|
*/
|
||||||
|
public static function isDepartmentReadonlyProject(array $context, int $projectId): bool
|
||||||
|
{
|
||||||
|
return !empty($context['enabled'])
|
||||||
|
&& isset($context['project_id_map'][$projectId])
|
||||||
|
&& !isset($context['own_project_id_map'][$projectId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为项目数据附加部门只读标记
|
||||||
|
*/
|
||||||
|
public static function appendDepartmentReadonlyProject(array $project, array $context): array
|
||||||
|
{
|
||||||
|
$project['department_readonly'] = self::isDepartmentReadonlyProject($context, intval($project['id']));
|
||||||
|
return $project;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -912,6 +912,12 @@ class WebSocketDialog extends AbstractModel
|
|||||||
if ($projectId > 0 && ProjectUser::whereProjectId($projectId)->whereUserid($userid)->exists()) {
|
if ($projectId > 0 && ProjectUser::whereProjectId($projectId)->whereUserid($userid)->exists()) {
|
||||||
return $dialog;
|
return $dialog;
|
||||||
}
|
}
|
||||||
|
if ($projectId > 0 && $checkOwner === false) {
|
||||||
|
$departmentView = UserDepartment::ownerViewContext(User::auth(), true);
|
||||||
|
if (UserDepartment::isDepartmentReadonlyProject($departmentView, $projectId)) {
|
||||||
|
return $dialog;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'okr':
|
case 'okr':
|
||||||
|
|||||||
@ -73,6 +73,10 @@ export default {
|
|||||||
itemWidth: {
|
itemWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 100
|
default: 100
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -328,6 +332,12 @@ export default {
|
|||||||
this.onDateMove(e.clientX);
|
this.onDateMove(e.clientX);
|
||||||
},
|
},
|
||||||
itemMouseDown(e, item) {
|
itemMouseDown(e, item) {
|
||||||
|
if (this.readonly) {
|
||||||
|
if (e.target.classList.contains('timeline-title')) {
|
||||||
|
this.clickItem(item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onItemMove(item, e.target, e.clientX);
|
this.onItemMove(item, e.target, e.clientX);
|
||||||
},
|
},
|
||||||
@ -347,6 +357,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
onItemMove(item, target, clientX) {
|
onItemMove(item, target, clientX) {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let type = 'moveX';
|
let type = 'moveX';
|
||||||
if (target.classList.contains('timeline-resizer')) {
|
if (target.classList.contains('timeline-resizer')) {
|
||||||
type = 'moveW';
|
type = 'moveW';
|
||||||
@ -384,6 +397,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMoveOver(target) {
|
onMoveOver(target) {
|
||||||
|
if (this.readonly) {
|
||||||
|
this.mouseItem = null;
|
||||||
|
this.dateMove = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.mouseItem != null) {
|
if (this.mouseItem != null) {
|
||||||
const {start, end} = this.mouseItem.time;
|
const {start, end} = this.mouseItem.time;
|
||||||
let isM = false;
|
let isM = false;
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
:option-full="optionFull"
|
:option-full="optionFull"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:placeholderFull="placeholderFull"
|
:placeholderFull="placeholderFull"
|
||||||
:readOnly="windowTouch"
|
:readOnly="readonly || windowTouch"
|
||||||
:readOnlyFull="false"
|
:readOnlyFull="false"
|
||||||
:readOnlyImagePreview="false"
|
:readOnlyImagePreview="false"
|
||||||
@on-blur="onBlur"
|
@on-blur="onBlur"
|
||||||
@ -24,10 +24,10 @@
|
|||||||
transfer>
|
transfer>
|
||||||
<div :style="{userSelect:operateVisible ? 'none' : 'auto', height: operateStyles.height}"></div>
|
<div :style="{userSelect:operateVisible ? 'none' : 'auto', height: operateStyles.height}"></div>
|
||||||
<DropdownMenu slot="list">
|
<DropdownMenu slot="list">
|
||||||
<DropdownItem v-if="operateMenu.checked" @click.native="onLiPreview">{{ $L(operateMenu.checked === 'checked' ? '标记未选' : '标记已选') }}</DropdownItem>
|
<DropdownItem v-if="operateMenu.checked && !readonly" @click.native="onLiPreview">{{ $L(operateMenu.checked === 'checked' ? '标记未选' : '标记已选') }}</DropdownItem>
|
||||||
<DropdownItem v-if="operateMenu.link" @click.native="onLinkPreview">{{ $L('打开链接') }}</DropdownItem>
|
<DropdownItem v-if="operateMenu.link" @click.native="onLinkPreview">{{ $L('打开链接') }}</DropdownItem>
|
||||||
<DropdownItem v-if="operateMenu.img" @click.native="onImagePreview">{{ $L('查看图片') }}</DropdownItem>
|
<DropdownItem v-if="operateMenu.img" @click.native="onImagePreview">{{ $L('查看图片') }}</DropdownItem>
|
||||||
<DropdownItem @click.native="onEditing">{{ $L('编辑描述') }}</DropdownItem>
|
<DropdownItem v-if="!readonly" @click.native="onEditing">{{ $L('编辑描述') }}</DropdownItem>
|
||||||
<DropdownItem v-if="operateMenu.history" @click.native="onHistory">{{ $L('历史记录') }}</DropdownItem>
|
<DropdownItem v-if="operateMenu.history" @click.native="onHistory">{{ $L('历史记录') }}</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@ -99,6 +99,10 @@ export default {
|
|||||||
placeholderFull: {
|
placeholderFull: {
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -202,6 +206,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onEditing() {
|
onEditing() {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$refs.desc.onFull()
|
this.$refs.desc.onFull()
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -210,6 +217,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onBlur() {
|
onBlur() {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$emit('on-blur');
|
this.$emit('on-blur');
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -319,6 +329,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onLiPreview() {
|
onLiPreview() {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.operateMenu.checked) {
|
if (!this.operateMenu.checked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,6 +82,24 @@
|
|||||||
<DropdownItem name="exportCheckin">{{$L('导出签到数据')}}</DropdownItem>
|
<DropdownItem name="exportCheckin">{{$L('导出签到数据')}}</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<!-- 部门负责人视角 -->
|
||||||
|
<DropdownItem
|
||||||
|
v-else-if="item.path === 'departmentOwnerView'"
|
||||||
|
:key="`menu-${index}`"
|
||||||
|
:divided="!!item.divided"
|
||||||
|
:name="item.path"
|
||||||
|
:style="item.style || {}">
|
||||||
|
<div class="manage-menu-flex">
|
||||||
|
<div class="manage-menu-title">
|
||||||
|
{{$L(item.name)}}
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
v-if="item.selectedCount > 0"
|
||||||
|
class="manage-menu-report-badge"
|
||||||
|
:overflow-count="999"
|
||||||
|
:count="item.selectedCount"/>
|
||||||
|
</div>
|
||||||
|
</DropdownItem>
|
||||||
<!-- 其他菜单 -->
|
<!-- 其他菜单 -->
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-else-if="item.visible !== false"
|
v-else-if="item.visible !== false"
|
||||||
@ -146,12 +164,23 @@
|
|||||||
<div class="menu-title">{{item.label}}</div>
|
<div class="menu-title">{{item.label}}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div v-if="ownerProjectTabsVisible" class="owner-project-tabs">
|
||||||
|
<div
|
||||||
|
v-for="item in ownerProjectTabs"
|
||||||
|
:key="item.type"
|
||||||
|
:class="['owner-project-tab', ownerProjectTab === item.type ? 'active' : '']"
|
||||||
|
:title="$L(item.name)"
|
||||||
|
@click="ownerProjectTab = item.type">
|
||||||
|
<span>{{$L(item.name)}}</span>
|
||||||
|
<Badge :overflow-count="999" :count="item.count"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="menuProject" class="menu-project">
|
<div ref="menuProject" class="menu-project">
|
||||||
<Draggable
|
<Draggable
|
||||||
:list="projectDraggableList"
|
:list="projectDraggableList"
|
||||||
:animation="150"
|
:animation="150"
|
||||||
:disabled="$isEEUIApp || windowTouch || !!projectKeyValue"
|
:disabled="$isEEUIApp || windowTouch || !!projectKeyValue || ownerProjectTabsVisible"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
draggable="li:not(.pinned)"
|
draggable="li:not(.pinned)"
|
||||||
@ -170,6 +199,12 @@
|
|||||||
<div class="project-h1">
|
<div class="project-h1">
|
||||||
<em @click.stop="toggleOpenMenu(item.id)"></em>
|
<em @click.stop="toggleOpenMenu(item.id)"></em>
|
||||||
<div class="title" v-html="transformEmojiToHtml(item.name)"></div>
|
<div class="title" v-html="transformEmojiToHtml(item.name)"></div>
|
||||||
|
<ETooltip v-if="item.department_readonly && item.personal" :content="$L('个人项目,只读查看')" placement="right">
|
||||||
|
<UserAvatar class="readonly-owner-avatar" :userid="item.userid" :size="18"/>
|
||||||
|
</ETooltip>
|
||||||
|
<ETooltip v-else-if="item.department_readonly" :content="$L('负责人视角,只读查看')" placement="right">
|
||||||
|
<i class="taskfont readonly-project-avatar"></i>
|
||||||
|
</ETooltip>
|
||||||
<div v-if="item.top_at" class="icon-top"></div>
|
<div v-if="item.top_at" class="icon-top"></div>
|
||||||
<div v-if="item.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div>
|
<div v-if="item.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,7 +221,10 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="projectKeyLoading > 0" class="loading"><Loading/></li>
|
<li v-if="projectKeyLoading > 0 || departmentOwnerProjectsRefreshing" class="loading"><Loading/></li>
|
||||||
|
<li v-else-if="projectLists.length === 0" class="nothing">
|
||||||
|
{{$L(projectKeyValue ? `没有任何与"${projectKeyValue}"相关的结果` : `没有任何项目`)}}
|
||||||
|
</li>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
</div>
|
</div>
|
||||||
</Scrollbar>
|
</Scrollbar>
|
||||||
@ -330,6 +368,9 @@
|
|||||||
<!--弹出 MCP 服务器信息-->
|
<!--弹出 MCP 服务器信息-->
|
||||||
<MCPHelper v-model="mcpHelperShow"/>
|
<MCPHelper v-model="mcpHelperShow"/>
|
||||||
|
|
||||||
|
<!--负责人视角-->
|
||||||
|
<DepartmentOwnerView v-model="departmentOwnerViewShow"/>
|
||||||
|
|
||||||
<!--导出任务统计-->
|
<!--导出任务统计-->
|
||||||
<TaskExport v-model="exportTaskShow"/>
|
<TaskExport v-model="exportTaskShow"/>
|
||||||
|
|
||||||
@ -456,6 +497,7 @@ import transformEmojiToHtml from "../utils/emoji";
|
|||||||
import {languageName} from "../language";
|
import {languageName} from "../language";
|
||||||
import {AINormalizeJsonContent, PROJECT_AI_SYSTEM_PROMPT, withLanguagePreferencePrompt} from "../utils/ai";
|
import {AINormalizeJsonContent, PROJECT_AI_SYSTEM_PROMPT, withLanguagePreferencePrompt} from "../utils/ai";
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
|
import DepartmentOwnerView from "./manage/components/DepartmentOwnerView.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -481,7 +523,8 @@ export default {
|
|||||||
ProjectArchived,
|
ProjectArchived,
|
||||||
MicroApps,
|
MicroApps,
|
||||||
ComplaintManagement,
|
ComplaintManagement,
|
||||||
Draggable
|
Draggable,
|
||||||
|
DepartmentOwnerView
|
||||||
},
|
},
|
||||||
directives: {longpress, TransferDom},
|
directives: {longpress, TransferDom},
|
||||||
data() {
|
data() {
|
||||||
@ -519,6 +562,7 @@ export default {
|
|||||||
|
|
||||||
projectDraggableList: [],
|
projectDraggableList: [],
|
||||||
projectDragging: false,
|
projectDragging: false,
|
||||||
|
ownerProjectTab: 'mine',
|
||||||
|
|
||||||
openMenu: {},
|
openMenu: {},
|
||||||
visibleMenu: false,
|
visibleMenu: false,
|
||||||
@ -550,6 +594,7 @@ export default {
|
|||||||
taskBrowseHistory: [],
|
taskBrowseHistory: [],
|
||||||
|
|
||||||
mcpHelperShow: false,
|
mcpHelperShow: false,
|
||||||
|
departmentOwnerViewShow: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -613,8 +658,10 @@ export default {
|
|||||||
|
|
||||||
'dialogIns',
|
'dialogIns',
|
||||||
'formOptions',
|
'formOptions',
|
||||||
|
'systemConfig',
|
||||||
'mobileTabbar',
|
'mobileTabbar',
|
||||||
'longpressData',
|
'longpressData',
|
||||||
|
'departmentOwnerProjectsRefreshing',
|
||||||
|
|
||||||
'mcpServerStatus',
|
'mcpServerStatus',
|
||||||
'microAppsIds'
|
'microAppsIds'
|
||||||
@ -626,6 +673,14 @@ export default {
|
|||||||
return this.microAppsIds?.includes('ai');
|
return this.microAppsIds?.includes('ai');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
departmentOwnerViewAvailable() {
|
||||||
|
return this.systemConfig.department_owner_project_view === 'open' && (this.userInfo.managed_departments || []).length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
cacheDepartmentOwnerIds() {
|
||||||
|
return (this.$store.state.cacheDepartmentOwnerIds || []).map(id => parseInt(id));
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* page className
|
* page className
|
||||||
* @param mobileTabbar
|
* @param mobileTabbar
|
||||||
@ -763,6 +818,16 @@ export default {
|
|||||||
{path: 'archivedProject', name: '已归档的项目'},
|
{path: 'archivedProject', name: '已归档的项目'},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
if (this.departmentOwnerViewAvailable) {
|
||||||
|
array.push({
|
||||||
|
path: 'departmentOwnerView',
|
||||||
|
name: '负责人视角',
|
||||||
|
divided: !userIsAdmin,
|
||||||
|
visible: true,
|
||||||
|
selected: this.cacheDepartmentOwnerIds.length > 0,
|
||||||
|
selectedCount: this.cacheDepartmentOwnerIds.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
array.push(...[
|
array.push(...[
|
||||||
{path: 'clearCache', name: '清除缓存', divided: true},
|
{path: 'clearCache', name: '清除缓存', divided: true},
|
||||||
{path: 'logout', name: '退出登录', style: {color: '#f40'}}
|
{path: 'logout', name: '退出登录', style: {color: '#f40'}}
|
||||||
@ -787,7 +852,7 @@ export default {
|
|||||||
* 项目列表
|
* 项目列表
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
projectLists() {
|
projectBaseLists() {
|
||||||
const {projectKeyValue, cacheProjects} = this;
|
const {projectKeyValue, cacheProjects} = this;
|
||||||
const data = $A.cloneJSON(cacheProjects).sort((a, b) => {
|
const data = $A.cloneJSON(cacheProjects).sort((a, b) => {
|
||||||
// 置顶优先
|
// 置顶优先
|
||||||
@ -807,6 +872,36 @@ export default {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ownerProjectTabsVisible() {
|
||||||
|
return this.departmentOwnerViewAvailable && this.cacheDepartmentOwnerIds.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
ownerProjectTabs() {
|
||||||
|
return [
|
||||||
|
{type: 'mine', name: '我的项目', count: this.projectBaseLists.filter(item => !item.department_readonly).length},
|
||||||
|
{type: 'readonly', name: '负责人视角', count: this.projectBaseLists.filter(item => item.department_readonly).length},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
routeProjectId() {
|
||||||
|
const {projectId} = this.$route.params;
|
||||||
|
return parseInt(/^\d+$/.test(projectId) ? projectId : 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
routeProject() {
|
||||||
|
if (this.routeProjectId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.cacheProjects.find(({id}) => id == this.routeProjectId) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
projectLists() {
|
||||||
|
if (!this.ownerProjectTabsVisible) {
|
||||||
|
return this.projectBaseLists;
|
||||||
|
}
|
||||||
|
return this.projectBaseLists.filter(item => this.ownerProjectTab === 'readonly' ? item.department_readonly : !item.department_readonly);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 最近打开的任务列表
|
* 最近打开的任务列表
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
@ -884,6 +979,31 @@ export default {
|
|||||||
immediate: true
|
immediate: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ownerProjectTabs: {
|
||||||
|
handler(tabs) {
|
||||||
|
if (!this.ownerProjectTabsVisible) {
|
||||||
|
this.ownerProjectTab = 'mine';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const active = tabs.find(item => item.type === this.ownerProjectTab);
|
||||||
|
if (!active || active.count === 0) {
|
||||||
|
const first = tabs.find(item => item.count > 0);
|
||||||
|
if (first) {
|
||||||
|
this.ownerProjectTab = first.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.syncOwnerProjectTabByRoute();
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
|
||||||
|
routeProject: {
|
||||||
|
handler() {
|
||||||
|
this.syncOwnerProjectTabByRoute();
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
|
||||||
projectLists: {
|
projectLists: {
|
||||||
handler(val) {
|
handler(val) {
|
||||||
if (!this.projectDragging) {
|
if (!this.projectDragging) {
|
||||||
@ -915,6 +1035,12 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
transformEmojiToHtml,
|
transformEmojiToHtml,
|
||||||
|
syncOwnerProjectTabByRoute() {
|
||||||
|
if (!this.ownerProjectTabsVisible || !this.routeProject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.ownerProjectTab = this.routeProject.department_readonly ? 'readonly' : 'mine';
|
||||||
|
},
|
||||||
chackPass() {
|
chackPass() {
|
||||||
if (this.userInfo.changepass === 1) {
|
if (this.userInfo.changepass === 1) {
|
||||||
this.goForward({name: 'manage-setting-password'});
|
this.goForward({name: 'manage-setting-password'});
|
||||||
@ -936,6 +1062,9 @@ export default {
|
|||||||
|
|
||||||
settingRoute(path) {
|
settingRoute(path) {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
|
case 'departmentOwnerView':
|
||||||
|
this.departmentOwnerViewShow = true;
|
||||||
|
return;
|
||||||
case 'allUser':
|
case 'allUser':
|
||||||
this.allUserShow = true;
|
this.allUserShow = true;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:value="value"
|
||||||
|
:title="$L('负责人视角')"
|
||||||
|
:mask-closable="false"
|
||||||
|
width="520"
|
||||||
|
@input="$emit('input', $event)">
|
||||||
|
<div class="department-owner-view-modal">
|
||||||
|
<Alert type="info" show-icon>
|
||||||
|
{{$L('可查看所选部门及所有下级部门成员参与的项目和任务,仅支持只读查看。')}}
|
||||||
|
</Alert>
|
||||||
|
<div v-if="managedDepartments.length > 1" class="department-owner-view-actions">
|
||||||
|
<a href="javascript:void(0)" @click="draftIds=[]">{{$L('清空')}}</a>
|
||||||
|
<a href="javascript:void(0)" @click="draftIds=managedDepartments.map(item => item.id)">{{$L('全选')}}</a>
|
||||||
|
<a href="javascript:void(0)" @click="reverseDraft">{{$L('反选')}}</a>
|
||||||
|
</div>
|
||||||
|
<CheckboxGroup v-model="draftIds" class="department-owner-view-list">
|
||||||
|
<div
|
||||||
|
v-for="dept in managedDepartments"
|
||||||
|
:key="dept.id"
|
||||||
|
:class="['department-owner-view-item', draftIds.includes(dept.id) ? 'active' : '']"
|
||||||
|
@click="toggleDraft(dept.id)">
|
||||||
|
<div class="department-owner-view-icon">
|
||||||
|
<i class="taskfont"></i>
|
||||||
|
</div>
|
||||||
|
<div class="department-owner-view-name">{{dept.name}}</div>
|
||||||
|
<Checkbox class="department-owner-view-checkbox" :label="dept.id" @click.native.stop><span></span></Checkbox>
|
||||||
|
</div>
|
||||||
|
</CheckboxGroup>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="adaption">
|
||||||
|
<Button type="default" :disabled="applyLoading" @click="$emit('input', false)">{{$L('取消')}}</Button>
|
||||||
|
<Button type="primary" :loading="applyLoading" @click="apply">{{$L('确定')}}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DepartmentOwnerView",
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
draftIds: [],
|
||||||
|
applyLoading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['userInfo', 'cacheDepartmentOwnerIds']),
|
||||||
|
managedDepartments() {
|
||||||
|
return (this.userInfo.managed_departments || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
id: parseInt(item.id)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
immediate: true,
|
||||||
|
handler(show) {
|
||||||
|
if (show) {
|
||||||
|
this.draftIds = (this.cacheDepartmentOwnerIds || []).map(id => parseInt(id));
|
||||||
|
} else {
|
||||||
|
this.applyLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleDraft(id) {
|
||||||
|
id = parseInt(id);
|
||||||
|
const index = this.draftIds.indexOf(id);
|
||||||
|
if (index > -1) {
|
||||||
|
this.draftIds.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.draftIds.push(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reverseDraft() {
|
||||||
|
const selected = this.draftIds.map(id => parseInt(id));
|
||||||
|
this.draftIds = this.managedDepartments
|
||||||
|
.map(item => item.id)
|
||||||
|
.filter(id => !selected.includes(id));
|
||||||
|
},
|
||||||
|
async apply() {
|
||||||
|
if (this.applyLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.applyLoading = true;
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch("setDepartmentOwnerIds", this.draftIds);
|
||||||
|
this.$emit('input', false);
|
||||||
|
} catch (e) {
|
||||||
|
$A.modalError(e?.msg || this.$L('切换失败'));
|
||||||
|
} finally {
|
||||||
|
this.applyLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.department-owner-view-modal {
|
||||||
|
.department-owner-view-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 14px;
|
||||||
|
margin: 12px 8px 0;
|
||||||
|
}
|
||||||
|
.department-owner-view-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.department-owner-view-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.department-owner-view-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #5BC7B0;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.department-owner-view-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.department-owner-view-checkbox {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -5,7 +5,8 @@
|
|||||||
:menuWidth="menuWidth"
|
:menuWidth="menuWidth"
|
||||||
:itemWidth="80"
|
:itemWidth="80"
|
||||||
@on-change="onChange"
|
@on-change="onChange"
|
||||||
@on-click="onClick">
|
@on-click="onClick"
|
||||||
|
:readonly="readonly">
|
||||||
<template #titleTool>
|
<template #titleTool>
|
||||||
<Dropdown class="project-gstc-dropdown-filtr" trigger="click" @on-click="onSwitchColumn">
|
<Dropdown class="project-gstc-dropdown-filtr" trigger="click" @on-click="onSwitchColumn">
|
||||||
<Icon class="project-gstc-dropdown-icon" :class="{filtr:filtrProjectId > 0}" type="md-funnel" />
|
<Icon class="project-gstc-dropdown-icon" :class="{filtr:filtrProjectId > 0}" type="md-funnel" />
|
||||||
@ -55,6 +56,10 @@ export default {
|
|||||||
flowInfo: {
|
flowInfo: {
|
||||||
default: {}
|
default: {}
|
||||||
},
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -215,6 +220,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onChange(item) {
|
onChange(item) {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const {time, baktime} = item;
|
const {time, baktime} = item;
|
||||||
if (Math.abs(baktime.end - time.end) > 1000 || Math.abs(baktime.start - time.start) > 1000) {
|
if (Math.abs(baktime.end - time.end) > 1000 || Math.abs(baktime.start - time.start) > 1000) {
|
||||||
//修改时间(变化超过1秒钟)
|
//修改时间(变化超过1秒钟)
|
||||||
@ -238,6 +246,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
editSubmit(save) {
|
editSubmit(save) {
|
||||||
|
if (this.readonly) {
|
||||||
|
this.editData = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.editData && this.editData.forEach(item => {
|
this.editData && this.editData.forEach(item => {
|
||||||
let task = this.lists.find(({id}) => id == item.id)
|
let task = this.lists.find(({id}) => id == item.id)
|
||||||
if (save) {
|
if (save) {
|
||||||
|
|||||||
@ -11,11 +11,31 @@
|
|||||||
<Input type="search" v-model="projectKeyValue" :placeholder="$L(loadProjects > 0 ? '更新中...' : '搜索')" clearable/>
|
<Input type="search" v-model="projectKeyValue" :placeholder="$L(loadProjects > 0 ? '更新中...' : '搜索')" clearable/>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="ownerViewAvailable"
|
||||||
|
class="owner-view-button"
|
||||||
|
@click="departmentOwnerViewShow=true">
|
||||||
|
<i class="taskfont"></i>
|
||||||
|
<em v-if="ownerDepartmentIds.length > 0">{{ownerDepartmentIds.length}}</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="owner-project-wrapper">
|
||||||
|
<div v-if="ownerProjectTabsVisible" class="owner-project-tabs">
|
||||||
|
<div
|
||||||
|
v-for="item in ownerProjectTabs"
|
||||||
|
:key="item.type"
|
||||||
|
:class="['owner-project-tab', ownerProjectTab === item.type ? 'active' : '']"
|
||||||
|
:title="$L(item.name)"
|
||||||
|
@click="ownerProjectTab = item.type">
|
||||||
|
<span>{{$L(item.name)}}</span>
|
||||||
|
<Badge :overflow-count="999" :count="item.count"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Draggable
|
<Draggable
|
||||||
:list="projectDraggableList"
|
:list="projectDraggableList"
|
||||||
:animation="150"
|
:animation="150"
|
||||||
:disabled="!(isDragging && !projectKeyValue)"
|
:disabled="!(isDragging && !projectKeyValue) || ownerProjectTabsVisible"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
draggable="li:not(.pinned)"
|
draggable="li:not(.pinned)"
|
||||||
@ -36,6 +56,12 @@
|
|||||||
<div class="item-left">
|
<div class="item-left">
|
||||||
<div class="project-h1">
|
<div class="project-h1">
|
||||||
<div class="project-name" v-html="transformEmojiToHtml(item.name)"></div>
|
<div class="project-name" v-html="transformEmojiToHtml(item.name)"></div>
|
||||||
|
<ETooltip v-if="item.department_readonly && item.personal" :content="$L('个人项目,只读查看')" placement="right">
|
||||||
|
<UserAvatar class="readonly-owner-avatar" :userid="item.userid" :size="18"/>
|
||||||
|
</ETooltip>
|
||||||
|
<ETooltip v-else-if="item.department_readonly" :content="$L('负责人视角,只读查看')" placement="right">
|
||||||
|
<i class="taskfont readonly-project-avatar"></i>
|
||||||
|
</ETooltip>
|
||||||
<div v-if="item.top_at" class="icon-top"></div>
|
<div v-if="item.top_at" class="icon-top"></div>
|
||||||
<div v-if="item.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div>
|
<div v-if="item.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -62,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<template v-if="projectLists.length === 0">
|
<template v-if="projectLists.length === 0">
|
||||||
<li v-if="projectKeyLoading > 0" class="loading"><Loading/></li>
|
<li v-if="projectKeyLoading > 0 || departmentOwnerProjectsRefreshing" class="loading"><Loading/></li>
|
||||||
<li v-else class="nothing">
|
<li v-else class="nothing">
|
||||||
{{$L(projectKeyValue ? `没有任何与"${projectKeyValue}"相关的结果` : `没有任何项目`)}}
|
{{$L(projectKeyValue ? `没有任何与"${projectKeyValue}"相关的结果` : `没有任何项目`)}}
|
||||||
</li>
|
</li>
|
||||||
@ -88,12 +114,14 @@
|
|||||||
<DropdownItem @click.native="handleChatClick">
|
<DropdownItem @click.native="handleChatClick">
|
||||||
{{ $L('项目讨论') }}
|
{{ $L('项目讨论') }}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem v-if="!projectKeyValue && !operateItem.top_at" @click.native="isDragging=!isDragging">
|
<DropdownItem v-if="!projectKeyValue && !operateItem.top_at && !ownerProjectTabsVisible" @click.native="isDragging=!isDragging">
|
||||||
{{ $L(isDragging ? '退出排序' : '调整排序') }}
|
{{ $L(isDragging ? '退出排序' : '调整排序') }}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
<DepartmentOwnerView v-model="departmentOwnerViewShow"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -103,10 +131,11 @@ import Draggable from 'vuedraggable'
|
|||||||
import longpress from "../../../directives/longpress";
|
import longpress from "../../../directives/longpress";
|
||||||
import TransferDom from "../../../directives/transfer-dom";
|
import TransferDom from "../../../directives/transfer-dom";
|
||||||
import transformEmojiToHtml from "../../../utils/emoji";
|
import transformEmojiToHtml from "../../../utils/emoji";
|
||||||
|
import DepartmentOwnerView from "./DepartmentOwnerView.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ProjectList",
|
name: "ProjectList",
|
||||||
components: {Draggable},
|
components: {Draggable, DepartmentOwnerView},
|
||||||
directives: {longpress, TransferDom},
|
directives: {longpress, TransferDom},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -120,13 +149,30 @@ export default {
|
|||||||
isDragging: false,
|
isDragging: false,
|
||||||
projectDraggableList: [],
|
projectDraggableList: [],
|
||||||
projectDragging: false,
|
projectDragging: false,
|
||||||
|
ownerProjectTab: 'mine',
|
||||||
|
departmentOwnerViewShow: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['cacheProjects', 'loadProjects', 'longpressData']),
|
...mapState(['cacheProjects', 'loadProjects', 'longpressData', 'userInfo', 'systemConfig', 'cacheDepartmentOwnerIds', 'departmentOwnerProjectsRefreshing']),
|
||||||
|
|
||||||
projectLists() {
|
managedDepartments() {
|
||||||
|
return (this.userInfo.managed_departments || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
id: parseInt(item.id)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
ownerViewAvailable() {
|
||||||
|
return this.systemConfig.department_owner_project_view === 'open' && this.managedDepartments.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
ownerDepartmentIds() {
|
||||||
|
return (this.cacheDepartmentOwnerIds || []).map(id => parseInt(id));
|
||||||
|
},
|
||||||
|
|
||||||
|
projectBaseLists() {
|
||||||
const {projectKeyValue, cacheProjects} = this;
|
const {projectKeyValue, cacheProjects} = this;
|
||||||
const data = $A.cloneJSON(cacheProjects).sort((a, b) => {
|
const data = $A.cloneJSON(cacheProjects).sort((a, b) => {
|
||||||
// 置顶优先
|
// 置顶优先
|
||||||
@ -145,9 +191,44 @@ export default {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ownerProjectTabsVisible() {
|
||||||
|
return this.ownerViewAvailable && this.ownerDepartmentIds.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
ownerProjectTabs() {
|
||||||
|
return [
|
||||||
|
{type: 'mine', name: '我的项目', count: this.projectBaseLists.filter(item => !item.department_readonly).length},
|
||||||
|
{type: 'readonly', name: '负责人视角', count: this.projectBaseLists.filter(item => item.department_readonly).length},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
projectLists() {
|
||||||
|
if (!this.ownerProjectTabsVisible) {
|
||||||
|
return this.projectBaseLists;
|
||||||
|
}
|
||||||
|
return this.projectBaseLists.filter(item => this.ownerProjectTab === 'readonly' ? item.department_readonly : !item.department_readonly);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
ownerProjectTabs: {
|
||||||
|
handler(tabs) {
|
||||||
|
if (!this.ownerProjectTabsVisible) {
|
||||||
|
this.ownerProjectTab = 'mine';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const active = tabs.find(item => item.type === this.ownerProjectTab);
|
||||||
|
if (!active || active.count === 0) {
|
||||||
|
const first = tabs.find(item => item.count > 0);
|
||||||
|
if (first) {
|
||||||
|
this.ownerProjectTab = first.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
|
||||||
projectLists: {
|
projectLists: {
|
||||||
handler(val) {
|
handler(val) {
|
||||||
if (!this.projectDragging) {
|
if (!this.projectDragging) {
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<div v-if="loading" class="project-load"><Loading/></div>
|
<div v-if="loading" class="project-load"><Loading/></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="project-icons">
|
<ul class="project-icons">
|
||||||
<li class="project-avatar" :class="{'cursor-default': !isOwnerOrDeputy}" @click="projectDropdown('user')">
|
<li class="project-avatar" :class="{'cursor-default': !isOwnerOrDeputy || isDepartmentReadonly}" @click="projectDropdown('user')">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<UserAvatarTip :userid="projectData.owner_userid" :size="36" :borderWidth="2" :openDelay="0">
|
<UserAvatarTip :userid="projectData.owner_userid" :size="36" :borderWidth="2" :openDelay="0">
|
||||||
@ -32,7 +32,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="project-icon" @click="addTaskOpen(0)">
|
<li v-if="!projectData.department_readonly" class="project-icon" @click="addTaskOpen(0)">
|
||||||
<ETooltip :disabled="$isEEUIApp || windowTouch" :content="$L('添加任务')">
|
<ETooltip :disabled="$isEEUIApp || windowTouch" :content="$L('添加任务')">
|
||||||
<Icon class="menu-icon" type="md-add" />
|
<Icon class="menu-icon" type="md-add" />
|
||||||
</ETooltip>
|
</ETooltip>
|
||||||
@ -52,7 +52,13 @@
|
|||||||
<li class="project-icon">
|
<li class="project-icon">
|
||||||
<EDropdown @command="projectDropdown" trigger="click" transfer>
|
<EDropdown @command="projectDropdown" trigger="click" transfer>
|
||||||
<Icon class="menu-icon" type="ios-more" />
|
<Icon class="menu-icon" type="ios-more" />
|
||||||
<EDropdownMenu v-if="isOwnerOrDeputy" slot="dropdown" class="project-panel-project-menu-dropdown">
|
<EDropdownMenu v-if="isDepartmentReadonly" slot="dropdown" class="project-panel-project-menu-dropdown">
|
||||||
|
<EDropdownItem command="favorite">{{$L(projectData.favorited ? '取消收藏' : '收藏项目')}}</EDropdownItem>
|
||||||
|
<EDropdownItem command="log" divided>{{$L('项目动态')}}</EDropdownItem>
|
||||||
|
<EDropdownItem command="archived_task">{{$L('已归档任务')}}</EDropdownItem>
|
||||||
|
<EDropdownItem command="deleted_task">{{$L('已删除任务')}}</EDropdownItem>
|
||||||
|
</EDropdownMenu>
|
||||||
|
<EDropdownMenu v-else-if="isOwnerOrDeputy" slot="dropdown" class="project-panel-project-menu-dropdown">
|
||||||
<EDropdownItem command="setting">{{$L('项目设置')}}</EDropdownItem>
|
<EDropdownItem command="setting">{{$L('项目设置')}}</EDropdownItem>
|
||||||
<EDropdownItem command="permissions">{{$L('权限设置')}}</EDropdownItem>
|
<EDropdownItem command="permissions">{{$L('权限设置')}}</EDropdownItem>
|
||||||
<EDropdownItem command="task_template">{{$L('任务模板')}}</EDropdownItem>
|
<EDropdownItem command="task_template">{{$L('任务模板')}}</EDropdownItem>
|
||||||
@ -83,6 +89,9 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<Alert v-if="projectData.department_readonly" class="project-readonly-alert" type="info" show-icon>
|
||||||
|
{{$L('当前为负责人视角:你可查看项目和任务,并参与讨论,但不能编辑项目或任务。')}}
|
||||||
|
</Alert>
|
||||||
<div class="project-subbox">
|
<div class="project-subbox">
|
||||||
<div class="project-subtitle user-select-auto" @click="showDesc">
|
<div class="project-subtitle user-select-auto" @click="showDesc">
|
||||||
<VMPreviewNostyle ref="descPreview" :value="projectData.desc"/>
|
<VMPreviewNostyle ref="descPreview" :value="projectData.desc"/>
|
||||||
@ -112,7 +121,7 @@
|
|||||||
<Draggable
|
<Draggable
|
||||||
:list="columnList"
|
:list="columnList"
|
||||||
:animation="150"
|
:animation="150"
|
||||||
:disabled="sortDisabled || $isEEUIApp || windowTouch"
|
:disabled="sortDisabled || isDepartmentReadonly || $isEEUIApp || windowTouch"
|
||||||
class="column-list"
|
class="column-list"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
draggable=".column-item"
|
draggable=".column-item"
|
||||||
@ -133,7 +142,7 @@
|
|||||||
<div class="column-head-icon">
|
<div class="column-head-icon">
|
||||||
<div v-if="columnLoad[column.id] === true" class="loading"><Loading /></div>
|
<div v-if="columnLoad[column.id] === true" class="loading"><Loading /></div>
|
||||||
<EDropdown
|
<EDropdown
|
||||||
v-else
|
v-else-if="!isDepartmentReadonly"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
size="medium"
|
size="medium"
|
||||||
@command="dropColumn(column, $event)">
|
@command="dropColumn(column, $event)">
|
||||||
@ -165,14 +174,14 @@
|
|||||||
</li>
|
</li>
|
||||||
</EDropdownMenu>
|
</EDropdownMenu>
|
||||||
</EDropdown>
|
</EDropdown>
|
||||||
<Icon class="last" type="md-add" @click="addTopShow(column.id, true)" />
|
<Icon v-if="!isDepartmentReadonly" class="last" type="md-add" @click="addTopShow(column.id, true)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Scrollbar
|
<Scrollbar
|
||||||
class="column-task"
|
class="column-task"
|
||||||
class-name="task-scrollbar"
|
class-name="task-scrollbar"
|
||||||
@on-scroll="handleTaskScroll">
|
@on-scroll="handleTaskScroll">
|
||||||
<div v-if="!!columnTopShow[column.id]" class="task-item additem">
|
<div v-if="!isDepartmentReadonly && !!columnTopShow[column.id]" class="task-item additem">
|
||||||
<TaskAddSimple
|
<TaskAddSimple
|
||||||
:column-id="column.id"
|
:column-id="column.id"
|
||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
@ -184,7 +193,7 @@
|
|||||||
<Draggable
|
<Draggable
|
||||||
:list="column.tasks"
|
:list="column.tasks"
|
||||||
:animation="150"
|
:animation="150"
|
||||||
:disabled="sortDisabled || $isEEUIApp || windowTouch"
|
:disabled="sortDisabled || isDepartmentReadonly || $isEEUIApp || windowTouch"
|
||||||
class="task-list"
|
class="task-list"
|
||||||
draggable=".task-draggable"
|
draggable=".task-draggable"
|
||||||
filter=".complete"
|
filter=".complete"
|
||||||
@ -207,7 +216,7 @@
|
|||||||
<pre>{{item.name}}</pre>
|
<pre>{{item.name}}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-menu" @click.stop="">
|
<div class="task-menu" @click.stop="">
|
||||||
<TaskMenu :ref="`taskMenu_${item.id}`" :task="item" icon="ios-more"/>
|
<TaskMenu v-if="!isDepartmentReadonly" :ref="`taskMenu_${item.id}`" :task="item" icon="ios-more"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!item.complete_at">
|
<template v-if="!item.complete_at">
|
||||||
@ -241,7 +250,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-item additem">
|
<div v-if="!isDepartmentReadonly" class="task-item additem">
|
||||||
<TaskAddSimple
|
<TaskAddSimple
|
||||||
:column-id="column.id"
|
:column-id="column.id"
|
||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
@ -250,7 +259,7 @@
|
|||||||
</Draggable>
|
</Draggable>
|
||||||
</Scrollbar>
|
</Scrollbar>
|
||||||
</li>
|
</li>
|
||||||
<li :class="['add-column', addColumnShow ? 'show-input' : '']">
|
<li v-if="!isDepartmentReadonly" :class="['add-column', addColumnShow ? 'show-input' : '']">
|
||||||
<div class="add-column-text" @click="addColumnOpen">
|
<div class="add-column-text" @click="addColumnOpen">
|
||||||
<Icon type="md-add" />{{$L('添加列表')}}
|
<Icon type="md-add" />{{$L('添加列表')}}
|
||||||
</div>
|
</div>
|
||||||
@ -321,6 +330,7 @@
|
|||||||
v-if="projectData.cacheParameter.showMy"
|
v-if="projectData.cacheParameter.showMy"
|
||||||
:list="transforTasks(myList)"
|
:list="transforTasks(myList)"
|
||||||
:task-visibilitys="taskRowVisibilitys"
|
:task-visibilitys="taskRowVisibilitys"
|
||||||
|
:readonly="isDepartmentReadonly"
|
||||||
open-key="my"
|
open-key="my"
|
||||||
@on-priority="addTaskOpen"
|
@on-priority="addTaskOpen"
|
||||||
fast-add-task/>
|
fast-add-task/>
|
||||||
@ -342,6 +352,7 @@
|
|||||||
v-if="projectData.cacheParameter.showHelp"
|
v-if="projectData.cacheParameter.showHelp"
|
||||||
:list="helpList"
|
:list="helpList"
|
||||||
:task-visibilitys="taskRowVisibilitys"
|
:task-visibilitys="taskRowVisibilitys"
|
||||||
|
:readonly="isDepartmentReadonly"
|
||||||
open-key="help"
|
open-key="help"
|
||||||
@on-priority="addTaskOpen"/>
|
@on-priority="addTaskOpen"/>
|
||||||
</div>
|
</div>
|
||||||
@ -362,6 +373,7 @@
|
|||||||
v-if="projectData.cacheParameter.showUndone"
|
v-if="projectData.cacheParameter.showUndone"
|
||||||
:list="unList"
|
:list="unList"
|
||||||
:task-visibilitys="taskRowVisibilitys"
|
:task-visibilitys="taskRowVisibilitys"
|
||||||
|
:readonly="isDepartmentReadonly"
|
||||||
open-key="undone"
|
open-key="undone"
|
||||||
@on-priority="addTaskOpen"/>
|
@on-priority="addTaskOpen"/>
|
||||||
</div>
|
</div>
|
||||||
@ -384,6 +396,7 @@
|
|||||||
v-if="projectData.cacheParameter.showCompleted"
|
v-if="projectData.cacheParameter.showCompleted"
|
||||||
:list="completedList"
|
:list="completedList"
|
||||||
:task-visibilitys="taskRowVisibilitys"
|
:task-visibilitys="taskRowVisibilitys"
|
||||||
|
:readonly="isDepartmentReadonly"
|
||||||
open-key="completed"
|
open-key="completed"
|
||||||
@on-priority="addTaskOpen"
|
@on-priority="addTaskOpen"
|
||||||
showCompleteAt/>
|
showCompleteAt/>
|
||||||
@ -391,7 +404,7 @@
|
|||||||
</Scrollbar>
|
</Scrollbar>
|
||||||
<div v-else-if="tabTypeActive === 'gantt'" class="project-gantt">
|
<div v-else-if="tabTypeActive === 'gantt'" class="project-gantt">
|
||||||
<!--甘特图-->
|
<!--甘特图-->
|
||||||
<ProjectGantt :projectColumn="columnList" :flowInfo="flowInfo"/>
|
<ProjectGantt :projectColumn="columnList" :flowInfo="flowInfo" :readonly="isDepartmentReadonly"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--项目设置-->
|
<!--项目设置-->
|
||||||
@ -824,7 +837,12 @@ export default {
|
|||||||
return this.projectData?.owner_userid === this.userId;
|
return this.projectData?.owner_userid === this.userId;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isDepartmentReadonly() {
|
||||||
|
return !!this.projectData?.department_readonly;
|
||||||
|
},
|
||||||
|
|
||||||
isOwnerOrDeputy() {
|
isOwnerOrDeputy() {
|
||||||
|
if (this.isDepartmentReadonly) return false;
|
||||||
if (!this.projectData) return false;
|
if (!this.projectData) return false;
|
||||||
if (this.projectData.owner_userid === this.userId) return true;
|
if (this.projectData.owner_userid === this.userId) return true;
|
||||||
return (this.projectData.deputy_userids || []).includes(this.userId);
|
return (this.projectData.deputy_userids || []).includes(this.userId);
|
||||||
@ -1249,6 +1267,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
sortUpdate(only_column) {
|
sortUpdate(only_column) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const oldSort = this.sortData;
|
const oldSort = this.sortData;
|
||||||
const newSort = this.getSort();
|
const newSort = this.getSort();
|
||||||
if (JSON.stringify(oldSort) === JSON.stringify(newSort)) {
|
if (JSON.stringify(oldSort) === JSON.stringify(newSort)) {
|
||||||
@ -1315,14 +1336,23 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
addTopShow(id, show) {
|
addTopShow(id, show) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$set(this.columnTopShow, id, show);
|
this.$set(this.columnTopShow, id, show);
|
||||||
},
|
},
|
||||||
|
|
||||||
addTaskOpen(params) {
|
addTaskOpen(params) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
emitter.emit('addTask', params);
|
emitter.emit('addTask', params);
|
||||||
},
|
},
|
||||||
|
|
||||||
addColumnOpen() {
|
addColumnOpen() {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.addColumnShow = true;
|
this.addColumnShow = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.addColumnName.focus();
|
this.$refs.addColumnName.focus();
|
||||||
@ -1360,6 +1390,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
dropColumn(column, command) {
|
dropColumn(column, command) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (command === 'title') {
|
if (command === 'title') {
|
||||||
this.titleColumn(column);
|
this.titleColumn(column);
|
||||||
}
|
}
|
||||||
@ -1617,6 +1650,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
projectDropdown(name) {
|
projectDropdown(name) {
|
||||||
|
if (this.isDepartmentReadonly && !['favorite', 'log', 'archived_task', 'deleted_task'].includes(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "favorite":
|
case "favorite":
|
||||||
this.toggleProjectFavorite();
|
this.toggleProjectFavorite();
|
||||||
@ -1643,7 +1679,7 @@ export default {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "user":
|
case "user":
|
||||||
if (!this.isOwnerOrDeputy) {
|
if (this.isDepartmentReadonly || !this.isOwnerOrDeputy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const userids = this.projectData.project_user.map(({userid}) => userid);
|
const userids = this.projectData.project_user.map(({userid}) => userid);
|
||||||
@ -1714,6 +1750,9 @@ export default {
|
|||||||
|
|
||||||
openTask(task, receive) {
|
openTask(task, receive) {
|
||||||
this.$store.dispatch("openTask", task)
|
this.$store.dispatch("openTask", task)
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (receive === true) {
|
if (receive === true) {
|
||||||
// 向任务窗口发送领取任务请求
|
// 向任务窗口发送领取任务请求
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -1777,7 +1816,7 @@ export default {
|
|||||||
this.$store.dispatch("call", {
|
this.$store.dispatch("call", {
|
||||||
url: 'project/flow/list',
|
url: 'project/flow/list',
|
||||||
data: {
|
data: {
|
||||||
project_id: this.projectId,
|
project_id: this.projectId
|
||||||
},
|
},
|
||||||
}).then(({data}) => {
|
}).then(({data}) => {
|
||||||
this.flowList = data;
|
this.flowList = data;
|
||||||
|
|||||||
@ -365,7 +365,7 @@ export default {
|
|||||||
this.$store.dispatch("call", {
|
this.$store.dispatch("call", {
|
||||||
url: 'project/flow/list',
|
url: 'project/flow/list',
|
||||||
data: {
|
data: {
|
||||||
project_id: this.projectId,
|
project_id: this.projectId
|
||||||
},
|
},
|
||||||
}).then(({data}) => {
|
}).then(({data}) => {
|
||||||
this.list = data.map(item => {
|
this.list = data.map(item => {
|
||||||
|
|||||||
@ -138,7 +138,7 @@ export default {
|
|||||||
data: {
|
data: {
|
||||||
task_id: this.taskId,
|
task_id: this.taskId,
|
||||||
page: Math.max(this.page, 1),
|
page: Math.max(this.page, 1),
|
||||||
pagesize: Math.max($A.runNum(this.pageSize), 10),
|
pagesize: Math.max($A.runNum(this.pageSize), 10)
|
||||||
},
|
},
|
||||||
}).then(({data}) => {
|
}).then(({data}) => {
|
||||||
this.page = data.current_page;
|
this.page = data.current_page;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<!--子任务-->
|
<!--子任务-->
|
||||||
<li v-if="ready && isSubTask">
|
<li v-if="ready && isSubTask">
|
||||||
<div class="subtask-icon">
|
<div v-if="!isDepartmentReadonly" class="subtask-icon">
|
||||||
<TaskMenu
|
<TaskMenu
|
||||||
:ref="`taskMenu_${taskDetail.id}`"
|
:ref="`taskMenu_${taskDetail.id}`"
|
||||||
:disabled="taskId === 0"
|
:disabled="taskId === 0"
|
||||||
@ -13,7 +13,7 @@
|
|||||||
v-if="taskDetail.flow_item_name"
|
v-if="taskDetail.flow_item_name"
|
||||||
class="subtask-flow"
|
class="subtask-flow"
|
||||||
:style="$A.generateColorVarStyle(taskDetail.flow_item_color, [10], 'flow-item-custom-color')">
|
:style="$A.generateColorVarStyle(taskDetail.flow_item_color, [10], 'flow-item-custom-color')">
|
||||||
<span :class="taskDetail.flow_item_status" @click.stop="openMenu($event, taskDetail)">{{taskDetail.flow_item_name}}</span>
|
<span :class="taskDetail.flow_item_status" @click.stop="!isDepartmentReadonly && openMenu($event, taskDetail)">{{taskDetail.flow_item_name}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="subtask-name">
|
<div class="subtask-name">
|
||||||
<Input
|
<Input
|
||||||
@ -24,11 +24,13 @@
|
|||||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||||
:maxlength="255"
|
:maxlength="255"
|
||||||
enterkeyhint="done"
|
enterkeyhint="done"
|
||||||
|
:readonly="isDepartmentReadonly"
|
||||||
@on-blur="updateBlur('name')"
|
@on-blur="updateBlur('name')"
|
||||||
@on-keydown="onNameKeydown"
|
@on-keydown="onNameKeydown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
v-if="!isDepartmentReadonly"
|
||||||
v-model="timeValue"
|
v-model="timeValue"
|
||||||
:open="timeOpen"
|
:open="timeOpen"
|
||||||
:options="timeOptions"
|
:options="timeOptions"
|
||||||
@ -46,7 +48,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<Icon v-else class="clock" type="ios-clock-outline" @click="openTime" />
|
<Icon v-else class="clock" type="ios-clock-outline" @click="openTime" />
|
||||||
</DatePicker>
|
</DatePicker>
|
||||||
|
<div v-else-if="showSubTime" :class="['subtask-time readonly-time', taskDetail.today ? 'today' : '', taskDetail.overdue ? 'overdue' : '']">
|
||||||
|
{{expiresFormat(taskDetail.end_at)}}
|
||||||
|
</div>
|
||||||
<UserSelect
|
<UserSelect
|
||||||
|
v-if="!isDepartmentReadonly"
|
||||||
class="subtask-avatar"
|
class="subtask-avatar"
|
||||||
v-model="ownerData.owner_userid"
|
v-model="ownerData.owner_userid"
|
||||||
:multiple-max="10"
|
:multiple-max="10"
|
||||||
@ -55,6 +61,11 @@
|
|||||||
:add-icon="false"
|
:add-icon="false"
|
||||||
:project-id="taskDetail.project_id"
|
:project-id="taskDetail.project_id"
|
||||||
:before-submit="onOwner"/>
|
:before-submit="onOwner"/>
|
||||||
|
<UserAvatar
|
||||||
|
v-else-if="ownerData.owner_userid && ownerData.owner_userid.length > 0"
|
||||||
|
class="subtask-avatar readonly-avatar"
|
||||||
|
:userid="ownerData.owner_userid[0]"
|
||||||
|
:size="20"/>
|
||||||
</li>
|
</li>
|
||||||
<!--主任务-->
|
<!--主任务-->
|
||||||
<div
|
<div
|
||||||
@ -65,6 +76,7 @@
|
|||||||
<div v-show="taskDetail.id > 0" class="task-info" v-resize-observer="scrollIntoInput">
|
<div v-show="taskDetail.id > 0" class="task-info" v-resize-observer="scrollIntoInput">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<TaskMenu
|
<TaskMenu
|
||||||
|
v-if="!isDepartmentReadonly"
|
||||||
:ref="`taskMenu_${taskDetail.id}`"
|
:ref="`taskMenu_${taskDetail.id}`"
|
||||||
:disabled="taskId === 0"
|
:disabled="taskId === 0"
|
||||||
:task="taskDetail"
|
:task="taskDetail"
|
||||||
@ -76,10 +88,10 @@
|
|||||||
v-if="taskDetail.flow_item_name"
|
v-if="taskDetail.flow_item_name"
|
||||||
class="flow"
|
class="flow"
|
||||||
:style="$A.generateColorVarStyle(taskDetail.flow_item_color, [10], 'flow-item-custom-color')">
|
:style="$A.generateColorVarStyle(taskDetail.flow_item_color, [10], 'flow-item-custom-color')">
|
||||||
<span :class="taskDetail.flow_item_status" @click.stop="openMenu($event, taskDetail)">{{taskDetail.flow_item_name}}</span>
|
<span :class="taskDetail.flow_item_status" @click.stop="!isDepartmentReadonly && openMenu($event, taskDetail)">{{taskDetail.flow_item_name}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="taskDetail.archived_at" class="flow">
|
<div v-if="taskDetail.archived_at" class="flow">
|
||||||
<span class="archived" @click.stop="openMenu($event, taskDetail)">{{$L('已归档')}}</span>
|
<span class="archived" @click.stop="!isDepartmentReadonly && openMenu($event, taskDetail)">{{$L('已归档')}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav user-select-auto">
|
<div class="nav user-select-auto">
|
||||||
<p v-if="projectName"><span>{{projectName}}</span></p>
|
<p v-if="projectName"><span>{{projectName}}</span></p>
|
||||||
@ -90,7 +102,7 @@
|
|||||||
<ETooltip v-if="$Electron" :disabled="$isEEUIApp || windowTouch" :content="$L('独立窗口显示')">
|
<ETooltip v-if="$Electron" :disabled="$isEEUIApp || windowTouch" :content="$L('独立窗口显示')">
|
||||||
<i class="taskfont open" @click="openNewWin"></i>
|
<i class="taskfont open" @click="openNewWin"></i>
|
||||||
</ETooltip>
|
</ETooltip>
|
||||||
<div class="menu">
|
<div v-if="!isDepartmentReadonly" class="menu">
|
||||||
<TaskMenu
|
<TaskMenu
|
||||||
:disabled="taskId === 0"
|
:disabled="taskId === 0"
|
||||||
:task="taskDetail"
|
:task="taskDetail"
|
||||||
@ -104,7 +116,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Scrollbar ref="scroller" class="scroller" :touch-content-blur="false">
|
<Scrollbar ref="scroller" class="scroller" :touch-content-blur="false">
|
||||||
<Alert v-if="taskDetail.task_user !== undefined && getOwner.length === 0" class="receive-box" type="warning">
|
<Alert v-if="taskDetail.department_readonly" class="task-readonly-alert" type="info" show-icon>
|
||||||
|
{{$L('当前为负责人视角:你可查看任务内容、动态和附件,并参与讨论,但不能编辑任务。')}}
|
||||||
|
</Alert>
|
||||||
|
<Alert v-if="!isDepartmentReadonly && taskDetail.task_user !== undefined && getOwner.length === 0" class="receive-box" type="warning">
|
||||||
<span class="receive-text">{{$L('该任务尚未被领取,点击这里')}}</span>
|
<span class="receive-text">{{$L('该任务尚未被领取,点击这里')}}</span>
|
||||||
<EPopover
|
<EPopover
|
||||||
v-model="receiveShow"
|
v-model="receiveShow"
|
||||||
@ -143,6 +158,7 @@
|
|||||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||||
:maxlength="255"
|
:maxlength="255"
|
||||||
enterkeyhint="done"
|
enterkeyhint="done"
|
||||||
|
:readonly="isDepartmentReadonly"
|
||||||
@on-blur="updateBlur('name')"
|
@on-blur="updateBlur('name')"
|
||||||
@on-keydown="onNameKeydown"/>
|
@on-keydown="onNameKeydown"/>
|
||||||
</div>
|
</div>
|
||||||
@ -151,6 +167,7 @@
|
|||||||
class="desc"
|
class="desc"
|
||||||
:value="taskContent"
|
:value="taskContent"
|
||||||
:placeholder="$L('详细描述...')"
|
:placeholder="$L('详细描述...')"
|
||||||
|
:readonly="isDepartmentReadonly"
|
||||||
@on-history="onHistory"
|
@on-history="onHistory"
|
||||||
@on-blur="updateBlur('content', $event)"/>
|
@on-blur="updateBlur('content', $event)"/>
|
||||||
<Form class="items" label-position="left" label-width="auto" @submit.native.prevent>
|
<Form class="items" label-position="left" label-width="auto" @submit.native.prevent>
|
||||||
@ -159,7 +176,8 @@
|
|||||||
<i class="taskfont"></i>{{$L('标签')}}
|
<i class="taskfont"></i>{{$L('标签')}}
|
||||||
</div>
|
</div>
|
||||||
<div class="item-content tags">
|
<div class="item-content tags">
|
||||||
<EPopover v-model="tagShow" class="tags-select" placement="bottom">
|
<TaskTag v-if="isDepartmentReadonly" :tags="getTag"/>
|
||||||
|
<EPopover v-else v-model="tagShow" class="tags-select" placement="bottom">
|
||||||
<TaskTagSelect
|
<TaskTagSelect
|
||||||
ref="tagSelect"
|
ref="tagSelect"
|
||||||
v-model="tagValue"
|
v-model="tagValue"
|
||||||
@ -181,7 +199,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul class="item-content priority">
|
<ul class="item-content priority">
|
||||||
<li>
|
<li>
|
||||||
<TaskPriority :backgroundColor="taskDetail.p_color"><span ref="priorityText" @click="onPriority">{{taskDetail.p_name}}</span></TaskPriority>
|
<TaskPriority :backgroundColor="taskDetail.p_color"><span ref="priorityText" @click="!isDepartmentReadonly && onPriority($event)">{{taskDetail.p_name}}</span></TaskPriority>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -189,7 +207,11 @@
|
|||||||
<div class="item-label" slot="label">
|
<div class="item-label" slot="label">
|
||||||
<i class="taskfont"></i>{{$L('负责人')}}
|
<i class="taskfont"></i>{{$L('负责人')}}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isDepartmentReadonly" class="item-content user readonly-users">
|
||||||
|
<UserAvatar v-for="item in getOwner" :key="item.userid" :userid="item.userid" :size="28" showName/>
|
||||||
|
</div>
|
||||||
<UserSelect
|
<UserSelect
|
||||||
|
v-else
|
||||||
class="item-content user"
|
class="item-content user"
|
||||||
v-model="ownerData.owner_userid"
|
v-model="ownerData.owner_userid"
|
||||||
:multiple-max="10"
|
:multiple-max="10"
|
||||||
@ -203,7 +225,11 @@
|
|||||||
<div class="item-label" slot="label">
|
<div class="item-label" slot="label">
|
||||||
<i class="taskfont"></i>{{$L('协助人员')}}
|
<i class="taskfont"></i>{{$L('协助人员')}}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isDepartmentReadonly" class="item-content user readonly-users">
|
||||||
|
<UserAvatar v-for="item in getAssist" :key="item.userid" :userid="item.userid" :size="28" showName/>
|
||||||
|
</div>
|
||||||
<UserSelect
|
<UserSelect
|
||||||
|
v-else
|
||||||
ref="assist"
|
ref="assist"
|
||||||
class="item-content user"
|
class="item-content user"
|
||||||
v-model="assistData.assist_userid"
|
v-model="assistData.assist_userid"
|
||||||
@ -218,11 +244,11 @@
|
|||||||
<FormItem v-if="taskDetail.visibility > 1 || visibleForce || visibleKeep">
|
<FormItem v-if="taskDetail.visibility > 1 || visibleForce || visibleKeep">
|
||||||
<div class="item-label" slot="label">
|
<div class="item-label" slot="label">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
<span class="visibility-text color" @click="showCisibleDropdown">{{$L('可见性')}} <i class="taskfont"></i></span>
|
<span class="visibility-text color" @click="!isDepartmentReadonly && showCisibleDropdown($event)">{{$L('可见性')}} <i class="taskfont"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-content user">
|
<div class="item-content user">
|
||||||
<span v-if="taskDetail.visibility == 1 || taskDetail.visibility == 2" ref="visibilityText" class="visibility-text" @click="showCisibleDropdown">{{ taskDetail.visibility == 1 ? $L('项目人员可见') : $L('任务人员可见') }}</span>
|
<span v-if="taskDetail.visibility == 1 || taskDetail.visibility == 2" ref="visibilityText" class="visibility-text" @click="!isDepartmentReadonly && showCisibleDropdown($event)">{{ taskDetail.visibility == 1 ? $L('项目人员可见') : $L('任务人员可见') }}</span>
|
||||||
<UserSelect v-else
|
<UserSelect v-else-if="!isDepartmentReadonly"
|
||||||
ref="visibleUserSelectRef"
|
ref="visibleUserSelectRef"
|
||||||
v-model="taskDetail.visibility_appointor"
|
v-model="taskDetail.visibility_appointor"
|
||||||
:avatar-size="28"
|
:avatar-size="28"
|
||||||
@ -230,13 +256,16 @@
|
|||||||
:project-id="taskDetail.project_id"
|
:project-id="taskDetail.project_id"
|
||||||
:add-icon="false"
|
:add-icon="false"
|
||||||
@on-show-change="visibleUserSelectShowChange"/>
|
@on-show-change="visibleUserSelectShowChange"/>
|
||||||
|
<div v-else class="readonly-users">
|
||||||
|
<UserAvatar v-for="userid in taskDetail.visibility_appointor" :key="userid" :userid="userid" :size="28" showName/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem v-if="taskDetail.end_at || timeForce">
|
<FormItem v-if="taskDetail.end_at || timeForce">
|
||||||
<div class="item-label" slot="label">
|
<div class="item-label" slot="label">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
<span v-if="!taskDetail.end_at" @click="timeOpen = true" class="visibility-text color">{{$L('截止时间')}}</span>
|
<span v-if="!taskDetail.end_at" @click="!isDepartmentReadonly && (timeOpen = true)" class="visibility-text color">{{$L('截止时间')}}</span>
|
||||||
<span v-else class="visibility-text color" @click="showAtDropdown">{{$L('截止时间')}}</span>
|
<span v-else class="visibility-text color" @click="!isDepartmentReadonly && showAtDropdown($event)">{{$L('截止时间')}}</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="item-content">
|
<ul class="item-content">
|
||||||
<li>
|
<li>
|
||||||
@ -253,13 +282,13 @@
|
|||||||
@on-ok="timeOk"
|
@on-ok="timeOk"
|
||||||
transfer>
|
transfer>
|
||||||
<div class="picker-time">
|
<div class="picker-time">
|
||||||
<div v-if="!taskDetail.end_at" @click="timeOpen = true" class="time">{{taskDetail.end_at ? cutTime : '--'}}</div>
|
<div v-if="!taskDetail.end_at" @click="!isDepartmentReadonly && (timeOpen = true)" class="time">{{taskDetail.end_at ? cutTime : '--'}}</div>
|
||||||
<div v-else @click="showAtDropdown" class="time">{{taskDetail.end_at ? cutTime : '--'}}</div>
|
<div v-else @click="!isDepartmentReadonly && showAtDropdown($event)" class="time">{{taskDetail.end_at ? cutTime : '--'}}</div>
|
||||||
<template v-if="!taskDetail.complete_at && taskDetail.end_at">
|
<template v-if="!taskDetail.complete_at && taskDetail.end_at">
|
||||||
<Tag v-if="within24Hours(taskDetail.end_at)" :color="tagColor(taskDetail)" @on-click="showAtDropdown">
|
<Tag v-if="within24Hours(taskDetail.end_at)" :color="tagColor(taskDetail)" @on-click="!isDepartmentReadonly && showAtDropdown($event)">
|
||||||
<i class="taskfont"></i>{{expiresFormat(taskDetail.end_at)}}
|
<i class="taskfont"></i>{{expiresFormat(taskDetail.end_at)}}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag v-if="taskDetail.overdue" color="red" @on-click="showAtDropdown">{{$L('超期未完成')}}</Tag>
|
<Tag v-if="taskDetail.overdue" color="red" @on-click="!isDepartmentReadonly && showAtDropdown($event)">{{$L('超期未完成')}}</Tag>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</DatePicker>
|
</DatePicker>
|
||||||
@ -273,7 +302,7 @@
|
|||||||
<ul class="item-content loop">
|
<ul class="item-content loop">
|
||||||
<li>
|
<li>
|
||||||
<ETooltip :disabled="$isEEUIApp || windowTouch || !taskDetail.loop_at" :content="`${$L('下个周期')}: ${taskDetail.loop_at}`" placement="right">
|
<ETooltip :disabled="$isEEUIApp || windowTouch || !taskDetail.loop_at" :content="`${$L('下个周期')}: ${taskDetail.loop_at}`" placement="right">
|
||||||
<span ref="loopText" @click="onLoop">{{$L(loopLabel(taskDetail.loop))}}</span>
|
<span ref="loopText" @click="!isDepartmentReadonly && onLoop($event)">{{$L(loopLabel(taskDetail.loop))}}</span>
|
||||||
</ETooltip>
|
</ETooltip>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -291,7 +320,7 @@
|
|||||||
<div class="file-size">{{$A.bytesToSize(file.size)}}</div>
|
<div class="file-size">{{$A.bytesToSize(file.size)}}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="item-content file-up">
|
<ul v-if="!isDepartmentReadonly" class="item-content file-up">
|
||||||
<li>
|
<li>
|
||||||
<div class="add-button" @click="onUploadClick(true)">
|
<div class="add-button" @click="onUploadClick(true)">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
@ -314,7 +343,7 @@
|
|||||||
:main-end-at="taskDetail.end_at"
|
:main-end-at="taskDetail.end_at"
|
||||||
:can-update-blur="canUpdateBlur"/>
|
:can-update-blur="canUpdateBlur"/>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="item-content subtask-add">
|
<ul v-if="!isDepartmentReadonly" class="item-content subtask-add">
|
||||||
<li>
|
<li>
|
||||||
<Input
|
<Input
|
||||||
v-if="addsubShow"
|
v-if="addsubShow"
|
||||||
@ -377,6 +406,7 @@
|
|||||||
{{$L('已归档')}}
|
{{$L('已归档')}}
|
||||||
</span>
|
</span>
|
||||||
<Icon
|
<Icon
|
||||||
|
v-if="!isDepartmentReadonly"
|
||||||
type="md-close"
|
type="md-close"
|
||||||
class="related-remove"
|
class="related-remove"
|
||||||
@click.native.stop="removeRelatedTask(item)"/>
|
@click.native.stop="removeRelatedTask(item)"/>
|
||||||
@ -384,7 +414,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
<div v-if="menuList.length > 0" class="add">
|
<div v-if="!isDepartmentReadonly && menuList.length > 0" class="add">
|
||||||
<div class="add-wrap">
|
<div class="add-wrap">
|
||||||
<div class="add-button" @click="onAddItem">
|
<div class="add-button" @click="onAddItem">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
@ -484,6 +514,7 @@
|
|||||||
<div class="drag-text">{{$L('拖动到这里发送')}}</div>
|
<div class="drag-text">{{$L('拖动到这里发送')}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!taskDetail.id" class="task-load"><Loading/></div>
|
<div v-if="!taskDetail.id" class="task-load"><Loading/></div>
|
||||||
@ -884,6 +915,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
menuList() {
|
menuList() {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const {taskDetail} = this;
|
const {taskDetail} = this;
|
||||||
const list = [];
|
const list = [];
|
||||||
if ($A.arrayLength(taskDetail.task_tag) === 0) {
|
if ($A.arrayLength(taskDetail.task_tag) === 0) {
|
||||||
@ -963,6 +997,10 @@ export default {
|
|||||||
return this.systemConfig.task_visible === 'open' // 可见性保持显示
|
return this.systemConfig.task_visible === 'open' // 可见性保持显示
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isDepartmentReadonly() {
|
||||||
|
return !!this.taskDetail?.department_readonly;
|
||||||
|
},
|
||||||
|
|
||||||
isSubTask({taskDetail}) {
|
isSubTask({taskDetail}) {
|
||||||
return taskDetail.parent_id > 0
|
return taskDetail.parent_id > 0
|
||||||
},
|
},
|
||||||
@ -1137,6 +1175,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onNameKeydown(e) {
|
onNameKeydown(e) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
if (!e.shiftKey) {
|
if (!e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -1146,6 +1187,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
checkUpdate(action) {
|
checkUpdate(action) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let isModify = false;
|
let isModify = false;
|
||||||
if (this.openTask.name != this.taskDetail.name) {
|
if (this.openTask.name != this.taskDetail.name) {
|
||||||
isModify = true;
|
isModify = true;
|
||||||
@ -1187,12 +1231,20 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateBlur(action, params) {
|
updateBlur(action, params) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.canUpdateBlur) {
|
if (this.canUpdateBlur) {
|
||||||
this.updateData(action, params)
|
this.updateData(action, params)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateData(action, params) {
|
updateData(action, params) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let successCallback = null;
|
let successCallback = null;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'priority':
|
case 'priority':
|
||||||
@ -1396,6 +1448,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async onOwner(pick) {
|
async onOwner(pick) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
task_id: this.taskDetail.id,
|
task_id: this.taskDetail.id,
|
||||||
owner: this.ownerData.owner_userid
|
owner: this.ownerData.owner_userid
|
||||||
@ -1440,6 +1496,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onAssist() {
|
onAssist() {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($A.jsonStringify(this.taskDetail.assist_userid) === $A.jsonStringify(this.assistData.assist_userid)) {
|
if ($A.jsonStringify(this.taskDetail.assist_userid) === $A.jsonStringify(this.assistData.assist_userid)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1484,6 +1544,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openTime() {
|
openTime() {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.timeOpen = !this.timeOpen;
|
this.timeOpen = !this.timeOpen;
|
||||||
if (this.timeOpen) {
|
if (this.timeOpen) {
|
||||||
this.timeValue = this.taskDetail.end_at ? [this.taskDetail.start_at, this.taskDetail.end_at] : [];
|
this.timeValue = this.taskDetail.end_at ? [this.taskDetail.start_at, this.taskDetail.end_at] : [];
|
||||||
@ -1497,6 +1561,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
timeClear() {
|
timeClear() {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.updateData('times', {
|
this.updateData('times', {
|
||||||
start_at: false,
|
start_at: false,
|
||||||
end_at: false,
|
end_at: false,
|
||||||
@ -1505,6 +1573,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
timeOk() {
|
timeOk() {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const times = $A.newDateString(this.timeValue, "YYYY-MM-DD HH:mm");
|
const times = $A.newDateString(this.timeValue, "YYYY-MM-DD HH:mm");
|
||||||
this.updateData('times', {
|
this.updateData('times', {
|
||||||
start_at: times[0],
|
start_at: times[0],
|
||||||
@ -1514,6 +1586,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
addsubOpen() {
|
addsubOpen() {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.addsubShow = true;
|
this.addsubShow = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.addsub.focus()
|
this.$refs.addsub.focus()
|
||||||
@ -1537,6 +1613,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onAddsub() {
|
onAddsub() {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.addsubName == '') {
|
if (this.addsubName == '') {
|
||||||
$A.messageError('任务描述不能为空');
|
$A.messageError('任务描述不能为空');
|
||||||
return;
|
return;
|
||||||
@ -1602,6 +1682,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
removeRelatedTask(item) {
|
removeRelatedTask(item) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!item || !item.related_task_id) {
|
if (!item || !item.related_task_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1636,6 +1720,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onPriority(event) {
|
onPriority(event) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const list = this.taskPriority.map(item => {
|
const list = this.taskPriority.map(item => {
|
||||||
return {
|
return {
|
||||||
label: item.name,
|
label: item.name,
|
||||||
@ -1655,6 +1743,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onLoop(event) {
|
onLoop(event) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const list = this.loops.map(item => {
|
const list = this.loops.map(item => {
|
||||||
return {
|
return {
|
||||||
label: item.label,
|
label: item.label,
|
||||||
@ -1684,6 +1776,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onAddItem(event) {
|
onAddItem(event) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const list = this.menuList.map(item => {
|
const list = this.menuList.map(item => {
|
||||||
return {
|
return {
|
||||||
label: item.name,
|
label: item.name,
|
||||||
@ -1702,6 +1798,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
dropAddItem(command) {
|
dropAddItem(command) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'tag':
|
case 'tag':
|
||||||
this.tagForce = true;
|
this.tagForce = true;
|
||||||
@ -1765,6 +1865,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onEventMore(e) {
|
onEventMore(e) {
|
||||||
|
|
||||||
if (['image', 'file'].includes(e)) {
|
if (['image', 'file'].includes(e)) {
|
||||||
this.onUploadClick(false)
|
this.onUploadClick(false)
|
||||||
}
|
}
|
||||||
@ -1776,6 +1877,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
msgDialog(sendType = null) {
|
msgDialog(sendType = null) {
|
||||||
|
|
||||||
if (this.sendLoad > 0 || this.openLoad > 0) {
|
if (this.sendLoad > 0 || this.openLoad > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1895,6 +1997,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteFile(file) {
|
deleteFile(file) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$set(file, '_show_menu', false);
|
this.$set(file, '_show_menu', false);
|
||||||
this.$store.dispatch("forgetTaskFile", file.id)
|
this.$store.dispatch("forgetTaskFile", file.id)
|
||||||
//
|
//
|
||||||
@ -1910,6 +2016,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openMenu(event, task) {
|
openMenu(event, task) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const el = this.$refs[`taskMenu_${task.id}`];
|
const el = this.$refs[`taskMenu_${task.id}`];
|
||||||
el && el.handleClick(event)
|
el && el.handleClick(event)
|
||||||
},
|
},
|
||||||
@ -2010,12 +2119,18 @@ export default {
|
|||||||
okText: this.$L('立即下载'),
|
okText: this.$L('立即下载'),
|
||||||
content: `${file.name} (${$A.bytesToSize(file.size)})`,
|
content: `${file.name} (${$A.bytesToSize(file.size)})`,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
this.$store.dispatch('downUrl', $A.apiUrl(`project/task/filedown?file_id=${file.id}`))
|
const departmentOwnerIds = (this.$store.state.cacheDepartmentOwnerIds || []).join(',')
|
||||||
|
const url = $A.urlAddParams(`project/task/filedown?file_id=${file.id}`, departmentOwnerIds ? {department_owner_ids: departmentOwnerIds} : {})
|
||||||
|
this.$store.dispatch('downUrl', $A.apiUrl(url))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
showCisibleDropdown(event){
|
showCisibleDropdown(event){
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const list = [
|
const list = [
|
||||||
{label: '项目人员', value: 1},
|
{label: '项目人员', value: 1},
|
||||||
{label: '任务人员', value: 2},
|
{label: '任务人员', value: 2},
|
||||||
@ -2033,6 +2148,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showAtDropdown(event){
|
showAtDropdown(event){
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.timeOpen = false
|
this.timeOpen = false
|
||||||
const list = [
|
const list = [
|
||||||
{label: '任务延期', value: 1},
|
{label: '任务延期', value: 1},
|
||||||
@ -2060,6 +2179,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
dropVisible(command) {
|
dropVisible(command) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 1:
|
case 1:
|
||||||
case 2:
|
case 2:
|
||||||
@ -2077,6 +2200,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
dropDeadline(command) {
|
dropDeadline(command) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 1:
|
case 1:
|
||||||
this.delayTaskQuicks = [
|
this.delayTaskQuicks = [
|
||||||
@ -2108,6 +2235,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onDelay(){
|
onDelay(){
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$refs.formDelayTaskRef.validate((valid) => {
|
this.$refs.formDelayTaskRef.validate((valid) => {
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return
|
return
|
||||||
@ -2137,8 +2268,10 @@ export default {
|
|||||||
const list = [
|
const list = [
|
||||||
{label: '查看附件', value: 1},
|
{label: '查看附件', value: 1},
|
||||||
{label: '下载附件', value: 2},
|
{label: '下载附件', value: 2},
|
||||||
{label: '删除附件', value: 3, style: {color:'#FF7070'}},
|
|
||||||
];
|
];
|
||||||
|
if (!this.isDepartmentReadonly) {
|
||||||
|
list.push({label: '删除附件', value: 3, style: {color:'#FF7070'}});
|
||||||
|
}
|
||||||
this.$store.commit('menu/operation', {
|
this.$store.commit('menu/operation', {
|
||||||
event,
|
event,
|
||||||
list,
|
list,
|
||||||
@ -2179,6 +2312,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onTagAdd(tagName) {
|
onTagAdd(tagName) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 避免关闭选择框时触发更新
|
// 避免关闭选择框时触发更新
|
||||||
this.tagValue = this.getTag;
|
this.tagValue = this.getTag;
|
||||||
this.tagBakValue = $A.cloneJSON(this.tagValue);
|
this.tagBakValue = $A.cloneJSON(this.tagValue);
|
||||||
@ -2188,6 +2325,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onTagAddSave(result) {
|
onTagAddSave(result) {
|
||||||
|
if (this.isDepartmentReadonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const current = this.tagValue;
|
const current = this.tagValue;
|
||||||
const addData = result.filter(({data}) => data && data.id > 0).map(({data}) => data);
|
const addData = result.filter(({data}) => data && data.id > 0).map(({data}) => data);
|
||||||
// 合并数组,如果有重名标签则使用新添加的标签数据
|
// 合并数组,如果有重名标签则使用新添加的标签数据
|
||||||
|
|||||||
@ -17,10 +17,10 @@
|
|||||||
:class="['sub-icon', taskOpen[item.id] ? 'active' : '']"
|
:class="['sub-icon', taskOpen[item.id] ? 'active' : '']"
|
||||||
type="ios-arrow-forward"
|
type="ios-arrow-forward"
|
||||||
@click="getSublist(item)"/>
|
@click="getSublist(item)"/>
|
||||||
<TaskMenu :ref="`taskMenu_${item.id}`" :task="item"/>
|
<TaskMenu v-if="!readonly" :ref="`taskMenu_${item.id}`" :task="item"/>
|
||||||
<div class="item-title" @click="openTask(item)">
|
<div class="item-title" @click="openTask(item)">
|
||||||
<!--工作流状态-->
|
<!--工作流状态-->
|
||||||
<span v-if="item.flow_item_name" :class="item.flow_item_status" @click.stop="openMenu($event, item)">{{item.flow_item_name}}</span>
|
<span v-if="item.flow_item_name" :class="item.flow_item_status" @click.stop="!readonly && openMenu($event, item)">{{item.flow_item_name}}</span>
|
||||||
<!--是否子任务-->
|
<!--是否子任务-->
|
||||||
<span v-if="item.sub_top === true">{{$L('子任务')}}</span>
|
<span v-if="item.sub_top === true">{{$L('子任务')}}</span>
|
||||||
<!--有多少个子任务-->
|
<!--有多少个子任务-->
|
||||||
@ -51,7 +51,7 @@
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
size="small"
|
size="small"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:disabled="item.sub_top === true"
|
:disabled="readonly || item.sub_top === true"
|
||||||
@command="dropTask(item, $event)">
|
@command="dropTask(item, $event)">
|
||||||
<div class="task-column">{{columnName(item.column_id)}}</div>
|
<div class="task-column">{{columnName(item.column_id)}}</div>
|
||||||
<EDropdownMenu slot="dropdown">
|
<EDropdownMenu slot="dropdown">
|
||||||
@ -66,7 +66,7 @@
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
size="small"
|
size="small"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:disabled="item.sub_top === true"
|
:disabled="readonly || item.sub_top === true"
|
||||||
@command="dropTask(item, $event)">
|
@command="dropTask(item, $event)">
|
||||||
<TaskPriority :backgroundColor="item.p_color">{{item.p_name || $L('未设置')}}</TaskPriority>
|
<TaskPriority :backgroundColor="item.p_color">{{item.p_name || $L('未设置')}}</TaskPriority>
|
||||||
<EDropdownMenu slot="dropdown">
|
<EDropdownMenu slot="dropdown">
|
||||||
@ -85,7 +85,7 @@
|
|||||||
<li v-for="(user, keyu) in ownerUser(item.task_user)" :key="keyu" v-if="keyu < 3">
|
<li v-for="(user, keyu) in ownerUser(item.task_user)" :key="keyu" v-if="keyu < 3">
|
||||||
<UserAvatar :userid="user.userid" size="32" :borderWidth="2" :borderColor="item.color" :showName="ownerUser(item.task_user).length === 1"/>
|
<UserAvatar :userid="user.userid" size="32" :borderWidth="2" :borderColor="item.color" :showName="ownerUser(item.task_user).length === 1"/>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="ownerUser(item.task_user).length === 0" class="no-owner">
|
<li v-if="!readonly && ownerUser(item.task_user).length === 0" class="no-owner">
|
||||||
<Button type="primary" size="small" @click.stop="openTask(item, true)">{{$L('领取任务')}}</Button>
|
<Button type="primary" size="small" @click.stop="openTask(item, true)">{{$L('领取任务')}}</Button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -107,11 +107,12 @@
|
|||||||
v-if="taskOpen[item.id]===true"
|
v-if="taskOpen[item.id]===true"
|
||||||
:list="subTask(item.id)"
|
:list="subTask(item.id)"
|
||||||
:parent-id="item.id"
|
:parent-id="item.id"
|
||||||
:fast-add-task="item.parent_id===0 && fastAddTask"
|
:fast-add-task="!readonly && item.parent_id===0 && fastAddTask"
|
||||||
:open-key="openKey"
|
:open-key="openKey"
|
||||||
@command="dropTask"/>
|
@command="dropTask"
|
||||||
|
:readonly="readonly"/>
|
||||||
</div>
|
</div>
|
||||||
<TaskAddSimple v-if="fastAddTask || parentId > 0" :parent-id="parentId" row-mode @on-priority="onPriority"/>
|
<TaskAddSimple v-if="!readonly && (fastAddTask || parentId > 0)" :parent-id="parentId" row-mode @on-priority="onPriority"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -151,6 +152,10 @@ export default {
|
|||||||
taskVisibilitys: {
|
taskVisibilitys: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -202,6 +207,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
dropTask(task, command) {
|
dropTask(task, command) {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const el = this.$refs[`taskMenu_${task.id}`];
|
const el = this.$refs[`taskMenu_${task.id}`];
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return;
|
return;
|
||||||
@ -228,6 +236,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onPriority(data) {
|
onPriority(data) {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$emit("on-priority", data)
|
this.$emit("on-priority", data)
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -260,6 +271,9 @@ export default {
|
|||||||
|
|
||||||
openTask(task, receive) {
|
openTask(task, receive) {
|
||||||
this.$store.dispatch("openTask", task)
|
this.$store.dispatch("openTask", task)
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (receive === true) {
|
if (receive === true) {
|
||||||
// 向任务窗口发送领取任务请求
|
// 向任务窗口发送领取任务请求
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -269,6 +283,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openMenu(event, task) {
|
openMenu(event, task) {
|
||||||
|
if (this.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const el = this.$refs[`taskMenu_${task.id}`];
|
const el = this.$refs[`taskMenu_${task.id}`];
|
||||||
if (el) {
|
if (el) {
|
||||||
el[0].handleClick(event)
|
el[0].handleClick(event)
|
||||||
|
|||||||
@ -75,6 +75,13 @@
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<div v-if="formDatum.project_invite == 'open'" class="form-tip">{{$L('开启:项目管理员可生成链接邀请成员加入项目。')}}</div>
|
<div v-if="formDatum.project_invite == 'open'" class="form-tip">{{$L('开启:项目管理员可生成链接邀请成员加入项目。')}}</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem :label="$L('部门负责人视角')" prop="department_owner_project_view">
|
||||||
|
<RadioGroup v-model="formDatum.department_owner_project_view">
|
||||||
|
<Radio label="open">{{$L('开启')}}</Radio>
|
||||||
|
<Radio label="close">{{$L('关闭')}}</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
<div v-if="formDatum.department_owner_project_view == 'open'" class="form-tip">{{$L('开启后,部门负责人/部门管理员可只读查看本部门及下级部门成员参与的项目和项目内全部任务。')}}</div>
|
||||||
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="block-setting-box">
|
<div class="block-setting-box">
|
||||||
@ -351,6 +358,7 @@ export default {
|
|||||||
}).then(({data}) => {
|
}).then(({data}) => {
|
||||||
if (save) {
|
if (save) {
|
||||||
$A.messageSuccess('修改成功');
|
$A.messageSuccess('修改成功');
|
||||||
|
this.$store.dispatch("getUserInfo").catch(() => {});
|
||||||
}
|
}
|
||||||
this.formDatum = data;
|
this.formDatum = data;
|
||||||
this.formDatum_bak = $A.cloneJSON(this.formDatum);
|
this.formDatum_bak = $A.cloneJSON(this.formDatum);
|
||||||
|
|||||||
207
resources/assets/js/store/actions.js
vendored
207
resources/assets/js/store/actions.js
vendored
@ -232,6 +232,31 @@ export default {
|
|||||||
], true)) {
|
], true)) {
|
||||||
params.encrypt = true
|
params.encrypt = true
|
||||||
}
|
}
|
||||||
|
const departmentOwnerReadonlyUrls = [
|
||||||
|
'project/lists',
|
||||||
|
'project/one',
|
||||||
|
'project/column/lists',
|
||||||
|
'project/task/lists',
|
||||||
|
'project/task/one',
|
||||||
|
'project/task/content',
|
||||||
|
'project/task/content_history',
|
||||||
|
'project/task/files',
|
||||||
|
'project/task/fileinfo',
|
||||||
|
'project/task/subdata',
|
||||||
|
'project/task/related',
|
||||||
|
'project/flow/list',
|
||||||
|
'project/log/lists',
|
||||||
|
'project/tag/list',
|
||||||
|
]
|
||||||
|
if (params.departmentOwner !== false
|
||||||
|
&& state.systemConfig.department_owner_project_view === 'open'
|
||||||
|
&& departmentOwnerReadonlyUrls.includes(params.url)
|
||||||
|
&& (state.cacheDepartmentOwnerIds || []).length > 0) {
|
||||||
|
if (!$A.isJson(params.data)) params.data = {}
|
||||||
|
if (params.data.department_owner_ids === undefined) {
|
||||||
|
params.data.department_owner_ids = state.cacheDepartmentOwnerIds.join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
if (params.encrypt) {
|
if (params.encrypt) {
|
||||||
const userAgent = window.navigator.userAgent;
|
const userAgent = window.navigator.userAgent;
|
||||||
if (window.systemInfo.debug === "yes"
|
if (window.systemInfo.debug === "yes"
|
||||||
@ -620,7 +645,7 @@ export default {
|
|||||||
* @param dispatch
|
* @param dispatch
|
||||||
* @param timeout
|
* @param timeout
|
||||||
*/
|
*/
|
||||||
getBasicData({state, dispatch}, timeout) {
|
async getBasicData({state, dispatch}, timeout) {
|
||||||
if (typeof timeout === "number") {
|
if (typeof timeout === "number") {
|
||||||
window.__getBasicDataTimer && clearTimeout(window.__getBasicDataTimer)
|
window.__getBasicDataTimer && clearTimeout(window.__getBasicDataTimer)
|
||||||
if (timeout > -1) {
|
if (timeout > -1) {
|
||||||
@ -640,7 +665,7 @@ export default {
|
|||||||
dispatch("getTaskPriority", 1000);
|
dispatch("getTaskPriority", 1000);
|
||||||
dispatch("getReportUnread", 1000);
|
dispatch("getReportUnread", 1000);
|
||||||
dispatch("getApproveUnread", 1000);
|
dispatch("getApproveUnread", 1000);
|
||||||
dispatch("getProjectByQueue");
|
dispatch("getProjectsForDepartmentOwnerView").catch(() => {});
|
||||||
dispatch("getTaskForDashboard");
|
dispatch("getTaskForDashboard");
|
||||||
dispatch("dialogMsgRead");
|
dispatch("dialogMsgRead");
|
||||||
dispatch("updateMicroAppsStatus");
|
dispatch("updateMicroAppsStatus");
|
||||||
@ -1181,6 +1206,7 @@ export default {
|
|||||||
array: [
|
array: [
|
||||||
'cacheUserBasic',
|
'cacheUserBasic',
|
||||||
'cacheProjects',
|
'cacheProjects',
|
||||||
|
'cacheDepartmentOwnerIds',
|
||||||
'cacheColumns',
|
'cacheColumns',
|
||||||
'cacheTasks',
|
'cacheTasks',
|
||||||
'cacheProjectParameter',
|
'cacheProjectParameter',
|
||||||
@ -1551,6 +1577,146 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取有效的部门负责人视角部门ID
|
||||||
|
* @param state
|
||||||
|
* @param ids
|
||||||
|
* @returns {Array<number>}
|
||||||
|
*/
|
||||||
|
normalizeDepartmentOwnerIds({state}, ids) {
|
||||||
|
const validIds = (state.userInfo.managed_departments || []).map(item => parseInt(item.id));
|
||||||
|
if (!$A.isArray(ids)) ids = [];
|
||||||
|
return ids
|
||||||
|
.map(id => parseInt(id))
|
||||||
|
.filter(id => id > 0 && (validIds.length === 0 || validIds.includes(id)));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载负责人视角下的项目列表
|
||||||
|
* @param state
|
||||||
|
* @param dispatch
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async getProjectsForDepartmentOwnerView({state, dispatch}) {
|
||||||
|
await dispatch("systemSetting").catch(() => {});
|
||||||
|
if (state.systemConfig.department_owner_project_view !== 'open') {
|
||||||
|
state.cacheDepartmentOwnerIds = [];
|
||||||
|
await $A.IDBSet("cacheDepartmentOwnerIds", []).catch(() => {});
|
||||||
|
dispatch("getProjectByQueue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const restoredDepartmentOwnerIds = await dispatch("restoreDepartmentOwnerView");
|
||||||
|
if ((restoredDepartmentOwnerIds || []).length > 0) {
|
||||||
|
await dispatch("getProjects", {
|
||||||
|
__replace: true,
|
||||||
|
department_owner_ids: restoredDepartmentOwnerIds.join(',')
|
||||||
|
});
|
||||||
|
state.cacheDepartmentOwnerIds = restoredDepartmentOwnerIds;
|
||||||
|
await $A.IDBSet("cacheDepartmentOwnerIds", restoredDepartmentOwnerIds).catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch("getProjectByQueue");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复部门负责人视角
|
||||||
|
* @param state
|
||||||
|
* @param dispatch
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async restoreDepartmentOwnerView({state, dispatch}) {
|
||||||
|
if (state.departmentOwnerViewRestored) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (state.systemConfig.department_owner_project_view !== 'open') {
|
||||||
|
state.cacheDepartmentOwnerIds = [];
|
||||||
|
await $A.IDBSet("cacheDepartmentOwnerIds", []).catch(() => {});
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
state.departmentOwnerViewRestored = true;
|
||||||
|
const ids = await $A.IDBArray("cacheDepartmentOwnerIds", []);
|
||||||
|
if (!ids.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const restored = await dispatch("normalizeDepartmentOwnerIds", ids);
|
||||||
|
if (restored.length > 0) {
|
||||||
|
state.departmentOwnerProjectsRefreshing = true;
|
||||||
|
}
|
||||||
|
state.cacheDepartmentOwnerIds = restored;
|
||||||
|
await $A.IDBSet("cacheDepartmentOwnerIds", restored).catch(() => {});
|
||||||
|
return restored;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置部门负责人视角
|
||||||
|
* @param state
|
||||||
|
* @param dispatch
|
||||||
|
* @param ids
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async setDepartmentOwnerIds({state, dispatch}, ids) {
|
||||||
|
if (state.systemConfig.department_owner_project_view !== 'open') {
|
||||||
|
ids = [];
|
||||||
|
}
|
||||||
|
const normalized = await dispatch("normalizeDepartmentOwnerIds", ids);
|
||||||
|
const oldValue = (state.cacheDepartmentOwnerIds || []).map(id => parseInt(id)).sort().join(',');
|
||||||
|
const newValue = normalized.slice().sort().join(',');
|
||||||
|
if (oldValue === newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.departmentOwnerProjectsRefreshing = true;
|
||||||
|
await dispatch("refreshDepartmentOwnerProjects", normalized);
|
||||||
|
state.cacheDepartmentOwnerIds = normalized;
|
||||||
|
await $A.IDBSet("cacheDepartmentOwnerIds", normalized).catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换部门负责人视角后刷新项目数据
|
||||||
|
* @param state
|
||||||
|
* @param dispatch
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async refreshDepartmentOwnerProjects({state, dispatch}, ownerIds = state.cacheDepartmentOwnerIds) {
|
||||||
|
const currentProjectId = state.projectId;
|
||||||
|
ownerIds = (ownerIds || []).map(id => parseInt(id)).filter(id => id > 0);
|
||||||
|
state.departmentOwnerProjectsRefreshing = true;
|
||||||
|
state.callAt = state.callAt.filter(item => {
|
||||||
|
const key = String(item.key);
|
||||||
|
return !key.startsWith('projects::') && !key.startsWith('tasks::');
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await dispatch("getProjects", {
|
||||||
|
__replace: true,
|
||||||
|
department_owner_ids: ownerIds.join(',')
|
||||||
|
});
|
||||||
|
if (currentProjectId > 0) {
|
||||||
|
const exists = state.cacheProjects.find(({id}) => id == currentProjectId);
|
||||||
|
if (!exists) {
|
||||||
|
const project = $A.cloneJSON(state.cacheProjects).sort((a, b) => {
|
||||||
|
if (a.top_at || b.top_at) {
|
||||||
|
return $A.sortDay(b.top_at, a.top_at);
|
||||||
|
}
|
||||||
|
return b.id - a.id;
|
||||||
|
}).find(({id}) => id);
|
||||||
|
if (project) {
|
||||||
|
$A.goForward({name: 'manage-project', params: {projectId: project.id}});
|
||||||
|
} else {
|
||||||
|
$A.goForward({name: 'manage-dashboard'});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await dispatch("getProjectOne", currentProjectId).catch(() => {});
|
||||||
|
await dispatch("getTaskForProject", currentProjectId).catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if ((state.cacheDepartmentOwnerIds || []).length === 0 && currentProjectId > 0) {
|
||||||
|
$A.goForward({name: 'manage-dashboard'});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
state.departmentOwnerProjectsRefreshing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/** *****************************************************************************************/
|
/** *****************************************************************************************/
|
||||||
/** ************************************** 项目 **********************************************/
|
/** ************************************** 项目 **********************************************/
|
||||||
/** *****************************************************************************************/
|
/** *****************************************************************************************/
|
||||||
@ -1642,6 +1808,24 @@ export default {
|
|||||||
* @returns {Promise<unknown>}
|
* @returns {Promise<unknown>}
|
||||||
*/
|
*/
|
||||||
getProjects({state, dispatch}, requestData) {
|
getProjects({state, dispatch}, requestData) {
|
||||||
|
if (!$A.isJson(requestData)) {
|
||||||
|
requestData = {}
|
||||||
|
}
|
||||||
|
const replace = requestData.__replace === true;
|
||||||
|
delete requestData.__replace;
|
||||||
|
if (state.systemConfig.department_owner_project_view !== 'open') {
|
||||||
|
delete requestData.department_owner_ids
|
||||||
|
} else if (requestData.department_owner_ids === undefined) {
|
||||||
|
if ((state.cacheDepartmentOwnerIds || []).length > 0) {
|
||||||
|
requestData.department_owner_ids = state.cacheDepartmentOwnerIds.join(',')
|
||||||
|
} else {
|
||||||
|
delete requestData.department_owner_ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (replace) {
|
||||||
|
state.callAt = state.callAt.filter(item => !String(item.key).startsWith('projects::'))
|
||||||
|
$A.IDBSet("callAt", state.callAt).catch(() => {})
|
||||||
|
}
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
if (state.userId === 0) {
|
if (state.userId === 0) {
|
||||||
state.cacheProjects = [];
|
state.cacheProjects = [];
|
||||||
@ -1657,6 +1841,9 @@ export default {
|
|||||||
url: 'project/lists',
|
url: 'project/lists',
|
||||||
data: callData.get()
|
data: callData.get()
|
||||||
}).then(({data}) => {
|
}).then(({data}) => {
|
||||||
|
if (replace) {
|
||||||
|
state.cacheProjects = [];
|
||||||
|
}
|
||||||
dispatch("saveProject", data.data);
|
dispatch("saveProject", data.data);
|
||||||
callData.save(data).then(ids => dispatch("forgetProject", {id: ids}))
|
callData.save(data).then(ids => dispatch("forgetProject", {id: ids}))
|
||||||
state.projectTotal = data.total_all;
|
state.projectTotal = data.total_all;
|
||||||
@ -1666,6 +1853,9 @@ export default {
|
|||||||
console.warn(e);
|
console.warn(e);
|
||||||
reject(e)
|
reject(e)
|
||||||
}).finally(_ => {
|
}).finally(_ => {
|
||||||
|
if (replace) {
|
||||||
|
state.departmentOwnerProjectsRefreshing = false;
|
||||||
|
}
|
||||||
state.loadProjects--;
|
state.loadProjects--;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1702,7 +1892,7 @@ export default {
|
|||||||
dispatch("call", {
|
dispatch("call", {
|
||||||
url: 'project/one',
|
url: 'project/one',
|
||||||
data: {
|
data: {
|
||||||
project_id,
|
project_id
|
||||||
},
|
},
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -2133,9 +2323,14 @@ export default {
|
|||||||
* @returns {Promise<unknown>}
|
* @returns {Promise<unknown>}
|
||||||
*/
|
*/
|
||||||
getTasks({state, dispatch}, requestData) {
|
getTasks({state, dispatch}, requestData) {
|
||||||
if (requestData === null) {
|
if (!$A.isJson(requestData)) {
|
||||||
requestData = {}
|
requestData = {}
|
||||||
}
|
}
|
||||||
|
if ((state.cacheDepartmentOwnerIds || []).length > 0) {
|
||||||
|
requestData.department_owner_ids = state.cacheDepartmentOwnerIds.join(',')
|
||||||
|
} else {
|
||||||
|
delete requestData.department_owner_ids
|
||||||
|
}
|
||||||
const callData = $callData('tasks', requestData, state)
|
const callData = $callData('tasks', requestData, state)
|
||||||
//
|
//
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
@ -2435,7 +2630,7 @@ export default {
|
|||||||
dispatch("call", {
|
dispatch("call", {
|
||||||
url: 'project/task/content',
|
url: 'project/task/content',
|
||||||
data: {
|
data: {
|
||||||
task_id,
|
task_id
|
||||||
},
|
},
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
dispatch("saveTaskContent", result.data)
|
dispatch("saveTaskContent", result.data)
|
||||||
@ -2483,7 +2678,7 @@ export default {
|
|||||||
dispatch("call", {
|
dispatch("call", {
|
||||||
url: 'project/task/files',
|
url: 'project/task/files',
|
||||||
data: {
|
data: {
|
||||||
task_id,
|
task_id
|
||||||
},
|
},
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
result.data.forEach((data) => {
|
result.data.forEach((data) => {
|
||||||
|
|||||||
3
resources/assets/js/store/state.js
vendored
3
resources/assets/js/store/state.js
vendored
@ -103,6 +103,9 @@ export default {
|
|||||||
cacheColumns: [],
|
cacheColumns: [],
|
||||||
cacheTasks: [],
|
cacheTasks: [],
|
||||||
cacheProjectParameter: [],
|
cacheProjectParameter: [],
|
||||||
|
cacheDepartmentOwnerIds: [],
|
||||||
|
departmentOwnerViewRestored: false,
|
||||||
|
departmentOwnerProjectsRefreshing: false,
|
||||||
|
|
||||||
// Emoji
|
// Emoji
|
||||||
cacheEmojis: [],
|
cacheEmojis: [],
|
||||||
|
|||||||
36
resources/assets/sass/dark.scss
vendored
36
resources/assets/sass/dark.scss
vendored
@ -467,6 +467,17 @@ body.dark-mode-reverse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.menu-project {
|
||||||
|
> ul {
|
||||||
|
> li {
|
||||||
|
.project-h1 {
|
||||||
|
.readonly-project-avatar {
|
||||||
|
color: #1c1917;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -777,4 +788,29 @@ body.dark-mode-reverse {
|
|||||||
filter: invert(100%);
|
filter: invert(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.department-owner-view-modal {
|
||||||
|
.department-owner-view-icon {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.project-list {
|
||||||
|
.list-search {
|
||||||
|
.owner-view-button {
|
||||||
|
> em {
|
||||||
|
box-shadow: 0 0 0 2px #1c1917;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> ul {
|
||||||
|
> li {
|
||||||
|
.project-item {
|
||||||
|
.readonly-project-avatar {
|
||||||
|
color: #1c1917;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
resources/assets/sass/pages/common.scss
vendored
8
resources/assets/sass/pages/common.scss
vendored
@ -531,6 +531,14 @@ body {
|
|||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ivu-alert-icon {
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ivu-alert-message {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.vuepress-markdown-body {
|
.vuepress-markdown-body {
|
||||||
h1,
|
h1,
|
||||||
h2 {
|
h2 {
|
||||||
|
|||||||
@ -57,6 +57,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.owner-view-button {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(47, 128, 237, 0.08);
|
||||||
|
color: #808695;
|
||||||
|
> i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
> em {
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: -5px;
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: #ffffff;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 0 0 2px #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-project-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.owner-project-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 8px 12px;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
.owner-project-tab {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 9px;
|
||||||
|
color: #808695;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
> span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ivu-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #17233d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> ul {
|
> ul {
|
||||||
@ -202,6 +276,25 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.readonly-owner-avatar {
|
||||||
|
margin-left: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.readonly-project-avatar {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-left: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
background-color: #5BC7B0;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
.project-panel {
|
.project-panel {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
.project-readonly-alert {
|
||||||
|
margin: 0 32px 8px;
|
||||||
|
}
|
||||||
.project-titbox {
|
.project-titbox {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 32px 32px 4px;
|
padding: 32px 32px 4px;
|
||||||
@ -1050,6 +1053,9 @@
|
|||||||
|
|
||||||
body.window-portrait {
|
body.window-portrait {
|
||||||
.project-panel {
|
.project-panel {
|
||||||
|
.project-readonly-alert {
|
||||||
|
margin: 0 16px 8px;
|
||||||
|
}
|
||||||
.project-titbox {
|
.project-titbox {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@ -227,6 +227,9 @@
|
|||||||
.scrollbar-content {
|
.scrollbar-content {
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
|
.task-readonly-alert {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
.receive-box {
|
.receive-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
66
resources/assets/sass/pages/page-manage.scss
vendored
66
resources/assets/sass/pages/page-manage.scss
vendored
@ -29,7 +29,7 @@
|
|||||||
.menu-base {
|
.menu-base {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
background: #F4F5F7;
|
background: #F4F5F7;
|
||||||
@ -81,6 +81,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.owner-project-tabs {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #eeeeee;
|
||||||
|
.owner-project-tab {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 9px;
|
||||||
|
color: #808695;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
> span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ivu-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #17233d;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.menu-project {
|
.menu-project {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -148,6 +186,24 @@
|
|||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.readonly-owner-avatar {
|
||||||
|
margin-left: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.readonly-project-avatar {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-left: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
background-color: #5BC7B0;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.project-h2 {
|
.project-h2 {
|
||||||
display: none;
|
display: none;
|
||||||
@ -205,6 +261,12 @@
|
|||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.nothing {
|
||||||
|
padding: 18px 8px;
|
||||||
|
color: #999999;
|
||||||
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -515,7 +577,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (height <= 640px) {
|
@media (height <= 800px) {
|
||||||
.page-manage {
|
.page-manage {
|
||||||
.manage-box-menu {
|
.manage-box-menu {
|
||||||
.menu-base {
|
.menu-base {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user