diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 3e97444d7..178beca24 100755 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -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); } // diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index 53870bfff..586e1f006 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -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(); // diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index c7aaed184..8c670b3ed 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -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); // diff --git a/app/Models/Project.php b/app/Models/Project.php index 8f4937b08..b1c6fb20b 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -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); + } } diff --git a/app/Models/ProjectTask.php b/app/Models/ProjectTask.php index 3c99a9552..001274475 100644 --- a/app/Models/ProjectTask.php +++ b/app/Models/ProjectTask.php @@ -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 diff --git a/app/Models/UserDepartment.php b/app/Models/UserDepartment.php index 944dc2f25..77f708633 100644 --- a/app/Models/UserDepartment.php +++ b/app/Models/UserDepartment.php @@ -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; + } + } diff --git a/app/Models/WebSocketDialog.php b/app/Models/WebSocketDialog.php index 96413d368..1af0f0c22 100644 --- a/app/Models/WebSocketDialog.php +++ b/app/Models/WebSocketDialog.php @@ -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': diff --git a/resources/assets/js/components/GanttView.vue b/resources/assets/js/components/GanttView.vue index 4fcb64fdb..4191e3b1c 100644 --- a/resources/assets/js/components/GanttView.vue +++ b/resources/assets/js/components/GanttView.vue @@ -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; diff --git a/resources/assets/js/components/TEditorTask.vue b/resources/assets/js/components/TEditorTask.vue index 27985ddf8..42989d8bc 100755 --- a/resources/assets/js/components/TEditorTask.vue +++ b/resources/assets/js/components/TEditorTask.vue @@ -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>
- {{ $L(operateMenu.checked === 'checked' ? '标记未选' : '标记已选') }} + {{ $L(operateMenu.checked === 'checked' ? '标记未选' : '标记已选') }} {{ $L('打开链接') }} {{ $L('查看图片') }} - {{ $L('编辑描述') }} + {{ $L('编辑描述') }} {{ $L('历史记录') }} @@ -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; } diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index 877319fac..924706123 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -82,6 +82,24 @@ {{$L('导出签到数据')}} + + +
+
+ {{$L(item.name)}} +
+ +
+
{{item.label}} +
+
+ {{$L(item.name)}} + +
+
@@ -186,7 +221,10 @@

