mirror of
https://github.com/kuaifan/dootask.git
synced 2026-05-24 01:14:06 +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\ProjectTaskRelation;
|
||||
use App\Models\ProjectTaskAiEvent;
|
||||
use App\Models\UserDepartment;
|
||||
use App\Module\AiTaskSuggestion;
|
||||
use App\Observers\ProjectTaskObserver;
|
||||
|
||||
@ -128,6 +129,7 @@ class ProjectController extends AbstractController
|
||||
public function lists()
|
||||
{
|
||||
$user = User::auth();
|
||||
$departmentView = UserDepartment::ownerViewContext($user);
|
||||
//
|
||||
$all = Request::input('all');
|
||||
$type = Request::input('type', 'all');
|
||||
@ -141,6 +143,9 @@ class ProjectController extends AbstractController
|
||||
if ($all) {
|
||||
$user->identity('admin');
|
||||
$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 {
|
||||
$builder = Project::authData();
|
||||
}
|
||||
@ -180,8 +185,9 @@ class ProjectController extends AbstractController
|
||||
->orderBy('project_users.sort')
|
||||
->orderByDesc('projects.id')
|
||||
->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 = UserDepartment::appendDepartmentReadonlyProject($array, $departmentView);
|
||||
if ($getuserid == 'yes') {
|
||||
$array['userid_list'] = ProjectUser::whereProjectId($project->id)->pluck('userid')->toArray();
|
||||
}
|
||||
@ -250,13 +256,15 @@ class ProjectController extends AbstractController
|
||||
public function one()
|
||||
{
|
||||
$user = User::auth();
|
||||
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||
//
|
||||
$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), [
|
||||
'project_user' => $project->projectUser,
|
||||
]);
|
||||
$data = UserDepartment::appendDepartmentReadonlyProject($data, $departmentView);
|
||||
//
|
||||
return Base::retSuccess('success', $data);
|
||||
}
|
||||
@ -999,7 +1007,7 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
$project_id = intval(Request::input('project_id'));
|
||||
// 项目
|
||||
$project = Project::userProject($project_id);
|
||||
$project = Project::findForDepartmentView($project_id);
|
||||
//
|
||||
$list = ProjectColumn::whereProjectId($project->id)
|
||||
->orderBy('sort')
|
||||
@ -1230,6 +1238,7 @@ class ProjectController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
$userid = $user->userid;
|
||||
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||
//
|
||||
$parent_id = intval(Request::input('parent_id'));
|
||||
$project_id = intval(Request::input('project_id'));
|
||||
@ -1294,7 +1303,7 @@ class ProjectController extends AbstractController
|
||||
if ($parent_id > 0) {
|
||||
$isArchived = str_replace(['all', 'yes', 'no'], [null, false, true], $archived);
|
||||
$isDeleted = str_replace(['all', 'yes', 'no'], [null, false, true], $deleted);
|
||||
ProjectTask::userTask($parent_id, $isArchived, $isDeleted);
|
||||
ProjectTask::findForDepartmentView($parent_id, $isArchived, $isDeleted);
|
||||
$scopeAll = true;
|
||||
$archived = 'all';
|
||||
$builder->where('project_tasks.parent_id', $parent_id);
|
||||
@ -1302,17 +1311,23 @@ class ProjectController extends AbstractController
|
||||
$builder->where('project_tasks.parent_id', 0);
|
||||
}
|
||||
if ($project_id > 0) {
|
||||
Project::userProject($project_id);
|
||||
if (!UserDepartment::isDepartmentReadonlyProject($departmentView, $project_id)) {
|
||||
Project::userProject($project_id);
|
||||
}
|
||||
$scopeAll = true;
|
||||
$builder->where('project_tasks.project_id', $project_id);
|
||||
}
|
||||
if (!$scopeAll && $scope === 'all_project') {
|
||||
$scopeAll = true;
|
||||
$builder->whereIn('project_tasks.project_id', function ($query) use ($userid) {
|
||||
$query->select('project_id')
|
||||
->from('project_users')
|
||||
->where('userid', $userid);
|
||||
});
|
||||
if ($departmentView['enabled']) {
|
||||
$builder->whereIn('project_tasks.project_id', array_values(array_unique(array_merge($departmentView['own_project_ids'], $departmentView['project_ids']))));
|
||||
} else {
|
||||
$builder->whereIn('project_tasks.project_id', function ($query) use ($userid) {
|
||||
$query->select('project_id')
|
||||
->from('project_users')
|
||||
->where('userid', $userid);
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($scopeAll) {
|
||||
$builder->allData();
|
||||
@ -1391,12 +1406,15 @@ class ProjectController extends AbstractController
|
||||
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
|
||||
$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->orWhere("project_users.userid", $userid);
|
||||
$query->orWhere("project_task_users.userid", $userid);
|
||||
$query->orWhere("project_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) {
|
||||
@ -1439,6 +1457,7 @@ class ProjectController extends AbstractController
|
||||
$data = $list->toArray();
|
||||
// 还原字段
|
||||
foreach($data['data'] as &$item){
|
||||
$item['department_readonly'] = UserDepartment::isDepartmentReadonlyProject($departmentView, intval($item['project_id']));
|
||||
$item['file_num'] = $item['_file_num'] ?: 0;
|
||||
$item['msg_num'] = $item['_msg_num'] ?: 0;
|
||||
$item['sub_num'] = $item['_sub_num'] ?: 0;
|
||||
@ -2018,17 +2037,18 @@ class ProjectController extends AbstractController
|
||||
public function task__one()
|
||||
{
|
||||
$user = User::auth();
|
||||
$departmentView = UserDepartment::ownerViewContext($user, true);
|
||||
//
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
$archived = Request::input('archived', 'no');
|
||||
//
|
||||
$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)
|
||||
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
|
||||
->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(); //任务负责人、协助人
|
||||
$subTaskUserids = ProjectTaskUser::whereTaskPid($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['department_readonly'] = UserDepartment::isDepartmentReadonlyProject($departmentView, intval($task->project_id));
|
||||
$data['project_name'] = $task->project?->name;
|
||||
$data['column_name'] = $task->projectColumn?->name;
|
||||
$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]);
|
||||
}
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
$task = ProjectTask::findForDepartmentView($task_id);
|
||||
//
|
||||
return Base::retSuccess('success', [
|
||||
'id' => $task->id,
|
||||
@ -2099,7 +2120,7 @@ class ProjectController extends AbstractController
|
||||
return Base::retError('参数错误', ['task_id' => $task_id]);
|
||||
}
|
||||
|
||||
$task = ProjectTask::userTask($task_id, null);
|
||||
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||
|
||||
$relations = ProjectTaskRelation::whereTaskId($task->id)
|
||||
->orderByDesc('updated_at')
|
||||
@ -2117,7 +2138,7 @@ class ProjectController extends AbstractController
|
||||
$relatedTasks = [];
|
||||
foreach ($relatedTaskIds as $relatedId) {
|
||||
try {
|
||||
$relatedTask = ProjectTask::userTask($relatedId, null, true, ['project', 'projectColumn']);
|
||||
$relatedTask = ProjectTask::findForDepartmentView($relatedId, null, true, ['project', 'projectColumn']);
|
||||
|
||||
$flowItemParts = explode('|', $relatedTask->flow_item_name ?: '');
|
||||
$flowItemStatus = $flowItemParts[0] ?? '';
|
||||
@ -2243,7 +2264,7 @@ class ProjectController extends AbstractController
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
$history_id = intval(Request::input('history_id'));
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id, null);
|
||||
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||
//
|
||||
if ($history_id > 0) {
|
||||
$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 = ProjectTask::userTask($task_id, null);
|
||||
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||
//
|
||||
$data = ProjectTaskContent::select(['id', 'task_id', 'desc', 'userid', 'created_at'])
|
||||
->whereTaskId($task->id)
|
||||
@ -2312,7 +2333,7 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
$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);
|
||||
}
|
||||
@ -2401,7 +2422,7 @@ class ProjectController extends AbstractController
|
||||
$data = $file->toArray();
|
||||
$data['path'] = $file->getRawOriginal('path');
|
||||
//
|
||||
ProjectTask::userTask($file->task_id, null);
|
||||
ProjectTask::findForDepartmentView($file->task_id, null);
|
||||
//
|
||||
UserRecentItem::record(
|
||||
$user->userid,
|
||||
@ -2442,7 +2463,7 @@ class ProjectController extends AbstractController
|
||||
abort_if(empty($file), 403, "This file not exist.");
|
||||
//
|
||||
try {
|
||||
ProjectTask::userTask($file->task_id, null);
|
||||
ProjectTask::findForDepartmentView($file->task_id, null);
|
||||
} catch (\Throwable $e) {
|
||||
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 = Project::userProject($project_id, true);
|
||||
$project = Project::findForDepartmentView($project_id, true);
|
||||
//
|
||||
$list = ProjectFlow::with(['ProjectFlowItem'])->whereProjectId($project->id)->get();
|
||||
return Base::retSuccess('success', $list);
|
||||
@ -3488,10 +3509,10 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
$builder = ProjectLog::select(["*"]);
|
||||
if ($task_id > 0) {
|
||||
$task = ProjectTask::userTask($task_id, null);
|
||||
$task = ProjectTask::findForDepartmentView($task_id, null);
|
||||
$builder->whereTaskId($task->id);
|
||||
} else {
|
||||
$project = Project::userProject($project_id);
|
||||
$project = Project::findForDepartmentView($project_id);
|
||||
$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_time',
|
||||
'task_ai_auto_analyze',
|
||||
'department_owner_project_view',
|
||||
])) {
|
||||
unset($all[$key]);
|
||||
}
|
||||
@ -148,6 +149,7 @@ class SystemController extends AbstractController
|
||||
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
|
||||
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
|
||||
$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_version'] = Base::getVersion();
|
||||
//
|
||||
|
||||
@ -404,9 +404,22 @@ class UsersController extends AbstractController
|
||||
$data['nickname_original'] = $user->getRawOriginal('nickname');
|
||||
$data['department_name'] = $user->getDepartmentName();
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 获取我的部门列表
|
||||
*
|
||||
@ -3272,7 +3285,7 @@ class UsersController extends AbstractController
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
//
|
||||
ProjectTask::userTask($task_id, null, null);
|
||||
ProjectTask::findForDepartmentView($task_id, null, null);
|
||||
//
|
||||
UserTaskBrowse::recordBrowse($user->userid, $task_id);
|
||||
//
|
||||
|
||||
@ -723,4 +723,31 @@ class Project extends AbstractModel
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务(含部门负责人只读视角兜底)
|
||||
* @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
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use Cache;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* App\Models\UserDepartment
|
||||
@ -411,6 +413,93 @@ class UserDepartment extends AbstractModel
|
||||
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小时)
|
||||
* @param int|array $ids
|
||||
@ -453,4 +542,70 @@ class UserDepartment extends AbstractModel
|
||||
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()) {
|
||||
return $dialog;
|
||||
}
|
||||
if ($projectId > 0 && $checkOwner === false) {
|
||||
$departmentView = UserDepartment::ownerViewContext(User::auth(), true);
|
||||
if (UserDepartment::isDepartmentReadonlyProject($departmentView, $projectId)) {
|
||||
return $dialog;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'okr':
|
||||
|
||||
@ -73,6 +73,10 @@ export default {
|
||||
itemWidth: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -328,6 +332,12 @@ export default {
|
||||
this.onDateMove(e.clientX);
|
||||
},
|
||||
itemMouseDown(e, item) {
|
||||
if (this.readonly) {
|
||||
if (e.target.classList.contains('timeline-title')) {
|
||||
this.clickItem(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
this.onItemMove(item, e.target, e.clientX);
|
||||
},
|
||||
@ -347,6 +357,9 @@ export default {
|
||||
};
|
||||
},
|
||||
onItemMove(item, target, clientX) {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
let type = 'moveX';
|
||||
if (target.classList.contains('timeline-resizer')) {
|
||||
type = 'moveW';
|
||||
@ -384,6 +397,11 @@ export default {
|
||||
}
|
||||
},
|
||||
onMoveOver(target) {
|
||||
if (this.readonly) {
|
||||
this.mouseItem = null;
|
||||
this.dateMove = null;
|
||||
return;
|
||||
}
|
||||
if (this.mouseItem != null) {
|
||||
const {start, end} = this.mouseItem.time;
|
||||
let isM = false;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
:option-full="optionFull"
|
||||
:placeholder="placeholder"
|
||||
:placeholderFull="placeholderFull"
|
||||
:readOnly="windowTouch"
|
||||
:readOnly="readonly || windowTouch"
|
||||
:readOnlyFull="false"
|
||||
:readOnlyImagePreview="false"
|
||||
@on-blur="onBlur"
|
||||
@ -24,10 +24,10 @@
|
||||
transfer>
|
||||
<div :style="{userSelect:operateVisible ? 'none' : 'auto', height: operateStyles.height}"></div>
|
||||
<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.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>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
@ -99,6 +99,10 @@ export default {
|
||||
placeholderFull: {
|
||||
default: ''
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -202,6 +206,9 @@ export default {
|
||||
},
|
||||
|
||||
onEditing() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
this.$refs.desc.onFull()
|
||||
},
|
||||
|
||||
@ -210,6 +217,9 @@ export default {
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
this.$emit('on-blur');
|
||||
},
|
||||
|
||||
@ -319,6 +329,9 @@ export default {
|
||||
},
|
||||
|
||||
onLiPreview() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
if (!this.operateMenu.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -82,6 +82,24 @@
|
||||
<DropdownItem name="exportCheckin">{{$L('导出签到数据')}}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</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
|
||||
v-else-if="item.visible !== false"
|
||||
@ -146,12 +164,23 @@
|
||||
<div class="menu-title">{{item.label}}</div>
|
||||
</li>
|
||||
</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 ref="menuProject" class="menu-project">
|
||||
<Draggable
|
||||
:list="projectDraggableList"
|
||||
:animation="150"
|
||||
:disabled="$isEEUIApp || windowTouch || !!projectKeyValue"
|
||||
:disabled="$isEEUIApp || windowTouch || !!projectKeyValue || ownerProjectTabsVisible"
|
||||
tag="ul"
|
||||
item-key="id"
|
||||
draggable="li:not(.pinned)"
|
||||
@ -170,6 +199,12 @@
|
||||
<div class="project-h1">
|
||||
<em @click.stop="toggleOpenMenu(item.id)"></em>
|
||||
<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.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div>
|
||||
</div>
|
||||
@ -186,7 +221,10 @@
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</Scrollbar>
|
||||
@ -330,6 +368,9 @@
|
||||
<!--弹出 MCP 服务器信息-->
|
||||
<MCPHelper v-model="mcpHelperShow"/>
|
||||
|
||||
<!--负责人视角-->
|
||||
<DepartmentOwnerView v-model="departmentOwnerViewShow"/>
|
||||
|
||||
<!--导出任务统计-->
|
||||
<TaskExport v-model="exportTaskShow"/>
|
||||
|
||||
@ -456,6 +497,7 @@ import transformEmojiToHtml from "../utils/emoji";
|
||||
import {languageName} from "../language";
|
||||
import {AINormalizeJsonContent, PROJECT_AI_SYSTEM_PROMPT, withLanguagePreferencePrompt} from "../utils/ai";
|
||||
import Draggable from 'vuedraggable'
|
||||
import DepartmentOwnerView from "./manage/components/DepartmentOwnerView.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -481,7 +523,8 @@ export default {
|
||||
ProjectArchived,
|
||||
MicroApps,
|
||||
ComplaintManagement,
|
||||
Draggable
|
||||
Draggable,
|
||||
DepartmentOwnerView
|
||||
},
|
||||
directives: {longpress, TransferDom},
|
||||
data() {
|
||||
@ -519,6 +562,7 @@ export default {
|
||||
|
||||
projectDraggableList: [],
|
||||
projectDragging: false,
|
||||
ownerProjectTab: 'mine',
|
||||
|
||||
openMenu: {},
|
||||
visibleMenu: false,
|
||||
@ -550,6 +594,7 @@ export default {
|
||||
taskBrowseHistory: [],
|
||||
|
||||
mcpHelperShow: false,
|
||||
departmentOwnerViewShow: false,
|
||||
}
|
||||
},
|
||||
|
||||
@ -613,8 +658,10 @@ export default {
|
||||
|
||||
'dialogIns',
|
||||
'formOptions',
|
||||
'systemConfig',
|
||||
'mobileTabbar',
|
||||
'longpressData',
|
||||
'departmentOwnerProjectsRefreshing',
|
||||
|
||||
'mcpServerStatus',
|
||||
'microAppsIds'
|
||||
@ -626,6 +673,14 @@ export default {
|
||||
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
|
||||
* @param mobileTabbar
|
||||
@ -763,6 +818,16 @@ export default {
|
||||
{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(...[
|
||||
{path: 'clearCache', name: '清除缓存', divided: true},
|
||||
{path: 'logout', name: '退出登录', style: {color: '#f40'}}
|
||||
@ -787,7 +852,7 @@ export default {
|
||||
* 项目列表
|
||||
* @returns {Array}
|
||||
*/
|
||||
projectLists() {
|
||||
projectBaseLists() {
|
||||
const {projectKeyValue, cacheProjects} = this;
|
||||
const data = $A.cloneJSON(cacheProjects).sort((a, b) => {
|
||||
// 置顶优先
|
||||
@ -807,6 +872,36 @@ export default {
|
||||
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}
|
||||
@ -884,6 +979,31 @@ export default {
|
||||
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: {
|
||||
handler(val) {
|
||||
if (!this.projectDragging) {
|
||||
@ -915,6 +1035,12 @@ export default {
|
||||
|
||||
methods: {
|
||||
transformEmojiToHtml,
|
||||
syncOwnerProjectTabByRoute() {
|
||||
if (!this.ownerProjectTabsVisible || !this.routeProject) {
|
||||
return;
|
||||
}
|
||||
this.ownerProjectTab = this.routeProject.department_readonly ? 'readonly' : 'mine';
|
||||
},
|
||||
chackPass() {
|
||||
if (this.userInfo.changepass === 1) {
|
||||
this.goForward({name: 'manage-setting-password'});
|
||||
@ -936,6 +1062,9 @@ export default {
|
||||
|
||||
settingRoute(path) {
|
||||
switch (path) {
|
||||
case 'departmentOwnerView':
|
||||
this.departmentOwnerViewShow = true;
|
||||
return;
|
||||
case 'allUser':
|
||||
this.allUserShow = true;
|
||||
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"
|
||||
:itemWidth="80"
|
||||
@on-change="onChange"
|
||||
@on-click="onClick">
|
||||
@on-click="onClick"
|
||||
:readonly="readonly">
|
||||
<template #titleTool>
|
||||
<Dropdown class="project-gstc-dropdown-filtr" trigger="click" @on-click="onSwitchColumn">
|
||||
<Icon class="project-gstc-dropdown-icon" :class="{filtr:filtrProjectId > 0}" type="md-funnel" />
|
||||
@ -55,6 +56,10 @@ export default {
|
||||
flowInfo: {
|
||||
default: {}
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -215,6 +220,9 @@ export default {
|
||||
},
|
||||
|
||||
onChange(item) {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
const {time, baktime} = item;
|
||||
if (Math.abs(baktime.end - time.end) > 1000 || Math.abs(baktime.start - time.start) > 1000) {
|
||||
//修改时间(变化超过1秒钟)
|
||||
@ -238,6 +246,10 @@ export default {
|
||||
},
|
||||
|
||||
editSubmit(save) {
|
||||
if (this.readonly) {
|
||||
this.editData = [];
|
||||
return;
|
||||
}
|
||||
this.editData && this.editData.forEach(item => {
|
||||
let task = this.lists.find(({id}) => id == item.id)
|
||||
if (save) {
|
||||
|
||||
@ -11,11 +11,31 @@
|
||||
<Input type="search" v-model="projectKeyValue" :placeholder="$L(loadProjects > 0 ? '更新中...' : '搜索')" clearable/>
|
||||
</Form>
|
||||
</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>
|
||||
<Draggable
|
||||
:list="projectDraggableList"
|
||||
:animation="150"
|
||||
:disabled="!(isDragging && !projectKeyValue)"
|
||||
:disabled="!(isDragging && !projectKeyValue) || ownerProjectTabsVisible"
|
||||
tag="ul"
|
||||
item-key="id"
|
||||
draggable="li:not(.pinned)"
|
||||
@ -36,6 +56,12 @@
|
||||
<div class="item-left">
|
||||
<div class="project-h1">
|
||||
<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.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div>
|
||||
</div>
|
||||
@ -62,7 +88,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<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">
|
||||
{{$L(projectKeyValue ? `没有任何与"${projectKeyValue}"相关的结果` : `没有任何项目`)}}
|
||||
</li>
|
||||
@ -88,12 +114,14 @@
|
||||
<DropdownItem @click.native="handleChatClick">
|
||||
{{ $L('项目讨论') }}
|
||||
</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 ? '退出排序' : '调整排序') }}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<DepartmentOwnerView v-model="departmentOwnerViewShow"/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -103,10 +131,11 @@ import Draggable from 'vuedraggable'
|
||||
import longpress from "../../../directives/longpress";
|
||||
import TransferDom from "../../../directives/transfer-dom";
|
||||
import transformEmojiToHtml from "../../../utils/emoji";
|
||||
import DepartmentOwnerView from "./DepartmentOwnerView.vue";
|
||||
|
||||
export default {
|
||||
name: "ProjectList",
|
||||
components: {Draggable},
|
||||
components: {Draggable, DepartmentOwnerView},
|
||||
directives: {longpress, TransferDom},
|
||||
data() {
|
||||
return {
|
||||
@ -120,13 +149,30 @@ export default {
|
||||
isDragging: false,
|
||||
projectDraggableList: [],
|
||||
projectDragging: false,
|
||||
ownerProjectTab: 'mine',
|
||||
departmentOwnerViewShow: false,
|
||||
}
|
||||
},
|
||||
|
||||
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 data = $A.cloneJSON(cacheProjects).sort((a, b) => {
|
||||
// 置顶优先
|
||||
@ -145,9 +191,44 @@ export default {
|
||||
}
|
||||
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: {
|
||||
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: {
|
||||
handler(val) {
|
||||
if (!this.projectDragging) {
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<div v-if="loading" class="project-load"><Loading/></div>
|
||||
</div>
|
||||
<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>
|
||||
<li>
|
||||
<UserAvatarTip :userid="projectData.owner_userid" :size="36" :borderWidth="2" :openDelay="0">
|
||||
@ -32,7 +32,7 @@
|
||||
</template>
|
||||
</ul>
|
||||
</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('添加任务')">
|
||||
<Icon class="menu-icon" type="md-add" />
|
||||
</ETooltip>
|
||||
@ -52,7 +52,13 @@
|
||||
<li class="project-icon">
|
||||
<EDropdown @command="projectDropdown" trigger="click" transfer>
|
||||
<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="permissions">{{$L('权限设置')}}</EDropdownItem>
|
||||
<EDropdownItem command="task_template">{{$L('任务模板')}}</EDropdownItem>
|
||||
@ -83,6 +89,9 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Alert v-if="projectData.department_readonly" class="project-readonly-alert" type="info" show-icon>
|
||||
{{$L('当前为负责人视角:你可查看项目和任务,并参与讨论,但不能编辑项目或任务。')}}
|
||||
</Alert>
|
||||
<div class="project-subbox">
|
||||
<div class="project-subtitle user-select-auto" @click="showDesc">
|
||||
<VMPreviewNostyle ref="descPreview" :value="projectData.desc"/>
|
||||
@ -112,7 +121,7 @@
|
||||
<Draggable
|
||||
:list="columnList"
|
||||
:animation="150"
|
||||
:disabled="sortDisabled || $isEEUIApp || windowTouch"
|
||||
:disabled="sortDisabled || isDepartmentReadonly || $isEEUIApp || windowTouch"
|
||||
class="column-list"
|
||||
tag="ul"
|
||||
draggable=".column-item"
|
||||
@ -133,7 +142,7 @@
|
||||
<div class="column-head-icon">
|
||||
<div v-if="columnLoad[column.id] === true" class="loading"><Loading /></div>
|
||||
<EDropdown
|
||||
v-else
|
||||
v-else-if="!isDepartmentReadonly"
|
||||
trigger="click"
|
||||
size="medium"
|
||||
@command="dropColumn(column, $event)">
|
||||
@ -165,14 +174,14 @@
|
||||
</li>
|
||||
</EDropdownMenu>
|
||||
</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>
|
||||
<Scrollbar
|
||||
class="column-task"
|
||||
class-name="task-scrollbar"
|
||||
@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
|
||||
:column-id="column.id"
|
||||
:project-id="projectId"
|
||||
@ -184,7 +193,7 @@
|
||||
<Draggable
|
||||
:list="column.tasks"
|
||||
:animation="150"
|
||||
:disabled="sortDisabled || $isEEUIApp || windowTouch"
|
||||
:disabled="sortDisabled || isDepartmentReadonly || $isEEUIApp || windowTouch"
|
||||
class="task-list"
|
||||
draggable=".task-draggable"
|
||||
filter=".complete"
|
||||
@ -207,7 +216,7 @@
|
||||
<pre>{{item.name}}</pre>
|
||||
</div>
|
||||
<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>
|
||||
<template v-if="!item.complete_at">
|
||||
@ -241,7 +250,7 @@
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div class="task-item additem">
|
||||
<div v-if="!isDepartmentReadonly" class="task-item additem">
|
||||
<TaskAddSimple
|
||||
:column-id="column.id"
|
||||
:project-id="projectId"
|
||||
@ -250,7 +259,7 @@
|
||||
</Draggable>
|
||||
</Scrollbar>
|
||||
</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">
|
||||
<Icon type="md-add" />{{$L('添加列表')}}
|
||||
</div>
|
||||
@ -321,6 +330,7 @@
|
||||
v-if="projectData.cacheParameter.showMy"
|
||||
:list="transforTasks(myList)"
|
||||
:task-visibilitys="taskRowVisibilitys"
|
||||
:readonly="isDepartmentReadonly"
|
||||
open-key="my"
|
||||
@on-priority="addTaskOpen"
|
||||
fast-add-task/>
|
||||
@ -342,6 +352,7 @@
|
||||
v-if="projectData.cacheParameter.showHelp"
|
||||
:list="helpList"
|
||||
:task-visibilitys="taskRowVisibilitys"
|
||||
:readonly="isDepartmentReadonly"
|
||||
open-key="help"
|
||||
@on-priority="addTaskOpen"/>
|
||||
</div>
|
||||
@ -362,6 +373,7 @@
|
||||
v-if="projectData.cacheParameter.showUndone"
|
||||
:list="unList"
|
||||
:task-visibilitys="taskRowVisibilitys"
|
||||
:readonly="isDepartmentReadonly"
|
||||
open-key="undone"
|
||||
@on-priority="addTaskOpen"/>
|
||||
</div>
|
||||
@ -384,6 +396,7 @@
|
||||
v-if="projectData.cacheParameter.showCompleted"
|
||||
:list="completedList"
|
||||
:task-visibilitys="taskRowVisibilitys"
|
||||
:readonly="isDepartmentReadonly"
|
||||
open-key="completed"
|
||||
@on-priority="addTaskOpen"
|
||||
showCompleteAt/>
|
||||
@ -391,7 +404,7 @@
|
||||
</Scrollbar>
|
||||
<div v-else-if="tabTypeActive === 'gantt'" class="project-gantt">
|
||||
<!--甘特图-->
|
||||
<ProjectGantt :projectColumn="columnList" :flowInfo="flowInfo"/>
|
||||
<ProjectGantt :projectColumn="columnList" :flowInfo="flowInfo" :readonly="isDepartmentReadonly"/>
|
||||
</div>
|
||||
|
||||
<!--项目设置-->
|
||||
@ -824,7 +837,12 @@ export default {
|
||||
return this.projectData?.owner_userid === this.userId;
|
||||
},
|
||||
|
||||
isDepartmentReadonly() {
|
||||
return !!this.projectData?.department_readonly;
|
||||
},
|
||||
|
||||
isOwnerOrDeputy() {
|
||||
if (this.isDepartmentReadonly) return false;
|
||||
if (!this.projectData) return false;
|
||||
if (this.projectData.owner_userid === this.userId) return true;
|
||||
return (this.projectData.deputy_userids || []).includes(this.userId);
|
||||
@ -1249,6 +1267,9 @@ export default {
|
||||
},
|
||||
|
||||
sortUpdate(only_column) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
const oldSort = this.sortData;
|
||||
const newSort = this.getSort();
|
||||
if (JSON.stringify(oldSort) === JSON.stringify(newSort)) {
|
||||
@ -1315,14 +1336,23 @@ export default {
|
||||
},
|
||||
|
||||
addTopShow(id, show) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
this.$set(this.columnTopShow, id, show);
|
||||
},
|
||||
|
||||
addTaskOpen(params) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
emitter.emit('addTask', params);
|
||||
},
|
||||
|
||||
addColumnOpen() {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
this.addColumnShow = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.addColumnName.focus();
|
||||
@ -1360,6 +1390,9 @@ export default {
|
||||
},
|
||||
|
||||
dropColumn(column, command) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
if (command === 'title') {
|
||||
this.titleColumn(column);
|
||||
}
|
||||
@ -1617,6 +1650,9 @@ export default {
|
||||
},
|
||||
|
||||
projectDropdown(name) {
|
||||
if (this.isDepartmentReadonly && !['favorite', 'log', 'archived_task', 'deleted_task'].includes(name)) {
|
||||
return;
|
||||
}
|
||||
switch (name) {
|
||||
case "favorite":
|
||||
this.toggleProjectFavorite();
|
||||
@ -1643,7 +1679,7 @@ export default {
|
||||
break;
|
||||
|
||||
case "user":
|
||||
if (!this.isOwnerOrDeputy) {
|
||||
if (this.isDepartmentReadonly || !this.isOwnerOrDeputy) {
|
||||
return;
|
||||
}
|
||||
const userids = this.projectData.project_user.map(({userid}) => userid);
|
||||
@ -1714,6 +1750,9 @@ export default {
|
||||
|
||||
openTask(task, receive) {
|
||||
this.$store.dispatch("openTask", task)
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
if (receive === true) {
|
||||
// 向任务窗口发送领取任务请求
|
||||
setTimeout(() => {
|
||||
@ -1777,7 +1816,7 @@ export default {
|
||||
this.$store.dispatch("call", {
|
||||
url: 'project/flow/list',
|
||||
data: {
|
||||
project_id: this.projectId,
|
||||
project_id: this.projectId
|
||||
},
|
||||
}).then(({data}) => {
|
||||
this.flowList = data;
|
||||
|
||||
@ -365,7 +365,7 @@ export default {
|
||||
this.$store.dispatch("call", {
|
||||
url: 'project/flow/list',
|
||||
data: {
|
||||
project_id: this.projectId,
|
||||
project_id: this.projectId
|
||||
},
|
||||
}).then(({data}) => {
|
||||
this.list = data.map(item => {
|
||||
|
||||
@ -138,7 +138,7 @@ export default {
|
||||
data: {
|
||||
task_id: this.taskId,
|
||||
page: Math.max(this.page, 1),
|
||||
pagesize: Math.max($A.runNum(this.pageSize), 10),
|
||||
pagesize: Math.max($A.runNum(this.pageSize), 10)
|
||||
},
|
||||
}).then(({data}) => {
|
||||
this.page = data.current_page;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!--子任务-->
|
||||
<li v-if="ready && isSubTask">
|
||||
<div class="subtask-icon">
|
||||
<div v-if="!isDepartmentReadonly" class="subtask-icon">
|
||||
<TaskMenu
|
||||
:ref="`taskMenu_${taskDetail.id}`"
|
||||
:disabled="taskId === 0"
|
||||
@ -13,7 +13,7 @@
|
||||
v-if="taskDetail.flow_item_name"
|
||||
class="subtask-flow"
|
||||
: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 class="subtask-name">
|
||||
<Input
|
||||
@ -24,11 +24,13 @@
|
||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||
:maxlength="255"
|
||||
enterkeyhint="done"
|
||||
:readonly="isDepartmentReadonly"
|
||||
@on-blur="updateBlur('name')"
|
||||
@on-keydown="onNameKeydown"
|
||||
/>
|
||||
</div>
|
||||
<DatePicker
|
||||
v-if="!isDepartmentReadonly"
|
||||
v-model="timeValue"
|
||||
:open="timeOpen"
|
||||
:options="timeOptions"
|
||||
@ -46,7 +48,11 @@
|
||||
</div>
|
||||
<Icon v-else class="clock" type="ios-clock-outline" @click="openTime" />
|
||||
</DatePicker>
|
||||
<div v-else-if="showSubTime" :class="['subtask-time readonly-time', taskDetail.today ? 'today' : '', taskDetail.overdue ? 'overdue' : '']">
|
||||
{{expiresFormat(taskDetail.end_at)}}
|
||||
</div>
|
||||
<UserSelect
|
||||
v-if="!isDepartmentReadonly"
|
||||
class="subtask-avatar"
|
||||
v-model="ownerData.owner_userid"
|
||||
:multiple-max="10"
|
||||
@ -55,6 +61,11 @@
|
||||
:add-icon="false"
|
||||
:project-id="taskDetail.project_id"
|
||||
: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>
|
||||
<!--主任务-->
|
||||
<div
|
||||
@ -65,6 +76,7 @@
|
||||
<div v-show="taskDetail.id > 0" class="task-info" v-resize-observer="scrollIntoInput">
|
||||
<div class="head">
|
||||
<TaskMenu
|
||||
v-if="!isDepartmentReadonly"
|
||||
:ref="`taskMenu_${taskDetail.id}`"
|
||||
:disabled="taskId === 0"
|
||||
:task="taskDetail"
|
||||
@ -76,10 +88,10 @@
|
||||
v-if="taskDetail.flow_item_name"
|
||||
class="flow"
|
||||
: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 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 class="nav user-select-auto">
|
||||
<p v-if="projectName"><span>{{projectName}}</span></p>
|
||||
@ -90,7 +102,7 @@
|
||||
<ETooltip v-if="$Electron" :disabled="$isEEUIApp || windowTouch" :content="$L('独立窗口显示')">
|
||||
<i class="taskfont open" @click="openNewWin"></i>
|
||||
</ETooltip>
|
||||
<div class="menu">
|
||||
<div v-if="!isDepartmentReadonly" class="menu">
|
||||
<TaskMenu
|
||||
:disabled="taskId === 0"
|
||||
:task="taskDetail"
|
||||
@ -104,7 +116,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<EPopover
|
||||
v-model="receiveShow"
|
||||
@ -143,6 +158,7 @@
|
||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||
:maxlength="255"
|
||||
enterkeyhint="done"
|
||||
:readonly="isDepartmentReadonly"
|
||||
@on-blur="updateBlur('name')"
|
||||
@on-keydown="onNameKeydown"/>
|
||||
</div>
|
||||
@ -151,6 +167,7 @@
|
||||
class="desc"
|
||||
:value="taskContent"
|
||||
:placeholder="$L('详细描述...')"
|
||||
:readonly="isDepartmentReadonly"
|
||||
@on-history="onHistory"
|
||||
@on-blur="updateBlur('content', $event)"/>
|
||||
<Form class="items" label-position="left" label-width="auto" @submit.native.prevent>
|
||||
@ -159,7 +176,8 @@
|
||||
<i class="taskfont"></i>{{$L('标签')}}
|
||||
</div>
|
||||
<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
|
||||
ref="tagSelect"
|
||||
v-model="tagValue"
|
||||
@ -181,7 +199,7 @@
|
||||
</div>
|
||||
<ul class="item-content priority">
|
||||
<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>
|
||||
</ul>
|
||||
</FormItem>
|
||||
@ -189,7 +207,11 @@
|
||||
<div class="item-label" slot="label">
|
||||
<i class="taskfont"></i>{{$L('负责人')}}
|
||||
</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
|
||||
v-else
|
||||
class="item-content user"
|
||||
v-model="ownerData.owner_userid"
|
||||
:multiple-max="10"
|
||||
@ -203,7 +225,11 @@
|
||||
<div class="item-label" slot="label">
|
||||
<i class="taskfont"></i>{{$L('协助人员')}}
|
||||
</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
|
||||
v-else
|
||||
ref="assist"
|
||||
class="item-content user"
|
||||
v-model="assistData.assist_userid"
|
||||
@ -218,11 +244,11 @@
|
||||
<FormItem v-if="taskDetail.visibility > 1 || visibleForce || visibleKeep">
|
||||
<div class="item-label" slot="label">
|
||||
<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 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>
|
||||
<UserSelect v-else
|
||||
<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-if="!isDepartmentReadonly"
|
||||
ref="visibleUserSelectRef"
|
||||
v-model="taskDetail.visibility_appointor"
|
||||
:avatar-size="28"
|
||||
@ -230,13 +256,16 @@
|
||||
:project-id="taskDetail.project_id"
|
||||
:add-icon="false"
|
||||
@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>
|
||||
</FormItem>
|
||||
<FormItem v-if="taskDetail.end_at || timeForce">
|
||||
<div class="item-label" slot="label">
|
||||
<i class="taskfont"></i>
|
||||
<span v-if="!taskDetail.end_at" @click="timeOpen = true" class="visibility-text color">{{$L('截止时间')}}</span>
|
||||
<span v-else class="visibility-text color" @click="showAtDropdown">{{$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="!isDepartmentReadonly && showAtDropdown($event)">{{$L('截止时间')}}</span>
|
||||
</div>
|
||||
<ul class="item-content">
|
||||
<li>
|
||||
@ -253,13 +282,13 @@
|
||||
@on-ok="timeOk"
|
||||
transfer>
|
||||
<div class="picker-time">
|
||||
<div v-if="!taskDetail.end_at" @click="timeOpen = true" class="time">{{taskDetail.end_at ? cutTime : '--'}}</div>
|
||||
<div v-else @click="showAtDropdown" 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="!isDepartmentReadonly && showAtDropdown($event)" class="time">{{taskDetail.end_at ? cutTime : '--'}}</div>
|
||||
<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)}}
|
||||
</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>
|
||||
</div>
|
||||
</DatePicker>
|
||||
@ -273,7 +302,7 @@
|
||||
<ul class="item-content loop">
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
@ -291,7 +320,7 @@
|
||||
<div class="file-size">{{$A.bytesToSize(file.size)}}</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="item-content file-up">
|
||||
<ul v-if="!isDepartmentReadonly" class="item-content file-up">
|
||||
<li>
|
||||
<div class="add-button" @click="onUploadClick(true)">
|
||||
<i class="taskfont"></i>
|
||||
@ -314,7 +343,7 @@
|
||||
:main-end-at="taskDetail.end_at"
|
||||
:can-update-blur="canUpdateBlur"/>
|
||||
</ul>
|
||||
<ul class="item-content subtask-add">
|
||||
<ul v-if="!isDepartmentReadonly" class="item-content subtask-add">
|
||||
<li>
|
||||
<Input
|
||||
v-if="addsubShow"
|
||||
@ -377,6 +406,7 @@
|
||||
{{$L('已归档')}}
|
||||
</span>
|
||||
<Icon
|
||||
v-if="!isDepartmentReadonly"
|
||||
type="md-close"
|
||||
class="related-remove"
|
||||
@click.native.stop="removeRelatedTask(item)"/>
|
||||
@ -384,7 +414,7 @@
|
||||
</ul>
|
||||
</FormItem>
|
||||
</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-button" @click="onAddItem">
|
||||
<i class="taskfont"></i>
|
||||
@ -484,6 +514,7 @@
|
||||
<div class="drag-text">{{$L('拖动到这里发送')}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!taskDetail.id" class="task-load"><Loading/></div>
|
||||
@ -884,6 +915,9 @@ export default {
|
||||
},
|
||||
|
||||
menuList() {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return [];
|
||||
}
|
||||
const {taskDetail} = this;
|
||||
const list = [];
|
||||
if ($A.arrayLength(taskDetail.task_tag) === 0) {
|
||||
@ -963,6 +997,10 @@ export default {
|
||||
return this.systemConfig.task_visible === 'open' // 可见性保持显示
|
||||
},
|
||||
|
||||
isDepartmentReadonly() {
|
||||
return !!this.taskDetail?.department_readonly;
|
||||
},
|
||||
|
||||
isSubTask({taskDetail}) {
|
||||
return taskDetail.parent_id > 0
|
||||
},
|
||||
@ -1137,6 +1175,9 @@ export default {
|
||||
},
|
||||
|
||||
onNameKeydown(e) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 13) {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@ -1146,6 +1187,9 @@ export default {
|
||||
},
|
||||
|
||||
checkUpdate(action) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return false;
|
||||
}
|
||||
let isModify = false;
|
||||
if (this.openTask.name != this.taskDetail.name) {
|
||||
isModify = true;
|
||||
@ -1187,12 +1231,20 @@ export default {
|
||||
},
|
||||
|
||||
updateBlur(action, params) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.canUpdateBlur) {
|
||||
this.updateData(action, params)
|
||||
}
|
||||
},
|
||||
|
||||
updateData(action, params) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
let successCallback = null;
|
||||
switch (action) {
|
||||
case 'priority':
|
||||
@ -1396,6 +1448,10 @@ export default {
|
||||
},
|
||||
|
||||
async onOwner(pick) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = {
|
||||
task_id: this.taskDetail.id,
|
||||
owner: this.ownerData.owner_userid
|
||||
@ -1440,6 +1496,10 @@ export default {
|
||||
},
|
||||
|
||||
onAssist() {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($A.jsonStringify(this.taskDetail.assist_userid) === $A.jsonStringify(this.assistData.assist_userid)) {
|
||||
return;
|
||||
}
|
||||
@ -1484,6 +1544,10 @@ export default {
|
||||
},
|
||||
|
||||
openTime() {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.timeOpen = !this.timeOpen;
|
||||
if (this.timeOpen) {
|
||||
this.timeValue = this.taskDetail.end_at ? [this.taskDetail.start_at, this.taskDetail.end_at] : [];
|
||||
@ -1497,6 +1561,10 @@ export default {
|
||||
},
|
||||
|
||||
timeClear() {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateData('times', {
|
||||
start_at: false,
|
||||
end_at: false,
|
||||
@ -1505,6 +1573,10 @@ export default {
|
||||
},
|
||||
|
||||
timeOk() {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const times = $A.newDateString(this.timeValue, "YYYY-MM-DD HH:mm");
|
||||
this.updateData('times', {
|
||||
start_at: times[0],
|
||||
@ -1514,6 +1586,10 @@ export default {
|
||||
},
|
||||
|
||||
addsubOpen() {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addsubShow = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.addsub.focus()
|
||||
@ -1537,6 +1613,10 @@ export default {
|
||||
},
|
||||
|
||||
onAddsub() {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.addsubName == '') {
|
||||
$A.messageError('任务描述不能为空');
|
||||
return;
|
||||
@ -1602,6 +1682,10 @@ export default {
|
||||
},
|
||||
|
||||
removeRelatedTask(item) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item || !item.related_task_id) {
|
||||
return;
|
||||
}
|
||||
@ -1636,6 +1720,10 @@ export default {
|
||||
},
|
||||
|
||||
onPriority(event) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = this.taskPriority.map(item => {
|
||||
return {
|
||||
label: item.name,
|
||||
@ -1655,6 +1743,10 @@ export default {
|
||||
},
|
||||
|
||||
onLoop(event) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = this.loops.map(item => {
|
||||
return {
|
||||
label: item.label,
|
||||
@ -1684,6 +1776,10 @@ export default {
|
||||
},
|
||||
|
||||
onAddItem(event) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = this.menuList.map(item => {
|
||||
return {
|
||||
label: item.name,
|
||||
@ -1702,6 +1798,10 @@ export default {
|
||||
},
|
||||
|
||||
dropAddItem(command) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'tag':
|
||||
this.tagForce = true;
|
||||
@ -1765,6 +1865,7 @@ export default {
|
||||
},
|
||||
|
||||
onEventMore(e) {
|
||||
|
||||
if (['image', 'file'].includes(e)) {
|
||||
this.onUploadClick(false)
|
||||
}
|
||||
@ -1776,6 +1877,7 @@ export default {
|
||||
},
|
||||
|
||||
msgDialog(sendType = null) {
|
||||
|
||||
if (this.sendLoad > 0 || this.openLoad > 0) {
|
||||
return;
|
||||
}
|
||||
@ -1895,6 +1997,10 @@ export default {
|
||||
},
|
||||
|
||||
deleteFile(file) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$set(file, '_show_menu', false);
|
||||
this.$store.dispatch("forgetTaskFile", file.id)
|
||||
//
|
||||
@ -1910,6 +2016,9 @@ export default {
|
||||
},
|
||||
|
||||
openMenu(event, task) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
const el = this.$refs[`taskMenu_${task.id}`];
|
||||
el && el.handleClick(event)
|
||||
},
|
||||
@ -2010,12 +2119,18 @@ export default {
|
||||
okText: this.$L('立即下载'),
|
||||
content: `${file.name} (${$A.bytesToSize(file.size)})`,
|
||||
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){
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = [
|
||||
{label: '项目人员', value: 1},
|
||||
{label: '任务人员', value: 2},
|
||||
@ -2033,6 +2148,10 @@ export default {
|
||||
},
|
||||
|
||||
showAtDropdown(event){
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.timeOpen = false
|
||||
const list = [
|
||||
{label: '任务延期', value: 1},
|
||||
@ -2060,6 +2179,10 @@ export default {
|
||||
},
|
||||
|
||||
dropVisible(command) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 1:
|
||||
case 2:
|
||||
@ -2077,6 +2200,10 @@ export default {
|
||||
},
|
||||
|
||||
dropDeadline(command) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 1:
|
||||
this.delayTaskQuicks = [
|
||||
@ -2108,6 +2235,10 @@ export default {
|
||||
},
|
||||
|
||||
onDelay(){
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$refs.formDelayTaskRef.validate((valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
@ -2137,8 +2268,10 @@ export default {
|
||||
const list = [
|
||||
{label: '查看附件', value: 1},
|
||||
{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', {
|
||||
event,
|
||||
list,
|
||||
@ -2179,6 +2312,10 @@ export default {
|
||||
},
|
||||
|
||||
onTagAdd(tagName) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 避免关闭选择框时触发更新
|
||||
this.tagValue = this.getTag;
|
||||
this.tagBakValue = $A.cloneJSON(this.tagValue);
|
||||
@ -2188,6 +2325,10 @@ export default {
|
||||
},
|
||||
|
||||
onTagAddSave(result) {
|
||||
if (this.isDepartmentReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.tagValue;
|
||||
const addData = result.filter(({data}) => data && data.id > 0).map(({data}) => data);
|
||||
// 合并数组,如果有重名标签则使用新添加的标签数据
|
||||
|
||||
@ -17,10 +17,10 @@
|
||||
:class="['sub-icon', taskOpen[item.id] ? 'active' : '']"
|
||||
type="ios-arrow-forward"
|
||||
@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)">
|
||||
<!--工作流状态-->
|
||||
<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>
|
||||
<!--有多少个子任务-->
|
||||
@ -51,7 +51,7 @@
|
||||
trigger="click"
|
||||
size="small"
|
||||
placement="bottom"
|
||||
:disabled="item.sub_top === true"
|
||||
:disabled="readonly || item.sub_top === true"
|
||||
@command="dropTask(item, $event)">
|
||||
<div class="task-column">{{columnName(item.column_id)}}</div>
|
||||
<EDropdownMenu slot="dropdown">
|
||||
@ -66,7 +66,7 @@
|
||||
trigger="click"
|
||||
size="small"
|
||||
placement="bottom"
|
||||
:disabled="item.sub_top === true"
|
||||
:disabled="readonly || item.sub_top === true"
|
||||
@command="dropTask(item, $event)">
|
||||
<TaskPriority :backgroundColor="item.p_color">{{item.p_name || $L('未设置')}}</TaskPriority>
|
||||
<EDropdownMenu slot="dropdown">
|
||||
@ -85,7 +85,7 @@
|
||||
<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"/>
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
@ -107,11 +107,12 @@
|
||||
v-if="taskOpen[item.id]===true"
|
||||
:list="subTask(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"
|
||||
@command="dropTask"/>
|
||||
@command="dropTask"
|
||||
:readonly="readonly"/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -151,6 +152,10 @@ export default {
|
||||
taskVisibilitys: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -202,6 +207,9 @@ export default {
|
||||
},
|
||||
|
||||
dropTask(task, command) {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
const el = this.$refs[`taskMenu_${task.id}`];
|
||||
if (!el) {
|
||||
return;
|
||||
@ -228,6 +236,9 @@ export default {
|
||||
},
|
||||
|
||||
onPriority(data) {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
this.$emit("on-priority", data)
|
||||
},
|
||||
|
||||
@ -260,6 +271,9 @@ export default {
|
||||
|
||||
openTask(task, receive) {
|
||||
this.$store.dispatch("openTask", task)
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
if (receive === true) {
|
||||
// 向任务窗口发送领取任务请求
|
||||
setTimeout(() => {
|
||||
@ -269,6 +283,9 @@ export default {
|
||||
},
|
||||
|
||||
openMenu(event, task) {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
const el = this.$refs[`taskMenu_${task.id}`];
|
||||
if (el) {
|
||||
el[0].handleClick(event)
|
||||
|
||||
@ -75,6 +75,13 @@
|
||||
</RadioGroup>
|
||||
<div v-if="formDatum.project_invite == 'open'" class="form-tip">{{$L('开启:项目管理员可生成链接邀请成员加入项目。')}}</div>
|
||||
</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 class="block-setting-box">
|
||||
@ -351,6 +358,7 @@ export default {
|
||||
}).then(({data}) => {
|
||||
if (save) {
|
||||
$A.messageSuccess('修改成功');
|
||||
this.$store.dispatch("getUserInfo").catch(() => {});
|
||||
}
|
||||
this.formDatum = data;
|
||||
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)) {
|
||||
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) {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
if (window.systemInfo.debug === "yes"
|
||||
@ -620,7 +645,7 @@ export default {
|
||||
* @param dispatch
|
||||
* @param timeout
|
||||
*/
|
||||
getBasicData({state, dispatch}, timeout) {
|
||||
async getBasicData({state, dispatch}, timeout) {
|
||||
if (typeof timeout === "number") {
|
||||
window.__getBasicDataTimer && clearTimeout(window.__getBasicDataTimer)
|
||||
if (timeout > -1) {
|
||||
@ -640,7 +665,7 @@ export default {
|
||||
dispatch("getTaskPriority", 1000);
|
||||
dispatch("getReportUnread", 1000);
|
||||
dispatch("getApproveUnread", 1000);
|
||||
dispatch("getProjectByQueue");
|
||||
dispatch("getProjectsForDepartmentOwnerView").catch(() => {});
|
||||
dispatch("getTaskForDashboard");
|
||||
dispatch("dialogMsgRead");
|
||||
dispatch("updateMicroAppsStatus");
|
||||
@ -1181,6 +1206,7 @@ export default {
|
||||
array: [
|
||||
'cacheUserBasic',
|
||||
'cacheProjects',
|
||||
'cacheDepartmentOwnerIds',
|
||||
'cacheColumns',
|
||||
'cacheTasks',
|
||||
'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>}
|
||||
*/
|
||||
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) {
|
||||
if (state.userId === 0) {
|
||||
state.cacheProjects = [];
|
||||
@ -1657,6 +1841,9 @@ export default {
|
||||
url: 'project/lists',
|
||||
data: callData.get()
|
||||
}).then(({data}) => {
|
||||
if (replace) {
|
||||
state.cacheProjects = [];
|
||||
}
|
||||
dispatch("saveProject", data.data);
|
||||
callData.save(data).then(ids => dispatch("forgetProject", {id: ids}))
|
||||
state.projectTotal = data.total_all;
|
||||
@ -1666,6 +1853,9 @@ export default {
|
||||
console.warn(e);
|
||||
reject(e)
|
||||
}).finally(_ => {
|
||||
if (replace) {
|
||||
state.departmentOwnerProjectsRefreshing = false;
|
||||
}
|
||||
state.loadProjects--;
|
||||
});
|
||||
});
|
||||
@ -1702,7 +1892,7 @@ export default {
|
||||
dispatch("call", {
|
||||
url: 'project/one',
|
||||
data: {
|
||||
project_id,
|
||||
project_id
|
||||
},
|
||||
}).then(result => {
|
||||
setTimeout(() => {
|
||||
@ -2133,9 +2323,14 @@ export default {
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
getTasks({state, dispatch}, requestData) {
|
||||
if (requestData === null) {
|
||||
if (!$A.isJson(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)
|
||||
//
|
||||
return new Promise(function (resolve, reject) {
|
||||
@ -2435,7 +2630,7 @@ export default {
|
||||
dispatch("call", {
|
||||
url: 'project/task/content',
|
||||
data: {
|
||||
task_id,
|
||||
task_id
|
||||
},
|
||||
}).then(result => {
|
||||
dispatch("saveTaskContent", result.data)
|
||||
@ -2483,7 +2678,7 @@ export default {
|
||||
dispatch("call", {
|
||||
url: 'project/task/files',
|
||||
data: {
|
||||
task_id,
|
||||
task_id
|
||||
},
|
||||
}).then(result => {
|
||||
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: [],
|
||||
cacheTasks: [],
|
||||
cacheProjectParameter: [],
|
||||
cacheDepartmentOwnerIds: [],
|
||||
departmentOwnerViewRestored: false,
|
||||
departmentOwnerProjectsRefreshing: false,
|
||||
|
||||
// Emoji
|
||||
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%);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.ivu-alert-icon {
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.ivu-alert-message {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.vuepress-markdown-body {
|
||||
h1,
|
||||
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 {
|
||||
@ -202,6 +276,25 @@
|
||||
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 {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
.project-readonly-alert {
|
||||
margin: 0 32px 8px;
|
||||
}
|
||||
.project-titbox {
|
||||
width: 100%;
|
||||
padding: 32px 32px 4px;
|
||||
@ -1050,6 +1053,9 @@
|
||||
|
||||
body.window-portrait {
|
||||
.project-panel {
|
||||
.project-readonly-alert {
|
||||
margin: 0 16px 8px;
|
||||
}
|
||||
.project-titbox {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
@ -227,6 +227,9 @@
|
||||
.scrollbar-content {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.task-readonly-alert {
|
||||
margin-top: 18px;
|
||||
}
|
||||
.receive-box {
|
||||
display: flex;
|
||||
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 {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
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 {
|
||||
flex: 1;
|
||||
@ -148,6 +186,24 @@
|
||||
padding-left: 8px;
|
||||
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 {
|
||||
display: none;
|
||||
@ -205,6 +261,12 @@
|
||||
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 {
|
||||
.manage-box-menu {
|
||||
.menu-base {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user