├── README.md ├── composer.json ├── migrations └── database │ └── 2024_04_06_164639_HPlusAdmin.php └── src ├── Admin.php ├── Command └── AdminInstall.php ├── ConfigProvider.php ├── Controller ├── AbstractAdminController.php ├── MenuController.php ├── SettingController.php ├── System │ ├── AdminRolesController.php │ └── AdminUsersController.php └── UserController.php ├── Exception └── ApiAmisException.php ├── Model ├── AdminApi.php ├── AdminCodeGenerator.php ├── AdminMenu.php ├── AdminPage.php ├── AdminPermission.php ├── AdminRelationship.php ├── AdminRole.php ├── AdminSetting.php ├── AdminUser.php ├── BaseModel.php ├── Extension.php └── PersonalAccessToken.php ├── Service ├── AdminApiService.php ├── AdminBaseService.php ├── AdminCodeGeneratorService.php ├── AdminMenuService.php ├── AdminPageService.php ├── AdminPermissionService.php ├── AdminRelationshipService.php ├── AdminRoleService.php ├── AdminSettingService.php └── AdminUserService.php ├── Support ├── Core │ ├── Asset.php │ ├── Database.php │ ├── Hash.php │ └── Menu.php └── helpers.php └── Traits ├── AssetsTrait.php ├── HasApiBase.php ├── HasCheckActionTrait.php ├── HasElementTrait.php ├── HasIconifyPickerTrait.php ├── HasQueryPathTrait.php └── HasUploadTrait.php /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

HPlus Admin V3

5 |

拥抱后端开发的未来

6 |

基于最新技术栈Hyperf、PHP 8.1、Swoole 5打造的高性能后台管理框架,整合amis前端技术,为快速开发而生。