-
  • +
  • +
  • + {{$L(projectKeyValue ? `没有任何与"${projectKeyValue}"相关的结果` : `没有任何项目`)}} +
  • @@ -330,6 +368,9 @@ + + + @@ -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; diff --git a/resources/assets/js/pages/manage/components/DepartmentOwnerView.vue b/resources/assets/js/pages/manage/components/DepartmentOwnerView.vue new file mode 100644 index 000000000..fa553eef0 --- /dev/null +++ b/resources/assets/js/pages/manage/components/DepartmentOwnerView.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/resources/assets/js/pages/manage/components/ProjectGantt.vue b/resources/assets/js/pages/manage/components/ProjectGantt.vue index b1690b9b5..c47c7f06e 100644 --- a/resources/assets/js/pages/manage/components/ProjectGantt.vue +++ b/resources/assets/js/pages/manage/components/ProjectGantt.vue @@ -5,7 +5,8 @@ :menuWidth="menuWidth" :itemWidth="80" @on-change="onChange" - @on-click="onClick"> + @on-click="onClick" + :readonly="readonly"> -
  • +
  • @@ -52,7 +52,13 @@
  • - + + {{$L(projectData.favorited ? '取消收藏' : '收藏项目')}} + {{$L('项目动态')}} + {{$L('已归档任务')}} + {{$L('已删除任务')}} + + {{$L('项目设置')}} {{$L('权限设置')}} {{$L('任务模板')}} @@ -83,6 +89,9 @@
  • + + {{$L('当前为负责人视角:你可查看项目和任务,并参与讨论,但不能编辑项目或任务。')}} +
    @@ -112,7 +121,7 @@
    @@ -165,14 +174,14 @@ - +
    -
    +
    {{item.name}}
    - +
    -
    +
    -
  • +
  • {{$L('添加列表')}}
    @@ -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"/>
  • @@ -362,6 +373,7 @@ v-if="projectData.cacheParameter.showUndone" :list="unList" :task-visibilitys="taskRowVisibilitys" + :readonly="isDepartmentReadonly" open-key="undone" @on-priority="addTaskOpen"/>
    @@ -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 @@
    - +
    @@ -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; diff --git a/resources/assets/js/pages/manage/components/ProjectWorkflow.vue b/resources/assets/js/pages/manage/components/ProjectWorkflow.vue index a70c272f4..a7864265e 100644 --- a/resources/assets/js/pages/manage/components/ProjectWorkflow.vue +++ b/resources/assets/js/pages/manage/components/ProjectWorkflow.vue @@ -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 => { diff --git a/resources/assets/js/pages/manage/components/TaskContentHistory.vue b/resources/assets/js/pages/manage/components/TaskContentHistory.vue index 10bea75bc..30f31a5c2 100644 --- a/resources/assets/js/pages/manage/components/TaskContentHistory.vue +++ b/resources/assets/js/pages/manage/components/TaskContentHistory.vue @@ -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; diff --git a/resources/assets/js/pages/manage/components/TaskDetail.vue b/resources/assets/js/pages/manage/components/TaskDetail.vue index aa2684da7..df8a2b0e1 100755 --- a/resources/assets/js/pages/manage/components/TaskDetail.vue +++ b/resources/assets/js/pages/manage/components/TaskDetail.vue @@ -1,7 +1,7 @@ @@ -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) diff --git a/resources/assets/js/pages/manage/setting/components/SystemSetting.vue b/resources/assets/js/pages/manage/setting/components/SystemSetting.vue index 377cecfef..cbe75596f 100644 --- a/resources/assets/js/pages/manage/setting/components/SystemSetting.vue +++ b/resources/assets/js/pages/manage/setting/components/SystemSetting.vue @@ -75,6 +75,13 @@
    {{$L('开启:项目管理员可生成链接邀请成员加入项目。')}}
    + + + {{$L('开启')}} + {{$L('关闭')}} + +
    {{$L('开启后,部门负责人/部门管理员可只读查看本部门及下级部门成员参与的项目和项目内全部任务。')}}
    +
    @@ -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); diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js index 12596ea60..acd313414 100644 --- a/resources/assets/js/store/actions.js +++ b/resources/assets/js/store/actions.js @@ -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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} + */ + 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} */ 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} */ 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) => { diff --git a/resources/assets/js/store/state.js b/resources/assets/js/store/state.js index 27306455c..dc8a7fb06 100644 --- a/resources/assets/js/store/state.js +++ b/resources/assets/js/store/state.js @@ -103,6 +103,9 @@ export default { cacheColumns: [], cacheTasks: [], cacheProjectParameter: [], + cacheDepartmentOwnerIds: [], + departmentOwnerViewRestored: false, + departmentOwnerProjectsRefreshing: false, // Emoji cacheEmojis: [], diff --git a/resources/assets/sass/dark.scss b/resources/assets/sass/dark.scss index 421519be8..036160900 100644 --- a/resources/assets/sass/dark.scss +++ b/resources/assets/sass/dark.scss @@ -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; + } + } + } + } + } } diff --git a/resources/assets/sass/pages/common.scss b/resources/assets/sass/pages/common.scss index 3c3fe9bc6..c17c1ce17 100755 --- a/resources/assets/sass/pages/common.scss +++ b/resources/assets/sass/pages/common.scss @@ -531,6 +531,14 @@ body { padding-top: 3px; } + .ivu-alert-icon { + top: 10px; + } + + .ivu-alert-message { + line-height: 20px; + } + .vuepress-markdown-body { h1, h2 { diff --git a/resources/assets/sass/pages/components/project-list.scss b/resources/assets/sass/pages/components/project-list.scss index cb6ffcad2..5721fcc72 100644 --- a/resources/assets/sass/pages/components/project-list.scss +++ b/resources/assets/sass/pages/components/project-list.scss @@ -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; + } } } } diff --git a/resources/assets/sass/pages/components/project-panel.scss b/resources/assets/sass/pages/components/project-panel.scss index ef5e8729a..653ac29e6 100644 --- a/resources/assets/sass/pages/components/project-panel.scss +++ b/resources/assets/sass/pages/components/project-panel.scss @@ -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; diff --git a/resources/assets/sass/pages/components/task-detail.scss b/resources/assets/sass/pages/components/task-detail.scss index 65f7bd663..c48b6548c 100644 --- a/resources/assets/sass/pages/components/task-detail.scss +++ b/resources/assets/sass/pages/components/task-detail.scss @@ -227,6 +227,9 @@ .scrollbar-content { padding: 0 5px; } + .task-readonly-alert { + margin-top: 18px; + } .receive-box { display: flex; justify-content: center; diff --git a/resources/assets/sass/pages/page-manage.scss b/resources/assets/sass/pages/page-manage.scss index 4492955cf..e138e5918 100644 --- a/resources/assets/sass/pages/page-manage.scss +++ b/resources/assets/sass/pages/page-manage.scss @@ -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 {