diff --git a/app/Admin/Controllers/AdController.php b/app/Admin/Controllers/AdController.php index 74287f3..45279f8 100644 --- a/app/Admin/Controllers/AdController.php +++ b/app/Admin/Controllers/AdController.php @@ -7,7 +7,7 @@ use Slowlyo\OwlAdmin\Renderers\Page; use Slowlyo\OwlAdmin\Renderers\Form; use Slowlyo\OwlAdmin\Controllers\AdminController; use Slowlyo\OwlAdmin\Renderers\Operation; -use App\Services\Admin\AdService; +use App\Admin\Services\AdService; use App\Admin\Components; use App\Models\Keyword; use App\Models\Ad; diff --git a/app/Admin/Controllers/AdminController.php b/app/Admin/Controllers/AdminController.php new file mode 100644 index 0000000..b85eeab --- /dev/null +++ b/app/Admin/Controllers/AdminController.php @@ -0,0 +1,131 @@ +actionOfQuickEdit()) { + $result = $this->service->quickEdit($request->all()); + } elseif ($this->actionOfQuickEditItem()) { + $result = $this->service->quickEditItem($request->all()); + } else { + $result = $this->service->store($request->all()); + } + + if (! $result) { + admin_abort($this->service->getError() ?: __('admin.save').__('admin.failed')); + } + + DB::commit(); + } catch (Throwable $th) { + DB::rollBack(); + return $this->renderException($th); + } + + return $this->response()->successMessage(__('admin.save').__('admin.successfully')); + } + + /** + * {@inheritdoc} + */ + public function update(Request $request) + { + $input = Arr::except($request->all(), $this->service->primaryKey()); + + if ($request->filled('_fields')) { + $input = Arr::only($input, explode(',', $request->input('_fields'))); + } + + try { + DB::beginTransaction(); + + $result = $this->service->update($this->getPrimaryValue($request), $input); + + if (! $result) { + admin_abort($this->service->getError() ?: __('admin.save').__('admin.failed')); + } + + DB::commit(); + } catch (Throwable $th) { + DB::rollBack(); + return $this->renderException($th); + } + + return $this->response()->successMessage(__('admin.save').__('admin.successfully')); + } + + /** + * {@inheritdoc} + */ + public function destroy($ids) + { + try { + DB::beginTransaction(); + + $result = $this->service->delete($ids); + if ($result === false) { + admin_abort($this->service->getError()); + } + + DB::commit(); + } catch (Throwable $th) { + DB::rollBack(); + return $this->renderException($th); + } + + return $this->response()->successMessage(__('admin.delete').__('admin.successfully')); + } + + public function getQuickEditItemPath(array $fields = ['*']) + { + $path = $this->getUpdatePath(); + + if ($fields != ['*']) { + $path .= '?_fields='.implode(',', $fields); + } + + return $path; + } + + protected function renderException(Throwable $e) + { + report($e); + + if ($e instanceof AdminException) { + return $e->render(); + } + + $message = $e->getMessage(); + + if ($e instanceof ValidationException) { + $message = Arr::first($e->errors()); + if (is_array($message)) { + $message = Arr::first($message) ?: '参数错误'; + } + } + + return $this->response()->fail($message); + } +} diff --git a/app/Admin/Controllers/AdminRoleController.php b/app/Admin/Controllers/AdminRoleController.php deleted file mode 100644 index 5fe8ea2..0000000 --- a/app/Admin/Controllers/AdminRoleController.php +++ /dev/null @@ -1,50 +0,0 @@ -baseCRUD() - ->headerToolbar([ - $this->createButton(true), - ...$this->baseHeaderToolBar(), - ]) - ->filterTogglable(false) - ->itemCheckableOn('${id !== 1}') - ->columns([ - amis()->TableColumn()->label('ID')->name('id')->sortable(), - amis()->TableColumn()->label(__('admin.admin_role.name'))->name('name'), - amis()->TableColumn()->label(__('admin.admin_role.slug'))->name('slug')->type('tag'), - amis()->TableColumn() - ->label(__('admin.created_at')) - ->name('created_at') - ->type('datetime') - ->sortable(true), - amis()->TableColumn() - ->label(__('admin.updated_at')) - ->name('updated_at') - ->type('datetime') - ->sortable(true), - amis()->Operation()->label(__('admin.actions'))->buttons([ - $this->setPermission()->visibleOn('${slug != "administrator"}'), - $this->rowEditButton(true)->visibleOn('${slug != "administrator"}'), - $this->rowDeleteButton()->visibleOn('${slug != "administrator"}'), - ]), - ]); - - return $this->baseList($crud)->css([ - '.tree-full' => [ - 'overflow' => 'hidden !important', - ], - '.cxd-TreeControl > .cxd-Tree' => [ - 'height' => '100% !important', - 'max-height' => '100% !important', - ], - ]); - } -} \ No newline at end of file diff --git a/app/Admin/Controllers/AdminUserController.php b/app/Admin/Controllers/AdminUserController.php deleted file mode 100644 index a883744..0000000 --- a/app/Admin/Controllers/AdminUserController.php +++ /dev/null @@ -1,99 +0,0 @@ -baseCRUD() - ->headerToolbar([ - $this->createButton(true), - ...$this->baseHeaderToolBar(), - ]) - ->filter($this->baseFilter()->body( - amisMake()->TextControl('keyword', __('admin.keyword')) - ->size('md') - ->placeholder(__('admin.admin_user.search_username')) - )) - ->quickSaveItemApi(admin_url('quick-edit/admin_users/$id')) - ->itemCheckableOn('${id !== 1}') - ->columns([ - amisMake()->TableColumn('id', 'ID')->sortable(), - amisMake()->TableColumn('avatar', __('admin.admin_user.avatar'))->type('avatar')->src('${avatar}'), - amisMake()->TableColumn('username', __('admin.username')), - amisMake()->TableColumn('name', __('admin.admin_user.name')), - amisMake()->TableColumn('roles', __('admin.admin_user.roles'))->type('each')->items( - amisMake()->Tag()->label('${name}')->className('my-1') - ), - amisMake()->TableColumn('lock', __('admin.admin_user.lock'))->quickEdit(SwitchControl::make()->saveImmediately(true)->mode('inline')->disabledOn('${id === 1}')), - amisMake()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true), - Operation::make()->label(__('admin.actions'))->buttons([ - $this->rowEditButton(true)->visibleOn('${id != 1}'), - //单独修改密码 - $this->editPassword()->visibleOn('${id != 1}'), - $this->rowDeleteButton()->visibleOn('${id != 1}'), - ]), - ]); - - return $this->baseList($crud); - } - - public function form(): Form - { - return $this->baseForm()->body([ - amisMake()->ImageControl('avatar', __('admin.admin_user.avatar'))->receiver($this->uploadImagePath()), - amisMake()->TextControl('name', __('admin.admin_user.name'))->required(), - amisMake()->TextControl('username', __('admin.username'))->required(), - amisMake()->TextControl('password', __('admin.password'))->type('input-password')->required()->validations(['minLength' => 6])->hiddenOn('${id > 0}'), - amisMake()->TextControl('confirm_password', __('admin.confirm_password'))->type('input-password')->required()->validations(['minLength' => 6])->hiddenOn('${id > 0}'), - amisMake()->SelectControl('roles', __('admin.admin_user.roles')) - ->searchable() - ->multiple() - ->labelField('name') - ->valueField('id') - ->joinValues(false) - ->extractValue() - ->options(AdminRoleService::make()->query()->get(['id', 'name'])), - SwitchControl::make()->name('lock')->label(__('admin.admin_user.lock'))->value(false), - ]); - } - - public function detail(): Form - { - return $this->baseDetail()->body([]); - } - - public function editPassword() - { - return amisMake()->DialogAction()->icon('fa-regular fa-lightbulb')->label(__('admin.admin_user.edit_password'))->level('link')->dialog( - amisMake()->Dialog()->title(__('admin.admin_user.edit_password'))->body([ - amisMake()->Form()->title('') - ->api([ - 'method'=>'PUT', - 'url'=> admin_url('system/admin_users/$id') - ]) - ->body([ - amisMake()->TextControl('id')->value('${id}')->hidden(true), - amisMake()->TextControl('username')->value('${username}')->hidden(true), - amisMake()->TextControl('password', __('admin.password'))->type('input-password')->required()->validations(['minLength' => 6]), - amisMake()->TextControl('confirm_password', __('admin.confirm_password'))->type('input-password')->required()->validations(['minLength' => 6]), - ]), - ])->size('md') - ); - } -} diff --git a/app/Admin/Controllers/ArticleController.php b/app/Admin/Controllers/ArticleController.php index 2c646cc..3c96051 100644 --- a/app/Admin/Controllers/ArticleController.php +++ b/app/Admin/Controllers/ArticleController.php @@ -7,7 +7,7 @@ use Slowlyo\OwlAdmin\Admin; use Slowlyo\OwlAdmin\Renderers\Page; use Slowlyo\OwlAdmin\Renderers\Form; use Slowlyo\OwlAdmin\Controllers\AdminController; -use App\Services\Admin\ArticleService; +use App\Admin\Services\ArticleService; use App\Admin\Components; use App\Models\Keyword; use App\Traits\CustomActionTrait; diff --git a/app/Admin/Controllers/AuthController.php b/app/Admin/Controllers/AuthController.php index 95a3ee4..5117cd2 100644 --- a/app/Admin/Controllers/AuthController.php +++ b/app/Admin/Controllers/AuthController.php @@ -2,20 +2,22 @@ namespace App\Admin\Controllers; -use Slowlyo\OwlAdmin\Admin; +use App\Admin\Services\System\AdminUserService; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Validator; -use Symfony\Component\HttpFoundation\Response; -use Slowlyo\OwlAdmin\Models\AdminUser; -use Slowlyo\OwlAdmin\Controllers\AuthController as AdminAuthController; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Validator; +use Slowlyo\OwlAdmin\Admin; +use Slowlyo\OwlAdmin\Controllers\AuthController as AdminAuthController; +use Symfony\Component\HttpFoundation\Response; class AuthController extends AdminAuthController { + protected string $serviceName = AdminUserService::class; + public function login(Request $request) { if (Admin::config('admin.auth.login_captcha')) { - if (!$request->has('captcha')) { + if (! $request->has('captcha')) { return $this->response()->fail(__('admin.required', ['attribute' => __('admin.captcha')])); } @@ -29,22 +31,25 @@ class AuthController extends AdminAuthController 'username' => 'required', 'password' => 'required', ], [ - 'username' . '.required' => __('admin.required', ['attribute' => __('admin.username')]), - 'password.required' => __('admin.required', ['attribute' => __('admin.password')]), + 'username'.'.required' => __('admin.required', ['attribute' => __('admin.username')]), + 'password.required' => __('admin.required', ['attribute' => __('admin.password')]), ]); if ($validator->fails()) { abort(Response::HTTP_BAD_REQUEST, $validator->errors()->first()); } - $adminModel = Admin::config("admin.auth.model", AdminUser::class); - $user = $adminModel::query()->where('username', $request->username)->first(); - if($user && $user->lock){ + + $adminModel = Admin::adminUserModel(); + $user = $adminModel::query()->where('username', $request->username)->first(); + + if ($user && $user->lock) { abort(Response::HTTP_BAD_REQUEST, '您的账号已被锁定,需要联系超级管理员解锁。'); - }else{ + } else { if ($user && Hash::check($request->password, $user->password)) { $module = Admin::currentModule(true); - $prefix = $module ? $module . '.' : ''; - $token = $user->createToken($prefix . 'admin')->plainTextToken; + $prefix = $module ? $module.'.' : ''; + $token = $user->createToken($prefix.'admin')->plainTextToken; + return $this->response()->success(compact('token'), __('admin.login_successful')); } diff --git a/app/Admin/Controllers/System/AdminMenuController.php b/app/Admin/Controllers/System/AdminMenuController.php new file mode 100644 index 0000000..5445719 --- /dev/null +++ b/app/Admin/Controllers/System/AdminMenuController.php @@ -0,0 +1,128 @@ +baseCRUD() + ->loadDataOnce() + ->syncLocation(false) + ->footerToolbar([]) + ->headerToolbar([ + $this->createTypeButton('drawer', 'lg')->visible(Admin::user()->can('admin.system.admin_menus.create')), + ...$this->baseHeaderToolBar(), + ]) + ->filterTogglable(false) + ->footerToolbar(['statistics']) + ->bulkActions([ + $this->bulkDeleteButton()->reload('window')->visible(Admin::user()->can('admin.system.admin_menus.delete')), + ]) + ->columns([ + amis()->TableColumn('id', 'ID')->sortable(), + amis()->TableColumn('title', __('admin.admin_menu.title')), + amis()->TableColumn('icon', __('admin.admin_menu.icon')) + ->type('flex') + ->justify('start') + ->items([ + amis()->SvgIcon()->icon('${icon}')->className('mr-2 text-lg'), + '${icon}', + ]), + amis()->TableColumn('url', __('admin.admin_menu.url')), + amis()->TableColumn('order', __('admin.admin_menu.order'))->quickEdit( + amis()->NumberControl()->min(0)->saveImmediately(true) + ), + amis()->TableColumn('visible', __('admin.admin_menu.visible'))->quickEdit( + amis()->SwitchControl()->mode('inline')->saveImmediately(true) + ), + amis()->TableColumn('is_home', __('admin.admin_menu.is_home'))->quickEdit( + amis()->SwitchControl()->mode('inline')->saveImmediately(true) + ), + $this->rowActions([ + $this->rowEditTypeButton('drawer', 'lg')->visible(Admin::user()->can('admin.system.admin_menus.update')), + $this->rowDeleteButton()->visible(Admin::user()->can('admin.system.admin_menus.delete')), + ]), + ]); + + return $this->baseList($crud); + } + + public function form(): Form + { + return $this->baseForm()->body([ + amis()->GroupControl()->body([ + amis()->TextControl('title', __('admin.admin_menu.title'))->required(), + amis()->TextControl('icon', __('admin.admin_menu.icon')) + ->description( + __('admin.admin_menu.icon_description'). + ' https://icones.js.org' + ), + ]), + amis()->GroupControl()->body([ + amis()->TreeSelectControl('parent_id', __('admin.admin_menu.parent_id')) + ->labelField('title') + ->valueField('id') + ->showIcon(false) + ->value(0) + ->source('/system/admin_menus?_action=getData'), + amis() + ->TextControl('component', __('admin.admin_menu.component')) + ->description(__('admin.admin_menu.component_desc')) + ->value('amis'), + ]), + amis()->TextControl('url', __('admin.admin_menu.url')) + ->required() + ->validateOnChange() + ->validations(['matchRegexp' => '/^(http(s)?\:\/)?(\/)+/']) + ->validationErrors(['matchRegexp' => __('admin.need_start_with_slash')]) + ->placeholder('eg: /admin_menus'), + amis()->NumberControl('order', __('admin.admin_menu.order')) + ->required() + ->displayMode('enhance') + ->description(__('admin.order_asc')) + ->min(0) + ->value(0), + amis()->ListControl('url_type', __('admin.admin_menu.type')) + ->options(Admin::adminMenuModel()::getType()) + ->value(Admin::adminMenuModel()::TYPE_ROUTE), + amis()->SwitchControl('visible', __('admin.admin_menu.visible')) + ->onText(__('admin.admin_menu.show')) + ->offText(__('admin.admin_menu.hide')) + ->value(1), + amis()->SwitchControl('is_home', __('admin.admin_menu.is_home')) + ->onText(__('admin.yes')) + ->offText(__('admin.no')) + ->description(__('admin.admin_menu.is_home_description')) + ->value(0), + amis()->SwitchControl('is_full', __('admin.admin_menu.is_full')) + ->onText(__('admin.yes')) + ->offText(__('admin.no')) + ->description(__('admin.admin_menu.is_full_description')) + ->value(0), + ])->onEvent([ + 'submitSucc' => [ + 'actions' => [ + 'actionType' => 'custom', + 'script' => 'window.location.reload()', + ], + ], + ]); + } + + public function detail(): Form + { + return $this->baseDetail()->body([]); + } +} diff --git a/app/Admin/Controllers/System/AdminPermissionController.php b/app/Admin/Controllers/System/AdminPermissionController.php new file mode 100644 index 0000000..75a9692 --- /dev/null +++ b/app/Admin/Controllers/System/AdminPermissionController.php @@ -0,0 +1,223 @@ +baseCRUD() + ->loadDataOnce() + ->filterTogglable(false) + ->footerToolbar([]) + ->headerToolbar([ + $this->createTypeButton('drawer', 'lg')->visible(Admin::user()->can('admin.system.admin_permissions.create')), + ...$this->baseHeaderToolBar(), + ]) + ->bulkActions([ + $this->bulkDeleteButton()->visible(Admin::user()->can('admin.system.admin_permissions.delete')), + ]) + ->columns([ + amis()->TableColumn('id', 'ID')->sortable(), + amis()->TableColumn('name', __('admin.admin_permission.name')), + amis()->TableColumn('slug', __('admin.admin_permission.slug')), + amis()->TableColumn('http_method', __('admin.admin_permission.http_method')) + ->type('each') + ->items( + Tag::make()->label('${item}')->className('my-1') + ) + ->placeholder(Tag::make()->label('ANY')), + amis()->TableColumn('http_path', __('admin.admin_permission.http_path')) + ->type('each') + ->items( + Tag::make()->label('${item}')->className('my-1') + ), + $this->rowActions([ + $this->rowEditTypeButton('drawer', 'lg') + ->visible(Admin::user()->can('admin.system.admin_permissions.update')), + $this->rowDeleteButton()->visible(Admin::user()->can('admin.system.admin_permissions.delete')), + ]), + ]); + + return $this->baseList($crud); + } + + public function form(): Form + { + return $this->baseForm()->body([ + amis()->TextControl('name', __('admin.admin_permission.name'))->required(), + amis()->TextControl('slug', __('admin.admin_permission.slug'))->required(), + amis()->TreeSelectControl('parent_id', __('admin.parent')) + ->labelField('name') + ->valueField('id') + ->value(0) + ->options($this->service->getTree()), + amis()->CheckboxesControl('http_method', __('admin.admin_permission.http_method')) + ->options($this->getHttpMethods()) + ->description(__('admin.admin_permission.http_method_description')) + ->joinValues(false) + ->extractValue(), + amis()->NumberControl('order', __('admin.order')) + ->required() + ->labelRemark(__('admin.order_asc')) + ->displayMode('enhance') + ->min(0) + ->value(0), + amis()->ArrayControl('http_path', __('admin.admin_permission.http_path')) + ->items(amis()->TextControl()->options($this->getRoutes())->required()), + amis()->TreeSelectControl('menus', __('admin.menus')) + ->searchable() + ->multiple() + ->showIcon(false) + ->options(AdminMenuService::make()->getTree()) + ->labelField('title') + ->valueField('id') + ->autoCheckChildren(false) + ->joinValues(false) + ->extractValue(), + ]); + } + + public function detail(): Form + { + return $this->baseDetail()->body([]); + } + + private function getHttpMethods(): array + { + return collect(Admin::adminPermissionModel()::$httpMethods)->map(fn ($method) => [ + 'value' => $method, + 'label' => $method, + ])->toArray(); + } + + public function getRoutes(): array + { + $prefix = (string) Admin::config('admin.route.prefix'); + + $container = collect(); + + return collect(app('router')->getRoutes())->map(function ($route) use ($prefix, $container) { + if (! Str::startsWith($uri = $route->uri(), $prefix) && $prefix && $prefix !== '/') { + return null; + } + if (! Str::contains($uri, '{')) { + if ($prefix !== '/') { + $route = Str::replaceFirst($prefix, '', $uri.'*'); + } else { + $route = $uri.'*'; + } + + $route !== '*' && $container->push($route); + } + $path = preg_replace('/{.*}+/', '*', $uri); + $prefix !== '/' && $path = Str::replaceFirst($prefix, '', $path); + + return $path; + })->merge($container)->filter()->unique()->map(function ($method) { + return [ + 'value' => $method, + 'label' => $method, + ]; + })->values()->all(); + } + + public function autoGenerate() + { + $menus = Admin::adminMenuModel()::query()->get()->toArray(); + $slugMap = Admin::adminPermissionModel()::query()->get(['id', 'slug'])->keyBy('id')->toArray(); + $slugCache = []; + $permissions = []; + foreach ($menus as $menu) { + $_httpPath = + $menu['url_type'] == Admin::adminMenuModel()::TYPE_ROUTE ? $this->getHttpPath($menu['url']) : ''; + + $menuTitle = $menu['title']; + + // 避免名称重复 + if (in_array($menuTitle, data_get($permissions, '*.name', []))) { + $menuTitle = sprintf('%s(%s)', $menuTitle, $menu['id']); + } + + if ($_httpPath) { + $slug = Str::of(explode('?', $_httpPath)[0])->trim('/')->replace('/', '.')->replace('*', '')->value(); + } else { + $slug = Str::uuid(); + } + + if (in_array($slug, $slugCache)) { + $slug = $slug.'.'.$menu['id']; + } + $slugCache[] = $slug; + + $permissions[] = [ + 'id' => $menu['id'], + 'name' => $menuTitle, + 'slug' => data_get($slugMap, $menu['id'].'.slug') ?: $slug, + 'http_path' => json_encode($_httpPath ? [$_httpPath] : ''), + 'order' => $menu['order'], + 'parent_id' => $menu['parent_id'], + 'created_at' => $menu['created_at'], + 'updated_at' => $menu['updated_at'], + ]; + } + + Admin::adminPermissionModel()::query()->truncate(); + Admin::adminPermissionModel()::query()->insert($permissions); + + $permissionClass = Admin::adminPermissionModel(); + $pivotTable = (new $permissionClass)->menus()->getTable(); + + DB::table($pivotTable)->truncate(); + foreach ($permissions as $item) { + $query = DB::table($pivotTable); + $query->insert([ + 'permission_id' => $item['id'], + 'menu_id' => $item['id'], + ]); + + $_id = $item['id']; + while ($item['parent_id'] != 0) { + $query->clone()->insert([ + 'permission_id' => $_id, + 'menu_id' => $item['parent_id'], + ]); + + $item = Admin::adminMenuModel()::query()->find($item['parent_id']); + } + } + + return $this->response()->successMessage( + __('admin.successfully_message', ['attribute' => __('admin.admin_permission.auto_generate')]) + ); + } + + private function getHttpPath($uri) + { + $excepts = ['/', '', '-']; + if (in_array($uri, $excepts)) { + return ''; + } + + if (! str_starts_with($uri, '/')) { + $uri = '/'.$uri; + } + + return $uri.'*'; + } +} diff --git a/app/Admin/Controllers/System/AdminRoleController.php b/app/Admin/Controllers/System/AdminRoleController.php new file mode 100644 index 0000000..70f5ac6 --- /dev/null +++ b/app/Admin/Controllers/System/AdminRoleController.php @@ -0,0 +1,169 @@ +baseCRUD() + ->headerToolbar([ + $this->createTypeButton('drawer', 'lg') + ->visible(Admin::user()->can('admin.system.admin_roles.create')), + ...$this->baseHeaderToolBar(), + ]) + ->bulkActions([]) + ->filterTogglable(false) + ->itemCheckableOn('${id !== 1}') + ->columns([ + amis()->TableColumn()->label('ID')->name('id')->sortable(), + amis()->TableColumn()->label(__('admin.admin_role.name'))->name('name'), + amis()->TableColumn()->label(__('admin.admin_role.slug'))->name('slug')->type('tag'), + amis()->TableColumn() + ->label(__('admin.created_at')) + ->name('created_at') + ->type('datetime') + ->sortable(true), + amis()->TableColumn() + ->label(__('admin.updated_at')) + ->name('updated_at') + ->type('datetime') + ->sortable(true), + amis()->Operation()->label(__('admin.actions'))->buttons([ + $this->rowSetMenuButton() + ->visible(Admin::user()->can('admin.system.admin_roles.set_menus')) + ->visibleOn('${slug != "administrator"}'), + $this->rowSetPermissionButton() + ->visible(Admin::user()->can('admin.system.admin_roles.set_permissions')) + ->visibleOn('${slug != "administrator"}'), + $this->rowEditTypeButton('drawer', 'lg') + ->visible(Admin::user()->can('admin.system.admin_roles.update')) + ->visibleOn('${slug != "administrator"}'), + $this->rowDeleteButton() + ->visible(Admin::user()->can('admin.system.admin_roles.delete')) + ->visibleOn('${slug != "administrator"}'), + ]), + ]); + + return $this->baseList($crud)->css([ + '.tree-full' => [ + 'overflow' => 'hidden !important', + ], + '.cxd-TreeControl > .cxd-Tree' => [ + 'height' => '100% !important', + 'max-height' => '100% !important', + ], + ]); + } + + protected function rowSetPermissionButton(): DrawerAction + { + return amis()->DrawerAction()->label(__('admin.admin_role.set_permissions'))->icon('fa-solid fa-gear')->level('link')->drawer( + amis()->Drawer()->title(__('admin.admin_role.set_permissions'))->resizable()->closeOnOutside()->closeOnEsc()->body([ + amis() + ->Form() + ->api(admin_url('system/admin_roles/${id}/permissions')) + ->mode('normal') + ->data(['id' => '${id}']) + ->body( + amis()->TreeControl() + ->name('permissions') + ->label() + ->multiple() + ->options(AdminPermissionService::make()->getTree()) + ->searchable() + ->cascade() + ->value('${permission_ids}') + ->joinValues(false) + ->extractValue() + ->heightAuto() + ->showIcon(false) + ->initiallyOpen(false) + // ->size('full') + // ->className('h-full b-none') + // ->inputClassName('h-full tree-full') + ->labelField('name') + ->valueField('id'), + ), + ])->size('lg') + ); + } + + public function savePermissions() + { + $this->service->savePermissions(request('id'), request('permissions')); + + return $this->autoResponse('success', __('admin.save')); + } + + protected function rowSetMenuButton(): DrawerAction + { + return amis()->DrawerAction()->label(__('admin.admin_role.set_menus'))->icon('fa-solid fa-gear')->level('link')->drawer( + amis()->Drawer()->title(__('admin.admin_role.set_menus'))->resizable()->closeOnOutside()->closeOnEsc()->body([ + amis() + ->Form() + ->api(admin_url(admin_url('system/admin_roles/${id}/menus'))) + ->mode('normal') + ->data(['id' => '${id}']) + ->body( + amis()->TreeControl() + ->name('menus') + ->label() + ->multiple() + ->options(AdminMenuService::make()->getTree()) + ->searchable() + ->cascade() + ->value('${menu_ids}') + ->joinValues(false) + ->extractValue() + ->heightAuto() + ->showIcon(false) + ->initiallyOpen(false) + // ->size('full') + // ->className('h-full b-none') + // ->inputClassName('h-full tree-full') + ->labelField('title') + ->valueField('id') + ), + ])->size('lg') + ); + } + + public function saveMenus() + { + $this->service->saveMenus(request('id'), request('menus')); + + return $this->autoResponse('success', __('admin.save')); + } + + public function form(): Form + { + return $this->baseForm()->body([ + amis()->TextControl()->label(__('admin.admin_role.name'))->name('name')->required(), + amis()->TextControl() + ->label(__('admin.admin_role.slug')) + ->name('slug') + ->description(__('admin.admin_role.slug_description')) + ->required(), + ]); + } + + public function detail(): Form + { + return $this->baseDetail()->body([]); + } +} diff --git a/app/Admin/Controllers/System/AdminUserController.php b/app/Admin/Controllers/System/AdminUserController.php new file mode 100644 index 0000000..e27142f --- /dev/null +++ b/app/Admin/Controllers/System/AdminUserController.php @@ -0,0 +1,144 @@ +baseCRUD() + ->headerToolbar([ + $this->createTypeButton('drawer', 'lg')->visible(Admin::user()->can('admin.system.admin_users.create')), + ...$this->baseHeaderToolBar(), + ]) + ->bulkActions([]) + ->filter($this->baseFilter()->body( + amis()->TextControl('keyword', __('admin.keyword')) + ->size('md') + ->placeholder(__('admin.admin_user.search_username')) + )) + ->filterDefaultVisible() + ->quickSaveItemApi($this->getQuickEditItemPath(['lock'])) + ->itemCheckableOn('${id !== 1}') + ->columns([ + amis()->TableColumn('id', 'ID')->sortable(), + amis()->TableColumn('avatar', __('admin.admin_user.avatar'))->type('avatar')->src('${avatar}'), + amis()->TableColumn('username', __('admin.username')), + amis()->TableColumn('name', __('admin.admin_user.name')), + amis()->TableColumn('roles', __('admin.admin_user.roles'))->type('each')->items( + amis()->Tag()->label('${name}')->className('my-1') + ), + tap(amis()->TableColumn('lock', __('admin.admin_user.lock')), function (TableColumn $column) { + if (Admin::user()->can('admin.system.admin_users.update')) { + return $column->quickEdit( + amis()->SwitchControl()->saveImmediately()->mode('inline')->disabledOn('${id === 1}') + ); + } + + return $column->type('status')->source([ + 0 => ['label' => '未锁定', 'icon' => 'fa fa-times-circle', 'color' => '#30bf13'], + 1 => ['label' => '已锁定', 'icon' => 'fa fa-check-circle', 'color' => '#f70e47'], + ]); + }), + amis()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true), + $this->rowActions([ + $this->rowChangePasswordButton() + ->visible(Admin::user()->can('admin.system.admin_users.change_password')) + ->visibleOn('${id != 1}'), + $this->rowEditTypeButton('drawer', 'lg') + ->visible(Admin::user()->can('admin.system.admin_users.update')) + ->visibleOn('${id != 1}'), + // $this->rowDeleteButton() + // ->visible(Admin::user()->can('admin.system.admin_users.delete')) + // ->visibleOn('${id != 1}'), + ]), + ]); + + return $this->baseList($crud); + } + + public function form(): Form + { + return $this->baseForm()->body([ + amis()->ImageControl('avatar', __('admin.admin_user.avatar'))->receiver(admin_url('upload_image') . '?full-url=1'), + amis()->TextControl('name', __('admin.admin_user.name'))->required(), + amis()->TextControl('username', __('admin.username'))->required(), + amis()->TextControl('password', __('admin.password'))->type('input-password')->required()->validations(['minLength' => 6])->hiddenOn('${id > 0}'), + amis()->TextControl('confirm_password', __('admin.confirm_password'))->type('input-password')->required()->validations(['minLength' => 6])->hiddenOn('${id > 0}'), + amis()->SelectControl('roles', __('admin.admin_user.roles')) + ->searchable() + ->multiple() + ->labelField('name') + ->valueField('id') + ->joinValues(false) + ->extractValue() + ->options(AdminRoleService::make()->query()->get(['id', 'name'])), + amis()->SwitchControl()->name('lock')->label(__('admin.admin_user.lock'))->value(false), + ]); + } + + public function detail(): Form + { + return $this->baseDetail()->body([]); + } + + /** + * 修改密码 + */ + public function changePassword($id, Request $request) + { + $validator = Validator::make( + data: $request->input(), + rules: [ + 'password' => ['bail', 'required', 'confirmed', 'min:6'], + ], + attributes: [ + 'password' => '密码', + ], + ); + + if ($validator->fails()) { + admin_abort($validator->errors()->first()); + } + + $this->service->update($id, $request->only(['password'])); + + return $this->autoResponse('success', __('admin.save')); + } + + /** + * 修改密码按钮 + */ + protected function rowChangePasswordButton(): DrawerAction + { + return amis()->DrawerAction()->icon('fa-regular fa-lightbulb')->label(__('admin.admin_user.edit_password'))->level('link')->drawer( + amis()->Drawer()->title(__('admin.admin_user.edit_password'))->body([ + amis()->Form()->title('') + ->api([ + 'method' => 'POST', + 'url' => admin_url('system/admin_users/$id/change-password'), + ]) + ->body([ + amis()->TextControl('password', __('admin.password'))->type('input-password')->required()->validations(['minLength' => 6]), + amis()->TextControl('password_confirmation', __('admin.confirm_password'))->type('input-password')->required()->validations(['minLength' => 6]), + ]), + ])->size('lg') + ); + } +} diff --git a/app/Admin/Controllers/KeywordController.php b/app/Admin/Controllers/System/KeywordController.php similarity index 54% rename from app/Admin/Controllers/KeywordController.php rename to app/Admin/Controllers/System/KeywordController.php index 43152af..738404e 100644 --- a/app/Admin/Controllers/KeywordController.php +++ b/app/Admin/Controllers/System/KeywordController.php @@ -1,16 +1,20 @@ footerToolbar([]) //去掉分页-end ->headerToolbar([ - $this->createButton(true), + $this->createTypeButton('drawer', 'lg')->visible(Admin::user()->can('admin.system.keywords.create')), $this->exportAction(), - amis('reload')->align('right'), - amis('filter-toggler')->align('right'), + ...$this->baseHeaderToolBar(), ]) + ->bulkActions([]) ->filter($this->baseFilter()->body([ - amis()->TextControl('name', __('admin.keyword')) - ->size('md') - ->placeholder(__('admin.keywords.search_name')), - amis()->TextControl('parent_name', __('admin.keywords.parent_keyword')) - ->size('md') - ->placeholder(__('admin.keywords.search_name')) - ] - )) + amis()->TextControl('search', __('admin.keyword'))->placeholder(__('admin.keywords.search_name'))->size('md'), + ])) + ->filterDefaultVisible() ->columns([ // TableColumn::make()->name('id')->label('ID')->sortable(true), TableColumn::make()->name('name')->label('名称'), TableColumn::make()->name('key')->label('KEY')->copyable(true), TableColumn::make()->name('value')->label('值'), TableColumn::make()->name('sort')->label('排序'), - TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true), - amisMake()->Operation()->label(__('admin.actions'))->buttons([ - //按钮权限示例; - // amis()->action()->label('判断权限')->permission('keywords.custom', amis()->TextControl()->hidden()), - $this->rowEditButton(true), - $this->rowDeleteButton(), + TableColumn::make()->name('created_at')->label('创建时间')->type('datetime')->sortable(true), + amis()->Operation()->label(__('admin.actions'))->buttons([ + $this->rowEditTypeButton('drawer', 'lg') + ->visible(Admin::user()->can('admin.system.keywords.update')), + $this->rowDeleteButton()->visible(Admin::user()->can('admin.system.keywords.delete')), ]), ]); @@ -62,11 +60,12 @@ class KeywordController extends AdminController TextControl::make()->name('name')->label('名称')->required(true), TextControl::make()->name('key')->label('KEY')->required(true), TextControl::make()->name('value')->label('值'), - amisMake()->NumberControl()->name('sort')->value(0)->min()->label('排序'), + amis()->NumberControl()->name('sort')->value(0)->min()->label('排序'), ]); } - public function getTreeList(Request $request){ - return $this->service->getTree(); - } + public function getTreeList(Request $request) + { + return $this->service->getTree(); + } } diff --git a/app/Admin/Menu.php b/app/Admin/Menu.php new file mode 100644 index 0000000..23400db --- /dev/null +++ b/app/Admin/Menu.php @@ -0,0 +1,195 @@ +userMenus() + ->push(...array_map(fn ($item) => $this->formatItem($item), $this->menus)) + ->sortBy('order') + ->values() + ->toArray(); + + return array_merge($this->list2Menu($menus), $this->extra()); + } + + private function userMenus() + { + /** @var \App\Models\AdminUser */ + $user = Admin::user(); + + return $user->allMenus(); + } + + private function list2Menu($list, $parentId = 0, $parentName = ''): array + { + $data = []; + foreach ($list as $key => $item) { + if ($item['parent_id'] == $parentId) { + $idStr = "[{$item['id']}]"; + $_temp = [ + 'name' => $parentName ? $parentName.'-'.$idStr : $idStr, + 'path' => $item['url'], + 'component' => data_get($item, 'component') ?? 'amis', + 'is_home' => $item['is_home'], + 'is_full' => $item['is_full'] ?? 0, + 'is_link' => $item['url_type'] == Admin::adminMenuModel()::TYPE_LINK, + 'meta' => [ + 'title' => $item['title'], + 'icon' => $item['icon'] ?? '-', + 'hide' => $item['visible'] == 0, + 'order' => $item['order'], + ], + ]; + + $children = $this->list2Menu($list, (int) $item['id'], $_temp['name']); + + if (! empty($children)) { + $_temp['component'] = 'amis'; + $_temp['children'] = $children; + } + + $data[] = $_temp; + if (! in_array($_temp['path'], Admin::config('admin.route.without_extra_routes'))) { + array_push($data, ...$this->generateRoute($_temp)); + } + unset($list[$key]); + } + } + + return $data; + } + + private function generateRoute($item): array + { + $url = $item['path'] ?? ''; + $url = preg_replace('/\?.*/', '', $url); + + if (! $url || array_key_exists('children', $item)) { + return []; + } + + $menu = fn ($action, $path) => [ + 'name' => $item['name'].'-'.$action, + 'path' => $url.$path, + 'component' => 'amis', + 'meta' => [ + 'hide' => true, + 'icon' => Arr::get($item, 'meta.icon'), + 'title' => Arr::get($item, 'meta.title').' - '.__('admin.'.$action), + ], + ]; + + return [ + $menu('create', '/create'), + $menu('show', '/:id'), + $menu('edit', '/:id/edit'), + ]; + } + + public function add($menus) + { + $this->menus = array_merge($this->menus, $menus); + + return $this; + } + + private function formatItem($item) + { + return array_merge([ + 'title' => '', + 'url' => '', + 'url_type' => 1, + 'icon' => '', + 'parent_id' => 0, + 'id' => 999, + 'is_home' => 0, + 'visible' => 1, + 'order' => 99, + ], $item); + } + + /** + * 额外菜单 + * + * @return array|array[] + */ + private function extra() + { + $extraMenus = [ + [ + 'name' => 'user_setting', + 'path' => '/user_setting', + 'component' => 'amis', + 'meta' => [ + 'hide' => true, + 'title' => __('admin.user_setting'), + 'icon' => 'material-symbols:manage-accounts', + 'singleLayout' => 'basic', + ], + ], + ]; + + if (Admin::config('admin.show_development_tools')) { + $extraMenus = array_merge($extraMenus, $this->devToolMenus()); + } + + return $extraMenus; + } + + /** + * 开发者工具菜单 + * + * @return array[] + */ + private function devToolMenus() + { + return [ + [ + 'name' => 'dev_tools', + 'path' => '/dev_tools', + 'component' => 'amis', + 'meta' => [ + 'title' => __('admin.developer'), + 'icon' => 'fluent:window-dev-tools-20-regular', + ], + 'children' => [ + [ + 'name' => 'dev_tools_extensions', + 'path' => '/dev_tools/extensions', + 'component' => 'amis', + 'meta' => [ + 'title' => __('admin.extensions.menu'), + 'icon' => 'ion:extension-puzzle-outline', + ], + ], + [ + 'name' => 'dev_tools_code_generator', + 'path' => '/dev_tools/code_generator', + 'component' => 'amis', + 'meta' => [ + 'title' => __('admin.code_generator'), + 'icon' => 'ic:baseline-code', + ], + ], + [ + 'name' => 'dev_tools_editor', + 'path' => '/dev_tools/editor', + 'component' => 'editor', + 'meta' => [ + 'title' => __('admin.visual_editor'), + 'icon' => 'mdi:monitor-edit', + ], + ], + ], + ], + ]; + } +} diff --git a/app/Admin/Middleware/Authenticate.php b/app/Admin/Middleware/Authenticate.php new file mode 100644 index 0000000..8ac4d8f --- /dev/null +++ b/app/Admin/Middleware/Authenticate.php @@ -0,0 +1,22 @@ +authIntercept($request)) { + return Admin::response() + ->additional(['code' => Response::HTTP_UNAUTHORIZED]) + ->doNotDisplayToast() + ->fail(__('admin.please_login')); + } + + return $next($request); + } +} diff --git a/app/Admin/Middleware/CheckPermission.php b/app/Admin/Middleware/CheckPermission.php new file mode 100644 index 0000000..2ccf127 --- /dev/null +++ b/app/Admin/Middleware/CheckPermission.php @@ -0,0 +1,103 @@ + + */ + protected $exceptPaths = [ + '/api/*', + '/start_chunk_upload_file', + '/save_chunk_upload_file', + '/finish_chunk_upload_file', + '/upload_file', + '/upload_image', + '/upload_rich' + ]; + + /** + * @var array + */ + protected $exceptRoutes = [ + // + ]; + + /** + * Handle an incoming request. + * + * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + */ + public function handle(Request $request, Closure $next): Response + { + if ($this->inExceptArray($request) || $this->checkRoutePermission($request)) { + return $next($request); + } + + return Admin::response()->fail(__('admin.unauthorized'), ['route' => $request->route()->getName()]); + } + + protected function checkRoutePermission(Request $request): bool + { + if (is_null($user = Admin::user())) { + return false; + } + + $ability = $this->normalizeRouteAbility( + $original = $request->route()->getName() + ); + + return collect($ability) + ->when($ability !== $original, fn (Collection $collection) => $collection->push($original)) + ->contains(fn ($ability) => $user->can($ability)); + } + + protected function normalizeRouteAbility(string $ability) + { + foreach ($this->resourceAbilityMap() as $method => $map) { + if (str_ends_with($ability, ".{$method}")) { + return preg_replace("/(.*)\.{$method}$/", '${1}.'.$map, $ability); + } + } + + return $ability; + } + + protected function resourceAbilityMap(): array + { + return [ + 'index' => 'list', + 'show' => 'view', + 'store' => 'create', + 'edit' => 'update', + 'destroy' => 'delete', + ]; + } + + protected function inExceptArray(Request $request): bool + { + if (in_array($request->route()->getName(), $this->exceptRoutes)) { + return true; + } + + $permission = Admin::permission(); + + return $request->is( + collect($this->exceptPaths) + ->when(Admin::permission(), fn (Collection $collection, Permission $permission) => $collection->merge($permission->authExcept)->merge($permission->permissionExcept)) + ->map(function ($path) { + $path = trim((string) config('admin.route.prefix'), '/').'/'.trim((string) $path, '/'); + + return ltrim($path, '/'); + }) + ); + } +} diff --git a/app/Services/Admin/AdService.php b/app/Admin/Services/AdService.php similarity index 98% rename from app/Services/Admin/AdService.php rename to app/Admin/Services/AdService.php index 84fa949..4829c46 100644 --- a/app/Services/Admin/AdService.php +++ b/app/Admin/Services/AdService.php @@ -1,9 +1,9 @@ currentModel; + } + + public function sortColumn() + { + return 'id'; + } + + public function getTree() + { + $list = $this->query()->orderByDesc('sort')->get(); + $minNum = $list->min('parent_id'); + + return array2tree($list->toArray(), $minNum); + } + + public function list() + { + $query = $this->listQuery(); + if (request()->input('_all')) { + $list = (clone $query)->get(); + $items = $list->all(); + $total = $list->count(); + } else { + $list = (clone $query)->paginate(request()->input('perPage', 20)); + $items = $list->items(); + $total = $list->total(); + } + + return compact('items', 'total'); + } + + public function getModelFilter() + { + return $this->modelFilterName; + } + + public function listQuery() + { + $query = $this->query(); + + $this->addRelations($query); + + if ($filter = $this->getModelFilter()) { + $query->filter(request()->input(), $filter); + } + + if ($this->modelSortAble) { + $query->sort(); + } + + $this->sortable($query); + + return $query; + } + + public function store($data): bool + { + $data = $this->resloveData($data); + + $validate = $this->validate($data); + if ($validate !== true) { + $this->setError($validate); + + return false; + } + + $model = $this->modelName::create($data); + $this->currentModel = $model; + + return true; + } + + public function update($primaryKey, $data): bool + { + $model = $this->query()->whereKey($primaryKey)->firstOrFail(); + $data = $this->resloveData($data, $model); + $validate = $this->validate($data, $model); + if ($validate !== true) { + $this->setError($validate); + + return false; + } + $model->update($data); + $this->currentModel = $model; + return true; + } + + public function delete(string $ids): mixed + { + $this->preDelete(explode(',', $ids)); + + return parent::delete($ids); + } + + /** + * 处理表单数据 + * + * @param array $data + * @param Model $model 空 : 添加, 非空: 修改 + * @return array + */ + public function resloveData($data, $model = null) + { + return $data; + } + + /** + * 表单验证 + * + * @param array $data + * @param Model $model 空: 添加, 非空: 修改 + * @return mixed true: 验证通过, string: 错误提示 + */ + public function validate($data, $model = null) + { + // $createRules = [ + // 'key' => ['required', Rule::unique('keywords', 'key')], + // 'name' => ['required'], + // ]; + // $updateRules = [ + // 'key' => [Rule::unique('keywords', 'key')->ignore($model->id)] + // ]; + // $validator = Validator::make($data, $model ? $updateRules : $createRules, [ + // 'key.unique' => ':input 已经存在' + // ]); + // if ($validator->fails()) { + // return $validator->errors()->first(); + // } + return true; + } + + /** + * 删除的前置方法 + * + * @param array $ids 主键id + */ + public function preDelete(array $ids): void + { + } +} diff --git a/app/Admin/Services/System/AdminMenuService.php b/app/Admin/Services/System/AdminMenuService.php new file mode 100644 index 0000000..e85486d --- /dev/null +++ b/app/Admin/Services/System/AdminMenuService.php @@ -0,0 +1,95 @@ +modelName = Admin::adminMenuModel(); + } + + public function getTree(): array + { + $list = $this->query()->orderBy('order')->get()->toArray(); + + return array2tree($list); + } + + public function parentIsChild($id, $parent_id): bool + { + $parent = $this->query()->find($parent_id); + + do { + if ($parent->parent_id == $id) { + return true; + } + // 如果没有parent 则为顶级菜单 退出循环 + $parent = $parent->parent; + } while ($parent); + + return false; + } + + public function update($primaryKey, $data): bool + { + $columns = $this->getTableColumns(); + + $parent_id = Arr::get($data, 'parent_id'); + if ($parent_id != 0) { + amis_abort_if($this->parentIsChild($primaryKey, $parent_id), __('admin.admin_menu.parent_id_not_allow')); + } + + $model = $this->query()->whereKey($primaryKey)->first(); + + return $this->saveData($data, $columns, $model); + } + + public function store($data): bool + { + $columns = $this->getTableColumns(); + + $model = $this->getModel(); + + return $this->saveData($data, $columns, $model); + } + + public function changeHomePage($excludeId = 0) + { + $this->query()->when($excludeId, fn ($query) => $query->where('id', '<>', $excludeId))->update(['is_home' => 0]); + } + + public function list() + { + return ['items' => $this->getTree()]; + } + + protected function saveData($data, array $columns, AdminMenu $model): bool + { + foreach ($data as $k => $v) { + if (! in_array($k, $columns)) { + continue; + } + + $v = $k == 'parent_id' ? intval($v) : $v; + + $model->setAttribute($k, $v); + + if ($k == 'is_home' && $v == 1) { + $this->changeHomePage($model->getKey()); + } + } + + return $model->save(); + } +} diff --git a/app/Admin/Services/System/AdminPermissionService.php b/app/Admin/Services/System/AdminPermissionService.php new file mode 100644 index 0000000..1d16223 --- /dev/null +++ b/app/Admin/Services/System/AdminPermissionService.php @@ -0,0 +1,117 @@ +modelName = Admin::adminPermissionModel(); + } + + public function getTree(): array + { + $list = $this->query()->orderBy('order')->get()->toArray(); + + return array2tree($list); + } + + public function parentIsChild($id, $parent_id): bool + { + $parent = $this->query()->find($parent_id); + + do { + if ($parent->parent_id == $id) { + return true; + } + // 如果没有parent 则为顶级 退出循环 + $parent = $parent->parent; + } while ($parent); + + return false; + } + + public function getEditData($id): Model|\Illuminate\Database\Eloquent\Collection|Builder|array|null + { + $permission = parent::getEditData($id); + + $permission->load(['menus']); + + return $permission; + } + + public function store($data): bool + { + $this->checkRepeated($data); + + $columns = $this->getTableColumns(); + + $model = $this->getModel(); + + return $this->saveData($data, $columns, $model); + } + + public function update($primaryKey, $data): bool + { + $this->checkRepeated($data, $primaryKey); + + $columns = $this->getTableColumns(); + + $parent_id = Arr::get($data, 'parent_id'); + if ($parent_id != 0) { + amis_abort_if($this->parentIsChild($primaryKey, $parent_id), __('admin.admin_permission.parent_id_not_allow')); + } + + $model = $this->query()->whereKey($primaryKey)->first(); + + return $this->saveData($data, $columns, $model); + } + + public function checkRepeated($data, $id = 0) + { + $query = $this->query()->when($id, fn ($query) => $query->where('id', '<>', $id)); + + amis_abort_if($query->clone()->where('name', $data['name']) + ->exists(), __('admin.admin_permission.name_already_exists')); + + amis_abort_if($query->clone()->where('slug', $data['slug']) + ->exists(), __('admin.admin_permission.slug_already_exists')); + } + + public function list() + { + return ['items' => $this->getTree()]; + } + + protected function saveData($data, array $columns, AdminPermission $model): bool + { + $menus = Arr::pull($data, 'menus'); + + foreach ($data as $k => $v) { + if (! in_array($k, $columns)) { + continue; + } + + $model->setAttribute($k, $v); + } + + if ($model->save()) { + $model->menus()->sync(Arr::has($menus, '0.id') ? Arr::pluck($menus, 'id') : $menus); + + return true; + } + + return false; + } +} diff --git a/app/Admin/Services/System/AdminRoleService.php b/app/Admin/Services/System/AdminRoleService.php new file mode 100644 index 0000000..5c0e7e4 --- /dev/null +++ b/app/Admin/Services/System/AdminRoleService.php @@ -0,0 +1,100 @@ +modelName = Admin::adminRoleModel(); + } + + public function list() + { + $query = $this->listQuery(); + + /** @var \Illuminate\Pagination\LengthAwarePaginator */ + $list = $query->with(['menus', 'permissions'])->paginate(request()->input('perPage', 20)); + $list->through(function ($item) { + $instance = $item->withoutRelations(); + $instance->setAttribute('menu_ids', $item->menus->pluck('id')); + $instance->setAttribute('permission_ids', $item->permissions->pluck('id')); + + return $instance; + }); + $items = $list->items(); + $total = $list->total(); + + return compact('items', 'total'); + } + + public function store($data): bool + { + $this->checkRepeated($data); + + $columns = $this->getTableColumns(); + + $model = $this->getModel(); + + foreach ($data as $k => $v) { + if (! in_array($k, $columns)) { + continue; + } + + $model->setAttribute($k, $v); + } + + return $model->save(); + } + + public function update($primaryKey, $data): bool + { + $this->checkRepeated($data, $primaryKey); + + $columns = $this->getTableColumns(); + + $model = $this->query()->whereKey($primaryKey)->first(); + + foreach ($data as $k => $v) { + if (! in_array($k, $columns)) { + continue; + } + + $model->setAttribute($k, $v); + } + + return $model->save(); + } + + public function checkRepeated($data, $id = 0) + { + $query = $this->query()->when($id, fn ($query) => $query->where('id', '<>', $id)); + + amis_abort_if($query->clone() + ->where('name', $data['name']) + ->exists(), __('admin.admin_role.name_already_exists')); + + amis_abort_if($query->clone() + ->where('slug', $data['slug']) + ->exists(), __('admin.admin_role.slug_already_exists')); + } + + public function savePermissions($primaryKey, $permissions): void + { + $model = $this->query()->whereKey($primaryKey)->first(); + + $model->permissions()->detach(); + $model->permissions()->attach($permissions); + } + + public function saveMenus($primaryKey, $menus): void + { + $model = $this->query()->whereKey($primaryKey)->first(); + + $model->menus()->detach(); + $model->menus()->attach($menus); + } +} diff --git a/app/Admin/Services/System/AdminUserService.php b/app/Admin/Services/System/AdminUserService.php new file mode 100644 index 0000000..115bf13 --- /dev/null +++ b/app/Admin/Services/System/AdminUserService.php @@ -0,0 +1,161 @@ +modelName = Admin::adminUserModel(); + } + + public function getEditData($id): Model|\Illuminate\Database\Eloquent\Collection|Builder|array|null + { + $adminUser = parent::getEditData($id)->makeHidden('password'); + + $adminUser->load('roles'); + + return $adminUser; + } + + public function store($data): bool + { + if ($this->checkUsernameUnique($data['username'])) { + admin_abort(__('admin.admin_user.username_already_exists')); + } + + if (! data_get($data, 'password')) { + admin_abort(__('admin.required', ['attribute' => __('admin.password')])); + } + + if (array_key_exists('confirm_password', $data)) { + if ($data['password'] !== $data['confirm_password']) { + admin_abort(__('admin.admin_user.password_confirmation')); + } + unset($data['confirm_password']); + } + + if (Hash::needsRehash($data['password'])) { + $data['password'] = Hash::make($data['password']); + } + + $model = $this->getModel(); + $user = $model::create(Arr::except($data, ['roles'])); + + if (isset($data['roles'])) { + $user->roles()->attach($data['roles']); + } + + return true; + } + + public function update($primaryKey, $data): bool + { + if (isset($data['username']) && $this->checkUsernameUnique($data['username'], $primaryKey)) { + admin_abort(__('admin.admin_user.username_already_exists')); + } + + if (isset($data['password']) && Hash::needsRehash($data['password'])) { + $data['password'] = Hash::make($data['password']); + } + + $user = $this->query()->whereKey($primaryKey)->firstOrFail(); + + foreach (['username', 'password', 'name', 'avatar', 'lock'] as $attribute) { + if (array_key_exists($attribute, $data)) { + $user->{$attribute} = $data[$attribute]; + } + } + + if ($user->isDirty('lock') && $user->lock != 1) { + $user->error_num = 0; + $user->last_error_at = null; + } + + $user->save(); + + if (isset($data['roles'])) { + $roles = Arr::pull($data, 'roles'); + $user->roles()->detach(); + $user->roles()->attach(Arr::has($roles, '0.id') ? Arr::pluck($roles, 'id') : $roles); + } + + return true; + } + + public function checkUsernameUnique($username, $id = 0): bool + { + return $this->query() + ->where('username', $username) + ->when($id, fn ($query) => $query->where('id', '<>', $id)) + ->exists(); + } + + public function updateUserSetting($primaryKey, $data): bool + { + $this->passwordHandler($data, $primaryKey); + + return parent::update($primaryKey, $data); + } + + public function passwordHandler(&$data, $id = null) + { + $password = Arr::get($data, 'password'); + + if ($password) { + if ($password !== Arr::get($data, 'confirm_password')) { + admin_abort(__('admin.admin_user.password_confirmation')); + } + + if (strlen($password) < 6) { + admin_abort('密码长度至少6个字符'); + } + + if ($id) { + if (! Arr::get($data, 'old_password')) { + admin_abort(__('admin.admin_user.old_password_required')); + } + + $oldPassword = $this->query()->where('id', $id)->value('password'); + + if (! Hash::check($data['old_password'], $oldPassword)) { + admin_abort(__('admin.admin_user.old_password_error')); + } + } + + $data['password'] = bcrypt($password); + + unset($data['confirm_password']); + unset($data['old_password']); + } + } + + public function list() + { + $keyword = request()->keyword; + + $query = $this + ->query() + ->with('roles') + ->select(['id', 'name', 'username', 'avatar', 'created_at', 'lock']) + ->when($keyword, function ($query) use ($keyword) { + $query->where('username', 'like', "%{$keyword}%")->orWhere('name', 'like', "%{$keyword}%"); + }); + + $this->sortable($query); + + $items = (clone $query)->paginate(request()->input('perPage', 20))->items(); + $total = (clone $query)->count(); + + return compact('items', 'total'); + } +} diff --git a/app/Services/Admin/KeywordService.php b/app/Admin/Services/System/KeywordService.php similarity index 82% rename from app/Services/Admin/KeywordService.php rename to app/Admin/Services/System/KeywordService.php index 7e28bc1..e9c9a83 100644 --- a/app/Services/Admin/KeywordService.php +++ b/app/Admin/Services/System/KeywordService.php @@ -1,10 +1,11 @@ query()->filter(request()->all(), $this->modelFilterName)->orderByDesc('sort')->get(); + $list = $this->query()->sort()->filter(request()->all(), $this->modelFilterName)->get(); $minNum = $list->min('parent_id'); - return !$list->isEmpty() ? array2tree($list->toArray(), $minNum) :[]; + + return ! $list->isEmpty() ? array2tree($list->toArray(), $minNum) : []; } public function parentIsChild($id, $pid): bool @@ -53,7 +56,7 @@ class KeywordService extends BaseService $model = $this->getModel(); foreach ($data as $k => $v) { - if (!in_array($k, $columns)) { + if (! in_array($k, $columns)) { continue; } @@ -75,6 +78,7 @@ class KeywordService extends BaseService if ($pid != 0) { if ($this->parentIsChild($primaryKey, $pid)) { $this->setError('父级不允许设置为当前子权限'); + return false; } } @@ -82,7 +86,7 @@ class KeywordService extends BaseService $model = $this->query()->whereKey($primaryKey)->first(); foreach ($data as $k => $v) { - if (!in_array($k, $columns)) { + if (! in_array($k, $columns)) { continue; } @@ -94,10 +98,11 @@ class KeywordService extends BaseService public function hasRepeated($data, $id = 0): bool { - $query = $this->query()->when($id, fn($query) => $query->where('id', '<>', $id)); + $query = $this->query()->when($id, fn ($query) => $query->where('id', '<>', $id)); if ((clone $query)->where('key', $data['key'])->exists()) { $this->setError('KEY重复'); + return true; } @@ -107,7 +112,7 @@ class KeywordService extends BaseService public function delete(string $ids): mixed { $ids = explode(',', $ids); - if(count($ids) == 1){ + if (count($ids) == 1) { $this->query()->where('path', 'like', '%-'.$ids[0].'-%')->delete(); } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index b080bf1..39d69ee 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -19,15 +19,8 @@ Route::group([ $router->post('login', [App\Admin\Controllers\AuthController::class, 'login']); - $router->resource('system/admin_users', App\Admin\Controllers\AdminUserController::class); - $router->post('quick-edit/admin_users/{admin_user}',[\App\Admin\Controllers\AdminUserController::class, 'update']); - $router->get('system/admin_roles', [App\Admin\Controllers\AdminRoleController::class, 'index']); - $router->resource('system/settings', \App\Admin\Controllers\SettingController::class); - - $router->resource('system/keywords', \App\Admin\Controllers\KeywordController::class); - $router->resource('articles', \App\Admin\Controllers\ArticleController::class); $router->resource('ads', \App\Admin\Controllers\AdController::class); @@ -40,4 +33,26 @@ Route::group([ $router->post('start_chunk_upload_file', [\App\Admin\Controllers\IndexController::class, 'startChunk']); $router->post('save_chunk_upload_file', [\App\Admin\Controllers\IndexController::class, 'saveChunk']); $router->post('finish_chunk_upload_file', [\App\Admin\Controllers\IndexController::class, 'finishChunk']); + + + $router->group([ + 'prefix' => 'system', + 'as' => 'system.', + ], function (Router $router) { + // 账号管理 + $router->resource('admin_users', App\Admin\Controllers\System\AdminUserController::class); + $router->post('/admin_users/{admin_user}/change-password', [App\Admin\Controllers\System\AdminUserController::class, 'changePassword'])->name('admin_users.change_password'); + // 角色管理 + $router->resource('admin_roles', App\Admin\Controllers\System\AdminRoleController::class); + $router->post('admin_roles/{admin_role}/menus', [App\Admin\Controllers\System\AdminRoleController::class, 'saveMenus'])->name('admin_roles.set_menus'); + $router->post('admin_roles/{admin_role}/permissions', [App\Admin\Controllers\System\AdminRoleController::class, 'savePermissions'])->name('admin_roles.set_permissions'); + // 权限管理 + $router->resource('admin_permissions', App\Admin\Controllers\System\AdminPermissionController::class); + // 菜单管理 + $router->resource('admin_menus', App\Admin\Controllers\System\AdminMenuController::class); + // 系统设置 + $router->resource('settings', \App\Admin\Controllers\SettingController::class); + // 数据字典 + $router->resource('keywords', App\Admin\Controllers\System\KeywordController::class); + }); }); diff --git a/app/Models/Filters/AdFilter.php b/app/ModelFilters/AdFilter.php similarity index 98% rename from app/Models/Filters/AdFilter.php rename to app/ModelFilters/AdFilter.php index 5b1e5f6..8a5d398 100644 --- a/app/Models/Filters/AdFilter.php +++ b/app/ModelFilters/AdFilter.php @@ -1,6 +1,6 @@ where('name', 'like', '%'.$name.'%')->orWhere('key', 'like', '%'.$name.'%'); + } + + public function parentKey($key) + { + return $this->where('parent_key', $key); + } +} diff --git a/app/Models/AdminRole.php b/app/Models/AdminRole.php new file mode 100644 index 0000000..0a1ffcb --- /dev/null +++ b/app/Models/AdminRole.php @@ -0,0 +1,25 @@ +menus()->detach(); + $model->permissions()->detach(); + }); + } + + public function menus(): BelongsToMany + { + return $this->belongsToMany(Admin::adminMenuModel(), 'admin_role_menus', 'role_id', 'menu_id'); + } +} diff --git a/app/Models/AdminUser.php b/app/Models/AdminUser.php new file mode 100644 index 0000000..c5d8ed0 --- /dev/null +++ b/app/Models/AdminUser.php @@ -0,0 +1,121 @@ +belongsToMany(Admin::adminRoleModel(), 'admin_role_users', 'user_id', 'role_id')->withTimestamps(); + } + + /** + * 获取此用户的全部能力 + */ + public function allAbilities(): Collection + { + if ($this->allAbilities) { + return $this->allAbilities; + } + + $model = Admin::adminPermissionModel(); + $allPermissions = $model::all(['id', 'parent_id', 'slug'])->keyBy('id'); + + /** @var \Illuminate\Database\Eloquent\Collection 此用户的所有角色的权限集合 */ + $rolePermissions = $this->roles + ->pluck('permissions') + ->flatten() + ->keyBy('id'); + + $abilities = $rolePermissions->map(fn ($permission) => $permission->slug); + + foreach ($rolePermissions as $rolePermission) { + if (is_null($rolePermission->parent_id) || $abilities->has($rolePermission->parent_id)) { + continue; + } + + $parent = $allPermissions->get($rolePermission->parent_id); + + while ($parent) { + $abilities->put($parent->id, $parent->slug); + + if (is_null($parent->parent_id)) { + break; + } + + $parent = $allPermissions->get($parent->parent_id); + } + } + + unset($allPermissions, $rolePermissions); + + return $this->allAbilities = $abilities; + } + + public function can($abilities, $arguments = []): bool + { + if (empty($abilities)) { + return true; + } + + if ($this->isAdministrator()) { + return true; + } + + return collect($abilities)->every( + fn ($ability) => $this->allAbilities()->contains($ability) + ); + } + + public function allMenus(): Collection + { + $model = Admin::adminMenuModel(); + $allMenus = $model::all()->keyBy($this->getKeyName()); + + if ($this->isAdministrator()) { + return $allMenus; + } + + /** @var \Illuminate\Database\Eloquent\Collection */ + $roleMenus = $this->roles + ->pluck('menus') + ->flatten() + ->keyBy($this->getKeyName()); + + $allRoleMenus = $roleMenus->collect(); + + foreach ($roleMenus as $roleMenu) { + if (is_null($roleMenu->parent_id) || $allRoleMenus->has($roleMenu->parent_id)) { + continue; + } + + $parent = $allMenus->get($roleMenu->parent_id); + + while ($parent) { + $allRoleMenus->put($parent->id, $parent); + + if (is_null($parent->parent_id)) { + break; + } + + $parent = $allMenus->get($parent->parent_id); + } + } + + unset($allMenus, $roleMenus); + + return $allRoleMenus; + } +} diff --git a/app/Models/Filters/KeywordFilter.php b/app/Models/Filters/KeywordFilter.php deleted file mode 100644 index caf1040..0000000 --- a/app/Models/Filters/KeywordFilter.php +++ /dev/null @@ -1,31 +0,0 @@ -where('name','like', '%'.$name.'%') - ->orWhere('key','like', '%'.$name.'%'); - } - - public function parentName($parent_name) - { - if(request('has_owner', 1)){ - $this->where(function($q) use ($parent_name){ - $q->where('name','like', '%'.$parent_name.'%') - ->orWhere('key','like', '%'.$parent_name.'%'); - }); - } - return $this->orWhere('path','like', '%-'. - Keyword::where('name','like', '%'.$parent_name.'%')->orWhere('key','like', '%'.$parent_name.'%')->value('id') - . '-%' ?? ''); - } -} diff --git a/app/Models/Keyword.php b/app/Models/Keyword.php index d065b36..7bb1025 100644 --- a/app/Models/Keyword.php +++ b/app/Models/Keyword.php @@ -2,19 +2,21 @@ namespace App\Models; +use App\Admin\Components; +use App\ModelFilters\KeywordFilter; +use EloquentFilter\Filterable; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use EloquentFilter\Filterable; -use App\Admin\Components; use Illuminate\Support\Str; class Keyword extends Model { - use HasFactory; use Filterable; + use HasFactory; protected $fillable = ['name', 'key', 'value', 'parent_id', 'parent_key', 'path', 'sort', 'lv']; - + protected function serializeDate(\DateTimeInterface $date) { return $date->format('Y-m-d H:i:s'); @@ -23,26 +25,27 @@ class Keyword extends Model protected static function boot() { parent::boot(); + // 监听 Keyword 的创建事件,用于初始化 path 和 lv 字段值 - static::saving(function ($keyword) { - // 如果创建的是一个根类目 - if (! $keyword->parent_id) { - // 将层级设为 1 - $keyword->lv = 1; - // 将 path 设为 - - $keyword->path = '-'; - if(empty($keyword->key)){ - $keyword->key = Str::quickRandom($length = 16); + static::saving(function (Keyword $keyword) { + if (is_null($parent = $keyword->parent)) { + $keyword->forceFill([ + 'path' => '-', + 'lv' => 1, + ]); + + if ((string) $keyword->key === '') { + $keyword->key = Str::random(16); } } else { - // 将层级设为父类目的层级 + 1 - $keyword->lv = $keyword->parent->lv + 1; - $keyword->parent_key = $keyword->parent->key; - // 将 path 值设为父类目的 path 追加父类目 ID 以及最后跟上一个 - 分隔符 - $keyword->path = $keyword->parent->path.$keyword->parent_id.'-'; - //当前key是否为空 - if(empty($keyword->key)){ - $keyword->key = $keyword->parent_key . '_' . (self::where('parent_key', $keyword->parent_key)->count() + 1); + $keyword->forceFill([ + 'parent_key' => $parent->lv > 1 ? $parent->parent_key : $parent->key, + 'path' => $parent->full_path, + 'lv' => $parent->lv + 1, + ]); + + if ((string) $keyword->key === '') { + $keyword->key = $parent->key.'_'.($parent->children()->count() + 1); } } }); @@ -58,19 +61,32 @@ class Keyword extends Model return $this->hasMany(static::class, 'parent_id'); } - public function scopeAllChildrenOfKey($q, $parentKey) + public function scopeSort($q) { - $q->where('path','like', '%-'. - static::where('key', $parentKey)->value('id') - . '-%' ?? ''); + return $q->orderBy('sort', 'asc'); } - public static function tagsMap(String $key) + public function scopeAllChildrenOfKey($q, $parentKey) + { + $q->where('path', 'like', '%-'. + static::where('key', $parentKey)->value('id') + .'-%' ?? ''); + } + + public static function tagsMap(string $key) { $mapArr = []; - self::query()->where('parent_key', $key)->get()->map(function($item) use (&$mapArr){ + self::query()->where('parent_key', $key)->get()->map(function ($item) use (&$mapArr) { $mapArr[$item->id] = Components::make()->keywordsTag($item->name, $item->value); }); + return $mapArr; } + + protected function fullPath(): Attribute + { + return Attribute::make( + get: fn (mixed $value, array $attributes) => $attributes['path'].$attributes['id'].'-', + ); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f782b96..4d70882 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,10 @@ namespace App\Providers; +use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\Validator; class AppServiceProvider extends ServiceProvider { @@ -11,8 +14,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // - + $this->app->singleton('admin.menu', \App\Admin\Menu::class); } /** @@ -20,6 +22,24 @@ class AppServiceProvider extends ServiceProvider */ public function boot() { - \Schema::defaultStringLength(191); + JsonResource::withoutWrapping(); + + $this->definePolymorphicTypes(); + + Validator::extend('phone', function ($attribute, $value, $parameters, $validator) { + return preg_match('/^1[\d]{10}$/', $value); + }); + } + + /** + * 自定义多态类型 + */ + protected function definePolymorphicTypes(): void + { + Relation::enforceMorphMap( + collect([ + \App\Models\AdminUser::class, + ])->mapWithKeys(fn ($model) => [(new $model)->getTable() => $model])->all() + ); } } diff --git a/app/Services/Admin/BaseService.php b/app/Services/Admin/BaseService.php deleted file mode 100644 index 4fa0ecd..0000000 --- a/app/Services/Admin/BaseService.php +++ /dev/null @@ -1,58 +0,0 @@ -query()->orderByDesc('sort')->get(); - $minNum = $list->min('parent_id'); - return array2tree($list->toArray(), $minNum); - } - - public function getModelFilter() - { - return $this->modelFilterName; - } - - public function listQuery() - { - $model = $this->getModel(); - $filter = $this->getModelFilter(); - - $query = $this->query(); - if($this->withRelationships){ - $query->with($this->withRelationships); - } - - if ($filter) { - $query->filter(request()->input(), $filter); - } - - if($this->modelSortAble){ - $query->sort(); - } - - $this->sortable($query); - - return $query; - } - - public function getDetail($id) - { - return $this->query()->with($this->withRelationships)->find($id); - } -} diff --git a/config/admin.php b/config/admin.php index 12b20d7..e26b0b1 100644 --- a/config/admin.php +++ b/config/admin.php @@ -2,26 +2,32 @@ return [ // 应用名称 - 'name' => 'Owl Admin', + 'name' => 'Owl Admin', // 应用 logo - 'logo' => '/admin-assets/logo.png', + 'logo' => '/admin-assets/logo.png', // 默认头像 'default_avatar' => '/admin-assets/default-avatar.png', // 应用安装目录 - 'directory' => app_path('Admin'), + 'directory' => app_path('Admin'), // 引导文件 - 'bootstrap' => app_path('Admin/bootstrap.php'), + 'bootstrap' => app_path('Admin/bootstrap.php'), // 应用路由 - 'route' => [ - 'prefix' => 'admin-api', - 'domain' => null, - 'namespace' => 'App\\Admin\\Controllers', - 'middleware' => ['admin'], + 'route' => [ + 'prefix' => 'admin-api', + 'domain' => null, + 'namespace' => 'App\\Admin\\Controllers', + 'middleware' => [ + \App\Admin\Middleware\Authenticate::class, + 'admin.bootstrap', + \App\Admin\Middleware\CheckPermission::class, + 'sanctum', + 'substitute', + ], // 不包含额外路由, 配置后, 不会追加新增/详情/编辑页面路由 'without_extra_routes' => [ '/dashboard', @@ -30,70 +36,69 @@ return [ 'auth' => [ // 是否开启验证码 - 'login_captcha' => env('ADMIN_LOGIN_CAPTCHA', true), + 'login_captcha' => env('ADMIN_LOGIN_CAPTCHA', true), // 是否开启认证 - 'enable' => true, + 'enable' => true, // 是否开启鉴权 - 'permission' => true, + 'permission' => true, // token 有效期 (分钟), 为空则不会过期 'token_expiration' => null, - 'guard' => 'admin', - 'guards' => [ + 'guard' => 'admin', + 'guards' => [ 'admin' => [ - 'driver' => 'sanctum', + 'driver' => 'sanctum', 'provider' => 'admin', ], ], - 'providers' => [ + 'providers' => [ 'admin' => [ 'driver' => 'eloquent', - 'model' => \Slowlyo\OwlAdmin\Models\AdminUser::class, + 'model' => \App\Models\AdminUser::class, ], ], - 'except' => [ - + 'except' => [ ], ], 'upload' => [ - 'disk' => env("FILESYSTEM_DISK", 'public'), + 'disk' => env('FILESYSTEM_DISK', 'public'), // 文件上传目录 'directory' => [ 'image' => 'images', - 'file' => 'files', - 'rich' => 'rich', + 'file' => 'files', + 'rich' => 'rich', ], // 临时目录 'tem_directory' => [ 'image' => 'temporary/images', - 'file' => 'temporary/file', - 'rich' => 'temporary/rich', - ] + 'file' => 'temporary/file', + 'rich' => 'temporary/rich', + ], ], - 'https' => env('ADMIN_HTTPS', false), + 'https' => env('ADMIN_HTTPS', false), // 是否显示 [开发者工具] - 'show_development_tools' => env('ADMIN_SHOW_DEVELOPMENT_TOOLS', true), + 'show_development_tools' => env('ADMIN_SHOW_DEVELOPMENT_TOOLS', false), // 是否显示 [权限] 功能中的自动生成按钮 'show_auto_generate_permission_button' => env('ADMIN_SHOW_AUTO_GENERATE_PERMISSION_BUTTON', true), // 扩展 - 'extension' => [ + 'extension' => [ 'dir' => base_path('extensions'), ], 'layout' => [ // 浏览器标题, 功能名称使用 %title% 代替 - 'title' => '%title% | OwlAdmin', - 'header' => [ + 'title' => '%title% | 门店助手-管理后台', + 'header' => [ // 是否显示 [刷新] 按钮 - 'refresh' => true, + 'refresh' => true, // 是否显示 [暗色模式] 按钮 - 'dark' => true, + 'dark' => true, // 是否显示 [全屏] 按钮 - 'full_screen' => true, + 'full_screen' => true, // 是否显示 [主题配置] 按钮 'theme_config' => true, ], @@ -108,17 +113,17 @@ return [ */ 'keep_alive_exclude' => [], // 底部信息 - 'footer' => 'Owl Admin', + 'footer' => '', ], 'database' => [ - 'connection' => env('ADMIN_DB_CONNECTION') ?? env('DB_CONNECTION', 'mysql'), + 'connection' => env('DB_CONNECTION', 'mysql'), ], 'models' => [ - 'admin_user' => \Slowlyo\OwlAdmin\Models\AdminUser::class, - 'admin_role' => \Slowlyo\OwlAdmin\Models\AdminRole::class, - 'admin_menu' => \Slowlyo\OwlAdmin\Models\AdminMenu::class, + 'admin_user' => \App\Models\AdminUser::class, + 'admin_role' => \App\Models\AdminRole::class, + 'admin_menu' => \Slowlyo\OwlAdmin\Models\AdminMenu::class, 'admin_permission' => \Slowlyo\OwlAdmin\Models\AdminPermission::class, ], diff --git a/lang/zh_CN/admin.php b/lang/zh_CN/admin.php index a888d66..c34c2f9 100644 --- a/lang/zh_CN/admin.php +++ b/lang/zh_CN/admin.php @@ -1,231 +1,228 @@ '记住我', - 'login' => '登 录', - 'logout' => '退出登录', - 'username' => '用户名', - 'password' => '密码', - 'old_password' => '旧密码', + 'remember_me' => '记住我', + 'login' => '登 录', + 'logout' => '退出登录', + 'username' => '用户名', + 'password' => '密码', + 'old_password' => '旧密码', 'confirm_password' => '确认密码', - 'captcha' => '验证码', - 'captcha_error' => '验证码有误', - 'required' => '请填写:attribute', + 'captcha' => '验证码', + 'captcha_error' => '验证码有误', + 'required' => '请填写:attribute', 'login_successful' => '登录成功', - 'login_failed' => '用户名或密码错误', - 'user_setting' => '个人设置', - 'created_at' => '创建时间', - 'updated_at' => '更新时间', - 'deleted_at' => '删除时间', - 'actions' => '操作', - 'create' => '新增', - 'edit' => '编辑', - 'show' => '查看', - 'delete' => '删除', - 'copy' => '复制', - 'confirm_delete' => '确认删除选中项?', - 'back' => '返回', - 'reset' => '重置', - 'search' => '搜索', - 'list' => '列表', - 'add' => '新增', - 'save' => '保存', - 'detail' => '详情', + 'login_failed' => '用户名或密码错误', + 'user_setting' => '个人设置', + 'created_at' => '创建时间', + 'updated_at' => '更新时间', + 'deleted_at' => '删除时间', + 'actions' => '操作', + 'create' => '新增', + 'edit' => '编辑', + 'show' => '查看', + 'delete' => '删除', + 'copy' => '复制', + 'confirm_delete' => '确认删除选中项?', + 'confirm' => '是否确定?', + 'back' => '返回', + 'reset' => '重置', + 'search' => '搜索', + 'list' => '列表', + 'add' => '新增', + 'save' => '保存', + 'detail' => '详情', - 'developer' => '开发', - 'code_generator' => '代码生成器', - 'visual_editor' => '可视化编辑器', - 'terminal' => '终端', - 'administrator' => '管理员', - 'soft_delete' => '软删除', - 'keyword' => '关键字', - 'unknown_error' => '未知错误', - 'upload_file_error' => '上传文件错误', - 'parent' => '父级', - 'order' => '排序', - 'order_desc' => '降序排序', - 'order_asc' => '升序排序', - 'menus' => '菜单', - 'successfully' => '成功', - 'failed' => '失败', - 'successfully_message' => ':attribute成功', - 'failed_message' => ':attribute失败', - 'action_success' => '操作成功', - 'action_failed' => '操作失败', - 'save_success' => '保存成功', - 'save_failed' => '保存失败', - 'yes' => '是', - 'no' => '否', + 'developer' => '开发', + 'code_generator' => '代码生成器', + 'visual_editor' => '可视化编辑器', + 'terminal' => '终端', + 'administrator' => '管理员', + 'soft_delete' => '软删除', + 'keyword' => '关键字', + 'unknown_error' => '未知错误', + 'upload_file_error' => '上传文件错误', + 'parent' => '父级', + 'order' => '排序', + 'order_desc' => '降序排序', + 'order_asc' => '升序排序', + 'menus' => '菜单', + 'successfully' => '成功', + 'failed' => '失败', + 'successfully_message' => ':attribute成功', + 'failed_message' => ':attribute失败', + 'action_success' => '操作成功', + 'action_failed' => '操作失败', + 'save_success' => '保存成功', + 'save_failed' => '保存失败', + 'yes' => '是', + 'no' => '否', 'need_start_with_slash' => '需要以 / 开头', - 'cancel' => '取消', - 'please_login' => '请先登录', - 'unauthorized' => '无权访问', - 'user_disabled' => '用户已被禁用', + 'cancel' => '取消', + 'please_login' => '请先登录', + 'unauthorized' => '无权访问', 'code_generators' => [ - 'remark1' => '额外参数请参考', - 'remark2' => '数据库迁移', - 'remark3' => '多个参数使用英文逗号分割', - 'table_name' => '表名', - 'model_name' => '模型', - 'controller_name' => '控制器', - 'service_name' => 'Service', - 'primary_key' => '主键名称', - 'primary_key_description' => '使用 increments 方法', - 'options' => '可选项', + 'remark1' => '额外参数请参考', + 'remark2' => '数据库迁移', + 'remark3' => '多个参数使用英文逗号分割', + 'table_name' => '表名', + 'model_name' => '模型', + 'controller_name' => '控制器', + 'service_name' => 'Service', + 'primary_key' => '主键名称', + 'primary_key_description' => '使用 increments 方法', + 'options' => '可选项', 'create_database_migration' => '创建数据库迁移文件', - 'create_table' => '创建数据表', - 'create_model' => '创建模型', - 'create_controller' => '创建控制器', - 'create_service' => '创建Service', - 'app_title' => '功能名称', - 'column_name' => '字段名', - 'type' => '类型', - 'extra_params' => '额外参数', - 'nullable' => '允许空值', - 'index' => '索引', - 'default_value' => '默认值', - 'comment' => '注释', - 'exists_table' => '已有数据表', - 'generate_code' => '生成代码', - 'expand_more_settings' => '更多设置', - 'collapse_settings' => '收起设置', - 'confirm_generate_code' => '确认生成代码?', - 'preview' => '预览', - 'base_info' => '基本信息', - 'column_info' => '字段信息', - 'route_config' => '路由配置', - 'page_config' => '页面配置', - 'gen_route_menu' => '生成路由&菜单', - 'route' => '路由', - 'menu_name' => '菜单名称', - 'parent_menu' => '父级菜单', - 'menu_icon' => '菜单图标', - 'name_label_desc' => 'name和label属性取字段名和注释', - 'column_warning' => '如果字段名存在 no、status 会导致 form 回显失败!', - 'add_column' => '添加字段', - 'scope' => '作用域', - 'list_component' => '列表组件', - 'list_component_desc' => '列表组件, 默认为 TableColumn', - 'form_component' => '表单组件', - 'form_component_desc' => '表单组件, 默认为 TextControl', - 'detail_component' => '详情组件', - 'detail_component_desc' => '详情组件, 默认为 TextControl', - 'model_config' => '模型配置', - 'file_column' => '文件字段', - 'file_column_desc' => '文件字段会自动在模型中添加 获取/修改器 方法', - 'preview_code' => '预览代码', - 'property' => '属性', - 'property_name' => '属性名称', - 'value' => '值', - 'dialog_form' => '弹窗表单', - 'dialog_size' => '弹窗大小', - 'copy_record' => '复制记录', - 'copy_record_description' => '你可以复制后分享到 Github', - 'import_record' => '导入记录', + 'create_table' => '创建数据表', + 'create_model' => '创建模型', + 'create_controller' => '创建控制器', + 'create_service' => '创建Service', + 'app_title' => '功能名称', + 'column_name' => '字段名', + 'type' => '类型', + 'extra_params' => '额外参数', + 'nullable' => '允许空值', + 'index' => '索引', + 'default_value' => '默认值', + 'comment' => '注释', + 'exists_table' => '已有数据表', + 'generate_code' => '生成代码', + 'expand_more_settings' => '更多设置', + 'collapse_settings' => '收起设置', + 'confirm_generate_code' => '确认生成代码?', + 'preview' => '预览', + 'base_info' => '基本信息', + 'column_info' => '字段信息', + 'route_config' => '路由配置', + 'page_config' => '页面配置', + 'gen_route_menu' => '生成路由&菜单', + 'route' => '路由', + 'menu_name' => '菜单名称', + 'parent_menu' => '父级菜单', + 'menu_icon' => '菜单图标', + 'name_label_desc' => 'name和label属性取字段名和注释', + 'column_warning' => '如果字段名存在 no、status 会导致 form 回显失败!', + 'add_column' => '添加字段', + 'scope' => '作用域', + 'list_component' => '列表组件', + 'list_component_desc' => '列表组件, 默认为 TableColumn', + 'form_component' => '表单组件', + 'form_component_desc' => '表单组件, 默认为 TextControl', + 'detail_component' => '详情组件', + 'detail_component_desc' => '详情组件, 默认为 TextControl', + 'model_config' => '模型配置', + 'file_column' => '文件字段', + 'file_column_desc' => '文件字段会自动在模型中添加 获取/修改器 方法', + 'preview_code' => '预览代码', + 'property' => '属性', + 'property_name' => '属性名称', + 'value' => '值', + 'dialog_form' => '弹窗表单', + 'dialog_size' => '弹窗大小', + 'copy_record' => '复制记录', + 'copy_record_description' => '你可以复制后分享到 Github', + 'import_record' => '导入记录', 'import_record_placeholder' => '请输入导入的json记录', - 'import_record_desc' => '你可以在 Github 找到一些其他人分享的记录', - 'load_config' => '加载配置', - 'load_component_config' => '加载 :c 配置', - 'fill' => '填充', - 'save_current_config' => '保存当前配置', - 'input_config_name' => '请填写配置名称', - 'same_name_tips' => '相同名称的配置将会被覆盖', - 'save_path_dir' => '主应用', - 'save_path_select' => '选择目录', - 'save_path_select_tips' => '可选择项目根目录或插件根目录', - 'save_path_label_prefix' => '插件 -> ', + 'import_record_desc' => '你可以在 Github 找到一些其他人分享的记录', + 'load_config' => '加载配置', + 'load_component_config' => '加载 :c 配置', + 'fill' => '填充', + 'save_current_config' => '保存当前配置', + 'input_config_name' => '请填写配置名称', + 'same_name_tips' => '相同名称的配置将会被覆盖', ], 'admin_users' => '管理员', - 'admin_user' => [ - 'avatar' => '头像', - 'name' => '姓名', - 'roles' => '角色', - 'search_username' => '搜索用户名/名称', - 'password_confirmation' => '两次输入密码不一致', - 'old_password_required' => '请输入原密码', - 'old_password_error' => '原密码错误', + 'admin_user' => [ + 'avatar' => '头像', + 'name' => '姓名', + 'roles' => '角色', + 'lock' => '锁定', + 'edit_password' => '修改密码', + 'search_username' => '搜索用户名/姓名', + 'password_confirmation' => '两次输入密码不一致', + 'old_password_required' => '请输入原密码', + 'old_password_error' => '原密码错误', 'username_already_exists' => '用户名已存在', - 'cannot_delete' => '不可删除超级管理员', ], 'admin_roles' => '角色', - 'admin_role' => [ - 'name' => '名称', - 'slug' => '标识', - 'permissions' => '权限', - 'slug_description' => '角色的唯一标识, 不可重复', + 'admin_role' => [ + 'name' => '名称', + 'slug' => '标识', + 'permissions' => '权限', + 'slug_description' => '角色的唯一标识, 不可重复', 'name_already_exists' => '角色名称已存在', 'slug_already_exists' => '角色标识已存在', - 'set_permissions' => '设置权限', - 'cannot_delete' => '不可删除超级管理员', - 'used' => '不可删除正在使用的角色', + 'set_permissions' => '设置权限', + 'set_menus' => '设置菜单', ], 'admin_permissions' => '权限', - 'admin_permission' => [ - 'name' => '名称', - 'slug' => '标识', - 'http_method' => '请求方式', + 'admin_permission' => [ + 'name' => '名称', + 'slug' => '标识', + 'http_method' => '请求方式', 'http_method_description' => '不选则为ANY', - 'http_path' => '路由', - 'auto_generate' => '自动生成', - 'auto_generate_confirm' => '权限信息会在 截断权限表&权限菜单关联表 后重新生成, 是否继续操作 ?', - 'parent_id_not_allow' => '父级不允许设置为当前子权限', - 'name_already_exists' => '权限名称已存在', - 'slug_already_exists' => '权限标识已存在', + 'http_path' => '路由', + 'auto_generate' => '自动生成', + 'auto_generate_confirm' => '权限信息会在 截断权限表&权限菜单关联表 后重新生成, 是否继续操作 ?', + 'parent_id_not_allow' => '父级不允许设置为当前子权限', + 'name_already_exists' => '权限名称已存在', + 'slug_already_exists' => '权限标识已存在', ], 'admin_menus' => '菜单', - 'admin_menu' => [ - 'parent_id' => '父级', - 'order' => '排序', - 'title' => '名称', - 'icon' => '图标', - 'icon_description' => '请参考', - 'url' => '链接', - 'visible' => '可见', - 'type' => '类型', - 'iframe' => 'Iframe', - 'iframe_description' => '开启后页面将缓存,重新打开时不会重新加载', - 'api' => '页面Api', - 'api_description' => 'schemaApi, 页面初始化请求的api, 需要与Controller中的queryPath一致', - 'route' => '路由', - 'keep_alive' => '缓存页面', - 'link' => '外链', - 'class_name' => '类名', + 'admin_menu' => [ + 'parent_id' => '父级', + 'order' => '排序', + 'title' => '名称', + 'icon' => '图标', + 'icon_description' => '请参考', + 'url' => '链接', + 'visible' => '可见', + 'type' => '类型', + 'api' => '页面Api', + 'api_description' => 'schemaApi, 页面初始化请求的api, 需要与Controller中的queryPath一致', + 'route' => '路由', + 'link' => '外链', + 'class_name' => '类名', 'class_name_description' => '菜单的CSS类名, 一般用于自定义样式', - 'show' => '可见', - 'hide' => '隐藏', - 'is_home' => '首页', - 'is_home_description' => '在多页签卡模式下,页面标签将固定在左侧', - 'is_full' => '全屏', - 'is_full_description' => '开启后将隐藏该页面的菜单栏部分', - 'parent_id_not_allow' => '父级菜单不允许设置为当前子菜单', - 'component' => '组件', - 'component_desc' => '默认为 amis , 非自定义前端页面请勿修改', - 'url_exists' => '菜单路径重复', + 'show' => '可见', + 'hide' => '隐藏', + 'is_home' => '首页', + 'is_home_description' => '在多页签卡模式下,页面标签将固定在左侧', + 'is_full' => '全屏', + 'is_full_description' => '开启后将隐藏该页面的菜单栏部分', + 'parent_id_not_allow' => '父级菜单不允许设置为当前子菜单', + 'component' => '组件', + 'component_desc' => '默认为 amis , 非自定义前端页面请勿修改', + ], + + 'keywords' => [ + 'search_name' => '名称/KEY', + 'parent_keyword' => '父级关键字', ], 'extensions' => [ - 'name_invalid' => '无效的扩展名称', - 'exists' => '该扩展已存在:', - 'menu' => '扩展', - 'page_title' => '扩展', - 'create' => '创建', - 'install' => '安装', - 'create_extension' => '创建扩展', - 'create_tips' => '创建成功后会在
:dir
目录下创建基础的扩展目录结构', - 'local_install' => '本地安装', - 'more_extensions' => '更多扩展', - 'setting' => '设置', - 'enable' => '启用', - 'enable_confirm' => '确定要启用该扩展吗?', - 'disable' => '禁用', - 'disable_confirm' => '确定要禁用该扩展吗?', - 'uninstall' => '卸载', - 'uninstall_confirm' => ' + 'name_invalid' => '无效的扩展名称', + 'exists' => '该扩展已存在:', + 'menu' => '扩展', + 'page_title' => '扩展', + 'create' => '创建', + 'install' => '安装', + 'create_extension' => '创建扩展', + 'create_tips' => '创建成功后会在
:dir
目录下创建基础的扩展目录结构', + 'local_install' => '本地安装', + 'more_extensions' => '更多扩展', + 'setting' => '设置', + 'enable' => '启用', + 'enable_confirm' => '确定要启用该扩展吗?', + 'disable' => '禁用', + 'disable_confirm' => '确定要禁用该扩展吗?', + 'uninstall' => '卸载', + 'uninstall_confirm' => '
确认卸载该扩展?
卸载将会删除启用扩展后发布的所有文件及数据库, 且不可找回!!!
@@ -234,73 +231,34 @@ return [
', 'filter_placeholder' => '输入扩展名称', - 'form' => [ - 'create_extension' => '创建扩展', - 'name' => '名称', - 'namespace' => '命名空间', + 'form' => [ + 'create_extension' => '创建扩展', + 'name' => '名称', + 'namespace' => '命名空间', 'create_description' => '创建成功后会在 :dir 目录下创建基础的扩展目录结构', ], - 'card' => [ - 'author' => '作者', - 'version' => '版本', + 'card' => [ + 'author' => '作者', + 'version' => '版本', 'homepage' => '主页', - 'status' => '状态', + 'status' => '状态', ], - 'status_map' => [ - 'enabled' => '已启用', + 'status_map' => [ + 'enabled' => '已启用', 'disabled' => '已禁用', ], - 'validation' => [ - 'file' => '请选择文件', + 'validation' => [ + 'file' => '请选择文件', 'invalid_package' => '无效的扩展包', ], ], - 'export' => [ - 'title' => '导出', - 'all' => '全部', - 'page' => '本页', - 'selected_rows' => '选中行', - 'page_no_data' => '本页无数据', - 'selected_rows_no_data' => '请选择要导出的数据', + 'export' => [ + 'title' => '导出', + 'all' => '全部', + 'page' => '本页', + 'selected_rows' => '选中行', + 'page_no_data' => '本页无数据', + 'selected_rows_no_data' => '请选择要导出的数据', 'please_install_laravel_excel' => '请先安装 laravel-excel 扩展', ], - 'keywords' => [ - 'search_name' => '名称/KEY', - 'parent_keyword' => '父级关键字', - ], - 'articles' => [ - 'id' => '主键ID', - 'title' => '标题', - 'content' => '内容', - 'cover' =>'封面', - 'category' => '分类', - 'tags' => '标签', - 't_ids' => '标签', - 'published_at' => '定时发布', - 'published_at_g' => '发布时间', - 'is_enable' => '显示', - 'is_recommend' => '推荐', - 'sort' => '排序', - 'appendixes' => '附件', - 'published_at_remark' => '*若未设置发布时间且操作设置为显示,则默认生成发布时间', - ], - 'ads' => [ - 'id' => 'ID', - 'address' => '位置', - 'resource' =>'内容', - 'published_at' => '定时发布', - 'published_at_g' => '发布时间', - 'is_enable' => '显示', - 'remark' => '备注', - 'sort' => '排序', - 'published_at_remark' => '*若未设置发布时间且操作设置为显示,则默认生成发布时间', - 'jump_type' => '跳转类型', - 'jump_config'=>'跳转配置', - 'jump_config_arr'=>[ - 'web_link' => '网页地址', - 'app_link' => '应用路径', - 'mini_id' => '小程序ID', - 'mini_link'=> '小程序路径' - ], - ] ];