diff --git a/README-en.md b/README-en.md index 59a1cba7f..f88f6576f 100644 --- a/README-en.md +++ b/README-en.md @@ -1,5 +1,5 @@ -[Chinese](./README.md) | English -# Project Introduction +[中文](./README.md) | English +# Projects

diff --git a/README.md b/README.md index 3f327d0ce..5effcb547 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ PHP有很多优秀的后台管理系统,但基于Swoole的后台管理系统 如果觉着还不错的话,就请点个 ⭐star 支持一下吧,这将是对我最大的支持和鼓励! 在使用 MineAdmin 前请认真阅读[《免责声明》](https://doc.mineadmin.com/guide/start/declaration.html)并同意该声明。 -## 部门岗位分支(包含数据权限功能) -当前默认分支不带部门、岗位、数据权限等功能,如需使用这些功能请移步 [`【包含部门岗位的MineAdmin----master-department】`](https://github.com/mineadmin/MineAdmin/tree/master-department) 分支代码下载 +## 默认主分支(不包含部门、岗位、数据权限功能) +当前分支带有部门、岗位、数据权限等功能,如果不需要使用这些功能请移步 [`【主分支】`](https://github.com/mineadmin/MineAdmin) 下载代码 + ## 官方交流群 > QQ群用于交流学习,请勿水群 @@ -41,7 +42,10 @@ PHP有很多优秀的后台管理系统,但基于Swoole的后台管理系统 4. 操作日志,用户对系统的一些正常操作的查询 5. 登录日志,用户登录系统的记录查询 6. 附件管理,管理当前系统上传的文件及图片等信息 -7. 应用市场,可下载各种基础应用、插件、前端组件等等 +7. 部门管理,可以管理组织架构 +8. 岗位管理,在部门内管理,可以为部门设置岗位,再为用户分配岗位 +9. 数据权限,数据权限功能跟随岗位而设置,同时,也可以对用户单独设置数据权限,使岗位的数据权限失效。 +10. 应用市场,可下载各种基础应用、插件、前端组件等等 ## 环境需求 diff --git a/app/Http/Admin/Controller/Permission/DepartmentController.php b/app/Http/Admin/Controller/Permission/DepartmentController.php new file mode 100644 index 000000000..47fd80ab1 --- /dev/null +++ b/app/Http/Admin/Controller/Permission/DepartmentController.php @@ -0,0 +1,117 @@ + [], 'ApiKey' => []]], + tags: ['部门管理'], + )] + #[PageResponse(instance: DepartmentSchema::class)] + #[Permission(code: 'permission:department:index')] + public function pageList(): Result + { + return $this->success([ + 'list' => $this->service->getList($this->getRequestData()), + ]); + } + + #[Post( + path: '/admin/department', + operationId: 'departmentCreate', + summary: '创建部门', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['部门管理'], + )] + #[RequestBody( + content: new JsonContent(ref: DepartmentRequest::class) + )] + #[Permission(code: 'permission:department:save')] + #[ResultResponse(instance: new Result())] + public function create(DepartmentRequest $request): Result + { + $this->service->create(array_merge($request->validated(), [ + 'created_by' => $this->currentUser->id(), + ])); + return $this->success(); + } + + #[Put( + path: '/admin/department/{id}', + operationId: 'departmentSave', + summary: '保存部门', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['部门管理'], + )] + #[RequestBody( + content: new JsonContent(ref: DepartmentRequest::class) + )] + #[Permission(code: 'permission:department:update')] + #[ResultResponse(instance: new Result())] + public function save(int $id, DepartmentRequest $request): Result + { + $this->service->updateById($id, array_merge($request->validated(), [ + 'updated_by' => $this->currentUser->id(), + ])); + return $this->success(); + } + + #[Delete( + path: '/admin/department', + operationId: 'departmentDelete', + summary: '删除部门', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['部门管理'], + )] + #[ResultResponse(instance: new Result())] + #[Permission(code: 'permission:department:delete')] + public function delete(): Result + { + $this->service->deleteById($this->getRequestData()); + return $this->success(); + } +} diff --git a/app/Http/Admin/Controller/Permission/LeaderController.php b/app/Http/Admin/Controller/Permission/LeaderController.php new file mode 100644 index 000000000..e5e39be5a --- /dev/null +++ b/app/Http/Admin/Controller/Permission/LeaderController.php @@ -0,0 +1,100 @@ + [], 'ApiKey' => []]], + tags: ['部门领导管理'], + )] + #[PageResponse(instance: LeaderSchema::class)] + #[Permission(code: 'permission:leader:index')] + public function pageList(): Result + { + return $this->success( + $this->service->page( + $this->getRequestData(), + $this->getCurrentPage(), + $this->getPageSize() + ) + ); + } + + #[Post( + path: '/admin/leader', + operationId: 'leaderCreate', + summary: '部门领导岗位', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['部门领导管理'], + )] + #[RequestBody( + content: new JsonContent(ref: LeaderRequest::class) + )] + #[Permission(code: 'permission:leader:save')] + #[ResultResponse(instance: new Result())] + public function create(LeaderRequest $request): Result + { + $this->service->create(array_merge($request->validated(), [ + 'created_by' => $this->currentUser->id(), + ])); + return $this->success(); + } + + #[Delete( + path: '/admin/leader', + operationId: 'leaderDelete', + summary: '删除部门领导', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['部门领导管理'], + )] + #[ResultResponse(instance: new Result())] + #[Permission(code: 'permission:leader:delete')] + public function delete(): Result + { + $this->service->deleteByDoubleKey($this->getRequestData()); + return $this->success(); + } +} diff --git a/app/Http/Admin/Controller/Permission/PositionController.php b/app/Http/Admin/Controller/Permission/PositionController.php new file mode 100644 index 000000000..5682d64d4 --- /dev/null +++ b/app/Http/Admin/Controller/Permission/PositionController.php @@ -0,0 +1,137 @@ + [], 'ApiKey' => []]], + tags: ['岗位管理'], + )] + #[PageResponse(instance: PositionSchema::class)] + #[Permission(code: 'permission:position:index')] + public function pageList(): Result + { + return $this->success( + $this->service->page( + $this->getRequestData(), + $this->getCurrentPage(), + $this->getPageSize() + ) + ); + } + + #[Put( + path: '/admin/position/{id}/data_permission', + operationId: 'positionDataPermission', + summary: '设置岗位数据权限', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['岗位管理'], + )] + #[Permission(code: 'permission:position:data_permission')] + #[ResultResponse(instance: new Result())] + public function batchDataPermission(int $id, BatchGrantDataPermissionForPositionRequest $request): Result + { + $this->service->batchDataPermission($id, $request->validated()); + return $this->success(); + } + + #[Post( + path: '/admin/position', + operationId: 'positionCreate', + summary: '创建岗位', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['岗位管理'], + )] + #[RequestBody( + content: new JsonContent(ref: PositionRequest::class) + )] + #[Permission(code: 'permission:position:save')] + #[ResultResponse(instance: new Result())] + public function create(PositionRequest $request): Result + { + $this->service->create(array_merge($request->validated(), [ + 'created_by' => $this->currentUser->id(), + ])); + return $this->success(); + } + + #[Put( + path: '/admin/position/{id}', + operationId: 'positionSave', + summary: '保存岗位', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['岗位管理'], + )] + #[RequestBody( + content: new JsonContent(ref: PositionRequest::class) + )] + #[Permission(code: 'permission:position:update')] + #[ResultResponse(instance: new Result())] + public function save(int $id, PositionRequest $request): Result + { + $this->service->updateById($id, array_merge($request->validated(), [ + 'updated_by' => $this->currentUser->id(), + ])); + return $this->success(); + } + + #[Delete( + path: '/admin/position', + operationId: 'positionDelete', + summary: '删除岗位', + security: [['Bearer' => [], 'ApiKey' => []]], + tags: ['岗位管理'], + )] + #[ResultResponse(instance: new Result())] + #[Permission(code: 'permission:position:delete')] + public function delete(): Result + { + $this->service->deleteById($this->getRequestData()); + return $this->success(); + } +} diff --git a/app/Http/Admin/Request/Permission/BatchGrantDataPermissionForPositionRequest.php b/app/Http/Admin/Request/Permission/BatchGrantDataPermissionForPositionRequest.php new file mode 100644 index 000000000..118136d5d --- /dev/null +++ b/app/Http/Admin/Request/Permission/BatchGrantDataPermissionForPositionRequest.php @@ -0,0 +1,47 @@ + [ + 'required', + 'string', + Rule::enum(PolicyType::class), + ], + 'value' => [ + 'sometimes', + 'array', + 'min:1', + ], + ]; + } + + public function attributes(): array + { + return [ + 'policy_type' => '策略类型', + 'value' => '策略值', + ]; + } +} diff --git a/app/Http/Admin/Request/Permission/DepartmentRequest.php b/app/Http/Admin/Request/Permission/DepartmentRequest.php new file mode 100644 index 000000000..715e2e57a --- /dev/null +++ b/app/Http/Admin/Request/Permission/DepartmentRequest.php @@ -0,0 +1,62 @@ + 'required|string|max:60', + 'parent_id' => 'sometimes|integer', + ]; + if ($this->isCreate()) { + $rules['name'] = 'required|string|max:60|unique:department,name'; + } + if ($this->isUpdate()) { + $rules['name'] = 'required|string|max:60|unique:department,name,' . $this->route('id'); + } + $rules['department_users'] = 'sometimes|array'; + $rules['department_users.*'] = 'sometimes|integer'; + $rules['leader'] = 'sometimes|array'; + $rules['leader.*'] = 'sometimes|integer'; + return $rules; + } + + public function attributes(): array + { + return [ + 'name' => '部门名称', + 'parent_id' => '上级部门', + ]; + } +} diff --git a/app/Http/Admin/Request/Permission/LeaderRequest.php b/app/Http/Admin/Request/Permission/LeaderRequest.php new file mode 100644 index 000000000..d59dcc3ac --- /dev/null +++ b/app/Http/Admin/Request/Permission/LeaderRequest.php @@ -0,0 +1,51 @@ + 'required|array', + 'dept_id' => 'required|integer', + ]; + } + + public function attributes(): array + { + return [ + 'user_id' => '用户ID', + 'dept_id' => '部门ID', + ]; + } +} diff --git a/app/Http/Admin/Request/Permission/PositionRequest.php b/app/Http/Admin/Request/Permission/PositionRequest.php new file mode 100644 index 000000000..e40b6b69e --- /dev/null +++ b/app/Http/Admin/Request/Permission/PositionRequest.php @@ -0,0 +1,58 @@ + 'required|string|max:50', + 'dept_id' => 'required|integer|exists:department,id', + ]; + if ($this->isCreate()) { + $rules['name'] = 'required|string|max:50|unique:position,name'; + } + if ($this->isUpdate()) { + $rules['name'] = 'required|string|max:50|unique:position,name,' . $this->route('id'); + } + return $rules; + } + + public function attributes(): array + { + return [ + 'name' => '岗位名称', + 'dept_id' => '部门ID', + ]; + } +} diff --git a/app/Http/Admin/Request/Permission/RoleRequest.php b/app/Http/Admin/Request/Permission/RoleRequest.php index ab9d96fa8..4f51226f3 100644 --- a/app/Http/Admin/Request/Permission/RoleRequest.php +++ b/app/Http/Admin/Request/Permission/RoleRequest.php @@ -20,7 +20,7 @@ #[\Mine\Swagger\Attributes\FormRequest( schema: RoleSchema::class, only: [ - 'name', 'code', 'status', 'sort', 'remark', + 'name', 'code', 'status', 'sort', 'remark', 'policy', ] )] class RoleRequest extends FormRequest diff --git a/app/Http/Admin/Request/Permission/UserRequest.php b/app/Http/Admin/Request/Permission/UserRequest.php index 7f308fc20..eb9d53be1 100644 --- a/app/Http/Admin/Request/Permission/UserRequest.php +++ b/app/Http/Admin/Request/Permission/UserRequest.php @@ -13,8 +13,10 @@ namespace App\Http\Admin\Request\Permission; use App\Http\Common\Request\Traits\NoAuthorizeTrait; +use App\Model\Enums\DataPermission\PolicyType; use App\Schema\UserSchema; use Hyperf\Validation\Request\FormRequest; +use Hyperf\Validation\Rule; use Mine\Swagger\Attributes\FormRequest as FormRequestAnnotation; #[FormRequestAnnotation( @@ -43,6 +45,7 @@ 'status', 'backend_setting', 'remark', + 'policy', ] )] class UserRequest extends FormRequest @@ -62,7 +65,34 @@ public function rules(): array 'status' => 'sometimes|integer', 'backend_setting' => 'sometimes|array|max:255', 'remark' => 'sometimes|string|max:255', - 'password' => 'sometimes|string|min:6|max:20', + 'policy' => 'sometimes|array', + 'policy.policy_type' => [ + 'required_with:policy', + 'string', + 'max:20', + Rule::enum(PolicyType::class), + ], + 'policy.value' => [ + 'sometimes', + ], + 'department' => [ + 'sometimes', + 'array', + ], + 'department.*' => [ + 'required_with:department', + 'integer', + 'exists:department,id', + ], + 'position' => [ + 'sometimes', + 'array', + ], + 'position.*' => [ + 'sometimes', + 'integer', + 'exists:position,id', + ], ]; } @@ -80,7 +110,7 @@ public function attributes(): array 'backend_setting' => trans('user.backend_setting'), 'created_by' => trans('user.created_by'), 'remark' => trans('user.remark'), - 'password' => trans('user.password'), + 'department' => trans('user.department'), ]; } } diff --git a/app/Http/CurrentUser.php b/app/Http/CurrentUser.php index d5a37aebf..a09647af9 100644 --- a/app/Http/CurrentUser.php +++ b/app/Http/CurrentUser.php @@ -18,6 +18,7 @@ use App\Service\Permission\UserService; use Hyperf\Collection\Arr; use Hyperf\Collection\Collection; +use Hyperf\Context\Context; use Lcobucci\JWT\Token\RegisteredClaims; use Mine\Jwt\Traits\RequestScopedTokenTrait; @@ -30,9 +31,19 @@ public function __construct( private readonly UserService $userService ) {} + public static function ctxUser(): ?User + { + return Context::get('current_user'); + } + public function user(): ?User { - return $this->userService->getInfo($this->id()); + if (Context::has('current_user')) { + return Context::get('current_user'); + } + $user = $this->userService->getInfo($this->id()); + Context::set('current_user', $user); + return $user; } public function refresh(): array diff --git a/app/Library/DataPermission/Aspects/DataScopeAspect.php b/app/Library/DataPermission/Aspects/DataScopeAspect.php new file mode 100644 index 000000000..60c904105 --- /dev/null +++ b/app/Library/DataPermission/Aspects/DataScopeAspect.php @@ -0,0 +1,119 @@ +getAnnotationMetadata()->class[DataScope::class]) + || isset($proceedingJoinPoint->getAnnotationMetadata()->method[DataScope::class]) + ) { + return $this->handleDataScope($proceedingJoinPoint); + } + + if ($proceedingJoinPoint->className === Builder::class) { + if ($proceedingJoinPoint->methodName === 'runSelect') { + return $this->handleSelect($proceedingJoinPoint); + } + if ($proceedingJoinPoint->methodName === 'delete') { + return $this->handleDelete($proceedingJoinPoint); + } + if ($proceedingJoinPoint->methodName === 'update') { + return $this->handleUpdate($proceedingJoinPoint); + } + } + return $proceedingJoinPoint->process(); + } + + protected function handleDelete(ProceedingJoinPoint $proceedingJoinPoint) + { + return $proceedingJoinPoint->process(); + } + + protected function handleUpdate(ProceedingJoinPoint $proceedingJoinPoint) + { + return $proceedingJoinPoint->process(); + } + + protected function handleSelect(ProceedingJoinPoint $proceedingJoinPoint) + { + /** + * @var Builder $builder + */ + $builder = $proceedingJoinPoint->getInstance(); + if (! \in_array($builder->from, DataPermissionContext::getOnlyTables() ?: [], true)) { + return $proceedingJoinPoint->process(); + } + if (Context::has(self::CONTEXT_KEY) && $user = CurrentUser::ctxUser()) { + Context::destroy(self::CONTEXT_KEY); + $this->factory->build( + $builder, + $user + ); + Context::set(self::CONTEXT_KEY, 1); + } + return $proceedingJoinPoint->process(); + } + + protected function handleDataScope(ProceedingJoinPoint $proceedingJoinPoint) + { + Context::set(self::CONTEXT_KEY, 1); + /** + * @var null|DataScope $attribute + */ + $attribute = Arr::get($proceedingJoinPoint->getAnnotationMetadata()->class, DataScope::class); + if ($attribute === null) { + $attribute = Arr::get($proceedingJoinPoint->getAnnotationMetadata()->method, DataScope::class); + } + if ($attribute === null) { + return $proceedingJoinPoint->process(); + } + DataPermissionContext::setDeptColumn($attribute->getDeptColumn()); + DataPermissionContext::setCreatedByColumn($attribute->getCreatedByColumn()); + DataPermissionContext::setScopeType($attribute->getScopeType()); + DataPermissionContext::setOnlyTables($attribute->getOnlyTables()); + $result = $proceedingJoinPoint->process(); + Context::destroy(self::CONTEXT_KEY); + return $result; + } +} diff --git a/app/Library/DataPermission/Attribute/DataScope.php b/app/Library/DataPermission/Attribute/DataScope.php new file mode 100644 index 000000000..398ddfa3a --- /dev/null +++ b/app/Library/DataPermission/Attribute/DataScope.php @@ -0,0 +1,47 @@ +onlyTables; + } + + public function getDeptColumn(): string + { + return $this->deptColumn; + } + + public function getCreatedByColumn(): string + { + return $this->createdByColumn; + } + + public function getScopeType(): ScopeType + { + return $this->scopeType; + } +} diff --git a/app/Library/DataPermission/Context.php b/app/Library/DataPermission/Context.php new file mode 100644 index 000000000..e51308684 --- /dev/null +++ b/app/Library/DataPermission/Context.php @@ -0,0 +1,66 @@ +get(self::class); + } + + public function build( + Builder $builder, + User $user, + ): void { + if ($user->isSuperAdmin()) { + return; + } + if (($policy = $user->getPolicy()) === null) { + return; + } + $scopeType = Context::getScopeType(); + if ($policy->policy_type === PolicyType::CustomFunc) { + $customFunc = $policy->value[0] ?? null; + if (! \is_string($customFunc)) { + throw new \RuntimeException(\sprintf('Invalid custom function: %s', $customFunc)); + } + $this->rule->loadCustomFunc($customFunc, $builder, $user, $policy, $scopeType); + } + switch ($scopeType) { + case ScopeType::CREATED_BY: + self::handleCreatedBy($user, $policy, $builder); + break; + case ScopeType::DEPT: + self::handleDept($user, $policy, $builder); + break; + case ScopeType::DEPT_CREATED_BY: + self::handleDeptCreatedBy($user, $policy, $builder); + break; + case ScopeType::DEPT_OR_CREATED_BY: + self::handleDeptOrCreatedBy($user, $policy, $builder); + break; + } + } + + private function handleCreatedBy(User $user, Policy $policy, Builder $builder): void + { + $builder->when($this->rule->getCreatedByList($user, $policy), static function (Builder $query, array $createdByList) { + $query->whereIn(Context::getCreatedByColumn(), $createdByList); + }); + } + + private function handleDept(User $user, Policy $policy, Builder $builder): void + { + $builder->when($this->rule->getDeptIds($user, $policy), static function (Builder $query, array $deptList) { + $query->whereIn(Context::getDeptColumn(), $deptList); + }); + } + + private function handleDeptCreatedBy(User $user, Policy $policy, Builder $builder): void + { + $builder->when($this->rule->getDeptIds($user, $policy), static function (Builder $query, array $deptList) { + $query->whereIn(Context::getDeptColumn(), $deptList); + })->when($this->rule->getCreatedByList($user, $policy), static function (Builder $query, array $createdByList) { + $query->whereIn(Context::getCreatedByColumn(), $createdByList); + }); + } + + private function handleDeptOrCreatedBy(User $user, Policy $policy, Builder $builder): void + { + $createdByList = $this->rule->getCreatedByList($user, $policy); + $deptList = $this->rule->getDeptIds($user, $policy); + $builder->where(static function (Builder $query) use ($createdByList, $deptList) { + if ($createdByList) { + $query->whereIn(Context::getCreatedByColumn(), $createdByList); + } + if ($deptList) { + $query->orWhereIn(Context::getDeptColumn(), $deptList); + } + }); + } +} diff --git a/app/Library/DataPermission/Manager.php b/app/Library/DataPermission/Manager.php new file mode 100644 index 000000000..a04ab05d6 --- /dev/null +++ b/app/Library/DataPermission/Manager.php @@ -0,0 +1,15 @@ + + */ + private static array $customFunc = []; + + public static function registerCustomFunc(string $name, \Closure $func): void + { + self::$customFunc[$name] = $func; + } + + public static function getCustomFunc(string $name): \Closure + { + if (isset(self::$customFunc[$name])) { + return self::$customFunc[$name]; + } + throw new \RuntimeException('Custom func not found'); + } +} diff --git a/app/Library/DataPermission/Rule/Executable/AbstractExecutable.php b/app/Library/DataPermission/Rule/Executable/AbstractExecutable.php new file mode 100644 index 000000000..7e9648b42 --- /dev/null +++ b/app/Library/DataPermission/Rule/Executable/AbstractExecutable.php @@ -0,0 +1,66 @@ + $className + * @return T + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + protected function getRepository(string $className): mixed + { + return ApplicationContext::getContainer()->get($className); + } + + protected function getUser(): User + { + return $this->user; + } + + protected function getPolicy(): Policy + { + return $this->policy; + } + + /** + * @return Collection + */ + protected function loadCustomFunc(Policy $policy): Collection + { + $result = CustomFuncFactory::getCustomFunc($policy->value[0])->call($this, $this->getUser(), $this->getPolicy()); + if ($result instanceof Collection) { + return $result; + } + throw new \RuntimeException('Custom func must return Collection'); + } +} diff --git a/app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php b/app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php new file mode 100644 index 000000000..9d00fac92 --- /dev/null +++ b/app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php @@ -0,0 +1,150 @@ +getPolicy(); + $policyType = $policy->policy_type; + if ($policyType->isAll()) { + return null; + } + /* + * @var Department[]|Collection $departmentList + */ + if ($policyType->isCustomDept()) { + $departmentList = $this->getRepository(DepartmentRepository::class)->getByIds($policy->value); + } + if ($policyType->isDeptSelf()) { + $departmentList = $this->getUser()->department()->get(); + } + if ($policyType->isDeptTree()) { + $departmentList = collect(); + /** + * @var Collection|Department[] $currentList + */ + $currentList = $this->getUser()->department()->get(); + foreach ($currentList as $item) { + $departmentList->merge($item->getFlatChildren()); + } + } + if ($policyType->isCustomFunc()) { + $departmentList = $this->loadCustomFunc($policy); + } + $ids = [ + $this->getUser()->id, + ]; + if ($policyType->isSelf()) { + return $ids; + } + + if ($policyType->isNotCustomFunc() && $policyType->isNotCustomDept()) { + /** + * @var Collection|Department[] $leaderDepartmentList + */ + $leaderDepartmentList = $this->getUser()->department()->get(); + foreach ($leaderDepartmentList as $department) { + if ($policyType->isDeptSelf()) { + // @phpstan-ignore-next-line + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->where('id', $department->id); + // @phpstan-ignore-next-line + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + } + if ($policyType->isDeptTree()) { + // @phpstan-ignore-next-line + $department->getFlatChildren()->each(function (Department $department) use (&$ids) { + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->where('id', $department->id); + // @phpstan-ignore-next-line + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + }); + } + } + /** + * @var Collection|Position[] $positionList + */ + $positionList = $this->getUser()->position()->get(); + foreach ($positionList as $position) { + if ($policyType->isDeptSelf()) { + // @phpstan-ignore-next-line + $position->department()->get()->each(function (Department $department) use (&$ids) { + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->where('id', $department->id); + // @phpstan-ignore-next-line + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + }); + } + if ($policyType->isDeptTree()) { + // @phpstan-ignore-next-line + $position->department()->get()->each(function (Department $department) use (&$ids) { + $department->getFlatChildren()->each(function (Department $department) use (&$ids) { + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->where('id', $department->id); + // @phpstan-ignore-next-line + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + }); + }); + } + } + + if (! empty($departmentList)) { + foreach ($departmentList as $department) { + if ($policyType->isDeptSelf()) { + // @phpstan-ignore-next-line + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->where('id', $department->id); + // @phpstan-ignore-next-line + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + } + if ($policyType->isDeptTree()) { + // @phpstan-ignore-next-line + $department->getFlatChildren()->each(function (Department $department) use (&$ids) { + $this->getUser()->newQuery() + ->whereHas('department', static function ($query) use ($department) { + $query->where('id', $department->id); + // @phpstan-ignore-next-line + })->get()->each(static function (User $user) use (&$ids) { + $ids[] = $user->id; + }); + }); + } + } + } + } + return array_unique($ids); + } +} diff --git a/app/Library/DataPermission/Rule/Executable/DeptExecute.php b/app/Library/DataPermission/Rule/Executable/DeptExecute.php new file mode 100644 index 000000000..487f899ba --- /dev/null +++ b/app/Library/DataPermission/Rule/Executable/DeptExecute.php @@ -0,0 +1,64 @@ +getPolicy(); + $policyType = $policy->policy_type; + if ($policyType->isAll()) { + return null; + } + /* + * @var Department[]|Collection $departmentList + */ + if ($policyType->isCustomDept()) { + $departmentList = $this->getRepository(DepartmentRepository::class)->getByIds($policy->value); + } + if ($policyType->isCustomFunc()) { + $departmentList = $this->loadCustomFunc($policy); + } + if ($policyType->isDeptSelf() || $policyType->isSelf()) { + $departmentList = $this->getUser()->department()->get(); + } + if ($policyType->isDeptTree()) { + $departmentList = collect(); + /** + * @var Collection|Department[] $currentList + */ + $currentList = $this->getUser()->department()->get(); + foreach ($currentList as $item) { + $departmentList = $departmentList->merge($item->getFlatChildren()); + } + } + if (empty($departmentList)) { + return null; + } + $departmentList = $departmentList->merge($this->getUser()->dept_leader()->get()); + /** + * @var Collection|Position[] $positionList + */ + $positionList = $this->getUser()->position()->get(); + foreach ($positionList as $position) { + $departmentList = $departmentList->merge($position->department()->get()); + } + return $departmentList->pluck('id')->toArray(); + } +} diff --git a/app/Library/DataPermission/Rule/Rule.php b/app/Library/DataPermission/Rule/Rule.php new file mode 100644 index 000000000..1fbf37cc8 --- /dev/null +++ b/app/Library/DataPermission/Rule/Rule.php @@ -0,0 +1,83 @@ +cache = $this->cacheManager->getDriver($this->config->get('department.cache.driver')); + } + + public function isCache(): bool + { + return (bool) $this->config->get('department.cache.enable'); + } + + public function getTtl(): mixed + { + return $this->config->get('department.cache.ttl'); + } + + public function getPrefix(): string + { + return $this->config->get('department.cache.prefix'); + } + + public function getDeptIds(User $user, Policy $policy): array + { + $cacheKey = $this->getPrefix() . ':deptIds:' . $user->id; + if ($this->isCache() && $this->cache->has($cacheKey)) { + return $this->cache->get($cacheKey); + } + $execute = new DeptExecute($user, $policy); + $result = $execute->execute(); + $this->cache->set($cacheKey, $result, $this->getTtl()); + return $result; + } + + public function getCreatedByList(User $user, Policy $policy): array + { + $cacheKey = $this->getPrefix() . ':createdBy:' . $user->id; + if ($this->isCache() && $this->cache->has($cacheKey)) { + return $this->cache->get($cacheKey); + } + $execute = new CreatedByIdsExecute($user, $policy); + $result = $execute->execute(); + $this->cache->set($cacheKey, $result, $this->getTtl()); + return $result; + } + + public function loadCustomFunc(string $customFunc, Builder $builder, User $user, ?Policy $policy, ScopeType $scopeType): void + { + $func = $this->config->get('department.custom.' . $customFunc); + if ($func === null) { + throw new \RuntimeException(\sprintf('Custom function %s not found', $customFunc)); + } + $func($builder, $scopeType, $policy, $user); + } +} diff --git a/app/Library/DataPermission/Scope/DataScope.php b/app/Library/DataPermission/Scope/DataScope.php new file mode 100644 index 000000000..e27a7cb45 --- /dev/null +++ b/app/Library/DataPermission/Scope/DataScope.php @@ -0,0 +1,32 @@ +build($builder->getQuery(), $user); + } +} diff --git a/app/Library/DataPermission/Scope/DataScopes.php b/app/Library/DataPermission/Scope/DataScopes.php new file mode 100644 index 000000000..3d7f8a26b --- /dev/null +++ b/app/Library/DataPermission/Scope/DataScopes.php @@ -0,0 +1,27 @@ +checkRedisConnection(); + } + } + + private function checkRedisConnection(): void + { + try { + $redis = $this->container->get(RedisFactory::class)->get('default'); + $result = $redis->ping(); + if (! $result) { + $this->logger->error('Redis connection failed: Invalid ping response'); + $this->container->get(StdoutLoggerInterface::class)->error('Redis connection failed: Invalid ping response'); + } + } catch (\Throwable $e) { + $this->logger->error('Redis connection failed: ' . $e->getMessage()); + $this->container->get(StdoutLoggerInterface::class)->error('Redis connection failed, please check redis config in .env file'); + } + } +} diff --git a/app/Model/DataPermission/Policy.php b/app/Model/DataPermission/Policy.php new file mode 100644 index 000000000..040ea005b --- /dev/null +++ b/app/Model/DataPermission/Policy.php @@ -0,0 +1,70 @@ +|Position[] $positions + * @property Collection|User[] $users + */ +class Policy extends Model +{ + use SoftDeletes; + + /** + * The table associated with the model. + */ + protected ?string $table = 'data_permission_policy'; + + /** + * The attributes that are mass assignable. + */ + protected array $fillable = ['id', 'user_id', 'position_id', 'policy_type', 'is_default', 'created_at', 'updated_at', 'deleted_at', 'value']; + + /** + * The attributes that should be cast to native types. + */ + protected array $casts = [ + 'id' => 'integer', 'user_id' => 'integer', 'position_id' => 'integer', + 'is_default' => 'bool', 'created_at' => 'datetime', + 'updated_at' => 'datetime', 'deleted_at' => 'datetime', + 'policy_type' => PolicyType::class, 'value' => 'array', + ]; + + public function positions(): BelongsToMany + { + return $this->belongsToMany(Position::class, 'data_permission_policy_position', 'policy_id', 'position_id'); + } + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'data_permission_policy_user', 'policy_id', 'user_id'); + } +} diff --git a/app/Model/Enums/DataPermission/PolicyType.php b/app/Model/Enums/DataPermission/PolicyType.php new file mode 100644 index 000000000..167d3f93f --- /dev/null +++ b/app/Model/Enums/DataPermission/PolicyType.php @@ -0,0 +1,86 @@ +isDeptSelf(); + } + + public function isNotDeptTree(): bool + { + return ! $this->isDeptTree(); + } + + public function isNotAll(): bool + { + return ! $this->isAll(); + } + + public function isNotSelf(): bool + { + return ! $this->isSelf(); + } + + public function isNotCustomDept(): bool + { + return ! $this->isCustomDept(); + } + + public function isNotCustomFunc(): bool + { + return ! $this->isCustomFunc(); + } +} diff --git a/app/Model/Permission/Department.php b/app/Model/Permission/Department.php new file mode 100644 index 000000000..80635130b --- /dev/null +++ b/app/Model/Permission/Department.php @@ -0,0 +1,98 @@ +|Position[] $positions 岗位 + * @property Collection|User[] $department_users 部门用户 + * @property Collection|User[] $leader 部门领导 + * @property Collection|Department[] $children 子部门 + */ +class Department extends Model +{ + use SoftDeletes; + + /** + * The table associated with the model. + */ + protected ?string $table = 'department'; + + /** + * The attributes that are mass assignable. + */ + protected array $fillable = ['id', 'name', 'parent_id', 'created_at', 'updated_at', 'deleted_at']; + + /** + * The attributes that should be cast to native types. + */ + protected array $casts = ['id' => 'integer', 'parent_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime']; + + public function deleted(Deleted $event): void + { + $this->positions()->delete(); + $this->department_users()->detach(); + $this->leader()->detach(); + } + + public function positions(): HasMany + { + return $this->hasMany(Position::class, 'dept_id', 'id'); + } + + public function department_users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'user_dept', 'dept_id', 'user_id'); + } + + public function leader(): BelongsToMany + { + return $this->belongsToMany(User::class, 'dept_leader', 'dept_id', 'user_id'); + } + + public function children(): HasMany + { + // @phpstan-ignore-next-line + return $this->hasMany(self::class, 'parent_id', 'id')->with(['children', 'positions']); + } + + public function getFlatChildren(): BaseCollection + { + $flat = collect(); + $this->load('children'); // 预加载子部门 + $traverse = static function ($departments) use (&$traverse, $flat) { + foreach ($departments as $department) { + $flat->push($department); + if ($department->children->isNotEmpty()) { + $traverse($department->children); + } + } + }; + $traverse($this->children); + return $flat->prepend($this); // 包含自身 + } +} diff --git a/app/Model/Permission/Leader.php b/app/Model/Permission/Leader.php new file mode 100644 index 000000000..57e566722 --- /dev/null +++ b/app/Model/Permission/Leader.php @@ -0,0 +1,59 @@ + 'integer', 'dept_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime']; + + public function department(): BelongsTo + { + return $this->belongsTo(Department::class, 'dept_id', 'id'); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } +} diff --git a/app/Model/Permission/Position.php b/app/Model/Permission/Position.php new file mode 100644 index 000000000..3fcf3f415 --- /dev/null +++ b/app/Model/Permission/Position.php @@ -0,0 +1,68 @@ +|User[] $users + * @property Policy $policy + */ +class Position extends Model +{ + use SoftDeletes; + + /** + * The table associated with the model. + */ + protected ?string $table = 'position'; + + /** + * The attributes that are mass assignable. + */ + protected array $fillable = ['id', 'name', 'dept_id', 'created_at', 'updated_at', 'deleted_at']; + + /** + * The attributes that should be cast to native types. + */ + protected array $casts = ['id' => 'integer', 'dept_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime']; + + public function department(): BelongsTo + { + return $this->belongsTo(Department::class, 'dept_id', 'id'); + } + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'user_position', 'position_id', 'user_id'); + } + + public function policy(): HasOne + { + return $this->hasOne(Policy::class, 'position_id', 'id'); + } +} diff --git a/app/Model/Permission/User.php b/app/Model/Permission/User.php index 313b47282..e850a4f6e 100644 --- a/app/Model/Permission/User.php +++ b/app/Model/Permission/User.php @@ -12,6 +12,7 @@ namespace App\Model\Permission; +use App\Model\DataPermission\Policy; use App\Model\Enums\User\Status; use App\Model\Enums\User\Type; use Carbon\Carbon; @@ -19,6 +20,7 @@ use Hyperf\Database\Model\Events\Creating; use Hyperf\Database\Model\Events\Deleted; use Hyperf\Database\Model\Relations\BelongsToMany; +use Hyperf\Database\Model\Relations\HasOne; use Hyperf\DbConnection\Model\Model; /** @@ -41,6 +43,10 @@ * @property string $remark 备注 * @property null|Collection|Role[] $roles * @property mixed $password 密码 + * @property null|Policy $policy 数据权限策略 + * @property Collection|Department[] $department 部门 + * @property Collection|Department[] $dept_leader 部门领导 + * @property Collection|Position[] $position 岗位 */ final class User extends Model { @@ -87,6 +93,7 @@ public function roles(): BelongsToMany public function deleted(Deleted $event) { $this->roles()->detach(); + $this->policy()->delete(); } public function setPasswordAttribute($value): void @@ -136,4 +143,45 @@ public function hasPermission(string $permission): bool { return $this->roles()->whereRelation('menus', 'name', $permission)->exists(); } + + public function policy(): HasOne + { + return $this->hasOne(Policy::class, 'user_id', 'id'); + } + + public function department(): BelongsToMany + { + return $this->belongsToMany(Department::class, 'user_dept', 'user_id', 'dept_id'); + } + + public function dept_leader(): BelongsToMany + { + return $this->belongsToMany(Department::class, 'dept_leader', 'user_id', 'dept_id'); + } + + public function position(): BelongsToMany + { + return $this->belongsToMany(Position::class, 'user_position', 'user_id', 'position_id'); + } + + public function getPolicy(): ?Policy + { + /** + * @var null|Policy $policy + */ + $policy = $this->policy()->first(); + if (! empty($policy)) { + return $policy; + } + + $this->load('position'); + $positionList = $this->position; + foreach ($positionList as $position) { + $current = $position->policy()->first(); + if (! empty($current)) { + return $current; + } + } + return null; + } } diff --git a/app/Repository/Permission/DepartmentRepository.php b/app/Repository/Permission/DepartmentRepository.php new file mode 100644 index 000000000..972f1938a --- /dev/null +++ b/app/Repository/Permission/DepartmentRepository.php @@ -0,0 +1,66 @@ + + */ +final class DepartmentRepository extends IRepository +{ + public function __construct( + protected readonly Department $model + ) {} + + public function getByIds(array $ids): Collection + { + return $this->model->newQuery()->whereIn('id', $ids)->get(); + } + + public function handleSearch(Builder $query, array $params): Builder + { + return $query + ->when(isset($params['id']), static function (Builder $query) use ($params) { + $query->whereIn('id', Arr::wrap($params['id'])); + }) + ->when(isset($params['name']), static function (Builder $query) use ($params) { + $query->where('name', 'like', '%' . $params['name'] . '%'); + }) + ->when(isset($params['parent_id']), static function (Builder $query) use ($params) { + $query->where('parent_id', $params['parent_id']); + }) + ->when(isset($params['created_at']), static function (Builder $query) use ($params) { + $query->whereBetween('created_at', $params['created_at']); + }) + ->when(isset($params['updated_at']), static function (Builder $query) use ($params) { + $query->whereBetween('updated_at', $params['updated_at']); + }) + ->when(! empty($params['append_position']), static function (Builder $query) { + $query->with('positions:id,name'); + }) + ->when(isset($params['level']), static function (Builder $query) use ($params) { + if ((int) $params['level'] === 1) { + $query->where('parent_id', 0); + $query->with('children'); + } + + // todo 指定层级查询 + }) + ->with(['positions', 'department_users:id,nickname,username,avatar', 'leader:id,nickname,username,avatar']); + } +} diff --git a/app/Repository/Permission/LeaderRepository.php b/app/Repository/Permission/LeaderRepository.php new file mode 100644 index 000000000..8e78012db --- /dev/null +++ b/app/Repository/Permission/LeaderRepository.php @@ -0,0 +1,65 @@ + + */ +final class LeaderRepository extends IRepository +{ + public function __construct( + protected readonly Leader $model + ) {} + + public function create(array $data): mixed + { + foreach ($data['user_id'] as $id) { + Leader::query()->where('dept_id', $data['dept_id'])->where('user_id', $id)->forceDelete(); + Leader::create(['dept_id' => $data['dept_id'], 'user_id' => $id, 'created_at' => date('Y-m-d H:i:s')]); + } + // @phpstan-ignore-next-line + return null; + } + + public function deleteByDoubleKey(int $dept_id, array $user_ids): void + { + Leader::query()->where('dept_id', $dept_id)->whereIn('user_id', $user_ids)->forceDelete(); + } + + public function handleSearch(Builder $query, array $params): Builder + { + return $query + ->when(isset($params['user_id']), static function (Builder $query) use ($params) { + $query->where('user_id', $params['user_id']); + }) + ->when(isset($params['dept_id']), static function (Builder $query) use ($params) { + $query->where('dept_id', $params['dept_id']); + }) + ->when(isset($params['created_at']), static function (Builder $query) use ($params) { + $query->whereBetween('created_at', $params['created_at']); + }) + ->when(isset($params['updated_at']), static function (Builder $query) use ($params) { + $query->whereBetween('updated_at', $params['updated_at']); + }) + ->with(['department', 'user']); + } + + protected function enablePageOrderBy(): bool + { + return false; + } +} diff --git a/app/Repository/Permission/PositionRepository.php b/app/Repository/Permission/PositionRepository.php new file mode 100644 index 000000000..bee3ad160 --- /dev/null +++ b/app/Repository/Permission/PositionRepository.php @@ -0,0 +1,45 @@ + + */ +final class PositionRepository extends IRepository +{ + public function __construct( + protected readonly Position $model + ) {} + + public function handleSearch(Builder $query, array $params): Builder + { + return $query + ->when(isset($params['name']), static function (Builder $query) use ($params) { + $query->where('name', 'like', '%' . $params['name'] . '%'); + }) + ->when(isset($params['dept_id']), static function (Builder $query) use ($params) { + $query->where('dept_id', $params['dept_id']); + }) + ->when(isset($params['created_at']), static function (Builder $query) use ($params) { + $query->whereBetween('created_at', $params['created_at']); + }) + ->when(isset($params['updated_at']), static function (Builder $query) use ($params) { + $query->whereBetween('updated_at', $params['updated_at']); + }) + ->with(['department', 'policy']); + } +} diff --git a/app/Repository/Permission/RoleRepository.php b/app/Repository/Permission/RoleRepository.php index da1a9be20..4a894edeb 100644 --- a/app/Repository/Permission/RoleRepository.php +++ b/app/Repository/Permission/RoleRepository.php @@ -25,14 +25,18 @@ public function __construct( public function handleSearch(Builder $query, array $params): Builder { - return $query->when(Arr::get($params, 'name'), static function (Builder $query, $name) { - $query->where('name', 'like', '%' . $name . '%'); - })->when(Arr::get($params, 'code'), static function (Builder $query, $code) { - $query->whereIn('code', Arr::wrap($code)); - })->when(Arr::has($params, 'status'), static function (Builder $query) use ($params) { - $query->where('status', $params['status']); - })->when(Arr::get($params, 'created_at'), static function (Builder $query, $createdAt) { - $query->whereBetween('created_at', $createdAt); - }); + return $query + ->when(Arr::get($params, 'name'), static function (Builder $query, $name) { + $query->where('name', 'like', '%' . $name . '%'); + }) + ->when(Arr::get($params, 'code'), static function (Builder $query, $code) { + $query->whereIn('code', Arr::wrap($code)); + }) + ->when(Arr::has($params, 'status'), static function (Builder $query) use ($params) { + $query->where('status', $params['status']); + }) + ->when(Arr::get($params, 'created_at'), static function (Builder $query, $createdAt) { + $query->whereBetween('created_at', $createdAt); + }); } } diff --git a/app/Repository/Permission/UserRepository.php b/app/Repository/Permission/UserRepository.php index 000858318..f112ec791 100644 --- a/app/Repository/Permission/UserRepository.php +++ b/app/Repository/Permission/UserRepository.php @@ -72,6 +72,20 @@ public function handleSearch(Builder $query, array $params): Builder $query->whereHas('roles', static function (Builder $query) use ($roleId) { $query->where('role_id', $roleId); }); - }); + }) + ->when(Arr::get($params, 'department_id'), static function (Builder $query, $departmentId) { + $query->where(static function (Builder $query) use ($departmentId) { + $query->whereHas('department', static function (Builder $query) use ($departmentId) { + $query->where('id', $departmentId); + }); + $query->orWhereHas('dept_leader', static function (Builder $query) use ($departmentId) { + $query->where('id', $departmentId); + }); + $query->orWhereHas('position.department', static function (Builder $query) use ($departmentId) { + $query->where('id', $departmentId); + }); + }); + }) + ->with(['policy', 'department', 'dept_leader', 'position']); } } diff --git a/app/Schema/DepartmentSchema.php b/app/Schema/DepartmentSchema.php new file mode 100644 index 000000000..d8dffd557 --- /dev/null +++ b/app/Schema/DepartmentSchema.php @@ -0,0 +1,59 @@ + $this->policyType->value, + 'is_default' => $this->isDefault, + 'value' => $this->value, + ]; + } +} diff --git a/app/Schema/PositionSchema.php b/app/Schema/PositionSchema.php new file mode 100644 index 000000000..de178c639 --- /dev/null +++ b/app/Schema/PositionSchema.php @@ -0,0 +1,49 @@ +id = $model->id; @@ -66,10 +69,11 @@ public function __construct(Role $model) $this->createdAt = $model->created_at; $this->updatedAt = $model->updated_at->format(CarbonInterface::DEFAULT_TO_STRING_FORMAT); $this->remark = $model->remark; + $this->policy = isset($model->policy) ? new PolicySchema($model->policy) : null; } public function jsonSerialize(): mixed { - return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark]; + return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark, 'policy' => $this->policy->jsonSerialize()]; } } diff --git a/app/Schema/UserSchema.php b/app/Schema/UserSchema.php index c51e59385..1e5e00aad 100644 --- a/app/Schema/UserSchema.php +++ b/app/Schema/UserSchema.php @@ -72,6 +72,9 @@ final class UserSchema implements \JsonSerializable #[Property(property: 'remark', title: '备注', type: 'string')] public ?string $remark; + #[Property(property: 'policy', ref: '#/components/schemas/PolicySchema', title: '权限')] + public ?PolicySchema $policy; + public function __construct(User $model) { $this->id = $model->id; @@ -91,6 +94,7 @@ public function __construct(User $model) $this->createdAt = $model->created_at; $this->updatedAt = $model->updated_at; $this->remark = $model->remark; + $this->policy = isset($model->policy) ? new PolicySchema($model->policy) : null; } public function jsonSerialize(): mixed diff --git a/app/Service/Permission/DepartmentService.php b/app/Service/Permission/DepartmentService.php new file mode 100644 index 000000000..18510e3e2 --- /dev/null +++ b/app/Service/Permission/DepartmentService.php @@ -0,0 +1,69 @@ + + */ +class DepartmentService extends IService +{ + public function __construct( + protected readonly DepartmentRepository $repository + ) {} + + public function create(array $data): mixed + { + return Db::transaction(function () use ($data) { + $entity = $this->repository->create($data); + $this->handleEntity($entity, $data); + return $entity; + }); + } + + public function updateById(mixed $id, array $data): mixed + { + return Db::transaction(function () use ($id, $data) { + $entity = $this->repository->findById($id); + if (empty($entity)) { + throw new BusinessException(ResultCode::NOT_FOUND); + } + $this->handleEntity($entity, $data); + }); + } + + public function getPositionsByDepartmentId(int $id): array + { + $entity = $this->repository->findById($id); + if (empty($entity)) { + throw new BusinessException(ResultCode::NOT_FOUND); + } + return $entity->positions()->get(['id', 'name'])->toArray(); + } + + protected function handleEntity(Department $entity, array $data): void + { + if (isset($data['department_users'])) { + $entity->department_users()->sync($data['department_users']); + } + if (isset($data['leader'])) { + $entity->leader()->sync($data['leader']); + } + } +} diff --git a/app/Service/Permission/LeaderService.php b/app/Service/Permission/LeaderService.php new file mode 100644 index 000000000..c9f6881ec --- /dev/null +++ b/app/Service/Permission/LeaderService.php @@ -0,0 +1,38 @@ + + */ +class LeaderService extends IService +{ + public function __construct( + protected readonly LeaderRepository $repository + ) {} + + public function deleteByDoubleKey(array $data): bool + { + try { + $this->repository->deleteByDoubleKey($data['dept_id'], $data['user_ids']); + return true; + } catch (Exception $e) { + return false; + } + } +} diff --git a/app/Service/Permission/PositionService.php b/app/Service/Permission/PositionService.php new file mode 100644 index 000000000..c53c3f419 --- /dev/null +++ b/app/Service/Permission/PositionService.php @@ -0,0 +1,43 @@ + + */ +class PositionService extends IService +{ + public function __construct( + protected readonly PositionRepository $repository + ) {} + + public function batchDataPermission(int $id, array $policy): void + { + $entity = $this->repository->findById($id); + if ($entity === null) { + throw new BusinessException(ResultCode::NOT_FOUND); + } + $policyEntity = $entity->policy()->first(); + if (empty($policyEntity)) { + $entity->policy()->create($policy); + } else { + $policyEntity->update($policy); + } + } +} diff --git a/app/Service/Permission/UserService.php b/app/Service/Permission/UserService.php index 87b39d2d1..b1fe4339e 100644 --- a/app/Service/Permission/UserService.php +++ b/app/Service/Permission/UserService.php @@ -12,12 +12,17 @@ namespace App\Service\Permission; +use App\Exception\BusinessException; +use App\Http\Common\ResultCode; +use App\Library\DataPermission\Attribute\DataScope; +use App\Library\DataPermission\ScopeType; use App\Model\Permission\Role; use App\Model\Permission\User; use App\Repository\Permission\RoleRepository; use App\Repository\Permission\UserRepository; use App\Service\IService; use Hyperf\Collection\Collection; +use Hyperf\DbConnection\Db; /** * @extends IService @@ -62,4 +67,54 @@ public function batchGrantRoleForUser(int $id, array $roleCodes): void }) ); } + + public function create(array $data): mixed + { + return Db::transaction(function () use ($data) { + /** @var User $entity */ + $entity = parent::create($data); + $this->handleWith($entity, $data); + return $entity; + }); + } + + public function updateById(mixed $id, array $data): mixed + { + return Db::transaction(function () use ($id, $data) { + /** @var null|User $entity */ + $entity = $this->repository->findById($id); + if (empty($entity)) { + throw new BusinessException(ResultCode::NOT_FOUND); + } + $entity->fill($data)->save(); + $this->handleWith($entity, $data); + }); + } + + #[DataScope( + scopeType: ScopeType::CREATED_BY, + onlyTables: ['user'] + )] + public function page(array $params, int $page = 1, int $pageSize = 10): array + { + return parent::page($params, $page, $pageSize); // TODO: Change the autogenerated stub + } + + protected function handleWith(User $entity, array $data): void + { + if (isset($data['department'])) { + $entity->department()->sync($data['department']); + } + if (isset($data['position'])) { + $entity->position()->sync($data['position']); + } + if (! empty($data['policy'])) { + $policy = $entity->policy()->first(); + if ($policy) { + $policy->fill($data['policy'])->save(); + } else { + $entity->policy()->create($data['policy']); + } + } + } } diff --git a/config/autoload/casbin/rbac-model.conf b/config/autoload/casbin/rbac-model.conf deleted file mode 100644 index 7854845e4..000000000 --- a/config/autoload/casbin/rbac-model.conf +++ /dev/null @@ -1,14 +0,0 @@ -[request_definition] -r = sub, obj, act - -[policy_definition] -p = sub, obj, act - -[role_definition] -g = _, _ - -[policy_effect] -e = some(where (p.eft == allow)) - -[matchers] -m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "SuperAdmin" \ No newline at end of file diff --git a/config/autoload/department/cache.php b/config/autoload/department/cache.php new file mode 100644 index 000000000..f7b718194 --- /dev/null +++ b/config/autoload/department/cache.php @@ -0,0 +1,30 @@ + env('DATA_PERMISSION_CACHE_ENABLE', false), + /* + * 缓存时间,单位秒 + */ + 'ttl' => env('DATA_PERMISSION_CACHE_TTL', 60 * 5), + /* + * 缓存前缀 + */ + 'prefix' => env('DATA_PERMISSION_CACHE_PREFIX', 'data_permission'), + /* + * 缓存驱动 + */ + 'driver' => env('DATA_PERMISSION_CACHE_DRIVER', 'default'), +]; diff --git a/config/autoload/department/custom.php b/config/autoload/department/custom.php new file mode 100644 index 000000000..ca6db8a1c --- /dev/null +++ b/config/autoload/department/custom.php @@ -0,0 +1,54 @@ + function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) { + // 只针对 id 为 2 的用户生效 + if ($user->id !== 2) { + return; + } + // 获取当前上下文中的创建人字段名称 + $createdByColumn = Context::getCreatedByColumn(); + // 获取当前上下文中的部门字段名称 + $deptColumn = Context::getDeptColumn(); + switch ($scopeType){ + // 隔离类型为根据创建人 + case ScopeType::CREATED_BY: + // 创建人字段为当前用户 + $builder->where($createdByColumn, $user->id); + break; + case ScopeType::DEPT: + // 部门字段为当前用户部门 + $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); + break; + case ScopeType::DEPT_CREATED_BY: + // 部门字段为当前用户部门 + $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); + // 创建人为当前用户 + $builder->where($createdByColumn, $user->id); + break; + case ScopeType::DEPT_OR_CREATED_BY: + // 部门字段为当前用户部门 + $builder->whereIn($deptColumn, $user->department()->get()->pluck('id')); + // 创建人为当前用户 + $builder->orWhere($createdByColumn, $user->id); + break; + } + return; + } +]; diff --git a/config/config.php b/config/config.php index c9ecaadc5..56e410d1c 100644 --- a/config/config.php +++ b/config/config.php @@ -15,7 +15,7 @@ return [ 'app_name' => env('APP_NAME', 'MineAdmin'), 'scan_cacheable' => ! env('APP_DEBUG', false), - 'debug' => env('APP_DEBUG', false), + 'debug' => env('APP_DEBUG', true), StdoutLoggerInterface::class => [ 'log_level' => [ LogLevel::ALERT, diff --git a/databases/migrations/2025_02_24_195620_create_department_tables.php b/databases/migrations/2025_02_24_195620_create_department_tables.php new file mode 100644 index 000000000..b40bc1715 --- /dev/null +++ b/databases/migrations/2025_02_24_195620_create_department_tables.php @@ -0,0 +1,82 @@ +comment('部门表'); + $table->bigIncrements('id'); + $table->string('name', 50)->comment('部门名称'); + $table->bigInteger('parent_id')->default(0)->comment('父级部门ID'); + $table->datetimes(); + $table->softDeletes(); + }); + Schema::create('position', function (Blueprint $table) { + $table->comment('岗位表'); + $table->bigIncrements('id'); + $table->string('name', 50)->comment('岗位名称'); + $table->bigInteger('dept_id')->comment('部门ID'); + $table->datetimes(); + $table->softDeletes(); + }); + Schema::create('user_dept', function (Blueprint $table) { + $table->comment('用户-部门关联表'); + $table->bigInteger('user_id'); + $table->bigInteger('dept_id'); + $table->datetimes(); + $table->softDeletes(); + }); + Schema::create('user_position', function (Blueprint $table) { + $table->comment('用户-岗位关联表'); + $table->bigInteger('user_id'); + $table->bigInteger('position_id'); + $table->datetimes(); + $table->softDeletes(); + }); + Schema::create('dept_leader', function (Blueprint $table) { + $table->comment('部门领导表'); + $table->bigInteger('dept_id'); + $table->bigInteger('user_id'); + $table->datetimes(); + $table->softDeletes(); + }); + Schema::create('data_permission_policy', function (Blueprint $table) { + $table->comment('数据权限策略'); + $table->bigIncrements('id'); + $table->bigInteger('user_id')->default(0)->comment('用户ID(与角色二选一)'); + $table->bigInteger('position_id')->default(0)->comment('岗位ID(与用户二选一)'); + $table->string('policy_type', 20)->comment('策略类型(DEPT_SELF, DEPT_TREE, ALL, SELF, CUSTOM_DEPT, CUSTOM_FUNC)'); + $table->boolean('is_default')->default(true)->comment('是否默认策略(默认值:true)'); + $table->json('value')->nullable()->comment('策略值'); + $table->datetimes(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('department'); + Schema::dropIfExists('position'); + Schema::dropIfExists('user_dept'); + Schema::dropIfExists('user_position'); + Schema::dropIfExists('dept_leader'); + Schema::dropIfExists('data_permission_policy'); + } +}; diff --git a/databases/seeders/menu_seeder_20240926.php b/databases/seeders/menu_seeder_20240926.php index 95b2de4f7..fa3dc20a0 100644 --- a/databases/seeders/menu_seeder_20240926.php +++ b/databases/seeders/menu_seeder_20240926.php @@ -354,107 +354,6 @@ public function data(): array ], ], ], - /* [ - 'name' => '工具', - 'code' => 'devTools', - 'icon' => 'ma-icon-tool', - 'path' => 'devTools', - 'hidden' => '2', - 'type' => 'M', - 'children' => [ - [ - 'name' => '代码生成器', - 'code' => 'setting:code', - 'icon' => 'ma-icon-code', - 'path' => 'code', - 'component' => 'setting/code/index', - 'hidden' => '2', - 'type' => 'M', - 'children' => [ - [ - 'name' => '预览代码', - 'code' => 'setting:code:preview', - 'type' => 'B', - ], - [ - 'name' => '装载数据表', - 'code' => 'setting:code:loadTable', - 'type' => 'B', - ], - [ - 'name' => '删除业务表', - 'code' => 'setting:code:delete', - 'type' => 'B', - ], - [ - 'name' => '同步业务表', - 'code' => 'setting:code:sync', - 'type' => 'B', - ], - [ - 'name' => '生成代码', - 'code' => 'setting:code:generate', - 'type' => 'B', - ], - ], - ], - [ - 'name' => '数据源管理', - 'code' => 'setting:datasource', - 'icon' => 'icon-storage', - 'path' => 'setting/datasource', - 'component' => 'setting/datasource/index', - 'hidden' => '2', - 'type' => 'M', - 'children' => [ - [ - 'name' => '数据源管理列表', - 'code' => 'setting:datasource:index', - 'type' => 'B', - ], - [ - 'name' => '数据源管理保存', - 'code' => 'setting:datasource:save', - 'type' => 'B', - ], - [ - 'name' => '数据源管理更新', - 'code' => 'setting:datasource:update', - 'type' => 'B', - ], - [ - 'name' => '数据源管理读取', - 'code' => 'setting:datasource:read', - 'type' => 'B', - ], - [ - 'name' => '数据源管理删除', - 'code' => 'setting:datasource:delete', - 'type' => 'B', - ], - [ - 'name' => '数据源管理导入', - 'code' => 'setting:datasource:import', - 'type' => 'B', - ], - [ - 'name' => '数据源管理导出', - 'code' => 'setting:datasource:export', - 'type' => 'B', - ], - ], - ], - [ - 'name' => '系统接口', - 'code' => 'systemInterface', - 'icon' => 'icon-compass', - 'path' => 'systemInterface', - 'component' => 'setting/systemInterface/index', - 'hidden' => '2', - 'type' => 'M', - ], - ], - ],*/ ]; } diff --git a/databases/seeders/user_dept_20250310.php b/databases/seeders/user_dept_20250310.php new file mode 100644 index 000000000..1a24a848e --- /dev/null +++ b/databases/seeders/user_dept_20250310.php @@ -0,0 +1,89 @@ +firstOrFail(); + $now = Menu::create([ + 'name' => 'permission:department', + 'path' => '/permission/departments', + 'parent_id' => $parent->id, + 'component' => 'base/views/permission/department/index', + 'meta' => [ + 'title' => '部门管理', + 'icon' => 'mingcute:department-line', + 'i18n' => 'baseMenu.permission.department', + 'type' => 'M', + 'hidden' => 0, + 'componentPath' => 'modules/', + 'componentSuffix' => '.vue', + 'breadcrumbEnable' => 1, + 'copyright' => 1, + 'cache' => 1, + 'affix' => 0, + ] + ]); + $children = [ + 'permission:department:index' => '部门列表', + 'permission:department:save' => '部门新增', + 'permission:department:update' => '部门编辑', + 'permission:department:delete' => '部门删除', + 'permission:position:index' => '岗位列表', + 'permission:position:save' => '岗位新增', + 'permission:position:update' => '岗位编辑', + 'permission:position:delete' => '岗位删除', + 'permission:position:data_permission' => '设置岗位数据权限', + 'permission:leader:index' => '部门领导列表', + 'permission:leader:save' => '新增部门领导', + 'permission:leader:delete' => '部门领导移除', + ]; + $i18n = [ + 'baseMenu.permission.departmentList', + 'baseMenu.permission.departmentCreate', + 'baseMenu.permission.departmentSave', + 'baseMenu.permission.departmentDelete', + 'baseMenu.permission.positionList', + 'baseMenu.permission.positionCreate', + 'baseMenu.permission.positionSave', + 'baseMenu.permission.positionDelete', + 'baseMenu.permission.positionDataScope', + 'baseMenu.permission.leaderList', + 'baseMenu.permission.leaderCreate', + 'baseMenu.permission.leaderDelete', + ]; + $i = 0; + foreach ($children as $child => $title) { + Menu::create([ + 'name' => $child, + 'path' => '/permission/departments', + 'meta' => [ + 'title' => $title, + 'type' => 'B', + 'i18n' => $i18n[$i], + 'hidden' => 1, + 'cache' => 1, + 'affix' => 0, + ], + 'parent_id' => $now->id + ]); + $i++; + } + } +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist index ea2e2ebc1..39b582027 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,6 +7,7 @@ parameters: reportUnmatchedIgnoredErrors: false excludePaths: - tests/ + - app/Library/DataPermission/Scope/DataScopes.php ignoreErrors: - '#Static call to instance method Hyperf\\HttpServer\\Router\\Router::[a-zA-Z0-9\\_]+\(\)#' - '#Static call to instance method Hyperf\\DbConnection\\Db::[a-zA-Z0-9\\_]+\(\)#' diff --git a/storage/languages/zh_CN/user.php b/storage/languages/zh_CN/user.php index 714f237bd..23eab4ac2 100644 --- a/storage/languages/zh_CN/user.php +++ b/storage/languages/zh_CN/user.php @@ -44,4 +44,5 @@ 'old_password_error' => '旧密码错误', 'old_password' => '旧密码', 'password_confirmation' => '确认密码', + 'department' => '部门', ]; diff --git a/tests/Feature/Library/DataPermission/DataScopeAspectTest.php b/tests/Feature/Library/DataPermission/DataScopeAspectTest.php new file mode 100644 index 000000000..ddd7f2e8b --- /dev/null +++ b/tests/Feature/Library/DataPermission/DataScopeAspectTest.php @@ -0,0 +1,160 @@ +factory = $this->createMock(Factory::class); + $this->aspect = new DataScopeAspect($this->factory); + Context::set(DataScopeAspect::CONTEXT_KEY, null); + DataPermissionContext::setOnlyTables([]); + } + + protected function tearDown(): void + { + Context::destroy(DataScopeAspect::CONTEXT_KEY); + parent::tearDown(); + } + + public function testProcessWithAnnotationMetadata(): void + { + $pjp = $this->createMock(ProceedingJoinPoint::class); + $annotation = new DataScope(); + $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[ + 'class' => [DataScope::class => $annotation], + 'method' => [], + ])); + $pjp->expects(self::once())->method('process')->willReturn('result'); + + $result = $this->aspect->process($pjp); + self::assertEquals('result', $result); + } + + public function testProcessWithBuilderRunSelect(): void + { + $pjp = $this->createMock(ProceedingJoinPoint::class); + $pjp->className = Builder::class; + $pjp->methodName = 'runSelect'; + + $builder = $this->createMock(Builder::class); + $builder->from = 'test_table'; + + DataPermissionContext::setOnlyTables(['test_table']); + + $pjp->method('getInstance')->willReturn($builder); + $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[ + 'class' => [], + 'method' => [], + ])); + $pjp->expects(self::exactly(1))->method('process') + ->willReturn('select_result'); + + // 模拟 CurrentUser::ctxUser() 返回用户对象 + $user = new User(); + Context::set('current_user', $user); + + $this->factory->expects(self::once())->method('build')->with($builder, $user); + Context::set(DataScopeAspect::CONTEXT_KEY, 1); + $result = $this->aspect->process($pjp); + self::assertEquals('select_result', $result); + } + + public function testProcessWithBuilderDelete(): void + { + $pjp = $this->createMock(ProceedingJoinPoint::class); + $pjp->className = Builder::class; + $pjp->methodName = 'delete'; + $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[ + 'class' => [], + 'method' => [], + ])); + $pjp->expects(self::once())->method('process')->willReturn('delete_result'); + + $result = $this->aspect->process($pjp); + self::assertEquals('delete_result', $result); + } + + public function testProcessWithBuilderUpdate(): void + { + $pjp = $this->createMock(ProceedingJoinPoint::class); + $pjp->className = Builder::class; + $pjp->methodName = 'update'; + $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[ + 'class' => [], + 'method' => [], + ])); + $pjp->expects(self::once())->method('process')->willReturn('update_result'); + + $result = $this->aspect->process($pjp); + self::assertEquals('update_result', $result); + } + + public function testProcessWithoutAnnotationOrBuilder(): void + { + $pjp = $this->createMock(ProceedingJoinPoint::class); + $pjp->className = 'OtherClass'; + $pjp->methodName = 'otherMethod'; + $pjp->method('getAnnotationMetadata')->willReturn(new AnnotationMetadata(...[ + 'class' => [], + 'method' => [], + ])); + $pjp->expects(self::once())->method('process')->willReturn('other_result'); + + $result = $this->aspect->process($pjp); + self::assertEquals('other_result', $result); + } + + public function testHandleDataScopeSetsContextAndCallsProcess(): void + { + $pjp = $this->createMock(ProceedingJoinPoint::class); + $attribute = $this->createMock(DataScope::class); + $attribute->method('getDeptColumn')->willReturn('dept_id'); + $attribute->method('getCreatedByColumn')->willReturn('created_by'); + $attribute->method('getScopeType')->willReturn(ScopeType::DEPT_CREATED_BY); + $attribute->method('getOnlyTables')->willReturn(['table1']); + $metaData = new AnnotationMetadata([DataScope::class => $attribute], []); + $pjp->method('getAnnotationMetadata')->willReturn($metaData); + $pjp->expects(self::once())->method('process')->willReturn('scoped_result'); + + $result = $this->aspect->process($pjp); + self::assertEquals('scoped_result', $result); + self::assertNull(Context::get(DataScopeAspect::CONTEXT_KEY)); + } +} diff --git a/web/package.json b/web/package.json index ce4c68031..7e815a42a 100644 --- a/web/package.json +++ b/web/package.json @@ -47,7 +47,7 @@ "sortablejs": "1.15.6", "vaul-vue": "^0.3.0", "vue": "^3.5.13", - "vue-i18n": "^11.1.2", + "vue-i18n": "11.1.2", "vue-m-message": "^4.0.2", "vue-router": "^4.5.0", "vue3-sfc-loader": "^0.9.5", diff --git a/web/src/components/ma-tree/index.vue b/web/src/components/ma-tree/index.vue index 03c25661f..1cfc78f90 100644 --- a/web/src/components/ma-tree/index.vue +++ b/web/src/components/ma-tree/index.vue @@ -42,7 +42,7 @@ import { useLocalTrans } from '@/hooks/useLocalTrans.ts' defineOptions({ name: 'MaTree' }) const { treeKey = 'label' } = defineProps<{ - treeKey: string + treeKey?: string }>() const t = useLocalTrans() diff --git a/web/src/locales/en[English].yaml b/web/src/locales/en[English].yaml index e9f95c03d..c111918b4 100644 --- a/web/src/locales/en[English].yaml +++ b/web/src/locales/en[English].yaml @@ -175,3 +175,10 @@ dictionary: base: systemUser: System user normalUser: Normal user + dataScope: + all: Full data permissions + deptSelf: Current department data permissions + deptTree: Current department and all sub-departments data permissions + self: Personal data permissions + customDept: Custom department selection data permissions + customFunc: Custom function data permissions diff --git "a/web/src/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" "b/web/src/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" index 91fba092f..ff6f3ec3c 100644 --- "a/web/src/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" +++ "b/web/src/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" @@ -175,3 +175,10 @@ dictionary: base: systemUser: 系统用户 normalUser: 普通用户 + dataScope: + all: 全部数据权限 + deptSelf: 本部门数据权限 + deptTree: 本部门及所有子部门数据权限 + self: 本人数据权限 + customDept: 自选部门数据权限 + customFunc: 自定义函数数据权限 diff --git "a/web/src/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" "b/web/src/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" index f105f18c8..81264ad1b 100644 --- "a/web/src/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" +++ "b/web/src/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" @@ -175,3 +175,10 @@ dictionary: base: systemUser: 系統用戶 normalUser: 普通用戶 + dataScope: + all: 全部數據權限 + deptSelf: 本部門數據權限 + deptTree: 本部門及所有子部門數據權限 + self: 本人數據權限 + customDept: 自選部門數據權限 + customFunc: 自定義數據權限 diff --git a/web/src/modules/base/api/department.ts b/web/src/modules/base/api/department.ts new file mode 100644 index 000000000..45ca17e52 --- /dev/null +++ b/web/src/modules/base/api/department.ts @@ -0,0 +1,36 @@ +/** + * MineAdmin is committed to providing solutions for quickly building web applications + * Please view the LICENSE file that was distributed with this source code, + * For the full copyright and license information. + * Thank you very much for using MineAdmin. + * + * @Author X.Mo + * @Link https://github.com/mineadmin + */ +import type { PageList, ResponseStruct } from '#/global' + +export interface DepartmentVo { + id?: number + name?: string +} + +export interface DepartmentSearchVo { + name?: string + [key: string]: any +} + +export function page(data: DepartmentSearchVo | null = null): Promise>> { + return useHttp().get('/admin/department/list?level=1', { params: data }) +} + +export function create(data: DepartmentVo): Promise> { + return useHttp().post('/admin/department', data) +} + +export function save(id: number, data: DepartmentVo): Promise> { + return useHttp().put(`/admin/department/${id}`, data) +} + +export function deleteByIds(ids: number[]): Promise> { + return useHttp().delete('/admin/department', { data: ids }) +} diff --git a/web/src/modules/base/api/leader.ts b/web/src/modules/base/api/leader.ts new file mode 100644 index 000000000..18ea8d569 --- /dev/null +++ b/web/src/modules/base/api/leader.ts @@ -0,0 +1,37 @@ +/** + * MineAdmin is committed to providing solutions for quickly building web applications + * Please view the LICENSE file that was distributed with this source code, + * For the full copyright and license information. + * Thank you very much for using MineAdmin. + * + * @Author X.Mo + * @Link https://github.com/mineadmin + */ +import type { PageList, ResponseStruct } from '#/global' + +export interface LeaderVo { + user_id?: number | null + dept_id?: number + dept_name?: string +} + +export interface LeaderSearchVo { + user_id?: string + [key: string]: any +} + +export function page(data: LeaderSearchVo | null = null): Promise>> { + return useHttp().get('/admin/leader/list', { params: data }) +} + +export function create(data: LeaderVo): Promise> { + return useHttp().post('/admin/leader', data) +} + +export function save(id: number, data: LeaderVo): Promise> { + return useHttp().put(`/admin/leader/${id}`, data) +} + +export function deleteByDoubleKey(dept_id: number, user_ids: number[]): Promise> { + return useHttp().delete('/admin/leader', { data: { dept_id, user_ids } }) +} diff --git a/web/src/modules/base/api/position.ts b/web/src/modules/base/api/position.ts new file mode 100644 index 000000000..0069e11b8 --- /dev/null +++ b/web/src/modules/base/api/position.ts @@ -0,0 +1,43 @@ +/** + * MineAdmin is committed to providing solutions for quickly building web applications + * Please view the LICENSE file that was distributed with this source code, + * For the full copyright and license information. + * Thank you very much for using MineAdmin. + * + * @Author X.Mo + * @Link https://github.com/mineadmin + */ +import type { PageList, ResponseStruct } from '#/global' + +export interface PositionVo { + id?: number + dept_id?: number + dept_name?: string + name?: string + [key: string]: any +} + +export interface PositionSearchVo { + name?: string + [key: string]: any +} + +export function page(data: PositionSearchVo | null = null): Promise>> { + return useHttp().get('/admin/position/list', { params: data }) +} + +export function create(data: PositionVo): Promise> { + return useHttp().post('/admin/position', data) +} + +export function save(id: number, data: PositionVo): Promise> { + return useHttp().put(`/admin/position/${id}`, data) +} + +export function setDataScope(id: number, data: PositionVo): Promise> { + return useHttp().put(`/admin/position/${id}/data_permission`, data) +} + +export function deleteByIds(ids: number[]): Promise> { + return useHttp().delete('/admin/position', { data: ids }) +} diff --git a/web/src/modules/base/api/user.ts b/web/src/modules/base/api/user.ts index 4121a2f1f..2b609412c 100644 --- a/web/src/modules/base/api/user.ts +++ b/web/src/modules/base/api/user.ts @@ -25,6 +25,9 @@ export interface UserVo { backend_setting?: Record remark?: string password?: string + policy?: any + department?: any[] + position?: any[] } export interface UserSearchVo { diff --git a/web/src/modules/base/locales/en[English].yaml b/web/src/modules/base/locales/en[English].yaml index 550dd048a..c164c33e3 100644 --- a/web/src/modules/base/locales/en[English].yaml +++ b/web/src/modules/base/locales/en[English].yaml @@ -8,15 +8,17 @@ baseUserManage: userType: User type role: Role signed: Signed - mainTitle: User Manager + mainTitle: User Rule subTitle: Provide users with the functions of adding, editing, and deleting + dataScope: Data Scope setRole: Set role + setDataScope: Set data scope setRoleSuccess: The role was set successfully initPassword: Init password setPassword: Whether to reset the password to 123456? setPasswordSuccess: The password was reset successfully baseRoleManage: - mainTitle: Role Manager + mainTitle: Role Rule subTitle: Provide user roles and permission settings name: Role name code: Role code @@ -112,6 +114,27 @@ baseMenu: menuSave: Menu save menuUpdate: Menu updated menuDelete: Menu delete + department: Department + departmentList: Department list + departmentCreate: Add department + departmentSave: Edit department + departmentDelete: Delete department + positionList: Post list + positionCreate: Add post + positionSave: Edit post + positionDelete: Delete post + positionDataScope: Set post data scope +basePost: + name: Post Name + belongPost: Belongs to Position + belongDept: Belongs to Department + created_at: Creation Time + updated_at: Update Time + dataScope: Data Permissions + selectDept: Selected Departments + callFunc: Function Call Name + placeholder: + name: Please enter position name log: index: Log Management userLoginLog: Login Log diff --git "a/web/src/modules/base/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" "b/web/src/modules/base/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" index b5698cf03..873f5029d 100644 --- "a/web/src/modules/base/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" +++ "b/web/src/modules/base/locales/zh_CN[\347\256\200\344\275\223\344\270\255\346\226\207].yaml" @@ -3,6 +3,8 @@ baseUserManage: username: 用户名 nickname: 昵称 phone: 手机 + dept: 所属部门 + post: 部门岗位 email: 邮箱 password: 密码 userType: 用户类型 @@ -10,7 +12,9 @@ baseUserManage: signed: 个人签名 mainTitle: 用户管理 subTitle: 提供用户添加、编辑、删除功能,超管不可修改。 + dataScope: 数据权限 setRole: 赋予角色 + setDataScope: 设置数据权限 setRoleSuccess: 用户角色设置成功 initPassword: 初始化密码 setPassword: 是否将用户密码重置为[123456]? @@ -124,6 +128,43 @@ baseOperationLog: ip: 请求IP created_at: 创建时间 updated_at: 更新时间 +baseDepartment: + name: 部门名称 + created_at: 创建时间 + updated_at: 更新时间 + leaderCount: 负责人数 + positionsCount: 岗位数 + usersCount: 人员数 + parentDepartment: 上级部门 + page: + mainTitle: 部门管理 + subTitle: 提供公司、机构的人员部门管理、以及部门负责人。 + setLeader: 设置负责人 + previewUser: 查看用户 + managePost: 管理岗位 + placeholder: + name: 请输入部门名称 + parentDepartment: 请选择上级部门 + error: + selfNotParent: 不可选择自己作为上级部门,已还原默认数据 +basePost: + name: 岗位名称 + belongPost: 所属岗位 + belongDept: 所属部门 + created_at: 创建时间 + updated_at: 更新时间 + dataScope: 数据权限 + selectDept: 自选部门 + callFunc: 调用函数名 + placeholder: + name: 请输入岗位名称 +baseDeptLeader: + belongDept: 所属部门 + username: 用户名 + nickname: 昵称 + user_id: 选择用户 + placeholder: + user_id: 请选择要设置为负责人的用户 baseMenu: permission: index: 权限管理 @@ -147,6 +188,20 @@ baseMenu: menuSave: 菜单保存 menuUpdate: 菜单更新 menuDelete: 菜单删除 + department: 部门管理 + departmentList: 部门列表 + departmentCreate: 新增部门 + departmentSave: 编辑部门 + departmentDelete: 删除部门 + positionList: 岗位列表 + positionCreate: 新增岗位 + positionSave: 编辑岗位 + positionDelete: 删除岗位 + positionDataScope: 设置岗位数据权限 + leaderList: 部门领导列表 + leaderCreate: 新增部门领导 + leaderSave: 编辑部门领导 + leaderDelete: 删除部门领导 log: index: 日志管理 userLoginLog: 登录日志 diff --git "a/web/src/modules/base/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" "b/web/src/modules/base/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" index 46b315a38..8bf2c041a 100644 --- "a/web/src/modules/base/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" +++ "b/web/src/modules/base/locales/zh_TW[\347\271\201\351\253\224\344\270\255\346\226\207].yaml" @@ -10,7 +10,9 @@ baseUserManage: role: 角色 mainTitle: 用戶管理 subTitle: 提供使用者添加、編輯、刪除功能,超管不可修改。 + dataScope: 數據權限 setRole: 賦予角色 + setDataScope: 設置數據權限 setRoleSuccess: 用戶角色設置成功 initPassword: 重設密碼 setPassword: 是否將密碼重設為[123456]? @@ -112,6 +114,27 @@ baseMenu: menuSave: 菜單管保存 menuUpdate: 菜單管更新 menuDelete: 菜單管刪除 + department: 部門管理 + departmentList: 部門列表 + departmentCreate: 新增部門 + departmentSave: 編輯部門 + departmentDelete: 刪除部門 + positionList: 職位列表 + positionCreate: 新增職位 + positionSave: 編輯職位 + positionDelete: 刪除職位 + positionDataScope: 設置職位數據權限 +basePost: + name: 職位名稱 + belongPost: 所屬職位 + belongDept: 所屬部門 + created_at: 創建時間 + updated_at: 更新時間 + dataScope: 數據權限 + selectDept: 自選部門 + callFunc: 調用函數名 + placeholder: + name: 請輸入職位名稱 log: index: 日志管理 userLoginLog: 登錄日志 diff --git a/web/src/modules/base/views/permission/component/dataScope.vue b/web/src/modules/base/views/permission/component/dataScope.vue new file mode 100644 index 000000000..99d242f24 --- /dev/null +++ b/web/src/modules/base/views/permission/component/dataScope.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/web/src/modules/base/views/permission/department/data/getFormItems.tsx b/web/src/modules/base/views/permission/department/data/getFormItems.tsx new file mode 100644 index 000000000..28a853880 --- /dev/null +++ b/web/src/modules/base/views/permission/department/data/getFormItems.tsx @@ -0,0 +1,64 @@ +/** + * MineAdmin is committed to providing solutions for quickly building web applications + * Please view the LICENSE file that was distributed with this source code, + * For the full copyright and license information. + * Thank you very much for using MineAdmin. + * + * @Author X.Mo + * @Link https://github.com/mineadmin + */ +import type { MaFormItem } from '@mineadmin/form' +import { cloneDeep } from 'lodash-es' + +export default function getFormItems(formType: 'add' | 'edit' = 'add', t: any, model: any, msg: any): MaFormItem[] { + const treeSelectRef = ref() + const deptList = ref([]) + const sourceModel = cloneDeep(model) + + if (formType === 'add') { + model.parent_id = 0 + } + + useHttp().get('/admin/department/list?level=1').then((res: any) => { + deptList.value = res.data.list + deptList.value.unshift({ id: 0, name: '顶级部门', value: 0 } as any) + }) + + return [ + { + label: () => t('baseDepartment.parentDepartment'), prop: 'parent_id', + render: () => ( + { + if (val === model.id) { + msg.error(t('baseDepartment.error.selfNotParent')) + model.parent_id = sourceModel.parent_id + } + }} + > + + ), + renderProps: { + class: 'w-full', + placeholder: t('baseDepartment.placeholder.parentDepartment'), + }, + }, + { + label: () => t('baseDepartment.name'), + prop: 'name', + render: 'input', + renderProps: { + placeholder: t('form.pleaseInput', { msg: t('baseDepartment.name') }), + }, + itemProps: { + rules: [{ required: true, message: t('form.requiredInput', { msg: t('baseDepartment.placeholder.name') }) }], + }, + }, + ] +} diff --git a/web/src/modules/base/views/permission/department/data/getSearchItems.tsx b/web/src/modules/base/views/permission/department/data/getSearchItems.tsx new file mode 100644 index 000000000..2e5151200 --- /dev/null +++ b/web/src/modules/base/views/permission/department/data/getSearchItems.tsx @@ -0,0 +1,21 @@ +/** + * MineAdmin is committed to providing solutions for quickly building web applications + * Please view the LICENSE file that was distributed with this source code, + * For the full copyright and license information. + * Thank you very much for using MineAdmin. + * + * @Author X.Mo + * @Link https://github.com/mineadmin + */ + +import type { MaSearchItem } from '@mineadmin/search' + +export default function getSearchItems(t: any): MaSearchItem[] { + return [ + { + label: () => t('baseDepartment.name'), + prop: 'name', + render: 'input', + }, + ] +} diff --git a/web/src/modules/base/views/permission/department/data/getTableColumns.tsx b/web/src/modules/base/views/permission/department/data/getTableColumns.tsx new file mode 100644 index 000000000..ea16bc823 --- /dev/null +++ b/web/src/modules/base/views/permission/department/data/getTableColumns.tsx @@ -0,0 +1,106 @@ +/** + * MineAdmin is committed to providing solutions for quickly building web applications + * Please view the LICENSE file that was distributed with this source code, + * For the full copyright and license information. + * Thank you very much for using MineAdmin. + * + * @Author X.Mo + * @Link https://github.com/mineadmin + */ +import type { MaProTableColumns, MaProTableExpose } from '@mineadmin/pro-table' +import type { UseDialogExpose } from '@/hooks/useDialog.ts' + +import { useMessage } from '@/hooks/useMessage.ts' +import { deleteByIds } from '~/base/api/department.ts' +import { ResultCode } from '@/utils/ResultCode.ts' +import hasAuth from '@/utils/permission/hasAuth.ts' + +export default function getTableColumns(dialog: UseDialogExpose, formRef: any, t: any): MaProTableColumns[] { + const msg = useMessage() + + const showBtn = (auth: string | string[]) => { + return hasAuth(auth) + } + + return [ + // 多选列 + { type: 'selection', showOverflowTooltip: false, label: () => t('crud.selection') }, + // 普通列 + { label: () => t('baseDepartment.name'), prop: 'name', align: 'left' }, + { label: () => t('baseDepartment.leaderCount'), prop: 'leader', cellRender: ({ row }) => row.leader?.length ?? 0 }, + { label: () => t('baseDepartment.positionsCount'), prop: 'positions', cellRender: ({ row }) => row.positions?.length ?? 0 }, + { label: () => t('baseDepartment.usersCount'), prop: 'users', cellRender: ({ row }) => row.department_users?.length ?? 0 }, + { label: () => t('baseDepartment.created_at'), prop: 'created_at', width: 200 }, + { label: () => t('baseDepartment.updated_at'), prop: 'updated_at', width: 200 }, + + // 操作列 + { + type: 'operation', + label: () => t('crud.operation'), + width: '180px', + operationConfigure: { + actions: [ + { + name: 'setLeader', + show: () => showBtn('permission:leader:index'), + icon: 'material-symbols:checklist-rounded', + text: () => t('baseDepartment.page.setLeader'), + onClick: ({ row }) => { + dialog.setTitle(t('baseDepartment.page.setLeader')) + dialog.setAttr({ width: '55%' }) + dialog.open({ formType: 'setLeader', data: row }) + }, + }, + { + name: 'managePost', + icon: 'material-symbols:position-bottom-right-outline-rounded', + show: () => showBtn('permission:position:index'), + text: () => t('baseDepartment.page.managePost'), + onClick: ({ row }) => { + dialog.setTitle(t('baseDepartment.page.managePost')) + dialog.setAttr({ width: '55%' }) + dialog.open({ formType: 'position', data: row }) + }, + }, + { + name: 'viewUser', + icon: 'uil:users-alt', + show: () => showBtn('permission:department:update'), + text: () => t('baseDepartment.page.previewUser'), + onClick: ({ row }) => { + dialog.setTitle(t('baseDepartment.page.previewUser')) + dialog.setAttr({ width: '55%' }) + dialog.open({ formType: 'viewUser', data: row }) + }, + }, + { + name: 'edit', + icon: 'material-symbols:person-edit', + show: () => showBtn('permission:department:update'), + text: () => t('crud.edit'), + onClick: ({ row }) => { + dialog.setTitle(t('crud.edit')) + dialog.setAttr({ width: '550px' }) + dialog.open({ formType: 'edit', data: row }) + }, + }, + { + name: 'del', + show: () => showBtn('permission:department:delete'), + icon: 'mdi:delete', + text: () => t('crud.delete'), + onClick: ({ row }, proxy: MaProTableExpose) => { + msg.delConfirm(t('crud.delDataMessage')).then(async () => { + const response = await deleteByIds([row.id]) + if (response.code === ResultCode.SUCCESS) { + msg.success(t('crud.delSuccess')) + await proxy.refresh() + } + }) + }, + }, + ], + }, + }, + ] +} diff --git a/web/src/modules/base/views/permission/department/form.vue b/web/src/modules/base/views/permission/department/form.vue new file mode 100644 index 000000000..8b1e48af7 --- /dev/null +++ b/web/src/modules/base/views/permission/department/form.vue @@ -0,0 +1,78 @@ + + + + + + diff --git a/web/src/modules/base/views/permission/department/index.vue b/web/src/modules/base/views/permission/department/index.vue new file mode 100644 index 000000000..c8d513445 --- /dev/null +++ b/web/src/modules/base/views/permission/department/index.vue @@ -0,0 +1,218 @@ + + + + + + diff --git a/web/src/modules/base/views/permission/department/position.vue b/web/src/modules/base/views/permission/department/position.vue new file mode 100644 index 000000000..9fdd9754f --- /dev/null +++ b/web/src/modules/base/views/permission/department/position.vue @@ -0,0 +1,276 @@ + + + + + + + diff --git a/web/src/modules/base/views/permission/department/setLeader.vue b/web/src/modules/base/views/permission/department/setLeader.vue new file mode 100644 index 000000000..3b6eaed14 --- /dev/null +++ b/web/src/modules/base/views/permission/department/setLeader.vue @@ -0,0 +1,291 @@ + + + + + + + diff --git a/web/src/modules/base/views/permission/department/viewUser.vue b/web/src/modules/base/views/permission/department/viewUser.vue new file mode 100644 index 000000000..83fa07620 --- /dev/null +++ b/web/src/modules/base/views/permission/department/viewUser.vue @@ -0,0 +1,66 @@ + + + + + + + diff --git a/web/src/modules/base/views/permission/menu/index.vue b/web/src/modules/base/views/permission/menu/index.vue index 2a8da12c6..4733b35e8 100644 --- a/web/src/modules/base/views/permission/menu/index.vue +++ b/web/src/modules/base/views/permission/menu/index.vue @@ -11,7 +11,8 @@ import type { ElForm } from 'element-plus' import { useMessage } from '@/hooks/useMessage.ts' import getOnlyWorkAreaHeight from '@/utils/getOnlyWorkAreaHeight.ts' -import { create, type MenuVo, page, save } from '~/base/api/menu.ts' +import { create, page, save } from '~/base/api/menu.ts' +import type { MenuVo } from '~/base/api/menu.ts' import MenuTree from './menu-tree.vue' import MenuForm from './menu-form.vue' diff --git a/web/src/modules/base/views/permission/user/data/getFormItems.tsx b/web/src/modules/base/views/permission/user/data/getFormItems.tsx index 559ce27ed..d1149c05d 100644 --- a/web/src/modules/base/views/permission/user/data/getFormItems.tsx +++ b/web/src/modules/base/views/permission/user/data/getFormItems.tsx @@ -11,16 +11,59 @@ import type { MaFormItem } from '@mineadmin/form' import type { UserVo } from '~/base/api/user.ts' import MaUploadImage from '@/components/ma-upload-image/index.vue' import MaDictRadio from '@/components/ma-dict-picker/ma-dict-radio.vue' +import type { UseDialogExpose } from '@/hooks/useDialog.ts' -export default function getFormItems(formType: 'add' | 'edit' = 'add', t: any, model: UserVo): MaFormItem[] { +export default function getFormItems( + formType: 'add' | 'edit' = 'add', + t: any, + model: UserVo, + deptData: any, + dialog: UseDialogExpose, + scopeRef: any, +): MaFormItem[] { if (formType === 'add') { model.password = '123456' model.status = 1 model.user_type = 100 + model.department = [] + model.position = [] } + const departmentList = deptData.value.filter((_, index) => index > 0) + const deptIds = ref([]) + const postList = ref([]) + model.backend_setting = [] + if (formType === 'edit') { + const findNode = (nodes: any[], id: number) => { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].id === id) { + return nodes[i] + } + if (nodes[i].children) { + const node = findNode(nodes[i].children, id) + if (node) { + return node + } + } + } + return null + } + + model.department = model.department?.map((item: any) => { + // 添加 + deptIds.value.push(item.id) + const post = JSON.parse(JSON.stringify(findNode(departmentList, item.id) ?? null)) + if (post) { + post.disabled = true + postList.value.push(post) + } + return item.id + }) + model.position = model.position?.map((item: any) => item.id) + } + return [ { label: () => t('baseUserManage.avatar'), @@ -73,6 +116,115 @@ export default function getFormItems(formType: 'add' | 'edit' = 'add', t: any, m placeholder: t('form.pleaseInput', { msg: t('baseUserManage.phone') }), }, }, + { + label: () => t('baseUserManage.dept'), + prop: 'department', + render: () => , + renderProps: { + data: departmentList, + multiple: true, + filterable: true, + clearable: true, + props: { label: 'name' }, + checkStrictly: true, + nodeKey: 'id', + placeholder: t('form.pleaseInput', { msg: t('baseUserManage.dept') }), + onNodeClick: (row: any) => { + if (deptIds.value.includes(row.id)) { + // 移除 + deptIds.value = deptIds.value.filter((id: number) => id !== row.id) + postList.value = postList.value.filter((item: any) => item.id !== row.id) + } + else { + // 添加 + deptIds.value.push(row.id) + const post = JSON.parse(JSON.stringify(row)) + post.disabled = true + postList.value.push(post) + } + }, + onRemoveTag: (value: number) => { + const current = postList.value.find((item: any) => item.id === value)?.positions ?? [] + if (current.length > 0) { + current?.map((item: any) => { + if (model.position?.includes(item.id)) { + model.position?.splice(model.position?.indexOf(item.id), 1) + } + }) + } + postList.value = postList.value.filter((item: any) => item.id !== value) + }, + onClear: () => { + postList.value = [] + model.position = [] + }, + }, + }, + { + label: () => t('baseUserManage.post'), + prop: 'position', + render: () => , + cols: { md: 12, xs: 24 }, + renderProps: { + data: postList, + defaultExpandAll: true, + multiple: true, + filterable: true, + clearable: true, + props: { label: 'name', children: 'positions' }, + checkStrictly: true, + nodeKey: 'id', + placeholder: t('form.pleaseInput', { msg: t('baseUserManage.post') }), + }, + }, + { + render: () => , + cols: { md: 12, xs: 24 }, + renderProps: { + type: 'primary', + plain: true, + onClick: async () => { + dialog.setTitle(t('baseUserManage.setDataScope')) + dialog.open() + if (formType === 'add') { + model.policy = { + value: [], + name: model.username, + policy_type: '', + } + } + if (formType === 'edit') { + if (model.policy) { + model.policy.name = model.username + if (model.policy.policy_type === 'CUSTOM_FUNC') { + model.policy.func_name = model.policy.value + } + if (model.policy.policy_type === 'CUSTOM_DEPT') { + await nextTick(() => { + scopeRef.value.deptRef.elTree?.setCheckedKeys(model.policy.value, true) + }) + } + } + else { + model.policy = { name: model.username } + } + } + }, + }, + itemSlots: { + label: () => ( +

+ {t('baseUserManage.dataScope')} + + + +
+ ), + }, + renderSlots: { + default: () => t('baseUserManage.setDataScope'), + }, + }, { label: () => t('baseUserManage.email'), prop: 'email', diff --git a/web/src/modules/base/views/permission/user/data/getTableColumns.tsx b/web/src/modules/base/views/permission/user/data/getTableColumns.tsx index 99f24fc63..2a3ab5b6d 100644 --- a/web/src/modules/base/views/permission/user/data/getTableColumns.tsx +++ b/web/src/modules/base/views/permission/user/data/getTableColumns.tsx @@ -86,7 +86,7 @@ export default function getTableColumns(dialog: UseDialogExpose, formRef: any, t const response = await deleteByIds([row.id]) if (response.code === ResultCode.SUCCESS) { msg.success(t('crud.delSuccess')) - proxy.refresh() + await proxy.refresh() } }) }, diff --git a/web/src/modules/base/views/permission/user/form.vue b/web/src/modules/base/views/permission/user/form.vue index 253b6a599..c8550f197 100644 --- a/web/src/modules/base/views/permission/user/form.vue +++ b/web/src/modules/base/views/permission/user/form.vue @@ -14,17 +14,35 @@ import getFormItems from './data/getFormItems.tsx' import type { MaFormExpose } from '@mineadmin/form' import useForm from '@/hooks/useForm.ts' import { ResultCode } from '@/utils/ResultCode.ts' +import type { UseDialogExpose } from '@/hooks/useDialog.ts' +import useDialog from '@/hooks/useDialog.ts' +import DataScope from '../component/dataScope.vue' defineOptions({ name: 'permission:user:form' }) - const { formType = 'add', data = null } = defineProps<{ - formType: 'add' | 'edit' + formType?: 'add' | 'edit' data?: UserVo | null }>() const t = useTrans().globalTrans const userForm = ref() const userModel = ref({}) +const scopeRef = ref() +const deptData = inject('deptData') + +// 弹窗配置 +const maDialog: UseDialogExpose = useDialog({ + lgWidth: '750px', + ok: () => { + if (userModel.value.policy.policy_type === 'CUSTOM_FUNC') { + userModel.value.policy.value = [userModel.value.policy.func_name] + } + if (userModel.value.policy.policy_type === 'CUSTOM_DEPT') { + userModel.value.policy.value = scopeRef.value.deptRef.elTree?.getCheckedKeys() + } + maDialog.close() + }, +}) useForm('userForm').then((form: MaFormExpose) => { if (formType === 'edit' && data) { @@ -32,9 +50,9 @@ useForm('userForm').then((form: MaFormExpose) => { userModel.value[key] = data[key] }) } - form.setItems(getFormItems(formType, t, userModel.value)) + form.setItems(getFormItems(formType, t, userModel.value, deptData, maDialog, scopeRef)) form.setOptions({ - labelWidth: '80px', + labelWidth: '90px', }) }) @@ -43,8 +61,6 @@ function add(): Promise { return new Promise((resolve, reject) => { create(userModel.value).then((res: any) => { res.code === ResultCode.SUCCESS ? resolve(res) : reject(res) - }).catch((err) => { - reject(err) }) }) } @@ -52,10 +68,11 @@ function add(): Promise { // 更新操作 function edit(): Promise { return new Promise((resolve, reject) => { + if (userModel.value.policy === null) { + userModel.value.policy = [] + } save(userModel.value.id as number, userModel.value).then((res: any) => { res.code === ResultCode.SUCCESS ? resolve(res) : reject(res) - }).catch((err) => { - reject(err) }) }) } @@ -68,7 +85,12 @@ defineExpose({