7 | 8 | [GitHub](https://github.com/hyperf-plus/admin) | [文档](您的文档地址) | [在线体验](您的演示地址) | [社区支持](您的社区支持链接) 9 | 10 |
11 | 12 |

13 | 14 | PHP版本 15 | 16 | 17 | Swoole版本 18 | 19 | 20 | Hyperf版本 21 | 22 | 23 | 许可证 24 | 25 |

26 | 27 | ## 🚀 介绍 28 | 29 | HPlus Admin V3是一个革命性的后台管理框架,专为追求高性能、灵活性和前沿Web开发实践的开发者设计。该框架基于PHP 8.1、Swoole 5和Hyperf构建,提供了前所未有的性能提升和开发效率。HPlus Admin V3的一个重要特性是深度整合了amis前端框架,通过JSON配置即可快速构建出美观、功能丰富的界面,极大简化了前端的开发工作量。此外,HPlus Admin V3吸收了Laravel的开发理念,结合了Laravel在ORM、中间件、服务容器等方面的优势,为Hyperf提供了更加丰富的开发体验和生态支持。 30 | 31 | ## 💡 特点 32 | 33 | - **前沿技术栈:** 构建于PHP 8.1、Swoole 5和Hyperf之上,拥抱异步编程和协程的最新进展,为您的应用提供了前所未有的性能提升。 34 | 35 | - **与amis无缝集成:** HPlus Admin V3深度整合amis,让开发者能够通过JSON配置轻松构建出美观、功能丰富的前端界面。amis的这一特性极大地降低了前端开发的复杂性,使得无需深入了解复杂的前端框架或编写大量的JavaScript代码,就能快速开发出响应式、交云互动的Web应用。 36 | 37 | - **快速开发:** 提供了丰富的模板和构建工具,支持快速生成CRUD操作界面,大大加快了开发速度,使得从概念到产品的时间更短。 38 | 39 | - **高度可扩展:** 支持自定义插件和模块,开发者可以根据业务需求灵活添加功能,或者整合第三方服务,满足多变的业务场景。 40 | 41 | - **安全性:** 结合Hyperf的安全机制和Swoole的稳定性,提供了强大的安全保障,帮助您的应用抵御网络攻击,保护用户数据。 42 | 43 | 通过整合amis,HPlus Admin V3不仅仅是一个后台管理框架,它更是一个强大的全栈解决方案,使得从数据后端到前端界面的开发变得前所未有地简单和高效。无论是复杂的数据处理还是丰富的用户交互,HPlus Admin V3都能帮助您快速实现,加速产品的迭代和上市。 44 | 45 | ### 🔥 功能丰富的组件库 46 | 47 | - **丰富的组件库:** amis提供了150+的组件库,涵盖了从基础的表单元素到复杂的数据展示组件,甚至包括图表、对话框、标签页等高级组件。这些组件的丰富性保证了几乎所有类型的前端页面需求都能得到满足。 48 | 49 | - **高度可定制化:** 尽管amis的组件通过JSON配置即可使用,但它们也支持高度的定制化。开发者可以根据自己的需求调整组件的样式、行为和交互方式,甚至可以扩展自定义组件,以实现特定的功能。 50 | 51 | - **快速响应式布局:** amis的组件设计遵循响应式布局原则,能够自动适配不同大小的屏幕。无论是在PC端还是移动设备上,都能保证用户界面的美观和用户体验。 52 | 53 | - **声明式UI构建:** 通过JSON配置即可声明式地构建用户界面,这种方式使得界面的构建变得简单明了,大大减少了前端代码的编写量。开发者可以通过可视化工具直接生成这些JSON配置,实现所见即所得的界面开发。 54 | 55 | 56 | ## 🚀 快速开始 57 | 58 | ### 环境要求 59 | 60 | 确保您的服务器满足以下条件: 61 | 62 | - PHP >= 8.1 63 | - Swoole >= 5.0 64 | - Hyperf >= 3.0 65 | 66 | ### 安装步骤 67 | 【开发中暂未发布】 68 | 1. 创建Hyperf项目: 69 | 70 | ```bash 71 | composer create-project hyperf/hyperf-skeleton your-project-name 72 | ``` 73 | 74 | 2. 安装HPlus Admin V3包: 75 | 76 | ```bash 77 | cd your-project-name 78 | composer require hyperf-plus/admin 79 | ``` 80 | 81 | 3. 发布配置文件和资源: 82 | 83 | ```bash 84 | php bin/hyperf.php vendor:publish hyperf-plus/admin 85 | ``` 86 | 87 | 4. 初始化数据库: 88 | 89 | ```bash 90 | php bin/hyperf.php admin:install 91 | ``` 92 | 93 | 5. 启动项目: 94 | 95 | ```bash 96 | php bin/hyperf.php start 97 | ``` 98 | 99 | 现在您可以通过访问`http://localhost:9501/admin`来使用HPlus Admin V3了,默认账号和密码均为`admin`。 100 | 101 | ## 🤝 贡献 102 | 103 | 欢迎各位开发者对HPlus Admin V3的贡献,无论是通过提交Pull Request来修复bug或添加新特性,还是通过提供意见和反馈来帮助我们改进项目。如果您喜欢这个项目,不妨给我们一个Star,您的支持是我们前进的最大动力! 104 | 105 | ## 🔒 许可证 106 | 107 | HPlus Admin V3遵循MIT许可证发布。详细内容请参见[LICENSE](LICENSE)文件。 108 | 109 | 感谢您对HPlus Admin V3的关注,期待您的加入和贡献! 110 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf-plus/admin", 3 | "description": "开箱即用的Hyperf后台扩展,amis最强后台管理系统", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "毛自豪", 8 | "email": "4213509@qq.com" 9 | } 10 | ], 11 | "homepage": "https://github.com/lphkxd/hyperf-admin", 12 | "keywords": [ 13 | "hyperf", 14 | "Admin", 15 | "amis" 16 | ], 17 | "require": { 18 | "php": ">=8.1", 19 | "ext-swoole": ">=5", 20 | "96qbhy/hyperf-auth": "^3.1", 21 | "hyperf-plus/helper": "^3.0", 22 | "hyperf-plus/route": "^3.1", 23 | "hyperf-plus/swagger": "^3.1", 24 | "hyperf-plus/validate": "^3.1", 25 | "hyperf-plus/ui": "^3.1" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "HPlus\\Admin\\": "src/" 30 | }, 31 | "files": [ 32 | "../ui/src/Support/helpers.php" 33 | ] 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "HPlus\\Admin\\Tests\\": "tests" 38 | } 39 | }, 40 | "extra": { 41 | "hyperf": { 42 | "config": "\\HPlus\\Admin\\ConfigProvider" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /migrations/database/2024_04_06_164639_HPlusAdmin.php: -------------------------------------------------------------------------------- 1 | up(); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Database::make()->down(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Admin.php: -------------------------------------------------------------------------------- 1 | get(AdminSettingService::class); 58 | } 59 | 60 | /** 61 | * @return Menu|mixed 62 | * @throws ContainerExceptionInterface 63 | * @throws NotFoundExceptionInterface 64 | */ 65 | public static function menu() 66 | { 67 | return ApplicationContext::getContainer()->get(Menu::class); 68 | } 69 | 70 | public static function user() 71 | { 72 | return auth('admin')->user(); 73 | } 74 | 75 | public static function adminMenuModel() 76 | { 77 | return config('admin.database.menu_model', AdminMenu::class); 78 | } 79 | 80 | public static function adminUserModel() 81 | { 82 | return config('admin.database.user_model', AdminUser::class); 83 | } 84 | 85 | public static function adminRoleModel() 86 | { 87 | return config('admin.database.role_model', AdminRole::class); 88 | } 89 | 90 | public static function adminPermissionModel() 91 | { 92 | return config('admin.database.permission_model', AdminPermission::class); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Command/AdminInstall.php: -------------------------------------------------------------------------------- 1 | setDescription('安装Admin后台'); 37 | } 38 | 39 | public function handle() 40 | { 41 | $this->call('migrate', ['--path' => BASE_PATH . '/vendor/hyperf-plus/admin/migrations/database', '--realpath' => true]); 42 | if (config('admin.models.admin_user', AdminUser::class)::query()->count() == 0) { 43 | Database::make()->fillInitialData(); 44 | } 45 | $this->info('success'); 46 | } 47 | 48 | private function publishAssets() 49 | { 50 | $source = __DIR__ . '/../../admin-views/dist/'; 51 | $target = BASE_PATH . '/public/admin'; 52 | 53 | $this->copyDirectory($source, $target); 54 | } 55 | 56 | private function copyDirectory(string $source, string $target) 57 | { 58 | if (! is_dir($source)) { 59 | return; 60 | } 61 | 62 | if (! is_dir($target)) { 63 | mkdir($target, 0755, true); 64 | } 65 | 66 | # 复制目录 67 | 68 | $iterator = new RecursiveIteratorIterator( 69 | new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), 70 | RecursiveIteratorIterator::SELF_FIRST 71 | ); 72 | 73 | foreach ($iterator as $item) { 74 | if ($item->isDir()) { 75 | $dirPath = $target . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); 76 | if (! is_dir($dirPath)) { 77 | mkdir($dirPath); 78 | } 79 | } else { 80 | copy($item->getPathname(), $target . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 26 | 'scan' => [ 27 | 'paths' => [ 28 | __DIR__ . '/Command', 29 | __DIR__ . '/Controller', 30 | __DIR__ . '/Service', 31 | ], 32 | ], 33 | ], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Controller/AbstractAdminController.php: -------------------------------------------------------------------------------- 1 | service = make($this->serviceName); 73 | } else { 74 | $this->service = $this->defaultService($prefix); 75 | } 76 | $prefix = str_replace('/_', '/', $prefix); 77 | $this->queryPath = $prefix; 78 | } 79 | 80 | /** 81 | * 获取当前登录用户. 82 | */ 83 | public function user() 84 | { 85 | return Admin::user(); 86 | } 87 | 88 | public function getPrimaryValue($request): mixed 89 | { 90 | $primaryKey = $this->service->primaryKey(); 91 | 92 | return $request->{$primaryKey}; 93 | } 94 | 95 | /** 96 | * 获取新增页面. 97 | */ 98 | #[GetApi(summary: '列表')] 99 | #[PostApi(summary: '创建数据')] 100 | public function _self_path() 101 | { 102 | 103 | if ($this->request->getMethod() == 'POST') { 104 | return $this->store(); 105 | } 106 | 107 | if ($this->actionOfGetData()) { 108 | return $this->service->list($this->request->all()); 109 | } 110 | 111 | if ($this->actionOfExport()) { 112 | return $this->export(); 113 | } 114 | 115 | return $this->grid(); 116 | } 117 | 118 | /** 119 | * 获取新增页面. 120 | */ 121 | #[GetApi(summary: '创建数据')] 122 | public function create() 123 | { 124 | $this->isCreate = true; 125 | $form = amis()->Card() 126 | ->className('base-form') 127 | ->header(['title' => __('admin.create')]) 128 | ->toolbar([$this->backButton()]) 129 | ->body($this->form(false)->api($this->getStorePath())); 130 | 131 | return $this->basePage()->body($form); 132 | } 133 | 134 | public function store() 135 | { 136 | $response = fn ($result) => $this->autoResponse($result, __('admin.save')); 137 | 138 | if ($this->actionOfQuickEdit()) { 139 | return $response($this->service->quickEdit($this->request->all())); 140 | } 141 | 142 | if ($this->actionOfQuickEditItem()) { 143 | return $response($this->service->quickEditItem($this->request->all())); 144 | } 145 | return $response($this->service->store($this->request->all())); 146 | } 147 | 148 | #[GetApi(summary: '获取单条数据', path: '{id:\\d+}')] 149 | #[Path(name: '主键id', key: 'id')] 150 | public function edit($id) 151 | { 152 | $this->isEdit = true; 153 | 154 | if ($this->actionOfGetData()) { 155 | return $this->service->getEditData($id); 156 | } 157 | 158 | $form = amis()->Card() 159 | ->className('base-form') 160 | ->header(['title' => __('admin.edit')]) 161 | ->toolbar([$this->backButton()]) 162 | ->body( 163 | $this->form(true)->api($this->getUpdatePath())->initApi($this->getEditGetDataPath()) 164 | ); 165 | 166 | return $this->basePage()->body($form); 167 | } 168 | 169 | #[PutApi(summary: '保存单条数据', path: '{id:\\d+}')] 170 | #[Path(key: 'id', name: '主键id')] 171 | public function put($id) 172 | { 173 | $data = $this->request->all(); 174 | if (empty($id)) { 175 | throw new ApiAmisException('缺少更新条件,请先选择数据!'); 176 | } 177 | $result = $this->service->update($id, $data); 178 | return $this->autoResponse($result, __('admin.save')); 179 | } 180 | 181 | #[DeleteApi(summary: '删除数据', path: '{ids}')] 182 | public function destroy($ids) 183 | { 184 | $rows = $this->service->delete($ids); 185 | return $this->autoResponse($rows, __('admin.delete')); 186 | } 187 | 188 | /** 189 | * 根据传入的条件, 返回消息响应. 190 | * @param mixed $flag 191 | * @param mixed $text 192 | */ 193 | protected function autoResponse($flag, $text = '') 194 | { 195 | if (! $text) { 196 | $text = __('admin.actions'); 197 | } 198 | 199 | if ($flag) { 200 | return Admin::responseData([], $text . __('admin.successfully')); 201 | } 202 | 203 | return Admin::responseError($this->service->getError() ?? $text . __('admin.failed')); 204 | } 205 | 206 | private function defaultService(string $prefix) 207 | { 208 | # todo 根据路由自动挂载service 209 | $service = new class() extends AdminBaseService { 210 | }; 211 | $service->setModelName($prefix); 212 | return $service; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/Controller/MenuController.php: -------------------------------------------------------------------------------- 1 | baseDetail()->body([]); 33 | } 34 | 35 | /** 36 | * 保存排序. 37 | */ 38 | public function saveOrder() 39 | { 40 | return $this->autoResponse($this->service->reorder($this->request->post('ids'))); 41 | } 42 | 43 | protected function grid(): Page 44 | { 45 | $crud = $this->baseCRUD() 46 | ->draggable() 47 | ->saveOrderApi([ 48 | 'url' => '/system/admin_menus/save_order', 49 | 'data' => ['ids' => '${ids}'], 50 | ]) 51 | ->loadDataOnce() 52 | ->syncLocation(false) 53 | ->footerToolbar([]) 54 | ->headerToolbar([$this->createButton(true, 'lg'), ...$this->baseHeaderToolBar()]) 55 | ->filterTogglable(false) 56 | ->footerToolbar(['statistics']) 57 | ->bulkActions([$this->bulkDeleteButton()->reload('window')]) 58 | ->columns([ 59 | amis()->TableColumn('id', 'ID')->sortable(), 60 | amis()->TableColumn('title', __('admin.admin_menu.title')), 61 | amis()->TableColumn('icon', __('admin.admin_menu.icon')) 62 | ->type('flex') 63 | ->justify('start') 64 | ->items([ 65 | amis()->SvgIcon()->icon('${icon}')->className('mr-2 text-lg'), 66 | '${icon}', 67 | ]), 68 | amis()->TableColumn('url', __('admin.admin_menu.url')), 69 | amis()->TableColumn('order', __('admin.admin_menu.order'))->quickEdit( 70 | amis()->NumberControl()->min(0)->saveImmediately(true) 71 | ), 72 | amis()->TableColumn('visible', __('admin.admin_menu.visible'))->quickEdit( 73 | amis()->SwitchControl()->mode('inline')->saveImmediately(true) 74 | ), 75 | amis()->TableColumn('is_home', __('admin.admin_menu.is_home'))->quickEdit( 76 | amis()->SwitchControl()->mode('inline')->saveImmediately(true) 77 | ), 78 | $this->rowActions([ 79 | $this->rowEditButton(true, 'lg'), 80 | $this->rowDeleteButton(), 81 | ]), 82 | ]); 83 | 84 | return $this->baseList($crud); 85 | } 86 | 87 | protected function form(): Form 88 | { 89 | return $this->baseForm()->body([ 90 | amis()->GroupControl()->body([ 91 | amis()->TextControl('title', __('admin.admin_menu.title'))->required(), 92 | $this->iconifyPicker('icon', __('admin.admin_menu.icon')), 93 | ]), 94 | amis()->GroupControl()->body([ 95 | amis()->TreeSelectControl('parent_id', __('admin.admin_menu.parent_id')) 96 | ->labelField('title') 97 | ->valueField('id') 98 | ->showIcon(false) 99 | ->value(0) 100 | ->source('/system/admin_menus?_action=getData'), 101 | amis()->TextControl('component', __('admin.admin_menu.component')) 102 | ->description(__('admin.admin_menu.component_desc')) 103 | ->value('amis'), 104 | ]), 105 | amis()->TextControl('url', __('admin.admin_menu.url')) 106 | ->required() 107 | ->validateOnChange() 108 | ->validations(['matchRegexp' => '/^(http(s)?\:\/)?(\/)+/']) 109 | ->validationErrors(['matchRegexp' => __('admin.need_start_with_slash')]) 110 | ->placeholder('eg: /admin_menus'), 111 | amis()->NumberControl('order', __('admin.admin_menu.order')) 112 | ->required() 113 | ->displayMode('enhance') 114 | ->description(__('admin.order_asc')) 115 | ->min(0) 116 | ->value(0), 117 | // amis()->ListControl('url_type', __('admin.admin_menu.type')) 118 | // ->options(Admin::adminMenuModel()::getType()) 119 | // ->value(Admin::adminMenuModel()::TYPE_ROUTE), 120 | amis()->SwitchControl('visible', __('admin.admin_menu.visible')) 121 | ->onText(__('admin.admin_menu.show')) 122 | ->offText(__('admin.admin_menu.hide')) 123 | ->value(1), 124 | amis()->SwitchControl('is_home', __('admin.admin_menu.is_home')) 125 | ->onText(__('admin.yes')) 126 | ->offText(__('admin.no')) 127 | ->description(__('admin.admin_menu.is_home_description')) 128 | ->value(0), 129 | amis()->SwitchControl('is_full', __('admin.admin_menu.is_full')) 130 | ->onText(__('admin.yes')) 131 | ->offText(__('admin.no')) 132 | ->description(__('admin.admin_menu.is_full_description')) 133 | ->value(0), 134 | ])->onEvent([ 135 | 'submitSucc' => [ 136 | 'actions' => [ 137 | 'actionType' => 'custom', 138 | 'script' => 'window.location.reload()', 139 | ], 140 | ], 141 | ]); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Controller/SettingController.php: -------------------------------------------------------------------------------- 1 | UI::getNav(), 32 | 'assets' => UI::getAssets(), 33 | 'app_name' => Admin::config('admin.name'), 34 | 'locale' => Admin::config('app.locale'), 35 | 'layout' => Admin::config('admin.layout'), 36 | 'logo' => Admin::config('admin.logo'), 37 | 38 | 'login_captcha' => Admin::config('admin.auth.login_captcha'), 39 | 'show_development_tools' => Admin::config('admin.show_development_tools'), 40 | 'system_theme_setting' => Admin::setting()->get('system_theme_setting'), 41 | 'enabled_extensions' => Extension::query()->where('is_enabled', 1)->pluck('name')?->toArray(), 42 | ]; 43 | } 44 | 45 | #[PostApi(path: 'config', summary: '获取系配置统')] 46 | public function update() 47 | { 48 | $data = $this->request->all(); 49 | foreach ($data as $key => $value) { 50 | if ($key == 'system_theme_setting') { 51 | $data[$key] = $value; 52 | } else { 53 | unset($data[$key]); 54 | } 55 | } 56 | Admin::setting()->setMany($data); 57 | return Admin::responseMessage(__('admin.save_success')); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Controller/System/AdminRolesController.php: -------------------------------------------------------------------------------- 1 | baseCRUD() 22 | ->headerToolbar([ 23 | $this->createButton(true), 24 | ...$this->baseHeaderToolBar(), 25 | ]) 26 | ->filterTogglable(false) 27 | ->itemCheckableOn('${slug !== "administrator"}') 28 | ->columns([ 29 | amis()->TableColumn()->label('ID')->name('id')->sortable(), 30 | amis()->TableColumn()->label(__('admin.admin_role.name'))->name('name'), 31 | amis()->TableColumn()->label(__('admin.admin_role.slug'))->name('slug')->type('tag'), 32 | amis()->TableColumn() 33 | ->label(__('admin.created_at')) 34 | ->name('created_at') 35 | ->type('datetime') 36 | ->sortable(true), 37 | amis()->TableColumn() 38 | ->label(__('admin.updated_at')) 39 | ->name('updated_at') 40 | ->type('datetime') 41 | ->sortable(true), 42 | $this->rowActions([ 43 | $this->setPermission(), 44 | $this->rowEditButton(true), 45 | $this->rowDeleteButton()->hiddenOn('${slug == "administrator"}'), 46 | ]), 47 | ]); 48 | 49 | return $this->baseList($crud)->css([ 50 | '.tree-full' => [ 51 | 'overflow' => 'hidden !important', 52 | ], 53 | '.cxd-TreeControl > .cxd-Tree' => [ 54 | 'height' => '100% !important', 55 | 'max-height' => '100% !important', 56 | ], 57 | ]); 58 | } 59 | 60 | protected function setPermission() 61 | { 62 | return amis()->DrawerAction() 63 | ->label(__('admin.admin_role.set_permissions')) 64 | ->icon('fa-solid fa-gear') 65 | ->level('link') 66 | ->drawer( 67 | amis()->Drawer() 68 | ->title(__('admin.admin_role.set_permissions')) 69 | ->resizable() 70 | ->closeOnOutside() 71 | ->closeOnEsc() 72 | ->body([ 73 | amis()->Form() 74 | ->api(admin_url('system/admin_roles/save_permissions')) 75 | ->initApi($this->getEditGetDataPath()) 76 | ->mode('normal') 77 | ->data(['id' => '${id}']) 78 | ->body([ 79 | amis()->TreeControl() 80 | ->name('permissions') 81 | ->label() 82 | ->multiple() 83 | ->heightAuto() 84 | ->options(AdminPermissionService::make()->getTree()) 85 | ->searchable() 86 | ->cascade() 87 | ->joinValues(false) 88 | ->extractValue() 89 | ->size('full') 90 | ->className('h-full b-none') 91 | ->inputClassName('h-full tree-full') 92 | ->labelField('name') 93 | ->valueField('id'), 94 | ]), 95 | ]) 96 | ); 97 | } 98 | 99 | #[PostApi(summary: '保存权限')] 100 | public function savePermissions() 101 | { 102 | $data = $this->request->all(); 103 | 104 | $result = $this->service->savePermissions($data[$this->service->primaryKey()], $data['permissions'] ?? []); 105 | 106 | return $this->autoResponse($result, __('admin.save')); 107 | } 108 | 109 | protected function form(): Form 110 | { 111 | return $this->baseForm()->body([ 112 | amis()->TextControl()->label(__('admin.admin_role.name'))->name('name')->required(), 113 | amis()->TextControl() 114 | ->label(__('admin.admin_role.slug')) 115 | ->name('slug') 116 | ->description(__('admin.admin_role.slug_description')) 117 | ->required(), 118 | ]); 119 | } 120 | 121 | public function detail(): Form 122 | { 123 | return $this->baseDetail()->body([]); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Controller/System/AdminUsersController.php: -------------------------------------------------------------------------------- 1 | baseCRUD() 32 | ->headerToolbar([ 33 | $this->createButton(true), 34 | ...$this->baseHeaderToolBar(), 35 | ]) 36 | ->filter($this->baseFilter()->body( 37 | amis()->TextControl('keyword', __('admin.keyword')) 38 | ->size('md') 39 | ->placeholder(__('admin.admin_user.search_username')) 40 | )) 41 | ->itemCheckableOn('${!administrator}') 42 | ->columns([ 43 | amis()->TableColumn('id', 'ID')->sortable(), 44 | amis()->TableColumn('avatar', __('admin.admin_user.avatar'))->type('avatar')->src('${avatar}'), 45 | amis()->TableColumn('username', __('admin.username')), 46 | amis()->TableColumn('name', __('admin.admin_user.name')), 47 | amis()->TableColumn('roles', __('admin.admin_user.roles'))->type('each')->items( 48 | amis()->Tag()->label('${name}')->className('my-1') 49 | ), 50 | amis()->TableColumn('enabled', __('admin.extensions.card.status'))->quickEdit( 51 | amis()->SwitchControl()->mode('inline')->disabledOn('${id == 1}')->saveImmediately(true) 52 | ), 53 | amis()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true), 54 | $this->rowActions([ 55 | $this->rowEditButton(true), 56 | $this->rowDeleteButton()->hiddenOn('${administrator}'), 57 | ]), 58 | ]); 59 | 60 | return $this->baseList($crud); 61 | } 62 | 63 | public function form(): Form 64 | { 65 | return $this->baseForm()->body([ 66 | amis()->ImageControl('avatar', __('admin.admin_user.avatar'))->receiver($this->uploadImagePath()), 67 | amis()->TextControl('username', __('admin.username'))->required(), 68 | amis()->TextControl('name', __('admin.admin_user.name'))->required(), 69 | amis()->TextControl('password', __('admin.password'))->type('input-password'), 70 | amis()->TextControl('confirm_password', __('admin.confirm_password'))->type('input-password'), 71 | amis()->SelectControl('roles', __('admin.admin_user.roles')) 72 | ->searchable() 73 | ->multiple() 74 | ->labelField('name') 75 | ->valueField('id') 76 | ->joinValues(false) 77 | ->extractValue() 78 | ->options(AdminRoleService::make()->query()->get(['id', 'name'])), 79 | amis()->SwitchControl('enabled', __('admin.extensions.card.status')) 80 | ->onText(__('admin.extensions.enable')) 81 | ->offText(__('admin.extensions.disable')) 82 | ->disabledOn('${id == 1}') 83 | ->value(1), 84 | ]); 85 | } 86 | 87 | public function detail(): Form 88 | { 89 | return $this->baseDetail()->body([]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Controller/UserController.php: -------------------------------------------------------------------------------- 1 | baseCRUD() 32 | ->headerToolbar([ 33 | $this->createButton(true), 34 | ...$this->baseHeaderToolBar(), 35 | ]) 36 | ->filter($this->baseFilter()->body( 37 | amis()->TextControl('keyword', __('admin.keyword')) 38 | ->size('md') 39 | ->placeholder(__('admin.admin_user.search_username')) 40 | )) 41 | ->columns([ 42 | amis()->TableColumn('id', 'ID')->sortable(), 43 | amis()->TableColumn('avatar', __('admin.admin_user.avatar'))->type('avatar')->src('${avatar}'), 44 | amis()->TableColumn('username', __('admin.username')), 45 | amis()->TableColumn('name', __('admin.admin_user.name')), 46 | amis()->TableColumn('roles', __('admin.admin_user.roles'))->type('each')->items( 47 | amis()->Tag()->label('${name}')->className('my-1') 48 | ), 49 | amis()->TableColumn('enabled', __('admin.extensions.card.status'))->quickEdit( 50 | amis()->SwitchControl()->mode('inline')->disabledOn('${id == 1}')->saveImmediately(true) 51 | ), 52 | amis()->TableColumn('created_at', __('admin.created_at'))->type('datetime')->sortable(true), 53 | $this->rowActions([ 54 | $this->rowEditButton(true), 55 | $this->rowDeleteButton()->hiddenOn('${administrator}'), 56 | ]), 57 | ]); 58 | 59 | return $this->baseList($crud); 60 | } 61 | 62 | public function form(): Form 63 | { 64 | return $this->baseForm()->body([ 65 | // amis()->ImageControl('avatar', __('admin.admin_user.avatar'))->receiver($this->uploadImagePath()), 66 | amis()->TextControl('username', __('admin.username'))->required(), 67 | amis()->TextControl('name', __('admin.admin_user.name'))->required(), 68 | amis()->TextControl('password', __('admin.password'))->type('input-password'), 69 | amis()->TextControl('confirm_password', __('admin.confirm_password'))->type('input-password'), 70 | // amis()->SelectControl('roles', __('admin.admin_user.roles')) 71 | // ->searchable() 72 | // ->multiple() 73 | // ->labelField('name') 74 | // ->valueField('id') 75 | // ->joinValues(false) 76 | // ->extractValue() 77 | // ->options(AdminRoleService::make()->query()->get(['id', 'name'])), 78 | amis()->SwitchControl('enabled', __('admin.extensions.card.status')) 79 | ->onText(__('admin.extensions.enable')) 80 | ->offText(__('admin.extensions.disable')) 81 | ->disabledOn('${id == 1}') 82 | ->value(1), 83 | ]); 84 | } 85 | 86 | #[GetApi(summary: '验证码')] 87 | public function captcha() 88 | { 89 | return []; 90 | } 91 | 92 | #[GetApi(summary: '验证码')] 93 | public function example() 94 | { 95 | throw new UnauthorizedException('用户名或密码错误!'); 96 | } 97 | 98 | #[GetApi(summary: '菜单列表')] 99 | public function menus() 100 | { 101 | return Admin::responseData(Admin::menu()->all()); 102 | } 103 | 104 | #[GetApi(summary: '用户信息', path: 'current-user')] 105 | public function info($id = 0) 106 | { 107 | return [ 108 | 'name' => '超级管理员', 109 | 'avatar' => 'http://demo.owladmin.com/admin-assets/default-avatar.png', 110 | 'menus' => [ 111 | 'type' => 'dropdown-button', 112 | 'hideCaret' => true, 113 | 'trigger' => 'hover', 114 | 'label' => '超级管理员', 115 | 'className' => 'h-full w-full', 116 | 'btnClassName' => 'navbar-user w-full', 117 | 'menuClassName' => 'min-w-0 p-2', 118 | 'icon' => 'http://demo.owladmin.com/admin-assets/default-avatar.png', 119 | 'buttons' => [ 120 | [ 121 | 'type' => 'button', 122 | 'iconClassName' => 'pr-2', 123 | 'icon' => 'fa fa-user-gear', 124 | 'label' => '个人设置', 125 | 'onClick' => 'window.location.hash = "#/user_setting"', 126 | ], 127 | [ 128 | 'type' => 'button', 129 | 'iconClassName' => 'pr-2', 130 | 'label' => '退出登录', 131 | 'icon' => 'fa-solid fa-right-from-bracket', 132 | 'onClick' => 'window.$owl.logout()', 133 | ], 134 | ], 135 | ], 136 | ]; 137 | } 138 | 139 | #[PostApi(summary: '登录')] 140 | public function login() 141 | { 142 | return Admin::responseData([ 143 | 'token' => auth('admin')->login(AdminUser::first()), 144 | ]); 145 | throw new UnauthorizedException('用户名或密码错误!'); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Exception/ApiAmisException.php: -------------------------------------------------------------------------------- 1 | message = $message; 25 | $this->code = $code; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Model/AdminApi.php: -------------------------------------------------------------------------------- 1 | 'json', 30 | ]; 31 | 32 | public function templateTitle(): Attribute 33 | { 34 | return Attribute::get(function () { 35 | if (! (new ReflectionClass($this->template))->isSubclassOf(AdminBaseApi::class)) { 36 | return ''; 37 | } 38 | 39 | return app($this->template)->getTitle(); 40 | }); 41 | } 42 | 43 | public function method(): Attribute 44 | { 45 | return Attribute::get(function () { 46 | if (! (new ReflectionClass($this->template))->isSubclassOf(AdminBaseApi::class)) { 47 | return 'any'; 48 | } 49 | 50 | $method = app($this->template)->getMethod(); 51 | 52 | return in_array($method, self::METHODS) ? $method : 'any'; 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Model/AdminCodeGenerator.php: -------------------------------------------------------------------------------- 1 | 'array', 23 | 'needs' => 'array', 24 | 'menu_info' => 'array', 25 | 'page_info' => 'array', 26 | 'save_path' => 'array', 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /src/Model/AdminMenu.php: -------------------------------------------------------------------------------- 1 | __('admin.admin_menu.route'), 35 | self::TYPE_LINK => __('admin.admin_menu.link'), 36 | self::TYPE_IFRAME => __('admin.admin_menu.iframe'), 37 | self::TYPE_PAGE => __('admin.admin_menu.page'), 38 | ]; 39 | } 40 | 41 | /** 42 | * 父级菜单. 43 | */ 44 | public function parent(): BelongsTo 45 | { 46 | return $this->belongsTo(self::class, 'parent_id'); 47 | } 48 | 49 | public function getTitleAttribute($value) 50 | { 51 | $transKey = ($this->extension ? $this->extension . '::' : '') . "menu.{$value}"; 52 | $translate = __($transKey); 53 | 54 | return $translate == $transKey ? $value : $translate; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Model/AdminPage.php: -------------------------------------------------------------------------------- 1 | 'json', 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /src/Model/AdminPermission.php: -------------------------------------------------------------------------------- 1 | 'array', 36 | 'http_path' => 'array', 37 | ]; 38 | 39 | protected array $fillable = ['sign']; 40 | 41 | public function menus(): BelongsToMany 42 | { 43 | return $this->belongsToMany(AdminMenu::class, 'admin_permission_menu', 'permission_id', 'menu_id') 44 | ->withTimestamps(); 45 | } 46 | 47 | public function shouldPassThrough(Request $request): bool 48 | { 49 | if (empty($this->http_method) && empty($this->http_path)) { 50 | return true; 51 | } 52 | $method = $this->http_method; 53 | $matches = array_map(function ($path) use ($method) { 54 | $path = trim(Admin::config('admin.route.prefix'), '/') . $path; 55 | if (Str::contains($path, ':')) { 56 | [$method, $path] = explode(':', $path); 57 | $method = explode(',', $method); 58 | } 59 | return compact('method', 'path'); 60 | }, $this->http_path); 61 | foreach ($matches as $match) { 62 | if ($this->matchRequest($match, $request)) { 63 | return true; 64 | } 65 | } 66 | return false; 67 | } 68 | 69 | protected function matchRequest(array $match, Request $request): bool 70 | { 71 | if ($match['path'] == '/') { 72 | $path = '/'; 73 | } else { 74 | $path = trim($match['path'], '/'); 75 | } 76 | if (! $request->is($path)) { 77 | return false; 78 | } 79 | $method = collect($match['method'])->filter()->map(function ($method) { 80 | return strtoupper($method); 81 | }); 82 | return $method->isEmpty() || $method->contains($request->method()); 83 | } 84 | 85 | public function deleting(Deleting $event) 86 | { 87 | $event->getMethod()->menus()->detach(); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Model/AdminRelationship.php: -------------------------------------------------------------------------------- 1 | 'hasOne', 53 | self::TYPE_HAS_MANY => 'hasMany', 54 | self::TYPE_BELONGS_TO => 'belongsTo', 55 | self::TYPE_BELONGS_TO_MANY => 'belongsToMany', 56 | self::TYPE_HAS_ONE_THROUGH => 'hasOneThrough', 57 | self::TYPE_HAS_MANY_THROUGH => 'hasManyThrough', 58 | self::TYPE_MORPH_ONE => 'morphOne', 59 | self::TYPE_MORPH_MANY => 'morphMany', 60 | self::TYPE_MORPH_TO_MANY => 'morphToMany', 61 | ]; 62 | 63 | public const TYPE_LABEL_MAP = [ 64 | self::TYPE_HAS_ONE => '一对一', 65 | self::TYPE_HAS_MANY => '一对多', 66 | self::TYPE_BELONGS_TO => '一对多(反向)/属于', 67 | self::TYPE_BELONGS_TO_MANY => '多对多', 68 | self::TYPE_HAS_ONE_THROUGH => '远程一对一', 69 | self::TYPE_HAS_MANY_THROUGH => '远程一对多', 70 | self::TYPE_MORPH_ONE => '一对一(多态)', 71 | self::TYPE_MORPH_MANY => '一对多(多态)', 72 | self::TYPE_MORPH_TO_MANY => '多对多(多态)', 73 | ]; 74 | 75 | protected $casts = [ 76 | 'args' => 'json', 77 | 'extra' => 'json', 78 | ]; 79 | 80 | public static function typeOptions() 81 | { 82 | return collect(self::TYPE_MAP)->map(function ($item, $index) { 83 | return [ 84 | 'label' => config('app.locale') == 'zh_CN' ? self::TYPE_LABEL_MAP[$index] : self::TYPE_MAP[$index], 85 | 'method' => $item, 86 | 'value' => $index, 87 | ]; 88 | })->values(); 89 | } 90 | 91 | public function method(): Attribute 92 | { 93 | return Attribute::get(fn () => self::TYPE_MAP[$this->type]); 94 | } 95 | 96 | public function buildArgs() 97 | { 98 | $reflection = new ReflectionClass($this->model); 99 | $params = $reflection->getMethod($this->method)->getParameters(); 100 | 101 | $args = []; 102 | 103 | foreach ($params as $item) { 104 | $_value = data_get($this->args, $item->getName()); 105 | $args[] = [ 106 | 'name' => $item->getName(), 107 | 'value' => filled($_value) ? $_value : $item->getDefaultValue(), 108 | ]; 109 | } 110 | 111 | return $args; 112 | } 113 | 114 | public function getPreviewCode() 115 | { 116 | $className = Str::of($this->model)->explode('\\')->pop(); 117 | $args = collect($this->buildArgs()) 118 | ->pluck('value') 119 | ->map(fn ($item) => is_null($item) ? 'null' : (is_string($item) ? "'{$item}'" : $item)) 120 | ->implode(', '); 121 | 122 | return <<title}() { 128 | return \$this->{$this->method}({$args}); 129 | } 130 | } 131 | PHP; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Model/AdminRole.php: -------------------------------------------------------------------------------- 1 | belongsToMany(AdminPermission::class, 'admin_role_permissions', 'role_id', 'permission_id') 26 | ->withTimestamps(); 27 | } 28 | 29 | public function deleting(Deleting $event) 30 | { 31 | $event->getModel()->permissions()->detach(); 32 | } 33 | 34 | public function users() 35 | { 36 | return $this->belongsToMany(AdminUser::class, 'admin_role_users', 'role_id', 'user_id')->withTimestamps(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Model/AdminSetting.php: -------------------------------------------------------------------------------- 1 | 'json', 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /src/Model/AdminUser.php: -------------------------------------------------------------------------------- 1 | setConnection(config('admin.database.connection')); 31 | 32 | parent::__construct($attributes); 33 | } 34 | 35 | public function roles() 36 | { 37 | return $this->belongsToMany(AdminRole::class, 'admin_role_users', 'user_id', 'role_id')->withTimestamps(); 38 | } 39 | 40 | // public function getAvatarAttribute(): Attribute 41 | // { 42 | // $storage = \Illuminate\Support\Facades\Storage::disk(Admin::config('admin.upload.disk')); 43 | // 44 | // return Attribute::make( 45 | // get: fn($value) => $value ? admin_resource_full_path($value) : url(Admin::config('admin.default_avatar')), 46 | // set: fn($value) => str_replace($storage->url(''), '', $value) 47 | // ); 48 | // } 49 | 50 | public function deleting(Deleting $event) 51 | { 52 | $event->getModel()->roles()->detach(); 53 | } 54 | 55 | public function allPermissions(): Collection 56 | { 57 | return $this->roles()->with('permissions')->get()->pluck('permissions')->flatten(); 58 | } 59 | 60 | public function can($abilities, $arguments = []): bool 61 | { 62 | if (empty($abilities)) { 63 | return true; 64 | } 65 | 66 | if ($this->isAdministrator()) { 67 | return true; 68 | } 69 | 70 | return $this->roles->pluck('permissions')->flatten()->pluck('slug')->contains($abilities); 71 | } 72 | 73 | public function isAdministrator(): bool 74 | { 75 | return $this->isRole('administrator'); 76 | } 77 | 78 | public function isRole(string $role): bool 79 | { 80 | return $this->roles->pluck('slug')->contains($role); 81 | } 82 | 83 | public function inRoles(array $roles = []): bool 84 | { 85 | return $this->roles->pluck('slug')->intersect($roles)->isNotEmpty(); 86 | } 87 | 88 | public function visible(array $roles = []): bool 89 | { 90 | if ($this->isAdministrator()) { 91 | return true; 92 | } 93 | if (empty($roles)) { 94 | return false; 95 | } 96 | $roles = array_column($roles, 'slug'); 97 | 98 | return $this->inRoles($roles); 99 | } 100 | 101 | public function getAdministratorAttribute() 102 | { 103 | return $this->isAdministrator(); 104 | } 105 | 106 | public function getId() 107 | { 108 | return $this->id; 109 | } 110 | 111 | public static function retrieveById($key): ?Authenticatable 112 | { 113 | return self::find($key); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Model/BaseModel.php: -------------------------------------------------------------------------------- 1 | setConnection(config('admin.database.connection')); 24 | parent::__construct($attributes); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Model/Extension.php: -------------------------------------------------------------------------------- 1 | 'json', 21 | ]; 22 | 23 | protected ?string $table = 'admin_extensions'; 24 | } 25 | -------------------------------------------------------------------------------- /src/Model/PersonalAccessToken.php: -------------------------------------------------------------------------------- 1 | setConnection(Admin::config('admin.database.connection')); 23 | 24 | parent::__construct($attributes); 25 | } 26 | 27 | public static function findToken($token) 28 | { 29 | $expiration = config('admin.auth.token_expiration'); 30 | 31 | if (! str_contains($token, '|')) { 32 | return static::where('token', hash('sha256', $token)) 33 | ->when($expiration, fn ($q) => $q->where('created_at', '>=', now()->subMinutes($expiration))) 34 | ->first(); 35 | } 36 | 37 | [$id, $token] = explode('|', $token, 2); 38 | 39 | $instance = static::when($expiration, fn ($q) => $q->where('created_at', '>=', now()->subMinutes($expiration))) 40 | ->find($id); 41 | 42 | if ($instance) { 43 | return hash_equals($instance->token, hash('sha256', $token)) ? $instance : null; 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Service/AdminApiService.php: -------------------------------------------------------------------------------- 1 | query() 30 | ->where('path', $data['path']) 31 | ->when($primaryKey, fn ($q) => $q->where('id', '<>', $primaryKey)) 32 | ->exists(); 33 | 34 | $routes = AdminCodeGenerator::query()->get()->map(function ($item) { 35 | return $item->menu_info['enabled'] ? ltrim($item->menu_info['route'], '/') : ''; 36 | })->filter()->toArray(); 37 | 38 | admin_abort_if($exists || in_array(ltrim($data['path'], '/'), $routes), __('admin.apis.path_exists')); 39 | } 40 | 41 | public function saved($model, $isEdit = false) 42 | { 43 | RouteGenerator::refresh(); 44 | } 45 | 46 | public function deleted($ids) 47 | { 48 | RouteGenerator::refresh(); 49 | } 50 | 51 | public function getApiByPath($path) 52 | { 53 | $api = $this->query()->where('path', $path)->first(); 54 | 55 | if (! $api && str_starts_with($path, '/')) { 56 | $api = $this->query()->where('path', ltrim($path, '/'))->first(); 57 | } 58 | 59 | return $api; 60 | } 61 | 62 | public function getApiByTemplate($template) 63 | { 64 | return $this->query()->where('template', $template)->first(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Service/AdminBaseService.php: -------------------------------------------------------------------------------- 1 | modelName ?? \Hyperf\DbConnection\Model\Model::class); 52 | } 53 | 54 | public function setModelName($modelName) 55 | { 56 | $this->modelName = $modelName; 57 | } 58 | 59 | public function getTableColumns() 60 | { 61 | if (!$this->tableColumn) { 62 | $this->tableColumn = (new \Hyperf\Database\Schema\Schema)->connection($this->getModel()->getConnectionName())->getSchemaBuilder()->getColumnListing($this->getModel()->getTable()); 63 | // $this->tableColumn = Schema::getColumnListing($this->getModel()->getTable()); 64 | } 65 | return $this->tableColumn; 66 | } 67 | 68 | /** 69 | * @return Model 70 | */ 71 | public function query() 72 | { 73 | return $this->modelName::query(); 74 | } 75 | 76 | public function primaryKey() 77 | { 78 | return $this->getModel()->getKeyName(); 79 | } 80 | 81 | public function update($primaryKey, array $data) 82 | { 83 | $this->saving($data, $primaryKey); 84 | $columns = $this->getTableColumns(); 85 | $model = $this->query()->where($this->primaryKey(), $primaryKey)->first(); 86 | if (empty($model)) { 87 | throw new ApiAmisException('数据不存在!'); 88 | } 89 | foreach ($data as $k => $v) { 90 | if (!in_array($k, $columns)) { 91 | continue; 92 | } 93 | $model->setAttribute($k, $v); 94 | } 95 | $result = $model->save(); 96 | if ($result) { 97 | $this->saved($model, true); 98 | } 99 | return $result; 100 | } 101 | 102 | /** 103 | * 详情 获取数据. 104 | * @param mixed $id 105 | */ 106 | public function getDetail($id): null|Builder|Collection|Model 107 | { 108 | $query = $this->query(); 109 | 110 | $this->addRelations($query, 'detail'); 111 | 112 | return $query->find($id); 113 | } 114 | 115 | /** 116 | * 编辑 获取数据. 117 | * @param mixed $id 118 | */ 119 | public function getEditData($id): null|array|Builder|Collection|Model 120 | { 121 | $model = $this->getModel(); 122 | 123 | $hidden = collect([$model->getCreatedAtColumn(), $model->getUpdatedAtColumn()]) 124 | ->filter(fn($item) => $item !== null) 125 | ->toArray(); 126 | 127 | $query = $this->query(); 128 | 129 | $this->addRelations($query, 'edit'); 130 | 131 | return $query->find($id)->makeHidden($hidden); 132 | } 133 | 134 | /** 135 | * 列表 获取查询. 136 | * 137 | * @return Builder 138 | */ 139 | public function listQuery() 140 | { 141 | $query = $this->query(); 142 | 143 | // 处理排序 144 | $this->sortable($query); 145 | 146 | // 自动加载 TableColumn 内的关联关系 147 | $this->loadRelations($query); 148 | 149 | // 处理查询 150 | $this->searchable($query); 151 | 152 | // 追加关联关系 153 | $this->addRelations($query); 154 | 155 | return $query; 156 | } 157 | 158 | /** 159 | * 添加关联关系. 160 | * 161 | * 预留钩子, 方便处理只需要添加 [关联] 的情况 162 | * 163 | * @param string $scene 场景: list, detail, edit 164 | * @param mixed $query 165 | */ 166 | public function addRelations($query, string $scene = 'list') 167 | { 168 | } 169 | 170 | /** 171 | * 根据 tableColumn 定义的列, 自动加载关联关系. 172 | * @param mixed $query 173 | */ 174 | public function loadRelations($query) 175 | { 176 | // 根据当前控制器取出他的page定义的 column字段,如果字段包含 .证明是从关联关系里取,查询模型是否有这个关联关系,有了自动加with 177 | // $pageDefinition = Route::getCurrentRoute()->getController()?->list(); 178 | // if (!$pageDefinition instanceof Page) { 179 | // return; 180 | // } 181 | // $columns = $pageDefinition->toArray()['body']->amisSchema['columns'] ?? []; 182 | // $withRelations = []; 183 | // foreach ($columns as $column) { 184 | // // 直接跳过不是 TableColumn 实例跳出 185 | // if (!$column instanceof TableColumn) continue; 186 | // $field = $column->amisSchema['name']; 187 | // // 如果字段名包含点,尝试解析关联关系 188 | // $splitFieldName = explode('.', $field); 189 | // if (count($splitFieldName) > 1) { 190 | // $relationName = $splitFieldName[0]; 191 | // // 检查模型上是否存在该方法,如果存在则加入到关联数组中 192 | // if (method_exists($this->modelName, $relationName)) { 193 | // // 避免重复添加相同的关联关系 194 | // $withRelations[$relationName] = $relationName; 195 | // } 196 | // } 197 | // } 198 | // $query->with(array_values($withRelations)); 199 | } 200 | 201 | /** 202 | * 排序. 203 | * @param mixed $query 204 | */ 205 | public function sortable($query) 206 | { 207 | $orderBy = $this->request->query('orderBy'); 208 | $orderDir = $this->request->query('orderDir', null); 209 | if ($orderBy && $orderDir) { 210 | $query->orderBy($orderBy, $orderDir ?? 'asc'); 211 | } else { 212 | $query->orderByDesc($this->sortColumn()); 213 | } 214 | } 215 | 216 | /** 217 | * 搜索. 218 | * @param mixed $query 219 | */ 220 | public function searchable($query) 221 | { 222 | $params = $this->request->query(); 223 | collect(array_keys($params)) 224 | ->intersect($this->getTableColumns()) 225 | ->map(function ($field) use ($query, $params) { 226 | $value = $params[$field] ?? null; 227 | $query->when($value, function ($query) use ($field, $value) { 228 | $query->where($field, 'like', '%' . $value . '%'); 229 | }); 230 | }); 231 | } 232 | 233 | /** 234 | * 列表 排序字段. 235 | * 236 | * @return string 237 | */ 238 | public function sortColumn() 239 | { 240 | if (in_array('sort', $this->getTableColumns())) { 241 | return 'sort'; 242 | } 243 | 244 | if (in_array($this->getModel()->getKeyName(), $this->getTableColumns())) { 245 | return $this->getModel()->getKeyName(); 246 | } 247 | return Arr::first($this->getTableColumns()); 248 | } 249 | 250 | /** 251 | * 列表 获取数据. 252 | * 253 | * @return array 254 | */ 255 | public function list() 256 | { 257 | $query = $this->listQuery(); 258 | 259 | $list = $query->paginate((int)$this->request->input('perPage', 20)); 260 | $items = $list->items(); 261 | $total = $list->total(); 262 | 263 | return compact('items', 'total'); 264 | } 265 | 266 | /** 267 | * 新增. 268 | * 269 | * @param mixed $data 270 | * @return bool 271 | */ 272 | public function store($data) 273 | { 274 | $this->saving($data); 275 | 276 | $columns = $this->getTableColumns(); 277 | $model = $this->getModel(); 278 | 279 | foreach ($data as $k => $v) { 280 | if (!in_array($k, $columns)) { 281 | continue; 282 | } 283 | 284 | $model->setAttribute($k, $v); 285 | } 286 | 287 | $result = $model->save(); 288 | 289 | if ($result) { 290 | $this->saved($model); 291 | } 292 | 293 | return $result; 294 | } 295 | 296 | /** 297 | * 删除. 298 | * 299 | * @return mixed 300 | */ 301 | public function delete(string $ids) 302 | { 303 | $result = $this->query()->whereIn($this->primaryKey(), explode(',', $ids))->delete(); 304 | 305 | if ($result) { 306 | $this->deleted($ids); 307 | } 308 | 309 | return $result; 310 | } 311 | 312 | /** 313 | * 快速编辑. 314 | * 315 | * @param mixed $data 316 | * @return true 317 | */ 318 | public function quickEdit($data) 319 | { 320 | $rowsDiff = data_get($data, 'rowsDiff', []); 321 | 322 | foreach ($rowsDiff as $item) { 323 | $this->update(Arr::pull($item, $this->primaryKey()), $item); 324 | } 325 | 326 | return true; 327 | } 328 | 329 | /** 330 | * 快速编辑单条 331 | * 332 | * @param mixed $data 333 | * @return bool 334 | */ 335 | public function quickEditItem($data) 336 | { 337 | return $this->update(Arr::pull($data, $this->primaryKey()), $data); 338 | } 339 | 340 | /** 341 | * saving 钩子 (执行于新增/修改前). 342 | * 343 | * 可以通过判断 $primaryKey 是否存在来判断是新增还是修改 344 | * @param mixed $data 345 | * @param mixed $primaryKey 346 | */ 347 | public function saving(&$data, $primaryKey = '') 348 | { 349 | } 350 | 351 | /** 352 | * saved 钩子 (执行于新增/修改后). 353 | * 354 | * 可以通过 $isEdit 来判断是新增还是修改 355 | * @param mixed $model 356 | * @param mixed $isEdit 357 | */ 358 | public function saved($model, $isEdit = false) 359 | { 360 | } 361 | 362 | /** 363 | * deleted 钩子 (执行于删除后). 364 | * @param mixed $ids 365 | */ 366 | public function deleted($ids) 367 | { 368 | } 369 | 370 | /** 371 | * 重新排序菜单. 372 | * 373 | * @param mixed $ids 374 | * @return false|int 375 | */ 376 | public function reorder($ids) 377 | { 378 | if (check_blank($ids)) { 379 | return false; 380 | } 381 | 382 | $ids = json_decode('[' . str_replace('[', ',[', $ids) . ']'); 383 | 384 | $list = collect($this->refreshOrder($ids))->transform(fn($i) => $i * 10)->all(); 385 | 386 | $sql = 'update ' . $this->getModel()->getTable() . ' set `order` = case id '; 387 | 388 | foreach ($list as $k => $v) { 389 | $sql .= " when {$k} then {$v} "; 390 | } 391 | 392 | return Db::update($sql . ' else `order` end'); 393 | } 394 | 395 | public function refreshOrder($list) 396 | { 397 | $result = collect($list)->filter(fn($i) => !is_array($i))->values()->flip()->toArray(); 398 | 399 | collect($list)->filter(fn($i) => is_array($i))->each(function ($item) use (&$result) { 400 | $result = $this->refreshOrder($item) + $result; 401 | }); 402 | 403 | return $result; 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/Service/AdminCodeGeneratorService.php: -------------------------------------------------------------------------------- 1 | when($keyword, function ($query) use ($keyword) { 32 | $query->where(function ($q) use ($keyword) { 33 | $q->where('table_name', 'like', "%{$keyword}%")->orWhere('title', 'like', "%{$keyword}%"); 34 | }); 35 | }); 36 | } 37 | 38 | public function store($data) 39 | { 40 | amis_abort_if( 41 | $this->query()->where('table_name', $data['table_name'])->exists(), 42 | __('admin.code_generators.exists_table') 43 | ); 44 | 45 | return parent::store($this->filterData($data)); 46 | } 47 | 48 | public function update($primaryKey, $data) 49 | { 50 | $exists = $this->query() 51 | ->where('table_name', $data['table_name']) 52 | ->where($this->primaryKey(), '<>', $primaryKey) 53 | ->exists(); 54 | 55 | amis_abort_if($exists, __('admin.code_generators.exists_table')); 56 | 57 | return parent::update($primaryKey, $this->filterData($data)); 58 | } 59 | 60 | public function filterData($data) 61 | { 62 | admin_abort_if(! data_get($data, 'columns'), __('admin.required', ['attribute' => __('admin.code_generators.column_info')])); 63 | 64 | $data['columns'] = collect($data['columns']) 65 | ->map(fn ($item) => Arr::except($item, ['component_options'])) 66 | ->toArray(); 67 | 68 | return Arr::except($data, ['table_info', 'table_primary_keys']); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Service/AdminMenuService.php: -------------------------------------------------------------------------------- 1 | modelName = config('admin.models.admin_menu', AdminMenu::class); 31 | } 32 | 33 | public function getTree() 34 | { 35 | $list = $this->query()->orderBy('order')->get()->toArray(); 36 | 37 | return array2tree($list); 38 | } 39 | 40 | public function parentIsChild($id, $parent_id) 41 | { 42 | $parent = $this->query()->find($parent_id); 43 | 44 | do { 45 | if ($parent->parent_id == $id) { 46 | return true; 47 | } 48 | // 如果没有parent 则为顶级菜单 退出循环 49 | $parent = $parent->parent; 50 | } while ($parent); 51 | 52 | return false; 53 | } 54 | 55 | public function update($primaryKey, $data) 56 | { 57 | $columns = $this->getTableColumns(); 58 | 59 | $parent_id = Arr::get($data, 'parent_id'); 60 | if ($parent_id != 0) { 61 | amis_abort_if($this->parentIsChild($primaryKey, $parent_id), __('admin.admin_menu.parent_id_not_allow')); 62 | } 63 | 64 | $model = $this->query()->whereKey($primaryKey)->first(); 65 | 66 | $data['id'] = $primaryKey; 67 | 68 | return $this->saveData($data, $columns, $model); 69 | } 70 | 71 | public function store($data) 72 | { 73 | $columns = $this->getTableColumns(); 74 | 75 | $model = $this->getModel(); 76 | 77 | return $this->saveData($data, $columns, $model); 78 | } 79 | 80 | public function changeHomePage($excludeId = 0) 81 | { 82 | $this->query()->when($excludeId, fn ($query) => $query->where('id', '<>', $excludeId))->update(['is_home' => 0]); 83 | } 84 | 85 | public function list() 86 | { 87 | return ['items' => $this->getTree()]; 88 | } 89 | 90 | /** 91 | * 重新排序菜单. 92 | * 93 | * @param mixed $ids 94 | * @return false|int 95 | */ 96 | public function reorder($ids) 97 | { 98 | if (check_blank($ids)) { 99 | return false; 100 | } 101 | 102 | $ids = json_decode('[' . str_replace('[', ',[', $ids) . ']'); 103 | 104 | $list = collect($this->refreshOrder($ids))->transform(fn ($i) => $i * 10)->all(); 105 | 106 | $sql = 'update ' . $this->getModel()->getTable() . ' set `order` = case id '; 107 | 108 | foreach ($list as $k => $v) { 109 | $sql .= " when {$k} then {$v} "; 110 | } 111 | 112 | return Db::update($sql . ' else `order` end'); 113 | } 114 | 115 | public function refreshOrder($list) 116 | { 117 | $result = collect($list)->filter(fn ($i) => ! is_array($i))->values()->flip()->toArray(); 118 | 119 | collect($list)->filter(fn ($i) => is_array($i))->each(function ($item) use (&$result) { 120 | $result = $this->refreshOrder($item) + $result; 121 | }); 122 | 123 | return $result; 124 | } 125 | 126 | /** 127 | * @param mixed $data 128 | * @return bool 129 | */ 130 | protected function saveData($data, array $columns, AdminMenu $model) 131 | { 132 | $urlExists = $this->query() 133 | ->where('url', data_get($data, 'url')) 134 | ->when(data_get($data, 'id'), fn ($q) => $q->where('id', '<>', data_get($data, 'id'))) 135 | ->exists(); 136 | 137 | admin_abort_if($urlExists, __('admin.admin_menu.url_exists')); 138 | 139 | foreach ($data as $k => $v) { 140 | if (! in_array($k, $columns)) { 141 | continue; 142 | } 143 | 144 | $v = $k == 'parent_id' ? intval($v) : $v; 145 | 146 | $model->setAttribute($k, $v); 147 | 148 | if ($k == 'is_home' && $v == 1) { 149 | $this->changeHomePage($model->getKey()); 150 | } 151 | } 152 | 153 | return $model->save(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Service/AdminPageService.php: -------------------------------------------------------------------------------- 1 | query() 36 | ->where('sign', $data['sign']) 37 | ->when($primaryKey, fn ($q) => $q->where('id', '<>', $primaryKey)) 38 | ->exists(); 39 | 40 | admin_abort_if($exists, __('admin.pages.sign_exists')); 41 | } 42 | 43 | public function saved($model, $isEdit = false) 44 | { 45 | if ($isEdit) { 46 | cache()->delete($this->cacheKeyPrefix . $model->sign); 47 | } 48 | } 49 | 50 | public function delete(string $ids) 51 | { 52 | $this->query()->whereIn('id', explode(',', $ids))->get()->map(function ($item) { 53 | cache()->delete($this->cacheKeyPrefix . $item->sign); 54 | }); 55 | 56 | return parent::delete($ids); 57 | } 58 | 59 | public function getEditData($id): \Hyperf\Database\Model\Collection|Builder|\Hyperf\Database\Model\Model|array|null 60 | { 61 | $data = parent::getEditData($id); 62 | 63 | $data->setAttribute('page', ['schema' => $data->schema]); 64 | $data->setAttribute('schema', ''); 65 | 66 | return $data; 67 | } 68 | 69 | /** 70 | * 获取页面结构. 71 | * 72 | * @param mixed $sign 73 | * @return mixed 74 | */ 75 | public function get($sign) 76 | { 77 | return cache_has_set($this->cacheKeyPrefix . $sign, function () use ($sign) { 78 | return $this->query()->where('sign', $sign)->value('schema'); 79 | }); 80 | } 81 | 82 | public function options() 83 | { 84 | return $this->query()->get(['sign as value', 'title as label']); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Service/AdminPermissionService.php: -------------------------------------------------------------------------------- 1 | modelName = Admin::adminPermissionModel(); 31 | } 32 | 33 | public function getTree() 34 | { 35 | $list = $this->query()->orderBy('order')->get()->toArray(); 36 | 37 | return array2tree($list); 38 | } 39 | 40 | public function parentIsChild($id, $parent_id) 41 | { 42 | $parent = $this->query()->find($parent_id); 43 | 44 | do { 45 | if ($parent->parent_id == $id) { 46 | return true; 47 | } 48 | // 如果没有parent 则为顶级 退出循环 49 | $parent = $parent->parent; 50 | } while ($parent); 51 | 52 | return false; 53 | } 54 | 55 | public function getEditData($id): \Hyperf\Database\Model\Collection|Builder|\Hyperf\Database\Model\Model|array|null 56 | { 57 | $permission = parent::getEditData($id); 58 | 59 | $permission->load(['menus']); 60 | 61 | return $permission; 62 | } 63 | 64 | public function store($data) 65 | { 66 | $this->checkRepeated($data); 67 | 68 | $columns = $this->getTableColumns(); 69 | 70 | $model = $this->getModel(); 71 | 72 | return $this->saveData($data, $columns, $model); 73 | } 74 | 75 | public function update($primaryKey, $data) 76 | { 77 | $this->checkRepeated($data, $primaryKey); 78 | 79 | $columns = $this->getTableColumns(); 80 | 81 | $parent_id = Arr::get($data, 'parent_id'); 82 | if ($parent_id != 0) { 83 | amis_abort_if($this->parentIsChild($primaryKey, $parent_id), __('admin.admin_permission.parent_id_not_allow')); 84 | } 85 | 86 | $model = $this->query()->whereKey($primaryKey)->first(); 87 | 88 | return $this->saveData($data, $columns, $model); 89 | } 90 | 91 | public function checkRepeated($data, $id = 0) 92 | { 93 | $query = $this->query()->when($id, fn ($query) => $query->where('id', '<>', $id)); 94 | 95 | amis_abort_if($query->clone()->where('name', $data['name']) 96 | ->exists(), __('admin.admin_permission.name_already_exists')); 97 | 98 | amis_abort_if($query->clone()->where('slug', $data['slug']) 99 | ->exists(), __('admin.admin_permission.slug_already_exists')); 100 | } 101 | 102 | public function list() 103 | { 104 | return ['items' => $this->getTree()]; 105 | } 106 | 107 | /** 108 | * @param mixed $data 109 | * @return bool 110 | */ 111 | protected function saveData($data, array $columns, AdminPermission $model) 112 | { 113 | $menus = Arr::pull($data, 'menus'); 114 | 115 | foreach ($data as $k => $v) { 116 | if (! in_array($k, $columns)) { 117 | continue; 118 | } 119 | 120 | $model->setAttribute($k, $v); 121 | } 122 | 123 | if ($model->save()) { 124 | $model->menus()->sync(Arr::has($menus, '0.id') ? Arr::pluck($menus, 'id') : $menus); 125 | 126 | return true; 127 | } 128 | 129 | return false; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Service/AdminRelationshipService.php: -------------------------------------------------------------------------------- 1 | transform(function ($item) { 39 | $item->setAttribute('preview_code', $item->getPreviewCode()); 40 | }); 41 | 42 | return $list; 43 | } 44 | 45 | public function getAll() 46 | { 47 | return cache()->rememberForever($this->cacheKey, function () { 48 | return self::query()->get(); 49 | }); 50 | } 51 | 52 | public function saving(&$data, $primaryKey = '') 53 | { 54 | $exists = self::query() 55 | ->where('model', $data['model']) 56 | ->where('title', $data['title']) 57 | ->when($primaryKey, fn ($q) => $q->where('id', '<>', $primaryKey)) 58 | ->exists(); 59 | 60 | admin_abort_if($exists, __('admin.relationships.rel_name_exists')); 61 | 62 | $methodExists = method_exists($data['model'], $data['title']); 63 | 64 | admin_abort_if($methodExists, __('admin.relationships.rel_name_exists')); 65 | } 66 | 67 | public function saved($model, $isEdit = false) 68 | { 69 | cache()->forget($this->cacheKey); 70 | } 71 | 72 | public function deleted($ids) 73 | { 74 | cache()->forget($this->cacheKey); 75 | } 76 | 77 | public function allModels() 78 | { 79 | $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(app_path('Models'))); 80 | $phpFiles = new RegexIterator($iterator, '/^.+\.php$/i', RegexIterator::GET_MATCH); 81 | 82 | foreach ($phpFiles as $phpFile) { 83 | $filePath = $phpFile[0]; 84 | require_once $filePath; 85 | } 86 | 87 | $modelDirClass = collect(get_declared_classes()) 88 | ->filter(fn ($i) => Str::startsWith($i, 'App\\Models')) 89 | ->toArray(); 90 | 91 | $composer = require base_path('/vendor/autoload.php'); 92 | $classMap = $composer->getClassMap(); 93 | $tables = Database::getTables(); 94 | 95 | $models = collect($classMap) 96 | ->keys() 97 | ->filter(fn ($item) => str_contains($item, 'Models\\')) 98 | ->filter(fn ($item) => @class_exists($item)) 99 | ->filter(fn ($item) => (new ReflectionClass($item))->isSubclassOf(Model::class)) 100 | ->merge($modelDirClass) 101 | ->unique() 102 | ->filter(fn ($item) => in_array(app($item)->getTable(), $tables)) 103 | ->values() 104 | ->map(fn ($item) => [ 105 | 'label' => Str::of($item)->explode('\\')->pop(), 106 | 'table' => app($item)->getTable(), 107 | 'value' => $item, 108 | ]); 109 | 110 | return compact('tables', 'models'); 111 | } 112 | 113 | public function generateModel($table) 114 | { 115 | $className = Str::of($table)->studly()->singular()->value(); 116 | 117 | $template = <<put($path, $template); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Service/AdminRoleService.php: -------------------------------------------------------------------------------- 1 | modelName = Admin::adminRoleModel(); 34 | } 35 | 36 | public function getEditData($id): null|array|Builder|Collection|Model 37 | { 38 | $permission = parent::getEditData($id); 39 | 40 | $permission->load(['permissions']); 41 | 42 | return $permission; 43 | } 44 | 45 | public function store($data) 46 | { 47 | $this->checkRepeated($data); 48 | 49 | $columns = $this->getTableColumns(); 50 | 51 | $model = $this->getModel(); 52 | 53 | foreach ($data as $k => $v) { 54 | if (! in_array($k, $columns)) { 55 | continue; 56 | } 57 | 58 | $model->setAttribute($k, $v); 59 | } 60 | 61 | return $model->save(); 62 | } 63 | 64 | public function update($primaryKey, $data) 65 | { 66 | $this->checkRepeated($data, $primaryKey); 67 | 68 | $columns = $this->getTableColumns(); 69 | 70 | $model = $this->query()->whereKey($primaryKey)->first(); 71 | 72 | foreach ($data as $k => $v) { 73 | if (! in_array($k, $columns)) { 74 | continue; 75 | } 76 | 77 | $model->setAttribute($k, $v); 78 | } 79 | 80 | return $model->save(); 81 | } 82 | 83 | /** 84 | * @param mixed $data 85 | * @param mixed $id 86 | * @throws ApiAmisException 87 | */ 88 | public function checkRepeated($data, $id = 0) 89 | { 90 | $query = $this->query()->when($id, fn ($query) => $query->where('id', '<>', $id)); 91 | 92 | amis_abort_if($query->clone() 93 | ->where('name', $data['name']) 94 | ->exists(), __('admin.admin_role.name_already_exists')); 95 | 96 | amis_abort_if($query->clone() 97 | ->where('slug', $data['slug']) 98 | ->exists(), __('admin.admin_role.slug_already_exists')); 99 | } 100 | 101 | public function savePermissions($primaryKey, $permissions) 102 | { 103 | $model = $this->query()->whereKey($primaryKey)->first(); 104 | 105 | return $model->permissions()->sync( 106 | Arr::has($permissions, '0.id') ? Arr::pluck($permissions, 'id') : $permissions 107 | ); 108 | } 109 | 110 | public function delete(string $ids) 111 | { 112 | $_ids = explode(',', $ids); 113 | $exists = $this->query() 114 | ->whereIn($this->primaryKey(), $_ids) 115 | ->where('slug', 'administrator') 116 | ->exists(); 117 | 118 | admin_abort_if($exists, __('admin.admin_role.cannot_delete')); 119 | 120 | $used = $this->query() 121 | ->whereIn($this->primaryKey(), $_ids) 122 | ->has('users') 123 | ->exists(); 124 | 125 | admin_abort_if($used, __('admin.admin_role.used')); 126 | 127 | return parent::delete($ids); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Service/AdminSettingService.php: -------------------------------------------------------------------------------- 1 | query()->firstOrNew(['key' => $key]); 40 | $setting->values = $value; 41 | $this->clearCache($key); 42 | $setting->save(); 43 | return true; 44 | } 45 | 46 | /** 47 | * 批量保存设置. 48 | * 49 | * @return bool 50 | */ 51 | public function setMany(array $data) 52 | { 53 | Db::beginTransaction(); 54 | try { 55 | foreach ($data as $key => $value) { 56 | $this->set($key, $value); 57 | } 58 | Db::commit(); 59 | } catch (Exception $e) { 60 | Db::rollBack(); 61 | throw new $e(); 62 | } 63 | 64 | return true; 65 | } 66 | 67 | /** 68 | * 批量保存设置项并返回后台响应格式数据. 69 | * 70 | * @throws Exception 71 | */ 72 | public function adminSetMany(array $data) 73 | { 74 | $prefix = __('admin.save'); 75 | $this->setMany($data); 76 | return Admin::response()->successMessage($prefix . __('admin.successfully')); 77 | } 78 | 79 | /** 80 | * 以数组形式返回所有设置. 81 | * 82 | * @return array 83 | */ 84 | public function all() 85 | { 86 | return $this->query()->pluck('values', 'key')->toArray(); 87 | } 88 | 89 | /** 90 | * 获取设置项. 91 | * 92 | * @param string $key 设置项key 93 | * @param null|mixed $default 默认值 94 | * @param bool $fresh 是否直接从数据库获取 95 | * 96 | * @return null|mixed 97 | */ 98 | public function get(string $key, mixed $default = null, bool $fresh = false) 99 | { 100 | if ($fresh) { 101 | return $this->query()->where('key', $key)->value('values') ?? $default; 102 | } 103 | 104 | $value = cache_has_set($this->getCacheKey($key), function () use ($key) { 105 | return $this->query()->where('key', $key)->value('values'); 106 | }); 107 | 108 | return $value ?? $default; 109 | } 110 | 111 | /** 112 | * 获取设置项中的某个值 113 | * 114 | * @param string $key 设置项key 115 | * @param string $path 通过点号分隔的路径, 同Arr::get() 116 | * @param null|mixed $default 117 | * 118 | * @return null|array|ArrayAccess|mixed 119 | */ 120 | public function arrayGet(string $key, string $path, $default = null) 121 | { 122 | $value = $this->get($key); 123 | 124 | if (is_array($value)) { 125 | return Arr::get($value, $path, $default); 126 | } 127 | 128 | return $default; 129 | } 130 | 131 | /** 132 | * 清除指定设置项. 133 | * 134 | * @return bool 135 | */ 136 | public function del(string $key) 137 | { 138 | if ($this->query()->where('key', $key)->delete()) { 139 | $this->clearCache($key); 140 | 141 | return true; 142 | } 143 | 144 | return false; 145 | } 146 | 147 | /** 148 | * 清除指定设置项的缓存. 149 | * @param mixed $key 150 | */ 151 | public function clearCache($key) 152 | { 153 | cache()->delete($this->getCacheKey($key)); 154 | } 155 | 156 | public function getCacheKey($key) 157 | { 158 | return $this->cacheKeyPrefix . $key; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Service/AdminUserService.php: -------------------------------------------------------------------------------- 1 | modelName = Admin::adminUserModel(); 37 | } 38 | 39 | public function getEditData($id): null|array|Builder|Collection|Model 40 | { 41 | $adminUser = parent::getEditData($id)->makeHidden('password'); 42 | 43 | $adminUser->load('roles'); 44 | 45 | return $adminUser; 46 | } 47 | 48 | /** 49 | * @param mixed $data 50 | * @throws ApiAmisException 51 | */ 52 | public function store($data) 53 | { 54 | $this->checkUsernameUnique($data['username']); 55 | 56 | admin_abort_if(! data_get($data, 'password'), __('admin.required', ['attribute' => __('admin.password')])); 57 | 58 | $this->passwordHandler($data); 59 | 60 | $columns = $this->getTableColumns(); 61 | 62 | $model = $this->getModel(); 63 | 64 | return $this->saveData($data, $columns, $model); 65 | } 66 | 67 | public function update($primaryKey, $data) 68 | { 69 | $this->checkUsernameUnique($data['username'], $primaryKey); 70 | $this->passwordHandler($data); 71 | 72 | $columns = $this->getTableColumns(); 73 | 74 | $model = $this->query()->whereKey($primaryKey)->first(); 75 | 76 | return $this->saveData($data, $columns, $model); 77 | } 78 | 79 | public function checkUsernameUnique($username, $id = 0) 80 | { 81 | $exists = $this->query() 82 | ->where('username', $username) 83 | ->when($id, fn ($query) => $query->where('id', '<>', $id)) 84 | ->exists(); 85 | 86 | admin_abort_if($exists, __('admin.admin_user.username_already_exists')); 87 | } 88 | 89 | public function updateUserSetting($primaryKey, $data): bool 90 | { 91 | $this->passwordHandler($data, $primaryKey); 92 | 93 | return parent::update($primaryKey, $data); 94 | } 95 | 96 | public function passwordHandler(&$data, $id = null) 97 | { 98 | $password = Arr::get($data, 'password'); 99 | 100 | if ($password) { 101 | admin_abort_if($password !== Arr::get($data, 'confirm_password'), __('admin.admin_user.password_confirmation')); 102 | /** @var Hash $hash */ 103 | $hash = make(Hash::class); 104 | if ($id) { 105 | admin_abort_if(! Arr::get($data, 'old_password'), __('admin.admin_user.old_password_required')); 106 | 107 | $oldPassword = $this->query()->where('id', $id)->value('password'); 108 | 109 | admin_abort_if(! $hash->check($data['old_password'], $oldPassword), __('admin.admin_user.old_password_error')); 110 | } 111 | $data['password'] = $hash->make($password); 112 | unset($data['confirm_password'], $data['old_password']); 113 | } 114 | } 115 | 116 | public function list() 117 | { 118 | $keyword = $params['keyword'] ?? null; 119 | 120 | $query = $this->query() 121 | ->with('roles') 122 | ->select(['id', 'name', 'username', 'avatar', 'enabled', 'created_at']) 123 | ->when($keyword, function ($query) use ($keyword) { 124 | $query->where('username', 'like', "%{$keyword}%")->orWhere('name', 'like', "%{$keyword}%"); 125 | }); 126 | 127 | $this->sortable($query); 128 | 129 | $list = $query->paginate((int)$this->request->query('perPage', 20)); 130 | $items = $list->items(); 131 | $total = $list->total(); 132 | 133 | return compact('items', 'total'); 134 | } 135 | 136 | public function delete(string $ids) 137 | { 138 | $exists = $this->query() 139 | ->whereIn($this->primaryKey(), explode(',', $ids)) 140 | ->whereHas('roles', fn ($q) => $q->where('slug', 'administrator')) 141 | ->exists(); 142 | 143 | admin_abort_if($exists, __('admin.admin_user.cannot_delete')); 144 | 145 | return parent::delete($ids); 146 | } 147 | 148 | /** 149 | * @param mixed $data 150 | * @return bool 151 | */ 152 | protected function saveData($data, array $columns, AdminUser $model) 153 | { 154 | $roles = Arr::pull($data, 'roles'); 155 | 156 | foreach ($data as $k => $v) { 157 | if (! in_array($k, $columns)) { 158 | continue; 159 | } 160 | 161 | $model->setAttribute($k, $v); 162 | } 163 | 164 | if ($model->save()) { 165 | $model->roles()->sync(Arr::has($roles, '0.id') ? Arr::pluck($roles, 'id') : $roles); 166 | 167 | return true; 168 | } 169 | 170 | return false; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Support/Core/Asset.php: -------------------------------------------------------------------------------- 1 | assetsHandler('js', $js); 32 | } 33 | 34 | public function css($css = null) 35 | { 36 | return $this->assetsHandler('css', $css); 37 | } 38 | 39 | public function scripts($scripts = null) 40 | { 41 | return $this->assetsHandler('scripts', $scripts); 42 | } 43 | 44 | public function styles($styles = null) 45 | { 46 | return $this->assetsHandler('styles', $styles); 47 | } 48 | 49 | public function appendNav($appendNav = null) 50 | { 51 | if (is_null($appendNav)) { 52 | return $this->appendNav; 53 | } 54 | 55 | $this->appendNav = $appendNav; 56 | 57 | return $this; 58 | } 59 | 60 | public function prependNav($prependNav = null) 61 | { 62 | if (is_null($prependNav)) { 63 | return $this->prependNav; 64 | } 65 | 66 | $this->prependNav = $prependNav; 67 | 68 | return $this; 69 | } 70 | 71 | private function assetsHandler($type, $assets) 72 | { 73 | if (is_null($assets)) { 74 | return $this->{$type}; 75 | } 76 | 77 | if (is_array($assets)) { 78 | $this->{$type} = array_merge($this->{$type}, $assets); 79 | } else { 80 | $this->{$type}[] = $assets; 81 | } 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Support/Core/Database.php: -------------------------------------------------------------------------------- 1 | moduleName = $moduleName; 30 | } 31 | 32 | public static function make($moduleName = null) 33 | { 34 | return new self($moduleName); 35 | } 36 | 37 | public function tableName($name) 38 | { 39 | return $this->moduleName . $name; 40 | } 41 | 42 | public function create($tableName, $callback) 43 | { 44 | Schema::create($this->tableName($tableName), $callback); 45 | } 46 | 47 | public function dropIfExists($tableName) 48 | { 49 | Schema::dropIfExists($this->tableName($tableName)); 50 | } 51 | 52 | public function initSchema() 53 | { 54 | $this->down(); 55 | $this->up(); 56 | } 57 | 58 | public function up() 59 | { 60 | $this->create('admin_users', function (Blueprint $table) { 61 | $table->id(); 62 | $table->string('username', 120)->unique(); 63 | $table->string('password', 80); 64 | $table->tinyInteger('enabled')->default(1); 65 | $table->string('name')->default(''); 66 | $table->string('avatar')->nullable(); 67 | $table->string('remember_token', 100)->nullable(); 68 | $table->timestamps(); 69 | }); 70 | 71 | $this->create('admin_roles', function (Blueprint $table) { 72 | $table->id(); 73 | $table->string('name', 50)->unique(); 74 | $table->string('slug', 50)->unique(); 75 | $table->timestamps(); 76 | }); 77 | 78 | $this->create('admin_permissions', function (Blueprint $table) { 79 | $table->id(); 80 | $table->string('name', 50)->unique(); 81 | $table->string('slug', 50)->unique(); 82 | $table->text('http_method')->nullable(); 83 | $table->text('http_path')->nullable(); 84 | $table->integer('order')->default(0); 85 | $table->integer('parent_id')->default(0); 86 | $table->timestamps(); 87 | }); 88 | 89 | $this->create('admin_menus', function (Blueprint $table) { 90 | $table->id(); 91 | $table->integer('parent_id')->default(0); 92 | $table->integer('order')->default(0); 93 | $table->string('title', 100)->comment('菜单名称'); 94 | $table->string('icon', 100)->nullable()->comment('菜单图标'); 95 | $table->string('url')->nullable()->comment('菜单路由'); 96 | $table->tinyInteger('url_type')->default(1)->comment('路由类型(1:路由,2:外链,3:iframe)'); 97 | $table->tinyInteger('visible')->default(1)->comment('是否可见'); 98 | $table->tinyInteger('is_home')->default(0)->comment('是否为首页'); 99 | $table->tinyInteger('keep_alive')->nullable()->comment('页面缓存'); 100 | $table->string('iframe_url')->nullable()->comment('iframe_url'); 101 | $table->string('component')->nullable()->comment('菜单组件'); 102 | $table->tinyInteger('is_full')->default(0)->comment('是否是完整页面'); 103 | $table->string('extension')->nullable()->comment('扩展'); 104 | 105 | $table->timestamps(); 106 | }); 107 | 108 | $this->create('admin_role_users', function (Blueprint $table) { 109 | $table->integer('role_id'); 110 | $table->integer('user_id'); 111 | $table->index(['role_id', 'user_id']); 112 | $table->timestamps(); 113 | }); 114 | 115 | $this->create('admin_role_permissions', function (Blueprint $table) { 116 | $table->integer('role_id'); 117 | $table->integer('permission_id'); 118 | $table->index(['role_id', 'permission_id']); 119 | $table->timestamps(); 120 | }); 121 | 122 | $this->create('admin_permission_menu', function (Blueprint $table) { 123 | $table->integer('permission_id'); 124 | $table->integer('menu_id'); 125 | $table->index(['permission_id', 'menu_id']); 126 | $table->timestamps(); 127 | }); 128 | 129 | // 如果是模块,跳过下面的表 130 | if ($this->moduleName) { 131 | return; 132 | } 133 | 134 | $this->create('admin_code_generators', function (Blueprint $table) { 135 | $table->id(); 136 | $table->string('title')->default('')->comment('名称'); 137 | $table->string('table_name')->default('')->comment('表名'); 138 | $table->string('primary_key')->default('id')->comment('主键名'); 139 | $table->string('model_name')->default('')->comment('模型名'); 140 | $table->string('controller_name')->default('')->comment('控制器名'); 141 | $table->string('service_name')->default('')->comment('服务名'); 142 | $table->longText('columns')->comment('字段信息'); 143 | $table->tinyInteger('need_timestamps')->default(0)->comment('是否需要时间戳'); 144 | $table->tinyInteger('soft_delete')->default(0)->comment('是否需要软删除'); 145 | $table->text('needs')->nullable()->comment('需要生成的代码'); 146 | $table->text('menu_info')->nullable()->comment('菜单信息'); 147 | $table->text('page_info')->nullable()->comment('页面信息'); 148 | $table->text('save_path')->nullable()->comment('保存位置'); 149 | $table->timestamps(); 150 | }); 151 | 152 | $this->create('admin_settings', function (Blueprint $table) { 153 | $table->string('key')->default(''); 154 | $table->longText('values')->nullable(); 155 | $table->timestamps(); 156 | }); 157 | 158 | $this->create('admin_extensions', function (Blueprint $table) { 159 | $table->id(); 160 | $table->string('name', 100)->unique(); 161 | $table->tinyInteger('is_enabled')->default(0); 162 | $table->timestamps(); 163 | }); 164 | 165 | $this->create('admin_pages', function (Blueprint $table) { 166 | $table->id(); 167 | $table->string('title')->comment('页面名称'); 168 | $table->string('sign')->comment('页面标识'); 169 | $table->longText('schema')->comment('页面结构'); 170 | $table->timestamps(); 171 | }); 172 | 173 | $this->create('admin_relationships', function (Blueprint $table) { 174 | $table->id(); 175 | $table->string('model')->comment('模型'); 176 | $table->string('title')->comment('关联名称'); 177 | $table->string('type')->comment('关联类型'); 178 | $table->string('remark')->comment('关联名称')->nullable(); 179 | $table->text('args')->comment('关联参数')->nullable(); 180 | $table->text('extra')->comment('额外参数')->nullable(); 181 | $table->timestamps(); 182 | }); 183 | 184 | $this->create('admin_apis', function (Blueprint $table) { 185 | $table->id(); 186 | $table->string('title')->comment('接口名称'); 187 | $table->string('path')->comment('接口路径'); 188 | $table->string('template')->comment('接口模板'); 189 | $table->tinyInteger('enabled')->default(1)->comment('是否启用'); 190 | $table->longText('args')->comment('接口参数')->nullable(); 191 | $table->timestamps(); 192 | }); 193 | } 194 | 195 | public function down() 196 | { 197 | $this->dropIfExists('admin_users'); 198 | $this->dropIfExists('admin_roles'); 199 | $this->dropIfExists('admin_permissions'); 200 | $this->dropIfExists('admin_menus'); 201 | $this->dropIfExists('admin_role_users'); 202 | $this->dropIfExists('admin_role_permissions'); 203 | $this->dropIfExists('admin_permission_menu'); 204 | 205 | // 如果是模块,跳过下面的表 206 | if ($this->moduleName) { 207 | return; 208 | } 209 | 210 | $this->dropIfExists('admin_code_generators'); 211 | $this->dropIfExists('admin_settings'); 212 | $this->dropIfExists('admin_extensions'); 213 | $this->dropIfExists('admin_pages'); 214 | $this->dropIfExists('admin_relationships'); 215 | $this->dropIfExists('admin_apis'); 216 | } 217 | 218 | /** 219 | * 填充初始数据. 220 | */ 221 | public function fillInitialData() 222 | { 223 | $data = function ($data) { 224 | foreach ($data as $k => $v) { 225 | if (is_array($v)) { 226 | $data[$k] = "['" . implode("','", $v) . "']"; 227 | } 228 | } 229 | $now = date('Y-m-d H:i:s'); 230 | 231 | return array_merge($data, ['created_at' => $now, 'updated_at' => $now]); 232 | }; 233 | 234 | $adminUser = Db::table($this->tableName('admin_users')); 235 | $adminMenu = Db::table($this->tableName('admin_menus')); 236 | $adminPermission = Db::table($this->tableName('admin_permissions')); 237 | $adminRole = Db::table($this->tableName('admin_roles')); 238 | 239 | // 创建初始用户 240 | $adminUser->truncate(); 241 | $adminUser->insert($data([ 242 | 'username' => 'admin', 243 | 'password' => $this->bcrypt('admin'), 244 | 'name' => 'Administrator', 245 | ])); 246 | 247 | // 创建初始角色 248 | $adminRole->truncate(); 249 | $adminRole->insert($data([ 250 | 'name' => 'Administrator', 251 | 'slug' => 'administrator', 252 | ])); 253 | 254 | // 用户 - 角色绑定 255 | Db::table($this->tableName('admin_role_users'))->truncate(); 256 | Db::table($this->tableName('admin_role_users'))->insert($data([ 257 | 'role_id' => 1, 258 | 'user_id' => 1, 259 | ])); 260 | 261 | // 创建初始权限 262 | $adminPermission->truncate(); 263 | $adminPermission->insert([ 264 | $data(['name' => '首页', 'slug' => 'home', 'http_path' => ['/home*'], 'parent_id' => 0]), 265 | $data(['name' => '系统', 'slug' => 'system', 'http_path' => '', 'parent_id' => 0]), 266 | $data(['name' => '管理员', 'slug' => 'admin_users', 'http_path' => ['/admin_users*'], 'parent_id' => 2]), 267 | $data(['name' => '角色', 'slug' => 'roles', 'http_path' => ['/roles*'], 'parent_id' => 2]), 268 | $data(['name' => '权限', 'slug' => 'permissions', 'http_path' => ['/permissions*'], 'parent_id' => 2]), 269 | $data(['name' => '菜单', 'slug' => 'menus', 'http_path' => ['/menus*'], 'parent_id' => 2]), 270 | $data(['name' => '设置', 'slug' => 'settings', 'http_path' => ['/settings*'], 'parent_id' => 2]), 271 | ]); 272 | 273 | // 角色 - 权限绑定 274 | Db::table($this->tableName('admin_role_permissions'))->truncate(); 275 | $permissionIds = Db::table($this->tableName('admin_permissions'))->orderBy('id')->pluck('id'); 276 | foreach ($permissionIds as $id) { 277 | Db::table($this->tableName('admin_role_permissions'))->insert($data([ 278 | 'role_id' => 1, 279 | 'permission_id' => $id, 280 | ])); 281 | } 282 | 283 | // 创建初始菜单 284 | $adminMenu->truncate(); 285 | $adminMenu->insert([ 286 | $data([ 287 | 'parent_id' => 0, 288 | 'title' => 'dashboard', 289 | 'icon' => 'mdi:chart-line', 290 | 'url' => '/home/dashboard', 291 | 'is_home' => 1, 292 | ]), 293 | $data([ 294 | 'parent_id' => 0, 295 | 'title' => 'admin_system', 296 | 'icon' => 'material-symbols:settings-outline', 297 | 'url' => '/system', 298 | 'is_home' => 0, 299 | ]), 300 | $data([ 301 | 'parent_id' => 2, 302 | 'title' => 'admin_users', 303 | 'icon' => 'ph:user-gear', 304 | 'url' => '/system/admin_users', 305 | 'is_home' => 0, 306 | ]), 307 | $data([ 308 | 'parent_id' => 2, 309 | 'title' => 'admin_roles', 310 | 'icon' => 'carbon:user-role', 311 | 'url' => '/system/admin_roles', 312 | 'is_home' => 0, 313 | ]), 314 | $data([ 315 | 'parent_id' => 2, 316 | 'title' => 'admin_permission', 317 | 'icon' => 'fluent-mdl2:permissions', 318 | 'url' => '/system/admin_permissions', 319 | 'is_home' => 0, 320 | ]), 321 | $data([ 322 | 'parent_id' => 2, 323 | 'title' => 'admin_menu', 324 | 'icon' => 'ant-design:menu-unfold-outlined', 325 | 'url' => '/system/admin_menus', 326 | 'is_home' => 0, 327 | ]), 328 | $data([ 329 | 'parent_id' => 2, 330 | 'title' => 'admin_setting', 331 | 'icon' => 'akar-icons:settings-horizontal', 332 | 'url' => '/system/settings', 333 | 'is_home' => 0, 334 | ]), 335 | ]); 336 | 337 | // 权限 - 菜单绑定 338 | Db::table($this->tableName('admin_permission_menu'))->truncate(); 339 | $menus = $adminMenu->get(); 340 | foreach ($menus as $menu) { 341 | $_list = []; 342 | $_list[] = $data(['permission_id' => $menu->id, 'menu_id' => $menu->id]); 343 | 344 | if ($menu->parent_id != 0) { 345 | $_list[] = $data(['permission_id' => $menu->parent_id, 'menu_id' => $menu->id]); 346 | } 347 | 348 | Db::table($this->tableName('admin_permission_menu'))->insert($_list); 349 | } 350 | } 351 | 352 | public static function getTables() 353 | { 354 | try { 355 | return collect(json_decode(json_encode(Schema::getAllTables()), true)) 356 | ->map(fn ($i) => config('database.default') == 'sqlite' ? $i['name'] : array_shift($i)) 357 | ->toArray(); 358 | } catch (Throwable $e) { 359 | } 360 | 361 | // laravel 11+ 362 | return array_column(Schema::getAllTables(), 'name'); 363 | } 364 | 365 | private function bcrypt(string $string) 366 | { 367 | return $string; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/Support/Core/Hash.php: -------------------------------------------------------------------------------- 1 | rounds = $options['rounds'] ?? $this->rounds; 32 | $this->verifyAlgorithm = $options['verify'] ?? $this->verifyAlgorithm; 33 | } 34 | 35 | /** 36 | * Hash the given value. 37 | * 38 | * @param string $value 39 | * @param array $options 40 | * @return string 41 | * 42 | * @throws \RuntimeException 43 | */ 44 | public function make($value, array $options = []) 45 | { 46 | $hash = password_hash($value, PASSWORD_BCRYPT, [ 47 | 'cost' => $this->cost($options), 48 | ]); 49 | 50 | if ($hash === false) { 51 | throw new \RuntimeException('Bcrypt hashing not supported.'); 52 | } 53 | 54 | return $hash; 55 | } 56 | 57 | /** 58 | * Check the given plain value against a hash. 59 | * 60 | * @param string $value 61 | * @param string $hashedValue 62 | * @param array $options 63 | * @return bool 64 | * 65 | * @throws \RuntimeException 66 | */ 67 | public function check($value, $hashedValue, array $options = []) 68 | { 69 | if ($this->verifyAlgorithm && !$this->isUsingCorrectAlgorithm($hashedValue)) { 70 | throw new RuntimeException('This password does not use the Bcrypt algorithm.'); 71 | } 72 | 73 | if (is_null($hashedValue) || strlen($hashedValue) === 0) { 74 | return false; 75 | } 76 | 77 | return password_verify($value, $hashedValue); 78 | } 79 | 80 | /** 81 | * Check if the given hash has been hashed using the given options. 82 | * 83 | * @param string $hashedValue 84 | * @param array $options 85 | * @return bool 86 | */ 87 | public function needsRehash($hashedValue, array $options = []) 88 | { 89 | return password_needs_rehash($hashedValue, PASSWORD_BCRYPT, [ 90 | 'cost' => $this->cost($options), 91 | ]); 92 | } 93 | 94 | /** 95 | * Verifies that the configuration is less than or equal to what is configured. 96 | * 97 | * @internal 98 | */ 99 | public function verifyConfiguration($value) 100 | { 101 | return $this->isUsingCorrectAlgorithm($value) && $this->isUsingValidOptions($value); 102 | } 103 | 104 | /** 105 | * Verify the hashed value's algorithm. 106 | * 107 | * @param string $hashedValue 108 | * @return bool 109 | */ 110 | protected function isUsingCorrectAlgorithm($hashedValue) 111 | { 112 | return $this->info($hashedValue)['algoName'] === 'bcrypt'; 113 | } 114 | /** 115 | * Get information about the given hashed value. 116 | * 117 | * @param string $hashedValue 118 | * @return array 119 | */ 120 | public function info($hashedValue) 121 | { 122 | return password_get_info($hashedValue); 123 | } 124 | 125 | 126 | /** 127 | * Verify the hashed value's options. 128 | * 129 | * @param string $hashedValue 130 | * @return bool 131 | */ 132 | protected function isUsingValidOptions($hashedValue) 133 | { 134 | ['options' => $options] = $this->info($hashedValue); 135 | 136 | if (!is_int($options['cost'] ?? null)) { 137 | return false; 138 | } 139 | 140 | if ($options['cost'] > $this->rounds) { 141 | return false; 142 | } 143 | 144 | return true; 145 | } 146 | 147 | /** 148 | * Set the default password work factor. 149 | * 150 | * @param int $rounds 151 | * @return $this 152 | */ 153 | public function setRounds($rounds) 154 | { 155 | $this->rounds = (int)$rounds; 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * Extract the cost value from the options array. 162 | * 163 | * @param array $options 164 | * @return int 165 | */ 166 | protected function cost(array $options = []) 167 | { 168 | return $options['rounds'] ?? $this->rounds; 169 | } 170 | } -------------------------------------------------------------------------------- /src/Support/Core/Menu.php: -------------------------------------------------------------------------------- 1 | userMenus(); 31 | array_map(function ($item) use ($menus) { 32 | $menus->push($this->formatItem($item)); 33 | }, $this->menus); 34 | $menus->sortBy('order') 35 | ->values() 36 | ->toArray(); 37 | return array_merge($this->list2Menu($menus), $this->extra()); 38 | } 39 | 40 | public function userMenus() 41 | { 42 | if (!Admin::config('admin.auth.enable')) { 43 | return collect([]); 44 | } 45 | 46 | /** @var AdminUser $user */ 47 | $user = Admin::user(); 48 | 49 | if ($user->isAdministrator() || Admin::config('admin.auth.permission') === false) { 50 | $list = AdminMenuService::make()->query()->orderBy('order')->get(); 51 | } else { 52 | $user->load('roles.permissions.menus'); 53 | $list = $user->roles 54 | ->pluck('permissions') 55 | ->flatten() 56 | ->pluck('menus') 57 | ->flatten() 58 | ->unique('id') 59 | ->sortBy('order'); 60 | } 61 | 62 | return $list; 63 | } 64 | 65 | public function list2Menu($list, $parentId = 0, $parentName = ''): array 66 | { 67 | $data = []; 68 | foreach ($list as $key => $item) { 69 | if ($item['parent_id'] == $parentId) { 70 | $_component = match ($item['url_type']) { 71 | Admin::adminMenuModel()::TYPE_IFRAME => 'iframe', 72 | Admin::adminMenuModel()::TYPE_PAGE => 'amis', 73 | default => data_get($item, 'component') ?? 'amis' 74 | }; 75 | 76 | $idStr = "[{$item['id']}]"; 77 | $_temp = [ 78 | 'name' => $parentName ? $parentName . '-' . $idStr : $idStr, 79 | 'path' => $item['url'], 80 | 'component' => $_component, 81 | 'is_home' => $item['is_home'], 82 | 'iframe_url' => $item['iframe_url'] ?? '', 83 | 'url_type' => $item['url_type'] ?? Admin::adminMenuModel()::TYPE_ROUTE, 84 | 'keep_alive' => $item['keep_alive'] ?? 0, 85 | 'is_full' => $item['is_full'] ?? 0, 86 | 'is_link' => $item['url_type'] == Admin::adminMenuModel()::TYPE_LINK, 87 | 'page_sign' => $item['url_type'] == Admin::adminMenuModel()::TYPE_PAGE ? data_get($item, 'component') : '', 88 | 'meta' => [ 89 | 'title' => $item['title'], 90 | 'icon' => $item['icon'] ?? '-', 91 | 'hide' => $item['visible'] == 0, 92 | 'order' => $item['order'], 93 | ], 94 | ]; 95 | 96 | $children = $this->list2Menu($list, (int)$item['id'], $_temp['name']); 97 | 98 | if (!empty($children)) { 99 | $_temp['component'] = 'amis'; 100 | $_temp['children'] = $children; 101 | } 102 | 103 | $data[] = $_temp; 104 | if (!in_array($_temp['path'], Admin::config('admin.route.without_extra_routes')) && $item['url_type'] != Admin::adminMenuModel()::TYPE_PAGE) { 105 | array_push($data, ...$this->generateRoute($_temp)); 106 | } 107 | unset($list[$key]); 108 | } 109 | } 110 | return $data; 111 | } 112 | 113 | public function generateRoute($item): array 114 | { 115 | $url = $item['path'] ?? ''; 116 | $url = preg_replace('/\?.*/', '', $url); 117 | 118 | if (!$url || array_key_exists('children', $item)) { 119 | return []; 120 | } 121 | 122 | $menu = fn($action, $path) => [ 123 | 'name' => $item['name'] . '-' . $action, 124 | 'path' => $url . $path, 125 | 'component' => $item['url_type'] == 3 ? 'iframe' : (data_get($item, 'component') ?? 'amis'), 126 | 'meta' => [ 127 | 'hide' => true, 128 | 'icon' => Arr::get($item, 'meta.icon'), 129 | 'title' => Arr::get($item, 'meta.title') . ' - ' . __('admin.' . $action), 130 | ], 131 | ]; 132 | 133 | return [ 134 | $menu('create', '/create'), 135 | $menu('show', '/:id'), 136 | $menu('edit', '/:id/edit'), 137 | ]; 138 | } 139 | 140 | public function add($menus) 141 | { 142 | $this->menus = array_merge($this->menus, $menus); 143 | 144 | return $this; 145 | } 146 | 147 | public function formatItem($item) 148 | { 149 | return array_merge([ 150 | 'title' => '', 151 | 'url' => '', 152 | 'url_type' => 1, 153 | 'icon' => '', 154 | 'parent_id' => 0, 155 | 'id' => 999, 156 | 'is_home' => 0, 157 | 'visible' => 1, 158 | 'order' => 99, 159 | ], $item); 160 | } 161 | 162 | /** 163 | * 额外菜单. 164 | * 165 | * @return array|array[] 166 | */ 167 | public function extra() 168 | { 169 | $extraMenus = []; 170 | 171 | if (Admin::config('admin.auth.enable')) { 172 | $extraMenus[] = [ 173 | 'name' => 'user_setting', 174 | 'path' => '/user_setting', 175 | 'component' => 'amis', 176 | 'meta' => [ 177 | 'hide' => true, 178 | 'title' => __('admin.user_setting'), 179 | 'icon' => 'material-symbols:manage-accounts', 180 | 'singleLayout' => 'basic', 181 | ], 182 | ]; 183 | } 184 | 185 | if (Admin::config('admin.show_development_tools')) { 186 | $extraMenus = array_merge($extraMenus, $this->devToolMenus()); 187 | } 188 | 189 | return $extraMenus; 190 | } 191 | 192 | /** 193 | * 开发者工具菜单. 194 | * 195 | * @return array[] 196 | */ 197 | public function devToolMenus() 198 | { 199 | return [ 200 | [ 201 | 'name' => 'dev_tools', 202 | 'path' => '/dev_tools', 203 | 'component' => 'amis', 204 | 'meta' => [ 205 | 'title' => __('admin.developer'), 206 | 'icon' => 'fluent:window-dev-tools-20-regular', 207 | ], 208 | 'children' => [ 209 | [ 210 | 'name' => 'dev_tools_extensions', 211 | 'path' => '/dev_tools/extensions', 212 | 'component' => 'amis', 213 | 'meta' => [ 214 | 'title' => __('admin.extensions.menu'), 215 | 'icon' => 'ion:extension-puzzle-outline', 216 | ], 217 | ], 218 | [ 219 | 'name' => 'dev_tools_pages', 220 | 'path' => '/dev_tools/pages', 221 | 'component' => 'amis', 222 | 'meta' => [ 223 | 'title' => __('admin.pages.menu'), 224 | 'icon' => 'iconoir:multiple-pages', 225 | ], 226 | ], 227 | [ 228 | 'name' => 'dev_tools_relationships', 229 | 'path' => '/dev_tools/relationships', 230 | 'component' => 'amis', 231 | 'meta' => [ 232 | 'title' => __('admin.relationships.menu'), 233 | 'icon' => 'ant-design:node-index-outlined', 234 | ], 235 | ], 236 | [ 237 | 'name' => 'dev_tools_apis', 238 | 'path' => '/dev_tools/apis', 239 | 'component' => 'amis', 240 | 'meta' => [ 241 | 'title' => __('admin.apis.menu'), 242 | 'icon' => 'ant-design:api-outlined', 243 | ], 244 | ], 245 | [ 246 | 'name' => 'dev_tools_code_generator', 247 | 'path' => '/dev_tools/code_generator', 248 | 'component' => 'amis', 249 | 'meta' => [ 250 | 'title' => __('admin.code_generator'), 251 | 'icon' => 'ic:baseline-code', 252 | ], 253 | ], 254 | [ 255 | 'name' => 'dev_tools_editor', 256 | 'path' => '/dev_tools/editor', 257 | 'component' => 'editor', 258 | 'meta' => [ 259 | 'title' => __('admin.visual_editor'), 260 | 'icon' => 'mdi:monitor-edit', 261 | ], 262 | ], 263 | ], 264 | ], 265 | ]; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Support/helpers.php: -------------------------------------------------------------------------------- 1 | $item) { 28 | if ($item['parent_id'] == $parentId) { 29 | $children = array2tree($list, (int) $item['id']); 30 | ! empty($children) && $item['children'] = $children; 31 | $data[] = $item; 32 | unset($list[$key]); 33 | } 34 | } 35 | return $data; 36 | } 37 | } 38 | 39 | if (! function_exists('admin_abort_if')) { 40 | /** 41 | * 生成树状数据. 42 | * 43 | * @param mixed $error 44 | * 45 | * @return array 46 | */ 47 | function admin_abort_if($error, string $message = '') 48 | { 49 | if ($error) { 50 | throw new ApiAmisException($message); 51 | } 52 | } 53 | } 54 | 55 | if (! function_exists('amis_abort_if')) { 56 | /** 57 | * 生成树状数据. 58 | * 59 | * @param mixed $error 60 | * 61 | * @return array 62 | */ 63 | function amis_abort_if($error, string $message = '') 64 | { 65 | if ($error) { 66 | throw new ApiAmisException($message); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Traits/AssetsTrait.php: -------------------------------------------------------------------------------- 1 | get(Asset::class); 26 | } 27 | 28 | /** 29 | * 加载 js 文件. 30 | * 31 | * @param null|mixed $js 32 | * @return Asset 33 | */ 34 | public static function js($js = null) 35 | { 36 | return static::asset()->js($js); 37 | } 38 | 39 | /** 40 | * 加载 css 文件. 41 | * 42 | * @param null|mixed $css 43 | * @return Asset 44 | */ 45 | public static function css($css = null) 46 | { 47 | return static::asset()->css($css); 48 | } 49 | 50 | /** 51 | * 加载 js 脚本. 52 | * 53 | * @param null|mixed $scripts 54 | * @return Asset 55 | */ 56 | public static function scripts($scripts = null) 57 | { 58 | return static::asset()->scripts($scripts); 59 | } 60 | 61 | /** 62 | * 加载样式表. 63 | * 64 | * @param null|mixed $styles 65 | * @return Asset 66 | */ 67 | public static function styles($styles = null) 68 | { 69 | return static::asset()->styles($styles); 70 | } 71 | 72 | public static function getAssets() 73 | { 74 | return [ 75 | 'js' => static::asset()->js(), 76 | 'css' => static::asset()->css(), 77 | 'scripts' => static::asset()->scripts(), 78 | 'styles' => static::asset()->styles(), 79 | ]; 80 | } 81 | 82 | /** 83 | * 在后面添加 Nav. 84 | * 85 | * @param null|mixed $appendNav 86 | * @return Asset 87 | */ 88 | public static function appendNav($appendNav = null) 89 | { 90 | return static::asset()->appendNav($appendNav); 91 | } 92 | 93 | /** 94 | * 在前面添加 Nav. 95 | * 96 | * @param null|mixed $prependNav 97 | * @return Asset 98 | */ 99 | public static function prependNav($prependNav = null) 100 | { 101 | return static::asset()->prependNav($prependNav); 102 | } 103 | 104 | public static function getNav() 105 | { 106 | return [ 107 | 'appendNav' => static::asset()->appendNav(), 108 | 'prependNav' => static::asset()->prependNav(), 109 | ]; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Traits/HasApiBase.php: -------------------------------------------------------------------------------- 1 | request->query('_action') == 'getData'; 25 | } 26 | 27 | /** 28 | * 是否为导出数据请求 29 | * 30 | * @return bool 31 | */ 32 | public function actionOfExport() 33 | { 34 | return $this->request->query('_action') == 'export'; 35 | } 36 | 37 | /** 38 | * 是否为快速编辑数据请求 39 | * 40 | * @return bool 41 | */ 42 | public function actionOfQuickEdit() 43 | { 44 | return $this->request->query('_action') == 'quickEdit'; 45 | } 46 | 47 | /** 48 | * 是否为快速编辑数据请求 49 | * 50 | * @return bool 51 | */ 52 | public function actionOfQuickEditItem() 53 | { 54 | return $this->request->query('_action') == 'quickEditItem'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Traits/HasElementTrait.php: -------------------------------------------------------------------------------- 1 | Page()->className('m:overflow-auto'); 37 | } 38 | 39 | /** 40 | * 返回列表按钮. 41 | */ 42 | protected function backButton(): OtherAction 43 | { 44 | $path = str_replace(config('admin.route.prefix'), '', $this->request->path()); 45 | $script = sprintf('window.$owl.hasOwnProperty(\'closeTabByPath\') && window.$owl.closeTabByPath(\'%s\')', $path); 46 | return amis()->OtherAction() 47 | ->label(__('admin.back')) 48 | ->icon('fa-solid fa-chevron-left') 49 | ->level('primary') 50 | ->onClick('window.history.back();' . $script); 51 | } 52 | 53 | /** 54 | * 批量删除按钮. 55 | */ 56 | protected function bulkDeleteButton(): AjaxAction 57 | { 58 | return amis()->AjaxAction() 59 | ->api($this->getBulkDeletePath()) 60 | ->icon('fa-solid fa-trash-can') 61 | ->label(__('admin.delete')) 62 | ->confirmText(__('admin.confirm_delete')); 63 | } 64 | 65 | /** 66 | * 新增按钮. 67 | */ 68 | protected function createButton(bool $dialog = false, string $dialogSize = ''): DialogAction|LinkAction 69 | { 70 | if ($dialog) { 71 | $form = $this->form(false)->api($this->getStorePath())->onEvent([]); 72 | $button = amis()->DialogAction()->dialog( 73 | amis()->Dialog()->title(__('admin.create'))->body($form)->size($dialogSize) 74 | ); 75 | } else { 76 | $button = amis()->LinkAction()->link($this->getCreatePath()); 77 | } 78 | 79 | return $button->label(__('admin.create'))->icon('fa fa-add')->level('primary'); 80 | } 81 | 82 | /** 83 | * 行编辑按钮. 84 | */ 85 | protected function rowEditButton(bool $dialog = false, string $dialogSize = ''): DialogAction|LinkAction 86 | { 87 | if ($dialog) { 88 | $form = $this->form(true) 89 | ->api($this->getUpdatePath()) 90 | ->initApi($this->getEditGetDataPath()) 91 | ->redirect('') 92 | ->onEvent([]); 93 | 94 | $button = amis()->DialogAction()->dialog( 95 | amis()->Dialog()->title(__('admin.edit'))->body($form)->size($dialogSize) 96 | ); 97 | } else { 98 | $button = amis()->LinkAction()->link($this->getEditPath()); 99 | } 100 | 101 | return $button->label(__('admin.edit'))->icon('fa-regular fa-pen-to-square')->level('link'); 102 | } 103 | 104 | /** 105 | * 行详情按钮. 106 | */ 107 | protected function rowShowButton(bool $dialog = false, string $dialogSize = ''): DialogAction|LinkAction 108 | { 109 | if ($dialog) { 110 | $button = amis()->DialogAction()->dialog( 111 | amis()->Dialog()->title(__('admin.show'))->body($this->detail('$id'))->size($dialogSize) 112 | ); 113 | } else { 114 | $button = amis()->LinkAction()->link($this->getShowPath()); 115 | } 116 | 117 | return $button->label(__('admin.show'))->icon('fa-regular fa-eye')->level('link'); 118 | } 119 | 120 | /** 121 | * 行删除按钮. 122 | */ 123 | protected function rowDeleteButton(): AjaxAction 124 | { 125 | return amis()->AjaxAction() 126 | ->label(__('admin.delete')) 127 | ->icon('fa-regular fa-trash-can') 128 | ->level('link') 129 | ->confirmText(__('admin.confirm_delete')) 130 | ->api($this->getDeletePath()); 131 | } 132 | 133 | /** 134 | * 操作列. 135 | * 136 | * @param bool $dialog 137 | */ 138 | protected function rowActions(array|bool $dialog = false, string $dialogSize = ''): Operation 139 | { 140 | if (is_array($dialog)) { 141 | return amis()->Operation()->label(__('admin.actions'))->buttons($dialog); 142 | } 143 | 144 | return amis()->Operation()->label(__('admin.actions'))->buttons([ 145 | $this->rowShowButton($dialog, $dialogSize), 146 | $this->rowEditButton($dialog, $dialogSize), 147 | $this->rowDeleteButton(), 148 | ]); 149 | } 150 | 151 | /** 152 | * 基础筛选器. 153 | */ 154 | protected function baseFilter(): Form 155 | { 156 | return amis()->Form() 157 | ->panelClassName('base-filter') 158 | ->title('') 159 | ->actions([ 160 | amis()->Button()->label(__('admin.reset'))->actionType('clear-and-submit'), 161 | amis('submit')->label(__('admin.search'))->level('primary'), 162 | ]); 163 | } 164 | 165 | /** 166 | * 基础筛选器 - 条件构造器. 167 | */ 168 | protected function baseFilterConditionBuilder(): ConditionBuilderControl 169 | { 170 | return amis()->ConditionBuilderControl('filter_condition_builder'); 171 | } 172 | 173 | protected function baseCRUD(): CRUDTable 174 | { 175 | $crud = amis()->CRUDTable() 176 | ->perPage(20) 177 | ->affixHeader(false) 178 | ->filterTogglable() 179 | ->filterDefaultVisible(false) 180 | ->api($this->getListGetDataPath()) 181 | ->quickSaveApi($this->getQuickEditPath()) 182 | ->quickSaveItemApi($this->getQuickEditItemPath()) 183 | ->bulkActions([$this->bulkDeleteButton()]) 184 | ->perPageAvailable([10, 20, 30, 50, 100, 200]) 185 | ->footerToolbar(['switch-per-page', 'statistics', 'pagination']) 186 | ->headerToolbar([ 187 | $this->createButton(), 188 | ...$this->baseHeaderToolBar(), 189 | ]); 190 | 191 | if (isset($this->service)) { 192 | $crud->set('primaryField', $this->service->primaryKey()); 193 | } 194 | 195 | return $crud; 196 | } 197 | 198 | protected function baseHeaderToolBar() 199 | { 200 | return [ 201 | 'bulkActions', 202 | amis('reload')->align('right'), 203 | amis('filter-toggler')->align('right'), 204 | ]; 205 | } 206 | 207 | /** 208 | * 基础表单. 209 | */ 210 | protected function baseForm(bool $back = true): Form 211 | { 212 | $path = str_replace(config('admin.route.prefix'), '', $this->request->path()); 213 | 214 | $form = amis()->Form()->panelClassName('px-48 m:px-0')->title(' ')->mode('horizontal'); 215 | 216 | if ($back) { 217 | $form->onEvent([ 218 | 'submitSucc' => [ 219 | 'actions' => [ 220 | ['actionType' => 'custom', 'script' => 'window.history.back()'], 221 | [ 222 | 'actionType' => 'custom', 223 | 'script' => sprintf('window.$owl.hasOwnProperty(\'closeTabByPath\') && window.$owl.closeTabByPath(\'%s\')', $path), 224 | ], 225 | ], 226 | ], 227 | ]); 228 | } 229 | 230 | return $form; 231 | } 232 | 233 | protected function baseDetail(): Form 234 | { 235 | return amis()->Form() 236 | ->panelClassName('px-48 m:px-0') 237 | ->title(' ') 238 | ->mode('horizontal') 239 | ->actions([]) 240 | ->initApi($this->getShowGetDataPath()); 241 | } 242 | 243 | /** 244 | * 基础列表. 245 | * @param mixed $crud 246 | */ 247 | protected function baseList($crud): Page 248 | { 249 | return $this->basePage()->body($crud); 250 | } 251 | 252 | /** 253 | * 导出按钮. 254 | * 255 | * @param bool $disableSelectedItem 256 | */ 257 | protected function exportAction($disableSelectedItem = false): Service 258 | { 259 | // 获取主键名称 260 | $primaryKey = $this->service->primaryKey(); 261 | // 下载路径 262 | $downloadPath = admin_url('_download_export', true); 263 | // 导出接口地址 264 | $exportPath = $this->getExportPath(); 265 | // 无数据提示 266 | $pageNoData = __('admin.export.page_no_data'); 267 | // 选中行无数据提示 268 | $selectedNoData = __('admin.export.selected_rows_no_data'); 269 | // 按钮点击事件 270 | $event = fn ($script) => ['click' => ['actions' => [['actionType' => 'custom', 'script' => $script]]]]; 271 | // 导出处理动作 272 | $doAction = "doAction([{actionType:'setValue',componentId:'export-action',args:{value:{showExportLoading:true}}},{actionType:'ajax',args:{api:{url:url.toString(),method:'get'}}},{actionType:'setValue',componentId:'export-action',args:{value:{showExportLoading:false}}},{actionType:'custom',expression:'\${event.data.responseResult.responseStatus===0}',script:'window.open(\\'{$downloadPath}?path=\\'+event.data.responseResult.responseData.path)'}])"; 273 | // 按钮 274 | $buttons = [ 275 | // 导出全部 276 | amis()->VanillaAction()->label(__('admin.export.all'))->onEvent( 277 | $event("let data=event.data;let params=Object.keys(data).filter(key=>key!=='page' && key!=='__super').reduce((obj,key)=>{obj[key]=data[key];return obj;},{});let url=new URL('{$exportPath}',window.location.origin);Object.keys(params).forEach(key=>url.searchParams.append(key,params[key]));{$doAction}") 278 | ), 279 | // 导出本页 280 | amis()->VanillaAction()->label(__('admin.export.page'))->onEvent( 281 | $event("let ids=event.data.items.map(item=>item.{$primaryKey});if(ids.length===0){return doAction({actionType:'toast',args:{msgType:'warning',msg:'{$pageNoData}'}})};let url=new URL('{$exportPath}',window.location.origin);url.searchParams.append('_ids',ids.join(','));{$doAction}") 282 | ), 283 | ]; 284 | // 导出选中项 285 | if (! $disableSelectedItem) { 286 | $buttons[] = amis()->VanillaAction()->label(__('admin.export.selected_rows'))->onEvent( 287 | $event("let ids=event.data.selectedItems.map(item=>item.{$primaryKey});if(ids.length===0){return doAction({actionType:'toast',args:{msgType:'warning',msg:'{$selectedNoData}'}})};let url=new URL('{$exportPath}',window.location.origin);url.searchParams.append('_ids',ids.join(','));{$doAction}") 288 | ); 289 | } 290 | 291 | return amis()->Service() 292 | ->id('export-action') 293 | ->set('align', 'right') 294 | ->set('data', ['showExportLoading' => false]) 295 | ->body( 296 | amis()->Spinner()->set('showOn', '${showExportLoading}')->overlay()->body( 297 | amis()->DropdownButton() 298 | ->label(__('admin.export.title')) 299 | ->set('icon', 'fa-solid fa-download') 300 | ->buttons($buttons) 301 | ->closeOnClick() 302 | ->align('right') 303 | ) 304 | ); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Traits/HasIconifyPickerTrait.php: -------------------------------------------------------------------------------- 1 | CRUDCards() 27 | ->perPage(40) 28 | ->loadDataOnce() 29 | ->set('columnsCount', 8) 30 | ->footerToolbar(['statistics', 'pagination']) 31 | ->api('/_iconify_search') 32 | ->filter( 33 | amis()->Form()->wrapWithPanel(false)->body([ 34 | amis()->GroupControl()->className('pt-3 pb-3')->body([ 35 | amis()->TextControl('query') 36 | ->size('md') 37 | ->value('${' . $name . ' || "home"}') 38 | ->clearable() 39 | ->required(), 40 | amis()->Button() 41 | ->label(__('admin.search')) 42 | ->level('primary') 43 | ->actionType('submit') 44 | ->icon('fa fa-search'), 45 | amis()->UrlAction() 46 | ->className('ml-2') 47 | ->icon('fa fa-external-link-alt') 48 | ->label('Icones') 49 | ->blank() 50 | ->url('https://icones.js.org/collection/all'), 51 | ]), 52 | ]) 53 | ) 54 | ->card( 55 | amis()->Card()->body([ 56 | amis()->SvgIcon()->icon('${icon}')->className('text-2xl'), 57 | ]) 58 | ); 59 | 60 | return amis()->PickerControl($name, $label) 61 | ->pickerSchema($schema) 62 | ->source('/_iconify_search') 63 | ->size('lg') 64 | ->labelField('icon') 65 | ->valueField('icon'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Traits/HasQueryPathTrait.php: -------------------------------------------------------------------------------- 1 | queryPath . '?_action=getData'); 29 | } 30 | 31 | /** 32 | * 导出. 33 | * 34 | * @return string 35 | */ 36 | public function getExportPath() 37 | { 38 | return admin_url($this->queryPath . '?_action=export', true); 39 | } 40 | 41 | /** 42 | * 删除. 43 | * 44 | * @return string 45 | */ 46 | public function getDeletePath() 47 | { 48 | $primaryKey = isset($this->service) ? $this->service->primaryKey() : 'id'; 49 | 50 | return 'delete:' . admin_url($this->queryPath . '/${' . $primaryKey . '}'); 51 | } 52 | 53 | /** 54 | * 批量删除. 55 | * 56 | * @return string 57 | */ 58 | public function getBulkDeletePath() 59 | { 60 | return 'delete:' . admin_url($this->queryPath . '/${ids}'); 61 | } 62 | 63 | /** 64 | * 编辑页面. 65 | * 66 | * @return string 67 | */ 68 | public function getEditPath() 69 | { 70 | return '/' . trim($this->queryPath, '/') . '/${' . $this->service->primaryKey() . '}'; 71 | } 72 | 73 | /** 74 | * 编辑 获取数据. 75 | * 76 | * @return string 77 | */ 78 | public function getEditGetDataPath() 79 | { 80 | $path = str_replace('/edit', '', $this->queryPath); 81 | 82 | $last = collect(explode('/', $path))->last(); 83 | 84 | if (! is_numeric($last)) { 85 | $primaryKey = isset($this->service) ? $this->service->primaryKey() : 'id'; 86 | 87 | $path .= '/${' . $primaryKey . '}'; 88 | } 89 | 90 | return admin_url($path . '?_action=getData'); 91 | } 92 | 93 | /** 94 | * 详情页面. 95 | * 96 | * @return string 97 | */ 98 | public function getShowPath() 99 | { 100 | return '/' . trim($this->queryPath, '/') . '/${' . $this->service->primaryKey() . '}'; 101 | } 102 | 103 | /** 104 | * 编辑保存. 105 | * 106 | * @return string 107 | */ 108 | public function getUpdatePath() 109 | { 110 | $path = str_replace('/edit', '', $this->queryPath); 111 | 112 | $last = collect(explode('/', $path))->last(); 113 | 114 | if (! is_numeric($last)) { 115 | $primaryKey = isset($this->service) ? $this->service->primaryKey() : 'id'; 116 | 117 | $path .= '/${' . $primaryKey . '}'; 118 | } 119 | 120 | return 'put:' . admin_url($path); 121 | } 122 | 123 | /** 124 | * 快速编辑. 125 | * 126 | * @return string 127 | */ 128 | public function getQuickEditPath() 129 | { 130 | return $this->getStorePath() . '?_action=quickEdit'; 131 | } 132 | 133 | public function getQuickEditItemPath() 134 | { 135 | return $this->getStorePath() . '?_action=quickEditItem'; 136 | } 137 | 138 | /** 139 | * 详情 获取数据. 140 | * 141 | * @return string 142 | */ 143 | public function getShowGetDataPath() 144 | { 145 | $path = $this->queryPath; 146 | 147 | $last = collect(explode('/', $this->queryPath))->last(); 148 | 149 | if (! is_numeric($last)) { 150 | $path .= '/${' . $this->service->primaryKey() . '}'; 151 | } 152 | 153 | return admin_url($path . '?_action=getData'); 154 | } 155 | 156 | /** 157 | * 新增页面. 158 | * 159 | * @return string 160 | */ 161 | public function getCreatePath() 162 | { 163 | return '/' . trim($this->queryPath, '/') . '/create'; 164 | } 165 | 166 | /** 167 | * 新增 保存. 168 | * 169 | * @return string 170 | */ 171 | public function getStorePath() 172 | { 173 | return 'post:' . admin_url(str_replace('/create', '', $this->queryPath)); 174 | } 175 | 176 | /** 177 | * 列表. 178 | * 179 | * @return string 180 | */ 181 | public function getListPath() 182 | { 183 | $path = $this->queryPath; 184 | 185 | if (Str::contains($this->queryPath, '/create')) { 186 | $path = str_replace('/create', '', $path); 187 | } 188 | 189 | if (Str::contains($this->queryPath, '/edit')) { 190 | $_path = explode('/', $path); 191 | array_pop($_path); 192 | array_pop($_path); 193 | $path = implode('/', $_path); 194 | } 195 | 196 | return '/' . trim($path, '/'); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Traits/HasUploadTrait.php: -------------------------------------------------------------------------------- 1 |